diff --git a/app/modules/manager_dashboard/__init__.py b/app/modules/manager_dashboard/__init__.py new file mode 100644 index 0000000..2832114 --- /dev/null +++ b/app/modules/manager_dashboard/__init__.py @@ -0,0 +1 @@ +"""Manager Dashboard V1 module.""" diff --git a/app/modules/manager_dashboard/service.py b/app/modules/manager_dashboard/service.py new file mode 100644 index 0000000..957694a --- /dev/null +++ b/app/modules/manager_dashboard/service.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +from datetime import date, timedelta +from typing import Any + +from sqlalchemy import func, or_, select +from sqlalchemy.orm import Session, selectinload + +from app.modules.clients.models import Client +from app.modules.core.iam.models import User +from app.modules.core.rbac.models import Role, UserRole +from app.modules.employees.service import build_employee_scope, list_employee_work_assignable_users +from app.modules.services.execution import CLOSED_TASK_STATUSES +from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance, ServiceCatalogue, ServiceTaskComment + +MANAGER_ROLES = {"Manager", "Branch Manager", "System Admin", "Firm Admin", "Partner"} +MANAGER_PERMISSIONS = {"employees.work.manage", "services.tasks.review", "services.tasks.assign"} +REVIEW_STATUSES = {"completed", "ready_review", "review_pending", "pending_review", "manager_review"} +CLIENT_PENDING_STATUSES = {"blocked", "client_pending", "waiting_client", "documents_pending"} + + +def _count(db: Session, stmt) -> int: + return int(db.execute(stmt).scalar() or 0) + + +def get_user_role_names(db: Session, user_id: int) -> list[str]: + return list( + db.execute( + select(Role.name) + .join(UserRole, UserRole.role_id == Role.id) + .where(UserRole.user_id == int(user_id), Role.is_active.is_(True)) + .order_by(Role.name.asc()) + ).scalars().all() + ) + + +def get_user_permission_names(db: Session, user_id: int) -> set[str]: + # Lightweight best-effort permission lookup through the active roles already used by the ERP RBAC tables. + rows = db.execute( + select(Role.permissions) + .join(UserRole, UserRole.role_id == Role.id) + .where(UserRole.user_id == int(user_id), Role.is_active.is_(True)) + ).scalars().all() + permissions: set[str] = set() + for value in rows: + if isinstance(value, (list, tuple, set)): + permissions.update(str(v) for v in value if v) + elif isinstance(value, str): + # Supports both comma separated and JSON-like textual storage without being destructive. + cleaned = value.replace("[", "").replace("]", "").replace('"', "").replace("'", "") + permissions.update(v.strip() for v in cleaned.split(",") if v.strip()) + return permissions + + +def can_access_manager_dashboard(db: Session, current_user) -> bool: + roles = set(get_user_role_names(db, current_user.id)) + if roles.intersection(MANAGER_ROLES): + return True + return bool(get_user_permission_names(db, current_user.id).intersection(MANAGER_PERMISSIONS)) + + +def _active_financial_year(request) -> str | None: + value = request.session.get("active_financial_year") or getattr(request.state, "year_code", None) + value = (value or "").strip() + return value or None + + +def _scope(db: Session, request, current_user): + return build_employee_scope( + db, + current_user, + tenant_id=request.session.get("active_tenant_id") or getattr(current_user, "tenant_id", None), + branch_id=request.session.get("active_branch_id"), + ) + + +def _task_scope(stmt, scope, fy: str | None = None): + stmt = stmt.where( + ClientServiceTaskInstance.tenant_id == scope.tenant_id, + ClientServiceTaskInstance.is_active.is_(True), + ) + if getattr(scope, "branch_id", None) is not None: + stmt = stmt.where(ClientServiceTaskInstance.branch_id == scope.branch_id) + if fy: + stmt = stmt.where(ClientServiceTaskInstance.financial_year == fy) + return stmt + + +def _client_scope(stmt, scope): + stmt = stmt.where(Client.tenant_id == scope.tenant_id, Client.is_active.is_(True)) + if getattr(scope, "branch_id", None) is not None: + stmt = stmt.where(Client.branch_id == scope.branch_id) + return stmt + + +def _is_open(status: str | None) -> bool: + return (status or "pending").strip().lower() not in CLOSED_TASK_STATUSES + + +def _status_label(status: str | None) -> str: + return (status or "pending").replace("_", " ").replace("-", " ").title() + + +def _task_href(task: ClientServiceTaskInstance) -> str: + task_id = getattr(task, "id", None) + if task_id: + return f"/employees/work/tasks/{task_id}/communication" + return "/manager/work" + + +def _task_row(task: ClientServiceTaskInstance) -> dict[str, Any]: + today = date.today() + client = getattr(task, "client", None) + catalogue = getattr(task, "catalogue", None) + subscription = getattr(task, "subscription", None) + assignee = getattr(task, "assigned_to", None) + status = (getattr(task, "status", None) or "pending").strip().lower() + due_date = getattr(task, "internal_target_date", None) + service_name = getattr(catalogue, "service_name", None) or getattr(catalogue, "name", None) + if not service_name and subscription is not None: + sub_catalogue = getattr(subscription, "catalogue", None) + service_name = getattr(sub_catalogue, "service_name", None) or getattr(sub_catalogue, "name", None) + return { + "id": getattr(task, "id", None), + "subscription_id": getattr(task, "subscription_id", None), + "client_name": getattr(client, "client_name", None) or "Unlinked Client", + "client_code": getattr(client, "client_code", None) or "", + "service_name": service_name or "Service", + "task_name": getattr(task, "task_name", None) or "Task", + "period": getattr(task, "financial_year", None) or "-", + "due_date": due_date, + "status": status, + "status_label": _status_label(status), + "priority": getattr(task, "priority", None) or "normal", + "assigned_to": getattr(assignee, "full_name", None) or getattr(assignee, "email", None) or "Unassigned", + "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)), + "href": _task_href(task), + } + + +def _load_tasks(db: Session, scope, fy: str | None) -> list[ClientServiceTaskInstance]: + stmt = ( + select(ClientServiceTaskInstance) + .options( + selectinload(ClientServiceTaskInstance.client), + selectinload(ClientServiceTaskInstance.catalogue), + selectinload(ClientServiceTaskInstance.assigned_to), + selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by), + selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue), + ) + ) + stmt = _task_scope(stmt, scope, fy) + stmt = stmt.order_by( + ClientServiceTaskInstance.internal_target_date.is_(None), + ClientServiceTaskInstance.internal_target_date.asc(), + ClientServiceTaskInstance.priority.desc(), + ClientServiceTaskInstance.id.desc(), + ).limit(350) + return list(db.execute(stmt).scalars().all()) + + +def _staff_rows(db: Session, scope, task_rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + try: + users = list_employee_work_assignable_users(db, scope) + except Exception: + stmt = select(User).where(User.tenant_id == scope.tenant_id, User.is_active.is_(True)) + if getattr(scope, "branch_id", None) is not None: + stmt = stmt.where(User.branch_id == scope.branch_id) + users = list(db.execute(stmt.order_by(User.full_name.asc(), User.email.asc()).limit(100)).scalars().all()) + + stats_by_name: dict[str, dict[str, int]] = {} + for row in task_rows: + name = row.get("assigned_to") or "Unassigned" + bucket = stats_by_name.setdefault(name, {"active": 0, "overdue": 0, "review": 0, "client_pending": 0, "due_today": 0}) + if _is_open(row.get("status")): + bucket["active"] += 1 + if row.get("is_overdue"): + bucket["overdue"] += 1 + if row.get("is_due_today"): + bucket["due_today"] += 1 + if row.get("status") in REVIEW_STATUSES: + bucket["review"] += 1 + if row.get("status") in CLIENT_PENDING_STATUSES: + bucket["client_pending"] += 1 + + rows: list[dict[str, Any]] = [] + seen = set() + for user in users: + name = getattr(user, "full_name", None) or getattr(user, "email", None) or "User" + seen.add(name) + stats = stats_by_name.get(name, {"active": 0, "overdue": 0, "review": 0, "client_pending": 0, "due_today": 0}) + total = stats["active"] + stats["review"] + rows.append({ + "name": name, + "email": getattr(user, "email", None) or "", + "designation": getattr(user, "designation", None) or "", + **stats, + "load_status": "Heavy" if total >= 40 else ("Balanced" if total >= 10 else "Light"), + }) + if "Unassigned" in stats_by_name and "Unassigned" not in seen: + stats = stats_by_name["Unassigned"] + rows.append({"name": "Unassigned", "email": "", "designation": "Allocation pending", **stats, "load_status": "Needs allocation"}) + rows.sort(key=lambda r: (r["active"] + r["review"], r["overdue"], r["due_today"]), reverse=True) + return rows[:30] + + +def _client_rows(db: Session, scope, task_rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + clients = db.execute(_client_scope(select(Client), scope).order_by(Client.client_name.asc()).limit(100)).scalars().all() + by_client: dict[str, dict[str, int]] = {} + for row in task_rows: + key = row["client_name"] + bucket = by_client.setdefault(key, {"open": 0, "overdue": 0, "client_pending": 0, "review": 0}) + if _is_open(row.get("status")): + bucket["open"] += 1 + if row.get("is_overdue"): + bucket["overdue"] += 1 + if row.get("status") in CLIENT_PENDING_STATUSES: + bucket["client_pending"] += 1 + if row.get("status") in REVIEW_STATUSES: + bucket["review"] += 1 + out: list[dict[str, Any]] = [] + for client in clients: + name = getattr(client, "client_name", None) or "Client" + stats = by_client.get(name, {"open": 0, "overdue": 0, "client_pending": 0, "review": 0}) + out.append({ + "id": getattr(client, "id", None), + "client_name": name, + "client_code": getattr(client, "client_code", None) or "", + "pan": getattr(client, "pan", None) or "", + "gstin": getattr(client, "gstin", None) or "", + **stats, + "href": f"/clients/{getattr(client, 'id', '')}/edit" if getattr(client, "id", None) else "/clients", + }) + return out[:50] + + +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": "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": "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"}, + ] + + +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": "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"}, + ] + + +def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[str, Any]: + scope = _scope(db, request, current_user) + fy = _active_financial_year(request) + today = date.today() + next_week = today + timedelta(days=7) + tasks = _load_tasks(db, scope, fy) + task_rows = [_task_row(t) for t in tasks] + open_rows = [r for r in task_rows if _is_open(r["status"])] + overdue_rows = [r for r in open_rows if r["is_overdue"]] + due_today_rows = [r for r in open_rows if r["is_due_today"]] + due_week_rows = [r for r in open_rows if r["due_date"] and today <= r["due_date"] <= next_week] + review_rows = [r for r in task_rows if r["status"] in REVIEW_STATUSES] + client_pending_rows = [r for r in open_rows if r["status"] in CLIENT_PENDING_STATUSES] + unassigned_rows = [r for r in open_rows if (r.get("assigned_to") or "Unassigned") == "Unassigned"] + in_progress_rows = [r for r in open_rows if r["status"] == "in_progress"] + + staff_rows = _staff_rows(db, scope, task_rows) + client_rows = _client_rows(db, scope, task_rows) + + tenant = getattr(scope, "tenant", None) + branch = getattr(scope, "branch", None) + overview = { + "tenant": tenant, + "branch": branch, + "tenant_id": getattr(scope, "tenant_id", None), + "branch_id": getattr(scope, "branch_id", None), + "financial_year": fy, + "total_tasks": len(task_rows), + "open_task_count": len(open_rows), + "unassigned_count": len(unassigned_rows), + "in_progress_count": len(in_progress_rows), + "overdue_count": len(overdue_rows), + "due_today_count": len(due_today_rows), + "due_week_count": len(due_week_rows), + "client_pending_count": len(client_pending_rows), + "review_pending_count": len(review_rows), + "staff_count": len(staff_rows), + "client_count": len(client_rows), + "today": today, + } + + return { + "roles": sorted(get_user_role_names(db, current_user.id)), + "overview": overview, + "team_work": task_rows[:100], + "due_today": due_today_rows[:30], + "due_week": due_week_rows[:30], + "overdue": overdue_rows[:30], + "unassigned": unassigned_rows[:30], + "in_progress": in_progress_rows[:30], + "review_queue": review_rows[:60], + "client_pending": client_pending_rows[:60], + "documents_pending": client_pending_rows[:40], + "staff_rows": staff_rows, + "clients": client_rows, + "reports": _report_cards(), + "wizards": _wizard_cards(), + } diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/dashboard.html b/app/modules/manager_dashboard/templates/manager_dashboard/dashboard.html new file mode 100644 index 0000000..a9a217e --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/dashboard.html @@ -0,0 +1,69 @@ +{% extends "ui/templates/base/layout.html" %} +{% block content %} +
+
+
+
+

