1378 lines
81 KiB
HTML
1378 lines
81 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
{% load survey_filters %}
|
|
|
|
{% block title %}{{ department.get_localized_name }} - {% trans "Department" %} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.page-header-gradient {
|
|
background: linear-gradient(135deg, #005696 0%, #0069a8 50%, #007bbd 100%);
|
|
color: white; padding: 1.5rem 2rem; border-radius: 1rem; margin-bottom: 1.5rem;
|
|
box-shadow: 0 10px 15px -3px rgba(0, 86, 150, 0.2);
|
|
}
|
|
.stat-card {
|
|
background: white; border-radius: 1rem; border: 2px solid #e2e8f0; padding: 1.25rem;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: all 0.3s ease;
|
|
}
|
|
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 8px 15px rgba(0,0,0,0.1); border-color: #005696; }
|
|
.tab-btn { padding: 0.75rem 1.5rem; font-size: 0.875rem; font-weight: 600; border-bottom: 3px solid transparent; transition: all 0.2s; }
|
|
.tab-btn.active { color: #005696; border-bottom-color: #005696; }
|
|
.tab-btn:not(.active) { color: #94a3b8; }
|
|
.tab-btn:not(.active):hover { color: #005696; background: #f8fafc; }
|
|
.table-row-hover { transition: all 0.2s ease; border-left: 3px solid transparent; }
|
|
.table-row-hover:hover { background: #f8fafc; border-left-color: #007bbd; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="p-6">
|
|
<!-- Breadcrumb -->
|
|
<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>
|
|
<span class="text-navy font-semibold">{{ department.get_localized_name }}</span>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="page-header-gradient">
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center">
|
|
<i data-lucide="building-2" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-2xl font-bold">{{ department.get_localized_name }}</h1>
|
|
{% if department.name_ar %}
|
|
<p class="text-white/70 text-sm">{{ department.name_ar }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-4 text-sm text-white/80 mt-3">
|
|
<span class="flex items-center gap-1"><i data-lucide="hash" class="w-4 h-4"></i> {{ department.code }}</span>
|
|
{% if department.category %}
|
|
<span class="flex items-center gap-1 px-2 py-0.5 bg-white/20 rounded-full text-xs font-bold uppercase">
|
|
{% if department.category == 'nursing' %}<i data-lucide="heart-pulse" class="w-3 h-3"></i>
|
|
{% elif department.category == 'support_services' %}<i data-lucide="wrench" class="w-3 h-3"></i>
|
|
{% elif department.category == 'medical' %}<i data-lucide="stethoscope" class="w-3 h-3"></i>
|
|
{% elif department.category == 'non_medical' %}<i data-lucide="briefcase" class="w-3 h-3"></i>
|
|
{% endif %}
|
|
{{ department.get_category_display }}
|
|
</span>
|
|
{% endif %}
|
|
<span class="flex items-center gap-1"><i data-lucide="hospital" class="w-4 h-4"></i> {{ department.hospital.name }}</span>
|
|
{% if department.location %}
|
|
<span class="flex items-center gap-1"><i data-lucide="map-pin" class="w-4 h-4"></i> {{ department.location }}</span>
|
|
{% endif %}
|
|
{% if department.phone %}
|
|
<span class="flex items-center gap-1"><i data-lucide="phone" class="w-4 h-4"></i> {{ department.phone }}</span>
|
|
{% endif %}
|
|
{% if department.email %}
|
|
<span class="flex items-center gap-1"><i data-lucide="mail" class="w-4 h-4"></i> {{ department.email }}</span>
|
|
{% endif %}
|
|
{% if department.parent %}
|
|
<span class="flex items-center gap-1"><i data-lucide="git-branch" class="w-4 h-4"></i> {% trans "Sub of" %} {{ department.parent.get_localized_name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="text-right">
|
|
<p class="text-white/60 text-xs uppercase tracking-wide">{% trans "Manager" %}</p>
|
|
{% if department.manager %}
|
|
<p class="font-bold">{{ department.manager.get_full_name }}</p>
|
|
{% else %}
|
|
<p class="text-white/50 italic">{% trans "Not assigned" %}</p>
|
|
{% endif %}
|
|
</div>
|
|
{% if department.manager %}
|
|
<div class="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
|
|
<i data-lucide="user" class="w-6 h-6"></i>
|
|
</div>
|
|
{% elif can_edit %}
|
|
<button onclick="document.getElementById('managerModal').classList.remove('hidden')"
|
|
class="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center hover:bg-white/30 transition" title="{% trans 'Set Manager' %}">
|
|
<i data-lucide="user-plus" class="w-6 h-6"></i>
|
|
</button>
|
|
{% endif %}
|
|
{% if can_edit %}
|
|
<button onclick="document.getElementById('managerModal').classList.remove('hidden')"
|
|
class="text-white/60 hover:text-white text-xs underline transition">
|
|
{% if department.manager %}{% trans "Change" %}{% else %}{% trans "Set" %}{% endif %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="text-right">
|
|
<p class="text-white/60 text-xs uppercase tracking-wide">{% trans "Champion" %}</p>
|
|
{% if department.respondent %}
|
|
<p class="font-bold">{{ department.respondent.get_full_name }}</p>
|
|
{% else %}
|
|
<p class="text-white/50 italic">{% trans "Not set" %}</p>
|
|
{% endif %}
|
|
</div>
|
|
{% if department.respondent %}
|
|
<div class="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
|
|
<i data-lucide="headphones" class="w-6 h-6"></i>
|
|
</div>
|
|
{% elif can_assign %}
|
|
<button onclick="document.getElementById('respondentModal').classList.remove('hidden')"
|
|
class="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center hover:bg-white/30 transition" title="{% trans 'Set Champion' %}">
|
|
<i data-lucide="user-plus" class="w-6 h-6"></i>
|
|
</button>
|
|
{% endif %}
|
|
{% if can_assign %}
|
|
<button onclick="document.getElementById('respondentModal').classList.remove('hidden')"
|
|
class="text-white/60 hover:text-white text-xs underline transition">
|
|
{% if department.respondent %}{% trans "Change" %}{% else %}{% trans "Set" %}{% endif %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
{% if can_edit %}
|
|
<a href="{% url 'organizations:department_update' department.pk %}"
|
|
class="flex items-center gap-2 px-4 py-2 bg-white/20 hover:bg-white/30 rounded-xl text-sm font-semibold transition whitespace-nowrap">
|
|
<i data-lucide="pencil" class="w-4 h-4"></i> {% trans "Edit Department" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
<div class="stat-card">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-purple-50 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="users" class="w-5 h-5 text-purple-600"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold">{% trans "Staff" %}</p>
|
|
<p class="text-xl font-bold text-navy">{{ stats.staff_count }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-red-50 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="file-text" class="w-5 h-5 text-red-600"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold">{% trans "Open Complaints" %}</p>
|
|
<p class="text-xl font-bold text-red-600">{{ stats.open_complaints }}</p>
|
|
<p class="text-[10px] text-slate-400">{{ stats.total_complaints }} {% trans "total" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-amber-50 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="help-circle" class="w-5 h-5 text-amber-600"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold">{% trans "Pending Inquiries" %}</p>
|
|
<p class="text-xl font-bold text-amber-600">{{ stats.pending_inquiries }}</p>
|
|
<p class="text-[10px] text-slate-400">{{ stats.total_inquiries }} {% trans "total" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="eye" class="w-5 h-5 text-blue"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold">{% trans "Open Observations" %}</p>
|
|
<p class="text-xl font-bold text-blue">{{ stats.open_observations }}</p>
|
|
<p class="text-[10px] text-slate-400">{{ stats.total_observations }} {% trans "total" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if pending_actions %}
|
|
<div class="bg-white rounded-xl shadow-sm border-2 border-red-200 p-5 mb-6">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-red-50 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="alert-circle" class="w-5 h-5 text-red-600"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-bold text-navy">{% trans "Pending Actions" %}</h3>
|
|
<p class="text-xs text-slate-400">{{ pending_actions_count }} {% trans "items awaiting response" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-slate-50">
|
|
<tr>
|
|
<th class="text-left px-4 py-2 text-xs font-bold text-slate-500 uppercase">{% trans "Type" %}</th>
|
|
<th class="text-left px-4 py-2 text-xs font-bold text-slate-500 uppercase">{% trans "Reference" %}</th>
|
|
<th class="text-left px-4 py-2 text-xs font-bold text-slate-500 uppercase">{% trans "Subject" %}</th>
|
|
<th class="text-center px-4 py-2 text-xs font-bold text-slate-500 uppercase">{% trans "SLA Deadline" %}</th>
|
|
<th class="text-center px-4 py-2 text-xs font-bold text-slate-500 uppercase">{% trans "Action" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for action in pending_actions %}
|
|
<tr class="hover:bg-slate-50 transition">
|
|
<td class="px-4 py-2">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-bold bg-{{ action.badge_color }}-100 text-{{ action.badge_color }}-700">
|
|
{{ action.type_label }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-2 font-mono text-xs">{{ action.reference }}</td>
|
|
<td class="px-4 py-2 text-slate-600">{{ action.subject|truncatechars:50 }}</td>
|
|
<td class="px-4 py-2 text-center">
|
|
{% if action.sla_due_at %}
|
|
{% if action.is_overdue %}
|
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-bold bg-red-100 text-red-700">
|
|
<i data-lucide="alert-triangle" class="w-3 h-3"></i>
|
|
{{ action.sla_due_at|date:"M d, H:i" }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-xs text-slate-500">{{ action.sla_due_at|date:"M d, H:i" }}</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="text-xs text-slate-300">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-2 text-center">
|
|
{% if action.type == 'complaint_department_response' %}
|
|
<button type="button"
|
|
onclick="openResponseModal('{{ action.item_id }}', '{{ action.reference }}', '{{ action.subject|escapejs }}', '{{ action.complaint_id }}')"
|
|
class="inline-flex items-center gap-1 px-3 py-1 bg-navy text-white rounded-lg text-xs font-bold hover:bg-blue transition">
|
|
<i data-lucide="message-square" class="w-3 h-3"></i>
|
|
{% trans "Respond" %}
|
|
</button>
|
|
{% else %}
|
|
<a href="{{ action.url }}"
|
|
class="inline-flex items-center gap-1 px-3 py-1 bg-navy text-white rounded-lg text-xs font-bold hover:bg-blue transition">
|
|
<i data-lucide="external-link" class="w-3 h-3"></i>
|
|
{% trans "Respond" %}
|
|
</a>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Department Head Info -->
|
|
{% if staff_head %}
|
|
<div class="bg-gradient-to-r from-navy/5 to-blue/5 border border-navy/10 rounded-2xl p-5 mb-6 flex items-center gap-4">
|
|
<div class="w-14 h-14 bg-navy rounded-full flex items-center justify-center text-white font-bold text-lg">
|
|
{{ staff_head.first_name|truncatechars:1 }}{{ staff_head.last_name|truncatechars:1 }}
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-bold text-navy">{{ staff_head.get_localized_name }}</span>
|
|
<span class="px-2 py-0.5 bg-navy text-white text-[10px] font-bold rounded-full uppercase">{% trans "Department Head" %}</span>
|
|
</div>
|
|
<p class="text-sm text-slate">{{ staff_head.get_localized_job_title }} · {{ staff_head.employee_id }}</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Tabs -->
|
|
<div class="bg-white rounded-t-2xl border border-slate-200 border-b-0">
|
|
<div class="flex border-b border-slate-200 px-4">
|
|
<button class="tab-btn {% if active_tab == 'analytics' %}active{% endif %}" onclick="switchDeptTab('analytics', this)">
|
|
<i data-lucide="bar-chart-3" class="w-4 h-4 inline mr-1"></i> {% trans "Dashboard" %}
|
|
</button>
|
|
{% if not user.is_department_respondent or user.is_px_admin or user.is_hospital_admin or user.is_department_manager %}
|
|
<button class="tab-btn {% if active_tab == 'staff' %}active{% endif %}" onclick="switchDeptTab('staff', this)">
|
|
<i data-lucide="users" class="w-4 h-4 inline mr-1"></i> {% trans "Staff" %} ({{ stats.staff_count }})
|
|
</button>
|
|
{% endif %}
|
|
<button class="tab-btn {% if active_tab == 'complaints' %}active{% endif %}" onclick="switchDeptTab('complaints', this)">
|
|
<i data-lucide="file-text" class="w-4 h-4 inline mr-1"></i> {% trans "Complaints" %} ({{ stats.total_complaints }})
|
|
</button>
|
|
<button class="tab-btn {% if active_tab == 'inquiries' %}active{% endif %}" onclick="switchDeptTab('inquiries', this)">
|
|
<i data-lucide="help-circle" class="w-4 h-4 inline mr-1"></i> {% trans "Inquiries" %} ({{ stats.total_inquiries }})
|
|
</button>
|
|
<button class="tab-btn {% if active_tab == 'observations' %}active{% endif %}" onclick="switchDeptTab('observations', this)">
|
|
<i data-lucide="eye" class="w-4 h-4 inline mr-1"></i> {% trans "Observations" %} ({{ stats.total_observations }})
|
|
</button>
|
|
<button class="tab-btn {% if active_tab == 'standards' %}active{% endif %}" onclick="switchDeptTab('standards', this)">
|
|
<i data-lucide="clipboard-list" class="w-4 h-4 inline mr-1"></i> {% trans "Standards" %} ({{ standards_data|length }})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search & Filter Bar -->
|
|
<form method="get" id="filterForm" class="bg-white border-x border-slate-200 px-4 py-3">
|
|
<input type="hidden" name="tab" id="filterTabInput" value="{{ active_tab }}">
|
|
<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...' %}"
|
|
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>
|
|
<div id="filter-complaints" class="hidden">
|
|
<select name="complaint_status" onchange="document.getElementById('filterForm').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 Complaints" %}</option>
|
|
<option value="open" {% if complaint_status_filter == 'open' %}selected{% endif %}>{% trans "Open" %}</option>
|
|
<option value="in_progress" {% if complaint_status_filter == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option>
|
|
<option value="resolved" {% if complaint_status_filter == 'resolved' %}selected{% endif %}>{% trans "Resolved" %}</option>
|
|
<option value="closed" {% if complaint_status_filter == 'closed' %}selected{% endif %}>{% trans "Closed" %}</option>
|
|
</select>
|
|
</div>
|
|
<div id="filter-inquiries" class="hidden">
|
|
<select name="inquiry_status" onchange="document.getElementById('filterForm').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 Inquiries" %}</option>
|
|
<option value="open" {% if inquiry_status_filter == 'open' %}selected{% endif %}>{% trans "Open" %}</option>
|
|
<option value="in_progress" {% if inquiry_status_filter == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option>
|
|
<option value="resolved" {% if inquiry_status_filter == 'resolved' %}selected{% endif %}>{% trans "Resolved" %}</option>
|
|
<option value="closed" {% if inquiry_status_filter == 'closed' %}selected{% endif %}>{% trans "Closed" %}</option>
|
|
</select>
|
|
</div>
|
|
<div id="filter-observations" class="hidden">
|
|
<select name="observation_status" onchange="document.getElementById('filterForm').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 Observations" %}</option>
|
|
<option value="new" {% if observation_status_filter == 'new' %}selected{% endif %}>{% trans "New" %}</option>
|
|
<option value="triaged" {% if observation_status_filter == 'triaged' %}selected{% endif %}>{% trans "Triaged" %}</option>
|
|
<option value="in_progress" {% if observation_status_filter == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option>
|
|
<option value="resolved" {% if observation_status_filter == 'resolved' %}selected{% endif %}>{% trans "Resolved" %}</option>
|
|
<option value="closed" {% if observation_status_filter == 'closed' %}selected{% endif %}>{% trans "Closed" %}</option>
|
|
</select>
|
|
</div>
|
|
{% if search_query or complaint_status_filter or inquiry_status_filter or observation_status_filter %}
|
|
<a href="?tab={{ active_tab }}" 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>
|
|
|
|
<!-- Tab Panels -->
|
|
<div class="bg-white rounded-b-2xl border border-slate-200 border-t-0 p-6">
|
|
|
|
{% if not user.is_department_respondent or user.is_px_admin or user.is_hospital_admin or user.is_department_manager %}
|
|
<!-- Staff Tab -->
|
|
<div id="panel-staff" class="tab-panel {% if active_tab != 'staff' %}hidden{% endif %}">
|
|
<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 "Name" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Job Title" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Employee ID" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{{ department.get_category_display }}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "User Account" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Complaints" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for s in staff_list %}
|
|
<tr class="table-row-hover">
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-9 h-9 bg-navy/10 rounded-full flex items-center justify-center text-navy font-bold text-xs">
|
|
{{ s.first_name|truncatechars:1 }}{{ s.last_name|truncatechars:1 }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy text-sm">{{ s.get_localized_name }}</span>
|
|
{% if s.is_head %}
|
|
<span class="ml-1 px-1.5 py-0.5 bg-navy text-white text-[9px] font-bold rounded-full">{% trans "HEAD" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate-700">{{ s.get_localized_job_title }}</td>
|
|
<td class="px-4 py-3 text-sm font-mono text-slate">{{ s.employee_id }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="px-2 py-1 bg-slate-100 text-slate-700 rounded text-xs font-semibold">{{ s.get_department_type_display }}</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate">
|
|
{% if s.user %}
|
|
{% for g in s.user.groups.all %}
|
|
<span class="inline-block px-1.5 py-0.5 bg-blue-50 text-blue rounded text-[10px] font-bold mr-1">{{ g.name }}</span>
|
|
{% endfor %}
|
|
{% else %}
|
|
<span class="text-slate-400 text-xs">{% trans "No account" %}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
{% with count=staff_complaint_counts|get_item:s.pk %}
|
|
{% if count %}
|
|
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full {% if count > 5 %}bg-red-50 text-red-600{% elif count > 2 %}bg-amber-50 text-amber-700{% else %}bg-slate-100 text-slate-600{% endif %} text-xs font-bold">{{ count }}</span>
|
|
{% else %}
|
|
<span class="text-slate-400 text-xs">0</span>
|
|
{% endif %}
|
|
{% endwith %}
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="6" class="px-4 py-8 text-center text-slate">{% trans "No staff members in this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Complaints Tab -->
|
|
<div id="panel-complaints" class="tab-panel {% if active_tab != 'complaints' %}hidden{% endif %}">
|
|
<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 "Created" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for c in complaints %}
|
|
<tr class="table-row-hover cursor-pointer" onclick="window.location='{% url 'complaints:complaint_detail' 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 %}
|
|
{% if c.classification_obj %}<span class="px-1.5 py-0.5 bg-slate-100 text-slate-700 rounded text-[10px] font-semibold">{{ c.classification_obj.get_localized_name }}</span>{% endif %}
|
|
{% if not c.domain and not c.category and not c.subcategory_obj and not c.classification_obj %}<span class="text-slate-400">-</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 text-xs text-slate">{{ c.created_at|date:"Y-m-d" }}</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="7" class="px-4 py-8 text-center text-slate">{% trans "No complaints for this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Inquiries Tab (visible to everyone including respondents) -->
|
|
<div id="panel-inquiries" class="tab-panel {% if active_tab != 'inquiries' %}hidden{% endif %}">
|
|
{% if user.is_department_respondent and not user.is_px_admin and not user.is_hospital_admin and not user.is_department_manager %}
|
|
<div class="space-y-4">
|
|
{% for i in inquiries %}
|
|
<div class="border border-slate-200 rounded-xl p-5">
|
|
<p class="text-slate-800 text-sm whitespace-pre-wrap">{{ i.message }}</p>
|
|
{% if i.status in 'open,in_progress' %}
|
|
<div class="mt-4 pt-3 border-t border-slate-100">
|
|
<a href="{% url 'inquiries:inquiry_department_response' pk=i.pk %}" class="inline-flex items-center gap-2 px-4 py-2 bg-navy text-white rounded-lg text-sm font-semibold hover:bg-blue transition">
|
|
<i data-lucide="message-square" class="w-4 h-4"></i> {% trans "Respond" %}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-12 text-slate">
|
|
<i data-lucide="check-circle" class="w-12 h-12 mx-auto mb-3 text-green-300"></i>
|
|
<p class="font-semibold">{% trans "No pending inquiries" %}</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<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 "Subject" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Contact" %}</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 "Actions" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for i in inquiries %}
|
|
<tr class="table-row-hover">
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ i.reference_number }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate-700 max-w-xs truncate">{{ i.subject }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ i.contact_name|default:"-" }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ i.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 i.status == 'open' %}bg-blue-50 text-blue-700
|
|
{% elif i.status == 'in_progress' %}bg-amber-50 text-amber-700
|
|
{% elif i.status == 'resolved' %}bg-green-50 text-green-700
|
|
{% else %}bg-slate-50 text-slate-500{% endif %}">
|
|
{{ i.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center gap-2">
|
|
<a href="{% url 'inquiries:inquiry_detail' pk=i.pk %}" class="text-xs text-navy font-semibold hover:underline">
|
|
{% trans "View" %}
|
|
</a>
|
|
{% if can_respond and i.status in 'open,in_progress' %}
|
|
<a href="{% url 'inquiries:inquiry_department_response' pk=i.pk %}" class="text-xs text-green-600 font-semibold hover:underline">
|
|
{% trans "Respond" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="6" class="px-4 py-8 text-center text-slate">{% trans "No inquiries for this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Observations Tab -->
|
|
<div id="panel-observations" class="tab-panel {% if active_tab != 'observations' %}hidden{% endif %}">
|
|
<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 "Tracking Code" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Title" %}</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-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 "Created" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for o in observations %}
|
|
<tr class="table-row-hover cursor-pointer" onclick="window.location='{% url 'observations:observation_detail' pk=o.pk %}'">
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ o.tracking_code }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate-700 max-w-xs truncate">{{ o.title|default:"-" }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="px-2 py-1 rounded-full text-[10px] font-bold uppercase
|
|
{% if o.severity == 'critical' %}bg-gray-800 text-white
|
|
{% elif o.severity == 'high' %}bg-red-50 text-red-700
|
|
{% elif o.severity == 'medium' %}bg-amber-50 text-amber-700
|
|
{% else %}bg-green-50 text-green-700{% endif %}">
|
|
{{ o.get_severity_display }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ o.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 bg-slate-50 text-slate-600">
|
|
{{ o.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-slate">{{ o.created_at|date:"Y-m-d" }}</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="6" class="px-4 py-8 text-center text-slate">{% trans "No observations for this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Analytics Tab -->
|
|
<div id="panel-analytics" class="tab-panel {% if active_tab != 'analytics' %}hidden{% endif %}">
|
|
<div id="analyticsLoading" class="flex items-center justify-center py-20">
|
|
<div class="flex flex-col items-center gap-3">
|
|
<div class="w-8 h-8 border-3 border-navy border-t-transparent rounded-full animate-spin"></div>
|
|
<p class="text-sm text-slate-400">{% trans "Loading analytics..." %}</p>
|
|
</div>
|
|
</div>
|
|
<div id="analyticsContent" class="hidden">
|
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 p-4 border-b border-slate-100">
|
|
<div class="bg-gradient-to-br from-indigo-50 to-white rounded-xl p-4 border border-indigo-100">
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Avg Physician Rating" %}</p>
|
|
<p id="kpi-avg-rating" class="text-2xl font-bold text-indigo-600">-</p>
|
|
<p class="text-[10px] text-slate-400 mt-1">{% trans "out of 5.0" %}</p>
|
|
</div>
|
|
<div class="bg-gradient-to-br from-green-50 to-white rounded-xl p-4 border border-green-100">
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Resolution Rate" %}</p>
|
|
<p id="kpi-resolution-rate" class="text-2xl font-bold text-green-600">-</p>
|
|
<p id="kpi-total-complaints" class="text-[10px] text-slate-400 mt-1"></p>
|
|
</div>
|
|
<div class="bg-gradient-to-br from-amber-50 to-white rounded-xl p-4 border border-amber-100">
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Open Actions" %}</p>
|
|
<p id="kpi-open-actions" class="text-2xl font-bold text-amber-600">-</p>
|
|
<p class="text-[10px] text-slate-400 mt-1">{% trans "PX actions pending" %}</p>
|
|
</div>
|
|
<div class="bg-gradient-to-br from-purple-50 to-white rounded-xl p-4 border border-purple-100">
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Satisfaction Rate" %}</p>
|
|
<p id="kpi-satisfaction-rate" class="text-2xl font-bold text-purple-600">-</p>
|
|
<p class="text-[10px] text-slate-400 mt-1">{% trans "resolution satisfaction" %}</p>
|
|
</div>
|
|
<div class="bg-gradient-to-br from-orange-50 to-white rounded-xl p-4 border border-orange-100">
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Reopened" %}</p>
|
|
<p id="kpi-reopened" class="text-2xl font-bold text-orange-600">-</p>
|
|
<p class="text-[10px] text-slate-400 mt-1">{% trans "reopened complaints" %}</p>
|
|
</div>
|
|
<div class="bg-gradient-to-br from-sky-50 to-white rounded-xl p-4 border border-sky-100">
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Reassigned" %}</p>
|
|
<p id="kpi-reassigned" class="text-2xl font-bold text-sky-600">-</p>
|
|
<p class="text-[10px] text-slate-400 mt-1">{% trans "reassigned complaints" %}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4">
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<h4 class="text-sm font-bold text-navy mb-3">{% trans "Complaint Trend" %}</h4>
|
|
<div id="chartComplaintTrend" style="min-height: 280px;"></div>
|
|
</div>
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<h4 class="text-sm font-bold text-navy mb-3">{% trans "Physician Rating Trend" %}</h4>
|
|
<div id="chartPhysicianRating" style="min-height: 280px;"></div>
|
|
</div>
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<h4 class="text-sm font-bold text-navy mb-3">{% trans "Complaint Status" %}</h4>
|
|
<div id="chartComplaintStatus" style="min-height: 280px;"></div>
|
|
</div>
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<h4 class="text-sm font-bold text-navy mb-3">{% trans "Actions Overview" %}</h4>
|
|
<div id="chartActionsOverview" style="min-height: 280px;"></div>
|
|
</div>
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<h4 class="text-sm font-bold text-navy mb-3">{% trans "Complaint Severity" %}</h4>
|
|
<div id="chartSeverity" style="min-height: 280px;"></div>
|
|
</div>
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<h4 class="text-sm font-bold text-navy mb-3">{% trans "Satisfaction Distribution" %}</h4>
|
|
<div id="chartSatisfaction" style="min-height: 280px;"></div>
|
|
</div>
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<h4 class="text-sm font-bold text-navy mb-3">{% trans "Top Physicians" %}</h4>
|
|
<div id="chartTopPhysicians" style="min-height: 280px;"></div>
|
|
</div>
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<h4 class="text-sm font-bold text-navy mb-3">{% trans "Top Categories" %}</h4>
|
|
<div id="chartTopCategories" style="min-height: 280px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Standards Tab -->
|
|
<div id="panel-standards" class="tab-panel {% if active_tab != 'standards' %}hidden{% endif %}">
|
|
<div class="overflow-x-auto">
|
|
<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 "Code" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Title" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Activity Type" %}</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 "Evidence" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for item in standards_data %}
|
|
<tr class="table-row-hover {% if not item.standard.is_assessable or item.standard.is_heading %}bg-slate-50{% endif %}" data-standard-id="{{ item.standard.id }}">
|
|
<td class="px-4 py-3 whitespace-nowrap">
|
|
<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-semibold {% if not item.standard.is_assessable or item.standard.is_heading %}bg-slate-100 text-slate-500{% else %}bg-navy/10 text-navy{% endif %}">
|
|
{{ item.standard.code }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate-700 max-w-xs truncate {% if not item.standard.is_assessable or item.standard.is_heading %}italic{% endif %}">
|
|
<a href="{% url 'standards:standard_detail' pk=item.standard.id %}" class="hover:text-navy transition">
|
|
{{ item.standard.title }}
|
|
</a>
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap">
|
|
{% if item.standard.activity_type %}
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-medium bg-purple-100 text-purple-700">
|
|
{{ item.standard.activity_type.name }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-xs text-slate-400">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap">
|
|
{% if not item.standard.is_assessable or item.standard.is_heading %}
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-medium bg-slate-100 text-slate-500">
|
|
{% trans "Informational" %}
|
|
</span>
|
|
{% else %}
|
|
<select class="compliance-status-select text-xs font-medium rounded-lg px-2 py-1 border border-slate-200 focus:ring-2 focus:ring-navy/20 outline-none cursor-pointer"
|
|
data-department-id="{{ department.pk }}"
|
|
data-standard-id="{{ item.standard.id }}"
|
|
onchange="updateComplianceStatus(this)">
|
|
<option value="not_assessed" {% if not item.compliance or item.compliance.status == 'not_assessed' %}selected{% endif %}>
|
|
{% trans "Not Assessed" %}
|
|
</option>
|
|
<option value="met" {% if item.compliance and item.compliance.status == 'met' %}selected{% endif %}>
|
|
{% trans "Met" %}
|
|
</option>
|
|
<option value="partially_met" {% if item.compliance and item.compliance.status == 'partially_met' %}selected{% endif %}>
|
|
{% trans "Partially Met" %}
|
|
</option>
|
|
<option value="not_met" {% if item.compliance and item.compliance.status == 'not_met' %}selected{% endif %}>
|
|
{% trans "Not Met" %}
|
|
</option>
|
|
</select>
|
|
<span class="compliance-status-indicator ml-1 hidden">
|
|
<i data-lucide="check-circle" class="w-3 h-3 text-green-500 inline"></i>
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap">
|
|
{% if not item.standard.is_assessable or item.standard.is_heading %}
|
|
<span class="text-xs text-slate-400">-</span>
|
|
{% else %}
|
|
<div class="flex items-center gap-2">
|
|
<a href="{% url 'standards:department_standards' pk=department.pk %}"
|
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 transition"
|
|
id="evidence-count-{{ item.standard.id }}">
|
|
<i data-lucide="file-text" class="w-3 h-3 mr-1"></i>
|
|
{% if item.compliance %}{{ item.attachment_count }}{% else %}0{% endif %}
|
|
</a>
|
|
<button onclick="openEvidenceModal('{{ item.standard.id }}', '{{ department.pk }}', '{{ item.standard.code|escapejs }}')"
|
|
class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-slate-100 text-slate-500 hover:bg-navy hover:text-white transition"
|
|
title="{% trans 'Upload evidence' %}">
|
|
<i data-lucide="upload" class="w-3.5 h-3.5"></i>
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="5" class="px-4 py-8 text-center text-slate">
|
|
<i data-lucide="inbox" class="w-8 h-8 mx-auto mb-2 text-slate-300"></i>
|
|
{% trans "No standards for this department" %}
|
|
</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Set Manager Modal -->
|
|
<div id="managerModal" class="fixed inset-0 bg-black/50 z-50 hidden 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-indigo-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="user-cog" class="w-5 h-5 text-indigo-600"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Set Department Manager" %}</h3>
|
|
</div>
|
|
<p class="text-slate mb-4 text-sm">{% trans "Select a staff member to be the manager of this department." %}</p>
|
|
<form method="post" action="{% url 'organizations:set_department_manager' pk=department.pk %}">
|
|
{% csrf_token %}
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Manager" %} <span class="text-red-500">*</span></label>
|
|
<select name="manager_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 Staff Member" %}</option>
|
|
{% for m in managers %}
|
|
<option value="{{ m.id }}" {% if department.manager and department.manager.id == m.user_id %}selected{% endif %}>
|
|
{{ m.get_full_name }}{% if m.job_title %} ({{ m.get_localized_job_title }}){% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="document.getElementById('managerModal').classList.add('hidden')" 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="check" class="w-4 h-4"></i> {% trans "Save" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Set Champion Modal -->
|
|
<div id="respondentModal" class="fixed inset-0 bg-black/50 z-50 hidden 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="headphones" class="w-5 h-5 text-blue"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Set Department Champion" %}</h3>
|
|
</div>
|
|
<p class="text-slate mb-4 text-sm">{% trans "Select a staff member to be the default champion for inquiries in this department." %}</p>
|
|
<form method="post" action="{% url 'organizations:set_department_respondent' pk=department.pk %}">
|
|
{% csrf_token %}
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Champion" %} <span class="text-red-500">*</span></label>
|
|
<select name="respondent_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 Staff Member" %}</option>
|
|
{% for s in assignable_staff %}
|
|
<option value="{{ s.id }}" {% if department.respondent and department.respondent.id == s.id %}selected{% endif %}>
|
|
{{ s.get_full_name }} ({{ s.get_localized_job_title }})
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="document.getElementById('respondentModal').classList.add('hidden')" 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="check" class="w-4 h-4"></i> {% trans "Save" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Evidence Upload Modal -->
|
|
<div id="evidenceUploadModal" class="fixed inset-0 bg-black/50 z-50 hidden 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 justify-between items-center mb-4">
|
|
<div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Upload Evidence" %}</h3>
|
|
<p class="text-sm text-slate-500 mt-1" id="evidenceModalStandard">{% trans "Standard: " %}<span class="font-medium"></span></p>
|
|
</div>
|
|
<button onclick="closeEvidenceModal()" class="text-slate-400 hover:text-slate-600 transition">
|
|
<i data-lucide="x" class="w-6 h-6"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="evidenceUploadForm" class="space-y-4">
|
|
<input type="hidden" id="evidenceStandardId" name="standard_id">
|
|
<input type="hidden" id="evidenceDepartmentId" name="department_id">
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "File" %}</label>
|
|
<input type="file" id="evidenceFile" name="file"
|
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.zip"
|
|
class="block w-full text-sm text-slate border border-slate-200 rounded-lg cursor-pointer bg-white focus:outline-none focus:ring-2 focus:ring-navy focus:border-navy p-2">
|
|
<p class="mt-1 text-xs text-slate-400">{% trans "Accepted: PDF, DOC, XLS, images, ZIP (max 50MB)" %}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Description" %}</label>
|
|
<textarea id="evidenceDescription" name="description" rows="3"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-navy focus:border-transparent transition"
|
|
placeholder="{% trans 'Add a description for this evidence...' %}"></textarea>
|
|
</div>
|
|
|
|
<div id="evidenceUploadError" class="hidden text-sm text-red-600 bg-red-50 border border-red-200 p-3 rounded-lg"></div>
|
|
|
|
<div class="flex gap-3 pt-2">
|
|
<button type="button" onclick="submitEvidenceUpload()"
|
|
class="flex-1 px-4 py-2.5 bg-green-500 text-white font-medium rounded-xl hover:bg-green-600 transition flex items-center justify-center gap-2">
|
|
<i data-lucide="upload" class="w-4 h-4"></i>
|
|
{% trans "Upload" %}
|
|
</button>
|
|
<button type="button" onclick="closeEvidenceModal()"
|
|
class="flex-1 px-4 py-2.5 bg-slate-100 text-slate-700 font-medium rounded-xl hover:bg-slate-200 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Complaint Department Response Modal -->
|
|
<div id="responseModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden">
|
|
<div class="bg-gradient-to-r from-navy to-blue p-4 flex justify-between items-center">
|
|
<h3 class="text-white font-bold text-lg flex items-center gap-2">
|
|
<i data-lucide="message-square" class="w-5 h-5"></i>
|
|
{% trans "Submit Department Response" %}
|
|
</h3>
|
|
<button onclick="closeResponseModal()" class="text-white/80 hover:text-white transition">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-6 space-y-4">
|
|
<div class="bg-slate-50 rounded-xl p-4 border border-slate-200">
|
|
<p class="text-xs text-slate-500 uppercase font-bold mb-1">{% trans "Reference" %}</p>
|
|
<p id="responseModalRef" class="font-mono text-sm font-bold text-navy"></p>
|
|
<p class="text-xs text-slate-500 uppercase font-bold mb-1 mt-3">{% trans "Subject" %}</p>
|
|
<p id="responseModalSubject" class="text-sm text-slate-700"></p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-slate-700 mb-2">
|
|
{% trans "Your Response" %} <span class="text-red-500">*</span>
|
|
</label>
|
|
<textarea id="responseNotes" rows="5"
|
|
class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl text-slate-700 focus:outline-none focus:border-navy focus:ring-2 focus:ring-navy/20 resize-none"
|
|
placeholder="{% trans 'Enter your department\'s response to this complaint...' %}"></textarea>
|
|
</div>
|
|
|
|
<div id="responseError" class="hidden text-sm text-red-600 bg-red-50 border border-red-200 p-3 rounded-lg"></div>
|
|
<div id="responseSuccess" class="hidden text-sm text-green-600 bg-green-50 border border-green-200 p-3 rounded-lg"></div>
|
|
|
|
<div class="flex gap-3 pt-2">
|
|
<button type="button" id="responseSubmitBtn" onclick="submitDepartmentResponse()"
|
|
class="flex-1 px-4 py-2.5 bg-navy text-white font-medium rounded-xl hover:bg-blue transition flex items-center justify-center gap-2">
|
|
<i data-lucide="send" class="w-4 h-4"></i>
|
|
{% trans "Submit Response" %}
|
|
</button>
|
|
<button type="button" onclick="closeResponseModal()"
|
|
class="flex-1 px-4 py-2.5 bg-slate-100 text-slate-700 font-medium rounded-xl hover:bg-slate-200 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const tabFilters = {
|
|
staff: [],
|
|
complaints: ['filter-complaints'],
|
|
inquiries: ['filter-inquiries'],
|
|
observations: ['filter-observations'],
|
|
standards: [],
|
|
analytics: [],
|
|
};
|
|
|
|
let analyticsLoaded = false;
|
|
const deptAnalyticsCharts = {};
|
|
|
|
function switchDeptTab(tab, btn) {
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden'));
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.getElementById('panel-' + tab).classList.remove('hidden');
|
|
if (btn) btn.classList.add('active');
|
|
document.getElementById('filterTabInput').value = tab;
|
|
|
|
Object.values(tabFilters).flat().forEach(id => { const el = document.getElementById(id); if (el) el.classList.add('hidden'); });
|
|
(tabFilters[tab] || []).forEach(id => { const el = document.getElementById(id); if (el) el.classList.remove('hidden'); });
|
|
|
|
document.getElementById('filterForm').style.display = (tab === 'analytics') ? 'none' : '';
|
|
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('tab', tab);
|
|
history.replaceState(null, '', url);
|
|
|
|
if (tab === 'analytics' && !analyticsLoaded) {
|
|
loadDepartmentAnalytics();
|
|
}
|
|
}
|
|
|
|
function updateComplianceStatus(selectEl) {
|
|
const departmentId = selectEl.dataset.departmentId;
|
|
const standardId = selectEl.dataset.standardId;
|
|
const status = selectEl.value;
|
|
const indicator = selectEl.nextElementSibling;
|
|
|
|
selectEl.disabled = true;
|
|
indicator.classList.remove('hidden');
|
|
|
|
fetch('/standards/api/compliance/update/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCookie('csrftoken') || document.querySelector('[name=csrfmiddlewaretoken]')?.value || '',
|
|
},
|
|
body: JSON.stringify({
|
|
department_id: departmentId,
|
|
standard_id: standardId,
|
|
status: status,
|
|
}),
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Update select styling based on status
|
|
selectEl.classList.remove('bg-green-50', 'text-green-700', 'border-green-300',
|
|
'bg-yellow-50', 'text-yellow-700', 'border-yellow-300',
|
|
'bg-red-50', 'text-red-700', 'border-red-300',
|
|
'bg-slate-50', 'text-slate-600', 'border-slate-200');
|
|
|
|
if (status === 'met') {
|
|
selectEl.classList.add('bg-green-50', 'text-green-700', 'border-green-300');
|
|
} else if (status === 'partially_met') {
|
|
selectEl.classList.add('bg-yellow-50', 'text-yellow-700', 'border-yellow-300');
|
|
} else if (status === 'not_met') {
|
|
selectEl.classList.add('bg-red-50', 'text-red-700', 'border-red-300');
|
|
} else {
|
|
selectEl.classList.add('bg-slate-50', 'text-slate-600', 'border-slate-200');
|
|
}
|
|
} else {
|
|
alert('Error: ' + (data.error || 'Unknown error'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error updating compliance:', error);
|
|
alert('Failed to update compliance status.');
|
|
})
|
|
.finally(() => {
|
|
selectEl.disabled = false;
|
|
setTimeout(() => indicator.classList.add('hidden'), 1500);
|
|
});
|
|
}
|
|
|
|
// Evidence upload modal functions
|
|
let currentEvidenceStandardId = null;
|
|
|
|
function openEvidenceModal(standardId, departmentId, standardCode) {
|
|
currentEvidenceStandardId = standardId;
|
|
document.getElementById('evidenceStandardId').value = standardId;
|
|
document.getElementById('evidenceDepartmentId').value = departmentId;
|
|
document.getElementById('evidenceModalStandard').querySelector('span').textContent = standardCode;
|
|
document.getElementById('evidenceUploadError').classList.add('hidden');
|
|
document.getElementById('evidenceUploadForm').reset();
|
|
document.getElementById('evidenceUploadModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeEvidenceModal() {
|
|
document.getElementById('evidenceUploadModal').classList.add('hidden');
|
|
currentEvidenceStandardId = null;
|
|
}
|
|
|
|
function submitEvidenceUpload() {
|
|
const fileInput = document.getElementById('evidenceFile');
|
|
const descriptionInput = document.getElementById('evidenceDescription');
|
|
const standardId = document.getElementById('evidenceStandardId').value;
|
|
const departmentId = document.getElementById('evidenceDepartmentId').value;
|
|
const errorDiv = document.getElementById('evidenceUploadError');
|
|
|
|
if (!fileInput.files || fileInput.files.length === 0) {
|
|
errorDiv.textContent = '{% trans "Please select a file" %}';
|
|
errorDiv.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
const file = fileInput.files[0];
|
|
const maxSize = 50 * 1024 * 1024; // 50MB
|
|
if (file.size > maxSize) {
|
|
errorDiv.textContent = '{% trans "File size must be less than 50MB" %}';
|
|
errorDiv.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
errorDiv.classList.add('hidden');
|
|
|
|
const formData = new FormData();
|
|
formData.append('standard_id', standardId);
|
|
formData.append('department_id', departmentId);
|
|
formData.append('file', file);
|
|
formData.append('description', descriptionInput.value);
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.addEventListener('load', function() {
|
|
if (xhr.status === 200) {
|
|
const response = JSON.parse(xhr.responseText);
|
|
if (response.success) {
|
|
// Update evidence count badge
|
|
const countBadge = document.getElementById('evidence-count-' + standardId);
|
|
if (countBadge) {
|
|
countBadge.innerHTML = '<i data-lucide="file-text" class="w-3 h-3 mr-1"></i>' + response.attachment_count;
|
|
}
|
|
closeEvidenceModal();
|
|
// Re-init lucide icons for the updated badge
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
} else {
|
|
errorDiv.textContent = response.error || '{% trans "Upload failed" %}';
|
|
errorDiv.classList.remove('hidden');
|
|
}
|
|
} else {
|
|
errorDiv.textContent = '{% trans "Upload failed" %}';
|
|
errorDiv.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('error', function() {
|
|
errorDiv.textContent = '{% trans "Upload failed" %}';
|
|
errorDiv.classList.remove('hidden');
|
|
});
|
|
|
|
xhr.open('POST', '{% url "standards:attachment_upload_ajax" %}', true);
|
|
xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken') || document.querySelector('[name=csrfmiddlewaretoken]')?.value || '');
|
|
xhr.send(formData);
|
|
}
|
|
|
|
// Close modal on outside click
|
|
document.addEventListener('click', function(e) {
|
|
const modal = document.getElementById('evidenceUploadModal');
|
|
if (e.target === modal) {
|
|
closeEvidenceModal();
|
|
}
|
|
});
|
|
|
|
// Apply initial styling to compliance selects
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('.compliance-status-select').forEach(function(select) {
|
|
const event = new Event('change', { bubbles: true });
|
|
select.dispatchEvent(event);
|
|
});
|
|
});
|
|
|
|
(function() {
|
|
const activeTab = '{{ active_tab }}';
|
|
(tabFilters[activeTab] || []).forEach(id => { const el = document.getElementById(id); if (el) el.classList.remove('hidden'); });
|
|
if (activeTab === 'analytics') {
|
|
document.getElementById('filterForm').style.display = 'none';
|
|
loadDepartmentAnalytics();
|
|
}
|
|
})();
|
|
|
|
function loadDepartmentAnalytics() {
|
|
fetch('{% url "organizations:department_analytics_api" pk=department.pk %}')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
document.getElementById('analyticsLoading').innerHTML =
|
|
'<p class="text-red-500 text-sm">' + data.error + '</p>';
|
|
return;
|
|
}
|
|
document.getElementById('analyticsLoading').classList.add('hidden');
|
|
document.getElementById('analyticsContent').classList.remove('hidden');
|
|
renderDeptKPIs(data.kpi || {});
|
|
renderDeptCharts(data);
|
|
analyticsLoaded = true;
|
|
})
|
|
.catch(err => {
|
|
document.getElementById('analyticsLoading').innerHTML =
|
|
'<p class="text-red-500 text-sm">Failed to load analytics</p>';
|
|
});
|
|
}
|
|
|
|
function renderDeptKPIs(kpi) {
|
|
document.getElementById('kpi-avg-rating').textContent = kpi.avg_physician_rating || '0.0';
|
|
document.getElementById('kpi-resolution-rate').textContent = (kpi.resolution_rate || 0) + '%';
|
|
document.getElementById('kpi-total-complaints').textContent = (kpi.total_complaints || 0) + ' {% trans "total" %}';
|
|
document.getElementById('kpi-open-actions').textContent = kpi.open_actions || 0;
|
|
document.getElementById('kpi-satisfaction-rate').textContent = (kpi.satisfaction_rate || 0) + '%';
|
|
document.getElementById('kpi-reopened').textContent = kpi.reopened || 0;
|
|
document.getElementById('kpi-reassigned').textContent = kpi.reassigned || 0;
|
|
}
|
|
|
|
function renderDeptCharts(data) {
|
|
const common = {
|
|
fontFamily: 'Inter, sans-serif',
|
|
toolbar: { show: false },
|
|
animations: { enabled: true },
|
|
};
|
|
|
|
// Complaint Trend (area)
|
|
if (data.complaint_trend && data.complaint_trend.length > 0) {
|
|
deptAnalyticsCharts.complaintTrend = new ApexCharts(document.querySelector("#chartComplaintTrend"), {
|
|
series: [{ name: '{% trans "Complaints" %}', data: data.complaint_trend.map(i => i.count) }],
|
|
chart: { type: 'area', height: 280, ...common },
|
|
colors: ['#005696'],
|
|
stroke: { curve: 'smooth', width: 3 },
|
|
fill: { type: 'gradient', gradient: { shadeIntensity: 1, opacityFrom: 0.4, opacityTo: 0.05, stops: [0, 100] } },
|
|
dataLabels: { enabled: true, style: { fontSize: '11px' } },
|
|
xaxis: { categories: data.complaint_trend.map(i => i.period), labels: { style: { fontSize: '11px' } } },
|
|
yaxis: { labels: { style: { fontSize: '11px' } } },
|
|
grid: { borderColor: '#e2e8f0', strokeDashArray: 4 },
|
|
});
|
|
deptAnalyticsCharts.complaintTrend.render();
|
|
} else {
|
|
document.querySelector("#chartComplaintTrend").innerHTML = '<p class="text-slate-400 text-sm text-center py-10">{% trans "No trend data available" %}</p>';
|
|
}
|
|
|
|
// Physician Rating Trend (area)
|
|
if (data.physician_rating_trend && data.physician_rating_trend.some(i => i.average_rating > 0)) {
|
|
deptAnalyticsCharts.physicianRating = new ApexCharts(document.querySelector("#chartPhysicianRating"), {
|
|
series: [{ name: '{% trans "Avg Rating" %}', data: data.physician_rating_trend.map(i => i.average_rating) }],
|
|
chart: { type: 'area', height: 280, ...common },
|
|
colors: ['#7c3aed'],
|
|
stroke: { curve: 'smooth', width: 3 },
|
|
fill: { type: 'gradient', gradient: { shadeIntensity: 1, opacityFrom: 0.4, opacityTo: 0.05, stops: [0, 100] } },
|
|
dataLabels: { enabled: true, style: { fontSize: '11px' } },
|
|
xaxis: { categories: data.physician_rating_trend.map(i => i.period), labels: { style: { fontSize: '11px' } } },
|
|
yaxis: { min: 0, max: 5, labels: { style: { fontSize: '11px' }, formatter: v => Math.round(v * 10) / 10 } },
|
|
grid: { borderColor: '#e2e8f0', strokeDashArray: 4 },
|
|
tooltip: { y: { formatter: v => Math.round(v * 10) / 10 } },
|
|
});
|
|
deptAnalyticsCharts.physicianRating.render();
|
|
} else {
|
|
document.querySelector("#chartPhysicianRating").innerHTML = '<p class="text-slate-400 text-sm text-center py-10">{% trans "No physician rating data" %}</p>';
|
|
}
|
|
|
|
// Complaint Status (donut)
|
|
if (data.complaint_status && data.complaint_status.length > 0) {
|
|
const statusColors = { 'Open': '#3b82f6', 'In Progress': '#f59e0b', 'Resolved': '#10b981', 'Closed': '#64748b', 'Partially Resolved': '#06b6d4', 'Cancelled': '#94a3b8', 'Contacted': '#8b5cf6', 'No Response': '#a1a1aa' };
|
|
deptAnalyticsCharts.complaintStatus = new ApexCharts(document.querySelector("#chartComplaintStatus"), {
|
|
series: data.complaint_status.map(i => i.count),
|
|
chart: { type: 'donut', height: 280, ...common },
|
|
labels: data.complaint_status.map(i => i.status),
|
|
colors: data.complaint_status.map(i => statusColors[i.status] || '#94a3b8'),
|
|
legend: { position: 'bottom', fontSize: '12px' },
|
|
dataLabels: { enabled: true, style: { fontSize: '11px' } },
|
|
});
|
|
deptAnalyticsCharts.complaintStatus.render();
|
|
} else {
|
|
document.querySelector("#chartComplaintStatus").innerHTML = '<p class="text-slate-400 text-sm text-center py-10">{% trans "No status data" %}</p>';
|
|
}
|
|
|
|
// Actions Overview (donut)
|
|
if (data.actions_overview && data.actions_overview.length > 0) {
|
|
const actionColors = { 'Open': '#3b82f6', 'In Progress': '#f59e0b', 'Pending Approval': '#8b5cf6', 'Approved': '#06b6d4', 'Closed': '#10b981', 'Cancelled': '#94a3b8' };
|
|
deptAnalyticsCharts.actionsOverview = new ApexCharts(document.querySelector("#chartActionsOverview"), {
|
|
series: data.actions_overview.map(i => i.count),
|
|
chart: { type: 'donut', height: 280, ...common },
|
|
labels: data.actions_overview.map(i => i.status),
|
|
colors: data.actions_overview.map(i => actionColors[i.status] || '#94a3b8'),
|
|
legend: { position: 'bottom', fontSize: '12px' },
|
|
dataLabels: { enabled: true, style: { fontSize: '11px' } },
|
|
});
|
|
deptAnalyticsCharts.actionsOverview.render();
|
|
} else {
|
|
document.querySelector("#chartActionsOverview").innerHTML = '<p class="text-slate-400 text-sm text-center py-10">{% trans "No actions data" %}</p>';
|
|
}
|
|
|
|
// Complaint Severity (bar)
|
|
if (data.complaint_severity && data.complaint_severity.length > 0) {
|
|
const sevColors = { 'Low': '#10b981', 'Medium': '#f59e0b', 'High': '#ef4444', 'Critical': '#7c2d12' };
|
|
deptAnalyticsCharts.severity = new ApexCharts(document.querySelector("#chartSeverity"), {
|
|
series: [{ name: '{% trans "Count" %}', data: data.complaint_severity.map(i => i.count) }],
|
|
chart: { type: 'bar', height: 280, ...common },
|
|
colors: data.complaint_severity.map(i => sevColors[i.severity] || '#64748b'),
|
|
plotOptions: { bar: { borderRadius: 6, distributed: true, columnWidth: '50%' } },
|
|
dataLabels: { enabled: true, style: { fontSize: '12px' } },
|
|
xaxis: { categories: data.complaint_severity.map(i => i.severity), labels: { style: { fontSize: '12px' } } },
|
|
yaxis: { labels: { style: { fontSize: '11px' } } },
|
|
grid: { borderColor: '#e2e8f0', strokeDashArray: 4 },
|
|
legend: { show: false },
|
|
});
|
|
deptAnalyticsCharts.severity.render();
|
|
} else {
|
|
document.querySelector("#chartSeverity").innerHTML = '<p class="text-slate-400 text-sm text-center py-10">{% trans "No severity data" %}</p>';
|
|
}
|
|
|
|
// Satisfaction Distribution (donut)
|
|
if (data.satisfaction && data.satisfaction.length > 0) {
|
|
const satColors = { 'Satisfied': '#10b981', 'Neutral': '#f59e0b', 'Dissatisfied': '#ef4444', 'No Response': '#94a3b8', 'Escalated': '#7c3aed' };
|
|
deptAnalyticsCharts.satisfaction = new ApexCharts(document.querySelector("#chartSatisfaction"), {
|
|
series: data.satisfaction.map(i => i.count),
|
|
chart: { type: 'donut', height: 280, ...common },
|
|
labels: data.satisfaction.map(i => i.label),
|
|
colors: data.satisfaction.map(i => satColors[i.label] || '#94a3b8'),
|
|
legend: { position: 'bottom', fontSize: '12px' },
|
|
dataLabels: { enabled: true, style: { fontSize: '11px' } },
|
|
});
|
|
deptAnalyticsCharts.satisfaction.render();
|
|
} else {
|
|
document.querySelector("#chartSatisfaction").innerHTML = '<p class="text-slate-400 text-sm text-center py-10">{% trans "No satisfaction data" %}</p>';
|
|
}
|
|
|
|
// Top Physicians (horizontal bar)
|
|
if (data.top_physicians && data.top_physicians.length > 0) {
|
|
deptAnalyticsCharts.topPhysicians = new ApexCharts(document.querySelector("#chartTopPhysicians"), {
|
|
series: [{ name: '{% trans "Rating" %}', data: data.top_physicians.map(i => i.rating) }],
|
|
chart: { type: 'bar', height: 280, ...common },
|
|
colors: ['#7c3aed'],
|
|
plotOptions: { bar: { borderRadius: 6, horizontal: true, barHeight: '60%' } },
|
|
dataLabels: { enabled: true, formatter: v => Math.round(v * 10) / 10, style: { fontSize: '11px' } },
|
|
xaxis: { categories: data.top_physicians.map(i => i.name), max: 5, labels: { style: { fontSize: '11px' } } },
|
|
grid: { borderColor: '#e2e8f0', strokeDashArray: 4 },
|
|
});
|
|
deptAnalyticsCharts.topPhysicians.render();
|
|
} else {
|
|
document.querySelector("#chartTopPhysicians").innerHTML = '<p class="text-slate-400 text-sm text-center py-10">{% trans "No physician data" %}</p>';
|
|
}
|
|
|
|
// Top Categories (horizontal bar)
|
|
if (data.top_categories && data.top_categories.length > 0) {
|
|
deptAnalyticsCharts.topCategories = new ApexCharts(document.querySelector("#chartTopCategories"), {
|
|
series: [{ name: '{% trans "Count" %}', data: data.top_categories.map(i => i.count) }],
|
|
chart: { type: 'bar', height: 280, ...common },
|
|
colors: ['#005696'],
|
|
plotOptions: { bar: { borderRadius: 6, horizontal: true, barHeight: '60%' } },
|
|
dataLabels: { enabled: true, style: { fontSize: '11px' } },
|
|
xaxis: { categories: data.top_categories.map(i => i.category), labels: { style: { fontSize: '11px' } } },
|
|
grid: { borderColor: '#e2e8f0', strokeDashArray: 4 },
|
|
});
|
|
deptAnalyticsCharts.topCategories.render();
|
|
} else {
|
|
document.querySelector("#chartTopCategories").innerHTML = '<p class="text-slate-400 text-sm text-center py-10">{% trans "No category data" %}</p>';
|
|
}
|
|
}
|
|
|
|
// Complaint Department Response Modal
|
|
let currentResponseItemId = null;
|
|
let currentResponseComplaintId = null;
|
|
|
|
function openResponseModal(itemId, reference, subject, complaintId) {
|
|
currentResponseItemId = itemId;
|
|
currentResponseComplaintId = complaintId;
|
|
document.getElementById('responseModalRef').textContent = reference;
|
|
document.getElementById('responseModalSubject').textContent = subject;
|
|
document.getElementById('responseNotes').value = '';
|
|
document.getElementById('responseError').classList.add('hidden');
|
|
document.getElementById('responseSuccess').classList.add('hidden');
|
|
document.getElementById('responseModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeResponseModal() {
|
|
document.getElementById('responseModal').classList.add('hidden');
|
|
currentResponseItemId = null;
|
|
currentResponseComplaintId = null;
|
|
}
|
|
|
|
function submitDepartmentResponse() {
|
|
const notes = document.getElementById('responseNotes').value.trim();
|
|
const errorDiv = document.getElementById('responseError');
|
|
const successDiv = document.getElementById('responseSuccess');
|
|
const submitBtn = document.getElementById('responseSubmitBtn');
|
|
|
|
if (!notes) {
|
|
errorDiv.textContent = '{% trans "Please enter your response" %}';
|
|
errorDiv.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
if (!currentResponseItemId) {
|
|
errorDiv.textContent = '{% trans "Invalid request" %}';
|
|
errorDiv.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
errorDiv.classList.add('hidden');
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<span class="spinner"></span> {% trans "Submitting..." %}';
|
|
|
|
const formData = new FormData();
|
|
formData.append('response_notes', notes);
|
|
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]')?.value || getCookie('csrftoken') || '');
|
|
|
|
fetch('/complaints/departments/' + currentResponseItemId + '/response/', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
return response.json();
|
|
}
|
|
throw new Error('Network response was not ok');
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
successDiv.textContent = '{% trans "Response submitted successfully!" %}';
|
|
successDiv.classList.remove('hidden');
|
|
setTimeout(() => {
|
|
closeResponseModal();
|
|
window.location.reload();
|
|
}, 1500);
|
|
} else {
|
|
throw new Error(data.error || '{% trans "Failed to submit response" %}');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
errorDiv.textContent = error.message;
|
|
errorDiv.classList.remove('hidden');
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = '{% trans "Submit Response" %}';
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|