"""Today dashboard. Both roles land here, but they need different things from it: the project manager gets a management overview (every project, with its counters), the supervisor gets an operational view of the activities in the projects assigned to them. The data is the same query either way — only scoped by role and presented differently (see frontend TodayPage). """ from datetime import date, timedelta from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.authz import AuthContext, Perm, require_permission from app.db import get_db from app.models import Project, Task from app.serializers import _iso, native_task_dict router = APIRouter(prefix="/dashboard", tags=["dashboard"]) view_dashboard = require_permission(Perm.dashboard_view) async def _native_project_overview(db: AsyncSession, ctx: AuthContext) -> list[dict]: """کاشی‌های مدیریتی پروژه‌های بومی برای داشبورد مدیر پروژه.""" proj_stmt = ctx.scope_native(select(Project).order_by(Project.name), Project.id) projects = (await db.execute(proj_stmt)).scalars().all() tasks = (await db.execute(ctx.scope_native(select(Task), Task.project_id))).scalars().all() today_date = date.today() by_project: dict = {} for t in tasks: by_project.setdefault(t.project_id, []).append(t) rows: list[dict] = [] for p in projects: mine = by_project.get(p.id, []) open_tasks = [t for t in mine if t.status not in ("COMPLETED", "CANCELLED")] stats = { "total": len(mine), "active": len(open_tasks), "awaiting_qc": sum(1 for t in mine if t.qc_status in ("pre_qc_pending", "exec_qc_pending", "ready")), "failed_qc": sum(1 for t in mine if t.qc_status in ("pre_qc_failed", "qc_failed_rework")), "overdue": sum(1 for t in open_tasks if t.planned_end and t.planned_end < today_date), } rows.append( { "id": str(p.id), "name": p.name, "identifier": str(p.id), "active": p.status == "ACTIVE", "stats": stats, } ) return rows @router.get("/today") async def today(ctx: AuthContext = Depends(view_dashboard), db: AsyncSession = Depends(get_db)): native_stmt = ctx.scope_native( select(Task).options(selectinload(Task.project)), Task.project_id ) native_tasks = (await db.execute(native_stmt)).scalars().all() today_date = date.today() week_end = today_date + timedelta(days=7) def is_task_open(t: Task) -> bool: return t.status != "COMPLETED" and t.status != "CANCELLED" overdue = [t for t in native_tasks if is_task_open(t) and t.planned_end and t.planned_end < today_date] due_today = [t for t in native_tasks if is_task_open(t) and t.planned_end == today_date] upcoming = [ t for t in native_tasks if is_task_open(t) and t.planned_end and today_date < t.planned_end <= week_end ] awaiting_qc = [t for t in native_tasks if t.qc_status in ("pre_qc_pending", "exec_qc_pending", "ready")] failed_qc = [t for t in native_tasks if t.qc_status in ("pre_qc_failed", "qc_failed_rework")] blocked = [t for t in native_tasks if is_task_open(t) and t.status == "BLOCKED"] def _native_row(t: Task) -> dict: p_name = t.project.name if t.project else "" d = native_task_dict(t, project_name=p_name) d["subject"] = t.title d["percentage_done"] = t.progress d["start_date"] = _iso(t.planned_start) d["due_date"] = _iso(t.planned_end) d["is_closed"] = t.status == "COMPLETED" return d problems = len(overdue) + len(failed_qc) warnings = len(awaiting_qc) + len(blocked) ok = max(len(native_tasks) - problems - warnings, 0) data = { "role": ctx.role.value, "role_label": ctx.role_label, "scope": ctx.as_dict()["scope"], "summary": {"problems": problems, "warnings": warnings, "ok": ok}, "sections": { "overdue": [_native_row(t) for t in sorted(overdue, key=lambda x: x.planned_end or date.max)], "due_today": [_native_row(t) for t in due_today], "awaiting_qc": [_native_row(t) for t in awaiting_qc], "failed_qc": [_native_row(t) for t in failed_qc], "blocked": [_native_row(t) for t in blocked], "upcoming": [_native_row(t) for t in sorted(upcoming, key=lambda x: x.planned_end or date.max)[:10]], }, } # نمای مدیریتی پروژه‌ها فقط برای نقش بدون محدودیت (مدیر پروژه) if ctx.all_projects: data["projects"] = await _native_project_overview(db, ctx) return {"data": data}