Upgrade manager dashboard and wizards to v2

This commit is contained in:
A R R R Associates
2026-07-05 20:04:56 +05:30
parent f0fd8fb2ea
commit 21c71c7172
8 changed files with 115 additions and 12 deletions
+1 -1
View File
@@ -1 +1 @@
"""Manager Dashboard V1 module."""
"""Manager Dashboard V2 module."""
+57 -5
View File
@@ -101,6 +101,28 @@ def _status_label(status: str | None) -> str:
return (status or "pending").replace("_", " ").replace("-", " ").title()
def _days_overdue(due_date) -> int:
if not due_date:
return 0
delta = (date.today() - due_date).days
return delta if delta > 0 else 0
def _age_bucket(days: int) -> str:
if days >= 30:
return "30+ days"
if days >= 15:
return "15-29 days"
if days >= 8:
return "8-14 days"
if days >= 4:
return "4-7 days"
if days >= 1:
return "1-3 days"
return "Current"
def _task_href(task: ClientServiceTaskInstance) -> str:
task_id = getattr(task, "id", None)
if task_id:
@@ -136,6 +158,8 @@ def _task_row(task: ClientServiceTaskInstance) -> dict[str, Any]:
"comment_count": len([c for c in getattr(task, "comments", []) if not getattr(c, "is_deleted", False)]),
"is_overdue": bool(due_date and due_date < today and _is_open(status)),
"is_due_today": bool(due_date and due_date == today and _is_open(status)),
"days_overdue": _days_overdue(due_date) if _is_open(status) else 0,
"age_bucket": _age_bucket(_days_overdue(due_date)) if _is_open(status) else "Closed",
"href": _task_href(task),
}
@@ -239,9 +263,10 @@ def _client_rows(db: Session, scope, task_rows: list[dict[str, Any]]) -> list[di
def _report_cards() -> list[dict[str, str]]:
return [
{"group": "Execution", "title": "Team Work Status", "desc": "Open, overdue, in-progress and blocked assignments for the active branch/team.", "href": "/manager/dashboard?tab=team-work"},
{"group": "Execution", "title": "Due Today / This Week", "desc": "Work requiring immediate attention and allocation follow-up.", "href": "/manager/dashboard?tab=team-work"},
{"group": "Due Control", "title": "Due Calendar", "desc": "Due today, due this week and overdue work in one control view.", "href": "/manager/dashboard?tab=due-calendar"},
{"group": "Review", "title": "Manager Review Queue", "desc": "Completed or review-ready work waiting for manager action.", "href": "/manager/dashboard?tab=review-queue"},
{"group": "Client Follow-up", "title": "Client Pending Report", "desc": "Tasks blocked due to documents, clarification or client data pending.", "href": "/manager/dashboard?tab=client-pending"},
{"group": "Escalation", "title": "Escalation Register", "desc": "Unassigned, overdue and aged client-pending items requiring manager intervention.", "href": "/manager/dashboard?tab=escalations"},
{"group": "Team", "title": "Staff Workload Report", "desc": "Staff-wise active, overdue, review and client-pending workload.", "href": "/manager/dashboard?tab=team-work"},
{"group": "Documents", "title": "Document Checklist Report", "desc": "Document-related blocked tasks and communication timeline shortcuts.", "href": "/manager/dashboard?tab=documents"},
]
@@ -249,10 +274,12 @@ def _report_cards() -> list[dict[str, str]]:
def _wizard_cards() -> list[dict[str, str]]:
return [
{"title": "Task Assignment Wizard", "desc": "Open the detailed allocation board to assign or reassign staff.", "href": "/manager/work"},
{"title": "Review Wizard", "desc": "Review completed work and send corrections through the task timeline.", "href": "/manager/dashboard?tab=review-queue"},
{"title": "Client Query Wizard", "desc": "Handle client-pending tasks and record clarification requirements.", "href": "/manager/dashboard?tab=client-pending"},
{"title": "Document Checklist Wizard", "desc": "Verify pending documents and open engagement document storage.", "href": "/documents"},
{"title": "Task Assignment Wizard", "desc": "Allocate unassigned tasks and rebalance heavy workload using the existing manager work board.", "href": "/manager/work"},
{"title": "Due Control Wizard", "desc": "Review due today, due this week and overdue items before escalation.", "href": "/manager/dashboard?tab=due-calendar"},
{"title": "Review Wizard", "desc": "Open review-ready tasks and proceed through the existing task communication timeline.", "href": "/manager/dashboard?tab=review-queue"},
{"title": "Client Query Wizard", "desc": "Handle client-pending items and record clarification/document requirements.", "href": "/manager/dashboard?tab=client-pending"},
{"title": "Document Checklist Wizard", "desc": "Verify document-related blocked tasks and open the existing documents area.", "href": "/documents"},
{"title": "Escalation Wizard", "desc": "Review aged overdue, unassigned and client-pending items requiring partner/branch attention.", "href": "/manager/dashboard?tab=escalations"},
{"title": "Work Closure Wizard", "desc": "Open engagement progress and close work after review completion.", "href": "/employees/progress"},
{"title": "Alert / Follow-up Wizard", "desc": "Open alerts for reminders, escalations and follow-up actions.", "href": "/alerts"},
]
@@ -279,6 +306,26 @@ def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[
tenant = getattr(scope, "tenant", None)
branch = getattr(scope, "branch", None)
age_buckets = [
{"label": "1-3 days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "1-3 days"])},
{"label": "4-7 days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "4-7 days"])},
{"label": "8-14 days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "8-14 days"])},
{"label": "15-29 days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "15-29 days"])},
{"label": "30+ days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "30+ days"])},
]
status_summary: dict[str, int] = {}
service_summary: dict[str, int] = {}
for row in open_rows:
status_summary[row.get("status_label") or "Pending"] = status_summary.get(row.get("status_label") or "Pending", 0) + 1
service_summary[row.get("service_name") or "Service"] = service_summary.get(row.get("service_name") or "Service", 0) + 1
escalation_rows = sorted(
(unassigned_rows + overdue_rows + client_pending_rows),
key=lambda r: (r.get("days_overdue") or 0, r.get("is_overdue") or False, r.get("due_date") or date.max),
reverse=True,
)
overview = {
"tenant": tenant,
"branch": branch,
@@ -296,6 +343,7 @@ def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[
"review_pending_count": len(review_rows),
"staff_count": len(staff_rows),
"client_count": len(client_rows),
"escalation_count": len(escalation_rows),
"today": today,
}
@@ -311,6 +359,10 @@ def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[
"review_queue": review_rows[:60],
"client_pending": client_pending_rows[:60],
"documents_pending": client_pending_rows[:40],
"escalations": escalation_rows[:80],
"age_buckets": age_buckets,
"status_summary": sorted([{"label": k, "count": v} for k, v in status_summary.items()], key=lambda x: x["count"], reverse=True)[:10],
"service_summary": sorted([{"label": k, "count": v} for k, v in service_summary.items()], key=lambda x: x["count"], reverse=True)[:10],
"staff_rows": staff_rows,
"clients": client_rows,
"reports": _report_cards(),
@@ -5,7 +5,7 @@
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-brand-100">Manager Execution Control</p>
<h1 class="mt-2 text-2xl font-bold">Manager Dashboard</h1>
<h1 class="mt-2 text-2xl font-bold">Manager Dashboard V2</h1>
<p class="mt-2 max-w-3xl text-sm text-brand-100">Control team allocation, review queue, client pending items, documents and execution reports for the active branch/team.</p>
<p class="mt-3 text-xs text-brand-100">FY: {{ overview.financial_year or 'All' }}{% if overview.branch %} · Branch: {{ overview.branch.name or overview.branch.code }}{% endif %}</p>
</div>
@@ -19,9 +19,11 @@
{% set tabs = [
('overview','Overview'),
('team-work','Team Work'),
('due-calendar','Due Calendar'),
('review-queue','Review Queue'),
('client-pending','Client Pending'),
('documents','Documents'),
('escalations','Escalations'),
('reports','Reports'),
('wizards','Wizards')
] %}
@@ -5,7 +5,7 @@
<th class="px-4 py-3 text-left">Client</th>
<th class="px-4 py-3 text-left">Task</th>
<th class="px-4 py-3 text-left">Service / Period</th>
<th class="px-4 py-3 text-left">Due</th>
<th class="px-4 py-3 text-left">Due / Age</th>
<th class="px-4 py-3 text-left">Assigned</th>
<th class="px-4 py-3 text-left">Status</th>
<th class="px-4 py-3 text-right">Action</th>
@@ -17,7 +17,7 @@
<td class="px-4 py-3"><div class="font-semibold text-slate-900">{{ row.client_name }}</div><div class="text-xs text-slate-500">{{ row.client_code or '-' }}</div></td>
<td class="px-4 py-3"><div class="font-semibold text-slate-900">{{ row.task_name }}</div><div class="text-xs text-slate-500">{{ row.comment_count }} timeline item(s)</div></td>
<td class="px-4 py-3"><div>{{ row.service_name }}</div><div class="text-xs text-slate-500">{{ row.period }}</div></td>
<td class="px-4 py-3"><span class="{% if row.is_overdue %}text-red-700 font-semibold{% elif row.is_due_today %}text-amber-700 font-semibold{% endif %}">{{ row.due_date or '-' }}</span></td>
<td class="px-4 py-3"><span class="{% if row.is_overdue %}text-red-700 font-semibold{% elif row.is_due_today %}text-amber-700 font-semibold{% endif %}">{{ row.due_date or '-' }}</span>{% if row.days_overdue %}<div class="text-xs font-semibold text-red-600">{{ row.days_overdue }} day(s) overdue</div>{% endif %}</td>
<td class="px-4 py-3">{{ row.assigned_to }}</td>
<td class="px-4 py-3"><span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-700">{{ row.status_label }}</span></td>
<td class="px-4 py-3 text-right"><a href="{{ row.href }}" class="rounded-lg border border-slate-300 px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Open</a></td>
@@ -0,0 +1,27 @@
<div class="space-y-6">
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="af-metric-card border-amber-200 bg-amber-50"><div class="text-xs font-semibold uppercase text-amber-700">Due Today</div><div class="mt-2 text-3xl font-semibold text-amber-700">{{ overview.due_today_count }}</div><div class="mt-1 text-xs text-amber-700">Must be controlled today</div></div>
<div class="af-metric-card border-blue-200 bg-blue-50"><div class="text-xs font-semibold uppercase text-blue-700">Due This Week</div><div class="mt-2 text-3xl font-semibold text-blue-700">{{ overview.due_week_count }}</div><div class="mt-1 text-xs text-blue-700">Upcoming workload</div></div>
<div class="af-metric-card border-red-200 bg-red-50"><div class="text-xs font-semibold uppercase text-red-700">Overdue</div><div class="mt-2 text-3xl font-semibold text-red-700">{{ overview.overdue_count }}</div><div class="mt-1 text-xs text-red-700">Escalation required</div></div>
<div class="af-metric-card border-orange-200 bg-orange-50"><div class="text-xs font-semibold uppercase text-orange-700">Unassigned</div><div class="mt-2 text-3xl font-semibold text-orange-700">{{ overview.unassigned_count }}</div><div class="mt-1 text-xs text-orange-700">Allocation pending</div></div>
</section>
<section class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_340px]">
<div class="space-y-6">
<div class="af-card">
<div class="mb-4 flex items-center justify-between gap-3"><div><h3 class="text-lg font-semibold text-slate-900">Due Today</h3><p class="text-sm text-slate-500">Tasks that should be completed or reviewed today.</p></div><a href="/manager/work" class="af-btn af-btn-primary">Work Board</a></div>
{% set rows = due_today %}
{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
</div>
<div class="af-card">
<div class="mb-4"><h3 class="text-lg font-semibold text-slate-900">Due This Week</h3><p class="text-sm text-slate-500">Upcoming work due within the next seven days.</p></div>
{% set rows = due_week %}
{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
</div>
</div>
<aside class="space-y-4">
<div class="af-card"><h3 class="font-semibold text-slate-900">Overdue Ageing</h3><div class="mt-4 space-y-3">{% for item in age_buckets %}<div><div class="mb-1 flex justify-between text-xs font-semibold text-slate-600"><span>{{ item.label }}</span><span>{{ item.count }}</span></div><div class="h-2 rounded-full bg-slate-100"><div class="h-2 rounded-full bg-brand-600" style="width: {{ [item.count * 10, 100]|min }}%"></div></div></div>{% endfor %}</div></div>
<div class="af-card"><h3 class="font-semibold text-slate-900">Status Mix</h3><div class="mt-4 space-y-2 text-sm">{% for item in status_summary[:6] %}<div class="flex justify-between rounded-xl bg-slate-50 px-3 py-2"><span>{{ item.label }}</span><span class="font-semibold">{{ item.count }}</span></div>{% else %}<p class="text-sm text-slate-500">No open status data.</p>{% endfor %}</div></div>
</aside>
</section>
</div>
@@ -0,0 +1,20 @@
<div class="space-y-6">
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="af-metric-card border-red-200 bg-red-50"><div class="text-xs font-semibold uppercase text-red-700">Overdue</div><div class="mt-2 text-3xl font-semibold text-red-700">{{ overview.overdue_count }}</div><div class="mt-1 text-xs text-red-700">Ageing risk</div></div>
<div class="af-metric-card border-orange-200 bg-orange-50"><div class="text-xs font-semibold uppercase text-orange-700">Unassigned</div><div class="mt-2 text-3xl font-semibold text-orange-700">{{ overview.unassigned_count }}</div><div class="mt-1 text-xs text-orange-700">Manager action</div></div>
<div class="af-metric-card border-amber-200 bg-amber-50"><div class="text-xs font-semibold uppercase text-amber-700">Client Pending</div><div class="mt-2 text-3xl font-semibold text-amber-700">{{ overview.client_pending_count }}</div><div class="mt-1 text-xs text-amber-700">Follow-up needed</div></div>
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Escalation Items</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ overview.escalation_count }}</div><div class="mt-1 text-xs text-slate-500">Combined attention list</div></div>
</section>
<section class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_340px]">
<div class="af-card">
<div class="mb-4 flex items-center justify-between gap-3"><div><h3 class="text-lg font-semibold text-slate-900">Escalation Register</h3><p class="text-sm text-slate-500">Unassigned, overdue and client-pending tasks sorted by urgency.</p></div><a href="/alerts" class="af-btn af-btn-primary">Open Alerts</a></div>
{% set rows = escalations %}
{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
</div>
<aside class="space-y-4">
<div class="af-card"><h3 class="font-semibold text-slate-900">Ageing Buckets</h3><div class="mt-4 space-y-2 text-sm">{% for item in age_buckets %}<div class="flex justify-between rounded-xl bg-slate-50 px-3 py-2"><span>{{ item.label }}</span><span class="font-semibold">{{ item.count }}</span></div>{% endfor %}</div></div>
<div class="af-card"><h3 class="font-semibold text-slate-900">Escalation Actions</h3><div class="mt-4 grid gap-2"><a href="/manager/work" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Assign / Reassign Work</a><a href="/manager/dashboard?tab=client-pending" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Client Pending Follow-up</a><a href="/employees/progress" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Review Progress</a></div></div>
</aside>
</section>
</div>
@@ -5,7 +5,7 @@
<a href="/manager/dashboard?tab=team-work" class="af-metric-card border-blue-200 bg-blue-50"><div class="text-xs font-semibold uppercase text-blue-700">In Progress</div><div class="mt-2 text-3xl font-semibold text-blue-700">{{ overview.in_progress_count }}</div><div class="mt-1 text-xs text-blue-700">Being worked</div></a>
<a href="/manager/dashboard?tab=client-pending" class="af-metric-card border-amber-200 bg-amber-50"><div class="text-xs font-semibold uppercase text-amber-700">Client Pending</div><div class="mt-2 text-3xl font-semibold text-amber-700">{{ overview.client_pending_count }}</div><div class="mt-1 text-xs text-amber-700">Needs follow-up</div></a>
<a href="/manager/dashboard?tab=review-queue" class="af-metric-card border-emerald-200 bg-emerald-50"><div class="text-xs font-semibold uppercase text-emerald-700">Review Queue</div><div class="mt-2 text-3xl font-semibold text-emerald-700">{{ overview.review_pending_count }}</div><div class="mt-1 text-xs text-emerald-700">Review-ready</div></a>
<a href="/manager/dashboard?tab=team-work" class="af-metric-card border-red-200 bg-red-50"><div class="text-xs font-semibold uppercase text-red-700">Overdue</div><div class="mt-2 text-3xl font-semibold text-red-700">{{ overview.overdue_count }}</div><div class="mt-1 text-xs text-red-700">Escalate</div></a>
<a href="/manager/dashboard?tab=escalations" class="af-metric-card border-red-200 bg-red-50"><div class="text-xs font-semibold uppercase text-red-700">Escalations</div><div class="mt-2 text-3xl font-semibold text-red-700">{{ overview.escalation_count }}</div><div class="mt-1 text-xs text-red-700">Combined risk items</div></a>
</section>
<section class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
@@ -19,7 +19,7 @@
</div>
<div class="space-y-4">
<div class="af-card"><h3 class="font-semibold text-slate-900">Team Snapshot</h3><div class="mt-4 grid grid-cols-2 gap-3 text-sm"><div class="rounded-2xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Staff visible</div><div class="text-2xl font-semibold">{{ overview.staff_count }}</div></div><div class="rounded-2xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Clients visible</div><div class="text-2xl font-semibold">{{ overview.client_count }}</div></div><div class="rounded-2xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Due today</div><div class="text-2xl font-semibold">{{ overview.due_today_count }}</div></div><div class="rounded-2xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Due week</div><div class="text-2xl font-semibold">{{ overview.due_week_count }}</div></div></div></div>
<div class="af-card"><h3 class="font-semibold text-slate-900">Quick Actions</h3><div class="mt-4 grid gap-2"><a href="/manager/work" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Task Assignment Wizard</a><a href="/manager/dashboard?tab=review-queue" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Review Wizard</a><a href="/manager/dashboard?tab=client-pending" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Client Query Wizard</a><a href="/documents" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Document Checklist</a></div></div>
<div class="af-card"><h3 class="font-semibold text-slate-900">Quick Actions</h3><div class="mt-4 grid gap-2"><a href="/manager/work" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Task Assignment Wizard</a><a href="/manager/dashboard?tab=review-queue" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Review Wizard</a><a href="/manager/dashboard?tab=client-pending" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Client Query Wizard</a><a href="/documents" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Document Checklist</a><a href="/manager/dashboard?tab=due-calendar" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Due Calendar</a><a href="/manager/dashboard?tab=escalations" class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold hover:bg-slate-50">Escalation Register</a></div></div>
</div>
</section>
</div>
+3 -1
View File
@@ -11,14 +11,16 @@ from app.core.templating import templates
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.manager_dashboard.service import build_manager_dashboard_payload, can_access_manager_dashboard
router = APIRouter(prefix="/manager", tags=["manager-dashboard-v1-ui"])
router = APIRouter(prefix="/manager", tags=["manager-dashboard-v2-ui"])
VALID_TABS = {
"overview": "modules/manager_dashboard/templates/manager_dashboard/partials/overview.html",
"team-work": "modules/manager_dashboard/templates/manager_dashboard/partials/team_work.html",
"due-calendar": "modules/manager_dashboard/templates/manager_dashboard/partials/due_calendar.html",
"review-queue": "modules/manager_dashboard/templates/manager_dashboard/partials/review_queue.html",
"client-pending": "modules/manager_dashboard/templates/manager_dashboard/partials/client_pending.html",
"documents": "modules/manager_dashboard/templates/manager_dashboard/partials/documents.html",
"escalations": "modules/manager_dashboard/templates/manager_dashboard/partials/escalations.html",
"reports": "modules/manager_dashboard/templates/manager_dashboard/partials/reports.html",
"wizards": "modules/manager_dashboard/templates/manager_dashboard/partials/wizards.html",
}