more update
This commit is contained in:
parent
5dc61c8e30
commit
d63ed6f956
@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.1 on 2026-07-05 14:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('complaints', '0035_involved_department_routing_rejection'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='complaint',
|
||||
name='relation_to_patient',
|
||||
field=models.CharField(blank=True, choices=[('patient', 'Patient'), ('relative', 'Relative'), ('friend', 'Friend'), ('other', 'Other')], help_text="Complainant's relationship to the patient", max_length=20, verbose_name='Complinant'),
|
||||
),
|
||||
]
|
||||
@ -226,7 +226,7 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel):
|
||||
("other", _("Other")),
|
||||
],
|
||||
blank=True,
|
||||
verbose_name=_("Relation to Patient"),
|
||||
verbose_name=_("Complinant"),
|
||||
help_text="Complainant's relationship to the patient",
|
||||
)
|
||||
|
||||
|
||||
80
apps/organizations/migrations/0016_patient_verbose_names.py
Normal file
80
apps/organizations/migrations/0016_patient_verbose_names.py
Normal file
@ -0,0 +1,80 @@
|
||||
# Generated by Django 6.0.1 on 2026-07-05 13:33
|
||||
|
||||
import apps.core.encryption
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('organizations', '0015_hospital_branding'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, verbose_name='Address'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='City'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='date_of_birth',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Date of Birth'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name='Email'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='first_name',
|
||||
field=models.CharField(max_length=100, verbose_name='First Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='first_name_ar',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='First Name (Arabic)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='gender',
|
||||
field=models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female'), ('other', 'Other')], max_length=10, verbose_name='Gender'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='last_name',
|
||||
field=models.CharField(max_length=100, verbose_name='Last Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='last_name_ar',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Last Name (Arabic)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='national_id',
|
||||
field=apps.core.encryption.EncryptedCharField(blank=True, default='', max_length=256, verbose_name='National ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, max_length=20, verbose_name='Phone'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='primary_hospital',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patients', to='organizations.hospital', verbose_name='Primary Hospital'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='patient',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], db_index=True, default='active', max_length=20, verbose_name='Status'),
|
||||
),
|
||||
]
|
||||
@ -559,35 +559,37 @@ class Patient(UUIDModel, TimeStampedModel):
|
||||
"""Patient model"""
|
||||
|
||||
# Basic information
|
||||
mrn = models.CharField(max_length=50, unique=True, verbose_name="Medical Record Number")
|
||||
national_id = EncryptedCharField(max_length=256, blank=True, default="")
|
||||
mrn = models.CharField(max_length=50, unique=True, verbose_name=_("Medical Record Number"))
|
||||
national_id = EncryptedCharField(max_length=256, blank=True, default="", verbose_name=_("National ID"))
|
||||
national_id_hash = models.CharField(max_length=64, blank=True, db_index=True, default="")
|
||||
|
||||
first_name = models.CharField(max_length=100)
|
||||
last_name = models.CharField(max_length=100)
|
||||
first_name_ar = models.CharField(max_length=100, blank=True)
|
||||
last_name_ar = models.CharField(max_length=100, blank=True)
|
||||
first_name = models.CharField(max_length=100, verbose_name=_("First Name"))
|
||||
last_name = models.CharField(max_length=100, verbose_name=_("Last Name"))
|
||||
first_name_ar = models.CharField(max_length=100, blank=True, verbose_name=_("First Name (Arabic)"))
|
||||
last_name_ar = models.CharField(max_length=100, blank=True, verbose_name=_("Last Name (Arabic)"))
|
||||
|
||||
# Demographics
|
||||
date_of_birth = models.DateField(null=True, blank=True)
|
||||
date_of_birth = models.DateField(null=True, blank=True, verbose_name=_("Date of Birth"))
|
||||
gender = models.CharField(
|
||||
max_length=10, choices=[("male", _("Male")), ("female", _("Female")), ("other", _("Other"))], blank=True
|
||||
max_length=10, choices=[("male", _("Male")), ("female", _("Female")), ("other", _("Other"))], blank=True,
|
||||
verbose_name=_("Gender"),
|
||||
)
|
||||
nationality = models.CharField(max_length=100, blank=True, db_index=True)
|
||||
|
||||
# Contact
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
email = models.EmailField(blank=True)
|
||||
address = models.TextField(blank=True)
|
||||
city = models.CharField(max_length=100, blank=True)
|
||||
phone = models.CharField(max_length=20, blank=True, verbose_name=_("Phone"))
|
||||
email = models.EmailField(blank=True, verbose_name=_("Email"))
|
||||
address = models.TextField(blank=True, verbose_name=_("Address"))
|
||||
city = models.CharField(max_length=100, blank=True, verbose_name=_("City"))
|
||||
|
||||
# Primary hospital
|
||||
primary_hospital = models.ForeignKey(
|
||||
Hospital, on_delete=models.SET_NULL, null=True, blank=True, related_name="patients"
|
||||
Hospital, on_delete=models.SET_NULL, null=True, blank=True, related_name="patients",
|
||||
verbose_name=_("Primary Hospital"),
|
||||
)
|
||||
|
||||
# Status
|
||||
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
||||
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True, verbose_name=_("Status"))
|
||||
|
||||
class Meta:
|
||||
ordering = ["last_name", "first_name"]
|
||||
|
||||
@ -91,6 +91,7 @@ class DepartmentSerializer(serializers.ModelSerializer):
|
||||
hospital_name = serializers.CharField(source="hospital.name", read_only=True)
|
||||
parent_name = serializers.CharField(source="parent.name", read_only=True)
|
||||
manager_name = serializers.SerializerMethodField()
|
||||
display_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Department
|
||||
@ -101,6 +102,7 @@ class DepartmentSerializer(serializers.ModelSerializer):
|
||||
"name",
|
||||
"name_en",
|
||||
"name_ar",
|
||||
"display_name",
|
||||
"code",
|
||||
"category",
|
||||
"parent",
|
||||
@ -124,6 +126,10 @@ class DepartmentSerializer(serializers.ModelSerializer):
|
||||
return obj.manager.get_full_name()
|
||||
return None
|
||||
|
||||
def get_display_name(self, obj):
|
||||
"""Localized name respecting the active language (set by LocaleMiddleware)."""
|
||||
return obj.get_localized_name()
|
||||
|
||||
|
||||
class StaffSerializer(serializers.ModelSerializer):
|
||||
"""Staff serializer"""
|
||||
|
||||
@ -2165,6 +2165,36 @@ def department_detail(request, pk):
|
||||
"existing_ar": inq.department_response_ar or "",
|
||||
})
|
||||
|
||||
# Merge routed complaints into pending_actions (unified table)
|
||||
_existing_complaint_ids = {
|
||||
str(a.get("complaint_id")) for a in pending_actions if a.get("complaint_id")
|
||||
}
|
||||
pending_routing_involvements = list(
|
||||
ComplaintInvolvedDepartment.objects.filter(
|
||||
department=department,
|
||||
sent=True,
|
||||
response_submitted=False,
|
||||
routing_status="sent",
|
||||
).select_related("complaint", "complaint__department", "department").order_by("-sent_at")[:10]
|
||||
)
|
||||
for inv in pending_routing_involvements:
|
||||
if str(inv.complaint_id) not in _existing_complaint_ids:
|
||||
pending_actions.append({
|
||||
"type": "complaint_department_response",
|
||||
"type_label": _("Complaint Response"),
|
||||
"reference": inv.complaint.reference_number or str(inv.complaint.id),
|
||||
"subject": inv.complaint.title or _("No title"),
|
||||
"complaint_id": str(inv.complaint_id),
|
||||
"item_id": str(inv.id),
|
||||
"routing_inv_id": str(inv.id),
|
||||
"sla_due_at": None,
|
||||
"is_overdue": False,
|
||||
"url": "#",
|
||||
"explanation_url": "#",
|
||||
"badge_color": "blue",
|
||||
})
|
||||
_existing_complaint_ids.add(str(inv.complaint_id))
|
||||
|
||||
# Standards for this department (global + department-specific)
|
||||
from apps.standards.models import Standard, StandardCompliance
|
||||
from django.db.models import Count
|
||||
@ -2248,12 +2278,7 @@ def department_detail(request, pk):
|
||||
status__in=["open", "in_progress"],
|
||||
involved_departments__department=department,
|
||||
).distinct().select_related("department", "assigned_to")[:5],
|
||||
"pending_routing_involvements": ComplaintInvolvedDepartment.objects.filter(
|
||||
department=department,
|
||||
sent=True,
|
||||
response_submitted=False,
|
||||
routing_status="sent",
|
||||
).select_related("complaint", "complaint__department", "department").order_by("-sent_at")[:10],
|
||||
"pending_routing_involvements": pending_routing_involvements,
|
||||
"hospital_departments": (
|
||||
Department.objects.filter(hospital=department.hospital, status="active")
|
||||
.filter(Q(champion__isnull=False) | Q(manager__isnull=False))
|
||||
|
||||
@ -398,6 +398,8 @@ class ConvertToProjectForm(forms.Form):
|
||||
self.action = kwargs.pop("action", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
from apps.organizations.models import Staff
|
||||
|
||||
if self.user and self.user.hospital:
|
||||
# Filter templates by hospital (or global)
|
||||
from django.db.models import Q
|
||||
@ -408,7 +410,6 @@ class ConvertToProjectForm(forms.Form):
|
||||
).order_by("name")
|
||||
|
||||
# Filter project lead by hospital
|
||||
from apps.organizations.models import Staff
|
||||
self.fields["project_lead"].queryset = Staff.objects.filter(
|
||||
hospital=self.user.hospital, status="active"
|
||||
).order_by("first_name", "last_name")
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.1 on 2026-07-05 14:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reports', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='generatedreport',
|
||||
name='data_source',
|
||||
field=models.CharField(choices=[('complaints', 'Complaints'), ('inquiries', 'Inquiries'), ('observations', 'Observations'), ('px_actions', 'PX Actions'), ('surveys', 'Surveys'), ('physicians', 'Physician Ratings'), ('staff', 'Staff'), ('department', 'Departments'), ('suggestion', 'Suggestions'), ('appreciation', 'Appreciations'), ('patient', 'Patients')], max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reporttemplate',
|
||||
name='data_source',
|
||||
field=models.CharField(choices=[('complaints', 'Complaints'), ('inquiries', 'Inquiries'), ('observations', 'Observations'), ('px_actions', 'PX Actions'), ('surveys', 'Surveys'), ('physicians', 'Physician Ratings'), ('staff', 'Staff'), ('department', 'Departments'), ('suggestion', 'Suggestions'), ('appreciation', 'Appreciations'), ('patient', 'Patients')], default='complaints', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='savedreport',
|
||||
name='data_source',
|
||||
field=models.CharField(choices=[('complaints', 'Complaints'), ('inquiries', 'Inquiries'), ('observations', 'Observations'), ('px_actions', 'PX Actions'), ('surveys', 'Surveys'), ('physicians', 'Physician Ratings'), ('staff', 'Staff'), ('department', 'Departments'), ('suggestion', 'Suggestions'), ('appreciation', 'Appreciations'), ('patient', 'Patients')], default='complaints', max_length=50),
|
||||
),
|
||||
]
|
||||
BIN
data/visit_data.json.zip
Normal file
BIN
data/visit_data.json.zip
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -290,7 +290,7 @@
|
||||
{% endif %}
|
||||
<div class="mt-3 flex items-center justify-end gap-2 {% if not dept_response_progress.pending %}ml-12{% endif %}">
|
||||
<span class="text-xs text-amber-600">{% trans "Don't want to wait?" %}</span>
|
||||
<button type="button" onclick="switchTab('resolution')"
|
||||
<button type="button" onclick="switchTab('resolution', 'resolutionForm')"
|
||||
class="px-4 py-2 bg-amber-600 text-white rounded-lg font-bold text-sm hover:bg-amber-700 transition">
|
||||
{% trans "Proceed to Resolve" %}
|
||||
</button>
|
||||
@ -313,7 +313,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="switchTab('resolution')" class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700 transition whitespace-nowrap shrink-0">{% trans "Generate Resolution" %}</button>
|
||||
<button type="button" onclick="switchTab('resolution', 'resolutionForm')" class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700 transition whitespace-nowrap shrink-0">{% trans "Generate Resolution" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif workflow_steps.resolved %}
|
||||
@ -759,7 +759,7 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% if can_manage_actions and complaint.is_active_status %}
|
||||
{% comment %} {% if can_manage_actions and complaint.is_active_status %}
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" onclick="openSendToDeptModal()"
|
||||
class="flex-1 px-4 py-2.5 bg-navy text-white font-bold text-sm rounded-xl flex items-center justify-center gap-2 hover:bg-blue transition">
|
||||
@ -776,7 +776,7 @@
|
||||
{% trans "PDF (Letterhead)" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %} {% endcomment %}
|
||||
|
||||
{% include "complaints/partials/workflow_timeline.html" %}
|
||||
</div>
|
||||
|
||||
@ -237,7 +237,7 @@
|
||||
<div class="w-9 h-9 rounded-full bg-green-100 flex items-center justify-center shrink-0"><i data-lucide="check-circle-2" class="w-4 h-4 text-green-600"></i></div>
|
||||
<div><p class="text-sm font-bold text-navy">{% trans "Resolve this complaint" %}</p><p class="text-xs text-slate">{% trans "The department has responded. Review and close the complaint." %}</p></div>
|
||||
</div>
|
||||
<button type="button" onclick="switchTab('resolution')" class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700 transition whitespace-nowrap shrink-0">{% trans "Generate Resolution" %}</button>
|
||||
<button type="button" onclick="switchTab('resolution', 'resolutionForm')" class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700 transition whitespace-nowrap shrink-0">{% trans "Generate Resolution" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif workflow_steps.resolved %}
|
||||
|
||||
@ -823,7 +823,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
(data || []).forEach(dept => {
|
||||
const option = document.createElement('option');
|
||||
option.value = dept.id;
|
||||
option.textContent = dept.name_en || dept.name;
|
||||
option.textContent = dept.display_name || dept.name_en || dept.name;
|
||||
departmentSelect.appendChild(option);
|
||||
});
|
||||
})
|
||||
@ -860,7 +860,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
(data || []).forEach(sec => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = sec.id;
|
||||
opt.textContent = sec.name_en || sec.name;
|
||||
opt.textContent = sec.display_name || sec.name_en || sec.name;
|
||||
sectionSelect.appendChild(opt);
|
||||
});
|
||||
})
|
||||
|
||||
@ -180,7 +180,7 @@
|
||||
|
||||
{% if can_manage_actions and complaint.is_active_status %}
|
||||
{% if complaint.assigned_to == current_user or can_manage_actions %}
|
||||
<form method="post" action="{% url 'complaints:complaint_change_status' pk=complaint.pk %}" id="resolutionForm">
|
||||
<form method="post" action="{% url 'complaints:complaint_change_status' pk=complaint.pk %}" id="resolutionForm" class="scroll-mt-28">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="status" value="resolved">
|
||||
|
||||
|
||||
@ -182,6 +182,7 @@
|
||||
{% 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 }}"
|
||||
@ -197,6 +198,12 @@
|
||||
{% 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 }}')"
|
||||
@ -218,6 +225,7 @@
|
||||
{% trans "Respond" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -232,6 +240,47 @@
|
||||
</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">
|
||||
@ -383,85 +432,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_respond and pending_routing_involvements %}
|
||||
<div class="bg-white rounded-xl shadow-sm border-2 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="inbox" class="w-5 h-5 text-blue-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-navy">{% trans "Incoming Complaints" %}</h3>
|
||||
<p class="text-xs text-slate-400">{% trans "Complaints routed to your department awaiting your response" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{% for inv in pending_routing_involvements %}
|
||||
<div class="border border-slate-200 rounded-xl p-4 bg-slate-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=inv.complaint.pk %}" class="font-mono text-xs font-bold text-navy hover:underline">
|
||||
{{ inv.complaint.reference_number }}
|
||||
</a>
|
||||
<p class="text-sm text-slate mt-0.5">{{ inv.complaint.title|truncatechars:60 }}</p>
|
||||
</div>
|
||||
{% if inv.is_primary %}
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase whitespace-nowrap bg-navy/10 text-navy">{% trans "Primary" %}</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase whitespace-nowrap bg-slate-100 text-slate-600">{{ inv.get_role_display }}</span>
|
||||
{% endif %}
|
||||
</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="send" class="w-3.5 h-3.5"></i>
|
||||
{% if inv.sent_at %}{% trans "Sent" %} {{ inv.sent_at|date:"d M Y" }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{% url 'complaints:complaint_detail' pk=inv.complaint.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" %}
|
||||
</a>
|
||||
<button type="button" onclick="toggleRejectRouting('{{ inv.pk }}')"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 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.5 h-3.5"></i> {% trans "Wrong Department" %}
|
||||
</button>
|
||||
</div>
|
||||
<form method="post" action="{% url 'complaints:involved_department_reject_routing' pk=inv.pk %}" id="reject-routing-{{ inv.pk }}" class="hidden mt-3 pt-3 border-t border-slate-100 space-y-3">
|
||||
{% csrf_token %}
|
||||
<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>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function toggleRejectRouting(pk) {
|
||||
const form = document.getElementById('reject-routing-' + pk);
|
||||
if (form) form.classList.toggle('hidden');
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Department Head Info -->
|
||||
{% if staff_head %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user