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.
285 lines
11 KiB
HTML
285 lines
11 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% trans "New Observation" %} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.form-label {
|
|
display: block;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.field-error {
|
|
color: #ef4444;
|
|
font-size: 0.75rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
.form-control, .form-select {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
border: 2px solid #cbd5e1;
|
|
border-radius: 0.75rem;
|
|
font-size: 0.95rem;
|
|
transition: all 0.2s;
|
|
background: white;
|
|
color: #1e293b;
|
|
font-family: 'Inter', 'Noto Kufi Arabic', sans-serif;
|
|
}
|
|
.form-control:focus, .form-select:focus {
|
|
outline: none;
|
|
border-color: #005696;
|
|
box-shadow: 0 0 0 4px rgba(0, 86, 150, 0.1);
|
|
}
|
|
textarea.form-control {
|
|
resize: vertical;
|
|
min-height: 120px;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="mb-6">
|
|
<a href="{% url 'observations:observation_list' %}" class="text-blue-600 hover:text-blue-800 text-sm font-medium inline-flex items-center gap-1 mb-2">
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
|
{% trans "Back to Observations" %}
|
|
</a>
|
|
<h1 class="text-2xl font-bold text-gray-900">{% trans "New Observation" %}</h1>
|
|
</div>
|
|
|
|
{% if messages %}
|
|
{% for message in messages %}
|
|
<div class="bg-{% if message.tags == 'error' %}red{% elif message.tags == 'warning' %}yellow{% else %}green{% endif %}-50 border border-{% if message.tags == 'error' %}red{% elif message.tags == 'warning' %}yellow{% else %}green{% endif %}-200 rounded-xl p-4 mb-4 text-sm text-{% if message.tags == 'error' %}red{% elif message.tags == 'warning' %}yellow{% else %}green{% endif %}-800">
|
|
{{ message }}
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-6">
|
|
<form method="post" enctype="multipart/form-data" id="observationForm">
|
|
{% csrf_token %}
|
|
{% if communication_request %}
|
|
<input type="hidden" name="comm_req" value="{{ communication_request.id }}">
|
|
{% endif %}
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-5 mb-5">
|
|
<div>
|
|
<label class="form-label">{% trans "Hospital" %} <span class="text-red-500">*</span></label>
|
|
{{ form.hospital }}
|
|
{% if form.hospital.errors %}
|
|
<div class="field-error">{{ form.hospital.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<label class="form-label">{% trans "Location Type" %} <span class="text-red-500">*</span></label>
|
|
{{ form.location_type }}
|
|
{% if form.location_type.errors %}
|
|
<div class="field-error">{{ form.location_type.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<label class="form-label">{% trans "Area" %}</label>
|
|
{{ form.area }}
|
|
{% if form.area.errors %}
|
|
<div class="field-error">{{ form.area.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<label class="form-label">{% trans "Department" %}</label>
|
|
{{ form.assigned_department }}
|
|
{% if form.assigned_department.errors %}
|
|
<div class="field-error">{{ form.assigned_department.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 mb-5">
|
|
<div>
|
|
<label class="form-label">{% trans "Section" %}</label>
|
|
{{ form.section }}
|
|
{% if form.section.errors %}
|
|
<div class="field-error">{{ form.section.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<label class="form-label">{% trans "Assign To" %}</label>
|
|
{{ form.assigned_to }}
|
|
{% if form.assigned_to.errors %}
|
|
<div class="field-error">{{ form.assigned_to.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<label class="form-label">{% trans "When did this occur?" %}</label>
|
|
{{ form.incident_datetime }}
|
|
{% if form.incident_datetime.errors %}
|
|
<div class="field-error">{{ form.incident_datetime.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-5">
|
|
<div>
|
|
<label class="form-label">{% trans "Patient File Number" %}</label>
|
|
{{ form.patient_file_number }}
|
|
{% if form.patient_file_number.errors %}
|
|
<div class="field-error">{{ form.patient_file_number.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-5">
|
|
<label class="form-label">{% trans "Description" %} <span class="text-red-500">*</span></label>
|
|
{{ form.description }}
|
|
{% if form.description.errors %}
|
|
<div class="field-error">{{ form.description.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="flex gap-3 justify-end pt-4 border-t border-gray-100">
|
|
<a href="{% url 'observations:observation_list' %}" class="px-5 py-2.5 border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition">
|
|
{% trans "Cancel" %}
|
|
</a>
|
|
<button type="submit" class="px-6 py-2.5 bg-blue-700 text-white rounded-lg text-sm font-medium hover:bg-blue-800 transition">
|
|
{% trans "Create Observation" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
{% trans "Select assignee (optional)" as trans_select_assignee %}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const selAssignee = '{{ trans_select_assignee|escapejs }}';
|
|
const locationTypeSelect = document.getElementById('locationTypeSelect');
|
|
const areaSelect = document.getElementById('areaSelect');
|
|
const deptSelect = document.getElementById('id_assigned_department');
|
|
const assigneeSelect = document.getElementById('id_assigned_to');
|
|
const sectionSelect = document.getElementById('sectionSelect');
|
|
|
|
function getHospitalId() {
|
|
const sel = document.getElementById('hospitalSelect');
|
|
return sel ? sel.value : null;
|
|
}
|
|
|
|
function loadAreas(locationType) {
|
|
if (!areaSelect) return;
|
|
areaSelect.innerHTML = '<option value="">{% trans "Loading..." %}</option>';
|
|
var hospitalId = getHospitalId();
|
|
if (!hospitalId) {
|
|
areaSelect.innerHTML = '<option value="">{% trans "Select Area (optional)" %}</option>';
|
|
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) {
|
|
areaSelect.innerHTML = '<option value="">{% trans "Select Area (optional)" %}</option>';
|
|
(data || []).forEach(function(a) {
|
|
var opt = document.createElement('option');
|
|
opt.value = a.id;
|
|
opt.textContent = a.name_en;
|
|
areaSelect.appendChild(opt);
|
|
});
|
|
})
|
|
.catch(function(err) {
|
|
console.error('Error loading areas:', err);
|
|
areaSelect.innerHTML = '<option value="">{% trans "Error loading areas" %}</option>';
|
|
});
|
|
}
|
|
|
|
function loadDepartments(hospitalId) {
|
|
if (!deptSelect) return;
|
|
deptSelect.innerHTML = '<option value="">{% trans "Loading..." %}</option>';
|
|
if (!hospitalId) {
|
|
deptSelect.innerHTML = '<option value="">{% trans "Select Department" %}</option>';
|
|
return;
|
|
}
|
|
fetch('/organizations/dropdowns/departments-by-category/?hospital=' + encodeURIComponent(hospitalId))
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
deptSelect.innerHTML = '<option value="">{% trans "Select Department" %}</option>';
|
|
(data || []).forEach(function(d) {
|
|
var opt = document.createElement('option');
|
|
opt.value = d.id;
|
|
opt.textContent = d.name_en || d.name;
|
|
deptSelect.appendChild(opt);
|
|
});
|
|
})
|
|
.catch(function(err) {
|
|
console.error('Error loading departments:', err);
|
|
deptSelect.innerHTML = '<option value="">{% trans "Error loading departments" %}</option>';
|
|
});
|
|
}
|
|
|
|
if (locationTypeSelect) {
|
|
locationTypeSelect.addEventListener('change', function() {
|
|
loadAreas(this.value);
|
|
});
|
|
}
|
|
|
|
if (deptSelect) {
|
|
deptSelect.addEventListener('change', function() {
|
|
var deptId = this.value;
|
|
if (!deptId) {
|
|
if (assigneeSelect) assigneeSelect.innerHTML = '<option value="">' + selAssignee + '</option>';
|
|
if (sectionSelect) sectionSelect.innerHTML = '<option value="">{% trans "Select Section" %}</option>';
|
|
return;
|
|
}
|
|
|
|
if (assigneeSelect) {
|
|
fetch("{% url 'observations:get_users_by_department' %}?department_id=" + deptId)
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
var html = '<option value="">' + selAssignee + '</option>';
|
|
data.users.forEach(function(user) {
|
|
html += '<option value="' + user.id + '">' + user.name + '</option>';
|
|
});
|
|
assigneeSelect.innerHTML = html;
|
|
})
|
|
.catch(function() { console.error('Failed to load users'); });
|
|
}
|
|
|
|
if (sectionSelect) {
|
|
sectionSelect.innerHTML = '<option value="">{% trans "Loading..." %}</option>';
|
|
fetch('/organizations/dropdowns/sections/' + deptId + '/')
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
sectionSelect.innerHTML = '<option value="">{% trans "Select Section" %}</option>';
|
|
(data || []).forEach(function(s) {
|
|
var opt = document.createElement('option');
|
|
opt.value = s.id;
|
|
opt.textContent = s.name_en || s.name;
|
|
sectionSelect.appendChild(opt);
|
|
});
|
|
})
|
|
.catch(function(err) { console.error(err); });
|
|
}
|
|
});
|
|
}
|
|
|
|
var initHospitalId = getHospitalId();
|
|
if (initHospitalId) {
|
|
loadDepartments(initHospitalId);
|
|
loadAreas(null);
|
|
}
|
|
|
|
var hospitalSelect = document.getElementById('hospitalSelect');
|
|
if (hospitalSelect) {
|
|
hospitalSelect.addEventListener('change', function() {
|
|
loadDepartments(this.value);
|
|
loadAreas(null);
|
|
if (sectionSelect) sectionSelect.innerHTML = '<option value="">{% trans "Select Section" %}</option>';
|
|
if (assigneeSelect) assigneeSelect.innerHTML = '<option value="">' + selAssignee + '</option>';
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|