Add client dashboard ajax tabs v1
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% include "modules/clients/templates/clients/_client_tabs.html" %}
|
||||
<div class="space-y-6">
|
||||
<section class="rounded-3xl bg-gradient-to-r from-brand-700 to-slate-900 p-6 text-white shadow-soft">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-brand-100">Client Portal</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold">My Compliance & Firm Communication</h2>
|
||||
<p class="mt-2 max-w-3xl text-sm text-brand-100">Track your compliance status, pending actions, required documents, messages and firm updates.</p>
|
||||
<p class="mt-2 max-w-3xl text-sm text-brand-100">Track pending actions, service status, documents, messages, bills and firm updates from one place.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if client_row %}
|
||||
<a href="/client/profile" class="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-brand-700 hover:bg-brand-50">Update My Profile</a>
|
||||
<a href="/client/documents" class="rounded-xl border border-white/30 px-4 py-2 text-sm font-semibold text-white hover:bg-white/10">Upload Documents</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if client_row %}<a href="/client/profile" class="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-brand-700 hover:bg-brand-50">Update My Profile</a>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -19,75 +23,62 @@
|
||||
We could not find a client master linked to your login email in the current audit firm. Please contact your firm admin to map this login to the correct client record.
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
<a href="/client/compliance" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Active Compliance</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ total_engagements or 0 }}</div><div class="mt-1 text-xs text-slate-500">Services / filings</div></a>
|
||||
<a href="/client/compliance" class="af-metric-card border-amber-200 bg-amber-50 hover:border-amber-300"><div class="text-xs font-semibold uppercase text-amber-700">Pending Action</div><div class="mt-2 text-3xl font-semibold text-amber-700">{{ pending_from_client or 0 }}</div><div class="mt-1 text-xs text-amber-700">Required from you</div></a>
|
||||
<a href="/client/compliance" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">With Firm</div><div class="mt-2 text-3xl font-semibold text-brand-700">{{ with_firm or 0 }}</div><div class="mt-1 text-xs text-slate-500">Being handled</div></a>
|
||||
<a href="/client/documents" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Documents</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ recent_documents|length if recent_documents else 0 }}</div><div class="mt-1 text-xs text-slate-500">Recent uploads</div></a>
|
||||
<a href="/client/billing" class="af-metric-card border-amber-200 bg-amber-50 hover:border-amber-300"><div class="text-xs font-semibold uppercase text-amber-700">Outstanding Bills</div><div class="mt-2 text-3xl font-semibold text-amber-700">₹ {{ '%.2f'|format(billing_outstanding_amount or 0) }}</div><div class="mt-1 text-xs text-amber-700">{{ billing_open_count or 0 }} open bill(s)</div></a>
|
||||
</section>
|
||||
{% set client_tabs = [
|
||||
('overview','Overview'),
|
||||
('pending','Pending From Me'),
|
||||
('services','Services'),
|
||||
('documents','Documents'),
|
||||
('billing','Invoices & Payments'),
|
||||
('messages','Messages'),
|
||||
('reports','Reports')
|
||||
] %}
|
||||
|
||||
<section class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_380px]">
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3"><div><h3 class="text-lg font-semibold text-slate-900">Compliance Status</h3><p class="mt-1 text-sm text-slate-500">Simple client-facing status of your active services.</p></div><a href="/client/compliance" class="af-btn af-btn-primary">View All</a></div>
|
||||
<div class="mt-5 grid gap-3 md:grid-cols-4 text-sm">
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3"><div class="text-amber-700">Pending from You</div><div class="mt-1 text-2xl font-semibold text-amber-700">{{ pending_from_client or 0 }}</div></div>
|
||||
<div class="rounded-2xl border border-brand-200 bg-brand-50 px-4 py-3"><div class="text-brand-700">With Firm</div><div class="mt-1 text-2xl font-semibold text-brand-700">{{ with_firm or 0 }}</div></div>
|
||||
<div class="rounded-2xl border border-blue-200 bg-blue-50 px-4 py-3"><div class="text-blue-700">Clarification</div><div class="mt-1 text-2xl font-semibold text-blue-700">{{ clarification_required or 0 }}</div></div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3"><div class="text-emerald-700">Completed</div><div class="mt-1 text-2xl font-semibold text-emerald-700">{{ completed_engagements or 0 }}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 space-y-3">
|
||||
{% for row in due_soon_engagements[:6] %}
|
||||
<a href="/client/engagements/{{ row.id }}" class="block rounded-2xl border border-slate-200 p-4 hover:bg-slate-50">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div><div class="font-semibold text-slate-900">{{ row.catalogue.service_name if row.catalogue else 'Service' }}</div><div class="text-xs text-slate-500">FY {{ row.financial_year }}{% if row.assessment_year %} • AY {{ row.assessment_year }}{% endif %}</div></div>
|
||||
<div class="text-sm text-slate-600">Due: {{ row.current_due_date.strftime('%d-%m-%Y') if row.current_due_date else '-' }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No active compliance items found.</div>
|
||||
<section class="rounded-3xl border border-slate-200 bg-white p-2 shadow-soft">
|
||||
<div class="flex gap-2 overflow-x-auto" id="client-dashboard-tabs">
|
||||
{% for code, label in client_tabs %}
|
||||
<button type="button" data-tab="{{ code }}" class="client-dashboard-tab whitespace-nowrap rounded-2xl px-4 py-2 text-sm font-semibold transition {% if (active_tab|default('overview')) == code %}bg-brand-600 text-white{% else %}text-slate-600 hover:bg-slate-100{% endif %}">{{ label }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3"><h3 class="text-lg font-semibold text-slate-900">Latest Messages</h3><a href="/client/messages" class="text-sm font-semibold text-brand-700 hover:underline">View all</a></div>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for note in client_visible_comments[:5] %}
|
||||
<div class="rounded-2xl border border-slate-200 p-4 text-sm"><div class="font-semibold text-slate-900">{{ note.task.task_name if note.task else 'Message' }}</div><p class="mt-2 text-slate-700">{{ note.message }}</p><div class="mt-2 text-xs text-slate-500">{{ note.created_at_utc.strftime('%d-%m-%Y %I:%M %p') if note.created_at_utc else '' }}</div></div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No messages yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-6">
|
||||
<div class="af-card">
|
||||
<h3 class="text-base font-semibold text-slate-900">My Client Profile</h3>
|
||||
<div class="mt-4 space-y-3 text-sm">
|
||||
<div><div class="text-xs uppercase tracking-wide text-slate-500">Client</div><div class="font-semibold">{{ client_row.client_name }}</div></div>
|
||||
<div><div class="text-xs uppercase tracking-wide text-slate-500">PAN / GSTIN</div><div>{{ client_row.pan or '-' }}{% if client_row.gstin %} / {{ client_row.gstin }}{% endif %}</div></div>
|
||||
<div><div class="text-xs uppercase tracking-wide text-slate-500">Firm Contact</div><div>{{ client_row.partner_name or 'Firm team' }}</div></div>
|
||||
<div><div class="text-xs uppercase tracking-wide text-slate-500">Branch</div><div>{{ client_row.branch_name or '-' }}</div></div>
|
||||
</div>
|
||||
<a href="/client/profile" class="mt-5 inline-flex af-btn af-btn-secondary">Update Profile</a>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<h3 class="font-semibold text-slate-900">Quick Actions</h3>
|
||||
<div class="mt-4 grid gap-2 text-sm">
|
||||
<a href="/client/compliance" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">View My Compliance</a>
|
||||
<a href="/client/documents" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Upload / View Documents</a>
|
||||
<a href="/client/messages" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Messages from Firm</a>
|
||||
<a href="/client/billing" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">View Bills & Receipts</a>
|
||||
{% if billing_latest_due_invoice %}<a href="/client/billing/{{ billing_latest_due_invoice.id }}/pay-now" class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 font-semibold text-amber-700 hover:bg-amber-100">Pay Latest Due</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section id="client-dashboard-panel" class="min-h-[240px]">
|
||||
{% include "modules/clients/templates/clients/portal_partials/overview.html" %}
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const panel = document.getElementById('client-dashboard-panel');
|
||||
const buttons = Array.from(document.querySelectorAll('.client-dashboard-tab'));
|
||||
if (!panel || !buttons.length) return;
|
||||
|
||||
function setActive(tab) {
|
||||
buttons.forEach((btn) => {
|
||||
const active = btn.dataset.tab === tab;
|
||||
btn.classList.toggle('bg-brand-600', active);
|
||||
btn.classList.toggle('text-white', active);
|
||||
btn.classList.toggle('text-slate-600', !active);
|
||||
btn.classList.toggle('hover:bg-slate-100', !active);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTab(tab) {
|
||||
setActive(tab);
|
||||
panel.innerHTML = '<div class="rounded-3xl border border-slate-200 bg-white p-8 text-center text-sm text-slate-500 shadow-soft">Loading...</div>';
|
||||
try {
|
||||
const res = await fetch('/client/dashboard/tab/' + encodeURIComponent(tab), {headers: {'X-Requested-With':'fetch'}});
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
panel.innerHTML = await res.text();
|
||||
if (history && history.replaceState) {
|
||||
history.replaceState(null, '', '/client/dashboard?tab=' + encodeURIComponent(tab));
|
||||
}
|
||||
} catch (err) {
|
||||
panel.innerHTML = '<div class="rounded-3xl border border-red-200 bg-red-50 p-6 text-sm text-red-700 shadow-soft">Unable to load this tab. Please refresh the page or open the linked full page.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
buttons.forEach((btn) => btn.addEventListener('click', () => loadTab(btn.dataset.tab || 'overview')));
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="space-y-6">
|
||||
<section class="grid gap-4 md:grid-cols-3">
|
||||
<a href="/client/billing" class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Total Bills</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ billing_total_count or 0 }}</div></a>
|
||||
<a href="/client/billing?include_paid=no" class="af-metric-card border-amber-200 bg-amber-50"><div class="text-xs font-semibold uppercase text-amber-700">Open Bills</div><div class="mt-2 text-3xl font-semibold text-amber-700">{{ billing_open_count or 0 }}</div></a>
|
||||
<a href="/client/billing" class="af-metric-card border-amber-200 bg-amber-50"><div class="text-xs font-semibold uppercase text-amber-700">Outstanding</div><div class="mt-2 text-2xl font-semibold text-amber-700">₹ {{ '%.2f'|format(billing_outstanding_amount or 0) }}</div></a>
|
||||
</section>
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3"><div><h3 class="text-lg font-semibold text-slate-900">Invoices & Payments</h3><p class="mt-1 text-sm text-slate-500">View bills, receipts and payment options.</p></div><a href="/client/billing" class="af-btn af-btn-primary">Open Billing</a></div>
|
||||
<div class="mt-5 overflow-x-auto">
|
||||
<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">Invoice</th><th class="px-4 py-3">Date</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Balance</th><th class="px-4 py-3">Action</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
{% for invoice in billing_invoices[:8] if billing_invoices is defined %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-semibold text-slate-900">{{ invoice.invoice_no }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ invoice.invoice_date.strftime('%d-%m-%Y') if invoice.invoice_date else '-' }}</td>
|
||||
<td class="px-4 py-3"><span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">{{ invoice.status }}</span></td>
|
||||
<td class="px-4 py-3 text-slate-600">₹ {{ '%.2f'|format(invoice.balance_amount or 0) }}</td>
|
||||
<td class="px-4 py-3"><a href="/client/billing/{{ invoice.id }}" class="font-semibold text-brand-700 hover:underline">View</a>{% if invoice.balance_amount and invoice.balance_amount > 0 %}<span class="mx-1 text-slate-300">|</span><a href="/client/billing/{{ invoice.id }}/pay-now" class="font-semibold text-amber-700 hover:underline">Pay</a>{% endif %}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">No invoices found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3"><div><h3 class="text-lg font-semibold text-slate-900">Engagement Documents</h3><p class="mt-1 text-sm text-slate-500">Recent working documents and files shared for active services.</p></div><a href="/client/documents" class="af-btn af-btn-primary">Open Documents</a></div>
|
||||
<div class="mt-5 space-y-3">
|
||||
{% for doc in recent_documents or [] %}
|
||||
<a href="/client/documents" class="block rounded-2xl border border-slate-200 p-4 hover:bg-slate-50">
|
||||
<div class="font-semibold text-slate-900">{{ doc.document_title or doc.document_type or 'Document' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">{{ doc.financial_year or '-' }}{% if doc.updated_at_utc %} • Updated {{ doc.updated_at_utc.strftime('%d-%m-%Y') }}{% endif %}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No recent engagement documents found.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3"><div><h3 class="text-lg font-semibold text-slate-900">Permanent Documents</h3><p class="mt-1 text-sm text-slate-500">Profile and permanent records shared with the firm.</p></div><a href="/client/documents" class="af-btn af-btn-secondary">View All</a></div>
|
||||
<div class="mt-5 space-y-3">
|
||||
{% for doc in permanent_documents[:8] if permanent_documents is defined %}
|
||||
<a href="/client/documents" class="block rounded-2xl border border-slate-200 p-4 hover:bg-slate-50">
|
||||
<div class="font-semibold text-slate-900">{{ doc.document_title or doc.document_type or doc.category or 'Permanent Document' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">{{ doc.category or '-' }}{% if doc.updated_at_utc %} • Updated {{ doc.updated_at_utc.strftime('%d-%m-%Y') }}{% endif %}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No permanent documents found.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="af-card">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">Messages from Firm</h3><p class="mt-1 text-sm text-slate-500">Client-visible task comments, clarifications and updates.</p></div>
|
||||
<a href="/client/messages" class="af-btn af-btn-primary">Open Messages</a>
|
||||
</div>
|
||||
<div class="mt-5 space-y-3">
|
||||
{% for note in client_visible_comments or [] %}
|
||||
<div class="rounded-2xl border border-slate-200 p-4 text-sm">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div class="font-semibold text-slate-900">{{ note.task.task_name if note.task else (note.subscription.catalogue.service_name if note.subscription and note.subscription.catalogue else 'Message') }}</div>
|
||||
<div class="text-xs text-slate-500">{{ note.created_at_utc.strftime('%d-%m-%Y %I:%M %p') if note.created_at_utc else '' }}</div>
|
||||
</div>
|
||||
<p class="mt-2 text-slate-700">{{ note.message }}</p>
|
||||
{% if note.task %}<a href="/client/engagements/{{ note.task.subscription_id }}" class="mt-3 inline-flex text-xs font-semibold text-brand-700 hover:underline">Open related service</a>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No messages yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
<div class="space-y-6">
|
||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
<a href="/client/compliance" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Active Services</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ total_engagements or 0 }}</div><div class="mt-1 text-xs text-slate-500">Services / filings</div></a>
|
||||
<button type="button" data-tab="pending" class="client-dashboard-tab af-metric-card border-amber-200 bg-amber-50 text-left hover:border-amber-300"><div class="text-xs font-semibold uppercase text-amber-700">Pending From Me</div><div class="mt-2 text-3xl font-semibold text-amber-700">{{ pending_from_client or 0 }}</div><div class="mt-1 text-xs text-amber-700">Action required</div></button>
|
||||
<a href="/client/compliance" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">With Firm</div><div class="mt-2 text-3xl font-semibold text-brand-700">{{ with_firm or 0 }}</div><div class="mt-1 text-xs text-slate-500">Being handled</div></a>
|
||||
<a href="/client/documents" class="af-metric-card hover:border-brand-200"><div class="text-xs font-semibold uppercase text-slate-500">Recent Documents</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ recent_documents|length if recent_documents else 0 }}</div><div class="mt-1 text-xs text-slate-500">Latest documents</div></a>
|
||||
<a href="/client/billing" class="af-metric-card border-amber-200 bg-amber-50 hover:border-amber-300"><div class="text-xs font-semibold uppercase text-amber-700">Outstanding Bills</div><div class="mt-2 text-2xl font-semibold text-amber-700">₹ {{ '%.2f'|format(billing_outstanding_amount or 0) }}</div><div class="mt-1 text-xs text-amber-700">{{ billing_open_count or 0 }} open bill(s)</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 items-center justify-between gap-3"><div><h3 class="text-lg font-semibold text-slate-900">Needs Your Attention</h3><p class="mt-1 text-sm text-slate-500">Open items where the firm may need documents, replies or confirmation from you.</p></div><a href="/client/compliance" class="af-btn af-btn-primary">View Services</a></div>
|
||||
<div class="mt-5 space-y-3">
|
||||
{% set shown = namespace(count=0) %}
|
||||
{% for task in task_rows or [] %}
|
||||
{% if shown.count < 6 and (task.status or 'pending') in ['pending','blocked','client_pending','clarification_required','rework'] %}
|
||||
{% set shown.count = shown.count + 1 %}
|
||||
<a href="/client/engagements/{{ task.subscription_id }}" class="block rounded-2xl border border-amber-200 bg-amber-50 p-4 hover:bg-amber-100">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div><div class="font-semibold text-slate-900">{{ task.task_name or (task.catalogue.service_name if task.catalogue else 'Task') }}</div><div class="text-xs text-amber-700">{{ (task.status or 'pending')|replace('_',' ')|title }}{% if task.financial_year %} • FY {{ task.financial_year }}{% endif %}</div></div>
|
||||
<div class="text-sm text-slate-600">Due: {{ task.internal_target_date.strftime('%d-%m-%Y') if task.internal_target_date else '-' }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if shown.count == 0 %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No urgent action is pending from you.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3"><div><h3 class="text-lg font-semibold text-slate-900">Upcoming Compliance</h3><p class="mt-1 text-sm text-slate-500">Nearest active service due dates.</p></div><a href="/client/compliance" class="text-sm font-semibold text-brand-700 hover:underline">Open Compliance</a></div>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for row in due_soon_engagements[:6] %}
|
||||
<a href="/client/engagements/{{ row.id }}" class="block rounded-2xl border border-slate-200 p-4 hover:bg-slate-50">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div><div class="font-semibold text-slate-900">{{ row.catalogue.service_name if row.catalogue else 'Service' }}</div><div class="text-xs text-slate-500">FY {{ row.financial_year }}{% if row.assessment_year %} • AY {{ row.assessment_year }}{% endif %}</div></div>
|
||||
<div class="text-sm text-slate-600">Due: {{ row.current_due_date.strftime('%d-%m-%Y') if row.current_due_date else '-' }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No upcoming compliance due dates found.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-6">
|
||||
<div class="af-card">
|
||||
<h3 class="text-base font-semibold text-slate-900">My Client Profile</h3>
|
||||
<div class="mt-4 space-y-3 text-sm">
|
||||
<div><div class="text-xs uppercase tracking-wide text-slate-500">Client</div><div class="font-semibold">{{ client_row.client_name }}</div></div>
|
||||
<div><div class="text-xs uppercase tracking-wide text-slate-500">PAN / GSTIN</div><div>{{ client_row.pan or '-' }}{% if client_row.gstin %} / {{ client_row.gstin }}{% endif %}</div></div>
|
||||
<div><div class="text-xs uppercase tracking-wide text-slate-500">Firm Contact</div><div>{{ client_row.partner_name or 'Firm team' }}</div></div>
|
||||
<div><div class="text-xs uppercase tracking-wide text-slate-500">Branch</div><div>{{ client_row.branch_name or '-' }}</div></div>
|
||||
</div>
|
||||
<a href="/client/profile" class="mt-5 inline-flex af-btn af-btn-secondary">Update Profile</a>
|
||||
</div>
|
||||
|
||||
<div class="af-card">
|
||||
<h3 class="font-semibold text-slate-900">Quick Actions</h3>
|
||||
<div class="mt-4 grid gap-2 text-sm">
|
||||
<a href="/client/compliance" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">View My Services</a>
|
||||
<a href="/client/documents" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Upload / View Documents</a>
|
||||
<a href="/client/messages" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">Messages from Firm</a>
|
||||
<a href="/client/billing" class="rounded-xl border border-slate-200 px-4 py-3 font-semibold text-slate-700 hover:bg-slate-50">View Bills & Receipts</a>
|
||||
{% if billing_latest_due_invoice %}<a href="/client/billing/{{ billing_latest_due_invoice.id }}/pay-now" class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 font-semibold text-amber-700 hover:bg-amber-100">Pay Latest Due</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="af-card">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Pending From Me</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Documents, replies or clarifications currently expected from you.</p>
|
||||
</div>
|
||||
<a href="/client/messages" class="af-btn af-btn-secondary">Open Messages</a>
|
||||
</div>
|
||||
<div class="mt-5 overflow-x-auto">
|
||||
<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">Service / Task</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">FY / Period</th><th class="px-4 py-3">Due</th><th class="px-4 py-3">Action</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
{% set shown = namespace(count=0) %}
|
||||
{% for task in task_rows or [] %}
|
||||
{% if (task.status or 'pending') in ['pending','blocked','client_pending','clarification_required','rework'] %}
|
||||
{% set shown.count = shown.count + 1 %}
|
||||
<tr>
|
||||
<td class="px-4 py-3"><div class="font-semibold text-slate-900">{{ task.task_name or (task.catalogue.service_name if task.catalogue else 'Task') }}</div><div class="text-xs text-slate-500">{{ task.catalogue.service_name if task.catalogue else 'Service' }}</div></td>
|
||||
<td class="px-4 py-3"><span class="rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700">{{ (task.status or 'pending')|replace('_',' ')|title }}</span></td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ task.financial_year or '-' }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ task.internal_target_date.strftime('%d-%m-%Y') if task.internal_target_date else '-' }}</td>
|
||||
<td class="px-4 py-3"><a href="/client/engagements/{{ task.subscription_id }}" class="font-semibold text-brand-700 hover:underline">Open</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if shown.count == 0 %}
|
||||
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">No pending action from you.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="space-y-6">
|
||||
<div class="af-card">
|
||||
<h3 class="text-lg font-semibold text-slate-900">My Reports</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Client-facing report shortcuts. Detailed exports continue through the existing pages.</p>
|
||||
<div class="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<a href="/client/compliance" class="rounded-2xl border border-slate-200 p-5 hover:border-brand-200 hover:bg-brand-50"><div class="font-semibold text-slate-900">Compliance Status Report</div><p class="mt-2 text-sm text-slate-500">Status of GST, ITR, TDS, audit, notice and other active services.</p></a>
|
||||
<a href="/client/compliance" class="rounded-2xl border border-slate-200 p-5 hover:border-brand-200 hover:bg-brand-50"><div class="font-semibold text-slate-900">Pending Document Report</div><p class="mt-2 text-sm text-slate-500">Items where your input or documents are required.</p></a>
|
||||
<a href="/client/documents" class="rounded-2xl border border-slate-200 p-5 hover:border-brand-200 hover:bg-brand-50"><div class="font-semibold text-slate-900">Document Register</div><p class="mt-2 text-sm text-slate-500">Permanent and engagement documents visible to your login.</p></a>
|
||||
<a href="/client/billing" class="rounded-2xl border border-slate-200 p-5 hover:border-brand-200 hover:bg-brand-50"><div class="font-semibold text-slate-900">Invoice Outstanding Report</div><p class="mt-2 text-sm text-slate-500">Open bills, receipts and payment history.</p></a>
|
||||
<a href="/client/messages" class="rounded-2xl border border-slate-200 p-5 hover:border-brand-200 hover:bg-brand-50"><div class="font-semibold text-slate-900">Messages & Clarifications</div><p class="mt-2 text-sm text-slate-500">Firm messages and clarification history.</p></a>
|
||||
<a href="/client/profile" class="rounded-2xl border border-slate-200 p-5 hover:border-brand-200 hover:bg-brand-50"><div class="font-semibold text-slate-900">Profile Details</div><p class="mt-2 text-sm text-slate-500">Contact and statutory details maintained in the firm.</p></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="af-card">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div><h3 class="text-lg font-semibold text-slate-900">My Services</h3><p class="mt-1 text-sm text-slate-500">Client-facing status of services handled by the firm.</p></div>
|
||||
<a href="/client/compliance" class="af-btn af-btn-primary">Full Compliance Page</a>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-4 md:grid-cols-4 text-sm">
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3"><div class="text-amber-700">Pending From You</div><div class="mt-1 text-2xl font-semibold text-amber-700">{{ pending_from_client or 0 }}</div></div>
|
||||
<div class="rounded-2xl border border-brand-200 bg-brand-50 px-4 py-3"><div class="text-brand-700">With Firm</div><div class="mt-1 text-2xl font-semibold text-brand-700">{{ with_firm or 0 }}</div></div>
|
||||
<div class="rounded-2xl border border-blue-200 bg-blue-50 px-4 py-3"><div class="text-blue-700">Clarification</div><div class="mt-1 text-2xl font-semibold text-blue-700">{{ clarification_required or 0 }}</div></div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3"><div class="text-emerald-700">Completed</div><div class="mt-1 text-2xl font-semibold text-emerald-700">{{ completed_engagements or 0 }}</div></div>
|
||||
</div>
|
||||
<div class="mt-5 overflow-x-auto">
|
||||
<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">Service</th><th class="px-4 py-3">FY / AY</th><th class="px-4 py-3">Due Date</th><th class="px-4 py-3">Assigned Team</th><th class="px-4 py-3">Action</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
{% for row in engagements or [] %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-semibold text-slate-900">{{ row.catalogue.service_name if row.catalogue else 'Service' }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">FY {{ row.financial_year or '-' }}{% if row.assessment_year %} / AY {{ row.assessment_year }}{% endif %}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ row.current_due_date.strftime('%d-%m-%Y') if row.current_due_date else '-' }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ row.assigned_partner.full_name if row.assigned_partner else 'Firm team' }}</td>
|
||||
<td class="px-4 py-3"><a href="/client/engagements/{{ row.id }}" class="font-semibold text-brand-700 hover:underline">View</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">No active services found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -914,6 +914,30 @@ def _active_financial_year(request: Request) -> str | None:
|
||||
value = (value or "").strip()
|
||||
return value or None
|
||||
|
||||
CLIENT_PORTAL_TABS = {
|
||||
"overview": "modules/clients/templates/clients/portal_partials/overview.html",
|
||||
"pending": "modules/clients/templates/clients/portal_partials/pending.html",
|
||||
"services": "modules/clients/templates/clients/portal_partials/services.html",
|
||||
"documents": "modules/clients/templates/clients/portal_partials/documents.html",
|
||||
"billing": "modules/clients/templates/clients/portal_partials/billing.html",
|
||||
"messages": "modules/clients/templates/clients/portal_partials/messages.html",
|
||||
"reports": "modules/clients/templates/clients/portal_partials/reports.html",
|
||||
}
|
||||
|
||||
|
||||
def _client_dashboard_payload(db, client_row, financial_year: str | None = None) -> dict:
|
||||
summary = build_client_portal_summary(db, client_row, financial_year=financial_year)
|
||||
billing_summary = build_client_billing_summary(db, client_row, financial_year=financial_year)
|
||||
return {
|
||||
"client_visible_comments": list_client_visible_comments(db, client_row, limit=20, financial_year=financial_year),
|
||||
"recent_documents": list_client_engagement_documents(db, client_row, financial_year=financial_year)[:12],
|
||||
"permanent_documents": list_client_permanent_documents(db, client_row),
|
||||
"auditor_card": build_client_auditor_card(db, client_row),
|
||||
**summary,
|
||||
**billing_summary,
|
||||
}
|
||||
|
||||
|
||||
def _portal_context(request: Request, db, current_user, **extra):
|
||||
ctx = {
|
||||
"request": request,
|
||||
@@ -964,6 +988,43 @@ def client_portal_dashboard(request: Request):
|
||||
db.close()
|
||||
|
||||
|
||||
@portal_router.get("/dashboard/tab/{tab_name}")
|
||||
def client_portal_dashboard_tab(request: Request, tab_name: str):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
if not current_user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
|
||||
client_row, redirect = _portal_client_or_redirect(request, db, current_user)
|
||||
if redirect:
|
||||
return redirect
|
||||
|
||||
active_tab = tab_name if tab_name in CLIENT_PORTAL_TABS else "overview"
|
||||
if not client_row:
|
||||
return templates.TemplateResponse(
|
||||
CLIENT_PORTAL_TABS["overview"],
|
||||
_portal_context(request, db, current_user, client_row=None, active_tab=active_tab),
|
||||
status_code=200,
|
||||
)
|
||||
financial_year = _active_financial_year(request)
|
||||
payload = _client_dashboard_payload(db, client_row, financial_year=financial_year)
|
||||
return templates.TemplateResponse(
|
||||
CLIENT_PORTAL_TABS[active_tab],
|
||||
_portal_context(
|
||||
request,
|
||||
db,
|
||||
current_user,
|
||||
client_row=client_row,
|
||||
active_tab=active_tab,
|
||||
active_financial_year=financial_year,
|
||||
**payload,
|
||||
),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@portal_router.get("/billing")
|
||||
def client_portal_billing(request: Request, q: str = "", include_paid: str = "yes"):
|
||||
db = CommonSessionLocal()
|
||||
|
||||
Reference in New Issue
Block a user