Add engagement-level employee workflow board phase 1

This commit is contained in:
A R R R Associates
2026-07-19 23:36:38 +05:30
parent df52845194
commit f25f67389e
4 changed files with 317 additions and 74 deletions
+216 -51
View File
@@ -2703,113 +2703,278 @@ def list_employee_work_kanban(
status: str = "open", status: str = "open",
financial_year: str | None = None, financial_year: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return staff self-work as engagement cards in kanban columns. """Return exactly one employee-board card per assigned engagement.
Phase 7I does not introduce a new task table. It reuses existing The task engine remains unchanged. The board derives an engagement state from
client_service_task_instances and groups assigned tasks by engagement/service the staff member's assigned tasks using this precedence:
subscription so staff can open one engagement board and work through tasks.
completed -> all assigned tasks are closed
blocked -> at least one assigned task is blocked
in_progress -> at least one assigned task is started/completed
pending -> no assigned task has started
""" """
today = date.today() today = date.today()
status_filter = (status or "open").strip().lower() status_filter = (status or "open").strip().lower()
stmt = _employee_work_task_query(db, scope, assigned_only=True, financial_year=financial_year) valid_filters = {"open", "closed", "pending", "in_progress", "blocked", "completed"}
if status_filter not in valid_filters:
if status_filter == "open": status_filter = "open"
stmt = stmt.where(ClientServiceTaskInstance.status.notin_(list(CLOSED_TASK_STATUSES)))
elif status_filter == "closed":
stmt = stmt.where(ClientServiceTaskInstance.status.in_(list(CLOSED_TASK_STATUSES)))
elif status_filter in {code for code, _ in TASK_STATUSES}:
stmt = stmt.where(ClientServiceTaskInstance.status == status_filter)
stmt = _employee_work_task_query(
db,
scope,
assigned_only=True,
financial_year=financial_year,
)
if q.strip(): if q.strip():
like = f"%{q.strip()}%" like = f"%{q.strip()}%"
stmt = stmt.where( stmt = stmt.where(
or_( or_(
ClientServiceTaskInstance.task_name.ilike(like), ClientServiceTaskInstance.task_name.ilike(like),
ClientServiceTaskInstance.description.ilike(like), ClientServiceTaskInstance.description.ilike(like),
ClientServiceTaskInstance.client.has(or_(Client.client_name.ilike(like), Client.client_code.ilike(like))), ClientServiceTaskInstance.client.has(
ClientServiceTaskInstance.catalogue.has(or_(ServiceCatalogue.service_name.ilike(like), ServiceCatalogue.service_code.ilike(like))), or_(Client.client_name.ilike(like), Client.client_code.ilike(like))
),
ClientServiceTaskInstance.catalogue.has(
or_(
ServiceCatalogue.service_name.ilike(like),
ServiceCatalogue.service_code.ilike(like),
)
),
) )
) )
tasks = db.execute( tasks = db.execute(
stmt.order_by( stmt.order_by(
ClientServiceTaskInstance.subscription_id.asc(),
ClientServiceTaskInstance.sequence_no.asc(),
ClientServiceTaskInstance.internal_target_date.is_(None), ClientServiceTaskInstance.internal_target_date.is_(None),
ClientServiceTaskInstance.internal_target_date.asc(), ClientServiceTaskInstance.internal_target_date.asc(),
ClientServiceTaskInstance.priority.desc(), ClientServiceTaskInstance.id.asc(),
ClientServiceTaskInstance.sequence_no.asc(),
ClientServiceTaskInstance.id.desc(),
) )
).scalars().all() ).scalars().all()
summary = {"total": len(tasks), "open": 0, "pending": 0, "in_progress": 0, "blocked": 0, "completed": 0, "overdue": 0, "due_today": 0}
columns = [ columns = [
{"code": "pending", "label": "Pending", "cards": []}, {"code": "pending", "label": "Pending", "cards": []},
{"code": "in_progress", "label": "In Progress", "cards": []}, {"code": "in_progress", "label": "In Progress", "cards": []},
{"code": "blocked", "label": "Blocked", "cards": []}, {"code": "blocked", "label": "Blocked", "cards": []},
{"code": "completed", "label": "Completed", "cards": []}, {"code": "completed", "label": "Completed", "cards": []},
] ]
column_lookup = {c["code"]: c for c in columns} column_lookup = {column["code"]: column for column in columns}
card_lookup: dict[tuple[str, int], dict[str, Any]] = {} engagement_lookup: dict[int, dict[str, Any]] = {}
for task in tasks: for task in tasks:
_phase7i_task_card_enrich(task, today=today) _phase7i_task_card_enrich(task, today=today)
status_code = (task.status or "pending").strip().lower()
is_closed = status_code in CLOSED_TASK_STATUSES
summary["completed" if is_closed else "open"] += 1
if status_code in summary:
summary[status_code] += 1
if task.is_overdue:
summary["overdue"] += 1
if task.is_due_today:
summary["due_today"] += 1
column_code = "completed" if is_closed else status_code
if column_code not in column_lookup:
column_code = "pending"
subscription = getattr(task, "subscription", None) subscription = getattr(task, "subscription", None)
engagement_id = getattr(subscription, "id", None) or getattr(task, "subscription_id", 0) or 0 engagement_id = (
card_key = (column_code, engagement_id) getattr(subscription, "id", None)
if card_key not in card_lookup: or getattr(task, "subscription_id", 0)
or 0
)
if engagement_id not in engagement_lookup:
client = getattr(task, "client", None) client = getattr(task, "client", None)
card = { engagement_lookup[engagement_id] = {
"engagement_id": engagement_id, "engagement_id": engagement_id,
"subscription": subscription, "subscription": subscription,
"label": _subscription_label(subscription, task), "label": _subscription_label(subscription, task),
"client_name": getattr(client, "client_name", None) or "Unlinked Client", "client_name": getattr(client, "client_name", None) or "Unlinked Client",
"client_code": getattr(client, "client_code", None) or "", "client_code": getattr(client, "client_code", None) or "",
"service_name": getattr(getattr(subscription, "catalogue", None), "service_name", None) or getattr(getattr(task, "catalogue", None), "service_name", None) or "Service Engagement", "service_name": (
"financial_year": getattr(subscription, "financial_year", None) or getattr(task, "financial_year", None) or "-", getattr(getattr(subscription, "catalogue", None), "service_name", None)
"due_date": getattr(subscription, "current_due_date", None) if subscription else getattr(task, "internal_target_date", None), or getattr(getattr(task, "catalogue", None), "service_name", None)
"status": getattr(subscription, "status", None) or "active", or "Service Engagement"
),
"financial_year": (
getattr(subscription, "financial_year", None)
or getattr(task, "financial_year", None)
or "-"
),
"due_date": (
getattr(subscription, "current_due_date", None)
if subscription
else getattr(task, "internal_target_date", None)
),
"task_count": 0, "task_count": 0,
"open_count": 0, "pending_count": 0,
"completed_count": 0, "in_progress_count": 0,
"blocked_count": 0, "blocked_count": 0,
"completed_count": 0,
"overdue_count": 0, "overdue_count": 0,
"due_today_count": 0, "due_today_count": 0,
"latest_comment": None, "latest_comment": None,
"blocked_reason": None,
"blocked_task_name": None,
"next_task_name": None,
"tasks": [], "tasks": [],
} }
card_lookup[card_key] = card
column_lookup[column_code]["cards"].append(card) card = engagement_lookup[engagement_id]
card = card_lookup[card_key] task_status = (task.status or "pending").strip().lower()
is_closed = task_status in CLOSED_TASK_STATUSES
card["task_count"] += 1 card["task_count"] += 1
card["tasks"].append(task) card["tasks"].append(task)
if is_closed: if is_closed:
card["completed_count"] += 1 card["completed_count"] += 1
else: elif task_status == "blocked":
card["open_count"] += 1
if status_code == "blocked":
card["blocked_count"] += 1 card["blocked_count"] += 1
if not card["blocked_reason"]:
card["blocked_reason"] = (
(getattr(task, "remarks", None) or "").strip()
or (
getattr(task.latest_comment, "message", None)
if task.latest_comment
else None
)
or "Work is awaiting a dependency or clarification."
)
card["blocked_task_name"] = task.task_name
elif task_status == "in_progress":
card["in_progress_count"] += 1
else:
card["pending_count"] += 1
if task.is_overdue: if task.is_overdue:
card["overdue_count"] += 1 card["overdue_count"] += 1
if task.is_due_today: if task.is_due_today:
card["due_today_count"] += 1 card["due_today_count"] += 1
if task.latest_comment and not card.get("latest_comment"): if task.latest_comment and not card["latest_comment"]:
card["latest_comment"] = task.latest_comment card["latest_comment"] = task.latest_comment
if not is_closed and not card["next_task_name"]:
card["next_task_name"] = task.task_name
return {"summary": summary, "columns": columns, "q": q, "status": status_filter, "today": today} summary = {
"total": 0,
"open": 0,
"pending": 0,
"in_progress": 0,
"blocked": 0,
"completed": 0,
"overdue": 0,
"due_today": 0,
}
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
card["open_count"] = max(total - completed, 0)
if total and completed == total:
card_status = "completed"
card["action_label"] = "View"
elif card["blocked_count"]:
card_status = "blocked"
card["action_label"] = "Open / Follow Up"
elif card["in_progress_count"] or completed:
card_status = "in_progress"
card["action_label"] = "Continue"
else:
card_status = "pending"
card["action_label"] = "Start"
card["workflow_status"] = card_status
card["is_overdue"] = bool(card["overdue_count"])
card["is_due_today"] = bool(card["due_today_count"])
include = (
status_filter == card_status
or (status_filter == "open" and card_status != "completed")
or (status_filter == "closed" and card_status == "completed")
)
if not include:
continue
column_lookup[card_status]["cards"].append(card)
summary["total"] += 1
summary[card_status] += 1
summary["completed" if card_status == "completed" else "open"] += 1
if card["is_overdue"]:
summary["overdue"] += 1
if card["is_due_today"]:
summary["due_today"] += 1
for column in columns:
column["cards"].sort(
key=lambda card: (
card["due_date"] is None,
card["due_date"] or date.max,
card["client_name"].lower(),
card["service_name"].lower(),
)
)
return {
"summary": summary,
"columns": columns,
"q": q,
"status": status_filter,
"today": today,
}
def start_employee_engagement_workflow(
db: Session,
scope: EmployeeScope,
engagement_id: int,
*,
actor_user_id: int,
financial_year: str | None = None,
) -> ClientServiceTaskInstance:
"""Start the first pending task assigned to the employee in an engagement.
Existing task status, lock, tenant, branch and financial-year controls are
reused. No engagement or task records are duplicated.
"""
stmt = _employee_work_task_query(
db,
scope,
assigned_only=True,
financial_year=financial_year,
).where(ClientServiceTaskInstance.subscription_id == engagement_id)
tasks = db.execute(
stmt.order_by(
ClientServiceTaskInstance.sequence_no.asc(),
ClientServiceTaskInstance.internal_target_date.is_(None),
ClientServiceTaskInstance.internal_target_date.asc(),
ClientServiceTaskInstance.id.asc(),
)
).scalars().all()
if not tasks:
raise HTTPException(
status_code=404,
detail="Engagement work not found or not assigned to you",
)
already_started = next(
(
task
for task in tasks
if (task.status or "").strip().lower() == "in_progress"
),
None,
)
if already_started:
return already_started
first_pending = next(
(
task
for task in tasks
if (task.status or "pending").strip().lower() == "pending"
),
None,
)
if not first_pending:
return tasks[0]
return update_own_service_task_status(
db,
scope,
first_pending.id,
status="in_progress",
remarks=None,
actor_user_id=actor_user_id,
financial_year=financial_year,
)
def list_employee_engagement_documents(db: Session, scope: EmployeeScope, engagement_id: int, *, financial_year: str | None = None) -> list[EngagementDocument]: def list_employee_engagement_documents(db: Session, scope: EmployeeScope, engagement_id: int, *, financial_year: str | None = None) -> list[EngagementDocument]:
stmt = ( stmt = (
@@ -2,13 +2,11 @@
{% block content %} {% block content %}
<div class="space-y-6"> <div class="space-y-6">
{% include "modules/employees/templates/employees/_my_workspace_tabs.html" %} {% include "modules/employees/templates/employees/_my_workspace_tabs.html" %}
<div class="flex flex-wrap items-start justify-between gap-3"> <div class="flex flex-wrap items-start justify-between gap-3">
<div> <div>
<h2 class="text-xl font-semibold text-slate-900">My Work</h2> <h2 class="text-xl font-semibold text-slate-900">My Work</h2>
<p class="mt-1 text-sm text-slate-500">Board view of your assigned engagements. Open a card to work on tasks and refer to engagement documents.</p> <p class="mt-1 text-sm text-slate-500">One card per assigned engagement. Start, continue, follow up or view the full workflow from here.</p>
</div>
<div class="flex flex-wrap gap-2">
</div> </div>
</div> </div>
@@ -24,7 +22,7 @@
{% endif %} {% endif %}
<div class="grid gap-4 md:grid-cols-6"> <div class="grid gap-4 md:grid-cols-6">
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Total</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.total }}</div></div> <div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Engagements</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.total }}</div></div>
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Open</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.open }}</div></div> <div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Open</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.open }}</div></div>
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">In Progress</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.in_progress }}</div></div> <div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">In Progress</div><div class="mt-1 text-2xl font-semibold">{{ work_payload.summary.in_progress }}</div></div>
<div class="rounded-2xl border border-rose-200 bg-rose-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-rose-600">Blocked</div><div class="mt-1 text-2xl font-semibold text-rose-700">{{ work_payload.summary.blocked }}</div></div> <div class="rounded-2xl border border-rose-200 bg-rose-50 p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-rose-600">Blocked</div><div class="mt-1 text-2xl font-semibold text-rose-700">{{ work_payload.summary.blocked }}</div></div>
@@ -36,48 +34,85 @@
<div class="grid gap-3 md:grid-cols-[1fr_220px_auto]"> <div class="grid gap-3 md:grid-cols-[1fr_220px_auto]">
<input type="search" name="q" value="{{ q or '' }}" placeholder="Search engagement, task, client or service" class="rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100"> <input type="search" name="q" value="{{ q or '' }}" placeholder="Search engagement, task, client or service" class="rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
<select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100"> <select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
<option value="open" {% if status == 'open' %}selected{% endif %}>Open work</option> <option value="open" {% if status == 'open' %}selected{% endif %}>Open engagements</option>
<option value="pending" {% if status == 'pending' %}selected{% endif %}>Pending</option> <option value="pending" {% if status == 'pending' %}selected{% endif %}>Pending</option>
<option value="in_progress" {% if status == 'in_progress' %}selected{% endif %}>In Progress</option> <option value="in_progress" {% if status == 'in_progress' %}selected{% endif %}>In Progress</option>
<option value="blocked" {% if status == 'blocked' %}selected{% endif %}>Blocked</option> <option value="blocked" {% if status == 'blocked' %}selected{% endif %}>Blocked</option>
<option value="completed" {% if status == 'completed' %}selected{% endif %}>Completed</option> <option value="completed" {% if status == 'completed' %}selected{% endif %}>Completed</option>
<option value="closed" {% if status == 'closed' %}selected{% endif %}>Closed work</option> <option value="closed" {% if status == 'closed' %}selected{% endif %}>Closed engagements</option>
</select> </select>
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Apply</button> <button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Apply</button>
</div> </div>
</form> </form>
<div class="grid gap-4 xl:grid-cols-4"> <div class="space-y-5">
{% for column in work_payload.columns %} {% for column in work_payload.columns %}
<section class="min-h-[420px] rounded-2xl border border-slate-200 bg-slate-50 p-3 shadow-soft"> <section class="rounded-2xl border border-slate-200 bg-slate-50 p-4 shadow-soft">
<div class="mb-3 flex items-center justify-between px-1"> <div class="mb-4 flex items-center justify-between gap-3">
<h3 class="text-sm font-semibold text-slate-900">{{ column.label }}</h3> <div>
<span class="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-slate-600">{{ column.cards|length }}</span> <h3 class="text-base font-semibold text-slate-900">{{ column.label }}</h3>
{% if column.code == 'pending' %}<p class="mt-1 text-xs text-slate-500">Assigned engagements where work has not started.</p>{% endif %}
{% if column.code == 'in_progress' %}<p class="mt-1 text-xs text-slate-500">Started engagements with remaining assigned work.</p>{% endif %}
{% if column.code == 'blocked' %}<p class="mt-1 text-xs text-slate-500">Engagements waiting for documents, clarification, review or another dependency.</p>{% endif %}
{% if column.code == 'completed' %}<p class="mt-1 text-xs text-slate-500">All tasks assigned to you in these engagements are complete.</p>{% endif %}
</div>
<span class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-slate-600">{{ column.cards|length }}</span>
</div> </div>
<div class="space-y-3">
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{% for card in column.cards %} {% for card in column.cards %}
<article class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm hover:border-brand-200 hover:shadow-soft"> <article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm hover:border-brand-200 hover:shadow-soft">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div> <div>
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">{{ card.client_code or 'Client' }}</div> <div class="text-xs font-semibold uppercase tracking-wide text-slate-500">{{ card.client_code or 'Client' }}</div>
<h4 class="mt-1 text-sm font-semibold text-slate-900">{{ card.client_name }}</h4> <h4 class="mt-1 text-base font-semibold text-slate-900">{{ card.client_name }}</h4>
</div> </div>
{% if card.overdue_count %}<span class="rounded-full bg-red-100 px-2 py-1 text-[11px] font-semibold text-red-700">Overdue {{ card.overdue_count }}</span>{% elif card.due_today_count %}<span class="rounded-full bg-amber-100 px-2 py-1 text-[11px] font-semibold text-amber-700">Due today</span>{% endif %} {% if card.is_overdue %}<span class="rounded-full bg-red-100 px-2 py-1 text-[11px] font-semibold text-red-700">Overdue</span>{% elif card.is_due_today %}<span class="rounded-full bg-amber-100 px-2 py-1 text-[11px] font-semibold text-amber-700">Due today</span>{% endif %}
</div> </div>
<div class="mt-3 rounded-xl bg-slate-50 p-3"> <div class="mt-3 rounded-xl bg-slate-50 p-3">
<div class="text-sm font-semibold text-slate-900">{{ card.service_name }}</div> <div class="text-sm font-semibold text-slate-900">{{ card.service_name }}</div>
<div class="mt-1 text-xs text-slate-500">FY {{ card.financial_year }} · Due {{ card.due_date or '-' }}</div> <div class="mt-1 text-xs text-slate-500">FY {{ card.financial_year }} · Due {{ card.due_date or '-' }}</div>
</div> </div>
<div class="mt-3 grid grid-cols-3 gap-2 text-center text-xs">
<div class="rounded-xl border border-slate-200 p-2"><div class="font-semibold text-slate-900">{{ card.open_count }}</div><div class="text-slate-500">Open</div></div> <div class="mt-4">
<div class="rounded-xl border border-slate-200 p-2"><div class="font-semibold text-slate-900">{{ card.blocked_count }}</div><div class="text-slate-500">Blocked</div></div> <div class="flex items-center justify-between text-xs">
<div class="rounded-xl border border-slate-200 p-2"><div class="font-semibold text-slate-900">{{ card.completed_count }}</div><div class="text-slate-500">Done</div></div> <span class="font-semibold text-slate-600">My progress</span>
<span class="font-semibold text-slate-900">{{ card.progress_percent }}%</span>
</div>
<div class="mt-2 h-2 overflow-hidden rounded-full bg-slate-200">
<div class="h-full rounded-full bg-brand-600" style="width: {{ card.progress_percent }}%"></div>
</div>
<div class="mt-2 text-xs text-slate-500">{{ card.completed_count }} of {{ card.task_count }} assigned tasks completed</div>
</div> </div>
{% if column.code == 'blocked' %}
<div class="mt-4 rounded-xl border border-rose-200 bg-rose-50 p-3">
<div class="text-xs font-semibold uppercase text-rose-700">Pending reason</div>
<div class="mt-1 text-sm font-medium text-rose-900">{{ card.blocked_reason }}</div>
{% if card.blocked_task_name %}<div class="mt-1 text-xs text-rose-700">Task: {{ card.blocked_task_name }}</div>{% endif %}
</div>
{% elif card.next_task_name and column.code != 'completed' %}
<div class="mt-4 text-xs text-slate-500">Next: <span class="font-medium text-slate-700">{{ card.next_task_name }}</span></div>
{% endif %}
{% if card.latest_comment %}<p class="mt-3 truncate text-xs text-slate-500">Latest: {{ card.latest_comment.message }}</p>{% endif %} {% if card.latest_comment %}<p class="mt-3 truncate text-xs text-slate-500">Latest: {{ card.latest_comment.message }}</p>{% endif %}
<a href="/work/engagements/{{ card.engagement_id }}" class="mt-4 inline-flex w-full justify-center rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Open Work Details</a>
{% if column.code == 'pending' %}
<form method="post" action="/employee/work/engagements/{{ card.engagement_id }}/start" class="mt-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="inline-flex w-full justify-center rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Start</button>
</form>
{% elif column.code == 'in_progress' %}
<a href="/employee/work/engagements/{{ card.engagement_id }}" class="mt-4 inline-flex w-full justify-center rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Continue</a>
{% elif column.code == 'blocked' %}
<a href="/employee/work/engagements/{{ card.engagement_id }}" class="mt-4 inline-flex w-full justify-center rounded-xl bg-rose-600 px-4 py-2 text-sm font-semibold text-white hover:bg-rose-700">Open / Follow Up</a>
{% else %}
<a href="/employee/work/engagements/{{ card.engagement_id }}" class="mt-4 inline-flex w-full justify-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">View</a>
{% endif %}
</article> </article>
{% else %} {% else %}
<div class="rounded-2xl border border-dashed border-slate-300 bg-white p-6 text-center text-sm text-slate-500">No cards in this column.</div> <div class="md:col-span-2 xl:col-span-3 rounded-2xl border border-dashed border-slate-300 bg-white p-6 text-center text-sm text-slate-500">No engagement cards in this section.</div>
{% endfor %} {% endfor %}
</div> </div>
</section> </section>
@@ -11,7 +11,7 @@
</p> </p>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<a href="/employee/work" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">My Work Board</a> <a href="/employee/work" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Close Workspace</a>
<a href="/documents/engagements/{{ board.engagement_id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Full Documents</a> <a href="/documents/engagements/{{ board.engagement_id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Full Documents</a>
</div> </div>
</div> </div>
+43
View File
@@ -47,6 +47,7 @@ from app.modules.employees.service import (
list_employee_work_dashboard, list_employee_work_dashboard,
list_employee_work_kanban, list_employee_work_kanban,
get_employee_engagement_work_board, get_employee_engagement_work_board,
start_employee_engagement_workflow,
list_employee_work_assignable_users, list_employee_work_assignable_users,
list_visible_work_assignment_dashboard, list_visible_work_assignment_dashboard,
list_engagement_progress_dashboard, list_engagement_progress_dashboard,
@@ -2047,6 +2048,48 @@ def employee_my_work(request: Request, q: str = "", status: str = "open"):
db.close() db.close()
@portal_router.post("/work/engagements/{engagement_id}/start")
def employee_my_work_engagement_start(
request: Request,
engagement_id: int,
csrf_token: str = Form(...),
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.view_self")
except Exception:
return _redirect_denied()
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(
db,
current_user,
tenant_id=tenant_id,
branch_id=branch_id,
)
start_employee_engagement_workflow(
db,
scope,
engagement_id,
actor_user_id=current_user.id,
financial_year=_active_financial_year(request),
)
return RedirectResponse(
url=f"/employee/work/engagements/{engagement_id}",
status_code=303,
)
finally:
db.close()
@portal_router.get("/work/engagements/{engagement_id}") @portal_router.get("/work/engagements/{engagement_id}")
def employee_my_work_engagement_board(request: Request, engagement_id: int): def employee_my_work_engagement_board(request: Request, engagement_id: int):
db = CommonSessionLocal() db = CommonSessionLocal()