All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001): - new ReferenceSequence model + generate_reference() helper (apps/core) - Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save() - prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only) - removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources - migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012 - unit tests (format, sanitization, monthly reset, 40-thread concurrency) QA audit: - isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env) - feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion) - reports/feedback-modules-qa-report.md Also bundles accumulated in-progress work across complaints, observations, organizations, templates, and other modules.
699 lines
40 KiB
HTML
699 lines
40 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{{ staff.get_localized_name }} - {{ department.get_localized_name }} - PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="p-6 max-w-5xl mx-auto">
|
|
<div class="flex items-center gap-2 text-sm text-slate-500 mb-4">
|
|
<a href="{% url 'organizations:department_list' %}" class="hover:text-navy transition">{% trans "Departments" %}</a>
|
|
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
|
<a href="{% url 'organizations:department_detail' department.pk %}" class="hover:text-navy transition">{{ department.get_localized_name }}</a>
|
|
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
|
<a href="{% url 'organizations:department_detail' department.pk %}?tab=staff" class="hover:text-navy transition">{% trans "Staff" %}</a>
|
|
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
|
<span class="text-navy font-semibold">{{ staff.get_localized_name }}</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-12 h-12 bg-navy/10 rounded-xl flex items-center justify-center text-navy font-bold text-lg">
|
|
{{ staff.first_name|truncatechars:1 }}{{ staff.last_name|truncatechars:1 }}
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<h1 class="text-xl font-bold text-navy">{{ staff.get_localized_name }}</h1>
|
|
{% if staff.is_head %}
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-navy text-white uppercase">
|
|
<i data-lucide="crown" class="w-3 h-3 inline"></i> {% trans "Head" %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<p class="text-sm text-slate-500">
|
|
{{ staff.get_localized_job_title|default:"—" }}
|
|
{% if staff.specialization %} · {{ staff.specialization }}{% endif %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
{% if can_edit_staff %}
|
|
<button onclick="openEditStaffModal()"
|
|
class="px-3 py-2 bg-navy text-white text-sm font-semibold rounded-xl hover:bg-navy/90 transition flex items-center gap-1.5">
|
|
<i data-lucide="pencil" class="w-4 h-4"></i> {% trans "Edit" %}
|
|
</button>
|
|
{% endif %}
|
|
<a href="{% url 'organizations:department_detail' department.pk %}?tab=staff" class="text-sm text-slate hover:text-navy transition flex items-center gap-1">
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to staff" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div class="space-y-6">
|
|
<div class="bg-white rounded-2xl border border-slate-200 p-6">
|
|
<h3 class="text-sm font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="user" class="w-4 h-4"></i> {% trans "Personal Info" %}
|
|
</h3>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Name" %}</p>
|
|
<p class="text-sm font-medium text-navy">{{ staff.first_name }} {{ staff.last_name }}</p>
|
|
</div>
|
|
{% if staff.first_name_ar or staff.last_name_ar %}
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Name (Arabic)" %}</p>
|
|
<p class="text-sm font-medium text-navy" dir="rtl">{{ staff.first_name_ar }} {{ staff.last_name_ar }}</p>
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Employee ID" %}</p>
|
|
<p class="text-sm font-mono text-navy">{{ staff.employee_id }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Staff Type" %}</p>
|
|
<p class="text-sm text-navy">{{ staff.get_staff_type_display }}</p>
|
|
</div>
|
|
{% if staff.license_number %}
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "License No." %}</p>
|
|
<p class="text-sm text-navy">{{ staff.license_number }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-2xl border border-slate-200 p-6">
|
|
<h3 class="text-sm font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="building-2" class="w-4 h-4"></i> {% trans "Organization" %}
|
|
</h3>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Hospital" %}</p>
|
|
<p class="text-sm font-medium text-navy">{{ staff.hospital.name }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Department" %}</p>
|
|
<p class="text-sm font-medium text-navy">{{ department.get_localized_name }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{{ department.get_category_display }}</p>
|
|
<p class="text-sm text-navy">{{ staff.get_department_type_display }}</p>
|
|
</div>
|
|
{% if staff.section_fk %}
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Section" %}</p>
|
|
<p class="text-sm text-navy">{{ staff.section_fk.name }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if staff.subsection_fk %}
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Subsection" %}</p>
|
|
<p class="text-sm text-navy">{{ staff.subsection_fk.name }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if staff.report_to %}
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Reports To" %}</p>
|
|
<a href="{% url 'organizations:department_staff_detail' department.pk staff.report_to.pk %}" class="text-sm text-navy font-semibold hover:underline">
|
|
{{ staff.report_to.get_localized_name }}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if staff.email or staff.phone %}
|
|
<div class="bg-white rounded-2xl border border-slate-200 p-6">
|
|
<h3 class="text-sm font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="phone" class="w-4 h-4"></i> {% trans "Contact" %}
|
|
</h3>
|
|
<div class="space-y-3">
|
|
{% if staff.email %}
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Email" %}</p>
|
|
<p class="text-sm text-navy">{{ staff.email }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if staff.phone %}
|
|
<div>
|
|
<p class="text-[10px] text-slate-400 uppercase font-bold mb-0.5">{% trans "Phone" %}</p>
|
|
<p class="text-sm text-navy">{{ staff.phone }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if direct_reports %}
|
|
<div class="bg-white rounded-2xl border border-slate-200 p-6">
|
|
<h3 class="text-sm font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="users" class="w-4 h-4"></i> {% trans "Direct Reports" %} ({{ direct_reports|length }})
|
|
</h3>
|
|
<div class="space-y-2">
|
|
{% for dr in direct_reports %}
|
|
<a href="{% url 'organizations:department_staff_detail' department.pk dr.pk %}" class="flex items-center gap-2 px-3 py-2 bg-slate-50 rounded-xl hover:bg-slate-100 transition">
|
|
<div class="w-7 h-7 bg-navy/10 rounded-full flex items-center justify-center text-navy font-bold text-[10px]">
|
|
{{ dr.first_name|truncatechars:1 }}{{ dr.last_name|truncatechars:1 }}
|
|
</div>
|
|
<span class="text-sm font-medium text-navy">{{ dr.get_localized_name }}</span>
|
|
{% if dr.get_localized_job_title %}
|
|
<span class="text-xs text-slate-400">· {{ dr.get_localized_job_title }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="lg:col-span-2">
|
|
{% if complaint_counts.total > 0 %}
|
|
<div class="flex flex-wrap gap-3 mb-4">
|
|
<div class="px-3 py-2 bg-slate-100 rounded-xl text-center min-w-[80px]">
|
|
<p class="text-lg font-bold text-navy">{{ complaint_counts.total }}</p>
|
|
<p class="text-[10px] text-slate uppercase font-bold">{% trans "Total" %}</p>
|
|
</div>
|
|
{% if complaint_counts.open %}
|
|
<div class="px-3 py-2 bg-blue-50 rounded-xl text-center min-w-[80px]">
|
|
<p class="text-lg font-bold text-blue-700">{{ complaint_counts.open }}</p>
|
|
<p class="text-[10px] text-blue-600 uppercase font-bold">{% trans "Open" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if complaint_counts.in_progress %}
|
|
<div class="px-3 py-2 bg-amber-50 rounded-xl text-center min-w-[80px]">
|
|
<p class="text-lg font-bold text-amber-700">{{ complaint_counts.in_progress }}</p>
|
|
<p class="text-[10px] text-amber-600 uppercase font-bold">{% trans "In Progress" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if complaint_counts.resolved %}
|
|
<div class="px-3 py-2 bg-green-50 rounded-xl text-center min-w-[80px]">
|
|
<p class="text-lg font-bold text-green-700">{{ complaint_counts.resolved }}</p>
|
|
<p class="text-[10px] text-green-600 uppercase font-bold">{% trans "Resolved" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if complaint_counts.closed %}
|
|
<div class="px-3 py-2 bg-slate-50 rounded-xl text-center min-w-[80px]">
|
|
<p class="text-lg font-bold text-slate-600">{{ complaint_counts.closed }}</p>
|
|
<p class="text-[10px] text-slate uppercase font-bold">{% trans "Closed" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="bg-white rounded-2xl border border-slate-200 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
|
<h3 class="text-sm font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="file-text" class="w-4 h-4"></i> {% trans "Complaints" %} ({{ complaint_counts.total }})
|
|
</h3>
|
|
</div>
|
|
{% if complaint_rows %}
|
|
<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 "Patient" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Status" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Severity" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Role" %}</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 row in complaint_rows %}
|
|
{% with c=row.complaint %}
|
|
<tr class="hover:bg-slate-50 transition 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 truncate">{{ c.title|default:"—" }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate">{{ c.patient.mrn|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">
|
|
{% if c.severity %}
|
|
<span class="px-2 py-1 rounded-full text-[10px] font-bold uppercase
|
|
{% if c.severity == 'critical' %}bg-gray-800 text-white
|
|
{% elif c.severity == 'high' %}bg-red-50 text-red-700
|
|
{% elif c.severity == 'medium' %}bg-amber-50 text-amber-700
|
|
{% else %}bg-green-50 text-green-700{% endif %}">
|
|
{{ c.get_severity_display }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-slate-400 text-xs">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span class="px-2 py-1 rounded-full text-[10px] font-semibold
|
|
{% if row.role == 'Primary' %}bg-navy/10 text-navy
|
|
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
|
{{ row.role }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-slate">{{ c.created_at|date:"Y-m-d" }}</td>
|
|
</tr>
|
|
{% endwith %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="px-6 py-12 text-center">
|
|
<i data-lucide="inbox" class="w-10 h-10 mx-auto text-slate-300 mb-3"></i>
|
|
<p class="text-slate text-sm">{% trans "No complaints found for this staff member in this department." %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if appreciations %}
|
|
<div class="bg-white rounded-2xl border border-slate-200 overflow-hidden mt-6">
|
|
<div class="px-6 py-4 border-b border-slate-100">
|
|
<h3 class="text-sm font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="heart" class="w-4 h-4 text-pink-500"></i> {% trans "Appreciations" %} ({{ appreciations|length }})
|
|
</h3>
|
|
</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 "Category" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Message" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "From" %}</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 "Date" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for a in appreciations %}
|
|
<tr class="hover:bg-slate-50 transition cursor-pointer"
|
|
onclick="openRecordModal('appreciation', '{{ a.pk }}')">
|
|
<td class="px-4 py-3">
|
|
{% if a.category %}
|
|
<span class="px-2 py-1 rounded-full text-[10px] font-bold" style="background: {{ a.category.color }}15; color: {{ a.category.color }}">
|
|
{{ a.category.name_en }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-slate-400 text-xs">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate-700 max-w-xs truncate">{{ a.message_en|default:"—" }}</td>
|
|
<td class="px-4 py-3 text-sm text-slate">
|
|
{% if a.is_anonymous %}
|
|
<span class="text-slate-400 italic">{% trans "Anonymous" %}</span>
|
|
{% else %}
|
|
{{ a.sender.get_full_name|default:"—" }}
|
|
{% endif %}
|
|
</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:a.created_at|date:"Y-m-d" }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if can_edit_staff %}
|
|
<!-- Edit Staff Modal -->
|
|
<div id="editStaffModal" 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)closeEditStaffModal()">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg 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 class="w-10 h-10 bg-navy/10 rounded-xl flex items-center justify-center">
|
|
<i data-lucide="user-pen" class="w-5 h-5 text-navy"></i>
|
|
</div>
|
|
<h3 class="text-base font-bold text-navy">{% trans "Edit Staff Info" %}</h3>
|
|
</div>
|
|
<button onclick="closeEditStaffModal()" 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>
|
|
<form method="post" action="{% url 'organizations:department_staff_update' department.pk staff.pk %}" id="editStaffForm">
|
|
{% csrf_token %}
|
|
<div class="flex-1 overflow-y-auto px-6 py-5 space-y-5">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "First Name" %} *</label>
|
|
<input type="text" name="first_name" value="{{ staff.first_name }}" required
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Last Name" %} *</label>
|
|
<input type="text" name="last_name" value="{{ staff.last_name }}" required
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "First Name (Arabic)" %}</label>
|
|
<input type="text" name="first_name_ar" value="{{ staff.first_name_ar }}" dir="rtl"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Last Name (Arabic)" %}</label>
|
|
<input type="text" name="last_name_ar" value="{{ staff.last_name_ar }}" dir="rtl"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Job Title" %}</label>
|
|
<input type="text" name="job_title" value="{{ staff.job_title }}"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Job Title (Arabic)" %}</label>
|
|
<input type="text" name="job_title_ar" value="{{ staff.job_title_ar }}" dir="rtl"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Email" %}</label>
|
|
<input type="email" name="email" value="{{ staff.email }}"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Phone" %}</label>
|
|
<input type="tel" name="phone" value="{{ staff.phone }}"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Specialization" %}</label>
|
|
<input type="text" name="specialization" value="{{ staff.specialization }}"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "License No." %}</label>
|
|
<input type="text" name="license_number" value="{{ staff.license_number|default:'' }}"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">{% trans "Gender" %}</label>
|
|
<select name="gender"
|
|
class="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent">
|
|
<option value="">—</option>
|
|
<option value="male" {% if staff.gender == 'male' %}selected{% endif %}>{% trans "Male" %}</option>
|
|
<option value="female" {% if staff.gender == 'female' %}selected{% endif %}>{% trans "Female" %}</option>
|
|
<option value="other" {% if staff.gender == 'other' %}selected{% endif %}>{% trans "Other" %}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="px-6 py-4 border-t border-slate-200 bg-slate-50 flex items-center justify-end gap-3">
|
|
<button type="button" onclick="closeEditStaffModal()"
|
|
class="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 transition">{% trans "Cancel" %}</button>
|
|
<button type="submit"
|
|
class="px-5 py-2 bg-navy text-white text-sm font-semibold rounded-xl hover:bg-navy/90 transition flex items-center gap-1.5">
|
|
<i data-lucide="save" class="w-4 h-4"></i> {% trans "Save Changes" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- 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 "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 "Sent" as t_sent %}
|
|
{% trans "Visibility" as t_visibility %}
|
|
{% trans "Acknowledged" as t_acknowledged %}
|
|
|
|
var recordModalLabels = {
|
|
complaint: '{{ t_complaint|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 }}',
|
|
sent: '{{ t_sent|escapejs }}',
|
|
visibility: '{{ t_visibility|escapejs }}',
|
|
acknowledged: '{{ t_acknowledged|escapejs }}',
|
|
};
|
|
|
|
var recordApiUrls = {
|
|
complaint: '/organizations/api/dept-complaint/',
|
|
appreciation: '/organizations/api/dept-appreciation/',
|
|
};
|
|
|
|
var recordTypeConfig = {
|
|
complaint: { icon: 'alert-circle', color: 'red', bg: 'bg-red-50', text: 'text-red-600' },
|
|
appreciation: { icon: 'heart', color: 'pink', bg: 'bg-pink-50', text: 'text-pink-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 === '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);
|
|
}
|
|
|
|
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 === '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>';
|
|
}
|
|
|
|
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',
|
|
draft: 'bg-slate-100 text-slate-600', activated: 'bg-cyan-50 text-cyan-700',
|
|
sent: 'bg-green-50 text-green-700', acknowledged: 'bg-purple-50 text-purple-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');
|
|
}
|
|
|
|
function openEditStaffModal() {
|
|
document.getElementById('editStaffModal').classList.remove('hidden');
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function closeEditStaffModal() {
|
|
document.getElementById('editStaffModal').classList.add('hidden');
|
|
}
|
|
</script>
|
|
{% endblock %}
|