131 lines
5.8 KiB
Python
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
|