1508 lines
95 KiB
HTML
1508 lines
95 KiB
HTML
{% extends 'layouts/base.html' %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
{% get_current_language as LANG %}
|
|
|
|
{% block title %}{{ complaint.reference_number }} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.tab-active {
|
|
border-bottom: 3px solid #005696;
|
|
color: #005696;
|
|
font-weight: 700;
|
|
}
|
|
.tab-inactive {
|
|
color: #64748b;
|
|
font-weight: 500;
|
|
}
|
|
.tab-inactive:hover {
|
|
color: #005696;
|
|
}
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 30px;
|
|
}
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 8px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #e5e7eb;
|
|
}
|
|
.timeline-item {
|
|
position: relative;
|
|
padding-bottom: 24px;
|
|
}
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -26px;
|
|
top: 2px;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
border: 3px solid #005696;
|
|
z-index: 1;
|
|
}
|
|
.timeline-item.status_change::before { border-color: #f97316; }
|
|
.timeline-item.assignment::before { border-color: #3b82f6; }
|
|
.timeline-item.escalation::before { border-color: #ef4444; }
|
|
.timeline-item.note::before { border-color: #22c55e; }
|
|
|
|
.inner-tab-active {
|
|
border-bottom: 2px solid #005696;
|
|
color: #005696;
|
|
font-weight: 700;
|
|
background: white;
|
|
}
|
|
.inner-tab-inactive {
|
|
color: #64748b;
|
|
}
|
|
.ts-dropdown {
|
|
z-index: 9999 !important;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
|
|
|
<!-- Breadcrumb & Header -->
|
|
<header class="mb-6">
|
|
<div class="flex items-center gap-2 text-sm text-slate mb-2">
|
|
<a href="{% url 'complaints:complaint_list' %}" class="hover:text-navy">{% trans "Cases" %}</a>
|
|
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
|
<span class="font-bold text-navy">{{ complaint.reference_number }}</span>
|
|
<span class="ml-2 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider
|
|
{% if complaint.status == 'open' %}bg-yellow-100 text-yellow-700
|
|
{% elif complaint.status == 'in_progress' %}bg-blue-100 text-blue-700
|
|
{% elif complaint.status == 'partially_resolved' %}bg-amber-100 text-amber-700
|
|
{% elif complaint.status == 'resolved' %}bg-green-100 text-green-700
|
|
{% elif complaint.status == 'closed' %}bg-slate-100 text-slate-600
|
|
{% elif complaint.status == 'cancelled' %}bg-red-100 text-red-700
|
|
{% elif complaint.status == 'pending_external' %}bg-cyan-100 text-cyan-700
|
|
{% elif complaint.status == 'ovr_pending' %}bg-purple-100 text-purple-700
|
|
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
|
{{ complaint.get_status_display }}
|
|
</span>
|
|
{% if not complaint.sent_to_any_department %}
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase bg-amber-100 text-amber-700">
|
|
<i data-lucide="alert-circle" class="w-3 h-3 inline"></i> {% trans "Not Sent to Dept" %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if complaint.source_complaint.exists %}
|
|
{% with sc=complaint.source_complaint.first %}
|
|
<div class="bg-blue-50 border border-blue-200 rounded-xl p-3 mb-4 flex items-center gap-3">
|
|
<i data-lucide="link" class="w-4 h-4 text-blue-600 flex-shrink-0"></i>
|
|
<p class="text-sm text-blue-800">
|
|
{% trans "Converted from source complaint" %}
|
|
<a href="{% url 'px_sources:source_detail' pk=sc.px_source.pk %}" class="font-bold hover:underline">{{ sc.reference_number }}</a>
|
|
{% if sc.px_source %}({{ sc.px_source.get_localized_name }}){% endif %}
|
|
</p>
|
|
</div>
|
|
{% endwith %}
|
|
{% endif %}
|
|
{% if complaint.reopened_from %}
|
|
<div class="bg-amber-50 border border-amber-200 rounded-xl p-3 mb-4 flex items-center gap-3">
|
|
<i data-lucide="rotate-ccw" class="w-4 h-4 text-amber-600 flex-shrink-0"></i>
|
|
<p class="text-sm text-amber-800">
|
|
{% trans "Reopened from complaint" %}
|
|
<a href="{% url 'complaints:complaint_detail' pk=complaint.reopened_from.pk %}" class="font-bold hover:underline">{{ complaint.reopened_from.reference_number|default:complaint.reopened_from.pk }}</a>
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if complaint.reopenings.exists %}
|
|
<div class="bg-amber-50 border border-amber-200 rounded-xl p-3 mb-4 flex items-center gap-3">
|
|
<i data-lucide="git-branch" class="w-4 h-4 text-amber-600 flex-shrink-0"></i>
|
|
<p class="text-sm text-amber-800">
|
|
{% trans "This complaint has been reopened as" %}:
|
|
{% for r in complaint.reopenings.all %}
|
|
<a href="{% url 'complaints:complaint_detail' pk=r.pk %}" class="font-bold hover:underline">{{ r.reference_number|default:r.pk }}</a>{% if not forloop.last %}, {% endif %}
|
|
{% endfor %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-2xl font-bold text-navy">{{ complaint.title }}</h1>
|
|
<div class="flex items-center gap-3">
|
|
{% comment %} <a href="{% url 'complaints:complaint_pdf' complaint.id %}" target="_blank"
|
|
class="text-slate hover:text-navy px-3 py-2 text-sm font-semibold flex items-center gap-2 border rounded-lg hover:bg-light transition">
|
|
<i data-lucide="printer" class="w-4 h-4"></i> {% trans "PDF View" %}
|
|
</a> {% endcomment %}
|
|
{% if can_edit and complaint.is_active_status and complaint.assigned_to == current_user %}
|
|
<button onclick="showResolveModal()" class="bg-navy text-white px-4 py-2 rounded-lg text-sm font-bold shadow-md hover:bg-blue transition">
|
|
{% trans "Resolve Case" %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Workflow Stepper -->
|
|
{% if not workflow_steps.cancelled %}
|
|
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 mb-6">
|
|
<div class="flex items-center justify-between">
|
|
<!-- Step 1: Created -->
|
|
<div class="flex flex-col items-center flex-shrink-0">
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-green-500 text-white">
|
|
<i data-lucide="check" class="w-5 h-5"></i>
|
|
</div>
|
|
<span class="text-xs font-bold text-slate mt-2">{% trans "Created" %}</span>
|
|
</div>
|
|
<div class="flex-1 h-1 mx-2 rounded-full {% if workflow_steps.activated %}bg-green-500{% else %}bg-slate-200{% endif %}"></div>
|
|
|
|
<!-- Step 2: Activate -->
|
|
<div class="flex flex-col items-center flex-shrink-0">
|
|
{% if workflow_steps.activated %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-green-500 text-white"><i data-lucide="check" class="w-5 h-5"></i></div>
|
|
{% else %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-navy text-white ring-4 ring-navy/20"><i data-lucide="play" class="w-5 h-5"></i></div>
|
|
{% endif %}
|
|
<span class="text-xs font-bold mt-2 {% if workflow_steps.activated %}text-slate{% else %}text-navy{% endif %}">{% trans "Activate" %}</span>
|
|
</div>
|
|
<div class="flex-1 h-1 mx-2 rounded-full {% if workflow_steps.sent_to_department %}bg-green-500{% else %}bg-slate-200{% endif %}"></div>
|
|
|
|
<!-- Step 3: Send to Department -->
|
|
<div class="flex flex-col items-center flex-shrink-0">
|
|
{% if workflow_steps.sent_to_department %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-green-500 text-white"><i data-lucide="check" class="w-5 h-5"></i></div>
|
|
{% elif workflow_steps.activated %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-navy text-white ring-4 ring-navy/20"><i data-lucide="send" class="w-5 h-5"></i></div>
|
|
{% else %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center border-2 border-slate-200 text-slate-300"><i data-lucide="send" class="w-5 h-5"></i></div>
|
|
{% endif %}
|
|
<span class="text-xs font-bold mt-2 {% if workflow_steps.sent_to_department %}text-slate{% elif workflow_steps.activated %}text-navy{% else %}text-slate-300{% endif %}">{% trans "Send to Dept" %}</span>
|
|
</div>
|
|
<div class="flex-1 h-1 mx-2 rounded-full {% if workflow_steps.department_responded %}bg-green-500{% else %}bg-slate-200{% endif %}"></div>
|
|
|
|
<!-- Step 4: Response -->
|
|
<div class="flex flex-col items-center flex-shrink-0">
|
|
{% if workflow_steps.department_responded %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-green-500 text-white"><i data-lucide="check" class="w-5 h-5"></i></div>
|
|
{% elif workflow_steps.sent_to_department %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-navy text-white ring-4 ring-navy/20"><i data-lucide="message-square" class="w-5 h-5"></i></div>
|
|
{% else %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center border-2 border-slate-200 text-slate-300"><i data-lucide="message-square" class="w-5 h-5"></i></div>
|
|
{% endif %}
|
|
<span class="text-xs font-bold mt-2 {% if workflow_steps.department_responded %}text-slate{% elif workflow_steps.sent_to_department %}text-navy{% else %}text-slate-300{% endif %}">{% trans "Response" %}</span>
|
|
</div>
|
|
<div class="flex-1 h-1 mx-2 rounded-full {% if workflow_steps.resolved %}bg-green-500{% else %}bg-slate-200{% endif %}"></div>
|
|
|
|
<!-- Step 5: Resolve -->
|
|
<div class="flex flex-col items-center flex-shrink-0">
|
|
{% if workflow_steps.resolved %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-green-500 text-white"><i data-lucide="check" class="w-5 h-5"></i></div>
|
|
{% elif workflow_steps.department_responded %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-navy text-white ring-4 ring-navy/20"><i data-lucide="check-circle-2" class="w-5 h-5"></i></div>
|
|
{% else %}
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center border-2 border-slate-200 text-slate-300"><i data-lucide="check-circle-2" class="w-5 h-5"></i></div>
|
|
{% endif %}
|
|
<span class="text-xs font-bold mt-2 {% if workflow_steps.resolved %}text-slate{% elif workflow_steps.department_responded %}text-navy{% else %}text-slate-300{% endif %}">{% trans "Resolve" %}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Next Action Callout -->
|
|
{% if complaint.is_active_status and can_edit %}
|
|
{% if not workflow_steps.activated %}
|
|
<div class="mt-5 p-4 bg-blue-50 rounded-xl border border-blue-200 flex items-center justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-9 h-9 rounded-full bg-blue-100 flex items-center justify-center shrink-0"><i data-lucide="play" class="w-4 h-4 text-blue-600"></i></div>
|
|
<div><p class="text-sm font-bold text-navy">{% trans "Activate this complaint" %}</p><p class="text-xs text-slate">{% trans "Assign it to yourself to start working on it" %}</p></div>
|
|
</div>
|
|
<form method="post" action="{% url 'complaints:complaint_activate' pk=complaint.pk %}" class="shrink-0" data-on:submit="@post('{% url 'complaints:complaint_activate' pk=complaint.pk %}', {contentType:'form'})">{% csrf_token %}
|
|
<button type="submit" class="px-4 py-2 bg-navy text-white rounded-lg font-bold text-sm hover:bg-blue transition whitespace-nowrap">{% trans "Activate" %}</button>
|
|
</form>
|
|
</div>
|
|
{% elif not workflow_steps.sent_to_department %}
|
|
<div class="mt-5 p-4 bg-blue-50 rounded-xl border border-blue-200 flex items-center justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-9 h-9 rounded-full bg-blue-100 flex items-center justify-center shrink-0"><i data-lucide="send" class="w-4 h-4 text-blue-600"></i></div>
|
|
<div><p class="text-sm font-bold text-navy">{% trans "Send to department" %}</p><p class="text-xs text-slate">{% trans "Notify the champion and manager to collect their response" %}</p></div>
|
|
</div>
|
|
<button type="button" onclick="openSendToDeptModal()" class="px-4 py-2 bg-navy text-white rounded-lg font-bold text-sm hover:bg-blue transition whitespace-nowrap shrink-0">{% trans "Send to Department" %}</button>
|
|
</div>
|
|
{% elif not workflow_steps.department_responded %}
|
|
<div class="mt-5 p-4 bg-amber-50 rounded-xl border border-amber-200 flex items-center gap-3">
|
|
<div class="w-9 h-9 rounded-full bg-amber-100 flex items-center justify-center shrink-0"><i data-lucide="clock" class="w-4 h-4 text-amber-600 animate-pulse"></i></div>
|
|
<div><p class="text-sm font-bold text-amber-800">{% trans "Awaiting department response" %}</p><p class="text-xs text-amber-700">{% trans "The department champion and manager have been notified. You'll be notified when they respond." %}</p></div>
|
|
</div>
|
|
{% elif not workflow_steps.resolved %}
|
|
<div class="mt-5 p-4 bg-green-50 rounded-xl border border-green-200 flex items-center justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-9 h-9 rounded-full bg-green-100 flex items-center justify-center shrink-0"><i data-lucide="check-circle-2" class="w-4 h-4 text-green-600"></i></div>
|
|
<div><p class="text-sm font-bold text-navy">{% trans "Resolve this complaint" %}</p><p class="text-xs text-slate">{% trans "The department has responded. Review and close the complaint." %}</p></div>
|
|
</div>
|
|
<button type="button" onclick="switchTab('resolution')" class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700 transition whitespace-nowrap shrink-0">{% trans "Generate Resolution" %}</button>
|
|
</div>
|
|
{% endif %}
|
|
{% elif workflow_steps.resolved %}
|
|
{% if complaint.satisfaction %}
|
|
<!-- Satisfaction recorded: green banner + satisfaction pill -->
|
|
<div class="mt-5 p-4 bg-green-50 rounded-xl border border-green-200 flex items-start gap-3">
|
|
<div class="w-9 h-9 rounded-full bg-green-100 flex items-center justify-center shrink-0"><i data-lucide="check-circle-2" class="w-4 h-4 text-green-600"></i></div>
|
|
<div class="flex-1">
|
|
<p class="text-sm font-bold text-green-800">{% trans "Complaint resolved" %}</p>
|
|
<p class="text-xs text-green-700">{% trans "This complaint has been successfully resolved." %}</p>
|
|
<div class="flex items-center gap-2 mt-2 flex-wrap">
|
|
{% if complaint.resolution_outcome %}
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold
|
|
{% if complaint.resolution_outcome == 'patient' %}bg-blue-100 text-blue-700
|
|
{% elif complaint.resolution_outcome == 'hospital' %}bg-green-100 text-green-700
|
|
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
|
{% trans "Outcome:" %} {{ complaint.get_resolution_outcome_display }}
|
|
</span>
|
|
{% endif %}
|
|
{% if complaint.resolution_sent_at %}
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-cyan-100 text-cyan-700 flex items-center gap-1">
|
|
<svg class="w-2.5 h-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
|
|
{% trans "Sent to patient" %}
|
|
</span>
|
|
{% endif %}
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold inline-flex items-center gap-1
|
|
{% if complaint.satisfaction == 'satisfied' %}bg-green-200 text-green-800
|
|
{% elif complaint.satisfaction == 'neutral' %}bg-yellow-100 text-yellow-800
|
|
{% elif complaint.satisfaction == 'dissatisfied' %}bg-red-100 text-red-700
|
|
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
|
{% if complaint.satisfaction == 'satisfied' %}<i data-lucide="thumbs-up" class="w-3 h-3"></i>
|
|
{% elif complaint.satisfaction == 'neutral' %}<i data-lucide="minus-circle" class="w-3 h-3"></i>
|
|
{% elif complaint.satisfaction == 'dissatisfied' %}<i data-lucide="thumbs-down" class="w-3 h-3"></i>
|
|
{% else %}<i data-lucide="phone-off" class="w-3 h-3"></i>{% endif %}
|
|
{% if complaint.satisfaction_locked_by_patient %}<i data-lucide="lock" class="w-3 h-3"></i>{% endif %}
|
|
{% if complaint.satisfaction_locked_by_patient %}{% trans "Patient (locked):" %}{% else %}{% trans "Patient:" %}{% endif %} {{ complaint.get_satisfaction_display }}
|
|
</span>
|
|
{% if complaint.satisfaction_set_at %}
|
|
<span class="text-[10px] text-slate-400">{{ complaint.satisfaction_set_at|date:"Y-m-d H:i" }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<!-- Satisfaction NOT yet recorded: amber next-action callout -->
|
|
<div class="mt-5 p-4 bg-amber-50 rounded-xl border border-amber-200 flex items-center justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-9 h-9 rounded-full bg-amber-100 flex items-center justify-center shrink-0"><i data-lucide="phone-call" class="w-4 h-4 text-amber-600"></i></div>
|
|
<div>
|
|
<p class="text-sm font-bold text-amber-800">{% trans "Record patient satisfaction" %}</p>
|
|
<p class="text-xs text-amber-700">{% trans "Call the patient to confirm resolution, then capture their satisfaction level." %}</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" onclick="switchTab('resolution', 'patient-satisfaction')" class="px-4 py-2 bg-amber-600 text-white rounded-lg font-bold text-sm hover:bg-amber-700 transition whitespace-nowrap shrink-0">{% trans "Record Satisfaction" %}</button>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Tab Navigation -->
|
|
<nav class="bg-white px-6 flex gap-6 border-b shadow-sm mb-6 rounded-t-2xl">
|
|
<button class="py-4 text-sm tab-active" onclick="switchTab('details')" id="tab-details">
|
|
{% trans "Details" %}
|
|
</button>
|
|
<button class="py-4 text-sm {% if complaint.assigned_to == current_user or not complaint.is_active_status %}tab-inactive{% else %}text-slate-300 cursor-not-allowed{% endif %}"
|
|
{% if complaint.assigned_to == current_user or not complaint.is_active_status %}onclick="switchTab('departments')"{% else %}onclick="showActivationRequired()"{% endif %} id="tab-departments">
|
|
{% trans "Departments" %} ({{ complaint.involved_departments_count }})
|
|
{% if not complaint.assigned_to == current_user and complaint.is_active_status %}<i data-lucide="lock" class="w-3 h-3 inline ml-1"></i>{% endif %}
|
|
</button>
|
|
<button class="py-4 text-sm {% if complaint.assigned_to == current_user or not complaint.is_active_status %}tab-inactive{% else %}text-slate-300 cursor-not-allowed{% endif %}"
|
|
{% if complaint.assigned_to == current_user or not complaint.is_active_status %}onclick="switchTab('timeline')"{% else %}onclick="showActivationRequired()"{% endif %} id="tab-timeline">
|
|
{% trans "Timeline" %} ({{ stage_timeline.stages|length }})
|
|
{% if not complaint.assigned_to == current_user and complaint.is_active_status %}<i data-lucide="lock" class="w-3 h-3 inline ml-1"></i>{% endif %}
|
|
</button>
|
|
{% comment %} <button class="py-4 text-sm {% if complaint.assigned_to == current_user or not complaint.is_active_status %}tab-inactive{% else %}text-slate-300 cursor-not-allowed{% endif %}"
|
|
{% if complaint.assigned_to == current_user or not complaint.is_active_status %}onclick="switchTab('attachments')"{% else %}onclick="showActivationRequired()"{% endif %} id="tab-attachments">
|
|
{% trans "Attachments" %} ({{ complaint.attachments_count }})
|
|
{% if not complaint.assigned_to == current_user and complaint.is_active_status %}<i data-lucide="lock" class="w-3 h-3 inline ml-1"></i>{% endif %}
|
|
</button> {% endcomment %}
|
|
{% if can_admin %}
|
|
<button class="py-4 text-sm {% if complaint.assigned_to == current_user or not complaint.is_active_status %}tab-inactive{% else %}text-slate-300 cursor-not-allowed{% endif %} flex items-center gap-2"
|
|
{% if complaint.assigned_to == current_user or not complaint.is_active_status %}onclick="switchTab('actions')"{% else %}onclick="showActivationRequired()"{% endif %} id="tab-actions">
|
|
<i data-lucide="list-checks" class="w-4 h-4 {% if not complaint.assigned_to == current_user and complaint.is_active_status %}text-slate-300{% endif %}"></i>
|
|
{% trans "PX Actions" %}
|
|
{% if px_actions.count %}
|
|
<span class="ml-1 px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full">{{ px_actions.count }}</span>
|
|
{% endif %}
|
|
{% if linked_rcas %}
|
|
<span class="px-1.5 py-0.5 bg-purple-100 text-purple-700 text-xs rounded-full">{{ linked_rcas.count }}</span>
|
|
{% endif %}
|
|
{% if not complaint.assigned_to == current_user and complaint.is_active_status %}<i data-lucide="lock" class="w-3 h-3"></i>{% endif %}
|
|
</button>
|
|
{% endif %}
|
|
<button class="py-4 text-sm {% if complaint.assigned_to == current_user or not complaint.is_active_status %}tab-inactive{% else %}text-slate-300 cursor-not-allowed{% endif %} flex items-center gap-2"
|
|
{% if complaint.assigned_to == current_user or not complaint.is_active_status %}onclick="switchTab('ai')"{% else %}onclick="showActivationRequired()"{% endif %} id="tab-ai">
|
|
<i data-lucide="sparkles" class="w-4 h-4 {% if not complaint.assigned_to == current_user and complaint.is_active_status %}text-slate-300{% endif %}"></i> {% trans "AI Analysis" %}
|
|
{% if not complaint.assigned_to == current_user and complaint.is_active_status %}<i data-lucide="lock" class="w-3 h-3"></i>{% endif %}
|
|
</button>
|
|
<button class="py-4 text-sm {% if complaint.assigned_to == current_user or not complaint.is_active_status %}tab-inactive{% else %}text-slate-300 cursor-not-allowed{% endif %} flex items-center gap-2"
|
|
{% if complaint.assigned_to == current_user or not complaint.is_active_status %}onclick="switchTab('resolution')"{% else %}onclick="showActivationRequired()"{% endif %} id="tab-resolution">
|
|
<i data-lucide="check-circle-2" class="w-4 h-4 {% if not complaint.assigned_to == current_user and complaint.is_active_status %}text-slate-300{% endif %}"></i>
|
|
{% trans "Resolution" %}
|
|
{% if not complaint.assigned_to == current_user and complaint.is_active_status %}<i data-lucide="lock" class="w-3 h-3"></i>{% endif %}
|
|
</button>
|
|
{% comment %} <button class="py-4 text-sm {% if complaint.assigned_to == current_user or not complaint.is_active_status %}tab-inactive{% else %}text-slate-300 cursor-not-allowed{% endif %} flex items-center gap-1"
|
|
{% if complaint.assigned_to == current_user or not complaint.is_active_status %}onclick="switchTab('adverse')"{% else %}onclick="showActivationRequired()"{% endif %} id="tab-adverse">
|
|
<i data-lucide="shield-alert" class="w-4 h-4 {% if not complaint.assigned_to == current_user and complaint.is_active_status %}text-slate-300{% endif %}"></i>
|
|
{% trans "Adverse Actions" %}
|
|
{% if adverse_actions %}
|
|
<span class="ml-1 px-1.5 py-0.5 bg-red-100 text-red-700 text-xs rounded-full">{{ adverse_actions.count }}</span>
|
|
{% endif %}
|
|
{% if not complaint.assigned_to == current_user and complaint.is_active_status %}<i data-lucide="lock" class="w-3 h-3"></i>{% endif %}
|
|
</button> {% endcomment %}
|
|
<button class="py-4 text-sm {% if complaint.assigned_to == current_user or not complaint.is_active_status %}tab-inactive{% else %}text-slate-300 cursor-not-allowed{% endif %} flex items-center gap-2"
|
|
{% if complaint.assigned_to == current_user or not complaint.is_active_status %}onclick="switchTab('notes')"{% else %}onclick="showActivationRequired()"{% endif %} id="tab-notes">
|
|
<i data-lucide="message-square" class="w-4 h-4 {% if not complaint.assigned_to == current_user and complaint.is_active_status %}text-slate-300{% endif %}"></i> {% trans "Notes" %}
|
|
{% if notes_count %}
|
|
<span class="ml-1 px-1.5 py-0.5 bg-slate-100 text-slate-600 text-xs rounded-full">{{ notes_count }}</span>
|
|
{% endif %}
|
|
{% if not complaint.assigned_to == current_user and complaint.is_active_status %}<i data-lucide="lock" class="w-3 h-3"></i>{% endif %}
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- Main Content Grid -->
|
|
<main class="grid grid-cols-12 gap-6">
|
|
|
|
<!-- Left Column (Content) -->
|
|
<div class="col-span-8 space-y-6">
|
|
|
|
<!-- Details Tab -->
|
|
<div id="panel-details" class="tab-panel space-y-6">
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
|
|
{% if complaint.description %}
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<i data-lucide="message-square-text" class="w-4 h-4 text-slate-400"></i>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wide">{% trans "Description" %}</p>
|
|
</div>
|
|
<p class="text-base leading-relaxed text-slate">{{ complaint.description }}</p>
|
|
{% endif %}
|
|
|
|
<dl class="mt-6 pt-5 border-t border-slate-100 space-y-3">
|
|
<!-- Location -->
|
|
<div class="flex items-start gap-4">
|
|
<dt class="w-32 shrink-0 flex items-center gap-1.5 text-sm font-bold text-slate uppercase pt-0.5">
|
|
<i data-lucide="map-pin" class="w-3.5 h-3.5 text-slate-400 shrink-0"></i>
|
|
{% trans "Location" %}
|
|
{% if can_manage_actions and complaint.is_active_status %}
|
|
<button type="button" onclick="showLocationModal()"
|
|
title="{% trans 'Edit location details' %}"
|
|
class="text-slate hover:text-navy transition">
|
|
<i data-lucide="pencil" class="w-3 h-3"></i>
|
|
</button>
|
|
{% endif %}
|
|
</dt>
|
|
<dd class="flex-1 text-xs leading-relaxed">
|
|
{% if complaint.department or complaint.location_type or complaint.area or complaint.section %}
|
|
{% if complaint.department %}<span class="font-bold text-navy">{{ complaint.department.get_localized_name }}</span>{% else %}-{% endif %}
|
|
{% if complaint.location_type %}
|
|
<span class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide bg-navy/10 text-navy">{{ complaint.get_location_type_display }}</span>
|
|
{% endif %}
|
|
{% if complaint.area %}<span class="text-slate ml-2">· {% trans "Area" %}: {% if LANG == 'ar' and complaint.area.name_ar %}{{ complaint.area.name_ar }}{% else %}{{ complaint.area.name_en }}{% endif %}</span>{% endif %}
|
|
{% if complaint.department and complaint.department.category %}<span class="text-slate">· {% trans "Category" %}: {{ complaint.department.get_category_display }}</span>{% endif %}
|
|
{% if complaint.section %}<span class="text-slate">· {% trans "Section" %}: {{ complaint.section.get_localized_name }}</span>{% endif %}
|
|
{% if complaint.zone %}<span class="text-slate">· {% trans "Zone" %}: {{ complaint.zone }}</span>{% endif %}
|
|
{% if complaint.floor %}<span class="text-slate">· {% trans "Floor" %}: {{ complaint.floor }}</span>{% endif %}
|
|
{% else %}
|
|
<span class="font-bold text-navy">{% if complaint.legacy_location %}{{ complaint.legacy_location.name_en }}{% else %}-{% endif %}</span>
|
|
{% if complaint.legacy_main_section %}<span class="text-slate ml-2">{{ complaint.legacy_main_section.name_en }}{% if complaint.legacy_subsection %} > {{ complaint.legacy_subsection.name_en }}{% endif %}</span>{% endif %}
|
|
{% endif %}
|
|
</dd>
|
|
</div>
|
|
<!-- Severity -->
|
|
<div class="flex items-start gap-4">
|
|
<dt class="w-32 shrink-0 flex items-center gap-1.5 text-sm font-bold text-slate uppercase pt-0.5">
|
|
<i data-lucide="alert-circle" class="w-3.5 h-3.5 text-slate-400 shrink-0"></i>
|
|
{% trans "Severity" %}
|
|
</dt>
|
|
<dd class="flex-1 text-xs font-bold
|
|
{% if complaint.severity == 'critical' %}text-red-600
|
|
{% elif complaint.severity == 'high' %}text-orange-600
|
|
{% elif complaint.severity == 'medium' %}text-yellow-600
|
|
{% else %}text-green-600{% endif %}">
|
|
{{ complaint.get_severity_display }}
|
|
</dd>
|
|
</div>
|
|
<!-- Classification -->
|
|
<div class="flex items-start gap-4">
|
|
<dt class="w-32 shrink-0 flex items-center gap-1.5 text-sm font-bold text-slate uppercase pt-0.5">
|
|
<i data-lucide="tag" class="w-3.5 h-3.5 text-slate-400 shrink-0"></i>
|
|
{% trans "Classification" %}
|
|
</dt>
|
|
<dd class="flex-1 text-xs font-bold text-blue-700">
|
|
{% if complaint.ai_brief_en %}
|
|
{% if LANG == 'ar' and complaint.ai_brief_ar %}{{ complaint.ai_brief_ar }}{% else %}{{ complaint.ai_brief_en }}{% endif %}
|
|
{% else %}-{% endif %}
|
|
</dd>
|
|
</div>
|
|
<!-- Created -->
|
|
<div class="flex items-start gap-4">
|
|
<dt class="w-32 shrink-0 flex items-center gap-1.5 text-sm font-bold text-slate uppercase pt-0.5">
|
|
<i data-lucide="calendar" class="w-3.5 h-3.5 text-slate-400 shrink-0"></i>
|
|
{% trans "Created" %}
|
|
</dt>
|
|
<dd class="flex-1 text-xs font-bold text-navy">{{ complaint.created_at|date:"d M Y, h:i A" }}</dd>
|
|
</div>
|
|
<!-- Deadline -->
|
|
<div class="flex items-start gap-4">
|
|
<dt class="w-32 shrink-0 flex items-center gap-1.5 text-sm font-bold text-slate uppercase pt-0.5">
|
|
<i data-lucide="clock" class="w-3.5 h-3.5 text-slate-400 shrink-0"></i>
|
|
{% trans "Deadline" %}
|
|
</dt>
|
|
<dd class="flex-1 text-xs font-bold {% if complaint.is_overdue %}text-red-600{% else %}text-navy{% endif %}">
|
|
{{ complaint.due_at|date:"d M Y, h:i A" }}
|
|
{% if complaint.is_overdue %}
|
|
<span class="ml-1 px-1.5 py-0.5 rounded text-[10px] font-bold bg-red-500 text-white uppercase">{% trans "Overdue" %}</span>
|
|
{% endif %}
|
|
{% if complaint.due_at and complaint.status != 'resolved' and complaint.status != 'closed' and complaint.status != 'cancelled' %}
|
|
<p id="sla-countdown" class="text-xs font-bold mt-0.5"
|
|
data-due-at="{{ complaint.due_at|date:'c' }}"
|
|
data-overdue="{{ complaint.is_overdue|yesno:'true,false' }}">
|
|
</p>
|
|
{% elif complaint.sla_is_overdue_display %}
|
|
<p class="text-xs font-bold text-red-600 mt-0.5">{{ complaint.sla_is_overdue_display }}</p>
|
|
{% endif %}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
{% if complaint.domain or complaint.category or complaint.subcategory_obj or complaint.classification_obj %}
|
|
<div class="mt-5 pt-5 border-t border-slate-100 flex items-center gap-2 flex-wrap">
|
|
<span class="text-xs font-bold text-slate uppercase">{% trans "Taxonomy" %}:</span>
|
|
{% if complaint.domain %}<span class="text-sm text-navy font-medium">{{ complaint.domain.get_localized_name }}</span>{% endif %}
|
|
{% if complaint.category %}<i data-lucide="chevron-right" class="w-3 h-3 text-slate-300"></i><span class="text-sm text-navy font-medium">{{ complaint.category.get_localized_name }}</span>{% endif %}
|
|
{% if complaint.subcategory_obj %}<i data-lucide="chevron-right" class="w-3 h-3 text-slate-300"></i><span class="text-sm text-navy font-medium">{{ complaint.subcategory_obj.get_localized_name }}</span>{% endif %}
|
|
{% if complaint.classification_obj %}<i data-lucide="chevron-right" class="w-3 h-3 text-slate-300"></i><span class="text-sm text-navy font-medium">{{ complaint.classification_obj.get_localized_name }}</span>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if complaint.source %}
|
|
<div class="py-3 border-t border-slate-100 flex items-center gap-2">
|
|
<span class="text-[10px] font-bold text-slate uppercase">{% trans "Source" %}:</span>
|
|
<span class="text-sm text-navy font-medium">{{ complaint.source.get_localized_name }}</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if complaint.escalated_at %}
|
|
<div class="mt-3 p-3 bg-orange-50 border border-orange-200 rounded-xl flex items-center gap-3">
|
|
<i data-lucide="alert-triangle" class="w-5 h-5 text-orange-500 flex-shrink-0"></i>
|
|
<div>
|
|
<p class="text-xs font-bold text-orange-700 uppercase">{% trans "Escalated" %}</p>
|
|
<p class="text-xs text-orange-600">{% trans "Escalated on" %} {{ complaint.escalated_at|date:"d M Y, h:i A" }}</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if complaint.status == 'ovr_pending' %}
|
|
<div class="mt-3 p-4 bg-yellow-50 border-2 border-yellow-200 rounded-xl">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="px-2 py-0.5 text-[9px] font-bold rounded bg-yellow-100 text-yellow-700 uppercase">OVR</span>
|
|
<span class="text-sm font-bold text-yellow-700">{% trans "Pending Approval" %}</span>
|
|
</div>
|
|
<p class="text-xs text-yellow-600 mb-3">{% trans "OVR escalation requested. Please review and approve or reject." %}</p>
|
|
{% if request.user.is_px_admin or request.user.is_hospital_admin or request.user.is_px_management %}
|
|
<div class="flex gap-2">
|
|
<form method="post" action="{% url 'complaints:approve_ovr_escalation' pk=complaint.pk %}" data-on:submit="@post('{% url 'complaints:approve_ovr_escalation' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<button type="submit" class="px-4 py-2 bg-green-600 text-white text-sm font-semibold rounded-lg hover:bg-green-700 transition flex items-center gap-2">
|
|
<i data-lucide="check-circle" class="w-4 h-4"></i> {% trans "Approve" %}
|
|
</button>
|
|
</form>
|
|
<form method="post" action="{% url 'complaints:reject_ovr_escalation' pk=complaint.pk %}" data-on:submit="@post('{% url 'complaints:reject_ovr_escalation' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<button type="submit" class="px-4 py-2 bg-red-600 text-white text-sm font-semibold rounded-lg hover:bg-red-700 transition flex items-center gap-2">
|
|
<i data-lucide="x-circle" class="w-4 h-4"></i> {% trans "Reject" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-xs text-yellow-600">{% trans "Waiting for admin approval." %}</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if complaint.expected_result %}
|
|
<div class="mt-3 pt-3 border-t border-slate-100">
|
|
<p class="text-[10px] font-bold text-slate uppercase mb-1">{% trans "Expected Result" %}</p>
|
|
<p class="text-sm text-slate">{{ complaint.expected_result }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if complaint.patient %}
|
|
<div class="mt-3 pt-3 border-t border-slate-100 flex items-center gap-4 text-sm">
|
|
<i data-lucide="user" class="w-4 h-4 text-slate flex-shrink-0"></i>
|
|
<span class="font-bold text-navy">{{ complaint.patient.get_full_name }}</span>
|
|
<span class="text-slate">|</span>
|
|
<span class="text-slate">{% trans "MRN:" %} <span class="font-medium text-navy">{{ complaint.patient.mrn|default:"-" }}</span></span>
|
|
{% if complaint.patient.phone %}
|
|
<span class="text-slate">|</span>
|
|
<span class="text-slate">{{ complaint.patient.phone }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
{% if complaint.activated_at %}
|
|
{% include "complaints/partials/pdf_summary_panel.html" %}
|
|
{% else %}
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 text-center">
|
|
<div class="w-12 h-12 mx-auto bg-slate-100 rounded-full flex items-center justify-center mb-3">
|
|
<i data-lucide="lock" class="w-5 h-5 text-slate-400"></i>
|
|
</div>
|
|
<p class="text-sm font-semibold text-slate">{% trans "PDF Report" %}</p>
|
|
<p class="text-xs text-slate/60 mt-1">{% trans "Activate this complaint to enable PDF report generation." %}</p>
|
|
</section>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Departments Tab (inline; send-to-department opens #sendToDeptModal) -->
|
|
<div id="panel-departments" class="tab-panel hidden space-y-6">
|
|
<!-- Send status banner -->
|
|
{% if not complaint.sent_to_any_department %}
|
|
<div class="flex items-center gap-3 p-3 rounded-xl bg-amber-50 border border-amber-200">
|
|
<i data-lucide="send" class="w-4 h-4 text-amber-600 shrink-0"></i>
|
|
<p class="text-sm text-amber-700 font-semibold">{% trans "Not sent to any department yet" %}</p>
|
|
</div>
|
|
{% elif not complaint.all_departments_responded %}
|
|
<div class="flex items-center gap-3 p-3 rounded-xl bg-blue-50 border border-blue-200">
|
|
<i data-lucide="send" class="w-4 h-4 text-blue-600 shrink-0"></i>
|
|
<p class="text-sm text-blue-700 font-semibold">
|
|
{% if complaint.sent_to_department_at %}
|
|
{% blocktrans with dt=complaint.sent_to_department_at|date:"d M Y, h:i A" %}Sent to department on {{ dt }} — awaiting response{% endblocktrans %}
|
|
{% else %}
|
|
{% trans "Sent to department — awaiting response" %}
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% else %}
|
|
<div class="flex items-center gap-3 p-3 rounded-xl bg-green-50 border border-green-200">
|
|
<i data-lucide="check-circle-2" class="w-4 h-4 text-green-600 shrink-0"></i>
|
|
<p class="text-sm text-green-700 font-semibold">{% trans "All departments responded" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<!-- Involved Departments (read-only) -->
|
|
<section class="bg-white rounded-2xl p-4 shadow-sm border border-slate-100">
|
|
<h4 class="text-sm font-bold text-navy uppercase tracking-wide mb-3">{% trans "Involved Departments" %}</h4>
|
|
{% if complaint.involved_departments.exists %}
|
|
<ul class="divide-y divide-slate-100 border border-slate-100 rounded-xl overflow-hidden">
|
|
{% for dept in complaint.involved_departments.all %}
|
|
<li class="flex items-center justify-between p-3 gap-3">
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-semibold text-navy truncate">
|
|
{{ dept.department.get_localized_name }}
|
|
{% if dept.is_primary %}<span class="ml-1 text-[10px] font-bold text-navy bg-light px-1.5 py-0.5 rounded align-middle">{% trans "Primary" %}</span>{% endif %}
|
|
</p>
|
|
{% if dept.assigned_to %}<p class="text-[11px] text-slate-500"><i data-lucide="user-check" class="w-3 h-3 inline"></i> {{ dept.assigned_to.get_full_name }}</p>{% endif %}
|
|
{% if dept.routing_status == 'rejected' %}
|
|
<p class="text-[10px] text-red-500 mt-0.5 italic">{% trans "Rejected:" %} {{ dept.rejection_reason|truncatechars:80 }}{% if dept.suggested_department %} · {% trans "Suggested:" %} {{ dept.suggested_department.get_localized_name }}{% endif %}</p>
|
|
{% elif dept.forwarded_at or dept.sent_at %}
|
|
<p class="text-[10px] text-slate-400 mt-0.5"><i data-lucide="send" class="w-2.5 h-2.5 inline"></i> {% trans "Sent" %} {{ dept.forwarded_at|default:dept.sent_at|date:"d M Y" }}</p>
|
|
{% elif dept.sent %}
|
|
<p class="text-[10px] text-slate-400 mt-0.5"><i data-lucide="send" class="w-2.5 h-2.5 inline"></i> {% trans "Sent" %}</p>
|
|
{% else %}
|
|
<p class="text-[10px] text-slate-400 mt-0.5 italic">{% trans "Not sent" %}</p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="flex items-center gap-2 shrink-0">
|
|
<span class="text-[11px] font-bold
|
|
{% if dept.routing_status == 'rejected' %}text-red-600
|
|
{% elif not dept.response_submitted %}text-slate-400
|
|
{% elif dept.acceptance_status == 'acceptable' %}text-green-600
|
|
{% elif dept.acceptance_status == 'not_acceptable' %}text-red-600
|
|
{% else %}text-amber-600{% endif %}">
|
|
{% if dept.routing_status == 'rejected' %}{% trans "Wrong Department" %}
|
|
{% elif not dept.response_submitted %}{% trans "No response" %}
|
|
{% elif dept.acceptance_status == 'acceptable' %}{% trans "Accepted" %}
|
|
{% elif dept.acceptance_status == 'not_acceptable' %}{% trans "Rejected" %}
|
|
{% else %}{% trans "Pending" %}{% endif %}
|
|
</span>
|
|
{% if can_manage_actions and complaint.is_active_status %}
|
|
<form method="post" action="{% url 'complaints:involved_department_remove' pk=dept.pk %}" class="inline" data-on:submit="confirm('{% trans "Remove this department from the complaint?" %}') && @post('{% url 'complaints:involved_department_remove' pk=dept.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<button type="submit" title="{% trans 'Remove' %}" class="text-slate-300 hover:text-red-500 transition p-1">
|
|
<i data-lucide="x" class="w-3.5 h-3.5"></i>
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-sm text-slate-400 italic">{% trans "No departments involved yet" %}</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Involved Staff (read-only) -->
|
|
<section class="bg-white rounded-2xl p-4 shadow-sm border border-slate-100">
|
|
<h4 class="text-sm font-bold text-navy uppercase tracking-wide mb-3">{% trans "Involved Staff" %}</h4>
|
|
{% if complaint.involved_staff.exists %}
|
|
<ul class="divide-y divide-slate-100 border border-slate-100 rounded-xl overflow-hidden">
|
|
{% for staff_inv in complaint.involved_staff.all %}
|
|
<li class="flex items-center justify-between p-3 gap-3">
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
<div class="w-7 h-7 bg-light rounded-full flex items-center justify-center shrink-0"><i data-lucide="user" class="w-3.5 h-3.5 text-navy"></i></div>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-semibold text-navy truncate">{{ staff_inv.staff.get_localized_name }}</p>
|
|
{% if staff_inv.staff.department %}<p class="text-[11px] text-slate-500 truncate">{{ staff_inv.staff.department.get_localized_name }}</p>{% endif %}
|
|
</div>
|
|
</div>
|
|
<span class="text-[11px] font-bold shrink-0 {% if staff_inv.explanation_received %}text-green-600{% elif staff_inv.explanation_requested %}text-amber-600{% else %}text-slate-400{% endif %}">
|
|
{% if staff_inv.explanation_received %}{% trans "Responded" %}{% elif staff_inv.explanation_requested %}{% trans "Pending" %}{% else %}—{% endif %}
|
|
</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-sm text-slate-400 italic">{% trans "No staff members involved yet" %}</p>
|
|
{% endif %}
|
|
</section>
|
|
</div>
|
|
|
|
{% if can_manage_actions and complaint.is_active_status %}
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" onclick="openSendToDeptModal()"
|
|
class="flex-1 px-4 py-2.5 bg-navy text-white font-bold text-sm rounded-xl flex items-center justify-center gap-2 hover:bg-blue transition">
|
|
<i data-lucide="send" class="w-4 h-4"></i> {% trans "Send to Department" %}
|
|
</button>
|
|
<a href="{% url 'complaints:complaint_pdf' pk=complaint.id %}" target="_blank"
|
|
class="px-3 py-2.5 border border-slate-200 rounded-xl text-slate hover:border-navy hover:text-navy transition flex items-center gap-2 text-sm font-semibold">
|
|
<i data-lucide="file-text" class="w-4 h-4"></i>
|
|
{% trans "PDF" %}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% include "complaints/partials/workflow_timeline.html" %}
|
|
</div>
|
|
|
|
<!-- Timeline Tab -->
|
|
<div id="panel-timeline" class="tab-panel hidden">
|
|
{% include "complaints/partials/timeline_panel.html" %}
|
|
</div>
|
|
|
|
<!-- Attachments Tab -->
|
|
<div id="panel-attachments" class="tab-panel hidden">
|
|
{% include "complaints/partials/attachments_panel.html" %}
|
|
</div>
|
|
|
|
<!-- Actions & RCA Tab -->
|
|
<div id="panel-actions" class="tab-panel hidden space-y-6">
|
|
{% include "complaints/partials/actions_panel.html" %}
|
|
{% include "complaints/partials/rca_panel.html" %}
|
|
</div>
|
|
|
|
<!-- AI Analysis Tab -->
|
|
<div id="panel-ai" class="tab-panel hidden">
|
|
{% include "complaints/partials/ai_panel.html" %}
|
|
</div>
|
|
|
|
<!-- Resolution & PDF Tab -->
|
|
<div id="panel-resolution" class="tab-panel hidden">
|
|
{% include "complaints/partials/resolution_panel.html" %}
|
|
</div>
|
|
|
|
<!-- Adverse Actions Tab -->
|
|
<div id="panel-adverse" class="tab-panel hidden">
|
|
{% include "complaints/partials/adverse_actions_panel.html" %}
|
|
</div>
|
|
|
|
<div id="panel-notes" class="tab-panel hidden">
|
|
{% include "partials/notes_panel_ds.html" %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column (Sidebar) -->
|
|
<div class="col-span-4 space-y-6">
|
|
|
|
<!-- Quick Actions -->
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
|
|
<h3 class="font-bold text-navy mb-4 text-sm">{% trans "Quick Actions" %}</h3>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
{% if can_edit and complaint.is_active_status %}
|
|
{% if can_manage_actions or complaint.assigned_to == current_user %}
|
|
<button onclick="showAssignModal()" class="col-span-2 p-2 border border-blue-200 bg-blue-50 rounded-xl hover:bg-blue-100 flex items-center justify-center gap-2 group transition mb-2">
|
|
<i data-lucide="user-plus" class="w-4 h-4 text-blue"></i>
|
|
<span class="text-[10px] font-bold text-blue uppercase">
|
|
{% if complaint.assigned_to %}{% trans "Reassign" %}{% else %}{% trans "Assign" %}{% endif %}
|
|
</span>
|
|
</button>
|
|
{% endif %}
|
|
<!-- Activate Button - Always show when can edit and status is active -->
|
|
<form method="post" action="{% url 'complaints:complaint_activate' pk=complaint.pk %}" class="contents" data-on:submit="@post('{% url 'complaints:complaint_activate' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<button type="submit"
|
|
class="p-3 border-blue-200 bg-blue-50 rounded-xl hover:bg-blue-100 flex flex-col items-center gap-2 group transition col-span-2 mb-2"
|
|
{% if complaint.assigned_to == current_user %}disabled style="opacity: 0.5; cursor: not-allowed;"{% endif %}>
|
|
<i data-lucide="{% if complaint.assigned_to == current_user %}check-circle{% else %}play-circle{% endif %}" class="w-5 h-5 text-blue"></i>
|
|
<span class="text-[10px] font-bold text-blue uppercase">
|
|
{% if complaint.assigned_to == current_user %}{% trans "Activated" %}{% else %}{% trans "Activate" %}{% endif %}
|
|
</span>
|
|
</button>
|
|
</form>
|
|
|
|
{% if complaint.assigned_to == current_user or can_manage_actions %}
|
|
<!-- Action buttons only shown when activated or PX-team -->
|
|
<button onclick="showResolveModal()" class="p-3 border rounded-xl hover:bg-light flex flex-col items-center gap-2 group transition">
|
|
<i data-lucide="check-circle-2" class="w-5 h-5 text-slate group-hover:text-green-600"></i>
|
|
<span class="text-[10px] font-bold uppercase">{% trans "Resolve" %}</span>
|
|
</button>
|
|
<button onclick="showFollowUpModal()" class="p-3 border rounded-xl hover:bg-light flex flex-col items-center gap-2 group transition">
|
|
<i data-lucide="clock" class="w-5 h-5 text-slate group-hover:text-orange-500"></i>
|
|
<span class="text-[10px] font-bold uppercase">{% trans "Follow Up" %}</span>
|
|
</button>
|
|
<button onclick="showEscalateModal()" class="p-3 border-red-100 bg-red-50 rounded-xl hover:bg-red-100 flex flex-col items-center gap-2 group transition">
|
|
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-500"></i>
|
|
<span class="text-[10px] font-bold text-red-600 uppercase">{% trans "Escalate" %}</span>
|
|
</button>
|
|
<form method="post" action="{% url 'complaints:toggle_escalated_ovr' pk=complaint.pk %}" class="contents" data-on:submit="@post('{% url 'complaints:toggle_escalated_ovr' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<button type="submit" name="is_escalated_ovr" value="{% if not complaint.is_escalated_ovr %}on{% endif %}"
|
|
class="p-3 border rounded-xl hover:bg-light flex flex-col items-center gap-2 group transition
|
|
{% if complaint.is_escalated_ovr %}border-orange-200 bg-orange-50{% endif %}">
|
|
<i data-lucide="shield-alert" class="w-5 h-5 {% if complaint.is_escalated_ovr %}text-orange-600{% else %}text-slate group-hover:text-orange-500{% endif %}"></i>
|
|
<span class="text-[10px] font-bold uppercase {% if complaint.is_escalated_ovr %}text-orange-700{% endif %}">
|
|
{% if complaint.is_escalated_ovr %}{% trans "Remove OVR" %}{% else %}{% trans "OVR Escalate" %}{% endif %}
|
|
</span>
|
|
</button>
|
|
</form>
|
|
<!-- Close Button -->
|
|
<button onclick="showCloseModal()" class="p-3 border rounded-xl hover:bg-light flex flex-col items-center gap-2 group transition">
|
|
<i data-lucide="circle-check" class="w-5 h-5 text-slate-600 group-hover:text-emerald-600"></i>
|
|
<span class="text-[10px] font-bold uppercase text-slate-600">{% trans "Close" %}</span>
|
|
</button>
|
|
{% else %}
|
|
<!-- Not yet activated: show Cancel for admins -->
|
|
{% if can_manage_actions %}
|
|
<form method="post" action="{% url 'complaints:complaint_change_status' pk=complaint.pk %}" class="col-span-2 contents" data-on:submit="@post('{% url 'complaints:complaint_change_status' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="status" value="cancelled">
|
|
<button type="submit" onclick="return confirm('{% trans "Are you sure you want to cancel this complaint?" %}')"
|
|
class="p-3 border-red-200 bg-red-50 rounded-xl hover:bg-red-100 flex flex-col items-center gap-2 group transition col-span-2">
|
|
<i data-lucide="x-circle" class="w-5 h-5 text-red-500"></i>
|
|
<span class="text-[10px] font-bold text-red-600 uppercase">{% trans "Cancel Complaint" %}</span>
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<div class="col-span-2 bg-yellow-50 border border-yellow-200 rounded-xl p-3 text-center">
|
|
<i data-lucide="lock" class="w-4 h-4 text-yellow-600 mx-auto mb-1"></i>
|
|
<p class="text-xs text-yellow-700">{% trans "Activate this complaint to perform actions" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% elif complaint.status == 'resolved' or complaint.status == 'closed' %}
|
|
{% if can_edit %}
|
|
<form method="post" action="{% url 'complaints:complaint_reopen' pk=complaint.pk %}" class="col-span-2 contents" data-on:submit="@post('{% url 'complaints:complaint_reopen' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="note" value="Complaint reopened">
|
|
<button type="submit"
|
|
class="p-3 border-amber-200 bg-amber-50 rounded-xl hover:bg-amber-100 flex flex-col items-center gap-2 group transition col-span-2">
|
|
<i data-lucide="rotate-ccw" class="w-5 h-5 text-amber-600"></i>
|
|
<span class="text-[10px] font-bold text-amber-700 uppercase">{% trans "Reopen" %}</span>
|
|
</button>
|
|
</form>
|
|
{% if current_user.is_px_admin or current_user.is_hospital_admin or complaint.assigned_to == current_user %}
|
|
<button onclick="showAssignModal()" class="col-span-2 p-2 border border-blue-200 bg-blue-50 rounded-xl hover:bg-blue-100 flex items-center justify-center gap-2 group transition">
|
|
<i data-lucide="user-plus" class="w-4 h-4 text-blue"></i>
|
|
<span class="text-[10px] font-bold text-blue uppercase">{% trans "Reassign" %}</span>
|
|
</button>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="col-span-2 text-center py-4 text-slate text-sm">
|
|
{% trans "No actions available for this status" %}
|
|
</div>
|
|
{% endif %}
|
|
{% if current_user.is_px_admin or current_user.is_hospital_admin %}
|
|
<div class="col-span-2 border-t border-gray-100 pt-3 mt-2">
|
|
<form method="post" action="{% url 'complaints:complaint_soft_delete' pk=complaint.pk %}" id="delete-form" data-on:submit="@post('{% url 'complaints:complaint_soft_delete' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<button type="submit" onclick="return confirm('{% trans "Are you sure you want to delete this complaint? It can be restored from the trash." %}')"
|
|
class="w-full p-2 border border-red-200 bg-red-50 rounded-xl hover:bg-red-100 flex items-center justify-center gap-2 transition">
|
|
<i data-lucide="trash-2" class="w-4 h-4 text-red-500"></i>
|
|
<span class="text-xs font-bold text-red-600 uppercase">{% trans "Delete" %}</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Resolve Modal -->
|
|
<div id="resolveModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
|
<div class="bg-white rounded-2xl p-6 w-full max-w-lg mx-4 shadow-2xl">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="check-circle" class="w-5 h-5 text-green-600"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Resolve Complaint" %}</h3>
|
|
</div>
|
|
<p class="text-slate mb-4 text-sm">{% trans "Please provide resolution details before marking this complaint as resolved." %}</p>
|
|
<form method="post" action="{% url 'complaints:complaint_change_status' pk=complaint.pk %}" data-on:submit="@post('{% url 'complaints:complaint_change_status' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="status" value="resolved">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Resolution Notes" %} <span class="text-red-500">*</span></label>
|
|
<textarea name="resolution" rows="4" class="w-full border border-slate-200 rounded-xl p-4 text-sm focus:ring-2 focus:ring-navy/20 outline-none" placeholder="{% trans 'Enter resolution details...' %}" required></textarea>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('resolveModal')" class="flex-1 px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-green-500 text-white rounded-xl font-semibold hover:bg-green-600 transition flex items-center justify-center gap-2">
|
|
<i data-lucide="check-circle" class="w-4 h-4"></i> {% trans "Mark Resolved" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assign Modal -->
|
|
<div id="assignModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
|
<div class="bg-white rounded-2xl p-6 w-full max-w-lg mx-4 shadow-2xl">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="user-plus" class="w-5 h-5 text-blue"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">
|
|
{% if complaint.assigned_to %}{% trans "Reassign Complaint" %}{% else %}{% trans "Assign Complaint" %}{% endif %}
|
|
</h3>
|
|
</div>
|
|
<p class="text-slate mb-4 text-sm">{% trans "Select a user to assign this complaint to." %}</p>
|
|
<form method="post" action="{% url 'complaints:complaint_assign' pk=complaint.pk %}" data-on:submit="@post('{% url 'complaints:complaint_assign' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Assign To" %} <span class="text-red-500">*</span></label>
|
|
<select name="user_id" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" required>
|
|
<option value="">{% trans "Select User" %}</option>
|
|
{% for user in assignable_users %}
|
|
<option value="{{ user.id }}">{{ user.get_full_name }} {% if user.department %}({{ user.department.name }}){% endif %}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('assignModal')" class="flex-1 px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition flex items-center justify-center gap-2">
|
|
<i data-lucide="user-plus" class="w-4 h-4"></i> {% trans "Assign" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Follow Up Modal -->
|
|
<div id="followUpModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
|
<div class="bg-white rounded-2xl p-6 w-full max-w-lg mx-4 shadow-2xl">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="clock" class="w-5 h-5 text-orange-500"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Add Follow Up Note" %}</h3>
|
|
</div>
|
|
<p class="text-slate mb-4 text-sm">{% trans "Add a note or update about this complaint." %}</p>
|
|
<form method="post" action="{% url 'complaints:complaint_add_note' pk=complaint.pk %}" data-on:submit="@post('{% url 'complaints:complaint_add_note' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Note" %} <span class="text-red-500">*</span></label>
|
|
<textarea name="note" rows="4" class="w-full border border-slate-200 rounded-xl p-4 text-sm focus:ring-2 focus:ring-navy/20 outline-none" placeholder="{% trans 'Enter your note...' %}" required></textarea>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('followUpModal')" class="flex-1 px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-orange-500 text-white rounded-xl font-semibold hover:bg-orange-600 transition flex items-center justify-center gap-2">
|
|
<i data-lucide="plus" class="w-4 h-4"></i> {% trans "Add Note" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Escalate Modal -->
|
|
<div id="escalateModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
|
<div class="bg-white rounded-2xl p-6 w-full max-w-2xl mx-4 shadow-2xl max-h-[90vh] overflow-y-auto">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-500"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Escalate Complaint" %}</h3>
|
|
</div>
|
|
<form method="post" action="{% url 'complaints:complaint_escalate' pk=complaint.pk %}" data-on:submit="@post('{% url 'complaints:complaint_escalate' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Escalate To" %} <span class="text-red-500">*</span></label>
|
|
<select name="escalate_to" onchange="document.getElementById('escalateSubmitBtn').disabled = !this.value" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" required>
|
|
<option value="">{% trans "Select Person" %}</option>
|
|
{% regroup escalation_targets by group as target_groups %}
|
|
{% for group in target_groups %}
|
|
{% if group.grouper == 'department_roles' %}
|
|
<optgroup label="{% trans 'Department Roles' %}">
|
|
{% else %}
|
|
<optgroup label="{% trans 'Managers & Admins' %}">
|
|
{% endif %}
|
|
{% for target in group.list %}
|
|
<option value="{{ target.staff.id }}" {% if target.staff.id|stringformat:"s" == default_escalation_target %}selected{% endif %}>
|
|
{{ target.staff.get_full_name }}{% if target.role_label %} ({{ target.role_label }}){% elif target.is_line_manager %} ({% trans "Line Manager" %}){% elif target.is_manager %} ({% trans "Manager" %}){% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</optgroup>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="border-t border-slate-100 pt-4 mb-4">
|
|
<p class="text-xs font-bold text-slate uppercase mb-3">{% trans "Email Preview" %} <span class="text-slate-400 font-normal normal-case">({% trans "editable" %})</span></p>
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-semibold text-slate mb-1">{% trans "Subject" %}</label>
|
|
<input type="text" name="email_subject" value="{{ escalation_email_subject }}"
|
|
class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate mb-1">{% trans "Body" %}</label>
|
|
<textarea name="email_body" rows="12"
|
|
class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none font-mono whitespace-pre">{{ escalation_email_body }}</textarea>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('escalateModal')" class="flex-1 px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
<button type="submit" id="escalateSubmitBtn" disabled class="flex-1 px-4 py-2 bg-red-500 text-white rounded-xl font-semibold hover:bg-red-600 transition flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<i data-lucide="send" class="w-4 h-4"></i> {% trans "Send Escalation Email" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Close Modal -->
|
|
<div id="closeModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
|
<div class="bg-white rounded-2xl p-6 w-full max-w-lg mx-4 shadow-2xl">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-emerald-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="circle-check" class="w-5 h-5 text-emerald-600"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Close Complaint" %}</h3>
|
|
</div>
|
|
<p class="text-slate mb-4 text-sm">{% trans "Close this complaint. This will trigger a satisfaction survey." %}</p>
|
|
<form method="post" action="{% url 'complaints:complaint_change_status' pk=complaint.pk %}" data-on:submit="@post('{% url 'complaints:complaint_change_status' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="status" value="closed">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Closing Note (Optional)" %}</label>
|
|
<textarea name="note" rows="3" class="w-full border border-slate-200 rounded-xl p-4 text-sm focus:ring-2 focus:ring-navy/20 outline-none" placeholder="{% trans 'Enter closing note...' %}"></textarea>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('closeModal')" class="flex-1 px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-emerald-600 text-white rounded-xl font-semibold hover:bg-emerald-700 transition flex items-center justify-center gap-2">
|
|
<i data-lucide="circle-check" class="w-4 h-4"></i> {% trans "Close Complaint" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Location Edit Modal -->
|
|
<div id="locationModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
|
<div class="bg-white rounded-2xl p-6 w-full max-w-2xl mx-4 shadow-2xl max-h-[90vh] overflow-y-auto">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="map-pin" class="w-5 h-5 text-navy"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Edit Location Details" %}</h3>
|
|
</div>
|
|
<p class="text-slate mb-4 text-sm">{% trans "Update where the incident occurred. Leave optional fields blank to clear them." %}</p>
|
|
<form method="post" action="{% url 'complaints:complaint_update_location' pk=complaint.pk %}" id="locationEditForm" data-on:submit="@post('{% url 'complaints:complaint_update_location' pk=complaint.pk %}', {contentType:'form'})">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="hospital" value="{{ complaint.hospital.id }}" id="locationHospitalInput">
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Location Type" %} <span class="text-red-500">*</span></label>
|
|
<select name="location_type" id="locationTypeSelect" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select Location Type" %}</option>
|
|
<option value="OP" {% if complaint.location_type == 'OP' %}selected{% endif %}>{% trans "Outpatient" %}</option>
|
|
<option value="IP" {% if complaint.location_type == 'IP' %}selected{% endif %}>{% trans "Inpatient" %}</option>
|
|
<option value="ER" {% if complaint.location_type == 'ER' %}selected{% endif %}>{% trans "Emergency" %}</option>
|
|
<option value="GENERAL" {% if complaint.location_type == 'GENERAL' %}selected{% endif %}>{% trans "General" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Area" %}</label>
|
|
<select name="area" id="areaSelect" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" data-current="{{ complaint.area.id|default:'' }}">
|
|
{% if complaint.area %}
|
|
<option value="{{ complaint.area.id }}" selected>{% if LANG == 'ar' and complaint.area.name_ar %}{{ complaint.area.name_ar }}{% else %}{{ complaint.area.name_en }}{% endif %}</option>
|
|
{% else %}
|
|
<option value="">{% trans "Select Area" %}</option>
|
|
{% endif %}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Department Category" %}</label>
|
|
<select id="deptCategorySelect" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select Category" %}</option>
|
|
<option value="medical" {% if complaint.department.category == 'medical' %}selected{% endif %}>{% trans "Medical" %}</option>
|
|
<option value="non_medical" {% if complaint.department.category == 'non_medical' %}selected{% endif %}>{% trans "Non-Medical" %}</option>
|
|
<option value="nursing" {% if complaint.department.category == 'nursing' %}selected{% endif %}>{% trans "Nursing" %}</option>
|
|
<option value="administrative" {% if complaint.department.category == 'administrative' %}selected{% endif %}>{% trans "Administrative" %}</option>
|
|
<option value="support_services" {% if complaint.department.category == 'support_services' %}selected{% endif %}>{% trans "Support Services" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Department" %}</label>
|
|
<select name="department" id="departmentSelect" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" data-current="{{ complaint.department.id|default:'' }}">
|
|
{% if complaint.department %}
|
|
<option value="{{ complaint.department.id }}" selected>{{ complaint.department.get_localized_name }}</option>
|
|
{% else %}
|
|
<option value="">{% trans "Select Department" %}</option>
|
|
{% endif %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Section" %}</label>
|
|
<select name="section" id="sectionSelect" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" data-current="{{ complaint.section.id|default:'' }}">
|
|
{% if complaint.section %}
|
|
<option value="{{ complaint.section.id }}" selected>{{ complaint.section.get_localized_name }}</option>
|
|
{% else %}
|
|
<option value="">{% trans "Select Section" %}</option>
|
|
{% endif %}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Zone" %}</label>
|
|
<input type="text" name="zone" id="zoneInput" value="{{ complaint.zone|default:'' }}"
|
|
class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none"
|
|
placeholder="{% trans 'e.g. CENTER1-2, Gate 3' %}">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate mb-2">
|
|
{% trans "Floor" %}
|
|
<span class="text-[10px] font-normal text-slate/60 ml-1">{% trans "(auto-filled from department if empty)" %}</span>
|
|
</label>
|
|
<input type="text" name="floor" id="floorInput" value="{{ complaint.floor|default:'' }}"
|
|
class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none"
|
|
placeholder="{% trans 'e.g. GF, 1st Floor, Basement' %}"
|
|
data-default-from-dept="{% if complaint.department.floor %}{{ complaint.department.floor }}{% endif %}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('locationModal')" class="flex-1 px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition flex items-center justify-center gap-2">
|
|
<i data-lucide="save" class="w-4 h-4"></i> {% trans "Save Changes" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Send to Department modal (department + staff selects) -->
|
|
<div id="sendToDeptModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md mx-4">
|
|
<div class="flex items-center justify-between p-5 border-b border-slate-100">
|
|
<h3 class="text-lg font-bold text-navy flex items-center gap-2"><i data-lucide="send" class="w-5 h-5"></i> {% trans "Send to Department" %}</h3>
|
|
<button type="button" onclick="closeModal('sendToDeptModal')" class="p-1.5 rounded-lg hover:bg-slate-100 text-slate-400"><i data-lucide="x" class="w-5 h-5"></i></button>
|
|
</div>
|
|
<form id="sendToDeptForm" onsubmit="handleSendToDeptSubmit(event)">
|
|
{% csrf_token %}
|
|
<div class="p-5 space-y-4">
|
|
<div id="sendDeptError" class="hidden text-sm text-red-600 bg-red-50 border border-red-200 p-3 rounded-lg"></div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate-700 mb-1.5">{% trans "Department" %} <span class="text-red-500">*</span></label>
|
|
<select id="sendDeptDeptSelect" name="department_id" onchange="loadSendDeptStaff(this.value)"
|
|
class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" required>
|
|
<option value="">{% trans "Select Department" %}</option>
|
|
{% for dept in hospital_departments %}
|
|
<option value="{{ dept.id }}">{{ dept.get_localized_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate-700 mb-1.5">{% trans "Staff" %} <span class="text-slate-400 font-normal">({% trans "optional" %})</span></label>
|
|
<select id="sendDeptStaffSelect" name="staff_id"
|
|
class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select department first" %}</option>
|
|
</select>
|
|
<p class="text-xs text-slate-400 mt-1">{% trans "Records the staff as involved. The department champion and manager are notified." %}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate-700 mb-1.5">{% trans "Note" %} <span class="text-slate-400 font-normal">({% trans "optional" %})</span></label>
|
|
<textarea name="note" rows="2" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" placeholder="{% trans 'Add context or instructions...' %}"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="p-5 border-t border-slate-100 flex gap-3">
|
|
<button type="button" onclick="closeModal('sendToDeptModal')" class="flex-1 px-4 py-2.5 border border-slate-200 text-slate-600 rounded-xl font-semibold hover:bg-slate-50 transition">{% trans "Cancel" %}</button>
|
|
<button type="submit" id="sendDeptSubmitBtn" class="flex-1 px-4 py-2.5 bg-navy text-white rounded-xl font-bold hover:bg-blue transition flex items-center justify-center gap-2">
|
|
<i data-lucide="send" class="w-4 h-4"></i> {% trans "Send" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Switching JavaScript -->
|
|
<script>
|
|
function showActivationRequired() {
|
|
alert('{% trans "Please activate this complaint first to access this tab." %}');
|
|
}
|
|
|
|
function switchTab(tabName, scrollToId) {
|
|
// Hide all panels
|
|
document.querySelectorAll('.tab-panel').forEach(panel => {
|
|
panel.classList.add('hidden');
|
|
});
|
|
|
|
// Show selected panel
|
|
document.getElementById('panel-' + tabName).classList.remove('hidden');
|
|
|
|
// Update tab styles
|
|
document.querySelectorAll('main nav button').forEach(tab => {
|
|
tab.classList.remove('tab-active');
|
|
tab.classList.add('tab-inactive');
|
|
});
|
|
|
|
document.getElementById('tab-' + tabName).classList.remove('tab-inactive');
|
|
document.getElementById('tab-' + tabName).classList.add('tab-active');
|
|
|
|
// Reinitialize icons for newly visible content
|
|
if (window.lucide) {
|
|
lucide.createIcons();
|
|
}
|
|
|
|
// Optional: scroll to a specific element inside the newly shown panel
|
|
if (scrollToId) {
|
|
setTimeout(function() {
|
|
var el = document.getElementById(scrollToId);
|
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}, 50);
|
|
}
|
|
}
|
|
|
|
// Modal functions
|
|
function showResolveModal() {
|
|
document.getElementById('resolveModal').style.display = 'flex';
|
|
}
|
|
|
|
function showAssignModal() {
|
|
document.getElementById('assignModal').style.display = 'flex';
|
|
}
|
|
|
|
function showFollowUpModal() {
|
|
document.getElementById('followUpModal').style.display = 'flex';
|
|
}
|
|
|
|
function showEscalateModal() {
|
|
document.getElementById('escalateModal').style.display = 'flex';
|
|
}
|
|
|
|
function showCloseModal() {
|
|
document.getElementById('closeModal').style.display = 'flex';
|
|
}
|
|
|
|
function showLocationModal() {
|
|
document.getElementById('locationModal').style.display = 'flex';
|
|
populateLocationDropdowns();
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
var el = document.getElementById(modalId);
|
|
if (el) el.style.display = 'none';
|
|
}
|
|
|
|
// Close modal when clicking outside the dialog
|
|
window.onclick = function(event) {
|
|
if (event && event.target) {
|
|
var modals = ['resolveModal', 'assignModal', 'followUpModal', 'escalateModal', 'closeModal', 'locationModal', 'sendToDeptModal'];
|
|
if (modals.indexOf(event.target.id) !== -1) {
|
|
event.target.style.display = 'none';
|
|
}
|
|
}
|
|
};
|
|
|
|
// ─── Send to Department modal (department + staff selects) ───
|
|
function openSendToDeptModal() {
|
|
var m = document.getElementById('sendToDeptModal');
|
|
if (!m) return;
|
|
var f = document.getElementById('sendToDeptForm');
|
|
if (f) f.reset();
|
|
var staffSel = document.getElementById('sendDeptStaffSelect');
|
|
if (staffSel) staffSel.innerHTML = '<option value="">{% trans "Select department first" %}</option>';
|
|
var err = document.getElementById('sendDeptError');
|
|
if (err) { err.classList.add('hidden'); err.textContent = ''; }
|
|
m.style.display = 'flex';
|
|
if (window.lucide) lucide.createIcons();
|
|
}
|
|
|
|
function loadSendDeptStaff(deptId) {
|
|
var staffSel = document.getElementById('sendDeptStaffSelect');
|
|
if (!staffSel) return;
|
|
if (!deptId) {
|
|
staffSel.innerHTML = '<option value="">{% trans "Select department first" %}</option>';
|
|
return;
|
|
}
|
|
staffSel.innerHTML = '<option value="">{% trans "Loading..." %}</option>';
|
|
fetch('/organizations/dropdowns/department-staff/' + encodeURIComponent(deptId) + '/')
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (items) {
|
|
var html = '<option value="">{% trans "Select staff (optional)" %}</option>';
|
|
(items || []).forEach(function (it) {
|
|
var id = it.staff_id || (it.staff && it.staff.id);
|
|
var name = it.name || (it.staff && it.staff.name) || id;
|
|
if (id) html += '<option value="' + id + '">' + name + '</option>';
|
|
});
|
|
staffSel.innerHTML = html;
|
|
})
|
|
.catch(function () {
|
|
staffSel.innerHTML = '<option value="">{% trans "Select staff (optional)" %}</option>';
|
|
});
|
|
}
|
|
|
|
function handleSendToDeptSubmit(event) {
|
|
event.preventDefault();
|
|
var deptSelect = document.getElementById('sendDeptDeptSelect');
|
|
var deptId = deptSelect.value;
|
|
var staffSel = document.getElementById('sendDeptStaffSelect');
|
|
var err = document.getElementById('sendDeptError');
|
|
var btn = document.getElementById('sendDeptSubmitBtn');
|
|
|
|
if (!deptId) {
|
|
if (err) { err.textContent = '{% trans "Please select a department." %}'; err.classList.remove('hidden'); }
|
|
return;
|
|
}
|
|
if (err) err.classList.add('hidden');
|
|
|
|
var deptName = deptSelect.selectedOptions[0] ? deptSelect.selectedOptions[0].textContent.trim() : '';
|
|
var confirmMsg = '{% trans "Send this complaint to" %} ' + deptName + '?\n{% trans "The department champion and manager will be notified." %}';
|
|
if (!confirm(confirmMsg)) return;
|
|
|
|
var formData = new FormData(event.target);
|
|
formData.set('recipient_type', 'department');
|
|
if (!formData.get('department_id')) formData.set('department_id', deptId);
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="inline-block w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></span>{% trans "Sending..." %}';
|
|
|
|
fetch('{% url "complaints:complaint_send_to" pk=complaint.pk %}', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-CSRFToken': formData.get('csrfmiddlewaretoken') || ''
|
|
}
|
|
})
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
if (data.success) {
|
|
window.location.reload();
|
|
} else {
|
|
throw new Error(data.error || '{% trans "Failed to send." %}');
|
|
}
|
|
})
|
|
.catch(function (e) {
|
|
if (err) { err.textContent = e.message; err.classList.remove('hidden'); }
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i data-lucide="send" class="w-4 h-4"></i> {% trans "Send" %}';
|
|
if (window.lucide) lucide.createIcons();
|
|
});
|
|
}
|
|
|
|
// Location modal dependent dropdowns
|
|
function populateLocationDropdowns() {
|
|
var hospitalId = document.getElementById('locationHospitalInput').value;
|
|
var locationType = document.getElementById('locationTypeSelect').value;
|
|
var category = document.getElementById('deptCategorySelect').value;
|
|
loadLocationAreas(hospitalId, locationType);
|
|
loadLocationDepartments(hospitalId, category);
|
|
}
|
|
|
|
function loadLocationAreas(hospitalId, locationType) {
|
|
var sel = document.getElementById('areaSelect');
|
|
if (!sel || !hospitalId) return;
|
|
var current = sel.dataset.current || sel.value;
|
|
var url = '/organizations/dropdowns/areas/?hospital=' + encodeURIComponent(hospitalId);
|
|
if (locationType) url += '&location_type=' + encodeURIComponent(locationType);
|
|
fetch(url)
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (items) {
|
|
sel.innerHTML = '<option value="">{% trans "Select Area" %}</option>';
|
|
(items || []).forEach(function (it) {
|
|
var opt = document.createElement('option');
|
|
opt.value = it.id;
|
|
opt.textContent = ('{{ LANG }}' === 'ar' && it.name_ar) ? it.name_ar : (it.name_en || it.name_ar || it.id);
|
|
if (String(it.id) === String(current)) opt.selected = true;
|
|
sel.appendChild(opt);
|
|
});
|
|
sel.dataset.current = '';
|
|
})
|
|
.catch(function (err) { console.error('Error loading areas:', err); });
|
|
}
|
|
|
|
function loadLocationDepartments(hospitalId, category) {
|
|
var sel = document.getElementById('departmentSelect');
|
|
if (!sel || !hospitalId) return;
|
|
var current = sel.dataset.current || sel.value;
|
|
var url = '/organizations/dropdowns/departments-by-category/?hospital=' + encodeURIComponent(hospitalId);
|
|
if (category) url += '&category=' + encodeURIComponent(category);
|
|
fetch(url)
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (items) {
|
|
sel.innerHTML = '<option value="">{% trans "Select Department" %}</option>';
|
|
(items || []).forEach(function (it) {
|
|
var opt = document.createElement('option');
|
|
opt.value = it.id;
|
|
opt.textContent = ('{{ LANG }}' === 'ar' && it.name_ar) ? it.name_ar : (it.name_en || it.name || it.id);
|
|
if (it.floor) opt.setAttribute('data-floor', it.floor);
|
|
if (String(it.id) === String(current)) opt.selected = true;
|
|
sel.appendChild(opt);
|
|
});
|
|
sel.dataset.current = '';
|
|
if (sel.value) {
|
|
loadLocationSections(sel.value);
|
|
applyDepartmentFloorDefault();
|
|
}
|
|
})
|
|
.catch(function (err) { console.error('Error loading departments:', err); });
|
|
}
|
|
|
|
function loadLocationSections(deptId) {
|
|
var sel = document.getElementById('sectionSelect');
|
|
if (!sel) return;
|
|
if (!deptId) {
|
|
sel.innerHTML = '<option value="">{% trans "Select Section" %}</option>';
|
|
return;
|
|
}
|
|
var current = sel.dataset.current || sel.value;
|
|
fetch('/organizations/dropdowns/sections/' + encodeURIComponent(deptId) + '/')
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (items) {
|
|
sel.innerHTML = '<option value="">{% trans "Select Section" %}</option>';
|
|
(items || []).forEach(function (it) {
|
|
var opt = document.createElement('option');
|
|
opt.value = it.id;
|
|
opt.textContent = ('{{ LANG }}' === 'ar' && it.name_ar) ? it.name_ar : (it.name_en || it.name_ar || it.id);
|
|
if (String(it.id) === String(current)) opt.selected = true;
|
|
sel.appendChild(opt);
|
|
});
|
|
sel.dataset.current = '';
|
|
})
|
|
.catch(function (err) { console.error('Error loading sections:', err); });
|
|
}
|
|
|
|
function applyDepartmentFloorDefault() {
|
|
var deptSel = document.getElementById('departmentSelect');
|
|
var floorInput = document.getElementById('floorInput');
|
|
if (!deptSel || !floorInput) return;
|
|
if (floorInput.value.trim()) return; // don't overwrite a user-entered value
|
|
var selected = deptSel.options[deptSel.selectedIndex];
|
|
if (!selected) return;
|
|
var deptFloor = selected.getAttribute('data-floor') || floorInput.getAttribute('data-default-from-dept') || '';
|
|
if (deptFloor) floorInput.value = deptFloor;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var locTypeSel = document.getElementById('locationTypeSelect');
|
|
var catSel = document.getElementById('deptCategorySelect');
|
|
var deptSel = document.getElementById('departmentSelect');
|
|
var hospInput = document.getElementById('locationHospitalInput');
|
|
|
|
if (locTypeSel) {
|
|
locTypeSel.addEventListener('change', function () {
|
|
loadLocationAreas(hospInput.value, this.value);
|
|
});
|
|
}
|
|
if (catSel) {
|
|
catSel.addEventListener('change', function () {
|
|
var secSel = document.getElementById('sectionSelect');
|
|
if (secSel) secSel.innerHTML = '<option value="">{% trans "Select Section" %}</option>';
|
|
loadLocationDepartments(hospInput.value, this.value);
|
|
});
|
|
}
|
|
if (deptSel) {
|
|
deptSel.addEventListener('change', function () {
|
|
loadLocationSections(this.value);
|
|
applyDepartmentFloorDefault();
|
|
});
|
|
}
|
|
});
|
|
|
|
// SLA Countdown Timer
|
|
(function() {
|
|
var el = document.getElementById('sla-countdown');
|
|
if (!el || !el.dataset.dueAt) return;
|
|
|
|
var dueAt = new Date(el.dataset.dueAt).getTime();
|
|
var isOverdue = el.dataset.overdue === 'true';
|
|
|
|
function updateCountdown() {
|
|
var now = Date.now();
|
|
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 by" %} ';
|
|
if (overD > 0) txt += overD + 'd ';
|
|
txt += overH + 'h ' + overM + 'm';
|
|
el.textContent = txt;
|
|
el.className = 'text-xs font-bold mt-1 text-red-600 animate-pulse';
|
|
return;
|
|
}
|
|
|
|
var d = Math.floor(diff / 86400000);
|
|
var h = Math.floor((diff % 86400000) / 3600000);
|
|
var m = Math.floor((diff % 3600000) / 60000);
|
|
var s = Math.floor((diff % 60000) / 1000);
|
|
|
|
var parts = [];
|
|
if (d > 0) parts.push(d + 'd');
|
|
parts.push(h + 'h');
|
|
parts.push(m + 'm');
|
|
parts.push(s + 's');
|
|
el.textContent = parts.join(' ') + ' {% trans "remaining" %}';
|
|
|
|
if (diff < 21600000) {
|
|
el.className = 'text-xs font-bold mt-1 text-red-600';
|
|
} else if (diff < 86400000) {
|
|
el.className = 'text-xs font-bold mt-1 text-orange-600';
|
|
} else {
|
|
el.className = 'text-xs font-bold mt-1 text-green-600';
|
|
}
|
|
}
|
|
|
|
updateCountdown();
|
|
setInterval(updateCountdown, 1000);
|
|
})();
|
|
</script>
|
|
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// Re-render Lucide icons after Datastar morphs new content into the DOM.
|
|
(function () {
|
|
function reinit() { if (window.lucide) { try { lucide.createIcons(); } catch (e) {} } }
|
|
document.addEventListener('datastar-fetch', function (e) {
|
|
if (e && e.detail && e.detail.type === 'finished') reinit();
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|