HH/docs/complaint_workflow.html
2026-07-11 19:24:28 +03:00

599 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Complaint Workflow — PX360</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--navy: #0a1e3f;
--blue: #007bbd;
--light: #e8f1f8;
--slate: #64748b;
--green: #16a34a;
--red: #dc2626;
--amber: #d97706;
--purple: #7c3aed;
--teal: #0d9488;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
color: #1e293b;
line-height: 1.6;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, var(--navy), var(--blue));
color: white;
padding: 3rem 2rem;
text-align: center;
}
.header h1 { font-size: 2rem; font-weight: 800; margin-bottom: 0.5rem; }
.header p { opacity: 0.85; font-size: 0.95rem; }
.container { max-width: 1100px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.section-card {
background: white;
border-radius: 1rem;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
}
.section-card h2 {
font-size: 1.35rem;
font-weight: 700;
color: var(--navy);
margin-bottom: 0.25rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-card .subtitle {
color: var(--slate);
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.section-card h3 {
font-size: 1rem;
font-weight: 600;
color: var(--blue);
margin: 1.5rem 0 0.75rem;
}
.mermaid {
display: flex;
justify-content: center;
padding: 1rem 0;
}
.mermaid-wrapper {
background: #fafbfc;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
padding: 1.5rem;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
th {
text-align: left;
padding: 0.6rem 0.75rem;
background: var(--light);
color: var(--navy);
font-weight: 600;
border-bottom: 2px solid var(--blue);
}
td {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid #e2e8f0;
vertical-align: top;
}
tr:hover td { background: #f8fafc; }
code {
background: #f1f5f9;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.8125rem;
color: var(--blue);
}
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-green { background: #dcfce7; color: #15803d; }
.badge-red { background: #fee2e2; color: #b91c1c; }
.badge-amber { background: #fef3c7; color: #b45309; }
.badge-blue { background: #dbeafe; color: #1e40af; }
.stepper {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0;
margin: 1.5rem 0;
position: relative;
}
.stepper::before {
content: '';
position: absolute;
top: 22px;
left: 5%;
right: 5%;
height: 3px;
background: #e2e8f0;
z-index: 0;
}
.step {
flex: 1;
text-align: center;
position: relative;
z-index: 1;
}
.step-circle {
width: 44px;
height: 44px;
border-radius: 50%;
background: white;
border: 3px solid var(--blue);
color: var(--blue);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 0.5rem;
font-weight: 700;
font-size: 1rem;
}
.step-label { font-size: 0.8125rem; font-weight: 600; color: var(--navy); }
.step-flag { font-size: 0.6875rem; color: var(--slate); margin-top: 0.125rem; }
.callout {
border-left: 4px solid;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
margin: 1rem 0;
font-size: 0.875rem;
}
.callout-warn { border-color: var(--amber); background: #fffbeb; color: #92400e; }
.callout-info { border-color: var(--blue); background: #eff6ff; color: #1e40af; }
.sub-workflow-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.sw-card {
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
padding: 1.25rem;
background: #fafbfc;
}
.sw-card h4 {
font-size: 0.9375rem;
font-weight: 700;
color: var(--navy);
margin-bottom: 0.25rem;
}
.sw-card .field { font-size: 0.75rem; color: var(--slate); margin-bottom: 0.5rem; }
.sw-states {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.sw-state {
background: white;
border: 1px solid #e2e8f0;
padding: 0.1875rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.6875rem;
color: #475569;
}
.sw-arrow { color: var(--slate); font-size: 0.6875rem; }
.legend {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-top: 1rem;
font-size: 0.8125rem;
}
.legend-item { display: flex; align-items: center; gap: 0.375rem; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; }
.tab-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 2px solid #e2e8f0;
overflow-x: auto;
}
.tab-btn {
padding: 0.75rem 1rem;
border: none;
background: none;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
color: var(--slate);
border-bottom: 2px solid transparent;
margin-bottom: -2px;
white-space: nowrap;
transition: all 0.2s;
}
.tab-btn.active { color: var(--blue); border-bottom-color: var(--blue); }
.tab-btn:hover { color: var(--navy); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.footer {
text-align: center;
padding: 2rem;
color: var(--slate);
font-size: 0.8125rem;
}
</style>
</head>
<body>
<div class="header">
<h1>Complaint Workflow Map</h1>
<p>Status state machine, stepper, entry points &amp; sub-workflows — auto-extracted from source</p>
</div>
<div class="container">
<div class="tab-bar">
<button class="tab-btn active" onclick="switchTab(event, 'overview')">Overview</button>
<button class="tab-btn" onclick="switchTab(event, 'stepper')">Stepper</button>
<button class="tab-btn" onclick="switchTab(event, 'entrypoints')">Entry Points</button>
<button class="tab-btn" onclick="switchTab(event, 'subworkflows')">Sub-Workflows</button>
</div>
<!-- TAB 1: OVERVIEW -->
<div id="overview" class="tab-panel active">
<div class="section-card">
<h2>1. Status State Machine</h2>
<p class="subtitle">8 statuses defined in <code>ComplaintStatus</code> (<code>models.py:25</code>). Transition map enforced at <code>complaint_service.py:284</code> — only via <code>ComplaintService.change_status</code>. PX Admins bypass.</p>
<div class="mermaid-wrapper">
<pre class="mermaid">
stateDiagram-v2
[*] --> open
open --> in_progress : activate
open --> cancelled
in_progress --> partially_resolved
in_progress --> resolved
in_progress --> cancelled
in_progress --> pending_external
in_progress --> ovr_pending : escalate
partially_resolved --> resolved
partially_resolved --> in_progress
partially_resolved --> cancelled
partially_resolved --> pending_external
resolved --> closed
resolved --> in_progress : reopen
closed --> in_progress : reopen
cancelled --> open
cancelled --> in_progress
pending_external --> resolved
pending_external --> in_progress
pending_external --> cancelled
pending_external --> closed
ovr_pending --> in_progress : approve / reject
ovr_pending --> resolved
ovr_pending --> cancelled
closed --> [*]
</pre>
</div>
<h3>Transition Table</h3>
<table>
<thead>
<tr><th>From</th><th>Allowed Next Statuses</th></tr>
</thead>
<tbody>
<tr><td><code>open</code></td><td>in_progress, cancelled</td></tr>
<tr><td><code>in_progress</code></td><td>partially_resolved, resolved, cancelled, pending_external, ovr_pending</td></tr>
<tr><td><code>partially_resolved</code></td><td>resolved, in_progress, cancelled, pending_external</td></tr>
<tr><td><code>resolved</code></td><td>closed, in_progress</td></tr>
<tr><td><code>closed</code></td><td>in_progress</td></tr>
<tr><td><code>cancelled</code></td><td>open, in_progress</td></tr>
<tr><td><code>pending_external</code></td><td>resolved, in_progress, cancelled, closed</td></tr>
<tr><td><code>ovr_pending</code></td><td>in_progress, resolved, cancelled</td></tr>
</tbody>
</table>
<h3>Active vs Inactive (<code>is_active_status</code>)</h3>
<table>
<thead>
<tr><th>Active</th><th>Inactive</th></tr>
</thead>
<tbody>
<tr>
<td>
<span class="badge badge-green">open</span>
<span class="badge badge-green">in_progress</span>
<span class="badge badge-green">partially_resolved</span>
<span class="badge badge-green">pending_external</span>
</td>
<td>
<span class="badge badge-red">resolved</span>
<span class="badge badge-red">closed</span>
<span class="badge badge-red">cancelled</span>
<span class="badge badge-amber">ovr_pending ⚠️</span>
</td>
</tr>
</tbody>
</table>
<div class="callout callout-warn">
<strong>Inconsistency:</strong> <code>ovr_pending</code> is treated as <em>inactive</em> by <code>is_active_status</code> even though it's a mid-lifecycle state. Service methods guarding on active status would reject operations on OVR-pending complaints.
</div>
</div>
</div>
<!-- TAB 2: STEPPER -->
<div id="stepper" class="tab-panel">
<div class="section-card">
<h2>2. Detail Page Stepper</h2>
<p class="subtitle">6 ordered progress flags rendered inline in <code>complaint_detail.html:147</code>. Driven by a boolean dict built at <code>ui_views.py:745</code>. Hidden when status is <code>cancelled</code>.</p>
<div class="stepper">
<div class="step">
<div class="step-circle">1</div>
<div class="step-label">Created</div>
<div class="step-flag">always ✓</div>
</div>
<div class="step">
<div class="step-circle">2</div>
<div class="step-label">Activate</div>
<div class="step-flag">activated_at set</div>
</div>
<div class="step">
<div class="step-circle">3</div>
<div class="step-label">Taxonomy</div>
<div class="step-flag">taxonomy_reviewed_at</div>
</div>
<div class="step">
<div class="step-circle">4</div>
<div class="step-label">Send to Dept</div>
<div class="step-flag">sent_to_department</div>
</div>
<div class="step">
<div class="step-circle">5</div>
<div class="step-label">Response</div>
<div class="step-flag">dept responded</div>
</div>
<div class="step">
<div class="step-circle">6</div>
<div class="step-label">Resolve</div>
<div class="step-flag">resolved / closed</div>
</div>
</div>
<h3>Step Details</h3>
<table>
<thead>
<tr><th>#</th><th>Step</th><th>Flag</th><th>Icon</th><th>Action when not done</th></tr>
</thead>
<tbody>
<tr><td>1</td><td><strong>Created</strong></td><td>always green</td><td><code>check</code></td><td></td></tr>
<tr><td>2</td><td><strong>Activate</strong></td><td><code>activated_at</code></td><td><code>play</code></td><td>"Activate this complaint" → <code>complaint_activate</code></td></tr>
<tr><td>3</td><td><strong>Taxonomy</strong></td><td><code>taxonomy_reviewed_at</code></td><td><code>clipboard-check</code></td><td>"Review taxonomy" → <code>confirm_taxonomy</code></td></tr>
<tr><td>4</td><td><strong>Send to Dept</strong></td><td><code>sent_to_department</code> / <code>forwarded_to_dept_at</code></td><td><code>send</code></td><td>"Send to department" modal</td></tr>
<tr><td>5</td><td><strong>Response</strong></td><td>explanation used OR dept responded</td><td><code>message-square</code></td><td>"Awaiting department response"</td></tr>
<tr><td>6</td><td><strong>Resolve</strong></td><td><code>status in (resolved, closed)</code></td><td><code>check-circle-2</code></td><td>"Generate Resolution"</td></tr>
</tbody>
</table>
<div class="callout callout-info">
The stepper is <strong>flag-driven, not status-driven</strong>. E.g. "resolved" lights up green whenever <code>status ∈ (resolved, closed)</code>, regardless of path taken.
</div>
</div>
</div>
<!-- TAB 3: ENTRY POINTS -->
<div id="entrypoints" class="tab-panel">
<div class="section-card">
<h2>3. Status-Mutating Entry Points</h2>
<p class="subtitle">Code paths that modify <code>complaint.status</code>. Only <code>ComplaintService.change_status</code> enforces the transition map — the rest set status directly.</p>
<table>
<thead>
<tr><th>Action</th><th>Location</th><th>Transition enforced?</th></tr>
</thead>
<tbody>
<tr><td><strong>Generic change status</strong></td><td><code>ui_views.py:1365</code></td><td><span class="badge badge-green">Yes</span></td></tr>
<tr><td>Activate (open→in_progress)</td><td><code>ui_views.py:2125</code></td><td><span class="badge badge-green">service guard</span></td></tr>
<tr><td>Assign / reopen-on-assign</td><td><code>ui_views.py:851</code></td><td><span class="badge badge-green">service guard</span></td></tr>
<tr><td>Reopen as new complaint</td><td><code>ui_views.py:1777</code></td><td><span class="badge badge-green">service guard</span></td></tr>
<tr><td>OVR toggle</td><td><code>ui_views.py:1549</code></td><td><span class="badge badge-red">direct</span></td></tr>
<tr><td>OVR approve</td><td><code>ui_views.py:1640</code></td><td><span class="badge badge-red">direct</span></td></tr>
<tr><td>OVR reject</td><td><code>ui_views.py:1755</code></td><td><span class="badge badge-red">direct</span></td></tr>
<tr><td>API auto-resolve</td><td><code>views.py:2040</code></td><td><span class="badge badge-red">direct</span></td></tr>
<tr><td>Convert to appreciation</td><td><code>views.py:2198</code></td><td><span class="badge badge-red">direct</span></td></tr>
<tr><td>Meeting auto-resolve</td><td><code>views.py:3279</code></td><td><span class="badge badge-red">direct</span></td></tr>
<tr><td>Bulk status change</td><td><code>utils.py:263</code></td><td><span class="badge badge-red">direct</span></td></tr>
<tr><td><code>close_stale_complaints</code> cmd</td><td><code>close_stale_complaints.py:113</code></td><td><span class="badge badge-red">direct (bulk UPDATE)</span></td></tr>
</tbody>
</table>
<div class="callout callout-warn">
<strong>7 code paths bypass the transition map.</strong> This means invalid transitions can occur via OVR views, bulk operations, API auto-resolve, convert-to-appreciation, meeting-resolve, and the stale-close command.
</div>
</div>
</div>
<!-- TAB 4: SUB-WORKFLOWS -->
<div id="subworkflows" class="tab-panel">
<div class="section-card">
<h2>4. Sub-Workflows</h2>
<p class="subtitle">Independent state machines attached to a complaint, each with its own field/model.</p>
<div class="mermaid-wrapper" style="margin-bottom: 1.5rem;">
<pre class="mermaid">
graph TD
C[COMPLAINT<br/>status: 8 states]
C --> DR["Dept Routing<br/>routing_status"]
C --> CI["Champion Investigation<br/>status"]
C --> MR["Manager Review<br/>manager_review_status"]
C --> PC["Patient Contact<br/>patient_contact_status"]
C --> SA["Satisfaction<br/>satisfaction"]
C --> AA["Adverse Action<br/>verification_status"]
DR --> |sent| DR2["accepted / rejected"]
CI --> |questions_sent| CI2["answers_received"]
CI2 --> CI3["reply_submitted / direct_reply_in_progress"]
MR --> |pending| MR2["approved / rejected"]
PC --> |not_contacted| PC2["contacted / no_response"]
SA --> SA2["satisfied / neutral / dissatisfied / no_response"]
AA --> |reported| AA2["under_investigation"]
AA2 --> AA3["verified / unfounded / resolved"]
style C fill:#0a1e3f,color:#fff,stroke:none
style DR fill:#dbeafe,stroke:#007bbd
style CI fill:#dcfce7,stroke:#16a34a
style MR fill:#fef3c7,stroke:#d97706
style PC fill:#fce7f3,stroke:#db2777
style SA fill:#ede9fe,stroke:#7c3aed
style AA fill:#fee2e2,stroke:#dc2626
</pre>
</div>
<div class="sub-workflow-grid">
<div class="sw-card">
<h4>Department Routing</h4>
<div class="field"><code>ComplaintInvolvedDepartment.routing_status</code></div>
<div class="sw-states">
<span class="sw-state">sent</span>
<span class="sw-arrow"></span>
<span class="sw-state">accepted</span>
<span class="sw-state">rejected</span>
</div>
</div>
<div class="sw-card">
<h4>Champion Investigation</h4>
<div class="field"><code>ChampionInvestigation.status</code></div>
<div class="sw-states">
<span class="sw-state">questions_sent</span>
<span class="sw-arrow"></span>
<span class="sw-state">answers_received</span>
<span class="sw-arrow"></span>
<span class="sw-state">reply_submitted</span>
<span class="sw-state">direct_reply_in_progress</span>
</div>
</div>
<div class="sw-card">
<h4>Manager Review</h4>
<div class="field"><code>ComplaintInvolvedDepartment.manager_review_status</code></div>
<div class="sw-states">
<span class="sw-state">pending</span>
<span class="sw-arrow"></span>
<span class="sw-state">approved</span>
<span class="sw-state">rejected</span>
</div>
</div>
<div class="sw-card">
<h4>Patient Contact</h4>
<div class="field"><code>Complaint.patient_contact_status</code></div>
<div class="sw-states">
<span class="sw-state">not_contacted</span>
<span class="sw-arrow"></span>
<span class="sw-state">contacted</span>
<span class="sw-state">contacted_no_response</span>
</div>
</div>
<div class="sw-card">
<h4>Satisfaction</h4>
<div class="field"><code>Complaint.satisfaction</code></div>
<div class="sw-states">
<span class="sw-state">satisfied</span>
<span class="sw-state">neutral</span>
<span class="sw-state">dissatisfied</span>
<span class="sw-state">no_response</span>
</div>
<div class="field" style="margin-top:0.5rem;">🔒 Locked by patient submission · max 3 PX-team changes · 5-day window</div>
</div>
<div class="sw-card">
<h4>Adverse Action</h4>
<div class="field"><code>ComplaintAdverseAction.verification_status</code></div>
<div class="sw-states">
<span class="sw-state">reported</span>
<span class="sw-arrow"></span>
<span class="sw-state">under_investigation</span>
<span class="sw-arrow"></span>
<span class="sw-state">verified</span>
<span class="sw-state">unfounded</span>
<span class="sw-state">resolved</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
Generated from source · apps/complaints/ · PX360
</div>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'base',
themeVariables: {
primaryColor: '#e8f1f8',
primaryTextColor: '#0a1e3f',
primaryBorderColor: '#007bbd',
lineColor: '#64748b',
fontSize: '14px',
fontFamily: 'Inter, sans-serif',
},
flowchart: { curve: 'basis', padding: 20 },
stateDiagram: { backgroundColor: '#fafbfc' },
});
function switchTab(e, tabId) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
e.currentTarget.classList.add('active');
document.getElementById(tabId).classList.add('active');
}
</script>
</body>
</html>