114 lines
4.8 KiB
Python
114 lines
4.8 KiB
Python
"""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}
|