115 lines
7.6 KiB
HTML
115 lines
7.6 KiB
HTML
{% extends "ui/templates/base/layout.html" %}
|
|
{% block content %}
|
|
{% include "modules/managers/templates/managers/_manager_tabs.html" %}
|
|
<div class="space-y-6">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-slate-900">Employee Attendance</h2>
|
|
<p class="text-sm text-slate-500">Review employee attendance, filter records, and manually mark attendance where required.</p>
|
|
</div>
|
|
<a href="/employees" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Employees</a>
|
|
</div>
|
|
|
|
<form method="get" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
|
<div class="grid gap-3 md:grid-cols-6">
|
|
<select name="employee_id" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
<option value="">All employees</option>
|
|
{% for emp in employees %}
|
|
<option value="{{ emp.id }}" {% if selected_employee_id == emp.id %}selected{% endif %}>{{ emp.full_name }} - {{ emp.employee_code }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<input type="date" name="from_date" value="{{ from_date }}" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
<input type="date" name="to_date" value="{{ to_date }}" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
<select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
<option value="">Any status</option>
|
|
{% for st in attendance_statuses %}<option value="{{ st }}" {% if selected_status == st %}selected{% endif %}>{{ st.replace('_',' ').title() }}</option>{% endfor %}
|
|
</select>
|
|
<select name="approval_status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
<option value="">Any approval</option>
|
|
{% for st in attendance_approval_statuses %}<option value="{{ st }}" {% if selected_approval_status == st %}selected{% endif %}>{{ st.title() }}</option>{% endfor %}
|
|
</select>
|
|
<button class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Filter</button>
|
|
</div>
|
|
</form>
|
|
|
|
{% if can_approve_employee_attendance(current_user, current_user_permissions, current_user_roles) %}
|
|
<form method="post" action="/employees/attendance/manual" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<h3 class="font-semibold text-slate-900">Manual Attendance Marking</h3>
|
|
<div class="mt-4 grid gap-3 md:grid-cols-5">
|
|
<select name="employee_id" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
<option value="">Select employee</option>
|
|
{% for emp in employees %}<option value="{{ emp.id }}">{{ emp.full_name }} - {{ emp.employee_code }}</option>{% endfor %}
|
|
</select>
|
|
<input type="date" name="attendance_date" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
<select name="status" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
{% for st in attendance_statuses %}<option value="{{ st }}">{{ st.replace('_',' ').title() }}</option>{% endfor %}
|
|
</select>
|
|
<input name="remarks" placeholder="Remarks" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
|
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Save</button>
|
|
</div>
|
|
</form>
|
|
{% endif %}
|
|
|
|
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft">
|
|
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
|
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
<tr>
|
|
<th class="px-4 py-3">Date</th>
|
|
<th class="px-4 py-3">Employee</th>
|
|
<th class="px-4 py-3">Punch</th>
|
|
<th class="px-4 py-3">Status</th>
|
|
<th class="px-4 py-3">Approval</th>
|
|
<th class="px-4 py-3">Timing</th>
|
|
<th class="px-4 py-3">Geo/IP Evidence</th>
|
|
<th class="px-4 py-3">Remarks</th>
|
|
<th class="px-4 py-3 text-right">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for row in rows %}
|
|
<tr class="hover:bg-slate-50">
|
|
<td class="px-4 py-3 font-medium text-slate-900">{{ row.attendance_date }}</td>
|
|
<td class="px-4 py-3 text-slate-600"><div class="font-medium text-slate-900">{{ row.employee.full_name if row.employee else ('Employee #' ~ row.employee_id) }}</div><div class="text-xs">{{ row.employee.employee_code if row.employee else '' }}</div></td>
|
|
<td class="px-4 py-3 text-slate-600"><div>In: {{ row.punch_in_local_at.strftime('%H:%M') if row.punch_in_local_at else (row.punch_in_utc.strftime('%H:%M UTC') if row.punch_in_utc else '-') }}</div><div>Out: {{ row.punch_out_local_at.strftime('%H:%M') if row.punch_out_local_at else (row.punch_out_utc.strftime('%H:%M UTC') if row.punch_out_utc else '-') }}</div></td>
|
|
<td class="px-4 py-3">{{ row.status.replace('_',' ').title() }}</td>
|
|
<td class="px-4 py-3">{{ row.approval_status.title() }}</td>
|
|
<td class="px-4 py-3 text-xs text-slate-600">
|
|
<div>TZ: {{ row.branch_timezone or 'Asia/Kolkata' }}</div>
|
|
<div>Rule: {{ (row.attendance_rule_status or '-').replace('_',' ').title() }}</div>
|
|
<div>Late: {{ (row.late_by_minutes ~ ' min') if row.late_by_minutes else '-' }}</div>
|
|
<div>Weekly Off: {{ 'Yes' if row.is_weekly_off else 'No' }}</div>
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-slate-600">
|
|
<div>Source: {{ (row.source or '-').replace('_',' ').title() }}</div>
|
|
<div>Geo: {{ (row.punch_in_geo_status or '-').replace('_',' ').title() }}</div>
|
|
<div>Distance: {{ (row.punch_in_distance_meters ~ ' m') if row.punch_in_distance_meters is not none else '-' }}</div>
|
|
<div>IP: {{ (row.punch_in_ip_status or '-').replace('_',' ').title() }}</div>
|
|
</td>
|
|
<td class="px-4 py-3 text-slate-600">{{ row.remarks or row.review_notes or '-' }}</td>
|
|
<td class="px-4 py-3 text-right">
|
|
{% if can_approve_employee_attendance(current_user, current_user_permissions, current_user_roles) %}
|
|
<form method="post" action="/employees/attendance/{{ row.id }}/review" class="inline-flex gap-2">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="hidden" name="approval_status" value="approved">
|
|
<input type="hidden" name="review_notes" value="Approved from attendance review">
|
|
<button class="rounded-lg border border-emerald-300 px-3 py-1.5 text-xs font-semibold text-emerald-700 hover:bg-emerald-50">Approve</button>
|
|
</form>
|
|
<form method="post" action="/employees/attendance/{{ row.id }}/review" class="inline-flex gap-2">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="hidden" name="approval_status" value="rejected">
|
|
<input type="hidden" name="review_notes" value="Rejected from attendance review">
|
|
<button class="rounded-lg border border-red-300 px-3 py-1.5 text-xs font-semibold text-red-700 hover:bg-red-50">Reject</button>
|
|
</form>
|
|
{% else %}-{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="9" class="px-4 py-8 text-center text-slate-500">No attendance records found.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|