Add manager engagement review workspace phase 5A
This commit is contained in:
@@ -181,6 +181,7 @@ def _load_tasks(db: Session, scope, fy: str | None) -> list[ClientServiceTaskIns
|
||||
selectinload(ClientServiceTaskInstance.catalogue),
|
||||
selectinload(ClientServiceTaskInstance.assigned_to),
|
||||
selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
|
||||
selectinload(ClientServiceTaskInstance.documents),
|
||||
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
|
||||
)
|
||||
)
|
||||
@@ -463,3 +464,170 @@ def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[
|
||||
"reports": _report_cards(),
|
||||
"wizards": _wizard_cards(),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5A - engagement-level Manager review workspace.
|
||||
# This is a presentation and navigation layer over the existing task review,
|
||||
# evidence, comment, rework, AQMM and lock controls. No duplicate review state
|
||||
# or review table is introduced.
|
||||
# ---------------------------------------------------------------------------
|
||||
_MANAGER_REVIEW_PENDING = {"pending", "review_pending", "pending_review", "manager_review", "ready_review"}
|
||||
_MANAGER_REVIEW_DONE = {"reviewed"}
|
||||
_MANAGER_REVIEW_REWORK = {"rework_required"}
|
||||
|
||||
|
||||
def _manager_review_task_state(task: ClientServiceTaskInstance) -> str:
|
||||
manager_status = (getattr(task, "manager_review_status", None) or "not_required").strip().lower()
|
||||
task_status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||
rework_status = (getattr(task, "rework_status", None) or "none").strip().lower()
|
||||
if manager_status in _MANAGER_REVIEW_REWORK or rework_status == "open":
|
||||
return "rework"
|
||||
if manager_status in _MANAGER_REVIEW_DONE:
|
||||
return "reviewed"
|
||||
if manager_status in _MANAGER_REVIEW_PENDING or task_status in REVIEW_STATUSES:
|
||||
return "pending_review"
|
||||
if getattr(task, "aqmm_manager_review_required", False) and task_status in CLOSED_TASK_STATUSES:
|
||||
return "pending_review"
|
||||
return "not_ready"
|
||||
|
||||
|
||||
def _manager_review_category(task: ClientServiceTaskInstance) -> str:
|
||||
return (getattr(task, "task_category", None) or "General Workflow").strip() or "General Workflow"
|
||||
|
||||
|
||||
def _manager_review_task_row(task: ClientServiceTaskInstance) -> dict[str, Any]:
|
||||
state = _manager_review_task_state(task)
|
||||
comments = [c for c in (getattr(task, "comments", None) or []) if not getattr(c, "is_deleted", False)]
|
||||
documents = list(getattr(task, "documents", None) or [])
|
||||
assignee = getattr(task, "assigned_to", None)
|
||||
return {
|
||||
"id": int(task.id),
|
||||
"task_name": getattr(task, "task_name", None) or "Task",
|
||||
"description": getattr(task, "description", None) or "",
|
||||
"category": _manager_review_category(task),
|
||||
"sequence_no": int(getattr(task, "sequence_no", 0) or 0),
|
||||
"task_status": (getattr(task, "status", None) or "pending").strip().lower(),
|
||||
"task_status_label": _status_label(getattr(task, "status", None)),
|
||||
"review_state": state,
|
||||
"review_state_label": state.replace("_", " ").title(),
|
||||
"manager_review_status": (getattr(task, "manager_review_status", None) or "not_required").strip().lower(),
|
||||
"manager_review_note": getattr(task, "manager_review_note", None) or "",
|
||||
"rework_status": (getattr(task, "rework_status", None) or "none").strip().lower(),
|
||||
"rework_reason": getattr(task, "rework_reason", None) or "",
|
||||
"assigned_to": getattr(assignee, "full_name", None) or getattr(assignee, "email", None) or "Unassigned",
|
||||
"response_type": (getattr(task, "response_type", None) or "NONE").strip().upper(),
|
||||
"response_required": bool(getattr(task, "response_required", False)),
|
||||
"checklist_response": getattr(task, "checklist_response", None),
|
||||
"checklist_text_response": getattr(task, "checklist_text_response", None),
|
||||
"checklist_number_response": getattr(task, "checklist_number_response", None),
|
||||
"checklist_date_response": getattr(task, "checklist_date_response", None),
|
||||
"checklist_remarks": getattr(task, "checklist_remarks", None) or "",
|
||||
"evidence_required": bool(getattr(task, "evidence_required", False) or getattr(task, "aqmm_evidence_required", False)),
|
||||
"evidence_count": len(documents),
|
||||
"comment_count": len(comments),
|
||||
"comments": comments[:20],
|
||||
"is_aqmm_task": bool(getattr(task, "is_aqmm_task", False)),
|
||||
"aqmm_reference": getattr(task, "aqmm_reference", None) or "",
|
||||
"blocks_final_release": bool(getattr(task, "aqmm_blocks_final_release", False)),
|
||||
"is_locked": bool(getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False)),
|
||||
"documents_href": f"/documents/tasks/{task.id}",
|
||||
"task_href": f"/services/work-tracker/tasks/{task.id}/edit",
|
||||
}
|
||||
|
||||
|
||||
def get_manager_review_workspace(
|
||||
db: Session,
|
||||
request,
|
||||
current_user,
|
||||
*,
|
||||
subscription_id: int,
|
||||
active_task_id: int | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Build one Manager review workspace for an engagement in active scope."""
|
||||
scope = _scope(db, request, current_user)
|
||||
fy = _active_financial_year(request)
|
||||
stmt = (
|
||||
select(ClientServiceTaskInstance)
|
||||
.options(
|
||||
selectinload(ClientServiceTaskInstance.client),
|
||||
selectinload(ClientServiceTaskInstance.catalogue),
|
||||
selectinload(ClientServiceTaskInstance.assigned_to),
|
||||
selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
|
||||
selectinload(ClientServiceTaskInstance.documents),
|
||||
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
|
||||
)
|
||||
.where(ClientServiceTaskInstance.subscription_id == int(subscription_id))
|
||||
)
|
||||
stmt = _task_scope(stmt, scope, fy)
|
||||
tasks = list(db.execute(stmt.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())).scalars().all())
|
||||
if not tasks:
|
||||
return None
|
||||
|
||||
task_rows = [_manager_review_task_row(task) for task in tasks]
|
||||
reviewable = [row for row in task_rows if row["review_state"] in {"pending_review", "reviewed", "rework"}]
|
||||
pending = [row for row in reviewable if row["review_state"] == "pending_review"]
|
||||
reviewed = [row for row in reviewable if row["review_state"] == "reviewed"]
|
||||
rework = [row for row in reviewable if row["review_state"] == "rework"]
|
||||
|
||||
categories_by_name: dict[str, dict[str, Any]] = {}
|
||||
for row in task_rows:
|
||||
category = categories_by_name.setdefault(row["category"], {
|
||||
"name": row["category"], "tasks": [], "total": 0, "reviewable": 0,
|
||||
"pending": 0, "reviewed": 0, "rework": 0,
|
||||
})
|
||||
category["tasks"].append(row)
|
||||
category["total"] += 1
|
||||
if row["review_state"] in {"pending_review", "reviewed", "rework"}:
|
||||
category["reviewable"] += 1
|
||||
category[row["review_state"].replace("pending_review", "pending")] += 1
|
||||
categories = list(categories_by_name.values())
|
||||
for category in categories:
|
||||
denominator = category["reviewable"] or 0
|
||||
category["progress_percent"] = round(category["reviewed"] * 100 / denominator) if denominator else 0
|
||||
if category["rework"]:
|
||||
category["status"] = "rework"
|
||||
elif category["pending"]:
|
||||
category["status"] = "pending_review"
|
||||
elif denominator and category["reviewed"] == denominator:
|
||||
category["status"] = "reviewed"
|
||||
else:
|
||||
category["status"] = "not_ready"
|
||||
|
||||
active = None
|
||||
if active_task_id:
|
||||
active = next((row for row in task_rows if row["id"] == int(active_task_id)), None)
|
||||
if active is None:
|
||||
active = (pending or rework or reviewed or task_rows)[0]
|
||||
|
||||
first = tasks[0]
|
||||
subscription = getattr(first, "subscription", None)
|
||||
weighted = _weighted_progress(tasks)
|
||||
sla = _engagement_sla(subscription, tasks)
|
||||
team = _engagement_team(subscription)
|
||||
return {
|
||||
"subscription_id": int(subscription_id),
|
||||
"client_name": getattr(getattr(first, "client", None), "client_name", None) or "Client",
|
||||
"service_name": getattr(getattr(first, "catalogue", None), "service_name", None) or "Service",
|
||||
"financial_year": getattr(first, "financial_year", None) or "-",
|
||||
"weighted_progress": weighted,
|
||||
"sla": sla,
|
||||
"team": team,
|
||||
"categories": categories,
|
||||
"tasks": task_rows,
|
||||
"active_task": active,
|
||||
"reviewable_count": len(reviewable),
|
||||
"pending_review_count": len(pending),
|
||||
"reviewed_count": len(reviewed),
|
||||
"rework_count": len(rework),
|
||||
"is_locked": bool(getattr(subscription, "is_locked", False)),
|
||||
}
|
||||
|
||||
|
||||
def get_next_manager_review_task_id(workspace: dict[str, Any], current_task_id: int) -> int | None:
|
||||
"""Return the next task requiring Manager action, preserving category/sequence order."""
|
||||
tasks = workspace.get("tasks") or []
|
||||
current_index = next((i for i, row in enumerate(tasks) if int(row["id"]) == int(current_task_id)), -1)
|
||||
ordered = tasks[current_index + 1:] + tasks[:max(current_index, 0)]
|
||||
next_row = next((row for row in ordered if row["review_state"] == "pending_review"), None)
|
||||
return int(next_row["id"]) if next_row else None
|
||||
|
||||
Reference in New Issue
Block a user