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")),
|
("other", _("Other")),
|
||||||
],
|
],
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Relation to Patient"),
|
verbose_name=_("Complinant"),
|
||||||
help_text="Complainant's relationship to the patient",
|
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"""
|
"""Patient model"""
|
||||||
|
|
||||||
# Basic information
|
# Basic information
|
||||||
mrn = models.CharField(max_length=50, unique=True, verbose_name="Medical Record Number")
|
mrn = models.CharField(max_length=50, unique=True, verbose_name=_("Medical Record Number"))
|
||||||
national_id = EncryptedCharField(max_length=256, blank=True, default="")
|
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="")
|
national_id_hash = models.CharField(max_length=64, blank=True, db_index=True, default="")
|
||||||
|
|
||||||
first_name = models.CharField(max_length=100)
|
first_name = models.CharField(max_length=100, verbose_name=_("First Name"))
|
||||||
last_name = models.CharField(max_length=100)
|
last_name = models.CharField(max_length=100, verbose_name=_("Last Name"))
|
||||||
first_name_ar = models.CharField(max_length=100, blank=True)
|
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)
|
last_name_ar = models.CharField(max_length=100, blank=True, verbose_name=_("Last Name (Arabic)"))
|
||||||
|
|
||||||
# Demographics
|
# 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(
|
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)
|
nationality = models.CharField(max_length=100, blank=True, db_index=True)
|
||||||
|
|
||||||
# Contact
|
# Contact
|
||||||
phone = models.CharField(max_length=20, blank=True)
|
phone = models.CharField(max_length=20, blank=True, verbose_name=_("Phone"))
|
||||||
email = models.EmailField(blank=True)
|
email = models.EmailField(blank=True, verbose_name=_("Email"))
|
||||||
address = models.TextField(blank=True)
|
address = models.TextField(blank=True, verbose_name=_("Address"))
|
||||||
city = models.CharField(max_length=100, blank=True)
|
city = models.CharField(max_length=100, blank=True, verbose_name=_("City"))
|
||||||
|
|
||||||
# Primary hospital
|
# Primary hospital
|
||||||
primary_hospital = models.ForeignKey(
|
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
|
||||||
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:
|
class Meta:
|
||||||
ordering = ["last_name", "first_name"]
|
ordering = ["last_name", "first_name"]
|
||||||
|
|||||||
@ -91,6 +91,7 @@ class DepartmentSerializer(serializers.ModelSerializer):
|
|||||||
hospital_name = serializers.CharField(source="hospital.name", read_only=True)
|
hospital_name = serializers.CharField(source="hospital.name", read_only=True)
|
||||||
parent_name = serializers.CharField(source="parent.name", read_only=True)
|
parent_name = serializers.CharField(source="parent.name", read_only=True)
|
||||||
manager_name = serializers.SerializerMethodField()
|
manager_name = serializers.SerializerMethodField()
|
||||||
|
display_name = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Department
|
model = Department
|
||||||
@ -101,6 +102,7 @@ class DepartmentSerializer(serializers.ModelSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"name_en",
|
"name_en",
|
||||||
"name_ar",
|
"name_ar",
|
||||||
|
"display_name",
|
||||||
"code",
|
"code",
|
||||||
"category",
|
"category",
|
||||||
"parent",
|
"parent",
|
||||||
@ -124,6 +126,10 @@ class DepartmentSerializer(serializers.ModelSerializer):
|
|||||||
return obj.manager.get_full_name()
|
return obj.manager.get_full_name()
|
||||||
return None
|
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):
|
class StaffSerializer(serializers.ModelSerializer):
|
||||||
"""Staff serializer"""
|
"""Staff serializer"""
|
||||||
|
|||||||
@ -2165,6 +2165,36 @@ def department_detail(request, pk):
|
|||||||
"existing_ar": inq.department_response_ar or "",
|
"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)
|
# Standards for this department (global + department-specific)
|
||||||
from apps.standards.models import Standard, StandardCompliance
|
from apps.standards.models import Standard, StandardCompliance
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
@ -2248,12 +2278,7 @@ def department_detail(request, pk):
|
|||||||
status__in=["open", "in_progress"],
|
status__in=["open", "in_progress"],
|
||||||
involved_departments__department=department,
|
involved_departments__department=department,
|
||||||
).distinct().select_related("department", "assigned_to")[:5],
|
).distinct().select_related("department", "assigned_to")[:5],
|
||||||
"pending_routing_involvements": ComplaintInvolvedDepartment.objects.filter(
|
"pending_routing_involvements": pending_routing_involvements,
|
||||||
department=department,
|
|
||||||
sent=True,
|
|
||||||
response_submitted=False,
|
|
||||||
routing_status="sent",
|
|
||||||
).select_related("complaint", "complaint__department", "department").order_by("-sent_at")[:10],
|
|
||||||
"hospital_departments": (
|
"hospital_departments": (
|
||||||
Department.objects.filter(hospital=department.hospital, status="active")
|
Department.objects.filter(hospital=department.hospital, status="active")
|
||||||
.filter(Q(champion__isnull=False) | Q(manager__isnull=False))
|
.filter(Q(champion__isnull=False) | Q(manager__isnull=False))
|
||||||
|
|||||||
@ -398,6 +398,8 @@ class ConvertToProjectForm(forms.Form):
|
|||||||
self.action = kwargs.pop("action", None)
|
self.action = kwargs.pop("action", None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
from apps.organizations.models import Staff
|
||||||
|
|
||||||
if self.user and self.user.hospital:
|
if self.user and self.user.hospital:
|
||||||
# Filter templates by hospital (or global)
|
# Filter templates by hospital (or global)
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -408,7 +410,6 @@ class ConvertToProjectForm(forms.Form):
|
|||||||
).order_by("name")
|
).order_by("name")
|
||||||
|
|
||||||
# Filter project lead by hospital
|
# Filter project lead by hospital
|
||||||
from apps.organizations.models import Staff
|
|
||||||
self.fields["project_lead"].queryset = Staff.objects.filter(
|
self.fields["project_lead"].queryset = Staff.objects.filter(
|
||||||
hospital=self.user.hospital, status="active"
|
hospital=self.user.hospital, status="active"
|
||||||
).order_by("first_name", "last_name")
|
).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 %}
|
{% endif %}
|
||||||
<div class="mt-3 flex items-center justify-end gap-2 {% if not dept_response_progress.pending %}ml-12{% 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>
|
<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">
|
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" %}
|
{% trans "Proceed to Resolve" %}
|
||||||
</button>
|
</button>
|
||||||
@ -313,7 +313,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif workflow_steps.resolved %}
|
{% elif workflow_steps.resolved %}
|
||||||
@ -759,7 +759,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</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">
|
<div class="flex items-center gap-3">
|
||||||
<button type="button" onclick="openSendToDeptModal()"
|
<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">
|
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)" %}
|
{% trans "PDF (Letterhead)" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %} {% endcomment %}
|
||||||
|
|
||||||
{% include "complaints/partials/workflow_timeline.html" %}
|
{% include "complaints/partials/workflow_timeline.html" %}
|
||||||
</div>
|
</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 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><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>
|
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif workflow_steps.resolved %}
|
{% elif workflow_steps.resolved %}
|
||||||
|
|||||||
@ -823,7 +823,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
(data || []).forEach(dept => {
|
(data || []).forEach(dept => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = dept.id;
|
option.value = dept.id;
|
||||||
option.textContent = dept.name_en || dept.name;
|
option.textContent = dept.display_name || dept.name_en || dept.name;
|
||||||
departmentSelect.appendChild(option);
|
departmentSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -860,7 +860,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
(data || []).forEach(sec => {
|
(data || []).forEach(sec => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = sec.id;
|
opt.value = sec.id;
|
||||||
opt.textContent = sec.name_en || sec.name;
|
opt.textContent = sec.display_name || sec.name_en || sec.name;
|
||||||
sectionSelect.appendChild(opt);
|
sectionSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@ -180,7 +180,7 @@
|
|||||||
|
|
||||||
{% if can_manage_actions and complaint.is_active_status %}
|
{% if can_manage_actions and complaint.is_active_status %}
|
||||||
{% if complaint.assigned_to == current_user or can_manage_actions %}
|
{% 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 %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="status" value="resolved">
|
<input type="hidden" name="status" value="resolved">
|
||||||
|
|
||||||
|
|||||||
@ -182,6 +182,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-center">
|
<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.type == 'complaint_department_response' %}
|
||||||
{% if action.explanation_url and action.explanation_url != '#' %}
|
{% if action.explanation_url and action.explanation_url != '#' %}
|
||||||
<a href="{{ action.explanation_url }}"
|
<a href="{{ action.explanation_url }}"
|
||||||
@ -197,6 +198,12 @@
|
|||||||
{% trans "Respond" %}
|
{% trans "Respond" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% 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' %}
|
{% elif action.type == 'observation_response' %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onclick="openDeptResponseModal('observation', '{{ action.item_id }}', '{{ action.url }}', '{{ action.reference }}', '{{ action.subject|escapejs }}', '{{ action.existing_en|escapejs }}', '{{ action.existing_ar|escapejs }}')"
|
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" %}
|
{% trans "Respond" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -232,6 +240,47 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if active_investigations and can_respond %}
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-blue-200 p-5 mb-6">
|
<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="flex items-center gap-3 mb-4">
|
||||||
@ -383,85 +432,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 -->
|
<!-- Department Head Info -->
|
||||||
{% if staff_head %}
|
{% if staff_head %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user