Per the workflow realignment, the point of appreciation is recognition — managers should see the appreciations their department received alongside other feedback. Adds an 'Appreciations Received' card (amber theme) after the Pending Actions section, showing the count and the 10 most recent SENT appreciations. The card only renders when the department has at least one appreciation.
2178 lines
135 KiB
HTML
2178 lines
135 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;
|
|
text-decoration: none; color: inherit;
|
|
}
|
|
.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>
|
|
{% 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>
|
|
<a href="{% url 'organizations:department_complaints_list' department.pk %}?status=open" class="stat-card block">
|
|
<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>
|
|
</a>
|
|
<a href="{% url 'organizations:department_inquiries_list' department.pk %}?status=open" class="stat-card block">
|
|
<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>
|
|
</a>
|
|
<a href="{% url 'organizations:department_observations_list' department.pk %}?status=open" class="stat-card block">
|
|
<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>
|
|
</a>
|
|
</div>
|
|
|
|
{% if pending_actions and can_respond %}
|
|
<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">
|
|
<div class="flex items-center justify-center gap-1.5">
|
|
{% if action.type == 'complaint_department_response' %}
|
|
{% if action.explanation_url and action.explanation_url != '#' %}
|
|
<a href="{{ action.explanation_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="message-square" class="w-3 h-3"></i>
|
|
{% trans "Respond" %}
|
|
</a>
|
|
{% else %}
|
|
<button type="button"
|
|
onclick="openDeptResponseModal('complaint', '{{ action.item_id }}', '/complaints/departments/{{ action.item_id }}/response/', '{{ action.reference }}', '{{ action.subject|escapejs }}')"
|
|
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>
|
|
{% endif %}
|
|
{% if action.routing_inv_id %}
|
|
<button type="button" onclick="toggleRejectRouting('{{ action.routing_inv_id }}')"
|
|
class="inline-flex items-center gap-1 px-2 py-1 border-2 border-red-200 text-red-600 rounded-lg text-xs font-semibold hover:border-red-400 hover:bg-red-50 transition">
|
|
<i data-lucide="log-out" class="w-3 h-3"></i> {% trans "Wrong Dept" %}
|
|
</button>
|
|
{% endif %}
|
|
{% elif action.type == 'observation_response' %}
|
|
<button type="button"
|
|
onclick="openDeptResponseModal('observation', '{{ action.item_id }}', '{{ action.url }}', '{{ action.reference }}', '{{ action.subject|escapejs }}', '{{ action.existing_en|escapejs }}', '{{ action.existing_ar|escapejs }}')"
|
|
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>
|
|
{% elif action.type == 'inquiry_response' %}
|
|
<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="hand" class="w-3 h-3"></i>
|
|
{% trans "Handle" %}
|
|
</a>
|
|
{% 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 %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if pending_actions_has_more %}
|
|
<div class="mt-3 pt-3 border-t border-slate-100 text-center">
|
|
<span class="text-xs text-slate-400">{% trans "Showing 10 of" %} {{ pending_actions_count }} — {% trans "respond to items to see more" %}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if dept_appreciations_count > 0 %}
|
|
<!-- Appreciations received by this department (recognition) -->
|
|
<div class="bg-gradient-to-br from-amber-50 to-yellow-50 border border-amber-200 rounded-2xl p-5 my-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center">
|
|
<i data-lucide="award" class="w-4 h-4 text-amber-600"></i>
|
|
</div>
|
|
<h3 class="text-sm font-bold text-navy">{% trans "Appreciations Received" %}</h3>
|
|
</div>
|
|
<span class="text-2xl font-bold text-amber-600">{{ dept_appreciations_count }}</span>
|
|
</div>
|
|
<ul class="space-y-2">
|
|
{% for apr in dept_appreciations %}
|
|
<li class="text-xs text-slate-700 flex items-start gap-2 bg-white/60 rounded-lg p-2">
|
|
<i data-lucide="quote" class="w-3 h-3 mt-0.5 shrink-0 text-amber-500"></i>
|
|
<span class="flex-1">
|
|
{{ apr.message_en|default:apr.message_ar|truncatewords:18 }}
|
|
<span class="block text-[10px] text-slate-400 mt-0.5">
|
|
{{ apr.sent_at|date:"M d, Y" }}
|
|
{% if apr.sender %}· {{ apr.sender.get_full_name }}{% endif %}
|
|
</span>
|
|
</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if can_respond and pending_routing_involvements %}
|
|
<!-- Reject routing forms (for "Wrong Dept" buttons in the Pending Actions table) -->
|
|
{% for inv in pending_routing_involvements %}
|
|
<form method="post" action="{% url 'complaints:involved_department_reject_routing' pk=inv.pk %}" id="reject-routing-{{ inv.pk }}" class="hidden mb-4 p-4 bg-white border-2 border-red-200 rounded-xl space-y-3">
|
|
{% csrf_token %}
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<i data-lucide="log-out" class="w-4 h-4 text-red-500"></i>
|
|
<span class="font-bold text-navy text-sm">{{ inv.complaint.reference_number }} — {% trans "Reject Routing" %}</span>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-navy mb-1">{% trans "Reason" %} <span class="text-red-500">*</span></label>
|
|
<textarea name="rejection_reason" rows="2" required
|
|
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:outline-none focus:border-navy resize-none"
|
|
placeholder="{% trans 'Why does this not belong to your department?' %}"></textarea>
|
|
</div>
|
|
{% if hospital_departments %}
|
|
<div>
|
|
<label class="block text-xs font-semibold text-navy mb-1">{% trans "Suggest Correct Department (optional)" %}</label>
|
|
<select name="suggested_department_id" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:outline-none focus:border-navy bg-white">
|
|
<option value="">{% trans "Select Department" %}</option>
|
|
{% for dept in hospital_departments %}
|
|
<option value="{{ dept.id }}">{{ dept.get_localized_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% endif %}
|
|
<div class="flex gap-2">
|
|
<button type="button" onclick="toggleRejectRouting('{{ inv.pk }}')"
|
|
class="px-3 py-1.5 border-2 border-slate-200 text-slate-600 rounded-lg text-xs font-semibold hover:border-slate-400 transition">{% trans "Cancel" %}</button>
|
|
<button type="submit" class="px-3 py-1.5 bg-red-500 text-white rounded-lg text-xs font-semibold hover:bg-red-600 transition">{% trans "Reject Routing" %}</button>
|
|
</div>
|
|
</form>
|
|
{% endfor %}
|
|
<script>
|
|
function toggleRejectRouting(pk) {
|
|
const form = document.getElementById('reject-routing-' + pk);
|
|
if (form) form.classList.toggle('hidden');
|
|
}
|
|
</script>
|
|
{% endif %}
|
|
|
|
{% if active_investigations and can_respond %}
|
|
<div class="bg-white rounded-xl shadow-sm border border-blue-200 p-5 mb-6">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="search" class="w-5 h-5 text-blue-600"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-bold text-navy">{% trans "Active Investigations" %}</h3>
|
|
<p class="text-xs text-slate-400">{{ active_investigations|length }} {% trans "investigation(s) in progress" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-3">
|
|
{% for inv in active_investigations %}
|
|
<div class="border border-slate-200 rounded-xl p-4 hover:border-blue-300 transition">
|
|
<div class="flex items-start justify-between gap-3 mb-2">
|
|
<div class="flex-1 min-w-0">
|
|
<h2 class="font-mono text-xs font-bold text-navy">
|
|
{{ inv.complaint.reference_number }}
|
|
</h2>
|
|
<p class="text-sm text-slate mt-0.5">{{ inv.complaint.title|truncatechars:60 }}</p>
|
|
</div>
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase whitespace-nowrap
|
|
{% if inv.status == 'questions_sent' %}bg-amber-100 text-amber-700
|
|
{% elif inv.status == 'answers_received' %}bg-blue-100 text-blue-700{% endif %}">
|
|
{{ inv.get_status_display }}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 text-xs text-slate mb-3">
|
|
<span class="inline-flex items-center gap-1">
|
|
<i data-lucide="help-circle" class="w-3.5 h-3.5"></i>
|
|
{{ inv.questions.count }} {% trans "questions" %}
|
|
</span>
|
|
<span class="inline-flex items-center gap-1">
|
|
{% with answered=inv.responses.all|length %}
|
|
<i data-lucide="users" class="w-3.5 h-3.5"></i>
|
|
{{ answered }} {% trans "staff" %}
|
|
{% endwith %}
|
|
</span>
|
|
</div>
|
|
<div class="space-y-1 mb-3">
|
|
{% for resp in inv.responses.all %}
|
|
<div class="flex items-center gap-2 text-xs">
|
|
{% if resp.is_completed %}
|
|
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-green-500 shrink-0"></i>
|
|
<span class="text-slate">{{ resp.staff.get_full_name }}</span>
|
|
<span class="text-slate ml-auto">{{ resp.completed_at|date:"M d, H:i" }}</span>
|
|
{% else %}
|
|
<i data-lucide="clock" class="w-3.5 h-3.5 text-amber-500 shrink-0"></i>
|
|
<span class="text-slate">{{ resp.staff.get_full_name }}</span>
|
|
<span class="text-amber-600 font-semibold ml-auto">{% trans "Pending" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% if inv.explanation.token %}
|
|
<a href="{% url 'complaints:champion_review_answers' complaint_id=inv.complaint.id token=inv.explanation.token %}"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-navy text-white rounded-lg text-xs font-semibold hover:bg-blue transition">
|
|
{% if inv.status == 'answers_received' %}
|
|
<i data-lucide="check-circle-2" class="w-3.5 h-3.5"></i> {% trans "Review & Submit Reply" %}
|
|
{% else %}
|
|
<i data-lucide="eye" class="w-3.5 h-3.5"></i> {% trans "View progress" %}
|
|
{% endif %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if my_pending_responses or my_assigned_complaints %}
|
|
<div class="bg-white rounded-xl shadow-sm border-2 border-amber-200 p-5 mb-6">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-amber-50 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="bell-ring" class="w-5 h-5 text-amber-600"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-bold text-navy">{% trans "Action Required From You" %}</h3>
|
|
<p class="text-xs text-slate-400">{% trans "Items assigned to you that need your response" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-3">
|
|
{% for resp in my_pending_responses %}
|
|
<div class="border border-amber-200 rounded-xl p-4 bg-amber-50/30">
|
|
<div class="flex items-start justify-between gap-3 mb-2">
|
|
<div class="flex-1 min-w-0">
|
|
<a href="{% url 'complaints:complaint_detail' pk=resp.investigation.complaint.pk %}" class="font-mono text-xs font-bold text-navy hover:underline">
|
|
{{ resp.investigation.complaint.reference_number }}
|
|
</a>
|
|
<p class="text-sm text-slate mt-0.5">{{ resp.investigation.complaint.title|truncatechars:60 }}</p>
|
|
</div>
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase whitespace-nowrap bg-amber-100 text-amber-700">
|
|
{% trans "Investigation" %}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 text-xs text-slate mb-3">
|
|
<span class="inline-flex items-center gap-1">
|
|
<i data-lucide="help-circle" class="w-3.5 h-3.5"></i>
|
|
{{ resp.investigation.questions.count }} {% trans "questions to answer" %}
|
|
</span>
|
|
{% if resp.investigation.champion %}
|
|
<span class="inline-flex items-center gap-1">
|
|
<i data-lucide="user" class="w-3.5 h-3.5"></i>
|
|
{{ resp.investigation.champion.get_full_name }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<a href="/complaints/{{ resp.investigation.complaint.id }}/investigate/respond/{{ resp.token }}/"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-amber-500 text-white rounded-lg text-xs font-semibold hover:bg-amber-600 transition">
|
|
<i data-lucide="message-square" class="w-3.5 h-3.5"></i> {% trans "Answer Questions" %}
|
|
</a>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% for c in my_assigned_complaints %}
|
|
<div class="border border-blue-200 rounded-xl p-4 bg-blue-50/30">
|
|
<div class="flex items-start justify-between gap-3 mb-2">
|
|
<div class="flex-1 min-w-0">
|
|
<a href="{% url 'complaints:complaint_detail' pk=c.pk %}" class="font-mono text-xs font-bold text-navy hover:underline">
|
|
{{ c.reference_number }}
|
|
</a>
|
|
<p class="text-sm text-slate mt-0.5">{{ c.title|truncatechars:60 }}</p>
|
|
</div>
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase whitespace-nowrap bg-blue-100 text-blue-700">
|
|
{% trans "Assigned to You" %}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 text-xs text-slate mb-3">
|
|
<span class="inline-flex items-center gap-1">
|
|
<i data-lucide="alert-circle" class="w-3.5 h-3.5"></i>
|
|
{{ c.get_severity_display }}
|
|
</span>
|
|
<span class="inline-flex items-center gap-1">
|
|
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
|
|
{% if c.is_overdue %}
|
|
<span class="text-red-600 font-semibold">{% trans "Overdue" %}</span>
|
|
{% else %}
|
|
{{ c.due_at|date:"M d, H:i" }}
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
<a href="{% url 'complaints:complaint_detail' pk=c.pk %}"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-navy text-white rounded-lg text-xs font-semibold hover:bg-blue transition">
|
|
<i data-lucide="external-link" class="w-3.5 h-3.5"></i> {% trans "View Complaint" %}
|
|
</a>
|
|
</div>
|
|
{% endfor %}
|
|
</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>
|
|
<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>
|
|
{% if can_manage_roles %}
|
|
<button class="tab-btn {% if active_tab == 'roles' %}active{% endif %}" onclick="switchDeptTab('roles', this)">
|
|
<i data-lucide="user-cog" class="w-4 h-4 inline mr-1"></i> {% trans "Roles" %}
|
|
</button>
|
|
{% endif %}
|
|
<button class="tab-btn {% if active_tab == 'sections' %}active{% endif %}" onclick="switchDeptTab('sections', this)">
|
|
<i data-lucide="layers" class="w-4 h-4 inline mr-1"></i> {% trans "Sections" %} ({{ org_sections|length }})
|
|
</button>
|
|
<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" %} ({{ all_inquiries_count }})
|
|
</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 == 'suggestions' %}active{% endif %}" onclick="switchDeptTab('suggestions', this)">
|
|
<i data-lucide="lightbulb" class="w-4 h-4 inline mr-1"></i> {% trans "Suggestions" %} ({{ stats.total_suggestions }})
|
|
</button>
|
|
<button class="tab-btn {% if active_tab == 'appreciations' %}active{% endif %}" onclick="switchDeptTab('appreciations', this)">
|
|
<i data-lucide="heart" class="w-4 h-4 inline mr-1"></i> {% trans "Appreciations" %} ({{ stats.total_appreciations }})
|
|
</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>
|
|
<button class="tab-btn {% if active_tab == 'projects' %}active{% endif %}" onclick="switchDeptTab('projects', this)">
|
|
<i data-lucide="folder-kanban" class="w-4 h-4 inline mr-1"></i> {% trans "Projects" %} ({{ stats.total_projects }})
|
|
</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 }}">
|
|
<input type="hidden" name="inquiry_direction" id="inquiryDirectionInput" value="{{ inquiry_direction }}">
|
|
<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>
|
|
<option value="contact_no_response" {% if inquiry_status_filter == 'contact_no_response' %}selected{% endif %}>{% trans "Contacted - No Response" %}</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>
|
|
<div id="filter-suggestions" class="hidden">
|
|
<select name="suggestion_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 Suggestions" %}</option>
|
|
<option value="submitted" {% if suggestion_status_filter == 'submitted' %}selected{% endif %}>{% trans "Submitted" %}</option>
|
|
<option value="reviewed" {% if suggestion_status_filter == 'reviewed' %}selected{% endif %}>{% trans "Reviewed" %}</option>
|
|
<option value="acknowledged" {% if suggestion_status_filter == 'acknowledged' %}selected{% endif %}>{% trans "Acknowledged" %}</option>
|
|
<option value="closed" {% if suggestion_status_filter == 'closed' %}selected{% endif %}>{% trans "Closed" %}</option>
|
|
</select>
|
|
</div>
|
|
<div id="filter-appreciations" class="hidden">
|
|
<select name="appreciation_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 Appreciations" %}</option>
|
|
<option value="draft" {% if appreciation_status_filter == 'draft' %}selected{% endif %}>{% trans "Draft" %}</option>
|
|
<option value="activated" {% if appreciation_status_filter == 'activated' %}selected{% endif %}>{% trans "Activated" %}</option>
|
|
<option value="sent" {% if appreciation_status_filter == 'sent' %}selected{% endif %}>{% trans "Sent" %}</option>
|
|
<option value="acknowledged" {% if appreciation_status_filter == 'acknowledged' %}selected{% endif %}>{% trans "Acknowledged" %}</option>
|
|
</select>
|
|
</div>
|
|
{% if search_query or complaint_status_filter or inquiry_status_filter or observation_status_filter or suggestion_status_filter or appreciation_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">
|
|
|
|
<!-- 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 cursor-pointer"
|
|
onclick="window.location='{% url 'organizations:department_staff_detail' department.pk s.pk %}'">
|
|
<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>
|
|
|
|
<!-- Roles Tab -->
|
|
{% if can_manage_roles %}
|
|
<div id="panel-roles" class="tab-panel {% if active_tab != 'roles' %}hidden{% endif %}">
|
|
<div class="p-6">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h3 class="font-bold text-navy text-lg flex items-center gap-2">
|
|
<i data-lucide="user-cog" class="w-5 h-5"></i> {% trans "Department Roles" %}
|
|
</h3>
|
|
<p class="text-sm text-slate-500 mt-1">{% trans "Manage who receives complaints, inquiries, and observations for this department." %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{% for role in role_data %}
|
|
<div class="bg-white border-2 border-slate-200 rounded-2xl p-5 hover:border-navy/30 transition">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-xl flex items-center justify-center text-white text-sm font-bold
|
|
{% if role.field == 'champion' %}bg-amber-500{% elif 'manager' in role.field %}bg-navy{% elif 'supervisor' in role.field %}bg-emerald-600{% else %}bg-indigo-600{% endif %}">
|
|
<i data-lucide="{% if role.field == 'champion' %}crown{% elif 'manager' in role.field %}briefcase{% elif 'supervisor' in role.field %}clipboard-check{% else %}shield{% endif %}" class="w-5 h-5"></i>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-bold text-navy text-sm">{{ role.label }}</h4>
|
|
{% if role.field == 'champion' %}
|
|
<span class="text-[10px] text-amber-600 font-semibold uppercase">{% trans "Primary Contact" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="min-h-[48px]">
|
|
{% if role.staff %}
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center text-xs font-bold text-navy">
|
|
{{ role.staff.get_full_name|first|upper }}
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-semibold text-slate-800">{{ role.staff.get_full_name }}</p>
|
|
<p class="text-[11px] text-slate-400">{{ role.staff.email|default:"No email" }}</p>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="flex items-center gap-2 px-3 py-2 bg-slate-50 rounded-lg">
|
|
<i data-lucide="user-x" class="w-4 h-4 text-slate-300"></i>
|
|
<span class="text-sm text-slate-400 italic">{% trans "Not assigned" %}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if can_manage_roles %}
|
|
<div class="mt-3 pt-3 border-t border-slate-100">
|
|
<button type="button"
|
|
onclick="openRoleModal('{{ role.field }}', '{{ role.label }}', '{% if role.staff %}{{ role.staff.id }}{% endif %}')"
|
|
class="text-xs font-semibold text-navy hover:text-blue transition flex items-center gap-1">
|
|
<i data-lucide="{% if role.staff %}pencil{% else %}plus{% endif %}" class="w-3.5 h-3.5"></i>
|
|
{% if role.staff %}{% trans "Change" %}{% else %}{% trans "Assign" %}{% endif %}
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Sections Tab -->
|
|
<div id="panel-sections" class="tab-panel {% if active_tab != 'sections' %}hidden{% endif %}">
|
|
<div class="p-4">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="font-semibold text-slate-800">{% trans "Department Sections" %}</h3>
|
|
<a href="{% url 'organizations:orgsection_create' %}?department={{ department.pk }}" class="inline-flex items-center gap-2 px-4 py-2 bg-[#005696] text-white rounded-xl text-sm font-semibold hover:bg-[#004670] transition">
|
|
<i data-lucide="plus" class="w-4 h-4"></i> {% trans "Add Section" %}
|
|
</a>
|
|
</div>
|
|
<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 "Section Name" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Location" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Champion" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Supervisor" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Deputy" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Sub-Sections" %}</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 sec in org_sections %}
|
|
<tr class="table-row-hover">
|
|
<td class="px-4 py-3">
|
|
<a href="{% url 'organizations:orgsection_detail' sec.pk %}" class="font-semibold text-[#005696] hover:underline">{{ sec.name_en }}</a>
|
|
{% if sec.name_ar %}<br><span class="text-xs text-slate-500" dir="rtl">{{ sec.name_ar }}</span>{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 text-slate-700 text-sm">{{ sec.location_type|default:"-" }}{% if sec.sub_location %} / {{ sec.sub_location }}{% endif %}</td>
|
|
<td class="px-4 py-3 text-slate-700 text-sm">{{ sec.champion.get_full_name|default:"-" }}</td>
|
|
<td class="px-4 py-3 text-slate-700 text-sm">{{ sec.supervisor.get_full_name|default:"-" }}</td>
|
|
<td class="px-4 py-3 text-slate-700 text-sm">{{ sec.deputy_supervisor.get_full_name|default:"-" }}</td>
|
|
<td class="px-4 py-3 text-center">
|
|
<span class="px-2 py-0.5 bg-sky-100 text-sky-600 rounded-full text-xs font-bold">{{ sec.subsections.count }}</span>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
{% if sec.status == 'active' %}
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-green-100 text-green-600">{% trans "Active" %}</span>
|
|
{% else %}
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-slate-100 text-slate-600">{% trans "Inactive" %}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<div class="flex gap-1">
|
|
<a href="{% url 'organizations:orgsection_detail' sec.pk %}" class="p-1.5 text-[#005696] hover:bg-sky-50 rounded-lg transition" title="{% trans 'View' %}"><i data-lucide="eye" class="w-4 h-4"></i></a>
|
|
<a href="{% url 'organizations:orgsection_update' sec.pk %}" class="p-1.5 text-[#005696] hover:bg-sky-50 rounded-lg transition" title="{% trans 'Edit' %}"><i data-lucide="edit-2" class="w-4 h-4"></i></a>
|
|
<a href="{% url 'organizations:orgsection_delete' sec.pk %}" class="p-1.5 text-red-500 hover:bg-red-50 rounded-lg transition" title="{% trans 'Delete' %}"><i data-lucide="trash-2" class="w-4 h-4"></i></a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="8" class="px-4 py-8 text-center text-slate-500">{% trans "No sections in this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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="openRecordModal('complaint', '{{ 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>
|
|
{% if stats.total_complaints > 0 %}
|
|
<div class="border-t border-slate-100 px-4 py-3 bg-slate-50 rounded-b-xl">
|
|
<a href="{% url 'organizations:department_complaints_list' department.pk %}" class="text-sm text-navy font-semibold hover:underline flex items-center gap-1">
|
|
<i data-lucide="external-link" class="w-4 h-4"></i>
|
|
{% blocktrans count counter=stats.total_complaints %}View all {{ counter }} complaint{% plural %}View all {{ counter }} complaints{% endblocktrans %}
|
|
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Inquiries Tab -->
|
|
<div id="panel-inquiries" class="tab-panel {% if active_tab != 'inquiries' %}hidden{% endif %}">
|
|
<div class="flex items-center gap-2 px-4 py-3 border-b border-slate-100">
|
|
<button type="button" onclick="setInquiryDirection('incoming')"
|
|
class="px-3 py-1.5 rounded-lg text-xs font-semibold border transition flex items-center gap-1
|
|
{% if inquiry_direction != 'outgoing' %}bg-navy text-white border-navy
|
|
{% else %}bg-white text-slate-600 border-slate-200 hover:border-navy{% endif %}">
|
|
<i data-lucide="inbox" class="w-3.5 h-3.5"></i> {% trans "Incoming" %}
|
|
<span class="ml-1 px-1.5 py-0.5 rounded-full text-[10px] font-bold
|
|
{% if inquiry_direction != 'outgoing' %}bg-white/20{% else %}bg-slate-100 text-slate-600{% endif %}">{{ incoming_count }}</span>
|
|
</button>
|
|
<button type="button" onclick="setInquiryDirection('outgoing')"
|
|
class="px-3 py-1.5 rounded-lg text-xs font-semibold border transition flex items-center gap-1
|
|
{% if inquiry_direction == 'outgoing' %}bg-navy text-white border-navy
|
|
{% else %}bg-white text-slate-600 border-slate-200 hover:border-navy{% endif %}">
|
|
<i data-lucide="send" class="w-3.5 h-3.5"></i> {% trans "Outgoing" %}
|
|
<span class="ml-1 px-1.5 py-0.5 rounded-full text-[10px] font-bold
|
|
{% if inquiry_direction == 'outgoing' %}bg-white/20{% else %}bg-slate-100 text-slate-600{% endif %}">{{ outgoing_count }}</span>
|
|
</button>
|
|
</div>
|
|
<script>
|
|
function setInquiryDirection(dir) {
|
|
var inp = document.getElementById('inquiryDirectionInput');
|
|
if (inp) {
|
|
inp.value = dir;
|
|
document.getElementById('filterTabInput').value = 'inquiries';
|
|
document.getElementById('filterForm').submit();
|
|
}
|
|
}
|
|
</script>
|
|
<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 "Department" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Outgoing Dept" %}</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>
|
|
<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-xs text-slate-600">{{ i.department.name|default:"-" }}</td>
|
|
<td class="px-4 py-3 text-xs text-slate-600">{{ i.outgoing_department.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
|
|
{% elif i.status == 'contact_no_response' %}bg-amber-100 text-amber-700
|
|
{% else %}bg-slate-50 text-slate-500{% endif %}">
|
|
{{ i.get_status_display }}
|
|
</span>
|
|
{% if i.contact_status == 'contacted_no_response' %}
|
|
<span class="block mt-1 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase bg-amber-100 text-amber-700 w-fit">{% trans "No Response" %}</span>
|
|
{% elif i.contact_status == 'contacted' %}
|
|
<span class="block mt-1 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase bg-green-50 text-green-600 w-fit">{% trans "Contacted" %}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-slate">{{ i.created_at|date:"Y-m-d" }}</td>
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center gap-2">
|
|
<a href="#" onclick="return openRecordModal('inquiry', '{{ i.pk }}')" class="text-xs text-navy font-semibold hover:underline">
|
|
{% trans "View" %}
|
|
</a>
|
|
{% if can_respond and i.handling_department_id == department.pk and i.status in 'open,in_progress' %}
|
|
<a href="{% url 'organizations:department_inquiry_detail' department.pk i.pk %}" class="text-xs text-green-600 font-semibold hover:underline">
|
|
{% trans "Handle" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="9" class="px-4 py-8 text-center text-slate">{% trans "No inquiries for this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if stats.total_inquiries > 0 %}
|
|
<div class="border-t border-slate-100 px-4 py-3 bg-slate-50 rounded-b-xl">
|
|
<a href="{% url 'organizations:department_inquiries_list' department.pk %}" class="text-sm text-navy font-semibold hover:underline flex items-center gap-1">
|
|
<i data-lucide="external-link" class="w-4 h-4"></i>
|
|
{% blocktrans count counter=stats.total_inquiries %}View all {{ counter }} inquiry{% plural %}View all {{ counter }} inquiries{% endblocktrans %}
|
|
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
|
</a>
|
|
</div>
|
|
{% 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>
|
|
<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 o in observations %}
|
|
<tr class="table-row-hover">
|
|
<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>
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center gap-2">
|
|
<a href="#" onclick="return openRecordModal('observation', '{{ o.pk }}')" class="text-xs text-navy font-semibold hover:underline">
|
|
{% trans "View" %}
|
|
</a>
|
|
{% if can_respond and not o.department_responded_at %}
|
|
<a href="#" onclick="return openDeptResponseModal('observation', '{{ o.pk }}', '{% url 'observations:observation_department_response' pk=o.pk %}', '{{ o.tracking_code }}', '{% if o.title %}{{ o.title|escapejs }}{% else %}{{ o.description|truncatewords:10|escapejs }}{% endif %}', '{{ o.department_response_en|default:'' }}', '{{ o.department_response_ar|default:'' }}')" class="text-xs text-green-600 font-semibold hover:underline">
|
|
{% trans "Respond" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="7" class="px-4 py-8 text-center text-slate">{% trans "No observations for this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if stats.total_observations > 0 %}
|
|
<div class="border-t border-slate-100 px-4 py-3 bg-slate-50 rounded-b-xl">
|
|
<a href="{% url 'organizations:department_observations_list' department.pk %}" class="text-sm text-navy font-semibold hover:underline flex items-center gap-1">
|
|
<i data-lucide="external-link" class="w-4 h-4"></i>
|
|
{% blocktrans count counter=stats.total_observations %}View all {{ counter }} observation{% plural %}View all {{ counter }} observations{% endblocktrans %}
|
|
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Suggestions Tab -->
|
|
<div id="panel-suggestions" class="tab-panel {% if active_tab != 'suggestions' %}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 "Title" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Priority" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Category" %}</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 "Created" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for s in suggestions %}
|
|
<tr class="table-row-hover cursor-pointer" onclick="openRecordModal('suggestion', '{{ s.pk }}')">
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ s.pk|truncatechars:8 }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate-700 max-w-xs truncate">{{ s.title|default:"-" }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="px-2 py-1 rounded-full text-[10px] font-bold uppercase
|
|
{% if s.priority == 'urgent' %}bg-red-100 text-red-700
|
|
{% elif s.priority == 'high' %}bg-orange-100 text-orange-700
|
|
{% elif s.priority == 'medium' %}bg-amber-100 text-amber-700
|
|
{% else %}bg-green-100 text-green-700{% endif %}">
|
|
{{ s.get_priority_display }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ s.get_category_display|default:"-" }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate">
|
|
{% if s.is_anonymous %}
|
|
<span class="text-slate-400 italic">{% trans "Anonymous" %}</span>
|
|
{% else %}
|
|
{{ s.contact_name|default:"-" }}
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ s.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 s.status == 'submitted' %}bg-blue-50 text-blue-700
|
|
{% elif s.status == 'reviewed' %}bg-amber-50 text-amber-700
|
|
{% elif s.status == 'acknowledged' %}bg-green-50 text-green-700
|
|
{% elif s.status == 'closed' %}bg-slate-100 text-slate-600
|
|
{% else %}bg-slate-50 text-slate-500{% endif %}">
|
|
{{ s.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-slate">{{ s.created_at|date:"Y-m-d" }}</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="8" class="px-4 py-8 text-center text-slate">{% trans "No suggestions for this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Appreciations Tab -->
|
|
<div id="panel-appreciations" class="tab-panel {% if active_tab != 'appreciations' %}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 "Category" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Sender" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Recipient" %}</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 "Sent" %}</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 a in appreciations %}
|
|
<tr class="table-row-hover cursor-pointer" onclick="openRecordModal('appreciation', '{{ a.pk }}')">
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ a.pk|truncatechars:8 }}</td>
|
|
<td class="px-4 py-3">
|
|
{% if a.category %}
|
|
<span class="px-2 py-1 rounded-full text-[10px] font-bold uppercase" style="background: {{ a.category.color }}15; color: {{ a.category.color }}">
|
|
{{ a.category.name_en }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-slate-400">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate">
|
|
{% if a.is_anonymous %}
|
|
<span class="text-slate-400 italic">{% trans "Anonymous" %}</span>
|
|
{% elif a.sender %}
|
|
{{ a.sender.get_full_name }}
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ a.get_recipient_name|default:"-" }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="px-2 py-1 rounded-full text-[10px] font-bold uppercase
|
|
{% if a.status == 'draft' %}bg-slate-100 text-slate-600
|
|
{% elif a.status == 'activated' %}bg-blue-50 text-blue-700
|
|
{% elif a.status == 'sent' %}bg-green-50 text-green-700
|
|
{% elif a.status == 'acknowledged' %}bg-purple-50 text-purple-700
|
|
{% else %}bg-slate-50 text-slate-500{% endif %}">
|
|
{{ a.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-slate">{{ a.sent_at|date:"Y-m-d"|default:"-" }}</td>
|
|
<td class="px-4 py-3 text-xs text-slate">{{ a.created_at|date:"Y-m-d" }}</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="7" class="px-4 py-8 text-center text-slate">{% trans "No appreciations 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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Projects Tab -->
|
|
<div id="panel-projects" class="tab-panel {% if active_tab != 'projects' %}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 "Project" %}</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 "Lead" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Team" %}</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 p in projects %}
|
|
<tr class="table-row-hover cursor-pointer" onclick="openRecordModal('project', '{{ p.pk }}')">
|
|
<td class="px-4 py-3 text-sm font-medium text-navy">{{ p.name }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="px-2 py-1 rounded-full text-[10px] font-bold uppercase
|
|
{% if p.status == 'completed' %}bg-green-50 text-green-700
|
|
{% elif p.status == 'active' %}bg-blue-50 text-blue-700
|
|
{% elif p.status == 'pending' %}bg-amber-50 text-amber-700
|
|
{% elif p.status == 'cancelled' %}bg-slate-100 text-slate-500
|
|
{% else %}bg-slate-50 text-slate-600{% endif %}">
|
|
{{ p.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ p.project_lead.get_full_name|default:"-" }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ p.team_members.all|length }}</td>
|
|
<td class="px-4 py-3 text-xs text-slate">{{ p.created_at|date:"Y-m-d" }}</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="5" class="px-4 py-8 text-center text-slate">{% trans "No projects for this department" %}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if stats.total_projects > 0 %}
|
|
<div class="border-t border-slate-100 px-4 py-3 bg-slate-50 rounded-b-xl">
|
|
<a href="{% url 'projects:project_list' %}?department={{ department.pk }}" class="text-sm text-navy font-semibold hover:underline flex items-center gap-1">
|
|
<i data-lucide="external-link" class="w-4 h-4"></i>
|
|
{% blocktrans count counter=stats.total_projects %}View all {{ counter }} project{% plural %}View all {{ counter }} projects{% endblocktrans %}
|
|
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Set Department Role Modal -->
|
|
<div id="roleModal" 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-navy/10 rounded-full flex items-center justify-center">
|
|
<i data-lucide="user-cog" class="w-5 h-5 text-navy"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold text-navy" id="roleModalTitle">{% trans "Set Role" %}</h3>
|
|
<p class="text-sm text-slate-500" id="roleModalHint"></p>
|
|
</div>
|
|
</div>
|
|
<form method="post" action="{% url 'organizations:set_department_role' pk=department.pk %}">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="role_field" id="roleModalField" value="">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Select Staff Member" %}</label>
|
|
<select name="staff_id" id="roleModalSelect" 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 "-- Clear / Remove --" %}</option>
|
|
{% for s in managers %}
|
|
<option value="{{ s.id }}" id="roleOption_{{ s.id }}">
|
|
{{ s.get_full_name }}{% if s.job_title %} ({{ s.job_title }}){% endif %}{% if s.department %} — {{ s.department.get_localized_name }}{% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
<p class="text-xs text-slate-400 mt-1">{% trans "Leave blank to remove the current assignee." %}</p>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="document.getElementById('roleModal').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>
|
|
|
|
<!-- Department Response Modal -->
|
|
{% include "components/department_response_modal.html" %}
|
|
|
|
<!-- Record Detail Modal -->
|
|
<div id="recordDetailModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4" onclick="if(event.target===this)closeRecordModal()">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col overflow-hidden" onclick="event.stopPropagation()">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-200 bg-slate-50">
|
|
<div class="flex items-center gap-3">
|
|
<div id="recordModalIcon" class="w-10 h-10 rounded-xl flex items-center justify-center"></div>
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<span id="recordModalRef" class="font-mono text-sm font-bold text-navy"></span>
|
|
<span id="recordModalStatusBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"></span>
|
|
<span id="recordModalSeverityBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"></span>
|
|
</div>
|
|
<p id="recordModalTypeLabel" class="text-xs text-slate-400 mt-0.5"></p>
|
|
</div>
|
|
</div>
|
|
<button onclick="closeRecordModal()" class="w-8 h-8 rounded-lg hover:bg-slate-200 flex items-center justify-center transition">
|
|
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
|
|
</button>
|
|
</div>
|
|
<div id="recordModalContent" class="flex-1 overflow-y-auto px-6 py-5 space-y-4">
|
|
<div id="recordModalLoading" class="flex items-center justify-center py-16">
|
|
<div class="w-8 h-8 border-3 border-navy border-t-transparent rounded-full animate-spin"></div>
|
|
</div>
|
|
<div id="recordModalBody" class="hidden space-y-4"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
{% trans "Complaint" as t_complaint %}
|
|
{% trans "Inquiry" as t_inquiry %}
|
|
{% trans "Observation" as t_observation %}
|
|
{% trans "Suggestion" as t_suggestion %}
|
|
{% trans "Appreciation" as t_appreciation %}
|
|
{% trans "Description" as t_description %}
|
|
{% trans "Patient" as t_patient %}
|
|
{% trans "MRN" as t_mrn %}
|
|
{% trans "Staff Involved" as t_staff_involved %}
|
|
{% trans "Department" as t_department %}
|
|
{% trans "Assigned To" as t_assigned_to %}
|
|
{% trans "Location" as t_location %}
|
|
{% trans "Classification" as t_classification %}
|
|
{% trans "Source" as t_source %}
|
|
{% trans "Created" as t_created %}
|
|
{% trans "SLA Deadline" as t_sla_deadline %}
|
|
{% trans "Overdue" as t_overdue %}
|
|
{% trans "Escalated" as t_escalated %}
|
|
{% trans "Expected Result" as t_expected_result %}
|
|
{% trans "AI Summary" as t_ai_summary %}
|
|
{% trans "Contact" as t_contact %}
|
|
{% trans "Category" as t_category %}
|
|
{% trans "Reporter" as t_reporter %}
|
|
{% trans "Subject" as t_subject %}
|
|
{% trans "No description" as t_no_desc %}
|
|
{% trans "Sender" as t_sender %}
|
|
{% trans "Recipient" as t_recipient %}
|
|
{% trans "Message" as t_message %}
|
|
{% trans "Priority" as t_priority %}
|
|
{% trans "Sent" as t_sent %}
|
|
{% trans "Visibility" as t_visibility %}
|
|
{% trans "Sentiment" as t_sentiment %}
|
|
{% trans "Acknowledged" as t_acknowledged %}
|
|
{% trans "Project" as t_project %}
|
|
{% trans "Lead" as t_lead %}
|
|
{% trans "Departments" as t_departments %}
|
|
{% trans "Team Members" as t_team_members %}
|
|
{% trans "Tasks" as t_tasks %}
|
|
{% trans "pending" as t_pending %}
|
|
{% trans "Start Date" as t_start_date %}
|
|
{% trans "Target Completion" as t_target_date %}
|
|
{% trans "Open Project" as t_open_project %}
|
|
{% trans "Department Response" as t_dept_response %}
|
|
|
|
var recordModalLabels = {
|
|
complaint: '{{ t_complaint|escapejs }}',
|
|
inquiry: '{{ t_inquiry|escapejs }}',
|
|
observation: '{{ t_observation|escapejs }}',
|
|
suggestion: '{{ t_suggestion|escapejs }}',
|
|
appreciation: '{{ t_appreciation|escapejs }}',
|
|
description: '{{ t_description|escapejs }}',
|
|
patient: '{{ t_patient|escapejs }}',
|
|
mrn: '{{ t_mrn|escapejs }}',
|
|
staff_involved: '{{ t_staff_involved|escapejs }}',
|
|
department: '{{ t_department|escapejs }}',
|
|
assigned_to: '{{ t_assigned_to|escapejs }}',
|
|
location: '{{ t_location|escapejs }}',
|
|
classification: '{{ t_classification|escapejs }}',
|
|
source: '{{ t_source|escapejs }}',
|
|
created: '{{ t_created|escapejs }}',
|
|
sla_deadline: '{{ t_sla_deadline|escapejs }}',
|
|
overdue: '{{ t_overdue|escapejs }}',
|
|
escalated: '{{ t_escalated|escapejs }}',
|
|
expected_result: '{{ t_expected_result|escapejs }}',
|
|
ai_summary: '{{ t_ai_summary|escapejs }}',
|
|
contact: '{{ t_contact|escapejs }}',
|
|
category: '{{ t_category|escapejs }}',
|
|
reporter: '{{ t_reporter|escapejs }}',
|
|
subject: '{{ t_subject|escapejs }}',
|
|
no_desc: '{{ t_no_desc|escapejs }}',
|
|
sender: '{{ t_sender|escapejs }}',
|
|
recipient: '{{ t_recipient|escapejs }}',
|
|
message: '{{ t_message|escapejs }}',
|
|
priority: '{{ t_priority|escapejs }}',
|
|
sent: '{{ t_sent|escapejs }}',
|
|
visibility: '{{ t_visibility|escapejs }}',
|
|
sentiment: '{{ t_sentiment|escapejs }}',
|
|
acknowledged: '{{ t_acknowledged|escapejs }}',
|
|
project: '{{ t_project|escapejs }}',
|
|
lead: '{{ t_lead|escapejs }}',
|
|
departments: '{{ t_departments|escapejs }}',
|
|
team_members: '{{ t_team_members|escapejs }}',
|
|
tasks: '{{ t_tasks|escapejs }}',
|
|
pending: '{{ t_pending|escapejs }}',
|
|
start_date: '{{ t_start_date|escapejs }}',
|
|
target_date: '{{ t_target_date|escapejs }}',
|
|
open_project: '{{ t_open_project|escapejs }}',
|
|
dept_response: '{{ t_dept_response|escapejs }}',
|
|
};
|
|
|
|
var recordApiUrls = {
|
|
complaint: '/organizations/api/dept-complaint/',
|
|
inquiry: '/organizations/api/dept-inquiry/',
|
|
observation: '/organizations/api/dept-observation/',
|
|
suggestion: '/organizations/api/dept-suggestion/',
|
|
appreciation: '/organizations/api/dept-appreciation/',
|
|
project: '/organizations/api/dept-project/',
|
|
};
|
|
|
|
var recordTypeConfig = {
|
|
complaint: { icon: 'alert-circle', color: 'red', bg: 'bg-red-50', text: 'text-red-600' },
|
|
inquiry: { icon: 'help-circle', color: 'amber', bg: 'bg-amber-50', text: 'text-amber-600' },
|
|
observation: { icon: 'eye', color: 'purple', bg: 'bg-purple-50', text: 'text-purple-600' },
|
|
suggestion: { icon: 'lightbulb', color: 'amber', bg: 'bg-amber-50', text: 'text-amber-600' },
|
|
appreciation: { icon: 'heart', color: 'pink', bg: 'bg-pink-50', text: 'text-pink-600' },
|
|
project: { icon: 'folder-kanban', color: 'indigo', bg: 'bg-indigo-50', text: 'text-indigo-600' },
|
|
};
|
|
|
|
function openRecordModal(type, pk) {
|
|
var modal = document.getElementById('recordDetailModal');
|
|
var config = recordTypeConfig[type];
|
|
var iconEl = document.getElementById('recordModalIcon');
|
|
iconEl.className = 'w-10 h-10 rounded-xl flex items-center justify-center ' + config.bg + ' ' + config.text;
|
|
iconEl.innerHTML = '<i data-lucide="' + config.icon + '" class="w-5 h-5"></i>';
|
|
document.getElementById('recordModalTypeLabel').textContent = recordModalLabels[type];
|
|
document.getElementById('recordModalRef').textContent = '';
|
|
document.getElementById('recordModalStatusBadge').textContent = '';
|
|
document.getElementById('recordModalSeverityBadge').textContent = '';
|
|
document.getElementById('recordModalStatusBadge').className = 'px-2 py-0.5 rounded-full text-[10px] font-bold uppercase hidden';
|
|
document.getElementById('recordModalSeverityBadge').className = 'px-2 py-0.5 rounded-full text-[10px] font-bold uppercase hidden';
|
|
document.getElementById('recordModalLoading').classList.remove('hidden');
|
|
document.getElementById('recordModalBody').classList.add('hidden');
|
|
modal.classList.remove('hidden');
|
|
lucide.createIcons();
|
|
|
|
fetch(recordApiUrls[type] + pk + '/')
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
if (d.error) { closeRecordModal(); alert(d.error); return; }
|
|
renderRecordModal(type, d);
|
|
})
|
|
.catch(function() { closeRecordModal(); });
|
|
|
|
return false;
|
|
}
|
|
|
|
function renderRecordModal(type, d) {
|
|
document.getElementById('recordModalRef').textContent = d.reference;
|
|
|
|
var sb = document.getElementById('recordModalStatusBadge');
|
|
sb.textContent = d.status_display || d.status;
|
|
sb.className = 'px-2 py-0.5 rounded-full text-[10px] font-bold uppercase ' + statusColorClass(d.status);
|
|
sb.classList.remove('hidden');
|
|
|
|
var svb = document.getElementById('recordModalSeverityBadge');
|
|
if (d.severity && d.severity_display) {
|
|
svb.textContent = d.severity_display;
|
|
svb.className = 'px-2 py-0.5 rounded-full text-[10px] font-bold uppercase ' + severityColorClass(d.severity);
|
|
svb.classList.remove('hidden');
|
|
}
|
|
|
|
var html = '';
|
|
|
|
if (d.title) {
|
|
html += '<h3 class="text-lg font-bold text-navy">' + escHtml(d.title) + '</h3>';
|
|
}
|
|
|
|
html += '<div class="bg-slate-50 rounded-xl p-4">';
|
|
html += '<p class="text-xs font-semibold text-slate-400 uppercase mb-1">' + recordModalLabels.description + '</p>';
|
|
html += '<p class="text-sm text-slate-700 whitespace-pre-wrap">' + escHtml(d.description || recordModalLabels.no_desc) + '</p>';
|
|
html += '</div>';
|
|
|
|
html += '<div class="grid grid-cols-2 gap-3">';
|
|
|
|
if (type === 'complaint') {
|
|
if (d.patient && d.patient !== '-') html += infoCard(recordModalLabels.patient, d.patient);
|
|
if (d.patient_mrn && d.patient_mrn !== '-') html += infoCard(recordModalLabels.mrn, d.patient_mrn);
|
|
if (d.staff && d.staff.length > 0) html += infoCard(recordModalLabels.staff_involved, d.staff.join(', '));
|
|
}
|
|
if (type === 'inquiry') {
|
|
html += infoCard(recordModalLabels.contact, [d.contact_name, d.contact_phone, d.contact_email].filter(function(v) { return v && v !== '-'; }).join(' | '));
|
|
if (d.category && d.category !== '-') html += infoCard(recordModalLabels.category, d.category);
|
|
if (d.priority_display) html += infoCard(recordModalLabels.priority, d.priority_display);
|
|
if (d.contact_status_display) html += infoCard(recordModalLabels.contact, d.contact_status_display);
|
|
if (d.outgoing_department) html += infoCard(recordModalLabels.department + ' (Outgoing)', d.outgoing_department);
|
|
}
|
|
if (type === 'observation') {
|
|
html += infoCard(recordModalLabels.reporter, d.reporter || '-');
|
|
if (d.category && d.category !== '-') html += infoCard(recordModalLabels.category, d.category);
|
|
}
|
|
if (type === 'suggestion') {
|
|
html += infoCard(recordModalLabels.contact, d.contact_name || '-');
|
|
if (d.category && d.category !== '-') html += infoCard(recordModalLabels.category, d.category);
|
|
if (d.priority && d.priority_display) html += infoCard(recordModalLabels.priority, d.priority_display);
|
|
if (d.sentiment) html += infoCard(recordModalLabels.sentiment, d.sentiment);
|
|
}
|
|
if (type === 'appreciation') {
|
|
html += infoCard(recordModalLabels.sender, d.sender || '-');
|
|
html += infoCard(recordModalLabels.recipient, d.recipient || '-');
|
|
if (d.category && d.category !== '-') html += infoCard(recordModalLabels.category, d.category);
|
|
if (d.visibility) html += infoCard(recordModalLabels.visibility, d.visibility);
|
|
}
|
|
if (type === 'project') {
|
|
html += infoCard(recordModalLabels.lead, d.lead || '-');
|
|
if (d.departments && d.departments.length) html += infoCard(recordModalLabels.departments, d.departments.join(', '));
|
|
html += infoCard(recordModalLabels.team_members, (d.team_count != null ? d.team_count : '-'));
|
|
if (d.tasks_total != null) html += infoCard(recordModalLabels.tasks, d.tasks_completed + ' / ' + d.tasks_total + ' (' + (d.tasks_pending || 0) + ' ' + recordModalLabels.pending + ')');
|
|
if (d.start_date && d.start_date !== '-') html += infoCard(recordModalLabels.start_date, d.start_date);
|
|
if (d.target_completion_date && d.target_completion_date !== '-') html += infoCard(recordModalLabels.target_date, d.target_completion_date);
|
|
}
|
|
|
|
// Generic record fields (do not apply to projects)
|
|
if (type !== 'project') {
|
|
html += infoCard(recordModalLabels.department, d.department);
|
|
html += infoCard(recordModalLabels.assigned_to, d.assigned_to);
|
|
html += infoCard(recordModalLabels.location, d.location);
|
|
|
|
if (d.taxonomy && d.taxonomy.length > 0) html += infoCard(recordModalLabels.classification, d.taxonomy.join(' > '));
|
|
if (d.source) html += infoCard(recordModalLabels.source, d.source);
|
|
}
|
|
|
|
html += infoCard(recordModalLabels.created, d.created_at);
|
|
|
|
if (d.due_at) {
|
|
var deadlineText = d.due_at;
|
|
if (d.is_overdue) deadlineText += ' <span class="text-red-600 font-bold">(' + recordModalLabels.overdue + ')</span>';
|
|
html += infoCard(recordModalLabels.sla_deadline, deadlineText);
|
|
}
|
|
if (d.escalated) html += infoCard('', '<span class="text-red-600 font-bold">' + recordModalLabels.escalated + '</span>');
|
|
|
|
html += '</div>';
|
|
|
|
if (d.expected_result) {
|
|
html += '<div class="bg-blue-50 rounded-xl p-4">';
|
|
html += '<p class="text-xs font-semibold text-blue-600 uppercase mb-1">' + recordModalLabels.expected_result + '</p>';
|
|
html += '<p class="text-sm text-slate-700">' + escHtml(d.expected_result) + '</p>';
|
|
html += '</div>';
|
|
}
|
|
|
|
if (d.ai_brief || d.ai_summary) {
|
|
var aiText = d.ai_brief || d.ai_summary;
|
|
html += '<div class="bg-indigo-50 rounded-xl p-4">';
|
|
html += '<p class="text-xs font-semibold text-indigo-600 uppercase mb-1">' + recordModalLabels.ai_summary + '</p>';
|
|
html += '<p class="text-sm text-slate-700">' + escHtml(aiText) + '</p>';
|
|
html += '</div>';
|
|
}
|
|
|
|
if (type === 'inquiry' && (d.department_response_en || d.department_response_ar)) {
|
|
html += '<div class="bg-green-50 rounded-xl p-4">';
|
|
html += '<h3 class="text-sm font-bold text-green-700 flex items-center gap-2 mb-2"><i data-lucide="check-circle" class="w-4 h-4"></i> ' + recordModalLabels.dept_response;
|
|
if (d.department_responded_at) html += '<span class="text-[10px] text-green-600 font-normal ml-2">' + escHtml(d.department_responded_at) + '</span>';
|
|
html += '</h3>';
|
|
if (d.department_response_en) html += '<p class="text-sm text-slate-700 whitespace-pre-wrap mb-2">' + escHtml(d.department_response_en) + '</p>';
|
|
if (d.department_response_ar) html += '<p class="text-sm text-slate-700 whitespace-pre-wrap" dir="rtl">' + escHtml(d.department_response_ar) + '</p>';
|
|
html += '</div>';
|
|
}
|
|
|
|
if (type === 'inquiry' && d.response) {
|
|
html += '<div class="bg-blue-50 rounded-xl p-4">';
|
|
html += '<h3 class="text-sm font-bold text-blue-700 flex items-center gap-2 mb-2"><i data-lucide="message-square" class="w-4 h-4"></i> {% filter escapejs %}{% trans "Response to Patient" %}{% endfilter %}';
|
|
if (d.responded_at) html += '<span class="text-[10px] text-blue-600 font-normal ml-2">' + escHtml(d.responded_at) + '</span>';
|
|
html += '</h3>';
|
|
if (d.responded_by) html += '<p class="text-[10px] text-slate-500 mb-2">{% filter escapejs %}{% trans "By" %}{% endfilter %} ' + escHtml(d.responded_by) + '</p>';
|
|
html += '<p class="text-sm text-slate-700 whitespace-pre-wrap">' + escHtml(d.response) + '</p>';
|
|
html += '</div>';
|
|
}
|
|
|
|
if (type === 'inquiry' && d.resolved_at) {
|
|
html += '<div class="bg-green-50 rounded-xl p-3 flex items-center gap-2">';
|
|
html += '<i data-lucide="check-circle-2" class="w-4 h-4 text-green-600"></i>';
|
|
html += '<span class="text-xs text-green-700 font-semibold">{% filter escapejs %}{% trans "Resolved" %}{% endfilter %} ' + escHtml(d.resolved_at);
|
|
if (d.resolved_by) html += ' · ' + escHtml(d.resolved_by);
|
|
html += '</span>';
|
|
html += '</div>';
|
|
}
|
|
|
|
if (type === 'appreciation') {
|
|
if (d.description_ar) {
|
|
html += '<div class="bg-slate-50 rounded-xl p-4">';
|
|
html += '<p class="text-xs font-semibold text-slate-400 uppercase mb-1">العربية</p>';
|
|
html += '<p class="text-sm text-slate-700 whitespace-pre-wrap" dir="rtl">' + escHtml(d.description_ar) + '</p>';
|
|
html += '</div>';
|
|
}
|
|
if (d.sent_at) html += '<div class="bg-green-50 rounded-xl p-3 text-center"><span class="text-xs text-green-700 font-semibold"><i data-lucide="send" class="w-3 h-3 inline mr-1"></i>' + recordModalLabels.sent + ': ' + escHtml(d.sent_at) + '</span></div>';
|
|
if (d.acknowledged_at) html += '<div class="bg-purple-50 rounded-xl p-3 text-center"><span class="text-xs text-purple-700 font-semibold"><i data-lucide="check-circle" class="w-3 h-3 inline mr-1"></i>' + recordModalLabels.acknowledged + ': ' + escHtml(d.acknowledged_at) + '</span></div>';
|
|
}
|
|
|
|
if (type === 'project' && d.detail_url) {
|
|
html += '<div class="text-center pt-2"><a href="' + d.detail_url + '" class="inline-flex items-center gap-1 px-4 py-2 bg-navy text-white rounded-xl text-sm font-semibold hover:bg-navy/90 transition"><i data-lucide="external-link" class="w-4 h-4"></i> ' + recordModalLabels.open_project + '</a></div>';
|
|
}
|
|
|
|
document.getElementById('recordModalBody').innerHTML = html;
|
|
document.getElementById('recordModalLoading').classList.add('hidden');
|
|
document.getElementById('recordModalBody').classList.remove('hidden');
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function infoCard(label, value) {
|
|
return '<div class="bg-white border border-slate-100 rounded-lg p-3">'
|
|
+ (label ? '<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">' + label + '</p>' : '')
|
|
+ '<p class="text-sm text-navy font-medium">' + value + '</p>'
|
|
+ '</div>';
|
|
}
|
|
|
|
function statusColorClass(status) {
|
|
var map = {
|
|
open: 'bg-blue-50 text-blue-700', in_progress: 'bg-amber-50 text-amber-700',
|
|
resolved: 'bg-green-50 text-green-700', closed: 'bg-slate-100 text-slate-600',
|
|
cancelled: 'bg-slate-50 text-slate-500', contacted: 'bg-cyan-50 text-cyan-700',
|
|
contacted_no_response: 'bg-orange-50 text-orange-700', new: 'bg-sky-50 text-sky-700',
|
|
triaged: 'bg-teal-50 text-teal-700', assigned: 'bg-teal-50 text-teal-700',
|
|
pending_external: 'bg-violet-50 text-violet-700',
|
|
submitted: 'bg-blue-50 text-blue-700', reviewed: 'bg-amber-50 text-amber-700',
|
|
acknowledged: 'bg-green-50 text-green-700',
|
|
draft: 'bg-slate-100 text-slate-600', activated: 'bg-cyan-50 text-cyan-700',
|
|
sent: 'bg-green-50 text-green-700',
|
|
};
|
|
return map[status] || 'bg-slate-50 text-slate-500';
|
|
}
|
|
|
|
function severityColorClass(severity) {
|
|
var map = {
|
|
critical: 'bg-gray-800 text-white', high: 'bg-red-50 text-red-700',
|
|
medium: 'bg-amber-50 text-amber-700', low: 'bg-green-50 text-green-700',
|
|
};
|
|
return map[severity] || 'bg-slate-50 text-slate-500';
|
|
}
|
|
|
|
function escHtml(str) {
|
|
var div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function closeRecordModal() {
|
|
document.getElementById('recordDetailModal').classList.add('hidden');
|
|
}
|
|
|
|
const tabFilters = {
|
|
staff: [],
|
|
complaints: ['filter-complaints'],
|
|
inquiries: ['filter-inquiries'],
|
|
observations: ['filter-observations'],
|
|
suggestions: ['filter-suggestions'],
|
|
appreciations: ['filter-appreciations'],
|
|
projects: [],
|
|
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' || tab === 'roles') ? 'none' : '';
|
|
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('tab', tab);
|
|
history.replaceState(null, '', url);
|
|
|
|
if (tab === 'analytics' && !analyticsLoaded) {
|
|
loadDepartmentAnalytics();
|
|
}
|
|
if (tab === 'roles' && window.lucide) lucide.createIcons();
|
|
}
|
|
|
|
function openRoleModal(roleField, roleLabel, currentStaffId) {
|
|
document.getElementById('roleModalField').value = roleField;
|
|
document.getElementById('roleModalTitle').textContent = roleLabel;
|
|
document.getElementById('roleModalHint').textContent = '';
|
|
var sel = document.getElementById('roleModalSelect');
|
|
sel.value = currentStaffId || '';
|
|
document.getElementById('roleModal').classList.remove('hidden');
|
|
}
|
|
|
|
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>';
|
|
}
|
|
}
|
|
|
|
// Department Response Modal is now handled by shared component
|
|
// openDeptResponseModal / closeDeptResponseModal / submitDeptResponse are in department_response_modal.html
|
|
|
|
</script>
|
|
{% endblock %}
|