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.
345 lines
17 KiB
HTML
345 lines
17 KiB
HTML
{% extends "layouts/public_base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% trans "Report an Observation" %}{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.file-upload-area {
|
|
border: 2px dashed #e2e8f0;
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.file-upload-area:hover {
|
|
border-color: #005696;
|
|
background: #f8fafc;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="min-h-screen py-8 px-4 sm:px-6 lg:px-8">
|
|
<div class="max-w-2xl mx-auto">
|
|
<!-- Main Card -->
|
|
<div class="glass-card rounded-3xl shadow-2xl p-8 md:p-10 animate-fade-in">
|
|
<!-- Header -->
|
|
<div class="text-center mb-6">
|
|
<div class="w-16 h-16 bg-gradient-to-br from-navy to-blue rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg">
|
|
<i data-lucide="eye" class="w-8 h-8 text-white"></i>
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-navy mb-2">{% trans "Report an Observation" %}</h1>
|
|
<p class="text-slate">{% trans "Help us improve by reporting issues you notice" %}</p>
|
|
</div>
|
|
|
|
<!-- Anonymous Notice -->
|
|
<div class="bg-blue-50 border border-blue-100 rounded-xl p-4 mb-6">
|
|
<div class="flex items-start gap-3">
|
|
<i data-lucide="shield-check" class="w-5 h-5 text-navy mt-0.5 flex-shrink-0"></i>
|
|
<div>
|
|
<strong class="text-navy text-sm">{% trans "Anonymous Reporting" %}</strong>
|
|
<p class="text-slate text-sm mt-1 mb-0">
|
|
{% trans "You can submit this report anonymously. Providing your information is optional but may help us follow up if needed." %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages -->
|
|
{% if messages %}
|
|
{% for message in messages %}
|
|
<div class="bg-green-50 border border-green-200 rounded-xl p-4 mb-4 text-sm text-green-800">
|
|
{{ message }}
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
<!-- Form -->
|
|
<form method="post" enctype="multipart/form-data" id="observationForm">
|
|
{% csrf_token %}
|
|
|
|
<!-- Honeypot field (hidden) -->
|
|
{{ form.website }}
|
|
|
|
<!-- Hospital -->
|
|
<div class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="building-2" class="w-4 h-4"></i>
|
|
{% trans "Hospital" %}
|
|
<span class="text-xs font-normal text-slate">({% trans "optional" %})</span>
|
|
</label>
|
|
<select name="hospital" id="id_hospital" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select hospital" %}</option>
|
|
{% for h in hospitals %}
|
|
<option value="{{ h.id }}">{{ h.name_en }}{% if h.name_ar %} - {{ h.name_ar }}{% endif %}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Location Type -->
|
|
<div class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="map-pin" class="w-4 h-4"></i>
|
|
{% trans "Location Type" %}
|
|
<span class="text-red-500">*</span>
|
|
</label>
|
|
<select name="location_type" id="id_location_type" required class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select Location Type" %}</option>
|
|
<option value="OP">{% trans "Outpatient (OP)" %}</option>
|
|
<option value="IP">{% trans "Inpatient (IP)" %}</option>
|
|
<option value="ER">{% trans "Emergency (ER)" %}</option>
|
|
<option value="GENERAL">{% trans "General" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Area (cascading from hospital + location_type) -->
|
|
<div id="obs_area_container" style="display: none;" class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="map" class="w-4 h-4"></i>
|
|
{% trans "Area" %}
|
|
<span class="text-xs font-normal text-slate">({% trans "optional" %})</span>
|
|
</label>
|
|
<select name="area" id="id_obs_area" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select Area (optional)" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Category -->
|
|
<div class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="tag" class="w-4 h-4"></i>
|
|
{% trans "Category" %}
|
|
<span class="text-xs font-normal text-slate">({% trans "optional" %})</span>
|
|
</label>
|
|
<select name="category" id="id_category" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select Category" %}</option>
|
|
<option value="medical">{% trans "Medical" %}</option>
|
|
<option value="nursing">{% trans "Nursing" %}</option>
|
|
<option value="administrative">{% trans "Administrative" %}</option>
|
|
<option value="support_services">{% trans "Support Services" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Department & Section (cascading) -->
|
|
<div id="obs_dept_container" style="display: none;" class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="building" class="w-4 h-4"></i>
|
|
{% trans "Department" %}
|
|
<span class="text-xs font-normal text-slate">({% trans "optional" %})</span>
|
|
</label>
|
|
<select name="department" id="id_obs_department" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select Department" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="obs_section_container" style="display: none;" class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="layers" class="w-4 h-4"></i>
|
|
{% trans "Section" %}
|
|
<span class="text-xs font-normal text-slate">({% trans "optional" %})</span>
|
|
</label>
|
|
<select name="section" id="id_obs_section" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none">
|
|
<option value="">{% trans "Select Section" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="file-text" class="w-4 h-4"></i>
|
|
{% trans "Description" %}
|
|
<span class="text-red-500">*</span>
|
|
</label>
|
|
{{ form.description }}
|
|
<p class="text-xs text-slate mt-1.5">{% trans "Please describe what you observed in detail." %}</p>
|
|
</div>
|
|
|
|
<!-- Location Text (free text) -->
|
|
<div class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="map-pin" class="w-4 h-4"></i>
|
|
{% trans "Location Details" %}
|
|
<span class="text-xs font-normal text-slate">({% trans "optional" %})</span>
|
|
</label>
|
|
{{ form.location_text }}
|
|
</div>
|
|
|
|
<!-- Incident Date/Time -->
|
|
<div class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="calendar" class="w-4 h-4"></i>
|
|
{% trans "When did this occur?" %}
|
|
</label>
|
|
{{ form.incident_datetime }}
|
|
</div>
|
|
|
|
<!-- Attachments -->
|
|
<div class="mb-6">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-3">
|
|
<i data-lucide="paperclip" class="w-4 h-4"></i>
|
|
{% trans "Attachments" %}
|
|
<span class="text-xs font-normal text-slate">({% trans "optional" %})</span>
|
|
</label>
|
|
<div class="file-upload-area" onclick="document.getElementById('attachments').click()">
|
|
<i data-lucide="cloud-upload" class="w-8 h-8 text-navy mx-auto mb-2"></i>
|
|
<p class="text-sm font-semibold text-slate mb-1">{% trans "Click to upload files" %}</p>
|
|
<p class="text-xs text-slate mb-0">{% trans "Images, PDF, Word, Excel (max 10MB each)" %}</p>
|
|
</div>
|
|
<input type="file" id="attachments" name="attachments" multiple
|
|
accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx"
|
|
style="display: none;" onchange="updateFileList()">
|
|
<div id="fileList" class="mt-2"></div>
|
|
</div>
|
|
|
|
<!-- Reporter Information -->
|
|
<div class="mb-8">
|
|
<label class="flex items-center gap-2 text-sm font-bold text-navy mb-2">
|
|
<i data-lucide="user" class="w-4 h-4"></i>
|
|
{% trans "Your Information" %}
|
|
<span class="text-xs font-normal text-slate">({% trans "optional" %})</span>
|
|
</label>
|
|
<p class="text-xs text-slate mb-3">{% trans "Providing your information helps us follow up if needed. Leave blank to submit anonymously." %}</p>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate mb-1.5">{% trans "Staff ID" %}</label>
|
|
{{ form.reporter_staff_id }}
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate mb-1.5">{% trans "Name" %}</label>
|
|
{{ form.reporter_name }}
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate mb-1.5">{% trans "Phone" %}</label>
|
|
{{ form.reporter_phone }}
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate mb-1.5">{% trans "Email" %}</label>
|
|
{{ form.reporter_email }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submit -->
|
|
<button type="submit" class="w-full bg-navy text-white py-4 rounded-xl font-bold text-lg hover:bg-blue transition shadow-lg shadow-navy/25 flex items-center justify-center gap-2 btn-hover">
|
|
<i data-lucide="send" class="w-5 h-5"></i>
|
|
{% trans "Submit Observation" %}
|
|
</button>
|
|
|
|
<!-- Track Link -->
|
|
<div class="text-center mt-4">
|
|
<a href="{% url 'observations:observation_track' %}" class="text-slate hover:text-navy text-sm font-medium transition inline-flex items-center gap-1">
|
|
<i data-lucide="search" class="w-4 h-4"></i>
|
|
{% trans "Track an existing observation" %}
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
function updateFileList() {
|
|
const input = document.getElementById('attachments');
|
|
const fileList = document.getElementById('fileList');
|
|
fileList.innerHTML = '';
|
|
|
|
if (input.files.length > 0) {
|
|
let html = '<div class="space-y-2 mt-3">';
|
|
for (let i = 0; i < input.files.length; i++) {
|
|
html += '<div class="flex items-center gap-2 bg-slate-50 rounded-lg px-3 py-2 text-sm text-slate"><i data-lucide="file" class="w-4 h-4"></i>' + input.files[i].name + '</div>';
|
|
}
|
|
html += '</div>';
|
|
fileList.innerHTML = html;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
// Hospital -> Location Type -> Area -> Category -> Department -> Section cascade
|
|
var obsHospitalSel = document.getElementById('id_hospital');
|
|
var obsLocationTypeSel = document.getElementById('id_location_type');
|
|
var obsAreaSel = document.getElementById('id_obs_area');
|
|
var obsDeptSel = document.getElementById('id_obs_department');
|
|
var obsSectionSel = document.getElementById('id_obs_section');
|
|
var obsCategorySel = document.getElementById('id_category');
|
|
var obsAreaContainer = document.getElementById('obs_area_container');
|
|
var obsDeptContainer = document.getElementById('obs_dept_container');
|
|
var obsSectionContainer = document.getElementById('obs_section_container');
|
|
|
|
function obsLoadAreas(hospitalId, locationType) {
|
|
obsAreaSel.innerHTML = '<option value="">{% trans "Select Area (optional)" %}</option>';
|
|
if (!hospitalId) { obsAreaContainer.style.display = 'none'; return; }
|
|
var url = '/organizations/dropdowns/areas/?hospital=' + encodeURIComponent(hospitalId);
|
|
if (locationType) url += '&location_type=' + encodeURIComponent(locationType);
|
|
fetch(url)
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
(data || []).forEach(function(a) {
|
|
var opt = document.createElement('option');
|
|
opt.value = a.id;
|
|
opt.textContent = a.name_en;
|
|
obsAreaSel.appendChild(opt);
|
|
});
|
|
obsAreaContainer.style.display = (data && data.length) ? 'block' : 'none';
|
|
})
|
|
.catch(function(err) { console.error(err); });
|
|
}
|
|
|
|
function obsLoadDepartments(hospitalId, category) {
|
|
obsDeptSel.innerHTML = '<option value="">{% trans "Select Department" %}</option>';
|
|
obsSectionSel.innerHTML = '<option value="">{% trans "Select Section" %}</option>';
|
|
obsSectionContainer.style.display = 'none';
|
|
if (!hospitalId) { obsDeptContainer.style.display = 'none'; return; }
|
|
var url = '/organizations/dropdowns/departments-by-category/?hospital=' + encodeURIComponent(hospitalId);
|
|
if (category) url += '&category=' + encodeURIComponent(category);
|
|
fetch(url)
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
(data || []).forEach(function(d) {
|
|
var opt = document.createElement('option');
|
|
opt.value = d.id;
|
|
opt.textContent = d.name_en || d.name;
|
|
obsDeptSel.appendChild(opt);
|
|
});
|
|
obsDeptContainer.style.display = (data && data.length) ? 'block' : 'none';
|
|
})
|
|
.catch(function(err) { console.error(err); });
|
|
}
|
|
|
|
obsLocationTypeSel.addEventListener('change', function() {
|
|
obsLoadAreas(obsHospitalSel.value, this.value);
|
|
});
|
|
|
|
obsCategorySel.addEventListener('change', function() {
|
|
obsLoadDepartments(obsHospitalSel.value, this.value);
|
|
});
|
|
|
|
obsHospitalSel.addEventListener('change', function() {
|
|
obsLoadAreas(this.value, obsLocationTypeSel.value);
|
|
obsLoadDepartments(this.value, obsCategorySel.value);
|
|
});
|
|
|
|
obsDeptSel.addEventListener('change', function() {
|
|
obsSectionSel.innerHTML = '<option value="">{% trans "Select Section" %}</option>';
|
|
if (!this.value) { obsSectionContainer.style.display = 'none'; return; }
|
|
fetch('/organizations/dropdowns/sections/' + this.value + '/')
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
(data || []).forEach(function(s) {
|
|
var opt = document.createElement('option');
|
|
opt.value = s.id;
|
|
opt.textContent = s.name_en || s.name;
|
|
obsSectionSel.appendChild(opt);
|
|
});
|
|
obsSectionContainer.style.display = (data && data.length) ? 'block' : 'none';
|
|
})
|
|
.catch(function(err) { console.error(err); });
|
|
});
|
|
</script>
|
|
{% endblock %}
|