Manager Execution Control

+

Manager Dashboard

+

Control team allocation, review queue, client pending items, documents and execution reports for the active branch/team.

+

FY: {{ overview.financial_year or 'All' }}{% if overview.branch %} · Branch: {{ overview.branch.name or overview.branch.code }}{% endif %}

+
+ +
+
+ + {% set tabs = [ + ('overview','Overview'), + ('team-work','Team Work'), + ('review-queue','Review Queue'), + ('client-pending','Client Pending'), + ('documents','Documents'), + ('reports','Reports'), + ('wizards','Wizards') + ] %} + +
+
+ {% for code, label in tabs %} + + {% endfor %} +
+
+ +
+ {% include 'modules/manager_dashboard/templates/manager_dashboard/partials/' ~ (active_tab|replace('-', '_')) ~ '.html' ignore missing %} +
+
+ + +{% endblock %} diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html new file mode 100644 index 0000000..e0585db --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html @@ -0,0 +1,30 @@ +
+ + + + + + + + + + + + + + {% for row in rows %} + + + + + + + + + + {% else %} + + {% endfor %} + +
ClientTaskService / PeriodDueAssignedStatusAction
{{ row.client_name }}
{{ row.client_code or '-' }}
{{ row.task_name }}
{{ row.comment_count }} timeline item(s)
{{ row.service_name }}
{{ row.period }}
{{ row.due_date or '-' }}{{ row.assigned_to }}{{ row.status_label }}Open
No records found for this view.
+
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/client_pending.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/client_pending.html new file mode 100644 index 0000000..eeb7019 --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/client_pending.html @@ -0,0 +1,4 @@ +
+

