Add phase 3 consultant task execution workflow
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
@@ -171,3 +171,55 @@ def get_consultant_document_centre(db: Session, *, consultant: ConsultantProfile
|
||||
term=f"%{q.strip()}%"; stmt=stmt.where(or_(PermanentClientDocument.title.ilike(term),PermanentClientDocument.category.ilike(term),PermanentClientDocument.document_code.ilike(term)))
|
||||
permanent_documents=db.execute(stmt.order_by(PermanentClientDocument.created_at_utc.desc()).limit(200)).scalars().all()
|
||||
return {"engagement_documents":engagement_documents,"permanent_documents":permanent_documents,"total":len(engagement_documents)+len(permanent_documents)}
|
||||
|
||||
|
||||
def consultant_can_execute_task(*, consultant: ConsultantProfile, task: ClientServiceTaskInstance) -> bool:
|
||||
return bool(task.execution_mode == "consultant" and task.assigned_consultant_id == consultant.id and task.is_active and not task.is_locked)
|
||||
|
||||
|
||||
def update_consultant_assignment(db: Session, *, consultant: ConsultantProfile, task: ClientServiceTaskInstance, action: str, note: str, user_id: int) -> tuple[bool, str | None]:
|
||||
if not consultant_can_execute_task(consultant=consultant, task=task):
|
||||
return False, "This task is not assigned to your consultant profile."
|
||||
action = (action or "").strip().lower()
|
||||
note = (note or "").strip()
|
||||
current = (task.consultant_assignment_status or "not_applicable").lower()
|
||||
now = datetime.now(timezone.utc)
|
||||
message = None
|
||||
if action == "accept" and current == "offered":
|
||||
task.consultant_assignment_status = "accepted"
|
||||
task.consultant_accepted_at_utc = now
|
||||
task.consultant_declined_at_utc = None
|
||||
task.consultant_decline_reason = None
|
||||
task.status = "pending"
|
||||
message = "Consultant accepted the assignment."
|
||||
elif action == "decline" and current == "offered":
|
||||
if not note:
|
||||
return False, "Decline reason is required."
|
||||
task.consultant_assignment_status = "declined"
|
||||
task.consultant_declined_at_utc = now
|
||||
task.consultant_decline_reason = note
|
||||
task.status = "pending"
|
||||
message = f"Consultant declined the assignment: {note}"
|
||||
elif action == "start" and current in {"accepted", "rework_required"}:
|
||||
task.consultant_assignment_status = "in_progress"
|
||||
task.consultant_started_at_utc = task.consultant_started_at_utc or now
|
||||
task.status = "in_progress"
|
||||
if current == "rework_required":
|
||||
task.rework_status = "in_progress"
|
||||
message = "Consultant started work on the assignment."
|
||||
elif action == "submit" and current in {"accepted", "in_progress", "rework_required"}:
|
||||
if not note:
|
||||
return False, "Submission note is required."
|
||||
task.consultant_assignment_status = "submitted"
|
||||
task.consultant_submitted_at_utc = now
|
||||
task.consultant_submission_note = note
|
||||
task.status = "pending_review"
|
||||
if task.rework_status in {"requested", "in_progress"}:
|
||||
task.rework_status = "resolved"
|
||||
task.rework_resolved_at_utc = now
|
||||
message = f"Consultant submitted the assignment for firm review: {note}"
|
||||
else:
|
||||
return False, "This action is not allowed for the current assignment status."
|
||||
task.updated_by_user_id = user_id
|
||||
db.add(ServiceTaskComment(tenant_id=task.tenant_id, branch_id=task.branch_id, subscription_id=task.subscription_id, task_instance_id=task.id, comment_type="consultant_execution", visibility="consultant", message=message, created_by_user_id=user_id))
|
||||
return True, None
|
||||
|
||||
@@ -22,6 +22,24 @@
|
||||
{% if task.description %}<div class="mt-5 rounded-xl bg-slate-50 p-4 text-sm text-slate-700 whitespace-pre-line">{{ task.description }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
{% if task.execution_mode == 'consultant' and task.assigned_consultant_id == consultant.id %}
|
||||
<div class="rounded-2xl border border-indigo-200 bg-indigo-50 p-5 shadow-soft">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div><h3 class="font-semibold text-slate-900">Consultant Assignment Execution</h3><p class="mt-1 text-sm text-slate-600">Status: <span class="font-semibold">{{ task.consultant_assignment_status.replace('_',' ').title() }}</span>{% if task.consultant_due_date %} • Due {{ task.consultant_due_date.strftime('%d-%m-%Y') }}{% endif %}</p></div>
|
||||
</div>
|
||||
{% if task.rework_reason %}<div class="mt-3 rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800"><strong>Rework:</strong> {{ task.rework_reason }}</div>{% endif %}
|
||||
<form method="post" action="/consultant/assignments/{{ task.id }}/execution" class="mt-4 space-y-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<textarea name="note" rows="3" placeholder="Reason for decline or submission note" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm"></textarea>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if task.consultant_assignment_status == 'offered' %}<button name="action" value="accept" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white">Accept Assignment</button><button name="action" value="decline" class="rounded-xl border border-red-300 bg-white px-4 py-2 text-sm font-semibold text-red-700">Decline</button>{% endif %}
|
||||
{% if task.consultant_assignment_status in ['accepted','rework_required'] %}<button name="action" value="start" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Start Work</button>{% endif %}
|
||||
{% if task.consultant_assignment_status in ['accepted','in_progress','rework_required'] %}<button name="action" value="submit" class="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white">Submit for Firm Review</button>{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||||
<div class="border-b border-slate-200 px-5 py-4"><h3 class="font-semibold text-slate-900">Consultant Communication Timeline</h3></div>
|
||||
<div class="divide-y divide-slate-100">
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<div class="mt-3 text-sm font-medium text-slate-800">{{ task.task_name }}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1 text-[11px] font-semibold">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">{{ task.status.replace('_',' ').title() }}</span>
|
||||
{% if task.execution_mode == 'consultant' and task.assigned_consultant_id == consultant.id %}<span class="rounded-full bg-indigo-50 px-2 py-1 text-indigo-700">Execution: {{ task.consultant_assignment_status.replace('_',' ').title() }}</span>{% endif %}
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">{{ task.priority.replace('_',' ').title() }}</span>
|
||||
{% if task.internal_target_date %}<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">Due {{ task.internal_target_date.strftime('%d-%m-%Y') }}</span>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@ from app.modules.consultants.service import (
|
||||
)
|
||||
from app.modules.consultants.portal_service import (
|
||||
get_consultant_assignment_detail,
|
||||
update_consultant_assignment,
|
||||
get_consultant_document_centre,
|
||||
get_consultant_work_board,
|
||||
)
|
||||
@@ -1837,3 +1838,32 @@ def consultant_documents_page(request: Request, q: str = ""):
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@portal_router.post("/assignments/{task_id}/execution")
|
||||
async def consultant_assignment_execution(request: Request, task_id: int, csrf_token: str = Form(...)):
|
||||
validate_csrf(request, csrf_token)
|
||||
form = await request.form()
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
if not (_has_perm(db, user, "consultants.portal.view") or _is_consultant(db, user)):
|
||||
return _redirect_denied()
|
||||
consultant = _get_own_consultant_or_dashboard(request, db, user)
|
||||
if not consultant:
|
||||
return RedirectResponse(url="/consultant/dashboard", status_code=303)
|
||||
detail = get_consultant_assignment_detail(db, consultant=consultant, task_id=task_id)
|
||||
if not detail:
|
||||
return RedirectResponse(url="/consultant/work", status_code=303)
|
||||
ok, error = update_consultant_assignment(db, consultant=consultant, task=detail["task"], action=str(form.get("action") or ""), note=str(form.get("note") or ""), user_id=user.id)
|
||||
if not ok:
|
||||
refreshed = get_consultant_assignment_detail(db, consultant=consultant, task_id=task_id) or detail
|
||||
return _render(request, "modules/consultants/templates/consultants/assignment_detail.html", db, user, title="Consultant Work Detail", consultant=consultant, **refreshed, errors=[error or "Action could not be completed."])
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/consultant/assignments/{task_id}?execution=updated", status_code=303)
|
||||
except Exception:
|
||||
db.rollback(); raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
Reference in New Issue
Block a user