HH/templates/organizations/department_complaints.html
ismail 7369d08012
All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
feat: unified reference numbers + feedback modules QA audit
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001):
- new ReferenceSequence model + generate_reference() helper (apps/core)
- Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save()
- prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only)
- removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources
- migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012
- unit tests (format, sanitization, monthly reset, 40-thread concurrency)

QA audit:
- isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env)
- feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion)
- reports/feedback-modules-qa-report.md

Also bundles accumulated in-progress work across complaints, observations,
organizations, templates, and other modules.
2026-06-14 14:29:23 +03:00

214 lines
13 KiB
HTML

{% extends "layouts/base.html" %}
{% load i18n %}
{% load analytics_extras %}
{% block title %}{{ department.get_localized_name }} - {% trans "Complaints" %} - PX360{% endblock %}
{% block content %}
<div class="p-6">
<div class="flex items-center gap-2 text-sm text-slate-500 mb-4">
<a href="{% url 'organizations:department_list' %}" class="hover:text-navy transition">{% trans "Departments" %}</a>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
<a href="{% url 'organizations:department_detail' department.pk %}" class="hover:text-navy transition">{{ department.get_localized_name }}</a>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
<span class="text-navy font-semibold">{% trans "Complaints" %}</span>
</div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-navy">{% trans "Department Complaints" %}</h1>
<p class="text-sm text-slate mt-1">{{ department.get_localized_name }} &middot; {{ department.hospital.name }}</p>
</div>
</div>
<form method="get" class="bg-white rounded-t-2xl border border-slate-200 border-b-0 px-4 py-3">
<div class="flex flex-wrap items-center gap-3">
<div class="relative flex-1 min-w-[200px]">
<i data-lucide="search" class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
<input type="text" name="search" value="{{ search_query|default:'' }}" placeholder="{% trans 'Search complaints...' %}"
class="pl-9 pr-4 py-2 w-full border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-navy/20 outline-none">
</div>
<select name="status" onchange="this.form.submit()"
class="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-navy/20 outline-none">
<option value="">{% trans "All Statuses" %}</option>
<option value="open" {% if status_filter == 'open' %}selected{% endif %}>{% trans "Open" %}</option>
<option value="in_progress" {% if status_filter == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option>
<option value="resolved" {% if status_filter == 'resolved' %}selected{% endif %}>{% trans "Resolved" %}</option>
<option value="closed" {% if status_filter == 'closed' %}selected{% endif %}>{% trans "Closed" %}</option>
</select>
<select name="severity" onchange="this.form.submit()"
class="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-navy/20 outline-none">
<option value="">{% trans "All Severities" %}</option>
<option value="low" {% if severity_filter == 'low' %}selected{% endif %}>{% trans "Low" %}</option>
<option value="medium" {% if severity_filter == 'medium' %}selected{% endif %}>{% trans "Medium" %}</option>
<option value="high" {% if severity_filter == 'high' %}selected{% endif %}>{% trans "High" %}</option>
<option value="critical" {% if severity_filter == 'critical' %}selected{% endif %}>{% trans "Critical" %}</option>
</select>
<input type="date" name="date_from" value="{{ date_from }}" placeholder="{% trans 'From' %}"
class="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-navy/20 outline-none">
<input type="date" name="date_to" value="{{ date_to }}" placeholder="{% trans 'To' %}"
class="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-navy/20 outline-none">
<button type="submit" class="px-3 py-2 bg-navy text-white rounded-lg text-sm font-semibold hover:bg-blue transition">
<i data-lucide="filter" class="w-3 h-3 inline"></i> {% trans "Filter" %}
</button>
{% if search_query or status_filter or severity_filter or date_from or date_to %}
<a href="?" class="text-xs text-slate-400 hover:text-red-500 transition font-semibold">
<i data-lucide="x" class="w-3 h-3 inline"></i> {% trans "Clear" %}
</a>
{% endif %}
</div>
</form>
{% include "partials/list_stats_bar.html" %}
<div class="bg-white border-x border-slate-200">
<table class="w-full">
<thead class="bg-slate-50 border-b">
<tr>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Reference" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Classification" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Staff" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Patient" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Assigned To" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Status" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Severity" %}</th>
<th class="px-4 py-3 text-center text-[10px] font-bold text-slate uppercase">{% trans "SLA" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Created" %}</th>
{% if can_respond %}
<th class="px-4 py-3 text-center text-[10px] font-bold text-slate uppercase">{% trans "Action" %}</th>
{% endif %}
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for c in complaints %}
<tr class="hover:bg-slate-50 transition border-l-3 border-transparent hover:border-navy cursor-pointer"
onclick="window.location='{% url 'organizations:department_complaint_detail' department.pk c.pk %}'">
<td class="px-4 py-3 text-sm font-mono text-navy">{{ c.reference_number }}</td>
<td class="px-4 py-3 text-sm text-slate-700 max-w-xs">
<div class="flex flex-wrap gap-1">
{% if c.domain %}<span class="px-1.5 py-0.5 bg-indigo-50 text-indigo-700 rounded text-[10px] font-semibold">{{ c.domain.get_localized_name }}</span>{% endif %}
{% if c.category %}<span class="px-1.5 py-0.5 bg-purple-50 text-purple-700 rounded text-[10px] font-semibold">{{ c.category.get_localized_name }}</span>{% endif %}
{% if c.subcategory_obj %}<span class="px-1.5 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] font-semibold">{{ c.subcategory_obj.get_localized_name }}</span>{% endif %}
</div>
</td>
<td class="px-4 py-3 text-sm text-slate max-w-xs">
{% if c.involved_staff.all %}
{% for inv in c.involved_staff.all %}
<div class="text-xs">{{ inv.staff.get_localized_name }}</div>
{% endfor %}
{% elif c.staff %}
<div class="text-xs">{{ c.staff.get_localized_name }}</div>
{% else %}
<span class="text-slate-400">-</span>
{% endif %}
</td>
<td class="px-4 py-3 text-sm text-slate">{{ c.patient.mrn|default:"-" }}</td>
<td class="px-4 py-3 text-sm text-slate">{{ c.assigned_to.get_full_name|default:"-" }}</td>
<td class="px-4 py-3">
<span class="px-2 py-1 rounded-full text-[10px] font-bold uppercase
{% if c.status == 'open' %}bg-blue-50 text-blue-700
{% elif c.status == 'in_progress' %}bg-amber-50 text-amber-700
{% elif c.status == 'resolved' %}bg-green-50 text-green-700
{% elif c.status == 'closed' %}bg-slate-100 text-slate-600
{% else %}bg-slate-50 text-slate-500{% endif %}">
{{ c.get_status_display }}
</span>
</td>
<td class="px-4 py-3">
<span class="px-2 py-1 rounded-full text-[10px] font-bold uppercase
{% if c.severity == 'critical' %}bg-gray-800 text-white
{% elif c.severity == 'high' %}bg-red-50 text-red-700
{% elif c.severity == 'medium' %}bg-amber-50 text-amber-700
{% else %}bg-green-50 text-green-700{% endif %}">
{{ c.get_severity_display }}
</span>
</td>
<td class="px-4 py-3 text-center">
{% if c.due_at and c.status != 'resolved' and c.status != 'closed' %}
<span class="sla-timer text-[10px] font-bold" data-due-at="{{ c.due_at|date:'c' }}" data-overdue="{{ c.is_overdue|yesno:'true,false' }}"></span>
{% elif c.is_overdue %}
<span class="text-[10px] font-bold text-red-600">{% trans "Overdue" %}</span>
{% else %}
<span class="text-[10px] text-slate-400">-</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs text-slate">{{ c.created_at|date:"Y-m-d" }}</td>
{% if can_respond %}
<td class="px-4 py-3 text-center" onclick="event.stopPropagation()">
{% with idept=complaint_dept_map|get_item:c.pk %}
{% if idept %}
{% if idept.response_submitted and idept.manager_review_status == 'pending' %}
<span class="px-2 py-1 bg-amber-50 text-amber-700 rounded text-[10px] font-bold inline-flex items-center gap-1">
<i data-lucide="clock" class="w-3 h-3"></i> {% trans "Pending Manager Review" %}
</span>
{% else %}
<button onclick="openDeptResponseModal('complaint', '{{ idept.pk }}', '{% url 'complaints:involved_department_response' idept.pk %}', '{{ c.reference_number }}', '{{ c.title|escapejs }}')"
class="px-3 py-1.5 bg-navy text-white rounded-lg text-[11px] font-semibold hover:bg-blue transition inline-flex items-center gap-1.5">
<i data-lucide="message-square" class="w-3 h-3"></i> {% trans "Respond" %}
</button>
{% endif %}
{% else %}
<span class="text-[10px] text-green-600 font-semibold">&#10003; {% trans "Submitted" %}</span>
{% endif %}
{% endwith %}
</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="{% if can_respond %}10{% else %}9{% endif %}" class="px-4 py-12 text-center text-slate">{% trans "No complaints found" %}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "partials/pagination.html" %}
</div>
{% if can_respond %}
{% include "components/department_response_modal.html" %}
{% endif %}
<script>
(function() {
var timers = document.querySelectorAll('.sla-timer');
if (!timers.length) return;
function updateTimers() {
var now = Date.now();
timers.forEach(function(el) {
var dueAt = new Date(el.dataset.dueAt).getTime();
var diff = dueAt - now;
if (diff <= 0) {
var overMs = Math.abs(diff);
var overD = Math.floor(overMs / 86400000);
var overH = Math.floor((overMs % 86400000) / 3600000);
var overM = Math.floor((overMs % 3600000) / 60000);
var txt = '{% trans "Overdue" %} ';
if (overD > 0) txt += overD + 'd ';
txt += overH + 'h ' + overM + 'm';
el.textContent = txt;
el.className = 'sla-timer text-[10px] font-bold text-red-600';
return;
}
var d = Math.floor(diff / 86400000);
var h = Math.floor((diff % 86400000) / 3600000);
var m = Math.floor((diff % 3600000) / 60000);
var parts = [];
if (d > 0) parts.push(d + 'd');
parts.push(h + 'h');
parts.push(m + 'm');
el.textContent = parts.join(' ');
if (diff < 21600000) {
el.className = 'sla-timer text-[10px] font-bold text-red-600';
} else if (diff < 86400000) {
el.className = 'sla-timer text-[10px] font-bold text-orange-600';
} else {
el.className = 'sla-timer text-[10px] font-bold text-green-600';
}
});
}
updateTimers();
setInterval(updateTimers, 60000);
})();
</script>
{% endblock %}