Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
{% set current_path = request.url.path %}
|
||||
{% set _tab_base = "inline-flex shrink-0 items-center rounded-xl px-3 py-2 text-sm font-semibold whitespace-nowrap transition" %}
|
||||
{% set _tab_active = "bg-brand-600 text-white shadow-soft" %}
|
||||
{% set _tab_idle = "border border-slate-300 bg-white text-slate-700 hover:bg-slate-50" %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-3 shadow-soft">
|
||||
<div class="mb-2 px-1 text-xs font-semibold uppercase tracking-wide text-slate-500">My Workspace</div>
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<div class="flex min-w-max flex-nowrap gap-2">
|
||||
{% if can_view_employee_portal(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employee/dashboard" class="{{ _tab_base }} {{ _tab_active if current_path == '/employee/dashboard' else _tab_idle }}">Overview</a>
|
||||
{% endif %}
|
||||
{% if can_view_own_employee_work(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employee/work" class="{{ _tab_base }} {{ _tab_active if current_path.startswith('/employee/work') else _tab_idle }}">My Work Board</a>
|
||||
{% endif %}
|
||||
{% if can_view_own_employee_attendance(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employee/attendance" class="{{ _tab_base }} {{ _tab_active if current_path.startswith('/employee/attendance') else _tab_idle }}">My Attendance</a>
|
||||
{% endif %}
|
||||
{% if can_view_employee_portal(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employee/profile" class="{{ _tab_base }} {{ _tab_active if current_path.startswith('/employee/profile') else _tab_idle }}">My Profile</a>
|
||||
{% endif %}
|
||||
{% if can_view_own_employee_leave(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employee/leave" class="{{ _tab_base }} {{ _tab_active if current_path.startswith('/employee/leave') else _tab_idle }}">My Leave</a>
|
||||
{% endif %}
|
||||
{% if can_view_own_employee_documents(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employee/documents" class="{{ _tab_base }} {{ _tab_active if current_path.startswith('/employee/documents') else _tab_idle }}">My Documents</a>
|
||||
{% endif %}
|
||||
{% if can_view_own_employee_payslips(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employee/payslips" class="{{ _tab_base }} {{ _tab_active if current_path.startswith('/employee/payslips') else _tab_idle }}">My Payslips</a>
|
||||
{% endif %}
|
||||
{% if can_request_own_employee_offboarding(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employee/offboarding" class="{{ _tab_base }} {{ _tab_active if current_path.startswith('/employee/offboarding') else _tab_idle }}">My Offboarding</a>
|
||||
{% endif %}
|
||||
<a href="/alerts" class="{{ _tab_base }} {{ _tab_active if current_path.startswith('/alerts') else _tab_idle }}">
|
||||
My Alert{% if unread_alert_count is defined and unread_alert_count > 0 %}<span class="ml-2 rounded-full bg-white/20 px-2 py-0.5 text-[10px]">{{ unread_alert_count }}</span>{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,114 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,108 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="mb-4 flex flex-wrap gap-3">
|
||||
<form method="post" action="/employees/{{ employee.id }}/onboarding/generate" class="inline-block">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button class="rounded-xl bg-indigo-700 px-4 py-2 text-sm font-semibold text-white">Generate Onboarding</button>
|
||||
</form>
|
||||
<form method="post" action="/employees/{{ employee.id }}/offboarding/initiate" class="inline-grid gap-2 rounded-2xl border bg-white p-3 align-top shadow-sm md:grid-cols-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<select name="request_type" class="rounded-lg border px-2 py-1 text-sm"><option value="resignation">Resignation</option><option value="termination">Termination</option><option value="contract_end">Contract End</option></select>
|
||||
<input type="date" name="requested_relieving_date" class="rounded-lg border px-2 py-1 text-sm">
|
||||
<input name="reason" placeholder="Reason" class="rounded-lg border px-2 py-1 text-sm">
|
||||
<button class="rounded-lg bg-rose-700 px-3 py-1 text-sm font-semibold text-white">Initiate Offboarding</button>
|
||||
</form>
|
||||
</div>
|
||||
<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.full_name }}</h2>
|
||||
<p class="text-sm text-slate-500">Employee Code: {{ employee.employee_code }} • Audit Firm {{ employee.tenant_id }} • Branch {{ employee.branch_id }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<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">Back</a>
|
||||
{% if can_manage_employees(current_user, current_user_permissions, current_user_roles) %}<a href="/employees/{{ employee.id }}/edit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Edit</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if link_error %}
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Unable to update login linkage. Please select an active user from the same audit firm and branch who is not already linked to another employee.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="rounded-2xl border {% if employee.user_id %}border-emerald-200 bg-emerald-50{% else %}border-amber-200 bg-amber-50{% endif %} p-4 shadow-soft">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="font-semibold {% if employee.user_id %}text-emerald-900{% else %}text-amber-900{% endif %}">Login user linkage</h3>
|
||||
{% if employee.user_id %}
|
||||
<p class="mt-1 text-sm text-emerald-800">This employee is linked to {% if employee.user %}{{ employee.user.full_name or employee.user.email }}{% else %}User #{{ employee.user_id }}{% endif %}. Employee self-service pages will work for this login.</p>
|
||||
{% else %}
|
||||
<p class="mt-1 text-sm text-amber-900">This employee is not linked to a login user yet. Attendance, My Workspace, profile, leave, documents and payslip self-service need this link.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if can_manage_employees(current_user, current_user_permissions, current_user_roles) %}
|
||||
<form method="post" action="/employees/{{ employee.id }}/link-user" class="flex flex-wrap items-center gap-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<select name="user_id" class="min-w-64 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||||
<option value="">Unlink / no login user</option>
|
||||
{% for u in linkable_users %}
|
||||
<option value="{{ u.id }}" {% if employee.user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }} — {{ u.email }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Update Link</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="space-y-6 lg:col-span-2">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Basic Details</h3>
|
||||
<dl class="mt-4 grid gap-4 md:grid-cols-2 text-sm">
|
||||
<div><dt class="text-slate-500">Email</dt><dd class="font-medium text-slate-900">{{ employee.email or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Mobile</dt><dd class="font-medium text-slate-900">{{ employee.mobile or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Department</dt><dd class="font-medium text-slate-900">{{ employee.department or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Designation</dt><dd class="font-medium text-slate-900">{{ employee.designation or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Employment Type</dt><dd class="font-medium text-slate-900">{{ employee.employment_type.replace('_',' ').title() }}</dd></div>
|
||||
<div><dt class="text-slate-500">Date of Joining</dt><dd class="font-medium text-slate-900">{{ employee.date_of_joining or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Date of Leaving</dt><dd class="font-medium text-slate-900">{{ employee.date_of_leaving or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Linked User</dt><dd class="font-medium text-slate-900">{% if employee.user %}{{ employee.user.full_name or employee.user.email }} <span class="text-xs text-slate-500">({{ employee.user.email }})</span>{% else %}<span class="text-amber-700">Not linked</span>{% endif %}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Statutory & Bank Details</h3>
|
||||
<dl class="mt-4 grid gap-4 md:grid-cols-2 text-sm">
|
||||
<div><dt class="text-slate-500">PAN</dt><dd class="font-medium text-slate-900">{{ employee.pan or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">UAN</dt><dd class="font-medium text-slate-900">{{ employee.uan or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">ESI No</dt><dd class="font-medium text-slate-900">{{ employee.esi_no or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">PF No</dt><dd class="font-medium text-slate-900">{{ employee.pf_no or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Bank Name</dt><dd class="font-medium text-slate-900">{{ employee.bank_name or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Bank Account</dt><dd class="font-medium text-slate-900">{{ employee.bank_account_no or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">IFSC</dt><dd class="font-medium text-slate-900">{{ employee.bank_ifsc or '-' }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Status</h3>
|
||||
<div class="mt-3"><span class="rounded-full px-3 py-1 text-sm font-semibold {% if employee.status == 'active' %}bg-emerald-50 text-emerald-700{% elif employee.status == 'relieved' %}bg-amber-50 text-amber-700{% else %}bg-slate-100 text-slate-600{% endif %}">{{ employee.status.replace('_',' ').title() }}</span></div>
|
||||
{% if can_change_employee_status(current_user, current_user_permissions, current_user_roles) %}
|
||||
<form method="post" action="/employees/{{ employee.id }}/status" class="mt-5 space-y-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Change Status</label><select name="status" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% for st in employee_statuses %}<option value="{{ st }}" {% if employee.status == st %}selected{% endif %}>{{ st.title() }}</option>{% endfor %}</select></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Date of Leaving</label><input type="date" name="date_of_leaving" value="{{ employee.date_of_leaving or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<button class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Update Status</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Emergency / Notes</h3>
|
||||
<dl class="mt-4 space-y-3 text-sm"><div><dt class="text-slate-500">Emergency Contact</dt><dd class="font-medium text-slate-900">{{ employee.emergency_contact_name or '-' }} {{ employee.emergency_contact_mobile or '' }}</dd></div><div><dt class="text-slate-500">Address</dt><dd class="whitespace-pre-line font-medium text-slate-900">{{ employee.address or '-' }}</dd></div><div><dt class="text-slate-500">Notes</dt><dd class="whitespace-pre-line font-medium text-slate-900">{{ employee.notes or '-' }}</dd></div></dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,61 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% if errors %}<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ errors|join(', ') }}</div>{% endif %}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Employee Document Types</h2>
|
||||
<p class="text-sm text-slate-500">Maintain branch-wise document categories like PAN, Aadhaar, certificates and bank proof.</p>
|
||||
</div>
|
||||
<form method="post" action="/employees/document-types/defaults">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700">Create Defaults</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h3 class="mb-4 font-semibold text-slate-900">Add Document Type</h3>
|
||||
<form method="post" action="/employees/document-types" class="grid gap-4 md:grid-cols-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div><label class="text-xs font-semibold text-slate-500">Code</label><input name="code" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" required></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Name</label><input name="name" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" required></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Description</label><input name="description" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||
<label class="flex items-center gap-2"><input type="checkbox" name="is_mandatory" value="1"> Mandatory</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" name="allow_employee_upload" value="1" checked> Employee Upload</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" name="requires_verification" value="1" checked> Verify</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" name="is_active" value="1" checked> Active</label>
|
||||
</div>
|
||||
<div class="md:col-span-4"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Save Type</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500"><tr><th class="px-4 py-3">Code</th><th class="px-4 py-3">Name</th><th class="px-4 py-3">Rules</th><th class="px-4 py-3">Edit</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium">{{ row.code }}</td><td class="px-4 py-3">{{ row.name }}<div class="text-xs text-slate-500">{{ row.description or '' }}</div></td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">{% if row.is_mandatory %}Mandatory{% else %}Optional{% endif %} • {% if row.allow_employee_upload %}Employee upload{% else %}HR upload only{% endif %} • {% if row.requires_verification %}Verification{% else %}No verification{% endif %} • {% if row.is_active %}Active{% else %}Inactive{% endif %}</td>
|
||||
<td class="px-4 py-3">
|
||||
<details><summary class="cursor-pointer text-brand-700">Edit</summary>
|
||||
<form method="post" action="/employees/document-types/{{ row.id }}/edit" class="mt-3 grid gap-2 rounded-xl border border-slate-200 p-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input name="name" value="{{ row.name }}" class="rounded-lg border border-slate-300 px-3 py-2">
|
||||
<input name="description" value="{{ row.description or '' }}" class="rounded-lg border border-slate-300 px-3 py-2">
|
||||
<label><input type="checkbox" name="is_mandatory" value="1" {% if row.is_mandatory %}checked{% endif %}> Mandatory</label>
|
||||
<label><input type="checkbox" name="allow_employee_upload" value="1" {% if row.allow_employee_upload %}checked{% endif %}> Employee upload allowed</label>
|
||||
<label><input type="checkbox" name="requires_verification" value="1" {% if row.requires_verification %}checked{% endif %}> Requires verification</label>
|
||||
<label><input type="checkbox" name="is_active" value="1" {% if row.is_active %}checked{% endif %}> Active</label>
|
||||
<button class="rounded-lg bg-slate-900 px-3 py-2 text-white">Update</button>
|
||||
</form>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}<tr><td colspan="4" class="px-4 py-6 text-center text-slate-500">No document types found.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,44 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% if errors %}<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ errors|join(', ') }}</div>{% endif %}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div><h2 class="text-xl font-semibold text-slate-900">Employee Documents</h2><p class="text-sm text-slate-500">Upload, review and archive employee documents with audit firm/branch scope.</p></div>
|
||||
<a href="/employees/document-types" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Document Types</a>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h3 class="mb-4 font-semibold text-slate-900">Upload Document</h3>
|
||||
<form method="post" enctype="multipart/form-data" action="/employees/{{ selected_employee_id or (employees[0].id if employees else 0) }}/documents/upload" onsubmit="if(!document.getElementById('upload_employee_id').value){return false;} this.action='/employees/'+document.getElementById('upload_employee_id').value+'/documents/upload';" class="grid gap-4 md:grid-cols-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div><label class="text-xs font-semibold text-slate-500">Employee</label><select id="upload_employee_id" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" required>{% for emp in employees %}<option value="{{ emp.id }}" {% if selected_employee_id==emp.id %}selected{% endif %}>{{ emp.employee_code }} - {{ emp.full_name }}</option>{% endfor %}</select></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Type</label><select name="document_type_id" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"><option value="">General</option>{% for dt in document_types %}<option value="{{ dt.id }}">{{ dt.code }} - {{ dt.name }}</option>{% endfor %}</select></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Title</label><input name="title" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">File</label><input type="file" name="document_file" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" required></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Document No</label><input name="document_no" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Issue Date</label><input type="date" name="issue_date" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Expiry Date</label><input type="date" name="expiry_date" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Visibility</label><select name="visibility" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"><option value="employee_and_hr">Employee and HR</option><option value="hr_only">HR Only</option></select></div>
|
||||
<div class="md:col-span-4"><label class="text-xs font-semibold text-slate-500">Remarks</label><input name="remarks" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div class="md:col-span-4"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Upload Document</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm"><thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500"><tr><th class="px-4 py-3">Employee</th><th class="px-4 py-3">Document</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Dates</th><th class="px-4 py-3">Review</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100">{% for row in rows %}<tr>
|
||||
<td class="px-4 py-3">{{ row.employee.employee_code if row.employee else '' }}<div class="font-medium">{{ row.employee.full_name if row.employee else '' }}</div></td>
|
||||
<td class="px-4 py-3"><div class="font-medium">{{ row.title }}</div><div class="text-xs text-slate-500">{{ row.document_type.code if row.document_type else 'GENERAL' }} • {{ row.original_filename }} • {{ row.file_size_bytes or 0 }} bytes</div><div class="text-xs text-slate-400">{{ row.storage_path }}</div></td>
|
||||
<td class="px-4 py-3"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs">{{ row.status }}</span><div class="text-xs text-slate-500">{{ row.visibility }}</div></td>
|
||||
<td class="px-4 py-3 text-xs">Issue: {{ row.issue_date or '-' }}<br>Expiry: {{ row.expiry_date or '-' }}</td>
|
||||
<td class="px-4 py-3">
|
||||
{% if row.status != 'archived' %}
|
||||
<form method="post" action="/employees/documents/{{ row.id }}/review" class="mb-2 flex gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input name="verification_notes" placeholder="Notes" class="w-32 rounded-lg border border-slate-300 px-2 py-1"><button name="status" value="verified" class="rounded-lg bg-emerald-600 px-2 py-1 text-xs text-white">Verify</button><button name="status" value="rejected" class="rounded-lg bg-red-600 px-2 py-1 text-xs text-white">Reject</button></form>
|
||||
<form method="post" action="/employees/documents/{{ row.id }}/archive"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="text-xs text-red-700">Archive</button></form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>{% else %}<tr><td colspan="5" class="px-4 py-6 text-center text-slate-500">No documents found.</td></tr>{% endfor %}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,107 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% set is_dict = employee is mapping %}
|
||||
{% set is_edit = mode == 'edit' %}
|
||||
{% macro val(name, default='') -%}
|
||||
{%- if employee -%}
|
||||
{%- if is_dict -%}{{ employee.get(name, default) or '' }}{%- else -%}{{ employee|attr(name) or '' }}{%- endif -%}
|
||||
{%- else -%}{{ default }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
<div class="mx-auto max-w-5xl space-y-6">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">{{ title }}</h2>
|
||||
<p class="text-sm text-slate-500">Create or update the employee master. Attendance, leave, payroll and ESS will be added in later phases.</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">Back</a>
|
||||
</div>
|
||||
|
||||
{% if errors %}
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800"><ul class="list-disc pl-5">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul></div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="space-y-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
{% if not is_edit %}
|
||||
<div class="rounded-2xl border border-brand-100 bg-brand-50 p-4">
|
||||
<h3 class="font-semibold text-slate-900">Employee Login Link</h3>
|
||||
<p class="mt-1 text-sm text-slate-600">Either link an existing unlinked login user or create a login user automatically for this employee.</p>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">Existing user</label>
|
||||
<select name="user_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">No existing user selected</option>
|
||||
{% for u in users %}<option value="{{ u.id }}">{{ u.full_name or u.email }} — {{ u.email }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2 rounded-xl bg-white p-3">
|
||||
<label class="inline-flex items-center gap-2 text-sm font-medium text-slate-700"><input type="checkbox" name="create_login_user" class="h-4 w-4 rounded border-slate-300"> Create login user</label>
|
||||
<p class="text-xs text-slate-500">When selected, Login Email and Temporary Password below are required.</p>
|
||||
</div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Login Email</label><input type="email" name="login_email" value="{{ val('login_email') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Temporary Password</label><input type="password" name="temporary_password" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"><p class="mt-1 text-xs text-slate-500">Minimum 8 characters. User must change password after login.</p></div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">Employee User Role</label>
|
||||
<select name="employee_role" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% for r in employee_role_names %}<option value="{{ r }}" {% if r == 'Staff' %}selected{% endif %}>{{ r }}</option>{% endfor %}</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 p-4 text-sm text-slate-600">Login user linkage can be changed below. The dropdown shows unlinked users from the same audit firm/branch, plus the currently linked user if any. New user creation is available only while creating an employee.</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">Linked user</label>
|
||||
{% set uid = employee.user_id if employee and not is_dict else employee.get('user_id') if employee else None %}
|
||||
<select name="user_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">No user linked</option>
|
||||
{% for u in users %}<option value="{{ u.id }}" {% if uid == u.id %}selected{% endif %}>{{ u.full_name or u.email }} — {{ u.email }}</option>{% endfor %}
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-slate-500">Selecting “No user linked” will keep this employee as HR master only. Employee self-service will not work until linked.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not is_edit and scope.allow_cross_tenant %}
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Audit Firm</label><select name="tenant_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% for t in tenants %}<option value="{{ t.id }}" {% if t.id == scope.tenant_id %}selected{% endif %}>{{ t.name }} ({{ t.code }})</option>{% endfor %}</select></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Branch</label><select name="branch_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% for b in branches %}<option value="{{ b.id }}" {% if b.id == scope.branch_id %}selected{% endif %}>{{ b.name }} ({{ b.code }})</option>{% endfor %}</select></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<input type="hidden" name="tenant_id" value="{{ scope.tenant_id }}">
|
||||
<input type="hidden" name="branch_id" value="{{ scope.branch_id or current_user.branch_id }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Employee Code *</label><input name="employee_code" value="{{ val('employee_code') }}" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Full Name *</label><input name="full_name" value="{{ val('full_name') }}" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Email</label><input type="email" name="email" value="{{ val('email') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Mobile</label><input name="mobile" value="{{ val('mobile') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Alternate Mobile</label><input name="alternate_mobile" value="{{ val('alternate_mobile') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Date of Joining</label><input type="date" name="date_of_joining" value="{{ val('date_of_joining') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Employment Type</label><select name="employment_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% set et = val('employment_type','full_time') %}{% for code in employment_types %}<option value="{{ code }}" {% if et == code %}selected{% endif %}>{{ code.replace('_',' ').title() }}</option>{% endfor %}</select></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Status</label><select name="status" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% set st = val('status','active') %}{% for code in employee_statuses %}<option value="{{ code }}" {% if st == code %}selected{% endif %}>{{ code.title() }}</option>{% endfor %}</select></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Department</label><input name="department" value="{{ val('department') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Designation</label><input name="designation" value="{{ val('designation') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Reporting Manager</label><select name="reporting_manager_user_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% set mid = employee.reporting_manager_user_id if employee and not is_dict else employee.get('reporting_manager_user_id') if employee else None %}<option value="">No manager selected</option>{% for m in managers %}<option value="{{ m.id }}" {% if mid == m.id %}selected{% endif %}>{{ m.full_name or m.email }}</option>{% endfor %}</select></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">PAN</label><input name="pan" value="{{ val('pan') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">UAN</label><input name="uan" value="{{ val('uan') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">ESI No</label><input name="esi_no" value="{{ val('esi_no') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">PF No</label><input name="pf_no" value="{{ val('pf_no') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Aadhaar Last 4</label><input name="aadhaar_last4" maxlength="4" value="{{ val('aadhaar_last4') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Bank Name</label><input name="bank_name" value="{{ val('bank_name') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Bank Account No</label><input name="bank_account_no" value="{{ val('bank_account_no') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Bank IFSC</label><input name="bank_ifsc" value="{{ val('bank_ifsc') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Emergency Contact Name</label><input name="emergency_contact_name" value="{{ val('emergency_contact_name') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Emergency Contact Mobile</label><input name="emergency_contact_mobile" value="{{ val('emergency_contact_mobile') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div class="md:col-span-2"><label class="mb-1 block text-sm font-medium text-slate-700">Address</label><textarea name="address" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ val('address') }}</textarea></div>
|
||||
<div class="md:col-span-2"><label class="mb-1 block text-sm font-medium text-slate-700">Notes</label><textarea name="notes" rows="3" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ val('notes') }}</textarea></div>
|
||||
</div>
|
||||
|
||||
<label class="inline-flex items-center gap-2 text-sm text-slate-700"><input type="checkbox" name="is_active" {% if not employee or val('status','active') == 'active' %}checked{% endif %}> Active employee</label>
|
||||
|
||||
<div class="flex justify-end gap-3"><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">Cancel</a><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Save Employee</button></div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,97 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-500">Employees / HRMS</p>
|
||||
<h1 class="text-2xl font-bold text-slate-900">HR Dashboard & Reports</h1>
|
||||
<p class="mt-1 text-sm text-slate-600">Live summary for the active audit firm{% if scope.branch_id %} and branch{% else %} across accessible branches{% endif %}.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/employees" class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-700">Employee Master</a>
|
||||
<a href="/employees/attendance" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Attendance</a>
|
||||
<a href="/employees/leave" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Leave</a>
|
||||
<a href="/employees/payroll/runs" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Payroll</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200"><p class="text-sm text-slate-500">Total Employees</p><p class="mt-2 text-3xl font-bold text-slate-900">{{ stats.employees.total }}</p><p class="mt-1 text-xs text-slate-500">Active: {{ stats.employees.active }}</p></div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200"><p class="text-sm text-slate-500">Today Attendance</p><p class="mt-2 text-3xl font-bold text-slate-900">{{ stats.attendance.today_present }}</p><p class="mt-1 text-xs text-slate-500">Records: {{ stats.attendance.today_total }}, Pending: {{ stats.attendance.today_pending }}</p></div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200"><p class="text-sm text-slate-500">Pending Leave</p><p class="mt-2 text-3xl font-bold text-slate-900">{{ stats.leave.pending }}</p><p class="mt-1 text-xs text-slate-500">Approved: {{ stats.leave.approved }}</p></div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200"><p class="text-sm text-slate-500">Docs to Verify</p><p class="mt-2 text-3xl font-bold text-slate-900">{{ stats.documents.uploaded }}</p><p class="mt-1 text-xs text-slate-500">Verified: {{ stats.documents.verified }}</p></div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200"><p class="text-sm text-slate-500">Payroll Pending</p><p class="mt-2 text-3xl font-bold text-slate-900">{{ stats.payroll.runs_draft + stats.payroll.runs_generated + stats.payroll.runs_approved }}</p><p class="mt-1 text-xs text-slate-500">Paid runs: {{ stats.payroll.runs_paid }}</p></div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Employee Status</h2>
|
||||
<div class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex justify-between"><span>Active</span><strong>{{ stats.employees.active }}</strong></div>
|
||||
<div class="flex justify-between"><span>Inactive</span><strong>{{ stats.employees.inactive }}</strong></div>
|
||||
<div class="flex justify-between"><span>Relieved</span><strong>{{ stats.employees.relieved }}</strong></div>
|
||||
<div class="flex justify-between"><span>Pending registrations</span><strong>{{ stats.employees.pending_registrations }}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Workflow Pending</h2>
|
||||
<div class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex justify-between"><span>Attendance approvals</span><strong>{{ stats.attendance.today_pending }}</strong></div>
|
||||
<div class="flex justify-between"><span>Leave approvals</span><strong>{{ stats.leave.pending }}</strong></div>
|
||||
<div class="flex justify-between"><span>Onboarding tasks</span><strong>{{ stats.onboarding.pending }}</strong></div>
|
||||
<div class="flex justify-between"><span>Offboarding requests</span><strong>{{ stats.offboarding.pending }}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Payroll Snapshot</h2>
|
||||
<div class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex justify-between"><span>Active salary structures</span><strong>{{ stats.payroll.salary_structures_active }}</strong></div>
|
||||
<div class="flex justify-between"><span>Draft runs</span><strong>{{ stats.payroll.runs_draft }}</strong></div>
|
||||
<div class="flex justify-between"><span>Generated payslips</span><strong>{{ stats.payroll.payslips_generated }}</strong></div>
|
||||
<div class="flex justify-between"><span>Paid payslips</span><strong>{{ stats.payroll.payslips_paid }}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-3">
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
|
||||
<div class="mb-4 flex items-center justify-between"><h2 class="text-lg font-semibold text-slate-900">Recent Employees</h2><a class="text-sm font-medium text-slate-600 hover:text-slate-900" href="/employees">View all</a></div>
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for emp in stats.recent_employees %}
|
||||
<tr><td class="px-3 py-2"><a class="font-medium text-slate-900 hover:underline" href="/employees/{{ emp.id }}">{{ emp.full_name }}</a><div class="text-xs text-slate-500">{{ emp.employee_code }} · {{ emp.designation or '-' }}</div></td><td class="px-3 py-2 text-right"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs">{{ emp.status }}</span></td></tr>
|
||||
{% else %}
|
||||
<tr><td class="px-3 py-4 text-slate-500">No employees found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
|
||||
<div class="mb-4 flex items-center justify-between"><h2 class="text-lg font-semibold text-slate-900">Recent Leave</h2><a class="text-sm font-medium text-slate-600 hover:text-slate-900" href="/employees/leave">View all</a></div>
|
||||
<div class="space-y-3 text-sm">
|
||||
{% for item in stats.recent_leave_requests %}
|
||||
<div class="rounded-xl border border-slate-200 p-3"><div class="flex justify-between gap-3"><strong>{{ item.employee.full_name if item.employee else 'Employee' }}</strong><span class="text-xs text-slate-500">{{ item.status }}</span></div><div class="mt-1 text-xs text-slate-500">{{ item.leave_type.code if item.leave_type else '-' }} · {{ item.from_date }} to {{ item.to_date }}</div></div>
|
||||
{% else %}
|
||||
<p class="text-slate-500">No leave requests found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
|
||||
<div class="mb-4 flex items-center justify-between"><h2 class="text-lg font-semibold text-slate-900">Recent Offboarding</h2><a class="text-sm font-medium text-slate-600 hover:text-slate-900" href="/employees/offboarding">View all</a></div>
|
||||
<div class="space-y-3 text-sm">
|
||||
{% for item in stats.recent_offboarding_requests %}
|
||||
<div class="rounded-xl border border-slate-200 p-3"><div class="flex justify-between gap-3"><strong>{{ item.employee.full_name if item.employee else 'Employee' }}</strong><span class="text-xs text-slate-500">{{ item.status }}</span></div><div class="mt-1 text-xs text-slate-500">Requested relieving date: {{ item.requested_relieving_date or '-' }}</div></div>
|
||||
{% else %}
|
||||
<p class="text-slate-500">No offboarding requests found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<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">Import Preview - {{ preview.import_label }}</h2>
|
||||
<p class="text-sm text-slate-500">Review validation results. Rows with error will be skipped during commit.</p>
|
||||
</div>
|
||||
<a href="/employees/imports" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back to Imports</a>
|
||||
</div>
|
||||
|
||||
{% if errors %}<div class="rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700">{{ errors|join(', ') }}</div>{% endif %}
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs uppercase text-slate-500">Total Rows</div><div class="text-2xl font-semibold">{{ preview.summary.total }}</div></div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4"><div class="text-xs uppercase text-emerald-700">Valid</div><div class="text-2xl font-semibold text-emerald-800">{{ preview.summary.valid }}</div></div>
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4"><div class="text-xs uppercase text-amber-700">Warnings</div><div class="text-2xl font-semibold text-amber-800">{{ preview.summary.warning }}</div></div>
|
||||
<div class="rounded-2xl border border-rose-200 bg-rose-50 p-4"><div class="text-xs uppercase text-rose-700">Errors</div><div class="text-2xl font-semibold text-rose-800">{{ preview.summary.error }}</div></div>
|
||||
</div>
|
||||
|
||||
<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">Excel Row</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Action</th><th class="px-4 py-3">Key Data</th><th class="px-4 py-3">Messages</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in preview.rows %}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-4 py-3 font-semibold">{{ row.row_no }}</td>
|
||||
<td class="px-4 py-3"><span class="rounded-full px-2 py-1 text-xs font-semibold {% if row.status == 'valid' %}bg-emerald-50 text-emerald-700{% elif row.status == 'warning' %}bg-amber-50 text-amber-700{% else %}bg-rose-50 text-rose-700{% endif %}">{{ row.status|title }}</span></td>
|
||||
<td class="px-4 py-3">{{ row.action|title }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">
|
||||
{% if preview.import_type == 'employees' %}{{ row.data.employee_code }} - {{ row.data.full_name }}{% elif preview.import_type == 'leave_types' %}{{ row.data.code }} - {{ row.data.name }}{% elif preview.import_type == 'leave_balances' %}{{ row.data.employee_code }} / {{ row.data.leave_code }}{% else %}{{ row.data.employee_code }} from {{ row.data.effective_from }}{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ row.messages|join('; ') if row.messages else '-' }}</td>
|
||||
</tr>
|
||||
{% else %}<tr><td colspan="5" class="px-4 py-6 text-center text-slate-500">No rows found.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/employees/imports/commit" class="flex items-center justify-end gap-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<a href="/employees/imports" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Cancel</a>
|
||||
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700" {% if preview.summary.total == 0 or (preview.summary.valid == 0 and preview.summary.warning == 0) %}disabled{% endif %}>Confirm Import</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<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">HR Excel Imports</h2>
|
||||
<p class="text-sm text-slate-500">Import employees, leave types, opening leave balances and salary structures with validation preview before commit.</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>
|
||||
|
||||
{% if errors %}<div class="rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700">{{ errors|join(', ') }}</div>{% endif %}
|
||||
{% if message %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-700">{{ message }}</div>{% endif %}
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
{% for key, label in import_types.items() %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-900">{{ label }}</h3>
|
||||
<p class="mt-1 text-xs text-slate-500">Download template, fill data, upload and preview before import.</p>
|
||||
</div>
|
||||
<a href="/employees/imports/template/{{ key }}" class="rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Template</a>
|
||||
</div>
|
||||
<form method="post" action="/employees/imports/preview" enctype="multipart/form-data" class="mt-4 space-y-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="import_type" value="{{ key }}">
|
||||
<input type="file" name="import_file" accept=".xlsx,.xlsm" required class="block w-full rounded-xl border border-slate-300 px-3 py-2 text-sm file:mr-3 file:rounded-lg file:border-0 file:bg-slate-100 file:px-3 file:py-1.5 file:text-sm file:font-semibold file:text-slate-700">
|
||||
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Upload & Preview</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
||||
<div class="font-semibold">Important</div>
|
||||
<p class="mt-1">For System Admin/Firm Admin using all-branch context, provide <code>branch_id</code> in Excel or switch to a specific active branch before importing. Existing rows are updated based on employee code, leave code, or employee/effective date as applicable.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% 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">Leave Balances</h2><p class="text-sm text-slate-500">View and adjust employee leave balances branch-wise.</p></div><a href="/employees/leave" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Leave Requests</a></div><form method="post" action="/employees/leave-balances/adjust" 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">Adjust Balance</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><select name="leave_type_id" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><option value="">Leave type</option>{% for lt in leave_types %}<option value="{{ lt.id }}">{{ lt.code }} - {{ lt.name }}</option>{% endfor %}</select><input type="number" name="adjusted_days" value="0" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><input name="notes" placeholder="Notes" 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><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">Employee</th><th class="px-4 py-3">Leave Type</th><th class="px-4 py-3">Opening</th><th class="px-4 py-3">Credit</th><th class="px-4 py-3">Availed</th><th class="px-4 py-3">Adjust</th><th class="px-4 py-3">Balance</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in rows %}<tr><td class="px-4 py-3">{{ row.employee.full_name if row.employee else row.employee_id }}</td><td class="px-4 py-3">{{ row.leave_type.code if row.leave_type else row.leave_type_id }}</td><td class="px-4 py-3">{{ row.opening_days }}</td><td class="px-4 py-3">{{ row.credited_days }}</td><td class="px-4 py-3">{{ row.availed_days }}</td><td class="px-4 py-3">{{ row.adjusted_days }}</td><td class="px-4 py-3 font-semibold">{{ row.balance_days }}</td></tr>{% else %}<tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">No leave balances found. Balances are created when leave is approved or adjusted.</td></tr>{% endfor %}</tbody></table></div></div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% 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 Leave Requests</h2><p class="text-sm text-slate-500">Review and approve/reject employee leave applications.</p></div><a href="/employees/leave-balances" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Leave Balances</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-4"><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><select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><option value="all" {% if status == 'all' %}selected{% endif %}>All statuses</option>{% for st in statuses %}<option value="{{ st }}" {% if 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><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">Employee</th><th class="px-4 py-3">Leave</th><th class="px-4 py-3">Period</th><th class="px-4 py-3">Days</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Reason / Review</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><td class="px-4 py-3"><div class="font-medium text-slate-900">{{ row.employee.full_name if row.employee else ('Employee #' ~ row.employee_id) }}</div><div class="text-xs text-slate-500">{{ row.employee.employee_code if row.employee else '' }}</div></td><td class="px-4 py-3">{{ row.leave_type.name if row.leave_type else row.leave_type_id }}</td><td class="px-4 py-3">{{ row.from_date }} to {{ row.to_date }}</td><td class="px-4 py-3">{{ row.days }}</td><td class="px-4 py-3">{{ row.status.title() }}</td><td class="px-4 py-3 text-slate-600"><div>{{ row.reason or '-' }}</div>{% if row.review_notes %}<div class="text-xs text-slate-500">Review: {{ row.review_notes }}</div>{% endif %}</td><td class="px-4 py-3 text-right">{% if row.status == 'pending' and can_approve_employee_leave(current_user, current_user_permissions, current_user_roles) %}<form method="post" action="/employees/leave/{{ row.id }}/review" class="inline-flex gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="status" value="approved"><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/leave/{{ row.id }}/review" class="inline-flex gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="status" value="rejected"><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="7" class="px-4 py-8 text-center text-slate-500">No leave requests found.</td></tr>{% endfor %}</tbody></table></div></div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<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">Leave Types</h2><p class="text-sm text-slate-500">Configure branch-wise leave masters such as CL, SL, EL and LOP.</p></div><form method="post" action="/employees/leave-types/defaults"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Create Defaults</button></form></div>
|
||||
<form method="post" action="/employees/leave-types/new" 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">Add Leave Type</h3><div class="mt-4 grid gap-3 md:grid-cols-4"><input name="code" required placeholder="Code e.g. CL" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><input name="name" required placeholder="Name" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><input type="number" name="annual_quota_days" value="0" min="0" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><input name="description" placeholder="Description" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"></div><div class="mt-3 flex flex-wrap gap-4 text-sm text-slate-700"><label><input type="checkbox" name="requires_approval" checked> Requires approval</label><label><input type="checkbox" name="is_paid" checked> Paid leave</label><label><input type="checkbox" name="carry_forward_allowed"> Carry forward</label><label><input type="checkbox" name="allow_negative_balance"> Allow negative balance</label><label><input type="checkbox" name="is_active" checked> Active</label></div><button class="mt-4 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Save Leave Type</button></form>
|
||||
<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">Code</th><th class="px-4 py-3">Name</th><th class="px-4 py-3">Quota</th><th class="px-4 py-3">Rules</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Edit</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in rows %}<tr><td class="px-4 py-3 font-semibold">{{ row.code }}</td><td class="px-4 py-3">{{ row.name }}</td><td class="px-4 py-3">{{ row.annual_quota_days }}</td><td class="px-4 py-3 text-slate-600">{{ 'Approval' if row.requires_approval else 'Auto approve' }} · {{ 'Paid' if row.is_paid else 'Unpaid' }}{% if row.allow_negative_balance %} · Negative allowed{% endif %}</td><td class="px-4 py-3">{{ 'Active' if row.is_active else 'Inactive' }}</td><td class="px-4 py-3"><details><summary class="cursor-pointer text-brand-700">Edit</summary><form method="post" action="/employees/leave-types/{{ row.id }}/edit" class="mt-3 grid gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input name="name" value="{{ row.name }}" class="rounded-lg border px-2 py-1"><input type="number" name="annual_quota_days" value="{{ row.annual_quota_days }}" class="rounded-lg border px-2 py-1"><input name="description" value="{{ row.description or '' }}" class="rounded-lg border px-2 py-1"><label><input type="checkbox" name="requires_approval" {% if row.requires_approval %}checked{% endif %}> Requires approval</label><label><input type="checkbox" name="is_paid" {% if row.is_paid %}checked{% endif %}> Paid</label><label><input type="checkbox" name="carry_forward_allowed" {% if row.carry_forward_allowed %}checked{% endif %}> Carry forward</label><label><input type="checkbox" name="allow_negative_balance" {% if row.allow_negative_balance %}checked{% endif %}> Negative allowed</label><label><input type="checkbox" name="is_active" {% if row.is_active %}checked{% endif %}> Active</label><button class="rounded-lg bg-slate-900 px-3 py-1.5 text-xs text-white">Update</button></form></details></td></tr>{% else %}<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No leave types found.</td></tr>{% endfor %}</tbody></table></div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<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">Employees</h2>
|
||||
<p class="text-sm text-slate-500">Employee master, user linkage, department, designation, reporting manager and status.</p>
|
||||
</div>
|
||||
{% if can_import_employee_hr(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employees/imports" class="inline-flex rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">HR Imports</a>
|
||||
{% endif %}
|
||||
{% if can_manage_employees(current_user, current_user_permissions, current_user_roles) %}
|
||||
<a href="/employees/new" class="inline-flex rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Add Employee</a>
|
||||
{% endif %}
|
||||
</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-[1fr_auto_auto_auto]">
|
||||
<input name="q" value="{{ q or '' }}" placeholder="Search by code, name, email, mobile, PAN, department or designation" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<select name="link_status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm text-slate-700">
|
||||
<option value="all" {% if link_status == 'all' %}selected{% endif %}>All login link status</option>
|
||||
<option value="linked" {% if link_status == 'linked' %}selected{% endif %}>Linked to login user</option>
|
||||
<option value="unlinked" {% if link_status == 'unlinked' %}selected{% endif %}>Not linked to login user</option>
|
||||
</select>
|
||||
<label class="inline-flex items-center gap-2 rounded-xl border border-slate-300 px-3 py-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="include_inactive" value="1" {% if include_inactive %}checked{% endif %}> Include inactive
|
||||
</label>
|
||||
<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>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-5">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Visible Employees</div><div class="mt-1 text-2xl font-semibold">{{ rows|length }}</div></div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-emerald-700">Linked Users</div><div class="mt-1 text-2xl font-semibold text-emerald-800">{{ link_summary.linked if link_summary else 0 }}</div></div>
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-amber-700">Unlinked Employees</div><div class="mt-1 text-2xl font-semibold text-amber-800">{{ link_summary.unlinked if link_summary else 0 }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Active Scope Branch</div><div class="mt-1 text-lg font-semibold">{{ scope.branch_id if scope.branch_id else 'All' }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Access</div><div class="mt-1 text-sm font-semibold">{{ 'Cross Audit Firm' if scope.allow_cross_tenant else 'Audit Firm Scoped' }} / {{ 'Cross Branch' if scope.allow_cross_branch else 'Branch Scoped' }}</div></div>
|
||||
</div>
|
||||
|
||||
{% if link_summary and link_summary.unlinked %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
|
||||
<div class="font-semibold">{{ link_summary.unlinked }} employee(s) are not linked to login users.</div>
|
||||
<p class="mt-1">Employee self-service pages such as My Workspace, attendance, leave, documents and payslips work fully only after the employee master is linked to an IAM user.</p>
|
||||
<a href="/employees?link_status=unlinked{% if include_inactive %}&include_inactive=1{% endif %}" class="mt-2 inline-flex rounded-lg border border-amber-300 px-3 py-1.5 text-xs font-semibold text-amber-900 hover:bg-amber-100">Show unlinked employees</a>
|
||||
</div>
|
||||
{% 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">Employee</th>
|
||||
<th class="px-4 py-3">Department</th>
|
||||
<th class="px-4 py-3">Contact</th>
|
||||
<th class="px-4 py-3">Joining</th>
|
||||
<th class="px-4 py-3">Status</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">
|
||||
<div class="font-semibold text-slate-900">{{ row.full_name }}</div>
|
||||
<div class="text-xs text-slate-500">{{ row.employee_code }}{% if row.user_id %} • User #{{ row.user_id }}{% endif %}</div>
|
||||
{% if row.user_id %}
|
||||
<span class="mt-1 inline-flex rounded-full bg-emerald-50 px-2 py-0.5 text-[11px] font-semibold text-emerald-700">Login linked</span>
|
||||
{% else %}
|
||||
<span class="mt-1 inline-flex rounded-full bg-amber-50 px-2 py-0.5 text-[11px] font-semibold text-amber-700">Login not linked</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-600"><div>{{ row.department or '-' }}</div><div class="text-xs">{{ row.designation or '-' }}</div></td>
|
||||
<td class="px-4 py-3 text-slate-600"><div>{{ row.email or '-' }}</div><div class="text-xs">{{ row.mobile or '-' }}</div></td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ row.date_of_joining or '-' }}</td>
|
||||
<td class="px-4 py-3"><span class="rounded-full px-2 py-1 text-xs font-semibold {% if row.status == 'active' %}bg-emerald-50 text-emerald-700{% elif row.status == 'relieved' %}bg-amber-50 text-amber-700{% else %}bg-slate-100 text-slate-600{% endif %}">{{ row.status.replace('_',' ').title() }}</span></td>
|
||||
<td class="px-4 py-3 text-right"><a href="/employees/{{ row.id }}" class="rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Open</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No employees found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div><h1 class="text-2xl font-semibold text-slate-900">Employee Offboarding</h1><p class="text-sm text-slate-500">Manage resignation, handover and relieving checklist.</p></div>
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm"><h2 class="mb-3 font-semibold">Initiate offboarding</h2><form method="post" id="offboardForm" class="grid gap-3 md:grid-cols-6"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><select name="employee_id" onchange="document.getElementById('offboardForm').action='/employees/'+this.value+'/offboarding/initiate'" required class="rounded-xl border px-3 py-2 text-sm md:col-span-2"><option value="">Select employee</option>{% for e in employees %}<option value="{{ e.id }}">{{ e.employee_code }} - {{ e.full_name }}</option>{% endfor %}</select><select name="request_type" class="rounded-xl border px-3 py-2 text-sm"><option value="resignation">Resignation</option><option value="termination">Termination</option><option value="contract_end">Contract End</option></select><input type="date" name="requested_relieving_date" class="rounded-xl border px-3 py-2 text-sm"><input name="reason" placeholder="Reason" class="rounded-xl border px-3 py-2 text-sm md:col-span-2"><textarea name="handover_notes" placeholder="Handover notes" class="rounded-xl border px-3 py-2 text-sm md:col-span-5"></textarea><button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Create Request</button></form></div>
|
||||
<div class="overflow-hidden rounded-2xl border bg-white shadow-sm"><table class="min-w-full divide-y text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Employee</th><th class="p-3">Requested Date</th><th class="p-3">Status</th><th class="p-3 text-left">Review / Completion</th></tr></thead><tbody class="divide-y">
|
||||
{% for r in rows %}<tr><td class="p-3"><div class="font-medium">{{ r.employee.full_name if r.employee else r.employee_id }}</div><div class="text-xs text-slate-500">{{ r.request_type }}{% if r.reason %} · {{ r.reason }}{% endif %}</div></td><td class="p-3 text-center">{{ r.requested_relieving_date or '-' }}</td><td class="p-3 text-center"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs">{{ r.status }}</span></td><td class="p-3 space-y-2">{% if r.status in ['pending','approved'] %}<form method="post" action="/employees/offboarding/{{ r.id }}/review" class="flex flex-wrap gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><select name="status" class="rounded-lg border px-2 py-1"><option value="approved">Approve</option><option value="rejected">Reject</option></select><input type="date" name="approved_relieving_date" value="{{ r.approved_relieving_date or r.requested_relieving_date or '' }}" class="rounded-lg border px-2 py-1"><input name="review_notes" placeholder="Review notes" class="rounded-lg border px-2 py-1"><button class="rounded-lg bg-blue-700 px-3 py-1 text-white">Submit</button></form>{% endif %}{% if r.status == 'approved' %}<form method="post" action="/employees/offboarding/{{ r.id }}/complete"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="rounded-lg bg-emerald-700 px-3 py-1 text-white">Complete Final Offboarding</button></form>{% endif %}</td></tr>{% else %}<tr><td colspan="4" class="p-6 text-center text-slate-500">No offboarding requests.</td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div><h1 class="text-2xl font-semibold text-slate-900">Onboarding Checklist</h1><p class="text-sm text-slate-500">Branch-wise reusable joining checklist items.</p></div>
|
||||
<form method="post" action="/employees/onboarding-checklist/defaults"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Create Defaults</button></form>
|
||||
</div>
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm"><h2 class="mb-3 font-semibold">Add checklist item</h2>
|
||||
<form method="post" action="/employees/onboarding-checklist" class="grid gap-3 md:grid-cols-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input name="code" required placeholder="Code" class="rounded-xl border px-3 py-2 text-sm">
|
||||
<input name="title" required placeholder="Title" class="rounded-xl border px-3 py-2 text-sm md:col-span-2">
|
||||
<input name="stage" value="joining" class="rounded-xl border px-3 py-2 text-sm">
|
||||
<input name="default_due_days" type="number" value="0" class="rounded-xl border px-3 py-2 text-sm">
|
||||
<input name="sort_order" type="number" value="0" class="rounded-xl border px-3 py-2 text-sm">
|
||||
<textarea name="description" placeholder="Description" class="rounded-xl border px-3 py-2 text-sm md:col-span-4"></textarea>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_mandatory" checked> Mandatory</label>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" checked> Active</label>
|
||||
<button class="rounded-xl bg-blue-700 px-4 py-2 text-sm font-semibold text-white md:col-span-6">Add Item</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="overflow-hidden rounded-2xl border bg-white shadow-sm"><table class="min-w-full divide-y text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Code</th><th class="p-3 text-left">Title</th><th class="p-3">Stage</th><th class="p-3">Due Days</th><th class="p-3">Status</th><th class="p-3 text-left">Update</th></tr></thead><tbody class="divide-y">
|
||||
{% for r in rows %}<tr><form method="post" action="/employees/onboarding-checklist/{{ r.id }}/edit"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><td class="p-3 font-medium">{{ r.code }}</td><td class="p-3"><input name="title" value="{{ r.title }}" class="w-full rounded-lg border px-2 py-1"><input type="hidden" name="code" value="{{ r.code }}"><textarea name="description" class="mt-2 w-full rounded-lg border px-2 py-1">{{ r.description or '' }}</textarea></td><td class="p-3"><input name="stage" value="{{ r.stage }}" class="w-28 rounded-lg border px-2 py-1"></td><td class="p-3"><input name="default_due_days" type="number" value="{{ r.default_due_days }}" class="w-20 rounded-lg border px-2 py-1"><input name="sort_order" type="hidden" value="{{ r.sort_order }}"></td><td class="p-3"><label><input type="checkbox" name="is_mandatory" {% if r.is_mandatory %}checked{% endif %}> Mandatory</label><br><label><input type="checkbox" name="is_active" {% if r.is_active %}checked{% endif %}> Active</label></td><td class="p-3"><button class="rounded-lg bg-slate-900 px-3 py-1 text-white">Save</button></td></form></tr>{% else %}<tr><td colspan="6" class="p-6 text-center text-slate-500">No checklist items yet.</td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"><div><h1 class="text-2xl font-semibold text-slate-900">Employee Onboarding</h1><p class="text-sm text-slate-500">Generate and track joining checklist tasks.</p></div><a href="/employees/onboarding-checklist" class="rounded-xl border px-4 py-2 text-sm font-semibold">Checklist Master</a></div>
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm"><form method="get" action="/employees/onboarding" class="flex flex-wrap gap-3"><select name="employee_id" class="rounded-xl border px-3 py-2 text-sm"><option value="">All employees</option>{% for e in employees %}<option value="{{ e.id }}" {% if selected_employee_id==e.id %}selected{% endif %}>{{ e.employee_code }} - {{ e.full_name }}</option>{% endfor %}</select><button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Filter</button></form></div>
|
||||
<div class="overflow-hidden rounded-2xl border bg-white shadow-sm"><table class="min-w-full divide-y text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Employee</th><th class="p-3 text-left">Task</th><th class="p-3">Due</th><th class="p-3">Status</th><th class="p-3 text-left">Action</th></tr></thead><tbody class="divide-y">
|
||||
{% for r in rows %}<tr><td class="p-3">{{ r.employee.full_name if r.employee else r.employee_id }}</td><td class="p-3"><div class="font-medium">{{ r.title }}</div><div class="text-xs text-slate-500">{{ r.stage }}{% if r.description %} · {{ r.description }}{% endif %}</div></td><td class="p-3 text-center">{{ r.due_date or '-' }}</td><td class="p-3 text-center"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs">{{ r.status }}</span></td><td class="p-3"><form method="post" action="/employees/onboarding/{{ r.id }}/status" class="flex flex-wrap gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><select name="status" class="rounded-lg border px-2 py-1">{% for st in onboarding_task_statuses %}<option value="{{ st }}" {% if r.status==st %}selected{% endif %}>{{ st }}</option>{% endfor %}</select><input name="review_notes" placeholder="Notes" class="rounded-lg border px-2 py-1"><button class="rounded-lg bg-blue-700 px-3 py-1 text-white">Update</button></form></td></tr>{% else %}<tr><td colspan="5" class="p-6 text-center text-slate-500">No onboarding tasks. Open an employee detail page and click Generate Onboarding.</td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<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">Payroll Runs</h2><p class="text-sm text-slate-500">Create monthly payroll run, generate payslips, approve and mark paid.</p></div><a href="/employees/payroll/payslips" class="rounded-xl border px-4 py-2 text-sm font-semibold">Payslips</a></div>
|
||||
{% if scope.branch_id is none %}<div class="rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">Please select a branch context before creating payroll run.</div>{% endif %}
|
||||
<form method="post" action="/employees/payroll/runs" class="grid gap-3 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm md:grid-cols-5">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input name="pay_year" type="number" min="2020" max="2100" value="{{ now().year if now is defined else 2026 }}" required class="rounded-xl border px-3 py-2 text-sm"><input name="pay_month" type="number" min="1" max="12" required placeholder="Month" class="rounded-xl border px-3 py-2 text-sm"><input name="notes" placeholder="Notes" class="rounded-xl border px-3 py-2 text-sm md:col-span-2"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Create Run</button>
|
||||
</form>
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"><table class="min-w-full divide-y divide-slate-200 text-sm"><thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left">Period</th><th class="px-4 py-3 text-left">Status</th><th class="px-4 py-3 text-right">Employees</th><th class="px-4 py-3 text-right">Gross</th><th class="px-4 py-3 text-right">Deduction</th><th class="px-4 py-3 text-right">Net</th><th class="px-4 py-3 text-right">Actions</th></tr></thead><tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}<tr><td class="px-4 py-3 font-medium">{{ '%02d' % row.pay_month }}/{{ row.pay_year }}</td><td class="px-4 py-3">{{ row.status.title() }}</td><td class="px-4 py-3 text-right">{{ row.total_employees }}</td><td class="px-4 py-3 text-right">{{ row.gross_amount }}</td><td class="px-4 py-3 text-right">{{ row.deduction_amount }}</td><td class="px-4 py-3 text-right font-semibold">{{ row.net_amount }}</td><td class="px-4 py-3"><div class="flex justify-end gap-2"><form method="post" action="/employees/payroll/runs/{{ row.id }}/generate"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="rounded-lg border px-2 py-1 text-xs">Generate</button></form>{% if row.status == 'generated' %}<form method="post" action="/employees/payroll/runs/{{ row.id }}/approve"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="rounded-lg bg-emerald-600 px-2 py-1 text-xs text-white">Approve</button></form>{% endif %}{% if row.status == 'approved' %}<form method="post" action="/employees/payroll/runs/{{ row.id }}/paid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="rounded-lg bg-indigo-600 px-2 py-1 text-xs text-white">Paid</button></form>{% endif %}<a href="/employees/payroll/payslips?payroll_run_id={{ row.id }}" class="rounded-lg border px-2 py-1 text-xs">View</a></div></td></tr>{% else %}<tr><td colspan="7" class="px-4 py-6 text-center text-slate-500">No payroll runs yet.</td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<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">Salary Structures</h2><p class="text-sm text-slate-500">Maintain branch-wise employee salary structures for payroll generation.</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>
|
||||
{% if errors %}<div class="rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700">{{ errors|join(', ') }}</div>{% endif %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h3 class="font-semibold text-slate-900">Add Salary Structure</h3>
|
||||
<form method="post" action="/employees/payroll/structures" class="mt-4 grid gap-3 md:grid-cols-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<select name="employee_id" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm md:col-span-2"><option value="">Select Employee</option>{% for emp in employees %}<option value="{{ emp.id }}">{{ emp.employee_code }} - {{ emp.full_name }}</option>{% endfor %}</select>
|
||||
<input type="date" name="effective_from" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input type="date" name="effective_to" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="monthly_ctc_amount" type="number" min="0" step="1" placeholder="Monthly CTC" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="basic_amount" type="number" min="0" step="1" placeholder="Basic" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="hra_amount" type="number" min="0" step="1" placeholder="HRA" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="allowance_amount" type="number" min="0" step="1" placeholder="Other Allowance" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="employee_pf_amount" type="number" min="0" step="1" placeholder="PF" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="employee_esi_amount" type="number" min="0" step="1" placeholder="ESI" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="professional_tax_amount" type="number" min="0" step="1" placeholder="Professional Tax" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="tds_amount" type="number" min="0" step="1" placeholder="TDS" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="other_deduction_amount" type="number" min="0" step="1" placeholder="Other Deduction" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="remarks" placeholder="Remarks" class="rounded-xl border border-slate-300 px-3 py-2 text-sm md:col-span-2">
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" checked> Active</label>
|
||||
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Save Structure</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm"><thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left">Employee</th><th class="px-4 py-3 text-left">Effective</th><th class="px-4 py-3 text-right">Gross</th><th class="px-4 py-3 text-right">Deductions</th><th class="px-4 py-3 text-right">Net</th><th class="px-4 py-3 text-left">Status</th></tr></thead><tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}{% set gross = row.basic_amount + row.hra_amount + row.allowance_amount %}{% set ded = row.employee_pf_amount + row.employee_esi_amount + row.professional_tax_amount + row.tds_amount + row.other_deduction_amount %}<tr><td class="px-4 py-3 font-medium">{{ row.employee.employee_code if row.employee else row.employee_id }} - {{ row.employee.full_name if row.employee else '' }}</td><td class="px-4 py-3">{{ row.effective_from }}{% if row.effective_to %} to {{ row.effective_to }}{% endif %}</td><td class="px-4 py-3 text-right">{{ gross }}</td><td class="px-4 py-3 text-right">{{ ded }}</td><td class="px-4 py-3 text-right font-semibold">{{ gross - ded }}</td><td class="px-4 py-3">{{ 'Active' if row.is_active else 'Inactive' }}</td></tr>{% else %}<tr><td colspan="6" class="px-4 py-6 text-center text-slate-500">No salary structures available.</td></tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<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 Payslips</h2><p class="text-sm text-slate-500">Review generated payroll slips for employees.</p></div><a href="/employees/payroll/runs" class="rounded-xl border px-4 py-2 text-sm font-semibold">Payroll Runs</a></div>
|
||||
<form method="get" class="grid gap-3 rounded-2xl border border-slate-200 bg-white p-4 md:grid-cols-4"><select name="payroll_run_id" class="rounded-xl border px-3 py-2 text-sm"><option value="">All Runs</option>{% for run in runs %}<option value="{{ run.id }}" {% if selected_run_id == run.id %}selected{% endif %}>{{ '%02d' % run.pay_month }}/{{ run.pay_year }} - {{ run.status }}</option>{% endfor %}</select><select name="employee_id" class="rounded-xl border 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.employee_code }} - {{ emp.full_name }}</option>{% endfor %}</select><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Filter</button></form>
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"><table class="min-w-full divide-y divide-slate-200 text-sm"><thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left">Period</th><th class="px-4 py-3 text-left">Employee</th><th class="px-4 py-3 text-right">Gross</th><th class="px-4 py-3 text-right">Deduction</th><th class="px-4 py-3 text-right">Net</th><th class="px-4 py-3 text-left">Status</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in rows %}<tr><td class="px-4 py-3">{{ '%02d' % row.pay_month }}/{{ row.pay_year }}</td><td class="px-4 py-3 font-medium">{{ row.employee.employee_code if row.employee else row.employee_id }} - {{ row.employee.full_name if row.employee else '' }}</td><td class="px-4 py-3 text-right">{{ row.gross_amount }}</td><td class="px-4 py-3 text-right">{{ row.deduction_amount }}</td><td class="px-4 py-3 text-right font-semibold">{{ row.net_amount }}</td><td class="px-4 py-3">{{ row.status.title() }}</td></tr>{% else %}<tr><td colspan="6" class="px-4 py-6 text-center text-slate-500">No payslips available.</td></tr>{% endfor %}</tbody></table></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,148 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
|
||||
<section class="overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-soft">
|
||||
<div class="bg-gradient-to-r from-brand-700 via-brand-600 to-slate-900 px-6 py-6 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-brand-100">My Workspace</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold">Today’s work, attendance and personal profile</h2>
|
||||
<p class="mt-2 max-w-3xl text-sm text-brand-100">Use this dashboard for your own assignments, attendance, leave, documents, payslips and alerts.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if employee %}
|
||||
<a href="/employee/work" class="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-brand-700 hover:bg-brand-50">Open My Work Board</a>
|
||||
<a href="/employee/attendance" class="rounded-xl border border-white/40 px-4 py-2 text-sm font-semibold text-white hover:bg-white/10">Punch / Attendance</a>
|
||||
{% else %}
|
||||
<a href="/employee/register" class="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-brand-700 hover:bg-brand-50">Request Employee Linkage</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if employee %}
|
||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-6">
|
||||
<a href="/employee/work" class="af-metric-card hover:border-brand-200">
|
||||
<div class="text-xs font-semibold uppercase text-slate-500">Open Work</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ work_payload.summary.open if work_payload else 0 }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Assigned to me</div>
|
||||
</a>
|
||||
<a href="/employee/work?status=in_progress" class="af-metric-card hover:border-brand-200">
|
||||
<div class="text-xs font-semibold uppercase text-slate-500">In Progress</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-brand-700">{{ work_payload.summary.in_progress if work_payload else 0 }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Currently active</div>
|
||||
</a>
|
||||
<a href="/employee/work?status=blocked" class="af-metric-card border-amber-200 bg-amber-50 hover:border-amber-300">
|
||||
<div class="text-xs font-semibold uppercase text-amber-700">Blocked</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-amber-700">{{ work_payload.summary.blocked if work_payload else 0 }}</div>
|
||||
<div class="mt-1 text-xs text-amber-700">Need clarification</div>
|
||||
</a>
|
||||
<a href="/employee/work?date_bucket=overdue" class="af-metric-card border-red-200 bg-red-50 hover:border-red-300">
|
||||
<div class="text-xs font-semibold uppercase text-red-700">Overdue</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-red-700">{{ work_payload.summary.overdue if work_payload else 0 }}</div>
|
||||
<div class="mt-1 text-xs text-red-700">Immediate action</div>
|
||||
</a>
|
||||
<a href="/employee/attendance" class="af-metric-card hover:border-brand-200">
|
||||
<div class="text-xs font-semibold uppercase text-slate-500">Attendance</div>
|
||||
<div class="mt-2 text-lg font-semibold text-slate-900">{% if today_attendance %}Marked{% else %}Not Marked{% endif %}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Today</div>
|
||||
</a>
|
||||
<a href="/employee/payslips" class="af-metric-card hover:border-brand-200">
|
||||
<div class="text-xs font-semibold uppercase text-slate-500">Payslips</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ payslips|length }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Available slips</div>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_380px]">
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">My Assignment Board Snapshot</h3>
|
||||
<p class="mt-1 text-sm text-slate-600">Your work is grouped by practical status so you can start from urgent items first.</p>
|
||||
</div>
|
||||
<a href="/employee/work" class="af-btn af-btn-primary">Open My Work Board</a>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-3 md:grid-cols-4">
|
||||
{% if work_payload %}
|
||||
{% for column in work_payload.columns %}
|
||||
<a href="/employee/work?status={{ column.code }}" class="rounded-2xl border border-slate-200 bg-slate-50 p-4 transition hover:border-brand-300 hover:bg-brand-50/50">
|
||||
<div class="text-sm font-semibold text-slate-900">{{ column.label }}</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ column.cards|length }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">assignment card(s)</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500 md:col-span-4">No work board data available yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Leave & Availability</h3>
|
||||
<p class="mt-1 text-sm text-slate-600">Balances and recent leave requests for your profile.</p>
|
||||
</div>
|
||||
<a href="/employee/leave" class="af-btn af-btn-secondary">Open Leave</a>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-3 md:grid-cols-3">
|
||||
{% for bal in leave_balances %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="text-xs font-semibold uppercase text-slate-500">{{ bal.leave_type.name if bal.leave_type else bal.leave_type_id }}</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ bal.balance_days }}</div>
|
||||
<div class="text-xs text-slate-500">days balance</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500 md:col-span-3">No leave balance available yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-900">Today’s Attendance</h3>
|
||||
<p class="mt-1 text-sm text-slate-600">{% if today_attendance %}Punch In: {{ today_attendance.punch_in_utc.strftime('%H:%M') if today_attendance.punch_in_utc else '-' }} · Punch Out: {{ today_attendance.punch_out_utc.strftime('%H:%M') if today_attendance.punch_out_utc else '-' }}{% else %}Attendance not marked for today.{% endif %}</p>
|
||||
</div>
|
||||
<a href="/employee/attendance" class="af-btn af-btn-secondary">Open</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<h3 class="font-semibold text-slate-900">Profile Summary</h3>
|
||||
<dl class="mt-4 grid gap-4 text-sm">
|
||||
<div><dt class="text-slate-500">Name</dt><dd class="font-medium text-slate-900">{{ employee.full_name }}</dd></div>
|
||||
<div><dt class="text-slate-500">Code / Status</dt><dd class="font-medium text-slate-900">{{ employee.employee_code }} · {{ employee.status.replace('_',' ').title() }}</dd></div>
|
||||
<div><dt class="text-slate-500">Designation</dt><dd class="font-medium text-slate-900">{{ employee.designation or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Email</dt><dd class="font-medium text-slate-900">{{ employee.email or current_user.email }}</dd></div>
|
||||
<div><dt class="text-slate-500">Mobile</dt><dd class="font-medium text-slate-900">{{ employee.mobile or current_user.mobile or '-' }}</dd></div>
|
||||
</dl>
|
||||
<a href="/employee/profile" class="mt-5 inline-flex af-btn af-btn-secondary">Update My Profile</a>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="font-semibold text-slate-900">Quick Links</h3>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-2 text-sm">
|
||||
<a href="/employee/documents" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">My Documents</a>
|
||||
<a href="/employee/payslips" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">My Payslips</a>
|
||||
<a href="/alerts" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">My Alert</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
{% elif pending_request %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-900 shadow-soft"><h3 class="font-semibold">Employee profile request is pending</h3><p class="mt-1">Your request #{{ pending_request.id }} is waiting for approval.</p></div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-6 shadow-soft"><h3 class="font-semibold text-amber-900">Your login is not linked to an employee master</h3><p class="mt-1 text-sm text-amber-900">Submit an employee link request or ask Firm Admin/Partner/Branch Manager to link your user account from Employee Master.</p><a href="/employee/register" class="mt-4 inline-flex rounded-xl bg-amber-700 px-4 py-2 text-sm font-semibold text-white hover:bg-amber-800">Request Employee Link</a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,105 @@
|
||||
{% 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-start justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Engagement Progress</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Client-wise and engagement-wise progress based on existing service task instances.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-4 xl:grid-cols-8">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Clients</div><div class="mt-1 text-2xl font-semibold">{{ progress_payload.summary.clients }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Engagements</div><div class="mt-1 text-2xl font-semibold">{{ progress_payload.summary.engagements }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Tasks</div><div class="mt-1 text-2xl font-semibold">{{ progress_payload.summary.tasks }}</div></div>
|
||||
<div class="rounded-2xl border border-blue-200 bg-blue-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-blue-600">Avg Progress</div><div class="mt-1 text-2xl font-semibold text-blue-700">{{ progress_payload.summary.average_progress }}%</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Open</div><div class="mt-1 text-2xl font-semibold">{{ progress_payload.summary.open }}</div></div>
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-red-600">Overdue</div><div class="mt-1 text-2xl font-semibold text-red-700">{{ progress_payload.summary.overdue }}</div></div>
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-amber-600">Due Today</div><div class="mt-1 text-2xl font-semibold text-amber-700">{{ progress_payload.summary.due_today }}</div></div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-emerald-600">Completed</div><div class="mt-1 text-2xl font-semibold text-emerald-700">{{ progress_payload.summary.completed }}</div></div>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/employees/progress" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="grid gap-3 md:grid-cols-[1fr_180px_240px_auto]">
|
||||
<input type="search" name="q" value="{{ q or '' }}" placeholder="Search client, engagement or task" class="rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||||
<select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="open" {% if status == 'open' %}selected{% endif %}>Open engagements</option>
|
||||
<option value="all" {% if status == 'all' %}selected{% endif %}>All tasks</option>
|
||||
<option value="overdue" {% if status == 'overdue' %}selected{% endif %}>Overdue only</option>
|
||||
<option value="due_today" {% if status == 'due_today' %}selected{% endif %}>Due today</option>
|
||||
<option value="closed" {% if status == 'closed' %}selected{% endif %}>Closed tasks</option>
|
||||
<option value="pending" {% if status == 'pending' %}selected{% endif %}>Pending</option>
|
||||
<option value="in_progress" {% if status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
||||
<option value="blocked" {% if status == 'blocked' %}selected{% endif %}>Blocked</option>
|
||||
<option value="completed" {% if status == 'completed' %}selected{% endif %}>Completed</option>
|
||||
</select>
|
||||
<select name="assigned_to_user_id" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="0">All assignees</option>
|
||||
{% for user in assignable_users %}
|
||||
<option value="{{ user.id }}" {% if selected_assigned_to_user_id == user.id %}selected{% endif %}>{{ user.full_name or user.email }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="space-y-4">
|
||||
{% for client_group in progress_payload.clients %}
|
||||
<details class="group rounded-2xl border border-slate-200 bg-white shadow-soft" {% if loop.first %}open{% endif %}>
|
||||
<summary class="flex cursor-pointer list-none flex-wrap items-center justify-between gap-4 rounded-2xl px-5 py-4 hover:bg-slate-50">
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-slate-900">{{ client_group.client_name }}</div>
|
||||
<div class="text-xs text-slate-500">{{ client_group.client_code or 'No client code' }} · {{ client_group.engagements|length }} engagement{{ '' if client_group.engagements|length == 1 else 's' }} · {{ client_group.task_count }} task{{ '' if client_group.task_count == 1 else 's' }}</div>
|
||||
</div>
|
||||
<div class="min-w-[240px]">
|
||||
<div class="flex justify-between text-xs font-semibold text-slate-500"><span>Client progress</span><span>{{ client_group.progress_percent }}%</span></div>
|
||||
<div class="mt-1 h-2 rounded-full bg-slate-100"><div class="h-2 rounded-full bg-brand-600" style="width: {{ client_group.progress_percent }}%"></div></div>
|
||||
<div class="mt-1 text-xs text-slate-500">Open {{ client_group.open_count }} · Completed {{ client_group.completed_count }} · Overdue {{ client_group.overdue_count }}</div>
|
||||
</div>
|
||||
<span class="text-lg text-slate-400 transition group-open:rotate-90">›</span>
|
||||
</summary>
|
||||
|
||||
<div class="space-y-3 border-t border-slate-100 p-4">
|
||||
{% for engagement_group in client_group.engagements %}
|
||||
<details class="group/eng rounded-xl border border-slate-200 bg-slate-50" {% if engagement_group.overdue_count > 0 or loop.first %}open{% endif %}>
|
||||
<summary class="flex cursor-pointer list-none flex-wrap items-center justify-between gap-4 px-4 py-3 hover:bg-slate-100">
|
||||
<div>
|
||||
<div class="font-semibold text-slate-900">{{ engagement_group.label }}</div>
|
||||
<div class="text-xs text-slate-500">Status: {{ engagement_group.status.replace('_',' ').title() }} · Due: {{ engagement_group.due_date or '-' }}</div>
|
||||
</div>
|
||||
<div class="min-w-[260px]">
|
||||
<div class="flex justify-between text-xs font-semibold text-slate-500"><span>{{ engagement_group.progress_status.replace('_',' ').title() }}</span><span>{{ engagement_group.progress_percent }}%</span></div>
|
||||
<div class="mt-1 h-2 rounded-full bg-slate-200"><div class="h-2 rounded-full {% if engagement_group.progress_status == 'completed' %}bg-emerald-600{% elif engagement_group.progress_status == 'overdue' %}bg-red-600{% else %}bg-brand-600{% endif %}" style="width: {{ engagement_group.progress_percent }}%"></div></div>
|
||||
<div class="mt-1 text-xs text-slate-500">Open {{ engagement_group.open_count }} · Completed {{ engagement_group.completed_count }} · Unassigned {{ engagement_group.unassigned_count }}</div>
|
||||
</div>
|
||||
<span class="text-slate-400 transition group-open/eng:rotate-90">›</span>
|
||||
</summary>
|
||||
<div class="overflow-x-auto border-t border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-100 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
<tr><th class="px-4 py-3">Task</th><th class="px-4 py-3">Assignee</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Priority</th><th class="px-4 py-3">Target</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
{% for task in engagement_group.tasks %}
|
||||
<tr class="{% if task.is_overdue %}bg-red-50{% elif task.is_due_today %}bg-amber-50{% endif %}">
|
||||
<td class="px-4 py-3"><div class="font-medium text-slate-900">{{ task.task_name }}</div>{% if task.description %}<div class="mt-1 text-xs text-slate-500">{{ task.description }}</div>{% endif %}</td>
|
||||
<td class="px-4 py-3">{{ task.assigned_to.full_name if task.assigned_to else 'Unassigned' }}</td>
|
||||
<td class="px-4 py-3"><span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-700">{{ task.status_label }}</span></td>
|
||||
<td class="px-4 py-3">{{ task.priority_label }}</td>
|
||||
<td class="px-4 py-3">{{ task.internal_target_date or '-' }}<div class="text-xs text-slate-500">{{ task.date_bucket }}</div></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-soft"><h3 class="font-semibold text-slate-900">No progress data found</h3><p class="mt-1 text-sm text-slate-500">No engagement task matches the selected filter.</p></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-3xl space-y-6">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Request Employee Profile</h2>
|
||||
<p class="text-sm text-slate-500">Use this when your login user is not yet linked to an employee master.</p>
|
||||
</div>
|
||||
<a href="/employee/dashboard" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back</a>
|
||||
</div>
|
||||
|
||||
{% if employee %}
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Your user is already linked to employee {{ employee.employee_code }}.</div>
|
||||
{% elif pending_request %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">Your request #{{ pending_request.id }} is already pending approval.</div>
|
||||
{% else %}
|
||||
{% if errors %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800"><ul class="list-disc pl-5">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul></div>{% endif %}
|
||||
{% set f = form if form is defined else {} %}
|
||||
<form method="post" class="space-y-5 rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Requested Employee Code</label><input name="requested_employee_code" value="{{ f.get('requested_employee_code','') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"><p class="mt-1 text-xs text-slate-500">Optional. Admin may approve with another code.</p></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Full Name *</label><input name="full_name" value="{{ f.get('full_name') or current_user.full_name or '' }}" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Email</label><input type="email" name="email" value="{{ f.get('email') or current_user.email or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Mobile</label><input name="mobile" value="{{ f.get('mobile','') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Department</label><input name="department" value="{{ f.get('department','') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Designation</label><input name="designation" value="{{ f.get('designation','') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Date of Joining</label><input type="date" name="date_of_joining" value="{{ f.get('date_of_joining','') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div class="md:col-span-2"><label class="mb-1 block text-sm font-medium text-slate-700">Remarks</label><textarea name="remarks" rows="3" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ f.get('remarks','') }}</textarea></div>
|
||||
</div>
|
||||
<div class="flex justify-end"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Submit Request</button></div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-4xl space-y-6">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Employee Registration Request #{{ item.id }}</h2>
|
||||
<p class="text-sm text-slate-500">Review and approve/reject employee self-service linkage request.</p>
|
||||
</div>
|
||||
<a href="/employees/registration-requests" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back</a>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Request Details</h3>
|
||||
<dl class="mt-4 grid gap-4 md:grid-cols-2 text-sm">
|
||||
<div><dt class="text-slate-500">Full Name</dt><dd class="font-medium text-slate-900">{{ item.full_name }}</dd></div>
|
||||
<div><dt class="text-slate-500">Requested Code</dt><dd class="font-medium text-slate-900">{{ item.requested_employee_code or 'Auto generate' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Email</dt><dd class="font-medium text-slate-900">{{ item.email or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Mobile</dt><dd class="font-medium text-slate-900">{{ item.mobile or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Department</dt><dd class="font-medium text-slate-900">{{ item.department or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Designation</dt><dd class="font-medium text-slate-900">{{ item.designation or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Status</dt><dd class="font-medium text-slate-900">{{ item.status.title() }}</dd></div>
|
||||
<div><dt class="text-slate-500">User ID</dt><dd class="font-medium text-slate-900">{{ item.user_id }}</dd></div>
|
||||
<div class="md:col-span-2"><dt class="text-slate-500">Remarks</dt><dd class="whitespace-pre-line font-medium text-slate-900">{{ item.remarks or '-' }}</dd></div>
|
||||
{% if item.review_notes %}<div class="md:col-span-2"><dt class="text-slate-500">Review Notes</dt><dd class="whitespace-pre-line font-medium text-slate-900">{{ item.review_notes }}</dd></div>{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{% if item.status == 'pending' %}
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<form method="post" action="/employees/registration-requests/{{ item.id }}/approve" class="space-y-4 rounded-2xl border border-emerald-200 bg-emerald-50 p-6 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<h3 class="font-semibold text-emerald-900">Approve Request</h3>
|
||||
<div><label class="mb-1 block text-sm font-medium text-emerald-900">Employee Code</label><input name="employee_code" value="{{ item.requested_employee_code or '' }}" placeholder="Leave blank to auto-generate" class="w-full rounded-xl border border-emerald-200 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-emerald-900">Review Notes</label><textarea name="review_notes" rows="3" class="w-full rounded-xl border border-emerald-200 px-3 py-2 text-sm"></textarea></div>
|
||||
<button class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700">Approve & Create Employee</button>
|
||||
</form>
|
||||
<form method="post" action="/employees/registration-requests/{{ item.id }}/reject" class="space-y-4 rounded-2xl border border-red-200 bg-red-50 p-6 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<h3 class="font-semibold text-red-900">Reject Request</h3>
|
||||
<div><label class="mb-1 block text-sm font-medium text-red-900">Reason</label><textarea name="review_notes" rows="5" class="w-full rounded-xl border border-red-200 px-3 py-2 text-sm"></textarea></div>
|
||||
<button class="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700">Reject Request</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<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 Registration Requests</h2>
|
||||
<p class="text-sm text-slate-500">Approve self-service employee profile requests and create linked employee masters.</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="flex flex-wrap items-end gap-3">
|
||||
<div><label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Status</label><select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><option value="all" {% if status == 'all' %}selected{% endif %}>All</option>{% for st in statuses %}<option value="{{ st }}" {% if status == st %}selected{% endif %}>{{ st.title() }}</option>{% endfor %}</select></div>
|
||||
<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>
|
||||
|
||||
<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">Request</th><th class="px-4 py-3">User</th><th class="px-4 py-3">Role Details</th><th class="px-4 py-3">Status</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"><div class="font-semibold text-slate-900">#{{ row.id }} {{ row.full_name }}</div><div class="text-xs text-slate-500">Requested Code: {{ row.requested_employee_code or 'Auto' }} • {{ row.created_at_utc }}</div></td>
|
||||
<td class="px-4 py-3 text-slate-600"><div>{{ row.email or '-' }}</div><div class="text-xs">User #{{ row.user_id }}</div></td>
|
||||
<td class="px-4 py-3 text-slate-600"><div>{{ row.department or '-' }}</div><div class="text-xs">{{ row.designation or '-' }}</div></td>
|
||||
<td class="px-4 py-3"><span class="rounded-full px-2 py-1 text-xs font-semibold {% if row.status == 'pending' %}bg-amber-50 text-amber-700{% elif row.status == 'approved' %}bg-emerald-50 text-emerald-700{% else %}bg-red-50 text-red-700{% endif %}">{{ row.status.title() }}</span></td>
|
||||
<td class="px-4 py-3 text-right"><a href="/employees/registration-requests/{{ row.id }}" class="rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Open</a></td>
|
||||
</tr>
|
||||
{% else %}<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">No registration requests found.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,248 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">My Attendance</h2>
|
||||
<p class="text-sm text-slate-500">Punch in/out and view your recent attendance records. Times are documented in your branch timezone.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% for error in errors or [] %}
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ error }}</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not employee %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-900 shadow-soft">
|
||||
Your login is not linked to an employee profile yet. Please request employee linkage first.
|
||||
<div class="mt-3"><a href="/employee/register" class="font-semibold underline">Request employee profile</a></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="text-xs font-semibold uppercase text-slate-500">Today</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ today_attendance.attendance_date if today_attendance else 'Not marked' }}</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="text-xs font-semibold uppercase text-slate-500">Punch In</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ today_attendance.punch_in_local_at.strftime('%H:%M') if today_attendance and today_attendance.punch_in_local_at else (today_attendance.punch_in_utc.strftime('%H:%M UTC') if today_attendance and today_attendance.punch_in_utc else '-') }}</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="text-xs font-semibold uppercase text-slate-500">Punch Out</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ today_attendance.punch_out_local_at.strftime('%H:%M') if today_attendance and today_attendance.punch_out_local_at else (today_attendance.punch_out_utc.strftime('%H:%M UTC') if today_attendance and today_attendance.punch_out_utc else '-') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Today's Action</h3>
|
||||
<p class="mt-1 text-xs text-slate-500">Location will be captured when your browser allows it. If you are outside the branch geofence, the attendance is saved as pending approval for OD/client visit review.</p>
|
||||
<div id="geo-status" class="mt-3 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">Location not captured yet.</div>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<form method="post" action="/employee/attendance/punch-in" class="attendance-geo-form space-y-3 rounded-xl border border-slate-200 p-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="latitude">
|
||||
<input type="hidden" name="longitude">
|
||||
<input type="hidden" name="accuracy_meters">
|
||||
<label class="block text-sm font-medium text-slate-700">Remarks / OD Reason</label>
|
||||
<input name="remarks" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Optional; required in practice for client visit/OD">
|
||||
<button class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-50" {% if today_attendance and today_attendance.punch_in_utc %}disabled{% endif %}>Punch In</button>
|
||||
</form>
|
||||
<form method="post" action="/employee/attendance/punch-out" class="attendance-geo-form space-y-3 rounded-xl border border-slate-200 p-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="latitude">
|
||||
<input type="hidden" name="longitude">
|
||||
<input type="hidden" name="accuracy_meters">
|
||||
<label class="block text-sm font-medium text-slate-700">Remarks / OD Reason</label>
|
||||
<input name="remarks" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Optional remarks">
|
||||
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-50" {% if not today_attendance or not today_attendance.punch_in_utc or today_attendance.punch_out_utc %}disabled{% endif %}>Punch Out</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Punch In</th>
|
||||
<th class="px-4 py-3">Punch Out</th>
|
||||
<th class="px-4 py-3">Duration</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Approval</th>
|
||||
<th class="px-4 py-3">Timing Rule</th>
|
||||
<th class="px-4 py-3">Geo/IP</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">{{ 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 '-') }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ 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 '-') }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ (row.work_duration_minutes ~ ' min') if row.work_duration_minutes else '-' }}</td>
|
||||
<td class="px-4 py-3">{{ row.status.replace('_',' ').title() }}</td>
|
||||
<td class="px-4 py-3">{{ row.approval_status.replace('_',' ').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>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">
|
||||
<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>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="8" class="px-4 py-8 text-center text-slate-500">No attendance records found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="location-consent-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-slate-900/60 p-4" aria-hidden="true">
|
||||
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-100 text-emerald-700">📍</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Enable location for attendance</h3>
|
||||
<p class="mt-2 text-sm text-slate-600">
|
||||
To mark attendance, this app needs your current location to verify whether you are within your branch geofence.
|
||||
Click Continue, then choose <span class="font-semibold text-slate-900">Allow</span> in the browser location popup.
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
If you are outside the branch or location is denied/unavailable, your attendance can still be saved as pending approval for OD/client visit review.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<button type="button" id="location-modal-cancel" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Cancel</button>
|
||||
<button type="button" id="location-modal-continue" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const statusBox = document.getElementById('geo-status');
|
||||
const forms = document.querySelectorAll('.attendance-geo-form');
|
||||
const modal = document.getElementById('location-consent-modal');
|
||||
const modalContinue = document.getElementById('location-modal-continue');
|
||||
const modalCancel = document.getElementById('location-modal-cancel');
|
||||
let lastPosition = null;
|
||||
let geoAttempted = false;
|
||||
let activeForm = null;
|
||||
|
||||
function updateStatus(message, tone) {
|
||||
if (!statusBox) return;
|
||||
statusBox.textContent = message;
|
||||
statusBox.className = 'mt-3 rounded-xl border px-3 py-2 text-xs ' + (
|
||||
tone === 'ok'
|
||||
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
||||
: tone === 'warn'
|
||||
? 'border-amber-200 bg-amber-50 text-amber-700'
|
||||
: 'border-slate-200 bg-slate-50 text-slate-600'
|
||||
);
|
||||
}
|
||||
|
||||
function setHidden(lat, lon, acc) {
|
||||
forms.forEach(function (form) {
|
||||
const latInput = form.querySelector('input[name="latitude"]');
|
||||
const lonInput = form.querySelector('input[name="longitude"]');
|
||||
const accInput = form.querySelector('input[name="accuracy_meters"]');
|
||||
if (latInput) latInput.value = lat || '';
|
||||
if (lonInput) lonInput.value = lon || '';
|
||||
if (accInput) accInput.value = acc || '';
|
||||
});
|
||||
}
|
||||
|
||||
function openLocationModal(form) {
|
||||
return new Promise(function (resolve) {
|
||||
activeForm = form;
|
||||
if (!modal || !modalContinue || !modalCancel) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
function cleanup(result) {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modalContinue.removeEventListener('click', onContinue);
|
||||
modalCancel.removeEventListener('click', onCancel);
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
function onContinue() {
|
||||
cleanup(true);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
updateStatus('Attendance punch cancelled. Location permission was not requested.', 'warn');
|
||||
cleanup(false);
|
||||
}
|
||||
|
||||
modalContinue.addEventListener('click', onContinue);
|
||||
modalCancel.addEventListener('click', onCancel);
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
modalContinue.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function captureLocation() {
|
||||
return new Promise(function (resolve) {
|
||||
geoAttempted = true;
|
||||
if (!navigator.geolocation) {
|
||||
setHidden('', '', '');
|
||||
updateStatus('Browser geolocation is not available. Punch will be saved as pending approval if branch geofence is enabled.', 'warn');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus('Browser location popup opened. Please choose Allow to capture location...', 'info');
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
const c = pos.coords || {};
|
||||
lastPosition = c;
|
||||
setHidden(c.latitude, c.longitude, c.accuracy);
|
||||
updateStatus('Location captured. Accuracy: ' + Math.round(c.accuracy || 0) + ' meters.', 'ok');
|
||||
resolve(true);
|
||||
}, function (err) {
|
||||
lastPosition = null;
|
||||
setHidden('', '', '');
|
||||
let reason = 'Location permission denied or unavailable.';
|
||||
if (err && err.code === 1) reason = 'Location permission denied. Please allow location for this site in browser settings.';
|
||||
if (err && err.code === 2) reason = 'Location unavailable. Please enable device GPS/location service and try again.';
|
||||
if (err && err.code === 3) reason = 'Location capture timed out. Please try again near a window or with GPS enabled.';
|
||||
updateStatus(reason + ' Punch will be saved as pending approval if branch geofence is enabled.', 'warn');
|
||||
resolve(false);
|
||||
}, { enableHighAccuracy: true, timeout: 20000, maximumAge: 0 });
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus('Click Punch In/Punch Out. The app will ask you to enable location before submitting attendance.', 'info');
|
||||
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener('submit', async function (event) {
|
||||
const latInput = form.querySelector('input[name="latitude"]');
|
||||
const lonInput = form.querySelector('input[name="longitude"]');
|
||||
if (!latInput || !lonInput) return;
|
||||
|
||||
if (!latInput.value || !lonInput.value) {
|
||||
event.preventDefault();
|
||||
const proceed = await openLocationModal(form);
|
||||
if (!proceed) return;
|
||||
|
||||
await captureLocation();
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
{% if errors %}<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ errors|join(', ') }}</div>{% endif %}
|
||||
<div><h2 class="text-xl font-semibold text-slate-900">My Documents</h2><p class="text-sm text-slate-500">Upload and track your own employee documents.</p></div>
|
||||
{% if not employee %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-800">Your user is not linked to an employee profile. <a class="font-semibold underline" href="/employee/register">Request employee profile linkage</a>.</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h3 class="mb-4 font-semibold text-slate-900">Upload Document</h3>
|
||||
<form method="post" enctype="multipart/form-data" action="/employee/documents/upload" class="grid gap-4 md:grid-cols-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div><label class="text-xs font-semibold text-slate-500">Type</label><select name="document_type_id" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"><option value="">General</option>{% for dt in document_types %}<option value="{{ dt.id }}">{{ dt.code }} - {{ dt.name }}</option>{% endfor %}</select></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Title</label><input name="title" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Document No</label><input name="document_no" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">File</label><input type="file" name="document_file" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" required></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Issue Date</label><input type="date" name="issue_date" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div><label class="text-xs font-semibold text-slate-500">Expiry Date</label><input type="date" name="expiry_date" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div class="md:col-span-2"><label class="text-xs font-semibold text-slate-500">Remarks</label><input name="remarks" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"></div>
|
||||
<div class="md:col-span-4"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Upload</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm"><thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500"><tr><th class="px-4 py-3">Document</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Dates</th><th class="px-4 py-3">Remarks</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100">{% for row in rows %}<tr><td class="px-4 py-3"><div class="font-medium">{{ row.title }}</div><div class="text-xs text-slate-500">{{ row.document_type.code if row.document_type else 'GENERAL' }} • {{ row.original_filename }}</div></td><td class="px-4 py-3"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs">{{ row.status }}</span></td><td class="px-4 py-3 text-xs">Issue: {{ row.issue_date or '-' }}<br>Expiry: {{ row.expiry_date or '-' }}</td><td class="px-4 py-3 text-xs">{{ row.remarks or '' }}<br>{{ row.verification_notes or '' }}</td></tr>{% else %}<tr><td colspan="4" class="px-4 py-6 text-center text-slate-500">No documents uploaded.</td></tr>{% endfor %}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}<div class="flex flex-wrap items-center justify-between gap-3"><div><h2 class="text-xl font-semibold text-slate-900">My Leave</h2><p class="text-sm text-slate-500">Apply for leave and track approval status.</p></div></div>{% if not employee %}<div class="rounded-2xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-900">Your login user is not linked to an employee profile. Please request employee linkage first.</div>{% else %}<div class="grid gap-4 md:grid-cols-3">{% for bal in balances %}<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">{{ bal.leave_type.name if bal.leave_type else bal.leave_type_id }}</div><div class="mt-1 text-2xl font-semibold text-slate-900">{{ bal.balance_days }}</div><div class="text-xs text-slate-500">Availed {{ bal.availed_days }} days</div></div>{% else %}<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft text-sm text-slate-600">No leave balance available yet.</div>{% endfor %}</div><form method="post" action="/employee/leave/apply" 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">Apply Leave</h3><div class="mt-4 grid gap-3 md:grid-cols-4"><select name="leave_type_id" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><option value="">Leave type</option>{% for lt in leave_types %}<option value="{{ lt.id }}">{{ lt.code }} - {{ lt.name }}</option>{% endfor %}</select><input type="date" name="from_date" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><input type="date" name="to_date" required class="rounded-xl border border-slate-300 px-3 py-2 text-sm"><input name="reason" placeholder="Reason" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"></div><button class="mt-4 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Submit Leave Request</button></form><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">Leave</th><th class="px-4 py-3">Period</th><th class="px-4 py-3">Days</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Reason</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><td class="px-4 py-3">{{ row.leave_type.name if row.leave_type else row.leave_type_id }}</td><td class="px-4 py-3">{{ row.from_date }} to {{ row.to_date }}</td><td class="px-4 py-3">{{ row.days }}</td><td class="px-4 py-3">{{ row.status.title() }}</td><td class="px-4 py-3 text-slate-600">{{ row.reason or row.review_notes or '-' }}</td><td class="px-4 py-3 text-right">{% if row.status == 'pending' %}<form method="post" action="/employee/leave/{{ row.id }}/cancel"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Cancel</button></form>{% else %}-{% endif %}</td></tr>{% else %}<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No leave requests found.</td></tr>{% endfor %}</tbody></table></div>{% endif %}</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
<div><h1 class="text-2xl font-semibold text-slate-900">My Offboarding</h1><p class="text-sm text-slate-500">Submit resignation/offboarding request and track status.</p></div>
|
||||
{% if not employee %}<div class="rounded-2xl border bg-amber-50 p-4 text-sm text-amber-800">Employee profile is not linked. Please request employee profile first.</div>{% else %}
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm"><form method="post" action="/employee/offboarding/request" class="grid gap-3 md:grid-cols-4"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><select name="request_type" class="rounded-xl border px-3 py-2 text-sm"><option value="resignation">Resignation</option><option value="contract_end">Contract End</option></select><input type="date" name="requested_relieving_date" class="rounded-xl border px-3 py-2 text-sm"><input name="reason" placeholder="Reason" class="rounded-xl border px-3 py-2 text-sm md:col-span-2"><textarea name="handover_notes" placeholder="Handover notes" class="rounded-xl border px-3 py-2 text-sm md:col-span-3"></textarea><button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Submit Request</button></form></div>
|
||||
{% endif %}
|
||||
<div class="overflow-hidden rounded-2xl border bg-white shadow-sm"><table class="min-w-full divide-y text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Type</th><th class="p-3">Requested Date</th><th class="p-3">Approved Date</th><th class="p-3">Status</th><th class="p-3 text-left">Notes</th></tr></thead><tbody class="divide-y">{% for r in rows %}<tr><td class="p-3">{{ r.request_type }}</td><td class="p-3 text-center">{{ r.requested_relieving_date or '-' }}</td><td class="p-3 text-center">{{ r.approved_relieving_date or '-' }}</td><td class="p-3 text-center"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs">{{ r.status }}</span></td><td class="p-3 text-xs text-slate-500">{{ r.review_notes or r.handover_notes or '-' }}</td></tr>{% else %}<tr><td colspan="5" class="p-6 text-center text-slate-500">No requests yet.</td></tr>{% endfor %}</tbody></table></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3"><div><h2 class="text-xl font-semibold text-slate-900">My Payslips</h2><p class="text-sm text-slate-500">View your generated monthly payslips.</p></div></div>
|
||||
{% if not employee %}<div class="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">Your user is not linked to an employee profile.</div>{% else %}
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"><table class="min-w-full divide-y divide-slate-200 text-sm"><thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left">Period</th><th class="px-4 py-3 text-right">Gross</th><th class="px-4 py-3 text-right">Deductions</th><th class="px-4 py-3 text-right">Net Pay</th><th class="px-4 py-3 text-left">Status</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in rows %}<tr><td class="px-4 py-3 font-medium">{{ '%02d' % row.pay_month }}/{{ row.pay_year }}</td><td class="px-4 py-3 text-right">{{ row.gross_amount }}</td><td class="px-4 py-3 text-right">{{ row.deduction_amount }}</td><td class="px-4 py-3 text-right font-semibold">{{ row.net_amount }}</td><td class="px-4 py-3">{{ row.status.title() }}</td></tr>{% else %}<tr><td colspan="5" class="px-4 py-6 text-center text-slate-500">No payslips generated yet.</td></tr>{% endfor %}</tbody></table></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,93 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% set profile_photo_url = get_user_profile_photo_url(current_user) %}
|
||||
{% set profile_initials = get_user_initials(current_user) %}
|
||||
<div class="mx-auto max-w-7xl space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">My Profile</h2>
|
||||
<p class="text-sm text-slate-500">Maintain your profile photo and public contact details. Official HR fields remain controlled by admin.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if saved %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Profile updated successfully.</div>{% endif %}
|
||||
{% if errors %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800"><ul class="list-disc pl-5">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul></div>{% endif %}
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[320px_minmax(0,1fr)]">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
{% if profile_photo_url %}
|
||||
<img src="{{ profile_photo_url }}" alt="Profile photo" class="h-28 w-28 rounded-3xl border border-slate-200 object-cover shadow-soft">
|
||||
{% else %}
|
||||
<div class="flex h-28 w-28 items-center justify-center rounded-3xl bg-brand-600 text-3xl font-bold text-white shadow-soft">{{ profile_initials }}</div>
|
||||
{% endif %}
|
||||
<h3 class="mt-4 text-lg font-semibold text-slate-900">{{ current_user.full_name or employee.full_name }}</h3>
|
||||
<p class="text-sm text-slate-500">{{ current_user.qualification or '' }}{% if current_user.qualification and (current_user.designation or employee.designation) %} • {% endif %}{{ current_user.designation or employee.designation or '' }}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">{{ current_user.email }}</p>
|
||||
</div>
|
||||
{% if current_user.bio %}
|
||||
<div class="mt-5 rounded-2xl bg-slate-50 p-4 text-sm text-slate-600">{{ current_user.bio }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Official Details</h3>
|
||||
<dl class="mt-4 grid gap-4 md:grid-cols-2 text-sm">
|
||||
<div><dt class="text-slate-500">Employee Code</dt><dd class="font-medium text-slate-900">{{ employee.employee_code }}</dd></div>
|
||||
<div><dt class="text-slate-500">Name</dt><dd class="font-medium text-slate-900">{{ employee.full_name }}</dd></div>
|
||||
<div><dt class="text-slate-500">Department</dt><dd class="font-medium text-slate-900">{{ employee.department or '-' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Official Designation</dt><dd class="font-medium text-slate-900">{{ employee.designation or '-' }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="space-y-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-900">Public Profile</h3>
|
||||
<p class="text-sm text-slate-500">These details will be reused in dashboards and future client-facing auditor cards.</p>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">Profile Photo</label>
|
||||
<input type="file" name="profile_photo" accept="image/png,image/jpeg,image/gif,image/webp" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<p class="mt-1 text-xs text-slate-400">JPG, PNG, GIF or WebP. Maximum 2 MB.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">Qualification</label>
|
||||
<input name="qualification" value="{{ current_user.qualification or '' }}" placeholder="CA, FCA, DISA" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">Display Designation</label>
|
||||
<input name="public_designation" value="{{ current_user.designation or employee.designation or '' }}" placeholder="Partner / Manager / Staff" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">Mobile</label>
|
||||
<input name="mobile" value="{{ employee.mobile or current_user.mobile or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">Short Bio</label>
|
||||
<textarea name="bio" rows="3" placeholder="Short client-facing profile or area of responsibility" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ current_user.bio or '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-900">Personal / HR Self-Service Details</h3>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Alternate Mobile</label><input name="alternate_mobile" value="{{ employee.alternate_mobile or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Emergency Contact Name</label><input name="emergency_contact_name" value="{{ employee.emergency_contact_name or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Emergency Contact Mobile</label><input name="emergency_contact_mobile" value="{{ employee.emergency_contact_mobile or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Bank Name</label><input name="bank_name" value="{{ employee.bank_name or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Bank Account No</label><input name="bank_account_no" value="{{ employee.bank_account_no or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Bank IFSC</label><input name="bank_ifsc" value="{{ employee.bank_ifsc or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||
<div class="md:col-span-2"><label class="mb-1 block text-sm font-medium text-slate-700">Address</label><textarea name="address" rows="3" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ employee.address or '' }}</textarea></div>
|
||||
</div>
|
||||
<div class="flex justify-end"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Save My Profile</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,87 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">My Work</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Board view of your assigned engagements. Open a card to work on tasks and refer to engagement documents.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not employee %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-900 shadow-soft">
|
||||
<h3 class="font-semibold">Your login is not linked to an employee master</h3>
|
||||
<p class="mt-1">You may still see tasks assigned directly to your user ID, but attendance, profile, leave, documents and payslip self-service require an employee master link.</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<a href="/employee/register" class="rounded-xl bg-amber-700 px-4 py-2 text-sm font-semibold text-white hover:bg-amber-800">Request Employee Link</a>
|
||||
<a href="/employee/dashboard" class="rounded-xl border border-amber-300 px-4 py-2 text-sm font-semibold text-amber-900 hover:bg-amber-100">Back to My Workspace</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-6">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Total</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.total }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Open</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.open }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">In Progress</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.in_progress }}</div></div>
|
||||
<div class="rounded-2xl border border-rose-200 bg-rose-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-rose-600">Blocked</div><div class="mt-1 text-2xl font-semibold text-rose-700">{{ work_payload.summary.blocked }}</div></div>
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-red-600">Overdue</div><div class="mt-1 text-2xl font-semibold text-red-700">{{ work_payload.summary.overdue }}</div></div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-emerald-600">Completed</div><div class="mt-1 text-2xl font-semibold text-emerald-700">{{ work_payload.summary.completed }}</div></div>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/employee/work" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="grid gap-3 md:grid-cols-[1fr_220px_auto]">
|
||||
<input type="search" name="q" value="{{ q or '' }}" placeholder="Search engagement, task, client or service" class="rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||||
<select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||||
<option value="open" {% if status == 'open' %}selected{% endif %}>Open work</option>
|
||||
<option value="pending" {% if status == 'pending' %}selected{% endif %}>Pending</option>
|
||||
<option value="in_progress" {% if status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
||||
<option value="blocked" {% if status == 'blocked' %}selected{% endif %}>Blocked</option>
|
||||
<option value="completed" {% if status == 'completed' %}selected{% endif %}>Completed</option>
|
||||
<option value="closed" {% if status == 'closed' %}selected{% endif %}>Closed work</option>
|
||||
</select>
|
||||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-4">
|
||||
{% for column in work_payload.columns %}
|
||||
<section class="min-h-[420px] rounded-2xl border border-slate-200 bg-slate-50 p-3 shadow-soft">
|
||||
<div class="mb-3 flex items-center justify-between px-1">
|
||||
<h3 class="text-sm font-semibold text-slate-900">{{ column.label }}</h3>
|
||||
<span class="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-slate-600">{{ column.cards|length }}</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{% for card in column.cards %}
|
||||
<article class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm hover:border-brand-200 hover:shadow-soft">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">{{ card.client_code or 'Client' }}</div>
|
||||
<h4 class="mt-1 text-sm font-semibold text-slate-900">{{ card.client_name }}</h4>
|
||||
</div>
|
||||
{% if card.overdue_count %}<span class="rounded-full bg-red-100 px-2 py-1 text-[11px] font-semibold text-red-700">Overdue {{ card.overdue_count }}</span>{% elif card.due_today_count %}<span class="rounded-full bg-amber-100 px-2 py-1 text-[11px] font-semibold text-amber-700">Due today</span>{% endif %}
|
||||
</div>
|
||||
<div class="mt-3 rounded-xl bg-slate-50 p-3">
|
||||
<div class="text-sm font-semibold text-slate-900">{{ card.service_name }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">FY {{ card.financial_year }} · Due {{ card.due_date or '-' }}</div>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-3 gap-2 text-center text-xs">
|
||||
<div class="rounded-xl border border-slate-200 p-2"><div class="font-semibold text-slate-900">{{ card.open_count }}</div><div class="text-slate-500">Open</div></div>
|
||||
<div class="rounded-xl border border-slate-200 p-2"><div class="font-semibold text-slate-900">{{ card.blocked_count }}</div><div class="text-slate-500">Blocked</div></div>
|
||||
<div class="rounded-xl border border-slate-200 p-2"><div class="font-semibold text-slate-900">{{ card.completed_count }}</div><div class="text-slate-500">Done</div></div>
|
||||
</div>
|
||||
{% if card.latest_comment %}<p class="mt-3 truncate text-xs text-slate-500">Latest: {{ card.latest_comment.message }}</p>{% endif %}
|
||||
<a href="/work/engagements/{{ card.engagement_id }}" class="mt-4 inline-flex w-full justify-center rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Open Work Details</a>
|
||||
</article>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 bg-white p-6 text-center text-sm text-slate-500">No cards in this column.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,105 @@
|
||||
{% 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-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Employee Work Allocation</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Assign and review engagement/service tasks grouped by priority, client and engagement.</p>
|
||||
</div>
|
||||
<a href="/employees/dashboard" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back to HR Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-5">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Total</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.total }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Open</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.open }}</div></div>
|
||||
<div class="rounded-2xl border border-orange-200 bg-orange-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-orange-600">Unassigned</div><div class="mt-1 text-2xl font-semibold text-orange-700">{{ work_payload.summary.unassigned }}</div></div>
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-red-600">Overdue</div><div class="mt-1 text-2xl font-semibold text-red-700">{{ work_payload.summary.overdue }}</div></div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-emerald-600">Completed</div><div class="mt-1 text-2xl font-semibold text-emerald-700">{{ work_payload.summary.completed }}</div></div>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/employees/work" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="grid gap-3 md:grid-cols-[1fr_180px_240px_auto]">
|
||||
<input type="search" name="q" value="{{ q or '' }}" placeholder="Search task, client or engagement" class="rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||||
<select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="open" {% if status == 'open' %}selected{% endif %}>Open tasks</option>
|
||||
<option value="unassigned" {% if status == 'unassigned' %}selected{% endif %}>Unassigned</option>
|
||||
<option value="closed" {% if status == 'closed' %}selected{% endif %}>Closed tasks</option>
|
||||
<option value="pending" {% if status == 'pending' %}selected{% endif %}>Pending</option>
|
||||
<option value="in_progress" {% if status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
||||
<option value="blocked" {% if status == 'blocked' %}selected{% endif %}>Blocked</option>
|
||||
<option value="completed" {% if status == 'completed' %}selected{% endif %}>Completed</option>
|
||||
</select>
|
||||
<select name="assigned_to_user_id" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="0">All assignees</option>
|
||||
{% for user in assignable_users %}
|
||||
<option value="{{ user.id }}" {% if selected_assigned_to_user_id == user.id %}selected{% endif %}>{{ user.full_name or user.email }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="space-y-4">
|
||||
{% for priority_group in work_payload.groups %}
|
||||
<details class="group rounded-2xl border border-slate-200 bg-white shadow-soft" {% if loop.first %}open{% endif %}>
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 rounded-2xl px-5 py-4 hover:bg-slate-50">
|
||||
<div><div class="text-sm font-semibold uppercase tracking-wide text-slate-500">Priority</div><div class="text-lg font-semibold text-slate-900">{{ priority_group.label }}</div></div>
|
||||
<div class="flex items-center gap-3 text-sm text-slate-500"><span>{{ priority_group.task_count }} task{{ '' if priority_group.task_count == 1 else 's' }}</span><span class="text-lg transition group-open:rotate-90">›</span></div>
|
||||
</summary>
|
||||
<div class="space-y-3 border-t border-slate-100 p-4">
|
||||
{% for client_group in priority_group.clients %}
|
||||
<details class="group/client rounded-xl border border-slate-200 bg-slate-50" {% if loop.first %}open{% endif %}>
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 hover:bg-slate-100">
|
||||
<div><div class="font-semibold text-slate-900">{{ client_group.client_name }}</div><div class="text-xs text-slate-500">{{ client_group.client_code or 'No client code' }}</div></div>
|
||||
<div class="flex items-center gap-3 text-sm text-slate-500"><span>{{ client_group.task_count }} task{{ '' if client_group.task_count == 1 else 's' }}</span><span class="transition group-open/client:rotate-90">›</span></div>
|
||||
</summary>
|
||||
<div class="space-y-3 border-t border-slate-200 p-3">
|
||||
{% for engagement_group in client_group.engagements %}
|
||||
<details class="group/eng rounded-xl border border-slate-200 bg-white" {% if loop.first %}open{% endif %}>
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 hover:bg-slate-50">
|
||||
<div><div class="font-semibold text-slate-900">{{ engagement_group.label }}</div><div class="text-xs text-slate-500">Open: {{ engagement_group.open_count }} · Completed: {{ engagement_group.completed_count }} · Status: {{ engagement_group.status.replace('_',' ').title() }}</div></div>
|
||||
<div class="flex items-center gap-3 text-sm text-slate-500"><span>{{ engagement_group.task_count }} task{{ '' if engagement_group.task_count == 1 else 's' }}</span><span class="transition group-open/eng:rotate-90">›</span></div>
|
||||
</summary>
|
||||
<div class="overflow-x-auto border-t border-slate-100">
|
||||
<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">Task</th><th class="px-4 py-3">Current</th><th class="px-4 py-3">Assign / Review</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
{% for task in engagement_group.tasks %}
|
||||
<tr class="align-top {% if task.is_overdue %}bg-red-50{% elif task.is_due_today %}bg-amber-50{% endif %}">
|
||||
<td class="px-4 py-3 min-w-[260px]"><div class="font-medium text-slate-900">{{ task.task_name }}</div>{% if task.description %}<div class="mt-1 max-w-xl text-xs text-slate-500">{{ task.description }}</div>{% endif %}<div class="mt-2 text-xs text-slate-500">Target: {{ task.internal_target_date or '-' }} · {{ task.date_bucket }}</div></td>
|
||||
<td class="px-4 py-3 min-w-[180px]"><div><span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-700">{{ task.status_label }}</span></div><div class="mt-2 text-xs text-slate-600">Assigned: {{ task.assigned_to.full_name if task.assigned_to else 'Unassigned' }}</div><div class="mt-1 text-xs text-slate-600">Priority: {{ task.priority_label }}</div>
|
||||
<div class="mt-2"><a href="/employees/work/tasks/{{ task.id }}/communication" class="inline-flex rounded-lg border border-slate-300 px-2.5 py-1 text-xs font-semibold text-slate-700 hover:bg-slate-50">Timeline{% if task.comment_count %} · {{ task.comment_count }}{% endif %}</a></div>
|
||||
{% if task.latest_comment %}<div class="mt-1 max-w-xs truncate text-xs text-slate-500">{{ task.latest_comment.message }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 min-w-[520px]">
|
||||
<form method="post" action="/employees/work/tasks/{{ task.id }}/assign" class="grid gap-2 md:grid-cols-5">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<select name="assigned_to_user_id" class="rounded-lg border border-slate-300 px-2 py-1 text-xs"><option value="">Unassigned</option>{% for user in assignable_users %}<option value="{{ user.id }}" {% if task.assigned_to_user_id == user.id %}selected{% endif %}>{{ user.full_name or user.email }}</option>{% endfor %}</select>
|
||||
<select name="status" class="rounded-lg border border-slate-300 px-2 py-1 text-xs"><option value="pending" {% if task.status == 'pending' %}selected{% endif %}>Pending</option><option value="in_progress" {% if task.status == 'in_progress' %}selected{% endif %}>In Progress</option><option value="blocked" {% if task.status == 'blocked' %}selected{% endif %}>Blocked</option><option value="completed" {% if task.status == 'completed' %}selected{% endif %}>Completed</option><option value="not_applicable" {% if task.status == 'not_applicable' %}selected{% endif %}>Not Applicable</option><option value="cancelled" {% if task.status == 'cancelled' %}selected{% endif %}>Cancelled</option></select>
|
||||
<select name="priority" class="rounded-lg border border-slate-300 px-2 py-1 text-xs"><option value="low" {% if task.priority == 'low' %}selected{% endif %}>Low</option><option value="normal" {% if task.priority == 'normal' %}selected{% endif %}>Normal</option><option value="high" {% if task.priority == 'high' %}selected{% endif %}>High</option><option value="urgent" {% if task.priority == 'urgent' %}selected{% endif %}>Urgent</option></select>
|
||||
<input type="date" name="internal_target_date" value="{{ task.internal_target_date or '' }}" class="rounded-lg border border-slate-300 px-2 py-1 text-xs">
|
||||
<button type="submit" class="rounded-lg bg-brand-600 px-3 py-1 text-xs font-semibold text-white hover:bg-brand-700">Save</button>
|
||||
<input type="text" name="remarks" value="{{ task.remarks or '' }}" placeholder="Remarks" class="md:col-span-5 rounded-lg border border-slate-300 px-2 py-1 text-xs">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-soft"><h3 class="font-semibold text-slate-900">No work items found</h3><p class="mt-1 text-sm text-slate-500">No engagement task matches the selected filter.</p></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,117 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">{{ board.label }}</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
{{ board.client.client_name if board.client else 'Unlinked Client' }}
|
||||
{% if board.client and board.client.client_code %} · {{ board.client.client_code }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/employee/work" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">My Work Board</a>
|
||||
<a href="/documents/engagements/{{ board.engagement_id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Full Documents</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-6">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Total Tasks</div><div class="mt-1 text-2xl font-semibold">{{ board.summary.total }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Open</div><div class="mt-1 text-2xl font-semibold">{{ board.summary.open }}</div></div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">In Progress</div><div class="mt-1 text-2xl font-semibold">{{ board.summary.in_progress }}</div></div>
|
||||
<div class="rounded-2xl border border-rose-200 bg-rose-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-rose-600">Blocked</div><div class="mt-1 text-2xl font-semibold text-rose-700">{{ board.summary.blocked }}</div></div>
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-red-600">Overdue</div><div class="mt-1 text-2xl font-semibold text-red-700">{{ board.summary.overdue }}</div></div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-emerald-600">Completed</div><div class="mt-1 text-2xl font-semibold text-emerald-700">{{ board.summary.completed }}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div class="grid gap-4 lg:grid-cols-2 2xl:grid-cols-4">
|
||||
{% for column in board.columns %}
|
||||
<section class="rounded-2xl border border-slate-200 bg-slate-50 p-3 shadow-soft">
|
||||
<div class="mb-3 flex items-center justify-between px-1">
|
||||
<h3 class="text-sm font-semibold text-slate-900">{{ column.label }}</h3>
|
||||
<span class="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-slate-600">{{ column.tasks|length }}</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{% for task in column.tasks %}
|
||||
<article class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm {% if task.is_overdue %}border-red-200 bg-red-50{% elif task.is_due_today %}border-amber-200 bg-amber-50{% endif %}">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-slate-900">{{ task.task_name }}</h4>
|
||||
{% if task.description %}<p class="mt-1 line-clamp-3 text-xs text-slate-500">{{ task.description }}</p>{% endif %}
|
||||
</div>
|
||||
<span class="rounded-full bg-white px-2 py-1 text-[11px] font-semibold text-slate-600">#{{ task.sequence_no }}</span>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-[11px] font-semibold">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-700">{{ task.priority_label }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-700">{{ task.date_bucket }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-700">Target {{ task.internal_target_date or '-' }}</span>
|
||||
</div>
|
||||
{% if task.latest_comment %}<p class="mt-3 truncate text-xs text-slate-500">Latest: {{ task.latest_comment.message }}</p>{% endif %}
|
||||
<form method="post" action="/employee/work/tasks/{{ task.id }}/status" class="mt-4 space-y-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="return_url" value="/employee/work/engagements/{{ board.engagement_id }}">
|
||||
<select name="status" class="w-full rounded-lg border border-slate-300 px-2 py-1.5 text-xs">
|
||||
<option value="pending" {% if task.status == 'pending' %}selected{% endif %}>Pending</option>
|
||||
<option value="in_progress" {% if task.status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
||||
<option value="blocked" {% if task.status == 'blocked' %}selected{% endif %}>Blocked</option>
|
||||
<option value="completed" {% if task.status == 'completed' %}selected{% endif %}>Completed</option>
|
||||
</select>
|
||||
<input type="text" name="remarks" value="" placeholder="Remark / reason" class="w-full rounded-lg border border-slate-300 px-2 py-1.5 text-xs">
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="flex-1 rounded-lg bg-brand-600 px-2.5 py-1.5 text-xs font-semibold text-white hover:bg-brand-700">Save</button>
|
||||
<a href="/employee/work/tasks/{{ task.id }}/communication" class="rounded-lg border border-slate-300 px-2.5 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Timeline{% if task.comment_count %} · {{ task.comment_count }}{% endif %}</a>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 bg-white p-6 text-center text-sm text-slate-500">No tasks.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<aside class="space-y-4">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-900">Engagement Documents</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Quick access while performing tasks.</p>
|
||||
</div>
|
||||
<a href="/documents/engagements/{{ board.engagement_id }}" class="text-sm font-semibold text-brand-700 hover:text-brand-800">Open</a>
|
||||
</div>
|
||||
<div class="mt-4 divide-y divide-slate-100">
|
||||
{% for doc in board.documents %}
|
||||
<div class="py-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">{{ doc.title }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">{{ doc.document_type }} · v{{ doc.current_version_no }} · {{ doc.status }}</div>
|
||||
</div>
|
||||
{% if doc.current_version_no and doc.versions %}
|
||||
<a href="/documents/{{ doc.id }}/download" class="rounded-lg border border-slate-300 px-2.5 py-1 text-xs font-semibold text-slate-700 hover:bg-slate-50">Download</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if doc.description %}<p class="mt-2 text-xs text-slate-500">{{ doc.description }}</p>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-xl border border-dashed border-slate-300 p-5 text-center text-sm text-slate-500">No engagement document uploaded yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Engagement Summary</h3>
|
||||
<dl class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex justify-between gap-4"><dt class="text-slate-500">Status</dt><dd class="font-medium text-slate-900">{{ board.subscription.status if board.subscription else '-' }}</dd></div>
|
||||
<div class="flex justify-between gap-4"><dt class="text-slate-500">FY</dt><dd class="font-medium text-slate-900">{{ board.subscription.financial_year if board.subscription else '-' }}</dd></div>
|
||||
<div class="flex justify-between gap-4"><dt class="text-slate-500">Due Date</dt><dd class="font-medium text-slate-900">{{ board.subscription.current_due_date if board.subscription else '-' }}</dd></div>
|
||||
<div class="flex justify-between gap-4"><dt class="text-slate-500">Review Partner</dt><dd class="font-medium text-slate-900">{{ board.subscription.review_partner.full_name if board.subscription and board.subscription.review_partner else '-' }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,96 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% if is_employee_self %}
|
||||
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
|
||||
{% else %}
|
||||
{% include "modules/managers/templates/managers/_manager_tabs.html" %}
|
||||
{% endif %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Task Communication</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Communication timeline for engagement task notes, clarifications and review remarks.</p>
|
||||
</div>
|
||||
<a href="{{ back_url }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back</a>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<div class="md:col-span-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Task</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ task.task_name }}</div>
|
||||
{% if task.description %}<p class="mt-1 text-sm text-slate-600">{{ task.description }}</p>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Client</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-900">{{ task.client.client_name if task.client else 'Unlinked Client' }}</div>
|
||||
<div class="text-xs text-slate-500">{{ task.client.client_code if task.client else '' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Engagement / Service</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-900">{{ task.engagement_label }}</div>
|
||||
<div class="text-xs text-slate-500">Target: {{ task.internal_target_date or '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 font-semibold text-slate-700">{{ task.status_label }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 font-semibold text-slate-700">{{ task.priority_label }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 font-semibold text-slate-700">{{ task.date_bucket }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 font-semibold text-slate-700">Assigned: {{ task.assigned_to.full_name if task.assigned_to else 'Unassigned' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h3 class="font-semibold text-slate-900">Add communication</h3>
|
||||
<form method="post" action="{{ post_url }}" class="mt-4 space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<label class="block text-sm font-medium text-slate-700">Type
|
||||
<select name="comment_type" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||||
{% for code, label in communication_types %}
|
||||
<option value="{{ code }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="block text-sm font-medium text-slate-700">Visibility
|
||||
<select name="visibility" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||||
{% for code, label in communication_visibilities %}
|
||||
<option value="{{ code }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="block text-sm font-medium text-slate-700">Message
|
||||
<textarea name="message" rows="4" required placeholder="Write note, client clarification, consultant communication, or review remark" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100"></textarea>
|
||||
</label>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Add to Timeline</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||||
<div class="border-b border-slate-100 px-5 py-4">
|
||||
<h3 class="font-semibold text-slate-900">Communication Timeline</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Newest communication appears first.</p>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-100">
|
||||
{% for item in task.communication_items %}
|
||||
<div class="p-5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-700">{{ item.comment_type.replace('_',' ').title() }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-700">{{ item.visibility.replace('_',' ').title() }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500">{{ item.created_at_utc }}</div>
|
||||
</div>
|
||||
<p class="mt-3 whitespace-pre-wrap text-sm text-slate-800">{{ item.message }}</p>
|
||||
<div class="mt-3 text-xs text-slate-500">By {{ item.created_by.full_name if item.created_by else 'System/User' }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-8 text-center text-sm text-slate-500">No communication has been added for this task yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user