Client Pending / Blocked Work

Tasks delayed due to client documents, data or clarifications.

Alerts
{% set rows = client_pending %}{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
+

Client-wise Pending View

{% for c in clients if c.client_pending or c.overdue %}{% else %}{% endfor %}
ClientOpenClient PendingOverdueAction
{{ c.client_name }}{{ c.open }}{{ c.client_pending }}{{ c.overdue }}Open
No client pending clients found.
+
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/documents.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/documents.html new file mode 100644 index 0000000..1f66d8b --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/documents.html @@ -0,0 +1,3 @@ +
+

Document Checklist Control

Document-related blocked tasks and timeline shortcuts.

Open Documents
{% set rows = documents_pending %}{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
+
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/overview.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/overview.html new file mode 100644 index 0000000..ab490c8 --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/overview.html @@ -0,0 +1,25 @@ +
+
+
Open Tasks
{{ overview.open_task_count }}
Visible team work
+
Unassigned
{{ overview.unassigned_count }}
Allocate first
+
In Progress
{{ overview.in_progress_count }}
Being worked
+
Client Pending
{{ overview.client_pending_count }}
Needs follow-up
+
Review Queue
{{ overview.review_pending_count }}
Review-ready
+
Overdue
{{ overview.overdue_count }}
Escalate
+
+ +
+
+
+

Attention Required

Unassigned, overdue and client-pending work.

+ Open Work Board +
+ {% set rows = (unassigned + overdue + client_pending)[:12] %} + {% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %} +
+
+

Team Snapshot

Staff visible
{{ overview.staff_count }}
Clients visible
{{ overview.client_count }}
Due today
{{ overview.due_today_count }}
Due week
{{ overview.due_week_count }}
+ +
+
+
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/reports.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/reports.html new file mode 100644 index 0000000..fbe0e05 --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/reports.html @@ -0,0 +1,8 @@ +
+

Manager Reports

Action reports for team execution, review, client pending and documents.

+
+ {% for card in reports %} +
{{ card.group }}

{{ card.title }}

{{ card.desc }}

View Report →
+ {% endfor %} +
+
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/review_queue.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/review_queue.html new file mode 100644 index 0000000..44ef62c --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/review_queue.html @@ -0,0 +1,3 @@ +
+

Manager Review Queue

Completed or review-ready work waiting for manager action.

Progress
{% set rows = review_queue %}{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
+
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/team_work.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/team_work.html new file mode 100644 index 0000000..2a5ba81 --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/team_work.html @@ -0,0 +1,10 @@ +
+
+
Total Visible
{{ overview.total_tasks }}
+
Unassigned
{{ overview.unassigned_count }}
+
In Progress
{{ overview.in_progress_count }}
+
Overdue
{{ overview.overdue_count }}
+
Due Today
{{ overview.due_today_count }}
+
+

Team Work Board Summary

Top 100 visible tasks. Use detailed board for assignment changes.

Detailed Board
{% set rows = team_work %}{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
+
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/wizards.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/wizards.html new file mode 100644 index 0000000..78ff5f9 --- /dev/null +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/wizards.html @@ -0,0 +1,8 @@ +
+

Manager Wizards

Guided shortcuts for assignment, review, client query, document checklist and closure.

+
+ {% for card in wizards %} +

{{ card.title }}

{{ card.desc }}

Open →
+ {% endfor %} +
+
diff --git a/app/modules/manager_dashboard/ui.py b/app/modules/manager_dashboard/ui.py new file mode 100644 index 0000000..df70226 --- /dev/null +++ b/app/modules/manager_dashboard/ui.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from fastapi import APIRouter, Request +from fastapi.responses import RedirectResponse + +from app.core.db.common import CommonSessionLocal +from app.core.http_responses import ui_access_denied +from app.core.security.csrf import get_or_create_csrf_token +from app.core.security.session_auth import get_current_user +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"]) + +VALID_TABS = { + "overview": "modules/manager_dashboard/templates/manager_dashboard/partials/overview.html", + "team-work": "modules/manager_dashboard/templates/manager_dashboard/partials/team_work.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", + "reports": "modules/manager_dashboard/templates/manager_dashboard/partials/reports.html", + "wizards": "modules/manager_dashboard/templates/manager_dashboard/partials/wizards.html", +} + + +def _ctx(request: Request, db, current_user, *, active_tab: str = "overview"): + payload = build_manager_dashboard_payload(db, request, current_user) + return { + "request": request, + "current_user": current_user, + "current_user_roles": get_user_roles(db, current_user.id), + "current_user_permissions": get_user_permissions(db, current_user.id), + "csrf_token": get_or_create_csrf_token(request), + "title": "Manager Dashboard", + "active_tab": active_tab, + **payload, + } + + +@router.get("/dashboard") +def dashboard(request: Request, tab: str = "overview"): + db = CommonSessionLocal() + try: + current_user = get_current_user(request, db=db) + if not current_user: + return RedirectResponse(url="/login", status_code=303) + if not can_access_manager_dashboard(db, current_user): + return ui_access_denied() + active_tab = tab if tab in VALID_TABS else "overview" + return templates.TemplateResponse( + "modules/manager_dashboard/templates/manager_dashboard/dashboard.html", + _ctx(request, db, current_user, active_tab=active_tab), + ) + finally: + db.close() + + +@router.get("/dashboard/tab/{tab_name}") +def 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) + if not can_access_manager_dashboard(db, current_user): + return ui_access_denied() + active_tab = tab_name if tab_name in VALID_TABS else "overview" + return templates.TemplateResponse( + VALID_TABS[active_tab], + _ctx(request, db, current_user, active_tab=active_tab), + ) + finally: + db.close() diff --git a/app/ui/app.py b/app/ui/app.py index 655d0ca..751f667 100644 --- a/app/ui/app.py +++ b/app/ui/app.py @@ -4,6 +4,7 @@ from fastapi.staticfiles import StaticFiles from app.modules.clients.ui import router as clients_ui_router, portal_router as client_portal_router from app.modules.consultants.ui import router as consultants_ui_router, portal_router as consultant_portal_router from app.modules.employees.ui import router as employees_ui_router, portal_router as employee_portal_router +from app.modules.manager_dashboard.ui import router as manager_dashboard_router from app.modules.managers.ui import router as managers_ui_router from app.modules.partner_dashboard.ui import router as partner_dashboard_router from app.modules.partners.ui import router as partners_ui_router @@ -54,6 +55,7 @@ def mount_ui(app: FastAPI) -> None: app.include_router(work_detail_ui_router) app.include_router(clients_ui_router) app.include_router(employees_ui_router) + app.include_router(manager_dashboard_router) app.include_router(managers_ui_router) app.include_router(partner_dashboard_router) app.include_router(partners_ui_router) @@ -63,3 +65,4 @@ def mount_ui(app: FastAPI) -> None: app.include_router(client_portal_router) app.include_router(consultant_portal_router) +