devsofttech_hms_ci4/app/Controllers/Immunization.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

360 lines
14 KiB
PHP

<?php
namespace App\Controllers;
use App\Libraries\ImmunizationMasterSyncService;
use App\Libraries\ImmunizationScheduleService;
class Immunization extends BaseController
{
public function index()
{
return view('immunization/index', [
'page_title' => 'Immunization Record',
'patient_id' => (int) ($this->request->getGet('patient_id') ?? 0),
]);
}
public function schedule()
{
if (! $this->db->tableExists('immunization_schedule_master') || ! $this->db->tableExists('immunization_vaccine_master')) {
return $this->response->setStatusCode(500)->setJSON([
'ok' => 0,
'error_text' => 'Immunization tables not found. Run migrations first.',
]);
}
$rows = $this->db->table('immunization_schedule_master s')
->select('s.*, v.vaccine_name, v.vaccine_code, v.vaccine_code_system, v.target_disease_name, v.route_name, v.site_name')
->join('immunization_vaccine_master v', 'v.id = s.vaccine_master_id', 'left')
->where('s.is_active', 1)
->orderBy('s.sort_order', 'ASC')
->get()
->getResultArray();
return $this->response->setJSON([
'ok' => 1,
'count' => count($rows),
'items' => $rows,
]);
}
public function scheduleMaster()
{
$syncService = new ImmunizationMasterSyncService($this->db);
return view('immunization/schedule_master', [
'page_title' => 'UIP Schedule Master',
'items' => $this->loadScheduleRows(true),
'sync_meta' => $syncService->latestSyncMeta(),
]);
}
public function syncUipMaster()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$force = (int) ($this->request->getPost('force') ?? 0) === 1;
$result = (new ImmunizationMasterSyncService($this->db))->syncUipMaster($force);
$httpCode = (int) ($result['ok'] ?? 0) === 1 ? 200 : 400;
return $this->response->setStatusCode($httpCode)->setJSON($result + [
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function updateSchedule(int $scheduleId = 0)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
if ($scheduleId <= 0 || ! $this->db->tableExists('immunization_schedule_master') || ! $this->db->tableExists('immunization_vaccine_master')) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid schedule record']);
}
$schedule = $this->db->table('immunization_schedule_master')->where('id', $scheduleId)->get(1)->getRowArray();
if (empty($schedule)) {
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Schedule not found']);
}
$now = date('Y-m-d H:i:s');
$ageLabel = $this->nullablePost('age_label') ?? (string) ($schedule['age_label'] ?? '');
$doseNumber = $this->nullablePost('dose_number') ?? (string) ($schedule['dose_number'] ?? '');
$notes = $this->nullablePost('notes');
$scheduleUpdate = [
'age_label' => $ageLabel,
'age_value' => max(0, (int) ($this->request->getPost('age_value') ?? ($schedule['age_value'] ?? 0))),
'age_unit' => $this->nullablePost('age_unit') ?? (string) ($schedule['age_unit'] ?? 'days'),
'age_offset_days' => max(0, (int) ($this->request->getPost('age_offset_days') ?? ($schedule['age_offset_days'] ?? 0))),
'dose_number' => $doseNumber,
'series_doses' => $this->nullablePost('series_doses'),
'notes' => $notes,
'sort_order' => max(0, (int) ($this->request->getPost('sort_order') ?? ($schedule['sort_order'] ?? 0))),
'is_active' => (int) ($this->request->getPost('is_active') ?? 0) === 1 ? 1 : 0,
'updated_at' => $now,
];
$vaccineUpdate = [
'vaccine_name' => $this->nullablePost('vaccine_name') ?? '',
'target_disease_name' => $this->nullablePost('target_disease_name'),
'route_name' => $this->nullablePost('route_name'),
'site_name' => $this->nullablePost('site_name'),
'updated_at' => $now,
];
if ($vaccineUpdate['vaccine_name'] === '') {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Vaccine name is required']);
}
$this->db->table('immunization_schedule_master')->where('id', $scheduleId)->update($scheduleUpdate);
$vaccineId = (int) ($schedule['vaccine_master_id'] ?? 0);
if ($vaccineId > 0) {
$this->db->table('immunization_vaccine_master')->where('id', $vaccineId)->update($vaccineUpdate);
}
if ($this->db->tableExists('immunization_records')) {
$this->db->table('immunization_records')
->where('schedule_id', $scheduleId)
->where('status !=', 'completed')
->update([
'vaccine_name' => $vaccineUpdate['vaccine_name'],
'dose_number' => $doseNumber,
'route_name' => $vaccineUpdate['route_name'],
'site_name' => $vaccineUpdate['site_name'],
'notes' => $notes,
'updated_at' => $now,
]);
}
return $this->response->setJSON([
'ok' => 1,
'item' => $this->loadScheduleRow($scheduleId),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function patient(int $patientId = 0)
{
if ($patientId <= 0) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid patient_id']);
}
foreach (['patient_master', 'immunization_records'] as $table) {
if (! $this->db->tableExists($table)) {
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $table . ' table not found']);
}
}
$patient = $this->loadPatientHeader($patientId);
if (empty($patient)) {
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Patient not found']);
}
$rows = $this->db->table('immunization_records r')
->select('r.*, s.age_label, s.age_offset_days, s.series_name, s.series_doses, v.target_disease_name')
->join('immunization_schedule_master s', 's.id = r.schedule_id', 'left')
->join('immunization_vaccine_master v', 'v.id = r.vaccine_master_id', 'left')
->where('r.patient_id', $patientId)
->orderBy('COALESCE(r.due_date, r.given_date)', 'ASC', false)
->orderBy('r.id', 'ASC')
->get()
->getResultArray();
return $this->response->setJSON([
'ok' => 1,
'patient' => $patient,
'summary' => $this->buildSummary($rows),
'count' => count($rows),
'items' => $rows,
]);
}
public function generatePatientSchedule(int $patientId = 0)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$result = (new ImmunizationScheduleService($this->db))->generateUipDueRecordsForPatient($patientId, [
'generation_mode' => (string) ($this->request->getPost('generation_mode') ?? 'full'),
]);
$httpCode = (int) ($result['ok'] ?? 0) === 1 ? 200 : 400;
return $this->response->setStatusCode($httpCode)->setJSON($result + [
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function complete(int $recordId = 0)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
if ($recordId <= 0 || ! $this->db->tableExists('immunization_records')) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid immunization record']);
}
$record = $this->db->table('immunization_records')->where('id', $recordId)->get(1)->getRowArray();
if (empty($record)) {
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Immunization record not found']);
}
$givenDate = $this->normalizeDateTime((string) ($this->request->getPost('given_date') ?? ''));
if ($givenDate === null) {
$givenDate = date('Y-m-d H:i:s');
}
$expiryDate = $this->normalizeDate((string) ($this->request->getPost('expiry_date') ?? ''));
$user = function_exists('auth') ? auth()->user() : null;
$userId = (int) ($user->id ?? 0);
$update = [
'given_date' => $givenDate,
'status' => 'completed',
'lot_number' => $this->nullablePost('lot_number'),
'expiry_date' => $expiryDate,
'manufacturer' => $this->nullablePost('manufacturer'),
'performer_id' => (int) ($this->request->getPost('performer_id') ?? 0) ?: null,
'location_name' => $this->nullablePost('location_name'),
'route_code' => $this->nullablePost('route_code') ?? (string) ($record['route_code'] ?? ''),
'route_name' => $this->nullablePost('route_name') ?? (string) ($record['route_name'] ?? ''),
'site_code' => $this->nullablePost('site_code') ?? (string) ($record['site_code'] ?? ''),
'site_name' => $this->nullablePost('site_name') ?? (string) ($record['site_name'] ?? ''),
'reaction_notes' => $this->nullablePost('reaction_notes'),
'notes' => $this->nullablePost('notes') ?? (string) ($record['notes'] ?? ''),
'updated_by' => $userId > 0 ? $userId : null,
'updated_at' => date('Y-m-d H:i:s'),
];
$this->db->table('immunization_records')->where('id', $recordId)->update($update);
return $this->response->setJSON([
'ok' => 1,
'record_id' => $recordId,
'status' => 'completed',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
/**
* @return array<string,mixed>
*/
private function loadPatientHeader(int $patientId): array
{
$fields = $this->db->getFieldNames('patient_master') ?? [];
$select = ['id'];
foreach (['p_code', 'p_fname', 'gender', 'dob', 'age', 'age_in_month', 'mphone1', 'abha_id', 'abha_no', 'abha_address', 'abha'] as $field) {
if (in_array($field, $fields, true)) {
$select[] = $field;
}
}
return $this->db->table('patient_master')
->select(implode(',', array_unique($select)))
->where('id', $patientId)
->get(1)
->getRowArray() ?? [];
}
/**
* @return array<int,array<string,mixed>>
*/
private function loadScheduleRows(bool $includeInactive = false): array
{
if (! $this->db->tableExists('immunization_schedule_master') || ! $this->db->tableExists('immunization_vaccine_master')) {
return [];
}
$builder = $this->db->table('immunization_schedule_master s')
->select('s.*, v.vaccine_name, v.vaccine_code, v.vaccine_code_system, v.target_disease_name, v.route_name, v.site_name')
->join('immunization_vaccine_master v', 'v.id = s.vaccine_master_id', 'left')
->orderBy('s.sort_order', 'ASC')
->orderBy('s.id', 'ASC');
if (! $includeInactive) {
$builder->where('s.is_active', 1);
}
return $builder->get()->getResultArray();
}
/**
* @return array<string,mixed>
*/
private function loadScheduleRow(int $scheduleId): array
{
if ($scheduleId <= 0) {
return [];
}
return $this->db->table('immunization_schedule_master s')
->select('s.*, v.vaccine_name, v.vaccine_code, v.vaccine_code_system, v.target_disease_name, v.route_name, v.site_name')
->join('immunization_vaccine_master v', 'v.id = s.vaccine_master_id', 'left')
->where('s.id', $scheduleId)
->get(1)
->getRowArray() ?? [];
}
/**
* @param array<int,array<string,mixed>> $rows
* @return array<string,int>
*/
private function buildSummary(array $rows): array
{
$summary = [
'total' => count($rows),
'due' => 0,
'overdue' => 0,
'completed' => 0,
'missed' => 0,
'postponed' => 0,
];
$today = date('Y-m-d');
foreach ($rows as $row) {
$status = strtolower(trim((string) ($row['status'] ?? 'due')));
if (isset($summary[$status])) {
$summary[$status]++;
}
$dueDate = trim((string) ($row['due_date'] ?? ''));
if ($status === 'due' && $dueDate !== '' && $dueDate < $today) {
$summary['overdue']++;
}
}
return $summary;
}
private function nullablePost(string $key): ?string
{
$value = trim((string) ($this->request->getPost($key) ?? ''));
return $value !== '' ? $value : null;
}
private function normalizeDateTime(string $value): ?string
{
$value = trim($value);
if ($value === '') {
return null;
}
$timestamp = strtotime($value);
return $timestamp === false ? null : date('Y-m-d H:i:s', $timestamp);
}
private function normalizeDate(string $value): ?string
{
$value = trim($value);
if ($value === '') {
return null;
}
$timestamp = strtotime($value);
return $timestamp === false ? null : date('Y-m-d', $timestamp);
}
}