devsofttech_hms_ci4/app/Views/immunization/index.php
DevSoftTech Server 6e78e6010e feat: add Day Care & Emergency admission module with IPD conversion and clean patient names
- Add Day Care (<24h) and Emergency (Casualty) admission pathways with prefixes DC- and ER-
- Add dedicated physical wards, ESI 5-level triage, MLC tracking, and PADSS day care discharge
- Implement audited 'Convert to IPD' workflow with SBAR handover and automated bed transfer
- Support unified IPD billing with dedicated group headers for Day Care and Emergency charges
- Redesign Hospital Admission Desk with executive card layout and modern triage controls
- Add comprehensive user documentation and SVG flowchart in public/help.html
- Completely remove legacy patient_master.p_lname column from database and application code
2026-09-21 23:41:24 +05:30

465 lines
20 KiB
PHP

<?php
$csrfName = csrf_token();
$csrfHash = csrf_hash();
$initialPatientId = (int) ($patient_id ?? 0);
?>
<div class="container-fluid py-3 immunization-ui">
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h5 class="mb-0">Immunization Record</h5>
<div class="small text-muted">Patient immunization schedule and vaccination completion</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnLoadSchedule">UIP Schedule Master</button>
</div>
</div>
<div class="card-body">
<input type="hidden" id="csrfName" value="<?= esc($csrfName) ?>">
<input type="hidden" id="csrfHash" value="<?= esc($csrfHash) ?>">
<div class="row g-2 align-items-end">
<div class="col-md-2">
<label class="form-label mb-1">UHID / Patient ID</label>
<input type="text" class="form-control" id="patientId" value="<?= $initialPatientId > 0 ? esc((string) $initialPatientId) : '' ?>" placeholder="e.g. P26071000012">
</div>
<div class="col-md-3">
<button type="button" class="btn btn-primary w-100" id="btnLoadPatient">Load Patient</button>
</div>
<div class="col-md-7">
<div class="small text-muted pb-2">Enter UHID like P26071000012 or internal patient id.</div>
</div>
</div>
<div class="mt-3" id="statusBox"></div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-3">
<div class="card shadow-sm h-100">
<div class="card-header py-2 fw-semibold">Patient</div>
<div class="card-body">
<div class="small text-muted mb-1">Selected patient</div>
<div class="fs-6 fw-semibold" id="patientName">-</div>
<div class="small mt-2" id="patientMeta">Enter a patient ID and load.</div>
<hr>
<div class="mb-2">
<label class="form-label mb-1">Generate Basis</label>
<select class="form-select form-select-sm" id="generationMode" disabled>
<option value="eligible" selected>Age/Gender eligible doses only</option>
<option value="full">Full UIP schedule</option>
</select>
<div class="small text-muted mt-1" id="generationHint">Load patient first.</div>
</div>
<button type="button" class="btn btn-sm btn-outline-success w-100 mb-3" id="btnGenerate" disabled>Generate UIP</button>
<div class="row g-2 text-center" id="summaryGrid">
<div class="col-6"><div class="summary-tile"><span id="sumTotal">0</span><small>Total</small></div></div>
<div class="col-6"><div class="summary-tile"><span id="sumDue">0</span><small>Due</small></div></div>
<div class="col-6"><div class="summary-tile"><span id="sumOverdue">0</span><small>Overdue</small></div></div>
<div class="col-6"><div class="summary-tile"><span id="sumCompleted">0</span><small>Completed</small></div></div>
</div>
</div>
</div>
</div>
<div class="col-lg-9">
<div class="card shadow-sm">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<div class="fw-semibold">Vaccination Timeline</div>
<div class="small text-muted" id="recordCount">No records loaded</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0" id="recordTable">
<thead class="table-light">
<tr>
<th>Age</th>
<th>Vaccine</th>
<th>Dose</th>
<th>Due</th>
<th>Given</th>
<th>Status</th>
<th>Lot</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
<tr><td colspan="8" class="text-center text-muted py-4">No patient loaded.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm mt-3 d-none" id="scheduleCard">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<div class="fw-semibold">UIP Master Schedule</div>
<div class="small text-muted" id="scheduleCount"></div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm mb-0" id="scheduleTable">
<thead class="table-light">
<tr>
<th>Age</th>
<th>Vaccine</th>
<th>Dose</th>
<th>Route</th>
<th>Target Disease</th>
<th>Notes</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="completeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header py-2">
<h5 class="modal-title">Complete Vaccination</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="completeRecordId">
<div class="alert alert-light border mb-3" id="completeTitle"></div>
<div class="row g-2">
<div class="col-md-4">
<label class="form-label mb-1">Given On</label>
<input type="datetime-local" class="form-control" id="givenDate">
</div>
<div class="col-md-4">
<label class="form-label mb-1">Lot Number</label>
<input type="text" class="form-control" id="lotNumber">
</div>
<div class="col-md-4">
<label class="form-label mb-1">Expiry On</label>
<input type="date" class="form-control" id="expiryDate">
</div>
<div class="col-md-4">
<label class="form-label mb-1">Manufacturer</label>
<input type="text" class="form-control" id="manufacturer">
</div>
<div class="col-md-4">
<label class="form-label mb-1">Location</label>
<input type="text" class="form-control" id="locationName">
</div>
<div class="col-md-4">
<label class="form-label mb-1">Performer ID</label>
<input type="number" min="1" step="1" class="form-control" id="performerId">
</div>
<div class="col-md-6">
<label class="form-label mb-1">Reaction Notes</label>
<textarea class="form-control" rows="2" id="reactionNotes"></textarea>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Notes</label>
<textarea class="form-control" rows="2" id="notes"></textarea>
</div>
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btnSaveComplete">Save Completed</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="jsonModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header py-2">
<h5 class="modal-title" id="jsonModalTitle">FHIR Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre class="json-preview mb-0" id="jsonPreview">{}</pre>
</div>
</div>
</div>
</div>
</div>
<style>
.immunization-ui .summary-tile {
border: 1px solid #e3e7ee;
border-radius: 6px;
padding: 10px 8px;
background: #f8fafc;
}
.immunization-ui .summary-tile span {
display: block;
font-weight: 700;
font-size: 1.15rem;
line-height: 1.1;
}
.immunization-ui .summary-tile small {
color: #6c757d;
}
.immunization-ui .json-preview {
min-height: 360px;
max-height: 70vh;
white-space: pre-wrap;
word-break: break-word;
background: #0f172a;
color: #dbeafe;
border-radius: 6px;
padding: 14px;
font-size: 12px;
}
</style>
<script>
(function () {
var currentPatientId = 0;
var records = [];
function esc(value) {
return $('<div>').text(value == null ? '' : String(value)).html();
}
function csrfPayload() {
var name = $('#csrfName').val();
var hash = $('#csrfHash').val();
var payload = {};
payload[name] = hash;
return payload;
}
function updateCsrf(response) {
if (response && response.csrfName && response.csrfHash) {
$('#csrfName').val(response.csrfName);
$('#csrfHash').val(response.csrfHash);
}
}
function ajaxError(xhr) {
var json = xhr.responseJSON || {};
return json.error_text || json.message || json.error || xhr.statusText || 'Request failed';
}
function showStatus(type, message) {
var cls = type === 'success' ? 'alert-success' : (type === 'warning' ? 'alert-warning' : 'alert-danger');
$('#statusBox').html('<div class="alert ' + cls + ' py-2 mb-0">' + esc(message) + '</div>');
}
function selectedPatientKey() {
return $.trim($('#patientId').val() || '');
}
function resolvePatientKey(callback) {
var key = selectedPatientKey();
if (!key) {
showStatus('warning', 'Enter UHID / Patient ID.');
return;
}
$.ajax({
url: '<?= site_url('Document_Patient/open_by_key') ?>',
method: 'GET',
dataType: 'json',
data: {patient_key: key},
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).done(function (data) {
var resolvedId = parseInt(data.patient_id || 0, 10);
if (parseInt(data.status || 0, 10) === 1 && resolvedId > 0) {
currentPatientId = resolvedId;
callback(resolvedId);
return;
}
showStatus('warning', data.message || 'Patient not found');
}).fail(function (xhr) {
showStatus('warning', ajaxError(xhr));
});
}
function badge(status, dueDate) {
var raw = (status || 'due').toLowerCase();
var label = raw;
var cls = 'bg-secondary';
var today = new window.Date().toISOString().slice(0, 10);
if (raw === 'completed') {
cls = 'bg-success';
} else if (raw === 'due' && dueDate && dueDate < today) {
cls = 'bg-danger';
label = 'overdue';
} else if (raw === 'due') {
cls = 'bg-warning text-dark';
}
return '<span class="badge ' + cls + '">' + esc(label) + '</span>';
}
function renderPatient(data) {
var patient = data.patient || {};
var name = $.trim(patient.p_fname || '') || ('Patient #' + currentPatientId);
$('#patientName').text(name);
$('#patientMeta').html([
patient.p_code ? 'UHID: ' + esc(patient.p_code) : '',
patient.gender ? 'Gender: ' + esc(patient.gender) : '',
patient.age ? 'Age: ' + esc(patient.age) : '',
patient.age_in_month ? 'Age in months: ' + esc(patient.age_in_month) : '',
patient.dob ? 'DOB: ' + esc(patient.dob) : '',
patient.mphone1 ? 'Mobile: ' + esc(patient.mphone1) : ''
].filter(Boolean).join('<br>') || 'Patient loaded.');
var summary = data.summary || {};
$('#sumTotal').text(summary.total || 0);
$('#sumDue').text(summary.due || 0);
$('#sumOverdue').text(summary.overdue || 0);
$('#sumCompleted').text(summary.completed || 0);
records = data.items || [];
$('#recordCount').text(records.length + ' record(s)');
$('#btnGenerate, #generationMode').prop('disabled', false);
$('#generationHint').text('Default creates only doses due for the patient age. Current child UIP master has no gender-specific dose rule.');
var rows = [];
records.forEach(function (record) {
var canComplete = (record.status || '').toLowerCase() !== 'completed';
rows.push('<tr>' +
'<td>' + esc(record.age_label || '-') + '</td>' +
'<td><div class="fw-semibold">' + esc(record.vaccine_name || '-') + '</div><div class="small text-muted">' + esc(record.target_disease_name || '') + '</div></td>' +
'<td>' + esc(record.dose_number || '-') + '</td>' +
'<td>' + esc(record.due_date || '-') + '</td>' +
'<td>' + esc(record.given_date || '-') + '</td>' +
'<td>' + badge(record.status, record.due_date) + '</td>' +
'<td>' + esc(record.lot_number || '-') + '</td>' +
'<td class="text-end"><button type="button" class="btn btn-sm btn-outline-primary btnComplete" data-id="' + esc(record.id) + '" ' + (canComplete ? '' : 'disabled') + '>Complete</button></td>' +
'</tr>');
});
$('#recordTable tbody').html(rows.length ? rows.join('') : '<tr><td colspan="8" class="text-center text-muted py-4">No immunization records yet. Click Generate UIP.</td></tr>');
}
function loadPatient() {
resolvePatientKey(function (patientId) {
currentPatientId = patientId;
$.ajax({
url: '<?= site_url('Immunization/patient') ?>/' + patientId,
method: 'GET',
dataType: 'json',
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).done(function (data) {
if (parseInt(data.ok || 0, 10) !== 1) {
showStatus('warning', data.error_text || 'Patient load failed');
return;
}
renderPatient(data);
showStatus('success', 'Patient immunization timeline loaded.');
}).fail(function (xhr) {
showStatus('warning', ajaxError(xhr));
});
});
}
function generateSchedule() {
resolvePatientKey(function (patientId) {
currentPatientId = patientId;
var payload = csrfPayload();
payload.generation_mode = $('#generationMode').val() || 'eligible';
$.ajax({
url: '<?= site_url('Immunization/generate_patient_schedule') ?>/' + patientId,
method: 'POST',
dataType: 'json',
data: payload,
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).done(function (data) {
updateCsrf(data);
if (parseInt(data.ok || 0, 10) !== 1) {
showStatus('warning', data.error || data.error_text || 'Schedule generation failed');
return;
}
var futureSkipped = parseInt(data.future_skipped || 0, 10);
var detail = 'UIP generated: ' + (data.created || 0) + ' created, ' + (data.skipped || 0) + ' already existed';
if (futureSkipped > 0) {
detail += ', ' + futureSkipped + ' future dose(s) skipped by age';
}
showStatus('success', detail + '.');
loadPatient();
}).fail(function (xhr) {
updateCsrf(xhr.responseJSON || {});
showStatus('warning', ajaxError(xhr));
});
});
}
function loadSchedule() {
load_form('<?= site_url('Immunization/schedule_master') ?>', 'UIP Schedule Master');
}
function openComplete(recordId) {
var record = records.find(function (item) { return parseInt(item.id, 10) === parseInt(recordId, 10); });
if (!record) {
return;
}
$('#completeRecordId').val(record.id);
$('#completeTitle').text((record.vaccine_name || 'Vaccine') + ' | Due: ' + (record.due_date || '-') + ' | Dose: ' + (record.dose_number || '-'));
$('#givenDate').val(new window.Date().toISOString().slice(0, 16));
$('#lotNumber').val(record.lot_number || '');
$('#expiryDate').val(record.expiry_date || '');
$('#manufacturer').val(record.manufacturer || '');
$('#locationName').val(record.location_name || '');
$('#performerId').val(record.performer_id || '');
$('#reactionNotes').val(record.reaction_notes || '');
$('#notes').val(record.notes || '');
bootstrap.Modal.getOrCreateInstance(document.getElementById('completeModal')).show();
}
function saveComplete() {
var recordId = parseInt($('#completeRecordId').val(), 10) || 0;
if (recordId <= 0) {
return;
}
var payload = csrfPayload();
payload.given_date = $('#givenDate').val();
payload.lot_number = $('#lotNumber').val();
payload.expiry_date = $('#expiryDate').val();
payload.manufacturer = $('#manufacturer').val();
payload.location_name = $('#locationName').val();
payload.performer_id = $('#performerId').val();
payload.reaction_notes = $('#reactionNotes').val();
payload.notes = $('#notes').val();
$.ajax({
url: '<?= site_url('Immunization/complete') ?>/' + recordId,
method: 'POST',
dataType: 'json',
data: payload,
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).done(function (data) {
updateCsrf(data);
if (parseInt(data.ok || 0, 10) !== 1) {
showStatus('warning', data.error_text || 'Save failed');
return;
}
bootstrap.Modal.getOrCreateInstance(document.getElementById('completeModal')).hide();
showStatus('success', 'Vaccination marked completed.');
loadPatient();
}).fail(function (xhr) {
updateCsrf(xhr.responseJSON || {});
showStatus('warning', ajaxError(xhr));
});
}
$('#btnLoadPatient').on('click', loadPatient);
$('#btnGenerate').on('click', generateSchedule);
$('#btnLoadSchedule').on('click', loadSchedule);
$('#btnSaveComplete').on('click', saveComplete);
$('#recordTable').on('click', '.btnComplete', function () {
openComplete($(this).data('id'));
});
$('#patientId').on('keydown', function (event) {
if (event.key === 'Enter') {
loadPatient();
}
});
if (selectedPatientKey() !== '') {
loadPatient();
}
})();
</script>