Staff Response & Conclusion
- Response
- {{ active.checklist_response or active.checklist_text_response or active.checklist_number_response or active.checklist_date_response or '-' }}
- Checklist remarks
- {{ active.checklist_remarks or '-' }}
- Work remarks
- {{ active.work_remarks or '-' }}
@@ -76,6 +78,43 @@
Communication Timeline
{% for comment in active.comments %}
{{ comment.comment_type.replace('_',' ').title() }} ยท {{ comment.created_at_utc }}
{{ comment.message }}
{% else %}
No communication recorded.
{% endfor %}
+ {% if active.can_execute and not active.is_locked and not workspace.is_locked %}
+
+ {% endif %}
+
{% if active.can_review and not active.is_locked and not workspace.is_locked %}
{% else %}
-
{% if workspace.is_locked or active.is_locked %}This engagement or task is locked and cannot be reviewed.{% elif active.review_state == 'not_ready' %}This task is visible for context but is not ready for your review. Required execution or prior review is pending.{% else %}This task is read-only for your current engagement role.{% endif %}
+
{% if workspace.is_locked or active.is_locked %}This engagement or task is locked and cannot be reviewed.{% elif active.can_execute %}This task is assigned to you for execution. Use the Partner Task Execution section above.{% elif active.review_state == 'not_ready' %}This task is visible for context but is not ready for your review. Required execution or prior review is pending.{% else %}This task is read-only for your current engagement role.{% endif %}
{% endif %}
{% else %}
No tasks are available in this engagement.
{% endif %}
diff --git a/app/modules/partner_dashboard/ui.py b/app/modules/partner_dashboard/ui.py
index 219d2f6..8ba93dd 100644
--- a/app/modules/partner_dashboard/ui.py
+++ b/app/modules/partner_dashboard/ui.py
@@ -16,6 +16,7 @@ from app.modules.partner_dashboard.service import (
get_partner_review_workspace,
)
from app.modules.services.execution import apply_normal_task_review, apply_task_review
+from app.modules.employees.service import build_employee_scope, save_employee_workflow_task
from app.modules.alerts.workflow_escalations import update_workflow_escalation
router = APIRouter(prefix="/partner", tags=["partner-dashboard-v2-ui"])
@@ -111,6 +112,85 @@ def partner_review_workspace(request: Request, subscription_id: int, task_id: in
db.close()
+@router.post("/reviews/engagements/{subscription_id}/tasks/{task_id}/execute")
+def partner_execute_assigned_task(
+ request: Request,
+ subscription_id: int,
+ task_id: int,
+ status: str = Form("in_progress"),
+ remarks: str = Form(""),
+ checklist_response: str = Form(""),
+ checklist_text_response: str = Form(""),
+ checklist_number_response: str = Form(""),
+ checklist_date_response: str = Form(""),
+ checklist_remarks: str = Form(""),
+ csrf_token: str = Form(...),
+):
+ """Execute a task from Partner Operations when the Partner is its actual assignee.
+
+ This deliberately reuses the existing employee task-save service so checklist
+ validation, evidence/approval gates, AQMM recalculation, review submission, locks
+ and audit timestamps remain identical to the existing execution workflow.
+ """
+ validate_csrf(request, csrf_token)
+ db = CommonSessionLocal()
+ try:
+ current_user = get_current_user(request, db=db)
+ if not current_user:
+ return RedirectResponse(url="/login", status_code=303)
+ if not can_access_partner_dashboard(db, current_user):
+ return ui_access_denied()
+
+ workspace = get_partner_review_workspace(
+ db, request, current_user, subscription_id=subscription_id, active_task_id=task_id
+ )
+ if not workspace:
+ return RedirectResponse(url="/partner/dashboard?tab=review", status_code=303)
+ active = workspace.get("active_task") or {}
+ if int(active.get("id") or 0) != int(task_id) or not active.get("can_execute"):
+ return RedirectResponse(
+ url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&execution_error=not_assigned",
+ status_code=303,
+ )
+ if active.get("is_locked") or workspace.get("is_locked"):
+ return RedirectResponse(
+ url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&execution_error=locked",
+ status_code=303,
+ )
+
+ 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)
+ try:
+ saved_task, _next_task = save_employee_workflow_task(
+ db,
+ scope,
+ task_id,
+ status=status,
+ remarks=remarks,
+ checklist_response=checklist_response,
+ checklist_text_response=checklist_text_response,
+ checklist_number_response=checklist_number_response,
+ checklist_date_response=checklist_date_response,
+ checklist_remarks=checklist_remarks,
+ actor_user_id=current_user.id,
+ financial_year=None,
+ )
+ except ValueError as exc:
+ db.rollback()
+ return RedirectResponse(
+ url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&execution_error=validation",
+ status_code=303,
+ )
+
+ return RedirectResponse(
+ url=f"/partner/reviews/engagements/{subscription_id}?task_id={saved_task.id}&executed=1",
+ status_code=303,
+ )
+ finally:
+ db.close()
+
+
@router.post("/reviews/engagements/{subscription_id}/tasks/{task_id}")
def partner_review_task_submit(
request: Request,