From fe23b2b25318c306a9087161a1dd260b49c4ee00 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Mon, 20 Jul 2026 00:47:34 +0530 Subject: [PATCH] Add advanced employee engagement workflow controls phase 4 --- app/modules/employees/service.py | 168 +++++++++++++++++- .../templates/employees/self_work.html | 4 +- .../employees/work_engagement_board.html | 18 ++ app/modules/employees/ui.py | 32 ++++ app/modules/manager_dashboard/service.py | 94 +++++++++- .../partials/escalations.html | 21 +-- .../partials/review_queue.html | 4 +- .../manager_dashboard/partials/team_work.html | 21 ++- 8 files changed, 330 insertions(+), 32 deletions(-) diff --git a/app/modules/employees/service.py b/app/modules/employees/service.py index 9133374..34b4a39 100644 --- a/app/modules/employees/service.py +++ b/app/modules/employees/service.py @@ -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, diff --git a/app/modules/employees/templates/employees/self_work.html b/app/modules/employees/templates/employees/self_work.html index 3a2418f..c71b835 100644 --- a/app/modules/employees/templates/employees/self_work.html +++ b/app/modules/employees/templates/employees/self_work.html @@ -78,10 +78,10 @@
My progress - {{ card.progress_percent }}% + {{ card.progress_percent }}% weighted
-
+
{{ card.completed_count }} of {{ card.task_count }} assigned tasks completed
diff --git a/app/modules/employees/templates/employees/work_engagement_board.html b/app/modules/employees/templates/employees/work_engagement_board.html index 86633ab..81dcaa0 100644 --- a/app/modules/employees/templates/employees/work_engagement_board.html +++ b/app/modules/employees/templates/employees/work_engagement_board.html @@ -30,6 +30,15 @@ {% if request.query_params.get('resumed') %}
Engagement workflow resumed.
{% endif %} {% if request.query_params.get('pause_error') or request.query_params.get('resume_error') %}
The workflow status could not be changed. Confirm the reason and that the engagement is not locked.
{% endif %} +
+
Weighted Progress
{{ board.progress_percent }}%
{{ board.weighted_progress.completed_weight }} of {{ board.weighted_progress.total_weight }} weighted points
+
SLA & Ageing
{{ board.sla.label }}
Age {{ board.sla.age_days }} day(s){% if board.sla.due_date %} · Due {{ board.sla.due_date }}{% endif %}
+
Engagement Team
{% for member in board.engagement_team %}{{ member.role }}: {{ member.name }}{% else %}No engagement team assigned.{% endfor %}
+
+ + {% if request.query_params.get('escalated') %}
Escalation alert sent successfully.
{% endif %} + {% if request.query_params.get('escalation_error') %}
Escalation could not be sent. Confirm the selected reviewer is assigned and enter details.
{% endif %} + {% if board.workflow_blocker %}
{{ board.workflow_blocker.label }}
@@ -200,6 +209,15 @@
+
+ Escalate dependency or review delay +
+ + + + +
+
Close Workspace
diff --git a/app/modules/employees/ui.py b/app/modules/employees/ui.py index 7bd8af0..a08ee6a 100644 --- a/app/modules/employees/ui.py +++ b/app/modules/employees/ui.py @@ -51,6 +51,7 @@ from app.modules.employees.service import ( save_employee_workflow_task, pause_employee_engagement_workflow, resume_employee_engagement_workflow, + escalate_employee_engagement_workflow, list_employee_work_assignable_users, list_visible_work_assignment_dashboard, list_engagement_progress_dashboard, @@ -2169,6 +2170,37 @@ def employee_workflow_resume(request: Request, engagement_id: int, csrf_token: s finally: db.close() +@portal_router.post("/work/engagements/{engagement_id}/escalate") +def employee_workflow_escalate( + request: Request, + engagement_id: int, + level: str = Form("manager"), + message: str = Form(""), + csrf_token: str = Form(""), +): + if not validate_csrf(request, csrf_token): + return _csrf_rejected(request) + with CommonSessionLocal() as db: + current_user = get_current_user(request, db) + if not current_user: + return RedirectResponse(url="/login", status_code=303) + scope = build_employee_scope( + db, current_user, + tenant_id=request.session.get("active_tenant_id") or getattr(current_user, "tenant_id", None), + branch_id=request.session.get("active_branch_id"), + ) + try: + escalate_employee_engagement_workflow( + db, scope, engagement_id, level=level, message=message, + actor_user_id=current_user.id, + financial_year=request.session.get("active_financial_year"), + ) + except ValueError: + db.rollback() + return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?escalation_error=1", status_code=303) + return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?escalated=1", status_code=303) + + @portal_router.post("/work/engagements/{engagement_id}/tasks/{task_id}/save") def employee_workflow_task_save( request: Request, diff --git a/app/modules/manager_dashboard/service.py b/app/modules/manager_dashboard/service.py index f1aa25e..4b24b14 100644 --- a/app/modules/manager_dashboard/service.py +++ b/app/modules/manager_dashboard/service.py @@ -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(), } diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/escalations.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/escalations.html index 2a183eb..65d0678 100644 --- a/app/modules/manager_dashboard/templates/manager_dashboard/partials/escalations.html +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/escalations.html @@ -1,20 +1,5 @@
-
-
Overdue
{{ overview.overdue_count }}
Ageing risk
-
Unassigned
{{ overview.unassigned_count }}
Manager action
-
Client Pending
{{ overview.client_pending_count }}
Follow-up needed
-
Escalation Items
{{ overview.escalation_count }}
Combined attention list
-
- -
-
-

Escalation Register

Unassigned, overdue and client-pending tasks sorted by urgency.

Open Alerts
- {% set rows = escalations %} - {% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %} -
- -
+
SLA Breached
{{ overview.sla_breached_count }}
SLA Warning
{{ overview.sla_warning_count }}
Task Escalations
{{ overview.escalation_count }}
Review Dependencies
{{ overview.engagement_review_count }}
+

SLA Breaches

Engagement-level statutory/internal due-date breaches.

{% for row in sla_breached %}{% else %}{% endfor %}
{{ row.client_name }} — {{ row.service_name }}
{{ row.financial_year }} · {{ row.progress_percent }}% complete
{{ row.sla.label }}
Age {{ row.sla.age_days }} day(s)
No SLA breaches.

SLA Warning Window

Due within seven days.

{% for row in sla_warning %}{% else %}{% endfor %}
{{ row.client_name }} — {{ row.service_name }}
{{ row.financial_year }} · {{ row.progress_percent }}% complete
{{ row.sla.label }}
No engagements in warning window.
+

Existing Task Escalation Register

Unassigned, overdue and client-pending tasks remain visible.

Open Alerts
{% set rows = escalations %}{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/review_queue.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/review_queue.html index 44ef62c..dfbdd0d 100644 --- a/app/modules/manager_dashboard/templates/manager_dashboard/partials/review_queue.html +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/review_queue.html @@ -1,3 +1,5 @@
-

Manager Review Queue

Completed or review-ready work waiting for manager action.

Progress
{% set rows = review_queue %}{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
+
Engagements Awaiting Review
{{ overview.engagement_review_count }}
Task Review Queue
{{ overview.review_pending_count }}
SLA Breached
{{ overview.sla_breached_count }}
+

Engagement Review Dashboard

Prioritised by review dependency, weighted completion and SLA ageing.

{% for row in engagement_review_queue %}{% else %}{% endfor %}
EngagementReview PendingWeighted ProgressSLA / AgeAction
{{ row.client_name }}
{{ row.service_name }} · {{ row.financial_year }}
{{ row.review_pending }}{{ row.progress_percent }}%
{{ row.sla.label }}
{{ row.sla.age_days }} day(s)
Open Workflow
No engagement review dependencies.
+

Detailed Task Review Queue

Existing task-level review workflow remains available.

{% set rows = review_queue %}{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}
diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/partials/team_work.html b/app/modules/manager_dashboard/templates/manager_dashboard/partials/team_work.html index 2a5ba81..764a25e 100644 --- a/app/modules/manager_dashboard/templates/manager_dashboard/partials/team_work.html +++ b/app/modules/manager_dashboard/templates/manager_dashboard/partials/team_work.html @@ -1,10 +1,17 @@
-
-
Total Visible
{{ overview.total_tasks }}
-
Unassigned
{{ overview.unassigned_count }}
-
In Progress
{{ overview.in_progress_count }}
-
Overdue
{{ overview.overdue_count }}
-
Due Today
{{ overview.due_today_count }}
+
+
Visible Engagements
{{ overview.engagement_count }}
+
SLA Breached
{{ overview.sla_breached_count }}
+
SLA Warning
{{ overview.sla_warning_count }}
+
Heavy Capacity
{{ overview.heavy_capacity_count }}
+
+ +
+

Engagement Capacity Plan

Derived from open task weight, review load and overdue work. Existing assignments remain unchanged.

Assign / Reassign
+ {% for row in capacity_rows %}{% else %}{% endfor %}
Team MemberEngagementsOpen TasksWeighted LoadReviewOverdueCapacity
{{ row.name }}{{ row.engagement_count }}{{ row.open_tasks }}{{ row.open_weight }}{{ row.review }}{{ row.overdue }}{{ row.load_status }}
No visible workload.
+
+ +

Engagement Team & Weighted Progress

One row per engagement using existing Partner, Manager, Staff and Review Partner assignments.

+ {% for row in advanced_engagements %}{% endfor %}
EngagementTeamProgressSLAStatus
{{ row.client_name }} — {{ row.service_name }}
{{ row.financial_year }}
Staff: {{ row.staff_name }}
Manager: {{ row.manager_name }}
Partner: {{ row.partner_name }}
{{ row.progress_percent }}%
{{ row.completed_weight }}/{{ row.total_weight }} weighted
{{ row.sla.label }}
Age {{ row.sla.age_days }} day(s)
{{ row.status.replace('_',' ').title() }}
-

Team Work Board Summary

Top 100 visible tasks. Use detailed board for assignment changes.

Detailed Board
{% set rows = team_work %}{% include 'modules/manager_dashboard/templates/manager_dashboard/partials/_task_table.html' %}