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(), }