Files
MMD 2747f9d4bb chore(pm): vendor the Aramesh/PM platform under pm/ (squash of Aramesh/PM @ 91f3271)
The PM panel (PmPanelPage -> iframe /pm/) needs the project-management
platform served at /pm/ on the ERP domain. Source: org repo Aramesh/PM,
branch feature/project-management-platform. Update via:
git subtree pull --prefix=pm aramesh-pm feature/project-management-platform --squash
2026-10-07 15:14:20 +03:30

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}