Add staff dashboard and work wizards v1

This commit is contained in:
A R R R Associates
2026-07-05 20:15:21 +05:30
parent 21c71c7172
commit bbf71599b8
14 changed files with 503 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Staff dashboard module."""
+245
View File
@@ -0,0 +1,245 @@
from __future__ import annotations
from datetime import date, timedelta
from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload
from app.modules.clients.models import Client
from app.modules.core.rbac.models import Role, UserRole
from app.modules.employees.service import build_employee_scope, get_employee_for_user
from app.modules.services.execution import CLOSED_TASK_STATUSES
from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance, ServiceCatalogue, ServiceTaskComment
STAFF_ROLES = {"Staff", "Branch Manager", "Manager", "System Admin", "Firm Admin", "Partner"}
STAFF_PERMISSIONS = {"employees.work.view_self", "employees.ess.view", "services.tasks.update_self"}
CLIENT_PENDING_STATUSES = {"blocked", "client_pending", "waiting_client", "documents_pending"}
RETURNED_STATUSES = {"returned", "correction_required", "rework", "rejected"}
REVIEW_STATUSES = {"completed", "ready_review", "review_pending", "pending_review", "manager_review"}
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]:
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):
cleaned = value.replace("[", "").replace("]", "").replace('"', "").replace("'", "")
permissions.update(v.strip() for v in cleaned.split(",") if v.strip())
return permissions
def can_access_staff_dashboard(db: Session, current_user) -> bool:
roles = set(get_user_role_names(db, current_user.id))
if roles.intersection(STAFF_ROLES):
return True
if get_employee_for_user(db, current_user):
return True
return bool(get_user_permission_names(db, current_user.id).intersection(STAFF_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 _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 _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 _task_href(task: ClientServiceTaskInstance) -> str:
task_id = getattr(task, "id", None)
if task_id:
return f"/employees/work/tasks/{task_id}/communication"
return "/employees/work"
def _service_name(task: ClientServiceTaskInstance) -> str:
catalogue = getattr(task, "catalogue", None)
service_name = getattr(catalogue, "service_name", None) or getattr(catalogue, "name", None)
if not service_name:
subscription = getattr(task, "subscription", None)
sub_catalogue = getattr(subscription, "catalogue", None) if subscription else None
service_name = getattr(sub_catalogue, "service_name", None) or getattr(sub_catalogue, "name", None)
return service_name or "Service"
def _task_row(task: ClientServiceTaskInstance) -> dict[str, Any]:
today = date.today()
client = getattr(task, "client", None)
status = (getattr(task, "status", None) or "pending").strip().lower()
due_date = getattr(task, "internal_target_date", None)
is_open = _is_open(status)
return {
"id": getattr(task, "id", None),
"client_name": getattr(client, "client_name", None) or "Unlinked Client",
"client_code": getattr(client, "client_code", None) or "",
"service_name": _service_name(task),
"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",
"remarks": getattr(task, "remarks", None) or "",
"comment_count": len([c for c in getattr(task, "comments", []) if not getattr(c, "is_deleted", False)]),
"document_count": len(getattr(task, "documents", []) or []),
"is_open": is_open,
"is_overdue": bool(due_date and due_date < today and is_open),
"is_due_today": bool(due_date and due_date == today and is_open),
"is_due_week": bool(due_date and today <= due_date <= today + timedelta(days=7) and is_open),
"days_overdue": _days_overdue(due_date) if is_open else 0,
"href": _task_href(task),
}
def _load_my_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),
selectinload(ClientServiceTaskInstance.documents),
)
.where(
ClientServiceTaskInstance.tenant_id == scope.tenant_id,
ClientServiceTaskInstance.assigned_to_user_id == scope.own_user_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)
stmt = stmt.order_by(
ClientServiceTaskInstance.internal_target_date.is_(None),
ClientServiceTaskInstance.internal_target_date.asc(),
ClientServiceTaskInstance.priority.desc(),
ClientServiceTaskInstance.id.desc(),
).limit(250)
return list(db.execute(stmt).scalars().all())
def _report_cards() -> list[dict[str, str]]:
return [
{"group": "My Work", "title": "My Pending Tasks", "desc": "All open work assigned to me.", "href": "/staff/dashboard?tab=my-tasks"},
{"group": "Due Control", "title": "Due Today", "desc": "Tasks that must be acted on today.", "href": "/staff/dashboard?tab=due-today"},
{"group": "Due Control", "title": "Overdue Tasks", "desc": "Assigned open work past internal target date.", "href": "/staff/dashboard?tab=overdue"},
{"group": "Client Follow-up", "title": "Client Pending", "desc": "Tasks blocked due to client data, documents or clarification pending.", "href": "/staff/dashboard?tab=client-pending"},
{"group": "Documents", "title": "Document Pending", "desc": "Assigned work requiring document upload or verification follow-up.", "href": "/staff/dashboard?tab=documents"},
{"group": "Correction", "title": "Returned Work", "desc": "Work returned for correction or rework.", "href": "/staff/dashboard?tab=returned-work"},
]
def _wizard_cards() -> list[dict[str, str]]:
return [
{"title": "My Task Work Wizard", "desc": "Open assigned task, read instructions, update status and add work notes.", "href": "/employees/work"},
{"title": "Document Upload Wizard", "desc": "Open document area for uploading supporting records and work papers.", "href": "/documents"},
{"title": "Mark Client Pending Wizard", "desc": "Use task communication to record document or clarification pending from client.", "href": "/staff/dashboard?tab=client-pending"},
{"title": "Submit for Review Wizard", "desc": "Open completed tasks and submit them through existing task communication workflow.", "href": "/staff/dashboard?tab=my-tasks"},
{"title": "Time / Remarks Wizard", "desc": "Update remarks and progress on current assigned task using existing work screen.", "href": "/employees/work"},
{"title": "Correction / Returned Work Wizard", "desc": "Open returned tasks and complete rework before resubmission.", "href": "/staff/dashboard?tab=returned-work"},
]
def build_staff_dashboard_payload(db: Session, request, current_user) -> dict[str, Any]:
scope = _scope(db, request, current_user)
fy = _active_financial_year(request)
today = date.today()
tasks = _load_my_tasks(db, scope, fy)
task_rows = [_task_row(t) for t in tasks]
open_rows = [r for r in task_rows if r["is_open"]]
completed_rows = [r for r in task_rows if not r["is_open"]]
due_today_rows = [r for r in open_rows if r["is_due_today"]]
overdue_rows = [r for r in open_rows if r["is_overdue"]]
due_week_rows = [r for r in open_rows if r["is_due_week"]]
client_pending_rows = [r for r in open_rows if r["status"] in CLIENT_PENDING_STATUSES]
returned_rows = [r for r in open_rows if r["status"] in RETURNED_STATUSES]
documents_rows = [r for r in open_rows if r["document_count"] > 0 or r["status"] in CLIENT_PENDING_STATUSES]
review_rows = [r for r in task_rows if r["status"] in REVIEW_STATUSES]
in_progress_rows = [r for r in open_rows if r["status"] == "in_progress"]
status_summary: dict[str, int] = {}
service_summary: dict[str, int] = {}
for row in open_rows:
status_summary[row["status_label"]] = status_summary.get(row["status_label"], 0) + 1
service_summary[row["service_name"]] = service_summary.get(row["service_name"], 0) + 1
employee = get_employee_for_user(db, current_user)
overview = {
"tenant_id": getattr(scope, "tenant_id", None),
"branch_id": getattr(scope, "branch_id", None),
"financial_year": fy,
"employee": employee,
"today": today,
"total_tasks": len(task_rows),
"open_count": len(open_rows),
"in_progress_count": len(in_progress_rows),
"due_today_count": len(due_today_rows),
"due_week_count": len(due_week_rows),
"overdue_count": len(overdue_rows),
"client_pending_count": len(client_pending_rows),
"documents_count": len(documents_rows),
"returned_count": len(returned_rows),
"review_count": len(review_rows),
"completed_count": len(completed_rows),
}
return {
"roles": sorted(get_user_role_names(db, current_user.id)),
"overview": overview,
"my_tasks": open_rows[:100],
"due_today": due_today_rows[:50],
"due_week": due_week_rows[:50],
"overdue": overdue_rows[:50],
"client_pending": client_pending_rows[:50],
"documents_pending": documents_rows[:50],
"returned_work": returned_rows[:50],
"completed": completed_rows[:50],
"review_ready": review_rows[:50],
"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],
"reports": _report_cards(),
"wizards": _wizard_cards(),
}
@@ -0,0 +1,70 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<section class="rounded-3xl bg-gradient-to-r from-slate-900 via-brand-700 to-brand-600 p-6 text-white shadow-soft">
<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">Staff Work Execution</p>
<h1 class="mt-2 text-2xl font-bold">Staff Dashboard V1</h1>
<p class="mt-2 max-w-3xl text-sm text-brand-100">Your assigned tasks, due work, client-pending items, documents, returned work and staff work wizards in one place.</p>
<p class="mt-3 text-xs text-brand-100">FY: {{ overview.financial_year or 'All' }}{% if overview.employee %} · {{ overview.employee.full_name }}{% endif %}</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/employees/work" class="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-brand-700 hover:bg-brand-50">Open My Work</a>
<a href="/documents" class="rounded-xl border border-white/40 px-4 py-2 text-sm font-semibold text-white hover:bg-white/10">Documents</a>
</div>
</div>
</section>
{% set tabs = [
('my-tasks','My Tasks'),
('due-today','Due Today'),
('overdue','Overdue'),
('client-pending','Client Pending'),
('documents','Documents'),
('returned-work','Returned Work'),
('reports','Reports'),
('wizards','Wizards')
] %}
<section class="rounded-3xl border border-slate-200 bg-white p-3 shadow-soft">
<div class="flex gap-2 overflow-x-auto" id="staff-tabs">
{% for code, label in tabs %}
<button type="button" data-tab="{{ code }}" class="staff-tab whitespace-nowrap rounded-2xl px-4 py-2 text-sm font-semibold transition {% if active_tab == code %}bg-brand-600 text-white{% else %}text-slate-600 hover:bg-slate-100{% endif %}">{{ label }}</button>
{% endfor %}
</div>
</section>
<section id="staff-dashboard-panel">
{% include 'modules/staff_dashboard/templates/staff_dashboard/partials/' ~ (active_tab|replace('-', '_')) ~ '.html' ignore missing %}
</section>
</div>
<script>
(function(){
const tabs = document.querySelectorAll('.staff-tab');
const panel = document.getElementById('staff-dashboard-panel');
async function loadTab(tab) {
tabs.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);
});
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('/staff/dashboard/tab/' + encodeURIComponent(tab), {headers: {'X-Requested-With':'fetch'}});
if (!res.ok) throw new Error('HTTP ' + res.status);
panel.innerHTML = await res.text();
const url = new URL(window.location.href);
url.searchParams.set('tab', tab);
window.history.replaceState({}, '', url);
} catch (err) {
panel.innerHTML = '<div class="rounded-3xl border border-red-200 bg-red-50 p-8 text-center text-sm font-semibold text-red-700 shadow-soft">Unable to load tab. Please refresh the page.</div>';
}
}
tabs.forEach(btn => btn.addEventListener('click', () => loadTab(btn.dataset.tab)));
})();
</script>
{% endblock %}
@@ -0,0 +1,46 @@
<div class="overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-soft">
<div class="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">Client</th>
<th class="px-4 py-3">Service / Task</th>
<th class="px-4 py-3">Due</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Priority</th>
<th class="px-4 py-3">Docs / Notes</th>
<th class="px-4 py-3 text-right">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for row in rows %}
<tr class="hover:bg-slate-50">
<td class="px-4 py-3">
<div class="font-semibold text-slate-900">{{ row.client_name }}</div>
<div class="text-xs text-slate-500">{{ row.client_code or '-' }}</div>
</td>
<td class="px-4 py-3">
<div class="text-xs font-semibold uppercase tracking-wide text-brand-600">{{ row.service_name }}</div>
<div class="font-medium text-slate-900">{{ row.task_name }}</div>
<div class="text-xs text-slate-500">Period: {{ row.period }}</div>
</td>
<td class="px-4 py-3">
{% if row.due_date %}
<div class="font-medium {% if row.is_overdue %}text-red-700{% elif row.is_due_today %}text-amber-700{% else %}text-slate-700{% endif %}">{{ row.due_date.strftime('%d-%b-%Y') }}</div>
{% if row.is_overdue %}<div class="text-xs text-red-600">{{ row.days_overdue }} day(s) overdue</div>{% endif %}
{% else %}
<span class="text-slate-400">No due date</span>
{% endif %}
</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">{{ row.status_label }}</span></td>
<td class="px-4 py-3"><span class="rounded-full bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">{{ row.priority|title }}</span></td>
<td class="px-4 py-3 text-xs text-slate-500">Docs: {{ row.document_count }}<br>Notes: {{ row.comment_count }}</td>
<td class="px-4 py-3 text-right"><a href="{{ row.href }}" class="rounded-xl bg-brand-600 px-3 py-2 text-xs font-semibold text-white hover:bg-brand-700">Open</a></td>
</tr>
{% else %}
<tr><td colspan="7" class="px-4 py-10 text-center text-sm text-slate-500">No tasks found for this view.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,5 @@
<div class="space-y-4">
<div class="rounded-3xl border border-orange-200 bg-orange-50 p-5 shadow-soft"><h2 class="text-lg font-bold text-orange-900">Client Pending</h2><p class="mt-1 text-sm text-orange-700">Work blocked due to client data, document or clarification pending.</p></div>
{% set rows = client_pending %}
{% include 'modules/staff_dashboard/templates/staff_dashboard/partials/_task_table.html' %}
</div>
@@ -0,0 +1,8 @@
<div class="space-y-4">
<div class="flex flex-col gap-3 rounded-3xl border border-slate-200 bg-white p-5 shadow-soft md:flex-row md:items-center md:justify-between">
<div><h2 class="text-lg font-bold text-slate-900">Documents</h2><p class="mt-1 text-sm text-slate-500">Document-linked or document-pending tasks assigned to you.</p></div>
<a href="/documents" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Open Documents</a>
</div>
{% set rows = documents_pending %}
{% include 'modules/staff_dashboard/templates/staff_dashboard/partials/_task_table.html' %}
</div>
@@ -0,0 +1,5 @@
<div class="space-y-4">
<div class="rounded-3xl border border-amber-200 bg-amber-50 p-5 shadow-soft"><h2 class="text-lg font-bold text-amber-900">Due Today</h2><p class="mt-1 text-sm text-amber-700">Tasks assigned to you with internal target date today.</p></div>
{% set rows = due_today %}
{% include 'modules/staff_dashboard/templates/staff_dashboard/partials/_task_table.html' %}
</div>
@@ -0,0 +1,10 @@
<div class="space-y-6">
<div class="grid gap-4 md:grid-cols-4">
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><p class="text-xs font-semibold uppercase text-slate-500">Open Tasks</p><p class="mt-2 text-3xl font-bold text-slate-900">{{ overview.open_count }}</p></div>
<div class="rounded-3xl border border-amber-200 bg-amber-50 p-5 shadow-soft"><p class="text-xs font-semibold uppercase text-amber-700">Due Today</p><p class="mt-2 text-3xl font-bold text-amber-800">{{ overview.due_today_count }}</p></div>
<div class="rounded-3xl border border-red-200 bg-red-50 p-5 shadow-soft"><p class="text-xs font-semibold uppercase text-red-700">Overdue</p><p class="mt-2 text-3xl font-bold text-red-800">{{ overview.overdue_count }}</p></div>
<div class="rounded-3xl border border-brand-200 bg-brand-50 p-5 shadow-soft"><p class="text-xs font-semibold uppercase text-brand-700">In Progress</p><p class="mt-2 text-3xl font-bold text-brand-800">{{ overview.in_progress_count }}</p></div>
</div>
{% set rows = my_tasks %}
{% include 'modules/staff_dashboard/templates/staff_dashboard/partials/_task_table.html' %}
</div>
@@ -0,0 +1,5 @@
<div class="space-y-4">
<div class="rounded-3xl border border-red-200 bg-red-50 p-5 shadow-soft"><h2 class="text-lg font-bold text-red-900">Overdue Tasks</h2><p class="mt-1 text-sm text-red-700">Assigned open tasks past internal target date.</p></div>
{% set rows = overdue %}
{% include 'modules/staff_dashboard/templates/staff_dashboard/partials/_task_table.html' %}
</div>
@@ -0,0 +1,13 @@
<div class="space-y-5">
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><h2 class="text-lg font-bold text-slate-900">Staff Reports</h2><p class="mt-1 text-sm text-slate-500">Quick report cards based on your assigned work.</p></div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{% for card in reports %}
<a href="{{ card.href }}" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft transition hover:-translate-y-0.5 hover:shadow-lg">
<p class="text-xs font-semibold uppercase tracking-wide text-brand-600">{{ card.group }}</p>
<h3 class="mt-2 text-base font-bold text-slate-900">{{ card.title }}</h3>
<p class="mt-2 text-sm text-slate-500">{{ card.desc }}</p>
<p class="mt-4 text-sm font-semibold text-brand-700">View report →</p>
</a>
{% endfor %}
</div>
</div>
@@ -0,0 +1,5 @@
<div class="space-y-4">
<div class="rounded-3xl border border-purple-200 bg-purple-50 p-5 shadow-soft"><h2 class="text-lg font-bold text-purple-900">Returned / Correction Work</h2><p class="mt-1 text-sm text-purple-700">Tasks returned for correction, rework or resubmission.</p></div>
{% set rows = returned_work %}
{% include 'modules/staff_dashboard/templates/staff_dashboard/partials/_task_table.html' %}
</div>
@@ -0,0 +1,12 @@
<div class="space-y-5">
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><h2 class="text-lg font-bold text-slate-900">Staff Work Wizards</h2><p class="mt-1 text-sm text-slate-500">Action shortcuts for task execution using existing ERP screens.</p></div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{% for card in wizards %}
<a href="{{ card.href }}" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft transition hover:-translate-y-0.5 hover:shadow-lg">
<h3 class="text-base font-bold text-slate-900">{{ card.title }}</h3>
<p class="mt-2 text-sm text-slate-500">{{ card.desc }}</p>
<p class="mt-4 text-sm font-semibold text-brand-700">Open →</p>
</a>
{% endfor %}
</div>
</div>
+75
View File
@@ -0,0 +1,75 @@
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.staff_dashboard.service import build_staff_dashboard_payload, can_access_staff_dashboard
router = APIRouter(prefix="/staff", tags=["staff-dashboard-v1-ui"])
VALID_TABS = {
"my-tasks": "modules/staff_dashboard/templates/staff_dashboard/partials/my_tasks.html",
"due-today": "modules/staff_dashboard/templates/staff_dashboard/partials/due_today.html",
"overdue": "modules/staff_dashboard/templates/staff_dashboard/partials/overdue.html",
"client-pending": "modules/staff_dashboard/templates/staff_dashboard/partials/client_pending.html",
"documents": "modules/staff_dashboard/templates/staff_dashboard/partials/documents.html",
"returned-work": "modules/staff_dashboard/templates/staff_dashboard/partials/returned_work.html",
"reports": "modules/staff_dashboard/templates/staff_dashboard/partials/reports.html",
"wizards": "modules/staff_dashboard/templates/staff_dashboard/partials/wizards.html",
}
def _ctx(request: Request, db, current_user, *, active_tab: str = "my-tasks"):
payload = build_staff_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": "Staff Dashboard",
"active_tab": active_tab,
**payload,
}
@router.get("/dashboard")
def dashboard(request: Request, tab: str = "my-tasks"):
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_staff_dashboard(db, current_user):
return ui_access_denied()
active_tab = tab if tab in VALID_TABS else "my-tasks"
return templates.TemplateResponse(
"modules/staff_dashboard/templates/staff_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_staff_dashboard(db, current_user):
return ui_access_denied()
active_tab = tab_name if tab_name in VALID_TABS else "my-tasks"
return templates.TemplateResponse(
VALID_TABS[active_tab],
_ctx(request, db, current_user, active_tab=active_tab),
)
finally:
db.close()
+3
View File
@@ -3,6 +3,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.staff_dashboard.ui import router as staff_dashboard_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
@@ -54,6 +55,7 @@ def mount_ui(app: FastAPI) -> None:
app.include_router(system_admin_dashboard_router)
app.include_router(work_detail_ui_router)
app.include_router(clients_ui_router)
app.include_router(staff_dashboard_router)
app.include_router(employees_ui_router)
app.include_router(manager_dashboard_router)
app.include_router(managers_ui_router)
@@ -66,3 +68,4 @@ def mount_ui(app: FastAPI) -> None:
app.include_router(consultant_portal_router)