Add advanced employee engagement workflow controls phase 4

This commit is contained in:
A R R R Associates
2026-07-20 00:47:34 +05:30
parent 031c278e8f
commit fe23b2b253
8 changed files with 330 additions and 32 deletions
+165 -3
View File
@@ -74,6 +74,15 @@ WORKFLOW_HOLD_REASONS = {
TASK_COMMUNICATION_VISIBILITY_CODES = {code for code, _ in TASK_COMMUNICATION_VISIBILITIES}
# Phase 4 derived controls deliberately reuse existing task, engagement, review and
# alert records. No duplicate workflow or capacity tables are introduced.
WORKFLOW_ESCALATION_LEVELS = {
"manager": "Manager",
"partner": "Engagement Partner",
"review_partner": "Review Partner",
}
@dataclass
class EmployeeScope:
@@ -2955,8 +2964,12 @@ def list_employee_work_kanban(
for card in engagement_lookup.values():
total = card["task_count"]
completed = card["completed_count"]
card["progress_percent"] = int(round((completed * 100) / total)) if total else 0
weighted = _weighted_progress(card.get("tasks", []))
card.update(weighted)
card["simple_progress_percent"] = int(round((completed * 100) / total)) if total else 0
card["open_count"] = max(total - completed, 0)
card["engagement_team"] = _engagement_team(card.get("subscription"))
card["sla"] = _engagement_sla(card.get("subscription"), card.get("tasks", []))
manual_blocker = _workflow_manual_blocker(card.get("subscription"))
automatic_blocker = _workflow_automatic_blocker(card.get("subscription"), card.get("tasks", []))
@@ -3118,6 +3131,148 @@ def _employee_workflow_next_task(tasks: list[ClientServiceTaskInstance], current
)
def _workflow_task_weight(task: ClientServiceTaskInstance) -> int:
"""Stable derived progress weight without changing the task schema.
Ordinary execution tasks count as 1. Review and AQMM/final-release tasks carry
higher weight so completion cannot appear artificially high while significant
review gates remain open.
"""
weight = 1
if bool(getattr(task, "is_aqmm_task", False)):
weight += 2
if bool(getattr(task, "aqmm_blocks_final_release", False)):
weight += 2
if any(bool(getattr(task, name, False)) for name in (
"aqmm_manager_review_required",
"aqmm_partner_review_required",
"aqmm_review_partner_required",
)):
weight += 1
if (getattr(task, "default_role_name", "") or "").strip().lower() in {"manager", "partner", "review partner"}:
weight += 1
return min(weight, 7)
def _weighted_progress(tasks: list[ClientServiceTaskInstance]) -> dict[str, int]:
total_weight = sum(_workflow_task_weight(task) for task in tasks)
completed_weight = sum(
_workflow_task_weight(task)
for task in tasks
if (getattr(task, "status", None) or "pending").strip().lower() in CLOSED_TASK_STATUSES
)
percent = int(round((completed_weight * 100) / total_weight)) if total_weight else 0
return {
"total_weight": total_weight,
"completed_weight": completed_weight,
"progress_percent": percent,
}
def _engagement_team(subscription: ClientServiceSubscription | None) -> list[dict[str, Any]]:
if subscription is None:
return []
team = []
for role, rel_name, id_name in (
("Engagement Partner", "assigned_partner", "assigned_partner_user_id"),
("Manager", "assigned_manager", "assigned_manager_user_id"),
("Primary Staff", "assigned_staff", "assigned_staff_user_id"),
("Review Partner", "review_partner", "review_partner_user_id"),
):
user = getattr(subscription, rel_name, None)
user_id = getattr(subscription, id_name, None)
if user_id:
team.append({
"role": role,
"user_id": user_id,
"name": getattr(user, "full_name", None) or getattr(user, "email", None) or f"User #{user_id}",
"email": getattr(user, "email", None) or "",
})
return team
def _engagement_sla(subscription: ClientServiceSubscription | None, tasks: list[ClientServiceTaskInstance]) -> dict[str, Any]:
today = date.today()
due_date = None
if subscription is not None:
due_date = getattr(subscription, "current_due_date", None) or getattr(subscription, "original_due_date", None) or getattr(subscription, "end_date", None)
if due_date is None:
dates = [getattr(task, "internal_target_date", None) for task in tasks if getattr(task, "internal_target_date", None)]
due_date = max(dates) if dates else None
started_dates = [getattr(task, "started_at_utc", None) for task in tasks if getattr(task, "started_at_utc", None)]
created_at = getattr(subscription, "created_at_utc", None) if subscription is not None else None
anchor = min(started_dates) if started_dates else created_at
age_days = max((datetime.now(timezone.utc) - anchor).days, 0) if anchor else 0
days_remaining = (due_date - today).days if due_date else None
if due_date and days_remaining < 0:
status = "breached"
label = f"Overdue by {abs(days_remaining)} day(s)"
elif due_date and days_remaining <= 3:
status = "critical"
label = f"Due in {days_remaining} day(s)"
elif due_date and days_remaining <= 7:
status = "warning"
label = f"Due in {days_remaining} day(s)"
elif due_date:
status = "on_track"
label = f"{days_remaining} day(s) remaining"
else:
status = "not_set"
label = "No SLA due date"
return {
"due_date": due_date,
"days_remaining": days_remaining,
"age_days": age_days,
"status": status,
"label": label,
}
def escalate_employee_engagement_workflow(
db: Session,
scope: EmployeeScope,
engagement_id: int,
*,
level: str,
message: str,
actor_user_id: int,
financial_year: str | None = None,
) -> int:
tasks = _employee_engagement_tasks(db, scope, engagement_id, financial_year=financial_year)
if not tasks:
raise ValueError("Engagement was not found or is not assigned to you.")
subscription = getattr(tasks[0], "subscription", None)
if subscription is None:
raise ValueError("Engagement subscription was not found.")
code = (level or "manager").strip().lower()
target_id = {
"manager": getattr(subscription, "assigned_manager_user_id", None),
"partner": getattr(subscription, "assigned_partner_user_id", None),
"review_partner": getattr(subscription, "review_partner_user_id", None),
}.get(code)
if not target_id:
raise ValueError(f"{WORKFLOW_ESCALATION_LEVELS.get(code, 'Selected reviewer')} is not assigned to this engagement.")
clean_message = (message or "").strip()[:1000]
if not clean_message:
raise ValueError("Escalation details are required.")
create_alert(
db,
user_id=int(target_id),
title=f"Workflow escalation: {_subscription_label(subscription, tasks[0])}",
message=clean_message,
tenant_id=subscription.tenant_id,
branch_id=subscription.branch_id,
role_context=code,
alert_type="escalation",
priority="high",
target_url=f"/employee/work/engagements/{engagement_id}",
created_by_user_id=actor_user_id,
commit=False,
)
db.commit()
return int(target_id)
def get_employee_engagement_work_board(
db: Session,
scope: EmployeeScope,
@@ -3199,7 +3354,9 @@ def get_employee_engagement_work_board(
category["in_progress"] += 1
for category in categories:
category["progress_percent"] = round(
weighted_category = _weighted_progress(category["tasks"])
category.update(weighted_category)
category["simple_progress_percent"] = round(
(category["completed"] / category["total"]) * 100
) if category["total"] else 0
if category["completed"] == category["total"] and category["total"]:
@@ -3225,7 +3382,8 @@ def get_employee_engagement_work_board(
selected_category = _employee_task_category(selected_task)
next_task = _employee_workflow_next_task(tasks, selected_task.id)
overall_progress = round((summary["completed"] / summary["total"]) * 100) if summary["total"] else 0
weighted_overall = _weighted_progress(tasks)
overall_progress = weighted_overall["progress_percent"]
subscription = getattr(tasks[0], "subscription", None)
manual_blocker = _workflow_manual_blocker(subscription)
automatic_blocker = _workflow_automatic_blocker(subscription, tasks)
@@ -3241,6 +3399,10 @@ def get_employee_engagement_work_board(
"label": _subscription_label(getattr(tasks[0], "subscription", None), tasks[0]),
"summary": summary,
"progress_percent": overall_progress,
"weighted_progress": weighted_overall,
"engagement_team": _engagement_team(subscription),
"sla": _engagement_sla(subscription, tasks),
"escalation_levels": WORKFLOW_ESCALATION_LEVELS,
"categories": categories,
"active_task": selected_task,
"active_category": selected_category,