Add advanced employee engagement workflow controls phase 4
This commit is contained in:
@@ -9,7 +9,13 @@ from sqlalchemy.orm import Session, selectinload
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.core.iam.models import User
|
||||
from app.modules.core.rbac.models import Permission, Role, RolePermission, UserRole
|
||||
from app.modules.employees.service import build_employee_scope, list_employee_work_assignable_users
|
||||
from app.modules.employees.service import (
|
||||
build_employee_scope,
|
||||
list_employee_work_assignable_users,
|
||||
_engagement_sla,
|
||||
_engagement_team,
|
||||
_weighted_progress,
|
||||
)
|
||||
from app.modules.services.execution import CLOSED_TASK_STATUSES
|
||||
from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance, ServiceCatalogue, ServiceTaskComment
|
||||
|
||||
@@ -288,6 +294,77 @@ def _wizard_cards() -> list[dict[str, str]]:
|
||||
]
|
||||
|
||||
|
||||
def _advanced_engagement_rows(tasks: list[ClientServiceTaskInstance]) -> list[dict[str, Any]]:
|
||||
grouped: dict[int, list[ClientServiceTaskInstance]] = {}
|
||||
for task in tasks:
|
||||
grouped.setdefault(int(task.subscription_id), []).append(task)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for subscription_id, group in grouped.items():
|
||||
first = group[0]
|
||||
subscription = getattr(first, "subscription", None)
|
||||
weighted = _weighted_progress(group)
|
||||
sla = _engagement_sla(subscription, group)
|
||||
team = _engagement_team(subscription)
|
||||
status_values = {(getattr(t, "status", None) or "pending").strip().lower() for t in group}
|
||||
completed = sum(1 for t in group if (getattr(t, "status", None) or "pending").strip().lower() in CLOSED_TASK_STATUSES)
|
||||
review_pending = sum(1 for t in group if (getattr(t, "manager_review_status", None) or "") == "pending" or (getattr(t, "partner_review_status", None) or "") == "pending" or (getattr(t, "review_partner_review_status", None) or "") == "pending")
|
||||
if completed == len(group) and group:
|
||||
status = "completed"
|
||||
elif "blocked" in status_values or getattr(subscription, "workflow_pause_reason", None):
|
||||
status = "blocked"
|
||||
elif "in_progress" in status_values or completed:
|
||||
status = "in_progress"
|
||||
else:
|
||||
status = "pending"
|
||||
rows.append({
|
||||
"subscription_id": 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 "-",
|
||||
"status": status,
|
||||
"task_count": len(group),
|
||||
"completed_count": completed,
|
||||
"review_pending": review_pending,
|
||||
"progress_percent": weighted["progress_percent"],
|
||||
"completed_weight": weighted["completed_weight"],
|
||||
"total_weight": weighted["total_weight"],
|
||||
"sla": sla,
|
||||
"team": team,
|
||||
"manager_name": next((m["name"] for m in team if m["role"] == "Manager"), "Unassigned"),
|
||||
"staff_name": next((m["name"] for m in team if m["role"] == "Primary Staff"), "Unassigned"),
|
||||
"partner_name": next((m["name"] for m in team if m["role"] == "Engagement Partner"), "Unassigned"),
|
||||
"href": f"/employee/work/engagements/{subscription_id}",
|
||||
})
|
||||
rows.sort(key=lambda r: (r["sla"]["status"] == "breached", r["review_pending"], -r["progress_percent"]), reverse=True)
|
||||
return rows
|
||||
|
||||
|
||||
def _capacity_rows(tasks: list[ClientServiceTaskInstance]) -> list[dict[str, Any]]:
|
||||
by_user: dict[str, dict[str, Any]] = {}
|
||||
for task in tasks:
|
||||
user = getattr(task, "assigned_to", None)
|
||||
name = getattr(user, "full_name", None) or getattr(user, "email", None) or "Unassigned"
|
||||
row = by_user.setdefault(name, {"name": name, "open_weight": 0, "open_tasks": 0, "overdue": 0, "review": 0, "engagement_ids": set()})
|
||||
status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||
if status not in CLOSED_TASK_STATUSES:
|
||||
row["open_tasks"] += 1
|
||||
row["open_weight"] += _weighted_progress([task])["total_weight"]
|
||||
row["engagement_ids"].add(int(task.subscription_id))
|
||||
if getattr(task, "internal_target_date", None) and task.internal_target_date < date.today():
|
||||
row["overdue"] += 1
|
||||
if status in REVIEW_STATUSES:
|
||||
row["review"] += 1
|
||||
out=[]
|
||||
for row in by_user.values():
|
||||
score = row["open_weight"] + row["review"] * 2 + row["overdue"] * 3
|
||||
row["engagement_count"] = len(row.pop("engagement_ids"))
|
||||
row["capacity_score"] = score
|
||||
row["load_status"] = "Heavy" if score >= 55 else ("Balanced" if score >= 18 else "Light")
|
||||
out.append(row)
|
||||
out.sort(key=lambda r: r["capacity_score"], reverse=True)
|
||||
return out
|
||||
|
||||
|
||||
def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[str, Any]:
|
||||
scope = _scope(db, request, current_user)
|
||||
fy = _active_financial_year(request)
|
||||
@@ -306,6 +383,11 @@ def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[
|
||||
|
||||
staff_rows = _staff_rows(db, scope, task_rows)
|
||||
client_rows = _client_rows(db, scope, task_rows)
|
||||
advanced_engagements = _advanced_engagement_rows(tasks)
|
||||
capacity_rows = _capacity_rows(tasks)
|
||||
sla_breached = [row for row in advanced_engagements if row["sla"]["status"] == "breached"]
|
||||
sla_warning = [row for row in advanced_engagements if row["sla"]["status"] in {"critical", "warning"}]
|
||||
engagement_review_queue = [row for row in advanced_engagements if row["review_pending"]]
|
||||
|
||||
tenant = getattr(scope, "tenant", None)
|
||||
branch = getattr(scope, "branch", None)
|
||||
@@ -347,6 +429,11 @@ def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[
|
||||
"staff_count": len(staff_rows),
|
||||
"client_count": len(client_rows),
|
||||
"escalation_count": len(escalation_rows),
|
||||
"engagement_count": len(advanced_engagements),
|
||||
"sla_breached_count": len(sla_breached),
|
||||
"sla_warning_count": len(sla_warning),
|
||||
"engagement_review_count": len(engagement_review_queue),
|
||||
"heavy_capacity_count": len([row for row in capacity_rows if row["load_status"] == "Heavy"]),
|
||||
"today": today,
|
||||
}
|
||||
|
||||
@@ -368,6 +455,11 @@ def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[
|
||||
"service_summary": sorted([{"label": k, "count": v} for k, v in service_summary.items()], key=lambda x: x["count"], reverse=True)[:10],
|
||||
"staff_rows": staff_rows,
|
||||
"clients": client_rows,
|
||||
"advanced_engagements": advanced_engagements,
|
||||
"capacity_rows": capacity_rows,
|
||||
"sla_breached": sla_breached,
|
||||
"sla_warning": sla_warning,
|
||||
"engagement_review_queue": engagement_review_queue,
|
||||
"reports": _report_cards(),
|
||||
"wizards": _wizard_cards(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user