""" 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