Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
+617
View File
@@ -0,0 +1,617 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{% set __title_user = current_user if current_user is defined else None %}
{% set __firm_branding = get_current_firm_branding(request, __title_user) %}
<meta name="theme-color" content="{{ __firm_branding.primary_color or '#1e3a8a' }}" />
{% if __firm_branding.favicon_url %}<link rel="icon" href="{{ __firm_branding.favicon_url }}" />{% endif %}
{% set __title_auth = __title_user and request.session.get("otp_verified", False) %}
{% if __title_auth %}
{% set __title_firm = get_current_tenant_name(request, __title_user) %}
{% set __title_branch = get_current_branch_name(request, __title_user) %}
<title>{{ title or "Workspace" }} | {{ __title_firm }}{% if __title_branch and __title_branch != "-" %} - {{ __title_branch }}{% endif %}</title>
{% else %}
<title>{{ title or "Welcome" }} | {{ __firm_branding.firm_name or "Audit Firm ERP" }}</title>
{% endif %}
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
50: 'var(--af-color-brand-50)',
100: 'var(--af-color-brand-100)',
200: 'var(--af-color-brand-200)',
300: 'var(--af-color-brand-300)',
400: 'var(--af-color-brand-400)',
500: 'var(--af-color-brand-500)',
600: 'var(--af-color-brand-600)',
700: 'var(--af-color-brand-700)',
800: 'var(--af-color-brand-800)',
900: 'var(--af-color-brand-900)'
},
afsuccess: {
50: 'var(--af-color-success-50)',
600: 'var(--af-color-success-600)',
700: 'var(--af-color-success-700)'
},
afwarning: {
50: 'var(--af-color-warning-50)',
600: 'var(--af-color-warning-600)',
700: 'var(--af-color-warning-700)'
},
afdanger: {
50: 'var(--af-color-danger-50)',
600: 'var(--af-color-danger-600)',
700: 'var(--af-color-danger-700)'
},
afinfo: {
50: 'var(--af-color-info-50)',
600: 'var(--af-color-info-600)',
700: 'var(--af-color-info-700)'
}
},
boxShadow: {
soft: 'var(--af-shadow-soft)',
card: 'var(--af-shadow-card)',
focus: '0 0 0 4px rgba(37, 99, 235, 0.12)'
},
borderRadius: {
card: 'var(--af-radius-card)',
control: 'var(--af-radius-control)'
}
}
}
}
</script>
<link rel="stylesheet" href="/static/css/theme_tokens.css?v=7q-dashboard-align-strong" />
<style>
:root {
--af-color-brand-500: {{ __firm_branding.primary_color or '#2563eb' }};
--af-color-brand-600: {{ __firm_branding.primary_color or '#1d4ed8' }};
--af-color-brand-700: {{ __firm_branding.accent_color or '#1e40af' }};
--af-color-brand-800: {{ __firm_branding.accent_color or '#1e3a8a' }};
--af-color-brand-900: {{ __firm_branding.accent_color or '#172554' }};
}
</style>
</head>
<body class="min-h-screen bg-slate-100 text-slate-900">
{% set otp_ok = request.session.get("otp_verified", False) %}
{% set full_auth = current_user and otp_ok %}
{% set ui_perms = current_user_permissions if full_auth else [] %}
{% set ui_roles = current_user_roles if full_auth else [] %}
{% set active_tenant_id = get_active_tenant_id(request, current_user) if full_auth else None %}
{% set active_branch_id = get_active_branch_id(request, current_user) if full_auth else None %}
{% set active_financial_year = get_active_financial_year(request, current_user) if full_auth else None %}
{% set active_assessment_year = get_active_assessment_year(request, current_user) if full_auth else None %}
{% set unread_alert_count = get_unread_alert_count(request, current_user) if full_auth else 0 %}
{% set current_path = request.url.path %}
{% set document_menu_roles = ["System Admin", "Firm Admin", "Partner"] %}
{% set can_view_documents_menu = full_auth and can_view_documents(current_user, ui_perms, ui_roles) and (ui_roles|select("in", document_menu_roles)|list|length > 0) %}
{% set management_menu_roles = ["System Admin", "Firm Admin", "Partner", "Manager", "Branch Manager"] %}
{% set can_view_management_menus = full_auth and (ui_roles|select("in", management_menu_roles)|list|length > 0) %}
{% set firm_branding = get_current_firm_branding(request, current_user) if full_auth else __firm_branding %}
{% set current_user_photo_url = get_user_profile_photo_url(current_user) if full_auth else None %}
{% set current_user_initials = get_user_initials(current_user) if full_auth else "U" %}
{% set current_firm_name = firm_branding.firm_name if firm_branding else "Audit Firm ERP" %}
{% set current_branch_name = firm_branding.branch_name if firm_branding else "" %}
{% set domain_context = get_domain_context(request) %}
{% set is_system_admin_user = full_auth and ("System Admin" in ui_roles) %}
{% set can_manage_local_storage_agent = full_auth and can_view_documents(current_user, ui_perms, ui_roles) and can_upload_documents(current_user, ui_perms, ui_roles) and (ui_roles|select("in", ["Firm Admin", "Partner", "Branch Manager"])|list|length > 0) %}
<div class="min-h-screen lg:grid lg:grid-cols-[260px_minmax(0,1fr)]">
<aside class="border-b border-slate-200 bg-slate-900 text-slate-100 lg:min-h-screen lg:border-b-0 lg:border-r lg:border-slate-800">
<div class="flex items-center gap-3 px-5 py-5">
{% if firm_branding.logo_url %}<img src="{{ firm_branding.logo_url }}" alt="{{ current_firm_name }} logo" class="h-11 w-11 rounded-2xl bg-white object-contain p-1 shadow-soft" />{% else %}<div class="flex h-11 w-11 items-center justify-center rounded-2xl bg-brand-500 font-bold text-white shadow-soft">{{ (current_firm_name[:2] if current_firm_name else 'AF')|upper }}</div>{% endif %}
<div class="min-w-0">
<div class="truncate text-sm font-semibold tracking-wide">{{ current_firm_name }}</div>
<div class="truncate text-xs text-slate-400">{% if full_auth %}{{ current_branch_name }}{% if current_branch_name and current_branch_name != "-" %} Branch{% endif %} • Workspace{% elif domain_context.is_resolved %}{{ current_branch_name or 'Domain Workspace' }}{% else %}Secure Practice Workspace{% endif %}</div>
</div>
</div>
{% if full_auth and ('Client' in ui_roles) %}
{% set sidebar_auditor = get_client_sidebar_auditor_card(request, current_user) %}
{% if sidebar_auditor %}
<div class="mx-4 mb-4 rounded-2xl border border-slate-700 bg-slate-800/80 p-3 shadow-soft">
<div class="text-[10px] font-semibold uppercase tracking-[0.20em] text-brand-200">Your Auditor</div>
<div class="mt-3 flex items-center gap-3">
{% if sidebar_auditor.photo_url %}
<img src="{{ sidebar_auditor.photo_url }}" alt="{{ sidebar_auditor.name }}" class="h-11 w-11 rounded-xl border border-white/10 object-cover" />
{% else %}
<div class="flex h-11 w-11 items-center justify-center rounded-xl border border-white/10 bg-brand-600 text-sm font-bold text-white">{{ sidebar_auditor.initials or 'AU' }}</div>
{% endif %}
<div class="min-w-0">
<div class="truncate text-sm font-semibold text-white">{{ sidebar_auditor.name or 'Firm team' }}</div>
<div class="truncate text-xs text-slate-300">{{ sidebar_auditor.designation or 'Auditor' }}</div>
{% if sidebar_auditor.qualification %}<div class="truncate text-[11px] text-slate-400">{{ sidebar_auditor.qualification }}</div>{% endif %}
</div>
</div>
<div class="mt-3 space-y-1 text-[11px] leading-5 text-slate-300">
{% if sidebar_auditor.mobile %}<div class="truncate">Mobile: {{ sidebar_auditor.mobile }}</div>{% endif %}
{% if sidebar_auditor.email %}<a href="mailto:{{ sidebar_auditor.email }}" class="block truncate text-brand-200 hover:text-white">{{ sidebar_auditor.email }}</a>{% endif %}
</div>
</div>
{% endif %}
{% endif %}
{% if full_auth %}
<nav class="space-y-4 px-4 pb-6">
{% set has_employee_self_menu = can_view_employee_portal(current_user, ui_perms, ui_roles) or can_view_own_employee_work(current_user, ui_perms, ui_roles) or can_view_own_employee_attendance(current_user, ui_perms, ui_roles) or can_view_own_employee_leave(current_user, ui_perms, ui_roles) or can_view_own_employee_documents(current_user, ui_perms, ui_roles) or can_view_own_employee_payslips(current_user, ui_perms, ui_roles) or can_request_own_employee_offboarding(current_user, ui_perms, ui_roles) %}
{% set has_team_workspace_menu = can_manage_employee_work(current_user, ui_perms, ui_roles) or can_view_employee_progress(current_user, ui_perms, ui_roles) or can_view_all_employee_attendance(current_user, ui_perms, ui_roles) or can_view_all_employee_leave(current_user, ui_perms, ui_roles) %}
{% set has_team_admin_menu = can_view_employee_dashboard(current_user, ui_perms, ui_roles) or can_view_employees(current_user, ui_perms, ui_roles) or can_approve_employee_registrations(current_user, ui_perms, ui_roles) or can_import_employee_hr(current_user, ui_perms, ui_roles) or can_manage_employee_leave_types(current_user, ui_perms, ui_roles) or can_manage_employee_leave_balances(current_user, ui_perms, ui_roles) or can_view_all_employee_documents(current_user, ui_perms, ui_roles) or can_manage_employee_document_types(current_user, ui_perms, ui_roles) or can_view_employee_onboarding(current_user, ui_perms, ui_roles) or can_manage_employee_onboarding(current_user, ui_perms, ui_roles) or can_view_employee_offboarding(current_user, ui_perms, ui_roles) or can_manage_employee_payroll_structures(current_user, ui_perms, ui_roles) or can_run_employee_payroll(current_user, ui_perms, ui_roles) or can_view_employee_payroll(current_user, ui_perms, ui_roles) %}
{% set has_firm_users_menu = is_system_admin_user and (can_view_users(current_user, ui_perms, ui_roles) or can_manage_users(current_user, ui_perms, ui_roles) or can_view_rbac(current_user, ui_perms, ui_roles)) %}
{% set has_firm_services_menu = can_view_services(current_user, ui_perms, ui_roles) %}
{% set has_firm_clients_menu = can_view_clients(current_user, ui_perms, ui_roles) or can_manage_clients(current_user, ui_perms, ui_roles) or can_export_clients(current_user, ui_perms, ui_roles) %}
{% set has_firm_consultants_menu = can_view_consultants(current_user, ui_perms, ui_roles) or can_manage_consultants(current_user, ui_perms, ui_roles) or can_manage_consultant_service_requests(current_user, ui_perms, ui_roles) or can_manage_consultant_conversions(current_user, ui_perms, ui_roles) %}
{% set has_email_settings_menu = ("Firm Admin" in ui_roles) or is_system_admin_user %}
{% set has_firm_admin_menu = has_firm_users_menu or has_firm_services_menu or has_firm_clients_menu or has_firm_consultants_menu or can_manage_local_storage_agent or has_email_settings_menu or ("Firm Admin" in ui_roles) or is_system_admin_user %}
{% if has_employee_self_menu %}
<details class="group rounded-xl" {% if current_path.startswith('/employee') or current_path.startswith('/alerts') %}open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
<span>My Workspace</span>
<span class="text-xs text-slate-400 transition group-open:rotate-90"></span>
</summary>
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
{% if can_view_employee_portal(current_user, ui_perms, ui_roles) %}
<a href="/employee/dashboard" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/employee/dashboard' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Overview</a>
{% endif %}
{% if can_view_own_employee_work(current_user, ui_perms, ui_roles) %}
<a href="/employee/work" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employee/work') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">My Work Board</a>
{% endif %}
{% if can_view_own_employee_attendance(current_user, ui_perms, ui_roles) %}
<a href="/employee/attendance" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employee/attendance') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">My Attendance</a>
{% endif %}
{% if can_view_own_employee_leave(current_user, ui_perms, ui_roles) %}
<a href="/employee/leave" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employee/leave') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">My Leave</a>
{% endif %}
{% if can_view_own_employee_documents(current_user, ui_perms, ui_roles) %}
<a href="/employee/documents" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employee/documents') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">My Documents</a>
{% endif %}
{% if can_view_own_employee_payslips(current_user, ui_perms, ui_roles) %}
<a href="/employee/payslips" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employee/payslips') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">My Payslips</a>
{% endif %}
<a href="/alerts" class="flex items-center justify-between rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/alerts') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}"><span>My Alert</span>{% if unread_alert_count > 0 %}<span class="rounded-full bg-brand-600 px-2 py-0.5 text-[10px] font-semibold text-white">{{ unread_alert_count }}</span>{% endif %}</a>
{% if can_request_own_employee_offboarding(current_user, ui_perms, ui_roles) %}
<a href="/employee/offboarding" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employee/offboarding') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">My Offboarding</a>
{% endif %}
</div>
</details>
{% endif %}
{% if 'Partner' in ui_roles %}
<details class="group rounded-xl" {% if current_path.startswith('/partner') %}open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
<span>Partner Workspace</span>
<span class="text-xs text-slate-400 transition group-open:rotate-90"></span>
</summary>
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
<a href="/partner/dashboard" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/partner/dashboard' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Overview</a>
<a href="/partner/reviews" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/partner/reviews') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Review Board</a>
<a href="/partner/clients" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/partner/clients') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">My Clients</a>
<a href="/alerts" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/alerts') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">My Alert</a>
</div>
</details>
{% endif %}
{% if has_team_workspace_menu %}
<details class="group rounded-xl" {% if current_path.startswith('/manager') or current_path.startswith('/employees/work') or current_path.startswith('/employees/progress') or current_path.startswith('/employees/attendance') or current_path.startswith('/employees/leave') %}open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
<span>Team Workspace</span>
<span class="text-xs text-slate-400 transition group-open:rotate-90"></span>
</summary>
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
{% if can_manage_employee_work(current_user, ui_perms, ui_roles) %}
<a href="/manager/dashboard" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/manager/dashboard' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Manager Dashboard</a>
<a href="/manager/work" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/manager/work') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Team Work Board</a>
<a href="/employees/work" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employees/work') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Detailed Allocation</a>
{% endif %}
{% if can_view_employee_progress(current_user, ui_perms, ui_roles) %}
<a href="/employees/progress" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employees/progress') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Engagement Progress</a>
{% endif %}
{% if can_view_all_employee_attendance(current_user, ui_perms, ui_roles) %}
<a href="/employees/attendance" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/employees/attendance') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Team Attendance</a>
{% endif %}
{% if can_view_all_employee_leave(current_user, ui_perms, ui_roles) %}
<a href="/employees/leave" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/employees/leave' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Team Leave</a>
{% endif %}
</div>
</details>
{% endif %}
{% if can_view_billing(current_user, ui_perms, ui_roles) %}
<details class="group rounded-xl" {% if current_path.startswith('/billing') %}open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
<span>Billing</span>
<span class="text-xs text-slate-400 transition group-open:rotate-90"></span>
</summary>
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
<a href="/billing" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/billing' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Invoices</a>
<a href="/billing/settings" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/billing/settings') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Billing Settings</a>
{% if can_create_billing(current_user, ui_perms, ui_roles) %}<a href="/billing/new" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/billing/new' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">New Invoice</a>{% endif %}
{% if can_generate_billing_invoices(current_user, ui_perms, ui_roles) %}<a href="/billing/generate" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Generate Bills</a>{% endif %}
{% if can_view_billing_fee_structure(current_user, ui_perms, ui_roles) %}<a href="/billing/fee-structures/list" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Fee Structure</a>{% endif %}
{% if can_import_billing_fee_structure(current_user, ui_perms, ui_roles) %}<a href="/billing/fee-structures/template" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Download Fee Template</a><a href="/billing/fee-structures/import" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Import Fee Structure</a>{% endif %}
</div>
</details>
{% endif %}
{% if has_team_admin_menu %}
<details class="group rounded-xl" {% if current_path.startswith('/employees') and not (current_path.startswith('/employees/work') or current_path.startswith('/employees/progress') or current_path.startswith('/employees/attendance') or current_path == '/employees/leave') %}open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
<span>Team Administration</span>
<span class="text-xs text-slate-400 transition group-open:rotate-90"></span>
</summary>
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
{% if can_view_employee_dashboard(current_user, ui_perms, ui_roles) %}<a href="/employees/dashboard" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">HR Dashboard</a>{% endif %}
{% if can_view_employees(current_user, ui_perms, ui_roles) %}<a href="/employees" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Employee Master</a>{% endif %}
{% if can_approve_employee_registrations(current_user, ui_perms, ui_roles) %}<a href="/employees/registration-requests" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Employee Registrations</a>{% endif %}
{% if can_import_employee_hr(current_user, ui_perms, ui_roles) %}<a href="/employees/imports" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">HR Imports</a>{% endif %}
{% if can_manage_employee_leave_types(current_user, ui_perms, ui_roles) or can_manage_employee_leave_balances(current_user, ui_perms, ui_roles) %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Attendance &amp; Leave Setup</div>
{% if can_manage_employee_leave_types(current_user, ui_perms, ui_roles) %}<a href="/employees/leave-types" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Leave Types</a>{% endif %}
{% if can_manage_employee_leave_balances(current_user, ui_perms, ui_roles) %}<a href="/employees/leave-balances" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Leave Balances</a>{% endif %}
{% endif %}
{% if can_view_all_employee_documents(current_user, ui_perms, ui_roles) or can_manage_employee_document_types(current_user, ui_perms, ui_roles) %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Employee Documents</div>
{% if can_view_all_employee_documents(current_user, ui_perms, ui_roles) %}<a href="/employees/documents" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Employee Documents</a>{% endif %}
{% if can_manage_employee_document_types(current_user, ui_perms, ui_roles) %}<a href="/employees/document-types" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Document Types</a>{% endif %}
{% endif %}
{% if can_view_employee_onboarding(current_user, ui_perms, ui_roles) or can_manage_employee_onboarding(current_user, ui_perms, ui_roles) or can_view_employee_offboarding(current_user, ui_perms, ui_roles) %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Employee Lifecycle</div>
{% if can_view_employee_onboarding(current_user, ui_perms, ui_roles) %}<a href="/employees/onboarding" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Employee Onboarding</a>{% endif %}
{% if can_manage_employee_onboarding(current_user, ui_perms, ui_roles) %}<a href="/employees/onboarding-checklist" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Onboarding Checklist</a>{% endif %}
{% if can_view_employee_offboarding(current_user, ui_perms, ui_roles) %}<a href="/employees/offboarding" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Employee Offboarding</a>{% endif %}
{% endif %}
{% if can_manage_employee_payroll_structures(current_user, ui_perms, ui_roles) or can_run_employee_payroll(current_user, ui_perms, ui_roles) or can_view_employee_payroll(current_user, ui_perms, ui_roles) %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Payroll</div>
{% if can_manage_employee_payroll_structures(current_user, ui_perms, ui_roles) %}<a href="/employees/payroll/structures" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Salary Structures</a>{% endif %}
{% if can_run_employee_payroll(current_user, ui_perms, ui_roles) %}<a href="/employees/payroll/runs" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Payroll Runs</a>{% endif %}
{% if can_view_employee_payroll(current_user, ui_perms, ui_roles) %}<a href="/employees/payroll/payslips" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Employee Payslips</a>{% endif %}
{% endif %}
</div>
</details>
{% endif %}
{% if has_firm_admin_menu %}
<details class="group rounded-xl" {% if current_path.startswith('/system-settings/users') or current_path.startswith('/system-settings/rbac') or current_path.startswith('/services') or current_path.startswith('/clients') or current_path.startswith('/consultants') or current_path.startswith('/email') or current_path.startswith('/notice-cases') %}open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
<span>Firm Administration</span>
<span class="text-xs text-slate-400 transition group-open:rotate-90"></span>
</summary>
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
{% if "Firm Admin" in ui_roles or is_system_admin_user %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Firm Setup</div>
<a href="/system-settings/branding" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/system-settings/branding') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Branding & Identity</a>
{% if has_email_settings_menu %}<a href="/email/settings" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/email') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Email Settings</a>{% endif %}
{% endif %}
{% if has_firm_users_menu %}
<div class="pt-2 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Users</div>
{% if can_view_users(current_user, ui_perms, ui_roles) %}<a href="/system-settings/users" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/system-settings/users' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">User List</a>{% endif %}
{% if can_manage_users(current_user, ui_perms, ui_roles) %}<a href="/system-settings/users/new" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/system-settings/users/new') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Create User</a>{% endif %}
{% if can_view_rbac(current_user, ui_perms, ui_roles) %}<a href="/system-settings/rbac/roles" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Roles &amp; Permissions</a>{% endif %}
{% endif %}
{% if can_view_notice_cases(current_user, ui_perms, ui_roles) %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Compliance</div>
<a href="/notice-cases" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/notice-cases') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Notices &amp; Cases</a>
{% if can_manage_notice_cases(current_user, ui_perms, ui_roles) %}<a href="/notice-cases/new" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">New Notice / Case</a>{% endif %}
{% endif %}
{% if has_firm_services_menu %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Services</div>
<a href="/services" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Firm Services</a>
<a href="/services/catalogue" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Service Catalogue</a>
{% if is_system_admin_user %}<a href="/services/catalogue/new" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Create Catalogue Service</a>{% endif %}
<a href="/services/categories" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Service Categories</a>
{% if is_system_admin_user %}<a href="/services/defaults" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">System Default Tasks</a>{% endif %}
<a href="/services/templates" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Firm Task Templates</a>
<a href="/services/bulk-imports" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Bulk Imports</a>
{% endif %}
{% if has_firm_clients_menu %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Clients</div>
{% if can_view_clients(current_user, ui_perms, ui_roles) %}<a href="/clients" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Client List</a>{% endif %}
{% if can_manage_clients(current_user, ui_perms, ui_roles) %}<a href="/clients/new" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Add Client</a>{% endif %}
{% if can_manage_clients(current_user, ui_perms, ui_roles) %}<a href="/clients/import" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Import Clients</a>{% endif %}
{% if can_export_clients(current_user, ui_perms, ui_roles) %}<a href="/clients/export" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Export Clients</a>{% endif %}
{% endif %}
{% if has_firm_consultants_menu %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Consultants</div>
{% if can_view_consultants(current_user, ui_perms, ui_roles) %}<a href="/consultants" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Consultant List</a>{% endif %}
{% if can_manage_consultants(current_user, ui_perms, ui_roles) %}<a href="/consultants/new" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Add Consultant</a>{% endif %}
{% if can_manage_consultant_service_requests(current_user, ui_perms, ui_roles) %}<a href="/consultants/service-requests" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Service Requests</a>{% endif %}
{% if can_manage_consultant_conversions(current_user, ui_perms, ui_roles) %}<a href="/consultants/conversion-requests" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Conversions</a>{% endif %}
{% endif %}
{% if can_manage_local_storage_agent %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Local Storage</div>
<a href="/documents/storage-nodes" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/documents/storage-nodes') or current_path.startswith('/documents/branch-storage-dashboard') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Local Storage Agent</a>
{% endif %}
</div>
</details>
{% endif %}
{% if is_system_admin_user %}
<details class="group rounded-xl" {% if current_path.startswith('/domains') or current_path.startswith('/platform-billing') or current_path.startswith('/marketplace') %}open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
<span>Platform</span>
<span class="text-xs text-slate-400 transition group-open:rotate-90"></span>
</summary>
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Domain Configuration</div>
<a href="/domains" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/domains' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Domain Mappings</a>
<a href="/domains/tenant-subdomains" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/domains/tenant-subdomains') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Tenant Subdomains</a>
<a href="/domains/firm-domain" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/domains/firm-domain') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Firm Domains</a>
<a href="/domains/consultant-domains" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/domains/consultant-domains') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Consultant Domains</a>
<a href="/domains/verification" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/domains/verification') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">DNS Verification</a>
<a href="/domains/ssl" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/domains/ssl') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">SSL Status</a>
{% if can_view_platform_billing(current_user, ui_perms, ui_roles) or can_view_marketplace_leads(current_user, ui_perms, ui_roles) %}
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Platform Operations</div>
{% if can_view_platform_billing(current_user, ui_perms, ui_roles) %}<a href="/platform-billing" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Platform Billing</a>{% endif %}
{% if can_view_marketplace_leads(current_user, ui_perms, ui_roles) %}<a href="/marketplace" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">Marketplace</a>{% endif %}
{% endif %}
</div>
</details>
{% endif %}
{% if can_view_management_menus and (can_view_settings(current_user, ui_perms, ui_roles) or can_view_audit(current_user, ui_perms, ui_roles)) %}
<details class="group rounded-xl" {% if current_path.startswith('/system-settings') %}open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
<span>Core Setup</span>
<span class="text-xs text-slate-400 transition group-open:rotate-90"></span>
</summary>
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
{% if can_view_settings(current_user, ui_perms, ui_roles) %}
<a href="/system-settings" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/system-settings' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Dashboard</a>
{% endif %}
{% if can_view_tenants(current_user, ui_perms, ui_roles) %}
<a href="/system-settings/tenants" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/system-settings/tenants') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Audit Firms</a>
{% endif %}
{% if can_view_branches(current_user, ui_perms, ui_roles) %}
<a href="/system-settings/branches" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/system-settings/branches') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Branches</a>
{% endif %}
{% if can_view_settings(current_user, ui_perms, ui_roles) %}
<a href="/system-settings/financial-years" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/system-settings/financial-years') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Financial Years</a>
{% endif %}
{% if can_view_audit(current_user, ui_perms, ui_roles) %}
<a href="/system-settings/audit-logs" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/system-settings/audit-logs') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Audit Logs</a>
{% endif %}
</div>
</details>
{% endif %}
</nav>
{% endif %}
</aside>
<div class="min-h-screen">
<header class="border-b border-slate-200 bg-white/90 backdrop-blur">
<div class="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
<div class="flex items-start justify-between gap-4">
<div>
<h1 class="text-lg font-semibold text-slate-900">{{ title or "Module Workspace" }}</h1>
</div>
<div class="text-right text-sm">
{% if full_auth %}
<div class="flex items-start justify-end gap-3">
<div class="min-w-0">
<div class="font-medium text-slate-800">{{ current_user.full_name or current_user.email }}</div>
<div class="text-slate-500">{{ current_user.email }}</div>
{% if current_user.qualification or current_user.designation %}
<div class="text-xs text-slate-500">{{ current_user.qualification or '' }}{% if current_user.qualification and current_user.designation %} • {% endif %}{{ current_user.designation or '' }}</div>
{% endif %}
<div class="text-xs text-slate-400">
Your Firm: {{ current_firm_name }}
• Branch: {{ current_branch_name }}{% if active_financial_year %} • FY: {{ active_financial_year }}{% endif %}
</div>
<div class="mt-2 flex justify-end gap-2">
{% if "Consultant" in ui_roles %}
<a class="inline-flex rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50" href="/consultant/profile">My Profile</a>
{% elif "Client" in ui_roles %}
<a class="inline-flex rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50" href="/client/profile">My Profile</a>
{% else %}
<a class="inline-flex rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50" href="/employee/profile">My Profile</a>
{% endif %}
<a class="inline-flex rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50" href="/change-password">Change Password</a>
<a class="inline-flex rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50" href="/logout">Logout</a>
</div>
</div>
{% if current_user_photo_url %}
<img src="{{ current_user_photo_url }}" alt="Profile photo" class="h-12 w-12 shrink-0 rounded-2xl border border-slate-200 object-cover shadow-soft">
{% else %}
<div class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-brand-600 text-sm font-bold text-white shadow-soft">{{ current_user_initials }}</div>
{% endif %}
</div>
{% else %}
{% if domain_context.is_resolved %}
<div class="mb-2 text-xs text-slate-500">{{ current_firm_name }}{% if current_branch_name %} • {{ current_branch_name }}{% endif %}</div>
{% endif %}
<a class="inline-flex rounded-lg bg-brand-600 px-3 py-2 text-sm font-medium text-white hover:bg-brand-700" href="/login">Login</a>
{% endif %}
</div>
</div>
{% if full_auth and (can_switch_service_tenant(current_user, ui_perms, ui_roles) or can_switch_service_branch(current_user, ui_perms, ui_roles) or can_switch_employee_tenant(current_user, ui_perms, ui_roles) or can_switch_employee_branch(current_user, ui_perms, ui_roles) or can_view_settings(current_user, ui_perms, ui_roles)) %}
<div class="mt-4 flex flex-wrap items-end gap-4 border-t border-slate-200 pt-4">
{% if can_switch_service_tenant(current_user, ui_perms, ui_roles) or can_switch_employee_tenant(current_user, ui_perms, ui_roles) %}
{% set context_tenants = get_context_tenants(request, current_user, ui_perms, ui_roles) %}
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Active Audit Firm Context</label>
<select onchange="if(this.value){window.location.href=this.value;}" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
{% for tenant in context_tenants %}
<option value="/system-settings/context/tenant/{{ tenant.id }}" {% if tenant.id == active_tenant_id %}selected{% endif %}>{{ tenant.name }} ({{ tenant.code }})</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if can_switch_service_branch(current_user, ui_perms, ui_roles) or can_switch_employee_branch(current_user, ui_perms, ui_roles) %}
{% set context_branches = get_context_branches(request, current_user, ui_perms, ui_roles) %}
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Active Branch Context</label>
<select onchange="if(this.value){window.location.href=this.value;}" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="/system-settings/context/branch/0" {% if not active_branch_id %}selected{% endif %}>All Branches</option>
{% for branch in context_branches %}
<option value="/system-settings/context/branch/{{ branch.id }}" {% if branch.id == active_branch_id %}selected{% endif %}>{{ branch.name }} ({{ branch.code }})</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if can_view_settings(current_user, ui_perms, ui_roles) %}
{% set context_financial_years = get_context_financial_years(request, current_user, ui_perms, ui_roles) %}
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Active Financial Year</label>
<select onchange="if(this.value){window.location.href=this.value;}" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
{% for fy in context_financial_years %}
<option value="/system-settings/context/financial-year/{{ fy.year_code }}" {% if fy.year_code == active_financial_year %}selected{% endif %}>FY {{ fy.year_code }}{% if fy.is_current %} (Current){% endif %}{% if fy.is_locked %} - Locked{% endif %}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="text-xs text-slate-500">
Active Scope:
{{ current_firm_name }}
• {{ current_branch_name if active_branch_id else "All Branches" }}{% if active_financial_year %} • FY {{ active_financial_year }}{% endif %}
</div>
</div>
{% endif %}
</div>
</header>
<main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
{% if flash %}
<div class="mb-6 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 shadow-soft">
{{ flash }}
</div>
{% endif %}
{% block content %}{% endblock %}
</main>
</div>
</div>
{% if full_auth %}
<div id="af-alert-toast-container" class="fixed right-4 top-4 z-50 flex w-[min(24rem,calc(100vw-2rem))] flex-col gap-3"></div>
<script>
(function () {
const POLL_URL = "/alerts/poll";
const POLL_INTERVAL_MS = 45000;
const STORAGE_KEY = "af_popup_seen_alert_ids_v1";
const MAX_STORED_IDS = 100;
function readSeenIds() {
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.map(String) : [];
} catch (err) {
return [];
}
}
function writeSeenIds(ids) {
try {
const unique = Array.from(new Set(ids.map(String))).slice(-MAX_STORED_IDS);
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(unique));
} catch (err) {
// Ignore localStorage issues; alerts page remains available.
}
}
function updateHeaderCount(count) {
const badge = document.getElementById("af-alerts-header-count");
if (!badge) return;
const n = Number(count || 0);
badge.textContent = String(n);
if (n > 0) {
badge.classList.remove("hidden");
} else {
badge.classList.add("hidden");
}
}
function priorityClasses(priority) {
if (priority === "critical") return "border-red-200 bg-red-50 text-red-900";
if (priority === "high") return "border-amber-200 bg-amber-50 text-amber-900";
if (priority === "low") return "border-slate-200 bg-white text-slate-800";
return "border-blue-200 bg-blue-50 text-blue-900";
}
function showToast(alert) {
const container = document.getElementById("af-alert-toast-container");
if (!container || !alert) return;
const wrapper = document.createElement("div");
wrapper.className = "overflow-hidden rounded-2xl border shadow-soft " + priorityClasses(alert.priority);
wrapper.innerHTML = `
<div class="flex items-start gap-3 p-4">
<div class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white/80 text-base">🔔</div>
<div class="min-w-0 flex-1">
<div class="text-xs font-semibold uppercase tracking-wide opacity-75">New Alert</div>
<div class="mt-0.5 line-clamp-2 text-sm font-semibold"></div>
<div class="mt-1 line-clamp-3 text-xs opacity-80"></div>
<div class="mt-3 flex items-center gap-2">
<a class="inline-flex rounded-lg bg-white/90 px-3 py-1.5 text-xs font-semibold text-slate-800 hover:bg-white" href="#">View</a>
<button type="button" class="inline-flex rounded-lg px-3 py-1.5 text-xs font-semibold opacity-70 hover:bg-white/50">Dismiss</button>
</div>
</div>
</div>`;
wrapper.querySelector(".text-sm.font-semibold").textContent = alert.title || "Alert";
wrapper.querySelector(".text-xs.opacity-80").textContent = alert.message || "You have a new notification.";
const viewLink = wrapper.querySelector("a");
viewLink.href = alert.target_url || "/alerts";
wrapper.querySelector("button").addEventListener("click", function () { wrapper.remove(); });
container.prepend(wrapper);
window.setTimeout(function () {
if (wrapper && wrapper.parentNode) wrapper.remove();
}, 12000);
}
async function pollAlerts() {
try {
const response = await fetch(POLL_URL, {
method: "GET",
credentials: "same-origin",
headers: { "Accept": "application/json" }
});
if (!response.ok) return;
const data = await response.json();
updateHeaderCount(data.unread_count || 0);
const alerts = Array.isArray(data.alerts) ? data.alerts : [];
if (!alerts.length) return;
const seen = readSeenIds();
const newlySeen = seen.slice();
alerts.reverse().forEach(function (alert) {
const id = String(alert.id);
if (!seen.includes(id)) {
showToast(alert);
newlySeen.push(id);
}
});
writeSeenIds(newlySeen);
} catch (err) {
// Keep polling silent; the /alerts page remains the fallback.
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
pollAlerts();
window.setInterval(pollAlerts, POLL_INTERVAL_MS);
});
} else {
pollAlerts();
window.setInterval(pollAlerts, POLL_INTERVAL_MS);
}
})();
</script>
{% endif %}
</body>
</html>
+63
View File
@@ -0,0 +1,63 @@
{% macro page_shell(title, subtitle='', actions='') -%}
<div class="flex flex-col gap-4 rounded-3xl bg-white p-6 shadow-soft sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-xl font-semibold">{{ title }}</h2>
{% if subtitle %}<p class="mt-1 text-sm text-slate-500">{{ subtitle }}</p>{% endif %}
</div>
{% if actions %}<div>{{ actions | safe }}</div>{% endif %}
</div>
{%- endmacro %}
{% macro alert(message, tone='amber') -%}
<div class="rounded-2xl border px-4 py-3 text-sm shadow-soft {% if tone == 'rose' %}border-rose-200 bg-rose-50 text-rose-900{% elif tone == 'emerald' %}border-emerald-200 bg-emerald-50 text-emerald-900{% else %}border-amber-200 bg-amber-50 text-amber-900{% endif %}">
{{ message }}
</div>
{%- endmacro %}
{% macro badge(text, tone='slate') -%}
<span class="rounded-full px-2.5 py-1 text-xs font-medium {% if tone == 'emerald' %}bg-emerald-50 text-emerald-700{% elif tone == 'rose' %}bg-rose-50 text-rose-700{% elif tone == 'amber' %}bg-amber-50 text-amber-700{% elif tone == 'sky' %}bg-sky-50 text-sky-700{% elif tone == 'brand' %}bg-brand-50 text-brand-700{% else %}bg-slate-100 text-slate-700{% endif %}">{{ text }}</span>
{%- endmacro %}
{% macro search_bar(action, q='', per_page=10, extra='') -%}
<form method="get" action="{{ action }}" class="flex flex-col gap-3 border-b border-slate-200 bg-slate-50/80 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-1 gap-3">
<input type="text" name="q" value="{{ q }}" placeholder="Search" class="w-full rounded-2xl border border-slate-300 bg-white px-4 py-2.5 text-sm" />
<select name="per_page" class="rounded-2xl border border-slate-300 bg-white px-3 py-2.5 text-sm">
{% for size in [10,15,25,50] %}
<option value="{{ size }}" {% if per_page == size %}selected{% endif %}>{{ size }}/page</option>
{% endfor %}
</select>
{% if extra %}{{ extra | safe }}{% endif %}
</div>
<div class="flex gap-2">
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700">Apply</button>
</div>
</form>
{%- endmacro %}
{% macro empty_state(title, subtitle='') -%}
<div class="rounded-3xl border border-dashed border-slate-300 bg-white px-6 py-10 text-center text-slate-500 shadow-soft">
<div class="text-sm font-semibold text-slate-700">{{ title }}</div>
{% if subtitle %}<div class="mt-1 text-sm">{{ subtitle }}</div>{% endif %}
</div>
{%- endmacro %}
{% macro pagination(page_obj, base_url, query='') -%}
{% if page_obj and page_obj.pages > 1 %}
<div class="flex items-center justify-between border-t border-slate-200 bg-white px-4 py-3 text-sm text-slate-600">
<div>Page {{ page_obj.page }} of {{ page_obj.pages }} • {{ page_obj.total }} records</div>
<div class="flex items-center gap-2">
{% if page_obj.has_prev %}
<a href="{{ build_page_url(base_url, page_obj.page - 1, query) }}" class="rounded-xl border border-slate-300 px-3 py-2 hover:bg-slate-50">Previous</a>
{% else %}
<span class="rounded-xl border border-slate-200 px-3 py-2 text-slate-300">Previous</span>
{% endif %}
{% if page_obj.has_next %}
<a href="{{ build_page_url(base_url, page_obj.page + 1, query) }}" class="rounded-xl border border-slate-300 px-3 py-2 hover:bg-slate-50">Next</a>
{% else %}
<span class="rounded-xl border border-slate-200 px-3 py-2 text-slate-300">Next</span>
{% endif %}
</div>
</div>
{% endif %}
{%- endmacro %}
+23
View File
@@ -0,0 +1,23 @@
{% extends "base/layout.html" %}
{% block content %}
<div class="max-w-md rounded-2xl bg-white border p-6">
<h1 class="text-2xl font-semibold">Login</h1>
<p class="text-slate-600 mt-1 text-sm">Use bootstrap admin (first run). Lockout + CSRF are enabled.</p>
<form method="post" class="mt-5 grid gap-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<label class="grid gap-1">
<span class="text-sm text-slate-600">Email</span>
<input class="border rounded-xl px-3 py-2" name="email" type="email" required />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Password</span>
<input class="border rounded-xl px-3 py-2" name="password" type="password" required />
</label>
<button class="rounded-xl bg-slate-900 text-white px-4 py-2 text-sm mt-2" type="submit">Sign in</button>
</form>
</div>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
{% extends "base/layout.html" %}
{% block content %}
<div class="max-w-md rounded-2xl bg-white border p-6">
<h1 class="text-2xl font-semibold">OTP Verification</h1>
<p class="text-slate-600 mt-1 text-sm">Dev mode: OTP is printed in console.</p>
<form method="post" class="mt-5 grid gap-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<label class="grid gap-1">
<span class="text-sm text-slate-600">OTP Code</span>
<input class="border rounded-xl px-3 py-2" name="otp" inputmode="numeric" required />
</label>
<button class="rounded-xl bg-slate-900 text-white px-4 py-2 text-sm mt-2" type="submit">Verify</button>
</form>
</div>
{% endblock %}
@@ -0,0 +1,226 @@
{% extends "base/layout.html" %}
{% block content %}
<h1 class="text-2xl font-semibold">Edit Branch</h1>
<form class="mt-4 rounded-2xl bg-white border p-5 max-w-3xl" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<div class="grid gap-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<label class="grid gap-1">
<span class="text-sm text-slate-600">Branch Name</span>
<input class="border rounded-xl px-3 py-2" name="name" value="{{ branch.name }}" required />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Timezone</span>
<input class="border rounded-xl px-3 py-2" name="timezone" value="{{ branch.timezone }}" />
{% if settings.timezone_locked %}
<span class="text-xs text-amber-700">Timezone is locked by policy.</span>
{% endif %}
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<label class="grid gap-1">
<span class="text-sm text-slate-600">Office Start Time (HH:MM)</span>
<input class="border rounded-xl px-3 py-2" name="office_start_time" value="{{ branch.office_start_time or '' }}" placeholder="09:30" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Office End Time (HH:MM)</span>
<input class="border rounded-xl px-3 py-2" name="office_end_time" value="{{ branch.office_end_time or '' }}" placeholder="18:30" />
</label>
</div>
<div class="border-t pt-4">
<div class="font-semibold">SMTP Credentials</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<label class="grid gap-1">
<span class="text-sm text-slate-600">SMTP Host</span>
<input class="border rounded-xl px-3 py-2" name="smtp_host" value="{{ branch.smtp_host or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">SMTP Port</span>
<input class="border rounded-xl px-3 py-2" name="smtp_port" value="{{ branch.smtp_port or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">SMTP Username</span>
<input class="border rounded-xl px-3 py-2" name="smtp_username" value="{{ branch.smtp_username or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">SMTP Password</span>
<input class="border rounded-xl px-3 py-2" name="smtp_password" value="{{ branch.smtp_password or '' }}" />
</label>
<label class="flex items-center gap-2 mt-2">
<input type="checkbox" name="smtp_use_tls" {% if branch.smtp_use_tls %}checked{% endif %} />
<span class="text-sm text-slate-700">Use TLS</span>
</label>
</div>
</div>
<div class="border-t pt-4">
<div class="font-semibold">Local Storage Root</div>
<label class="grid gap-1 mt-3">
<span class="text-sm text-slate-600">Local Storage Path</span>
<input class="border rounded-xl px-3 py-2" name="local_storage_path" value="{{ branch.local_storage_path or '' }}" placeholder="D:\AuditFirm\AuditFirm\Branch" />
</label>
</div>
<div class="border-t pt-4">
<div class="font-semibold">Branch Identity (Compliance)</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<label class="grid gap-1">
<span class="text-sm text-slate-600">GSTIN</span>
<input class="border rounded-xl px-3 py-2" name="gstin" value="{{ settings.gstin or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">PAN</span>
<input class="border rounded-xl px-3 py-2" name="pan" value="{{ settings.pan or '' }}" />
</label>
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">Address Line 1</span>
<input class="border rounded-xl px-3 py-2" name="address_line1" value="{{ settings.address_line1 or '' }}" />
</label>
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">Address Line 2</span>
<input class="border rounded-xl px-3 py-2" name="address_line2" value="{{ settings.address_line2 or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">City</span>
<input class="border rounded-xl px-3 py-2" name="city" value="{{ settings.city or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">State</span>
<input class="border rounded-xl px-3 py-2" name="state" value="{{ settings.state or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">PIN Code</span>
<input class="border rounded-xl px-3 py-2" name="pin_code" value="{{ settings.pin_code or '' }}" />
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4">
<label class="grid gap-1">
<span class="text-sm text-slate-600">Letterhead Logo Path</span>
<input class="border rounded-xl px-3 py-2" name="letterhead_logo_path" value="{{ settings.letterhead_logo_path or '' }}" placeholder="D:\AuditFirm\Assets\logo.png" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Signature Image Path</span>
<input class="border rounded-xl px-3 py-2" name="letterhead_signature_path" value="{{ settings.letterhead_signature_path or '' }}" placeholder="D:\AuditFirm\Assets\sign.png" />
</label>
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">Stamp Image Path</span>
<input class="border rounded-xl px-3 py-2" name="letterhead_stamp_path" value="{{ settings.letterhead_stamp_path or '' }}" placeholder="D:\AuditFirm\Assets\stamp.png" />
</label>
</div>
</div>
<div class="border-t pt-4">
<div class="font-semibold">Working Days & Holidays</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<label class="grid gap-1">
<span class="text-sm text-slate-600">Working Days CSV</span>
<input class="border rounded-xl px-3 py-2" name="working_days_csv" value="{{ settings.working_days_csv }}" />
</label>
<label class="flex items-center gap-2 mt-7">
<input type="checkbox" name="timezone_locked" {% if settings.timezone_locked %}checked{% endif %} />
<span class="text-sm text-slate-700">Lock Timezone</span>
</label>
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">Holidays JSON</span>
<textarea class="border rounded-xl px-3 py-2 h-28" name="holidays_json">{{ settings.holidays_json }}</textarea>
</label>
</div>
</div>
<div class="border-t pt-4">
<div class="font-semibold">Email Policy</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<label class="grid gap-1">
<span class="text-sm text-slate-600">From Name</span>
<input class="border rounded-xl px-3 py-2" name="email_from_name" value="{{ settings.email_from_name or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">From Email</span>
<input class="border rounded-xl px-3 py-2" name="email_from_email" value="{{ settings.email_from_email or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Reply-To</span>
<input class="border rounded-xl px-3 py-2" name="email_reply_to" value="{{ settings.email_reply_to or '' }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Default CC (CSV)</span>
<input class="border rounded-xl px-3 py-2" name="default_cc_csv" value="{{ settings.default_cc_csv or '' }}" />
</label>
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">Default BCC (CSV)</span>
<input class="border rounded-xl px-3 py-2" name="default_bcc_csv" value="{{ settings.default_bcc_csv or '' }}" />
</label>
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">Email Signature (HTML)</span>
<textarea class="border rounded-xl px-3 py-2 h-28" name="email_signature_html">{{ settings.email_signature_html or '' }}</textarea>
</label>
</div>
</div>
<div class="border-t pt-4">
<div class="font-semibold">Storage Policy</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<label class="grid gap-1">
<span class="text-sm text-slate-600">Storage Mode</span>
<select class="border rounded-xl px-3 py-2" name="storage_mode">
{% set sm = settings.storage_mode %}
<option value="local_only" {% if sm == "local_only" %}selected{% endif %}>local_only</option>
<option value="cloud_only" {% if sm == "cloud_only" %}selected{% endif %}>cloud_only</option>
<option value="hybrid" {% if sm == "hybrid" %}selected{% endif %}>hybrid</option>
</select>
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Max File Size (MB)</span>
<input class="border rounded-xl px-3 py-2" name="max_file_mb" value="{{ settings.max_file_mb }}" />
</label>
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">Allowed Extensions (CSV)</span>
<input class="border rounded-xl px-3 py-2" name="allowed_ext_csv" value="{{ settings.allowed_ext_csv }}" />
</label>
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">Folder Template</span>
<input class="border rounded-xl px-3 py-2" name="folder_template" value="{{ settings.folder_template }}" />
<span class="text-xs text-slate-500">Tokens: {root}, {client_code}, {fy}, {service}</span>
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Retention Years</span>
<input class="border rounded-xl px-3 py-2" name="retention_years" value="{{ settings.retention_years }}" />
</label>
</div>
</div>
<div class="border-t pt-4">
<div class="font-semibold">Security Policy</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<label class="grid gap-1 md:col-span-2">
<span class="text-sm text-slate-600">OTP Required Roles (CSV)</span>
<input class="border rounded-xl px-3 py-2" name="otp_required_roles_csv" value="{{ settings.otp_required_roles_csv }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Session Duration (minutes)</span>
<input class="border rounded-xl px-3 py-2" name="session_duration_minutes" value="{{ settings.session_duration_minutes }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Lockout Attempts</span>
<input class="border rounded-xl px-3 py-2" name="lockout_attempts" value="{{ settings.lockout_attempts }}" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Lockout Minutes</span>
<input class="border rounded-xl px-3 py-2" name="lockout_minutes" value="{{ settings.lockout_minutes }}" />
</label>
</div>
</div>
<div class="flex gap-3">
<button class="rounded-xl bg-slate-900 text-white px-4 py-2 text-sm" type="submit">Save</button>
<a class="rounded-xl border px-4 py-2 text-sm" href="/system-settings/branches">Back</a>
</div>
</div>
</form>
{% endblock %}
@@ -0,0 +1,33 @@
{% extends "base/layout.html" %}
{% block content %}
<h1 class="text-2xl font-semibold">Branches</h1>
<div class="mt-4 rounded-2xl bg-white border overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-600">
<tr>
<th class="text-left p-3">Audit Firm ID</th>
<th class="text-left p-3">Audit Firm</th>
<th class="text-left p-3">Code</th>
<th class="text-left p-3">Name</th>
<th class="text-left p-3">Timezone</th>
<th class="text-left p-3">Actions</th>
</tr>
</thead>
<tbody>
{% for b in branches %}
<tr class="border-t">
<td class="p-3">{{ b.id }}</td>
<td class="p-3">{{ b.tenant_id }}</td>
<td class="p-3">{{ b.code }}</td>
<td class="p-3">{{ b.name }}</td>
<td class="p-3">{{ b.timezone }}</td>
<td class="p-3">
<a class="text-slate-900 underline" href="/system-settings/branches/{{ b.id }}/edit">Edit</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
@@ -0,0 +1,17 @@
{% extends "base/layout.html" %}
{% block content %}
<h1 class="text-2xl font-semibold">System Settings</h1>
<p class="text-slate-600 mt-1">Super-admin tools (audit firms, branches, policies).</p>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<a href="/system-settings/tenants" class="rounded-2xl bg-white border p-5 hover:shadow-sm">
<div class="font-semibold">Audit Firms</div>
<div class="text-sm text-slate-600 mt-1">Create and manage audit firms.</div>
</a>
<a href="/system-settings/branches" class="rounded-2xl bg-white border p-5 hover:shadow-sm">
<div class="font-semibold">Branches</div>
<div class="text-sm text-slate-600 mt-1">SMTP, storage, identity, calendar and security policies.</div>
</a>
</div>
{% endblock %}
@@ -0,0 +1,25 @@
{% extends "base/layout.html" %}
{% block content %}
<h1 class="text-2xl font-semibold">Create Audit Firm</h1>
<form class="mt-4 rounded-2xl bg-white border p-5 max-w-xl" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<div class="grid gap-3">
<label class="grid gap-1">
<span class="text-sm text-slate-600">Audit Audit Firm Code</span>
<input class="border rounded-xl px-3 py-2" name="code" required />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-600">Audit Audit Firm Name</span>
<input class="border rounded-xl px-3 py-2" name="name" required />
</label>
<div class="flex gap-3">
<button class="rounded-xl bg-slate-900 text-white px-4 py-2 text-sm" type="submit">Save</button>
<a class="rounded-xl border px-4 py-2 text-sm" href="/system-settings/tenants">Cancel</a>
</div>
</div>
</form>
{% endblock %}
@@ -0,0 +1,30 @@
{% extends "base/layout.html" %}
{% block content %}
<div class="flex items-center justify-between">
<h1 class="text-2xl font-semibold">Audit Firms</h1>
<a href="/system-settings/tenants/new" class="rounded-xl bg-slate-900 text-white px-4 py-2 text-sm">Add Audit Firm</a>
</div>
<div class="mt-4 rounded-2xl bg-white border overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-600">
<tr>
<th class="text-left p-3">Audit Firm ID</th>
<th class="text-left p-3">Audit Firm Code</th>
<th class="text-left p-3">Audit Firm Name</th>
<th class="text-left p-3">Active</th>
</tr>
</thead>
<tbody>
{% for t in tenants %}
<tr class="border-t">
<td class="p-3">{{ t.id }}</td>
<td class="p-3">{{ t.code }}</td>
<td class="p-3">{{ t.name }}</td>
<td class="p-3">{{ "Yes" if t.is_active else "No" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}