HH/apps/complaints/workflow_log.py

131 lines
5.8 KiB
Python

"""
Workflow timeline builder for the complaint department-response flow.
Assembles a chronological list of events from ComplaintInvolvedDepartment,
ComplaintExplanation, ChampionInvestigation, InvestigationResponse,
InvestigationAnswer, and ComplaintUpdate — so the template can render
a visual timeline of the entire workflow.
"""
from datetime import datetime
def build_workflow_log(complaint):
"""Return a chronologically sorted list of workflow events for a complaint."""
events = []
# 1. Sent to department(s)
for idept in complaint.involved_departments.select_related("department", "added_by").all():
ts = idept.forwarded_at or idept.sent_at
if ts:
events.append({
"type": "sent_to_dept",
"timestamp": ts,
"actor": idept.added_by.get_full_name() if idept.added_by else "PX Team",
"department": idept.department.get_localized_name() if hasattr(idept.department, 'get_localized_name') else (idept.department.name if idept.department else ""),
"is_primary": idept.is_primary,
})
# 2. Explanations (direct replies + investigations)
for exp in complaint.explanations.select_related("staff", "staff__department", "requested_by").all():
champion_name = exp.staff.get_full_name() if exp.staff else "Department"
# Email sent timestamp
if exp.email_sent_at:
note = getattr(exp, "request_message", "") or ""
events.append({
"type": "explanation_sent",
"timestamp": exp.email_sent_at,
"actor": "PX Team",
"recipient": champion_name,
"note": note,
"attachment_count": exp.attachments.count(),
})
# Investigations linked to this explanation
investigations = list(
exp.investigation.select_related("champion", "champion__department").all()
)
if investigations:
for inv in investigations:
inv_ts = inv.created_at
events.append({
"type": "investigation_started",
"timestamp": inv_ts,
"actor": inv.champion.get_full_name() if inv.champion else champion_name,
"status": inv.status,
})
# Per-staff: questions sent + responses
for resp in inv.responses.select_related("staff", "staff__department").all():
staff_name = resp.staff.get_full_name() if resp.staff else "Unknown"
# Questions sent to this staff
q_sent_ts = resp.email_sent_at or inv_ts
questions = list(resp.questions.order_by("order").all())
events.append({
"type": "questions_sent",
"timestamp": q_sent_ts,
"actor": inv.champion.get_full_name() if inv.champion else champion_name,
"staff": staff_name,
"question_count": len(questions),
"questions": [
{"text": q.question_text, "type": q.question_type}
for q in questions
],
})
# Staff responded
if resp.is_completed and resp.completed_at:
qa_pairs = []
for ans in resp.answers.select_related("question").all():
qa_pairs.append({
"question": ans.question.question_text,
"question_type": ans.question.question_type,
"answer": ans.answer_text,
})
events.append({
"type": "staff_responded",
"timestamp": resp.completed_at,
"actor": staff_name,
"investigator": inv.champion.get_full_name() if inv.champion else "Department Champion",
"qa_pairs": qa_pairs,
})
else:
events.append({
"type": "staff_pending",
"timestamp": q_sent_ts,
"actor": staff_name,
"question_count": len(questions),
})
# Final reply submitted
if inv.status == "reply_submitted" and exp.responded_at:
events.append({
"type": "reply_submitted",
"timestamp": exp.responded_at,
"actor": inv.champion.get_full_name() if inv.champion else champion_name,
"final_reply": inv.final_reply,
"negligence": inv.negligence_finding,
"policy_issue": inv.policy_issue_finding,
"improvement_project": inv.requires_improvement_project,
"improvement_note": inv.improvement_project_note,
"attachment_count": exp.attachments.count(),
})
else:
# Direct reply (no investigation)
if exp.is_used and exp.responded_at:
events.append({
"type": "direct_reply",
"timestamp": exp.responded_at,
"actor": champion_name,
"reply": exp.explanation,
"attachment_count": exp.attachments.count(),
})
# Sort by timestamp (None timestamps go last)
events.sort(key=lambda e: e.get("timestamp") or datetime.max)
return events