Add patient immunization module
This commit is contained in:
parent
629590d9ab
commit
82fd31d105
|
|
@ -683,6 +683,15 @@ $routes->post('AbdmGateway/share_diagnosis_report_bundle', 'AbdmGateway::shareDi
|
|||
$routes->post('AbdmGateway/shareDiagnosisReportBundle', 'AbdmGateway::shareDiagnosisReportBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/diagnosis_report_fhir_preview', 'AbdmGateway::diagnosisReportFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/ipd_discharge_fhir_preview', 'AbdmGateway::ipdDischargeFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/immunization_fhir_preview', 'AbdmGateway::immunizationFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_immunization_bundle', 'AbdmGateway::shareImmunizationBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('Immunization', 'Immunization::index', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->get('Immunization/schedule_master', 'Immunization::scheduleMaster', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('Immunization/update_schedule/(:num)', 'Immunization::updateSchedule/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->get('Immunization/schedule', 'Immunization::schedule', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->get('Immunization/patient/(:num)', 'Immunization::patient/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('Immunization/generate_patient_schedule/(:num)', 'Immunization::generatePatientSchedule/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('Immunization/complete/(:num)', 'Immunization::complete/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('AbdmGateway/push_health_record', 'AbdmGateway::pushHealthRecord', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/health_records_list', 'AbdmGateway::healthRecordsList', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/bridge_record_status/(:num)', 'AbdmGateway::bridgeRecordStatus/$1', ['filter' => $abdmPermFilter]);
|
||||
|
|
@ -767,6 +776,7 @@ $routes->get('Opd_prescription/section_template_list', 'Opd_prescription::sectio
|
|||
$routes->get('Opd_prescription/section_past_data', 'Opd_prescription::section_past_data');
|
||||
$routes->get('Opd_prescription/template_workspace', 'Opd_prescription::template_workspace', ['filter' => 'permission:doctor_work.template_workspace.access,doctor_work.access']);
|
||||
$routes->get('doctor_work/document_workspace', 'DoctorDocument::workspace', ['filter' => 'permission:doctor_work.template_workspace.access,doctor_work.access']);
|
||||
$routes->get('doctor_work/patient_search', 'DoctorDocument::patient_search', ['filter' => 'permission:doctor_work.template_workspace.access,doctor_work.access']);
|
||||
|
||||
// Legacy-compatible doctor document templates and issue flow.
|
||||
$routes->get('Doc_Admin/doc_list', 'DoctorDocument::doc_list', ['filter' => 'permission:doctor_work.template_workspace.access,doctor_work.access']);
|
||||
|
|
|
|||
|
|
@ -2020,6 +2020,174 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
public function immunizationFhirPreview()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['status' => 'error', 'message' => 'AJAX only']);
|
||||
}
|
||||
|
||||
$patientId = (int) ($this->request->getGet('patient_id') ?? $this->request->getPost('patient_id') ?? 0);
|
||||
$recordId = (int) ($this->request->getGet('record_id') ?? $this->request->getPost('record_id') ?? 0);
|
||||
$abhaId = trim((string) ($this->request->getGet('abha_id') ?? $this->request->getPost('abha_id') ?? ''));
|
||||
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'patient_id is required.',
|
||||
]);
|
||||
}
|
||||
|
||||
$payload = $this->buildImmunizationGatewayPayload($patientId, $recordId, $abhaId);
|
||||
if ($payload === null) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'Unable to prepare ImmunizationRecord FHIR payload.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'ok',
|
||||
'patient_id' => $patientId,
|
||||
'record_id' => $recordId > 0 ? $recordId : null,
|
||||
'abha_id' => $abhaId,
|
||||
'care_context_reference' => (string) ($payload['care_context_reference'] ?? ''),
|
||||
'bundle' => (array) ($payload['bundle'] ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
public function shareImmunizationBundle()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$recordId = (int) $this->request->getPost('record_id');
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
$abhaAddressPost = trim((string) $this->request->getPost('abha_address'));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
$pushToGateway = (int) $this->request->getPost('push_to_gateway') === 1;
|
||||
|
||||
if ($abhaId === '' && $abhaAddressPost !== '') {
|
||||
$abhaId = $abhaAddressPost;
|
||||
}
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
|
||||
}
|
||||
|
||||
$payload = $this->buildImmunizationGatewayPayload($patientId, $recordId, $abhaId);
|
||||
if ($payload === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Unable to prepare ImmunizationRecord FHIR payload']);
|
||||
}
|
||||
|
||||
$bundle = (array) ($payload['bundle'] ?? []);
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$ccRef = (string) ($payload['care_context_reference'] ?? ('IMM-' . $patientId . '-' . date('Y-m-d')));
|
||||
$visitDate = (string) ($payload['visit_date'] ?? date('Y-m-d'));
|
||||
$patientName = (string) ($payload['patient_name'] ?? ('PATIENT-' . $patientId));
|
||||
|
||||
$consent = $abhaId !== '' ? $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle) : null;
|
||||
$effectiveConsent = (string) ($consent['consent_handle'] ?? $consentHandle);
|
||||
|
||||
$healthRecordId = $this->storeHealthRecord([
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'hi_type' => 'ImmunizationRecord',
|
||||
'entity_type' => 'immunization',
|
||||
'entity_id' => $recordId > 0 ? (string) $recordId : (string) $patientId,
|
||||
'fhir_bundle' => $bundleJson,
|
||||
'care_context_reference' => $ccRef,
|
||||
'consent_handle' => $effectiveConsent,
|
||||
]);
|
||||
|
||||
if ($abhaId === '') {
|
||||
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
|
||||
$this->db->table('health_records')->where('id', $healthRecordId)->update([
|
||||
'push_status' => 'local_only',
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'local_stored',
|
||||
'health_record_id' => $healthRecordId,
|
||||
'care_context_reference' => $ccRef,
|
||||
'message' => 'ABHA not available. Immunization record stored locally for later discovery.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $pushToGateway) {
|
||||
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
|
||||
$this->db->table('health_records')->where('id', $healthRecordId)->update([
|
||||
'push_status' => 'local_discovery_ready',
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'local_discovery_ready',
|
||||
'health_record_id' => $healthRecordId,
|
||||
'care_context_reference' => $ccRef,
|
||||
'consent_handle' => $effectiveConsent,
|
||||
'mode' => 'm2_hms_source',
|
||||
'message' => 'Immunization record registered in HMS for ABDM discovery/fetch callbacks.',
|
||||
]);
|
||||
}
|
||||
|
||||
$queueId = null;
|
||||
$bridgeRecordId = 0;
|
||||
$connectorError = null;
|
||||
try {
|
||||
$result = $this->connector->pushRecord([
|
||||
'patient_id' => (string) $patientId,
|
||||
'patient_name' => $patientName,
|
||||
'abha_id' => preg_match('/^\d{14}$/', $abhaId) === 1 ? $abhaId : '',
|
||||
'abha_address' => str_contains($abhaId, '@') ? $abhaId : '',
|
||||
'hi_type' => 'ImmunizationRecord',
|
||||
'record_type' => 'ImmunizationRecord',
|
||||
'visit_date' => $visitDate,
|
||||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => 'Immunization Record - ' . $visitDate,
|
||||
'notes' => 'Immunization Record - ' . $visitDate,
|
||||
'queue_id' => $ccRef,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
$queueId = (string) ($result['queue_id'] ?? '');
|
||||
$bridgeRecordId = (int) ($result['id'] ?? 0);
|
||||
} catch (\Throwable $e) {
|
||||
$connectorError = $e->getMessage();
|
||||
}
|
||||
|
||||
if ($healthRecordId > 0) {
|
||||
$this->updateHealthRecordTxn($healthRecordId, (string) ($queueId ?? ''), $connectorError, $bridgeRecordId);
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
'action' => 'push_record',
|
||||
'entity_type' => 'immunization',
|
||||
'entity_id' => $recordId > 0 ? (string) $recordId : (string) $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'patient_id' => $patientId,
|
||||
'request' => ['patient_id' => $patientId, 'record_id' => $recordId, 'hi_type' => 'ImmunizationRecord'],
|
||||
'response' => ['queue_id' => $queueId],
|
||||
'outcome' => $connectorError === null ? 'success' : 'failure',
|
||||
'error_message' => (string) ($connectorError ?? ''),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => $connectorError === null ? 1 : 0,
|
||||
'queue_id' => $queueId,
|
||||
'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null,
|
||||
'health_record_id' => $healthRecordId,
|
||||
'consent_handle' => $effectiveConsent,
|
||||
'care_context_reference' => $ccRef,
|
||||
'status' => $connectorError === null ? 'queued' : 'failed',
|
||||
'error' => $connectorError,
|
||||
]);
|
||||
}
|
||||
|
||||
public function ipdDischargeFhirPreview()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
|
|
@ -2372,6 +2540,120 @@ class AbdmGateway extends BaseController
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function buildImmunizationGatewayPayload(int $patientId, int $recordId = 0, string $preferredAbhaId = ''): ?array
|
||||
{
|
||||
if ($patientId <= 0 || ! $this->db->tableExists('patient_master') || ! $this->db->tableExists('immunization_records')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
||||
if (empty($patientRow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$recordsBuilder = $this->db->table('immunization_records r')
|
||||
->select('r.*, s.series_name, s.series_doses, s.age_label, v.target_disease_code, 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.given_date, r.due_date)', 'ASC', false)
|
||||
->orderBy('r.id', 'ASC');
|
||||
|
||||
if ($recordId > 0) {
|
||||
$recordsBuilder->where('r.id', $recordId);
|
||||
}
|
||||
|
||||
$records = $recordsBuilder->get()->getResultArray();
|
||||
if (empty($records)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$patientName = trim(trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . trim((string) ($patientRow['p_lname'] ?? '')));
|
||||
$rawAbha = trim($preferredAbhaId);
|
||||
if ($rawAbha === '') {
|
||||
foreach (['abha_address', 'abha_id', 'abha_no', 'abha'] as $field) {
|
||||
$candidate = trim((string) ($patientRow[$field] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$rawAbha = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hospital = $this->getHospitalProfileForFhir();
|
||||
$practitioner = null;
|
||||
$performerId = 0;
|
||||
foreach ($records as $record) {
|
||||
$performerId = (int) ($record['performer_id'] ?? 0);
|
||||
if ($performerId > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($performerId > 0 && $this->db->tableExists('doctor_master')) {
|
||||
$doctorRow = $this->db->table('doctor_master')->where('id', $performerId)->get(1)->getRowArray() ?? [];
|
||||
if (! empty($doctorRow)) {
|
||||
$doctorName = trim(trim((string) ($doctorRow['p_fname'] ?? '')) . ' ' . trim((string) ($doctorRow['p_lname'] ?? '')));
|
||||
$doctorRegNo = '';
|
||||
foreach (['doctor_reg_no', 'registration_no', 'reg_no'] as $field) {
|
||||
$candidate = trim((string) ($doctorRow[$field] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$doctorRegNo = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$practitioner = ['id' => (string) $performerId, 'name' => $doctorName, 'registration_number' => $doctorRegNo];
|
||||
}
|
||||
}
|
||||
|
||||
$latestDate = '';
|
||||
foreach ($records as $record) {
|
||||
$candidate = trim((string) ($record['given_date'] ?? $record['due_date'] ?? ''));
|
||||
if ($candidate !== '' && ($latestDate === '' || strtotime($candidate) > strtotime($latestDate))) {
|
||||
$latestDate = $candidate;
|
||||
}
|
||||
}
|
||||
$visitDate = $latestDate !== '' ? date('Y-m-d', strtotime($latestDate)) : date('Y-m-d');
|
||||
$ccRef = $recordId > 0 ? ('IMM-' . $recordId . '-' . $visitDate) : ('IMM-' . $patientId . '-' . $visitDate);
|
||||
|
||||
$birthDate = '';
|
||||
foreach (['dob', 'birth_date', 'date_of_birth', 'p_dob'] as $field) {
|
||||
$candidate = trim((string) ($patientRow[$field] ?? ''));
|
||||
if ($candidate !== '' && strtotime($candidate) !== false) {
|
||||
$birthDate = date('Y-m-d', strtotime($candidate));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$patient = [
|
||||
'id' => (string) $patientId,
|
||||
'name' => $patientName !== '' ? $patientName : ('PATIENT-' . $patientId),
|
||||
'gender' => (string) ($patientRow['gender'] ?? $patientRow['xgender'] ?? ''),
|
||||
'birthDate' => $birthDate,
|
||||
'abhaAddress' => $rawAbha,
|
||||
'phone' => (string) ($patientRow['mphone1'] ?? ''),
|
||||
];
|
||||
|
||||
$fhir = new FhirR4Builder();
|
||||
$bundle = $fhir->buildImmunizationRecordBundle($patient, $records, [
|
||||
'practitioner' => $practitioner,
|
||||
'organization' => ['name' => (string) ($hospital['name'] ?? ''), 'hfr_id' => (string) ($hospital['hfr_id'] ?? '')],
|
||||
'encounter' => ['id' => $recordId > 0 ? ('IMM-' . $recordId) : ('IMM-' . $patientId), 'status' => 'finished', 'period_start' => $latestDate],
|
||||
'care_context_reference' => $ccRef,
|
||||
]);
|
||||
|
||||
return [
|
||||
'bundle' => $bundle,
|
||||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => 'Immunization Record - ' . $visitDate,
|
||||
'patient_name' => $patient['name'],
|
||||
'visit_date' => $visitDate,
|
||||
'records' => $records,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $columns
|
||||
* @return array<int,array<string,mixed>>
|
||||
|
|
@ -3979,6 +4261,7 @@ class AbdmGateway extends BaseController
|
|||
'IPD' => 'HealthDocumentRecord',
|
||||
'LAB' => 'DiagnosticReportRecord',
|
||||
'DISCHARGE' => 'DischargeSummaryRecord',
|
||||
'IMMUNIZATION' => 'ImmunizationRecord',
|
||||
'MLC' => 'HealthDocumentRecord',
|
||||
default => 'HealthDocumentRecord',
|
||||
};
|
||||
|
|
@ -4179,6 +4462,9 @@ class AbdmGateway extends BaseController
|
|||
if (str_contains($v, 'discharge')) {
|
||||
return 'DISCHARGE';
|
||||
}
|
||||
if (str_contains($v, 'immunization')) {
|
||||
return 'IMMUNIZATION';
|
||||
}
|
||||
if ($entityType === 'ipd') {
|
||||
return 'IPD';
|
||||
}
|
||||
|
|
|
|||
340
app/Controllers/Immunization.php
Normal file
340
app/Controllers/Immunization.php
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
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()
|
||||
{
|
||||
return view('immunization/schedule_master', [
|
||||
'page_title' => 'UIP Schedule Master',
|
||||
'items' => $this->loadScheduleRows(true),
|
||||
]);
|
||||
}
|
||||
|
||||
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', 'p_lname', '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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,497 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateImmunizationModuleTables extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->createVaccineMasterTable();
|
||||
$this->createScheduleMasterTable();
|
||||
$this->createRecordsTable();
|
||||
$this->seedUipSchedule();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('immunization_records', true);
|
||||
$this->forge->dropTable('immunization_schedule_master', true);
|
||||
$this->forge->dropTable('immunization_vaccine_master', true);
|
||||
}
|
||||
|
||||
private function createVaccineMasterTable(): void
|
||||
{
|
||||
if ($this->tableExists('immunization_vaccine_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'vaccine_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'null' => false,
|
||||
],
|
||||
'vaccine_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'vaccine_code_system' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'vaccine_display' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'null' => true,
|
||||
],
|
||||
'target_disease_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'target_disease_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'null' => true,
|
||||
],
|
||||
'route_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'route_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'site_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'site_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'is_active' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 1,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey('vaccine_name');
|
||||
$this->forge->addKey('vaccine_code');
|
||||
$this->forge->addKey('is_active');
|
||||
$this->forge->createTable('immunization_vaccine_master', true);
|
||||
}
|
||||
|
||||
private function createScheduleMasterTable(): void
|
||||
{
|
||||
if ($this->tableExists('immunization_schedule_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'schedule_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'default' => 'UIP',
|
||||
],
|
||||
'age_label' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => false,
|
||||
],
|
||||
'age_value' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 0,
|
||||
],
|
||||
'age_unit' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 20,
|
||||
'default' => 'days',
|
||||
],
|
||||
'age_offset_days' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 0,
|
||||
],
|
||||
'vaccine_master_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
'dose_number' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'null' => true,
|
||||
],
|
||||
'series_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'default' => 'Indian Universal Immunization Programme',
|
||||
],
|
||||
'series_doses' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'null' => true,
|
||||
],
|
||||
'notes' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'sort_order' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 0,
|
||||
],
|
||||
'is_uip' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 1,
|
||||
],
|
||||
'is_active' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 1,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey('vaccine_master_id');
|
||||
$this->forge->addKey('age_offset_days');
|
||||
$this->forge->addKey('is_uip');
|
||||
$this->forge->addKey('is_active');
|
||||
$this->forge->createTable('immunization_schedule_master', true);
|
||||
}
|
||||
|
||||
private function createRecordsTable(): void
|
||||
{
|
||||
if ($this->tableExists('immunization_records')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'patient_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
'opd_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'ipd_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'schedule_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'vaccine_master_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'vaccine_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'null' => false,
|
||||
],
|
||||
'vaccine_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'vaccine_code_system' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'dose_number' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'null' => true,
|
||||
],
|
||||
'due_date' => [
|
||||
'type' => 'DATE',
|
||||
'null' => true,
|
||||
],
|
||||
'given_date' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'default' => 'due',
|
||||
],
|
||||
'lot_number' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'expiry_date' => [
|
||||
'type' => 'DATE',
|
||||
'null' => true,
|
||||
],
|
||||
'manufacturer' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'null' => true,
|
||||
],
|
||||
'performer_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'location_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'null' => true,
|
||||
],
|
||||
'route_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'route_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'site_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'site_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'reaction_notes' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'notes' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'abdm_care_context_reference' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 160,
|
||||
'null' => true,
|
||||
],
|
||||
'abdm_health_record_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'abdm_push_status' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'created_by' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'updated_by' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey('patient_id');
|
||||
$this->forge->addKey('schedule_id');
|
||||
$this->forge->addKey('vaccine_master_id');
|
||||
$this->forge->addKey('due_date');
|
||||
$this->forge->addKey('given_date');
|
||||
$this->forge->addKey('status');
|
||||
$this->forge->addKey('abdm_care_context_reference');
|
||||
$this->forge->createTable('immunization_records', true);
|
||||
}
|
||||
|
||||
private function seedUipSchedule(): void
|
||||
{
|
||||
if (! $this->tableExists('immunization_vaccine_master') || ! $this->tableExists('immunization_schedule_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $this->db->table('immunization_schedule_master')->where('is_uip', 1)->countAllResults();
|
||||
if ($existing > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$codeSystem = 'https://hms.local/immunization/uip';
|
||||
$vaccines = [
|
||||
'UIP-BCG' => ['BCG', 'Tuberculosis', 'Intradermal'],
|
||||
'UIP-OPV' => ['Oral Polio Vaccine', 'Poliomyelitis', 'Oral'],
|
||||
'UIP-HEPB' => ['Hepatitis B Vaccine', 'Hepatitis B', 'Intramuscular'],
|
||||
'UIP-PENTA' => ['Pentavalent Vaccine', 'Diphtheria, Pertussis, Tetanus, Hepatitis B and Hib', 'Intramuscular'],
|
||||
'UIP-ROTA' => ['Rotavirus Vaccine', 'Rotavirus gastroenteritis', 'Oral'],
|
||||
'UIP-FIPV' => ['Fractional IPV', 'Poliomyelitis', 'Intradermal'],
|
||||
'UIP-PCV' => ['Pneumococcal Conjugate Vaccine', 'Pneumococcal disease', 'Intramuscular'],
|
||||
'UIP-MR' => ['Measles Rubella Vaccine', 'Measles and rubella', 'Subcutaneous'],
|
||||
'UIP-JE' => ['Japanese Encephalitis Vaccine', 'Japanese encephalitis', 'Subcutaneous'],
|
||||
'UIP-DPT' => ['DPT Vaccine', 'Diphtheria, Pertussis and Tetanus', 'Intramuscular'],
|
||||
'UIP-TD' => ['Td Vaccine', 'Tetanus and diphtheria', 'Intramuscular'],
|
||||
];
|
||||
|
||||
$vaccineRows = [];
|
||||
foreach ($vaccines as $code => $vaccine) {
|
||||
$vaccineRows[] = [
|
||||
'vaccine_name' => $vaccine[0],
|
||||
'vaccine_code' => $code,
|
||||
'vaccine_code_system' => $codeSystem,
|
||||
'vaccine_display' => $vaccine[0],
|
||||
'target_disease_name' => $vaccine[1],
|
||||
'route_name' => $vaccine[2],
|
||||
'is_active' => 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
$this->db->table('immunization_vaccine_master')->insertBatch($vaccineRows);
|
||||
|
||||
$vaccineIds = [];
|
||||
$rows = $this->db->table('immunization_vaccine_master')->select('id,vaccine_code')->whereIn('vaccine_code', array_keys($vaccines))->get()->getResultArray();
|
||||
foreach ($rows as $row) {
|
||||
$vaccineIds[(string) $row['vaccine_code']] = (int) $row['id'];
|
||||
}
|
||||
|
||||
$schedule = [
|
||||
['At Birth', 0, 'days', 0, 'UIP-BCG', '1', '1', 'Preferably at birth or as early as possible.'],
|
||||
['At Birth', 0, 'days', 0, 'UIP-OPV', '0', '4', 'OPV birth dose.'],
|
||||
['At Birth', 0, 'days', 0, 'UIP-HEPB', '0', '4', 'Within 24 hours of birth where possible.'],
|
||||
['6 Weeks', 6, 'weeks', 42, 'UIP-OPV', '1', '4', ''],
|
||||
['6 Weeks', 6, 'weeks', 42, 'UIP-PENTA', '1', '3', ''],
|
||||
['6 Weeks', 6, 'weeks', 42, 'UIP-ROTA', '1', '3', ''],
|
||||
['6 Weeks', 6, 'weeks', 42, 'UIP-FIPV', '1', '2', ''],
|
||||
['6 Weeks', 6, 'weeks', 42, 'UIP-PCV', '1', '3', 'Where applicable as per local programme.'],
|
||||
['10 Weeks', 10, 'weeks', 70, 'UIP-OPV', '2', '4', ''],
|
||||
['10 Weeks', 10, 'weeks', 70, 'UIP-PENTA', '2', '3', ''],
|
||||
['10 Weeks', 10, 'weeks', 70, 'UIP-ROTA', '2', '3', ''],
|
||||
['14 Weeks', 14, 'weeks', 98, 'UIP-OPV', '3', '4', ''],
|
||||
['14 Weeks', 14, 'weeks', 98, 'UIP-PENTA', '3', '3', ''],
|
||||
['14 Weeks', 14, 'weeks', 98, 'UIP-ROTA', '3', '3', ''],
|
||||
['14 Weeks', 14, 'weeks', 98, 'UIP-FIPV', '2', '2', ''],
|
||||
['14 Weeks', 14, 'weeks', 98, 'UIP-PCV', '2', '3', 'Where applicable as per local programme.'],
|
||||
['9-12 Months', 9, 'months', 274, 'UIP-MR', '1', '2', ''],
|
||||
['9-12 Months', 9, 'months', 274, 'UIP-JE', '1', '2', 'In JE endemic districts.'],
|
||||
['9-12 Months', 9, 'months', 274, 'UIP-PCV', 'Booster', '3', 'Where applicable as per local programme.'],
|
||||
['16-24 Months', 16, 'months', 487, 'UIP-DPT', 'Booster 1', '3', ''],
|
||||
['16-24 Months', 16, 'months', 487, 'UIP-OPV', 'Booster', '4', ''],
|
||||
['16-24 Months', 16, 'months', 487, 'UIP-MR', '2', '2', ''],
|
||||
['16-24 Months', 16, 'months', 487, 'UIP-JE', '2', '2', 'In JE endemic districts.'],
|
||||
['5-6 Years', 5, 'years', 1826, 'UIP-DPT', 'Booster 2', '3', ''],
|
||||
['10 Years', 10, 'years', 3652, 'UIP-TD', '1', '2', ''],
|
||||
['16 Years', 16, 'years', 5844, 'UIP-TD', '2', '2', ''],
|
||||
];
|
||||
|
||||
$scheduleRows = [];
|
||||
foreach ($schedule as $index => $item) {
|
||||
$vaccineId = $vaccineIds[$item[4]] ?? 0;
|
||||
if ($vaccineId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scheduleRows[] = [
|
||||
'schedule_name' => 'UIP',
|
||||
'age_label' => $item[0],
|
||||
'age_value' => $item[1],
|
||||
'age_unit' => $item[2],
|
||||
'age_offset_days' => $item[3],
|
||||
'vaccine_master_id' => $vaccineId,
|
||||
'dose_number' => $item[5],
|
||||
'series_name' => 'Indian Universal Immunization Programme',
|
||||
'series_doses' => $item[6],
|
||||
'notes' => $item[7] !== '' ? $item[7] : null,
|
||||
'sort_order' => $index + 1,
|
||||
'is_uip' => 1,
|
||||
'is_active' => 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($scheduleRows)) {
|
||||
$this->db->table('immunization_schedule_master')->insertBatch($scheduleRows);
|
||||
}
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
{
|
||||
$rows = $this->db->query('SHOW TABLES LIKE ?', [$table])->getResultArray();
|
||||
return ! empty($rows);
|
||||
}
|
||||
}
|
||||
|
|
@ -1389,6 +1389,340 @@ class FhirR4Builder
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ABDM ImmunizationRecord FHIR DocumentBundle.
|
||||
*
|
||||
* @param array<string,mixed> $patient
|
||||
* @param array<int,array<string,mixed>> $immunizations
|
||||
* @param array<string,mixed> $context practitioner, organization, encounter, care_context_reference
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function buildImmunizationRecordBundle(array $patient, array $immunizations, array $context = []): array
|
||||
{
|
||||
$issuedAt = $this->isoTimestamp();
|
||||
$bundleUuid = $this->generateUuid();
|
||||
$compositionUuid = $this->generateUuid();
|
||||
$patientUuid = $this->generateUuid();
|
||||
$patientRef = 'urn:uuid:' . $patientUuid;
|
||||
|
||||
$practitioner = is_array($context['practitioner'] ?? null) ? (array) $context['practitioner'] : [];
|
||||
$organization = is_array($context['organization'] ?? null) ? (array) $context['organization'] : [];
|
||||
$encounter = is_array($context['encounter'] ?? null) ? (array) $context['encounter'] : [];
|
||||
|
||||
$hasPractitioner = trim((string) ($practitioner['id'] ?? '')) !== '' || trim((string) ($practitioner['name'] ?? '')) !== '';
|
||||
$hasOrganization = trim((string) ($organization['id'] ?? $organization['hfr_id'] ?? '')) !== '' || trim((string) ($organization['name'] ?? '')) !== '';
|
||||
$hasEncounter = trim((string) ($encounter['id'] ?? '')) !== '';
|
||||
|
||||
$practitionerUuid = $hasPractitioner ? $this->generateUuid() : '';
|
||||
$organizationUuid = $hasOrganization ? $this->generateUuid() : '';
|
||||
$encounterUuid = $hasEncounter ? $this->generateUuid() : '';
|
||||
$practitionerRef = $hasPractitioner ? ('urn:uuid:' . $practitionerUuid) : '';
|
||||
$organizationRef = $hasOrganization ? ('urn:uuid:' . $organizationUuid) : '';
|
||||
$encounterRef = $hasEncounter ? ('urn:uuid:' . $encounterUuid) : '';
|
||||
|
||||
$resourceEntries = [];
|
||||
|
||||
$patientResource = $this->buildPatientResource($patient);
|
||||
$patientResource['id'] = $patientUuid;
|
||||
$patientResource['meta'] = ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient']];
|
||||
$resourceEntries[] = ['fullUrl' => $patientRef, 'resource' => $patientResource];
|
||||
|
||||
if ($hasPractitioner) {
|
||||
$practitionerResource = [
|
||||
'resourceType' => 'Practitioner',
|
||||
'id' => $practitionerUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner']],
|
||||
'name' => [['text' => trim((string) ($practitioner['name'] ?? ''))]],
|
||||
];
|
||||
$registrationNumber = trim((string) ($practitioner['registration_number'] ?? ''));
|
||||
if ($registrationNumber !== '') {
|
||||
$practitionerResource['identifier'] = [[
|
||||
'type' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203',
|
||||
'code' => 'MD',
|
||||
'display' => 'Medical License number',
|
||||
]]],
|
||||
'system' => 'https://doctor.ndhm.gov.in',
|
||||
'value' => $registrationNumber,
|
||||
]];
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => $practitionerRef, 'resource' => $practitionerResource];
|
||||
}
|
||||
|
||||
if ($hasOrganization) {
|
||||
$hfrId = trim((string) ($organization['hfr_id'] ?? $organization['id'] ?? ''));
|
||||
$organizationResource = [
|
||||
'resourceType' => 'Organization',
|
||||
'id' => $organizationUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization']],
|
||||
'name' => trim((string) ($organization['name'] ?? '')),
|
||||
];
|
||||
if ($hfrId !== '') {
|
||||
$organizationResource['identifier'] = [[
|
||||
'type' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203',
|
||||
'code' => 'PRN',
|
||||
'display' => 'Provider number',
|
||||
]]],
|
||||
'system' => 'https://facility.ndhm.gov.in',
|
||||
'value' => $hfrId,
|
||||
]];
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => $organizationRef, 'resource' => $organizationResource];
|
||||
}
|
||||
|
||||
if ($hasEncounter) {
|
||||
$encounterResource = [
|
||||
'resourceType' => 'Encounter',
|
||||
'id' => $encounterUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter']],
|
||||
'identifier' => [['system' => 'https://ndhm.in', 'value' => (string) ($encounter['id'] ?? $encounterUuid)]],
|
||||
'status' => (string) ($encounter['status'] ?? 'finished'),
|
||||
'class' => [
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
|
||||
'code' => (string) ($encounter['class_code'] ?? 'AMB'),
|
||||
'display' => (string) ($encounter['class_display'] ?? 'Ambulatory'),
|
||||
],
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
];
|
||||
$periodStart = $this->normalizeIsoDateTime((string) ($encounter['period_start'] ?? ''), '');
|
||||
if ($periodStart !== '') {
|
||||
$encounterResource['period'] = ['start' => $periodStart];
|
||||
}
|
||||
if ($practitionerRef !== '') {
|
||||
$encounterResource['participant'] = [['individual' => ['reference' => $practitionerRef]]];
|
||||
}
|
||||
if ($organizationRef !== '') {
|
||||
$encounterResource['serviceProvider'] = ['reference' => $organizationRef];
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => $encounterRef, 'resource' => $encounterResource];
|
||||
}
|
||||
|
||||
$immunizationRefs = [];
|
||||
foreach ($immunizations as $immunization) {
|
||||
$vaccineName = trim((string) ($immunization['vaccine_name'] ?? $immunization['vaccine_display'] ?? ''));
|
||||
if ($vaccineName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$immunizationUuid = $this->generateUuid();
|
||||
$immunizationRef = 'urn:uuid:' . $immunizationUuid;
|
||||
$status = $this->normalizeImmunizationStatus((string) ($immunization['status'] ?? 'completed'));
|
||||
$occurrence = $this->normalizeIsoDateTime(
|
||||
(string) ($immunization['given_date'] ?? $immunization['occurrenceDateTime'] ?? $immunization['due_date'] ?? ''),
|
||||
$issuedAt
|
||||
);
|
||||
|
||||
$resource = [
|
||||
'resourceType' => 'Immunization',
|
||||
'id' => $immunizationUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Immunization']],
|
||||
'identifier' => [[
|
||||
'system' => 'https://ndhm.in/immunization',
|
||||
'value' => (string) ($immunization['id'] ?? $immunizationUuid),
|
||||
]],
|
||||
'status' => $status,
|
||||
'vaccineCode' => $this->buildSimpleCodeableConcept(
|
||||
(string) ($immunization['vaccine_code_system'] ?? ''),
|
||||
(string) ($immunization['vaccine_code'] ?? ''),
|
||||
$vaccineName,
|
||||
$vaccineName
|
||||
),
|
||||
'patient' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'occurrenceDateTime' => $occurrence,
|
||||
'recorded' => $issuedAt,
|
||||
'primarySource' => true,
|
||||
];
|
||||
|
||||
if ($encounterRef !== '') {
|
||||
$resource['encounter'] = ['reference' => $encounterRef];
|
||||
}
|
||||
if ($organizationRef !== '') {
|
||||
$resource['location'] = ['reference' => $organizationRef, 'display' => trim((string) ($organization['name'] ?? ''))];
|
||||
}
|
||||
$manufacturer = trim((string) ($immunization['manufacturer'] ?? ''));
|
||||
if ($manufacturer !== '') {
|
||||
$resource['manufacturer'] = ['display' => $manufacturer];
|
||||
}
|
||||
$lotNumber = trim((string) ($immunization['lot_number'] ?? ''));
|
||||
if ($lotNumber !== '') {
|
||||
$resource['lotNumber'] = $lotNumber;
|
||||
}
|
||||
$expiryDate = trim((string) ($immunization['expiry_date'] ?? $immunization['expirationDate'] ?? ''));
|
||||
if ($expiryDate !== '' && strtotime($expiryDate) !== false) {
|
||||
$resource['expirationDate'] = date('Y-m-d', strtotime($expiryDate));
|
||||
}
|
||||
$site = $this->buildSimpleCodeableConcept('http://snomed.info/sct', (string) ($immunization['site_code'] ?? ''), (string) ($immunization['site_name'] ?? ''), (string) ($immunization['site_name'] ?? ''));
|
||||
if (! empty($site['text']) || ! empty($site['coding'])) {
|
||||
$resource['site'] = $site;
|
||||
}
|
||||
$route = $this->buildSimpleCodeableConcept('http://snomed.info/sct', (string) ($immunization['route_code'] ?? ''), (string) ($immunization['route_name'] ?? ''), (string) ($immunization['route_name'] ?? ''));
|
||||
if (! empty($route['text']) || ! empty($route['coding'])) {
|
||||
$resource['route'] = $route;
|
||||
}
|
||||
if ($practitionerRef !== '') {
|
||||
$resource['performer'] = [[
|
||||
'function' => ['text' => 'Administering provider'],
|
||||
'actor' => ['reference' => $practitionerRef, 'display' => trim((string) ($practitioner['name'] ?? ''))],
|
||||
]];
|
||||
} elseif ($organizationRef !== '') {
|
||||
$resource['performer'] = [[
|
||||
'function' => ['text' => 'Administering organization'],
|
||||
'actor' => ['reference' => $organizationRef, 'display' => trim((string) ($organization['name'] ?? ''))],
|
||||
]];
|
||||
}
|
||||
|
||||
$protocol = [];
|
||||
$series = trim((string) ($immunization['series_name'] ?? ''));
|
||||
if ($series !== '') {
|
||||
$protocol['series'] = $series;
|
||||
}
|
||||
$dose = $this->buildDoseNumberElement((string) ($immunization['dose_number'] ?? ''));
|
||||
if (! empty($dose)) {
|
||||
$protocol += $dose;
|
||||
}
|
||||
$seriesDoses = $this->buildDoseNumberElement((string) ($immunization['series_doses'] ?? ''), 'seriesDoses');
|
||||
if (! empty($seriesDoses)) {
|
||||
$protocol += $seriesDoses;
|
||||
}
|
||||
$targetDisease = $this->buildSimpleCodeableConcept('http://snomed.info/sct', (string) ($immunization['target_disease_code'] ?? ''), (string) ($immunization['target_disease_name'] ?? ''), (string) ($immunization['target_disease_name'] ?? ''));
|
||||
if (! empty($targetDisease['text']) || ! empty($targetDisease['coding'])) {
|
||||
$protocol['targetDisease'] = [$targetDisease];
|
||||
}
|
||||
if ($organizationRef !== '') {
|
||||
$protocol['authority'] = ['reference' => $organizationRef];
|
||||
}
|
||||
if (! empty($protocol)) {
|
||||
$resource['protocolApplied'] = [$protocol];
|
||||
}
|
||||
|
||||
$note = trim((string) ($immunization['notes'] ?? ''));
|
||||
if ($note !== '') {
|
||||
$resource['note'] = [['text' => $note]];
|
||||
}
|
||||
|
||||
$immunizationRefs[] = ['reference' => $immunizationRef, 'display' => $vaccineName];
|
||||
$resourceEntries[] = ['fullUrl' => $immunizationRef, 'resource' => $resource];
|
||||
}
|
||||
|
||||
$composition = [
|
||||
'resourceType' => 'Composition',
|
||||
'id' => $compositionUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ImmunizationRecord']],
|
||||
'language' => 'en-IN',
|
||||
'text' => [
|
||||
'status' => 'generated',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml">Immunization Record</div>',
|
||||
],
|
||||
'identifier' => [
|
||||
'system' => 'https://ndhm.in/phr',
|
||||
'value' => $compositionUuid,
|
||||
],
|
||||
'status' => 'final',
|
||||
'type' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '41000179103',
|
||||
'display' => 'Immunization record',
|
||||
]],
|
||||
'text' => 'Immunization record',
|
||||
],
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'date' => $issuedAt,
|
||||
'title' => 'Immunization Record',
|
||||
'section' => [[
|
||||
'title' => 'Immunization record',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '41000179103',
|
||||
'display' => 'Immunization record',
|
||||
]]],
|
||||
'entry' => $immunizationRefs,
|
||||
]],
|
||||
];
|
||||
if ($practitionerRef !== '') {
|
||||
$composition['author'] = [['reference' => $practitionerRef]];
|
||||
} elseif ($organizationRef !== '') {
|
||||
$composition['author'] = [['reference' => $organizationRef]];
|
||||
}
|
||||
|
||||
$careContextReference = trim((string) ($context['care_context_reference'] ?? ('IMM-' . (string) ($patient['id'] ?? 'unknown') . '-' . date('YmdHis'))));
|
||||
$hfrId = trim((string) ($organization['hfr_id'] ?? $organization['id'] ?? ''));
|
||||
|
||||
return [
|
||||
'resourceType' => 'Bundle',
|
||||
'id' => $bundleUuid,
|
||||
'meta' => [
|
||||
'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'],
|
||||
'security' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/v3-Confidentiality',
|
||||
'code' => 'V',
|
||||
'display' => 'very restricted',
|
||||
]],
|
||||
],
|
||||
'identifier' => [
|
||||
'system' => $hfrId !== '' ? ('https://' . $hfrId . '.hfr.abdm.gov.in') : 'urn:ietf:rfc:3986',
|
||||
'value' => $careContextReference,
|
||||
],
|
||||
'type' => 'document',
|
||||
'timestamp' => $issuedAt,
|
||||
'entry' => array_merge([['fullUrl' => 'urn:uuid:' . $compositionUuid, 'resource' => $composition]], $resourceEntries),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeImmunizationStatus(string $status): string
|
||||
{
|
||||
$status = strtolower(trim($status));
|
||||
return match ($status) {
|
||||
'completed', 'done', 'given' => 'completed',
|
||||
'entered-in-error', 'entered_in_error', 'error' => 'entered-in-error',
|
||||
default => 'not-done',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function buildSimpleCodeableConcept(string $system, string $code, string $display, string $text): array
|
||||
{
|
||||
$concept = [];
|
||||
$text = trim($text);
|
||||
if ($text !== '') {
|
||||
$concept['text'] = $text;
|
||||
}
|
||||
|
||||
$system = trim($system);
|
||||
$code = trim($code);
|
||||
$display = trim($display);
|
||||
if ($system !== '' && $code !== '' && $display !== '') {
|
||||
$concept['coding'] = [[
|
||||
'system' => $system,
|
||||
'code' => $code,
|
||||
'display' => $display,
|
||||
]];
|
||||
}
|
||||
|
||||
return $concept;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function buildDoseNumberElement(string $value, string $fieldPrefix = 'doseNumber'): array
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (preg_match('/^[1-9]\d*$/', $value) === 1) {
|
||||
return [$fieldPrefix . 'PositiveInt' => (int) $value];
|
||||
}
|
||||
|
||||
return [$fieldPrefix . 'String' => $value];
|
||||
}
|
||||
|
||||
/** Generate a cryptographically random UUID v4. */
|
||||
private function generateUuid(): string
|
||||
{
|
||||
|
|
|
|||
153
app/Libraries/ImmunizationScheduleService.php
Normal file
153
app/Libraries/ImmunizationScheduleService.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use Config\Database;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
class ImmunizationScheduleService
|
||||
{
|
||||
private BaseConnection $db;
|
||||
|
||||
public function __construct(?BaseConnection $db = null)
|
||||
{
|
||||
$this->db = $db ?? Database::connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:int,created:int,skipped:int,error?:string,patient_id:int}
|
||||
*/
|
||||
public function generateUipDueRecordsForPatient(int $patientId, array $options = []): array
|
||||
{
|
||||
if ($patientId <= 0) {
|
||||
return ['ok' => 0, 'created' => 0, 'skipped' => 0, 'patient_id' => $patientId, 'error' => 'Invalid patient_id'];
|
||||
}
|
||||
|
||||
foreach (['patient_master', 'immunization_schedule_master', 'immunization_vaccine_master', 'immunization_records'] as $table) {
|
||||
if (! $this->db->tableExists($table)) {
|
||||
return ['ok' => 0, 'created' => 0, 'skipped' => 0, 'patient_id' => $patientId, 'error' => $table . ' table not found'];
|
||||
}
|
||||
}
|
||||
|
||||
$patient = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray();
|
||||
if (empty($patient)) {
|
||||
return ['ok' => 0, 'created' => 0, 'skipped' => 0, 'patient_id' => $patientId, 'error' => 'Patient not found'];
|
||||
}
|
||||
|
||||
$dob = $this->resolvePatientDob($patient);
|
||||
if ($dob === null) {
|
||||
return ['ok' => 0, 'created' => 0, 'skipped' => 0, 'patient_id' => $patientId, 'error' => 'Patient DOB is required'];
|
||||
}
|
||||
|
||||
$today = new DateTimeImmutable('today', new DateTimeZone('Asia/Kolkata'));
|
||||
$diff = $dob->diff($today);
|
||||
$ageDays = $diff->invert === 0 ? (int) $diff->days : 0;
|
||||
$generationMode = strtolower(trim((string) ($options['generation_mode'] ?? 'full')));
|
||||
if (! in_array($generationMode, ['eligible', 'full'], true)) {
|
||||
$generationMode = 'full';
|
||||
}
|
||||
|
||||
$scheduleRows = $this->db->table('immunization_schedule_master s')
|
||||
->select('s.*, v.vaccine_name, v.vaccine_code, v.vaccine_code_system, v.route_code, v.route_name, v.site_code, v.site_name')
|
||||
->join('immunization_vaccine_master v', 'v.id = s.vaccine_master_id', 'inner')
|
||||
->where('s.is_uip', 1)
|
||||
->where('s.is_active', 1)
|
||||
->where('v.is_active', 1)
|
||||
->orderBy('s.sort_order', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($scheduleRows)) {
|
||||
return ['ok' => 1, 'created' => 0, 'skipped' => 0, 'patient_id' => $patientId];
|
||||
}
|
||||
|
||||
$existingRows = $this->db->table('immunization_records')
|
||||
->select('schedule_id')
|
||||
->where('patient_id', $patientId)
|
||||
->where('schedule_id IS NOT NULL', null, false)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$existingScheduleIds = [];
|
||||
foreach ($existingRows as $row) {
|
||||
$existingScheduleIds[(int) ($row['schedule_id'] ?? 0)] = true;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$insertRows = [];
|
||||
$skipped = 0;
|
||||
$futureSkipped = 0;
|
||||
foreach ($scheduleRows as $schedule) {
|
||||
$scheduleId = (int) ($schedule['id'] ?? 0);
|
||||
if ($scheduleId <= 0 || isset($existingScheduleIds[$scheduleId])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$offsetDays = max(0, (int) ($schedule['age_offset_days'] ?? 0));
|
||||
if ($generationMode === 'eligible' && $offsetDays > $ageDays) {
|
||||
$futureSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$dueDate = $dob->modify('+' . $offsetDays . ' days')->format('Y-m-d');
|
||||
$insertRows[] = [
|
||||
'patient_id' => $patientId,
|
||||
'schedule_id' => $scheduleId,
|
||||
'vaccine_master_id' => (int) ($schedule['vaccine_master_id'] ?? 0),
|
||||
'vaccine_name' => (string) ($schedule['vaccine_name'] ?? ''),
|
||||
'vaccine_code' => (string) ($schedule['vaccine_code'] ?? ''),
|
||||
'vaccine_code_system' => (string) ($schedule['vaccine_code_system'] ?? ''),
|
||||
'dose_number' => (string) ($schedule['dose_number'] ?? ''),
|
||||
'due_date' => $dueDate,
|
||||
'status' => 'due',
|
||||
'route_code' => (string) ($schedule['route_code'] ?? ''),
|
||||
'route_name' => (string) ($schedule['route_name'] ?? ''),
|
||||
'site_code' => (string) ($schedule['site_code'] ?? ''),
|
||||
'site_name' => (string) ($schedule['site_name'] ?? ''),
|
||||
'notes' => (string) ($schedule['notes'] ?? ''),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertRows)) {
|
||||
$this->db->table('immunization_records')->insertBatch($insertRows);
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => 1,
|
||||
'created' => count($insertRows),
|
||||
'skipped' => $skipped,
|
||||
'future_skipped' => $futureSkipped,
|
||||
'patient_id' => $patientId,
|
||||
'generation_mode' => $generationMode,
|
||||
'patient_age_days' => $ageDays,
|
||||
'patient_gender' => (string) ($patient['gender'] ?? $patient['xgender'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $patient
|
||||
*/
|
||||
private function resolvePatientDob(array $patient): ?DateTimeImmutable
|
||||
{
|
||||
foreach (['dob', 'birth_date', 'date_of_birth', 'p_dob'] as $field) {
|
||||
$raw = trim((string) ($patient[$field] ?? ''));
|
||||
if ($raw === '' || $raw === '0000-00-00') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($raw);
|
||||
if ($timestamp === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return (new DateTimeImmutable('@' . $timestamp))->setTimezone(new DateTimeZone('Asia/Kolkata'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
45
app/Models/ImmunizationRecordModel.php
Normal file
45
app/Models/ImmunizationRecordModel.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ImmunizationRecordModel extends Model
|
||||
{
|
||||
protected $table = 'immunization_records';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = false;
|
||||
protected $allowedFields = [
|
||||
'patient_id',
|
||||
'opd_id',
|
||||
'ipd_id',
|
||||
'schedule_id',
|
||||
'vaccine_master_id',
|
||||
'vaccine_name',
|
||||
'vaccine_code',
|
||||
'vaccine_code_system',
|
||||
'dose_number',
|
||||
'due_date',
|
||||
'given_date',
|
||||
'status',
|
||||
'lot_number',
|
||||
'expiry_date',
|
||||
'manufacturer',
|
||||
'performer_id',
|
||||
'location_name',
|
||||
'route_code',
|
||||
'route_name',
|
||||
'site_code',
|
||||
'site_name',
|
||||
'reaction_notes',
|
||||
'notes',
|
||||
'abdm_care_context_reference',
|
||||
'abdm_health_record_id',
|
||||
'abdm_push_status',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
}
|
||||
30
app/Models/ImmunizationScheduleModel.php
Normal file
30
app/Models/ImmunizationScheduleModel.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ImmunizationScheduleModel extends Model
|
||||
{
|
||||
protected $table = 'immunization_schedule_master';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = false;
|
||||
protected $allowedFields = [
|
||||
'schedule_name',
|
||||
'age_label',
|
||||
'age_value',
|
||||
'age_unit',
|
||||
'age_offset_days',
|
||||
'vaccine_master_id',
|
||||
'dose_number',
|
||||
'series_name',
|
||||
'series_doses',
|
||||
'notes',
|
||||
'sort_order',
|
||||
'is_uip',
|
||||
'is_active',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
}
|
||||
28
app/Models/ImmunizationVaccineModel.php
Normal file
28
app/Models/ImmunizationVaccineModel.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ImmunizationVaccineModel extends Model
|
||||
{
|
||||
protected $table = 'immunization_vaccine_master';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = false;
|
||||
protected $allowedFields = [
|
||||
'vaccine_name',
|
||||
'vaccine_code',
|
||||
'vaccine_code_system',
|
||||
'vaccine_display',
|
||||
'target_disease_code',
|
||||
'target_disease_name',
|
||||
'route_code',
|
||||
'route_name',
|
||||
'site_code',
|
||||
'site_name',
|
||||
'is_active',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
}
|
||||
|
|
@ -71,6 +71,7 @@
|
|||
'back_url' => $historyBackUrl,
|
||||
'back_title' => 'Doctor Patient Search',
|
||||
]);
|
||||
$immunizationUrl = base_url('Immunization') . '?' . http_build_query(['patient_id' => $patientId]);
|
||||
$patientRawName = trim((string) ($patient['p_fname'] ?? '') . ' { ' . (string) ($patient['p_rname'] ?? '') . ' }');
|
||||
$patientAge = function_exists('get_age_1')
|
||||
? trim((string) get_age_1($patient['dob'] ?? null, $patient['age'] ?? '', $patient['age_in_month'] ?? '', $patient['estimate_dob'] ?? '', $patient['last_visit'] ?? null))
|
||||
|
|
@ -96,6 +97,9 @@
|
|||
<a href="javascript:load_form('<?= esc($documentUrl, 'js') ?>','Patient Documents');" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-file-earmark-text"></i> Issue Document
|
||||
</a>
|
||||
<a href="javascript:load_form('<?= esc($immunizationUrl, 'js') ?>','Immunization Record');" class="btn btn-success btn-sm">
|
||||
<i class="bi bi-shield-check"></i> Immunization
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-success" onclick="openPatientDoc()">Open Patient Documents</button>
|
||||
<button type="button" class="btn btn-outline-success" onclick="openPatientImmunization()">Open Immunization</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="load_form('<?= base_url('doctor_work/patient_search') ?>','Doctor Patient Search')">Search Patient with History</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="load_form('<?= base_url('Report/document_list') ?>', 'Document Issue Report')">Document Issue Report</button>
|
||||
</div>
|
||||
|
|
@ -68,4 +69,32 @@ function openPatientDoc() {
|
|||
alert('Unable to resolve patient. Try again.');
|
||||
});
|
||||
}
|
||||
|
||||
function openPatientImmunization() {
|
||||
var patientKey = (document.getElementById('doc_workspace_patient_id').value || '').trim();
|
||||
if (!patientKey) {
|
||||
alert('Enter patient id');
|
||||
return;
|
||||
}
|
||||
|
||||
var resolveUrl = '<?= base_url('Document_Patient/open_by_key') ?>?patient_key=' + encodeURIComponent(patientKey);
|
||||
fetch(resolveUrl, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data && Number(data.status) === 1 && Number(data.patient_id) > 0) {
|
||||
load_form('<?= base_url('Immunization') ?>?patient_id=' + Number(data.patient_id), 'Immunization Record');
|
||||
return;
|
||||
}
|
||||
alert((data && data.message) ? data.message : 'Patient not found');
|
||||
})
|
||||
.catch(function() {
|
||||
alert('Unable to resolve patient. Try again.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
465
app/Views/immunization/index.php
Normal file
465
app/Views/immunization/index.php
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
<?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.p_lname || '')].join(' ')) || ('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>
|
||||
241
app/Views/immunization/schedule_master.php
Normal file
241
app/Views/immunization/schedule_master.php
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<?php
|
||||
$csrfName = csrf_token();
|
||||
$csrfHash = csrf_hash();
|
||||
$items = isset($items) && is_array($items) ? $items : [];
|
||||
?>
|
||||
<div class="container-fluid py-3 immunization-master-ui">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<h5 class="mb-0">UIP Schedule Master</h5>
|
||||
<div class="small text-muted">Edit vaccine, age, route, dose and notes used for new patient schedules.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="load_form('<?= site_url('Immunization') ?>','Immunization Record')">Back to Immunization</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<input type="hidden" id="uipCsrfName" value="<?= esc($csrfName) ?>">
|
||||
<input type="hidden" id="uipCsrfHash" value="<?= esc($csrfHash) ?>">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0" id="uipScheduleMasterTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Sort</th>
|
||||
<th>Age</th>
|
||||
<th>Vaccine</th>
|
||||
<th>Dose</th>
|
||||
<th>Offset Days</th>
|
||||
<th>Route</th>
|
||||
<th>Target Disease</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($items === []) : ?>
|
||||
<tr><td colspan="9" class="text-center text-muted py-4">No UIP schedule found.</td></tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ($items as $item) : ?>
|
||||
<tr data-id="<?= (int) ($item['id'] ?? 0) ?>">
|
||||
<td><?= (int) ($item['sort_order'] ?? 0) ?></td>
|
||||
<td><?= esc((string) ($item['age_label'] ?? '')) ?></td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= esc((string) ($item['vaccine_name'] ?? '')) ?></div>
|
||||
<div class="small text-muted"><?= esc((string) ($item['vaccine_code'] ?? '')) ?></div>
|
||||
</td>
|
||||
<td><?= esc((string) ($item['dose_number'] ?? '')) ?></td>
|
||||
<td><?= (int) ($item['age_offset_days'] ?? 0) ?></td>
|
||||
<td><?= esc((string) ($item['route_name'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($item['target_disease_name'] ?? '')) ?></td>
|
||||
<td><?= ((int) ($item['is_active'] ?? 0) === 1) ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-secondary">Inactive</span>' ?></td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary btnEditSchedule" data-item='<?= esc(json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'attr') ?>'>Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="uipEditModal" 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">Edit UIP Schedule</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="scheduleId">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Age Label</label>
|
||||
<input type="text" class="form-control" id="ageLabel">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label mb-1">Age Value</label>
|
||||
<input type="number" min="0" step="1" class="form-control" id="ageValue">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label mb-1">Age Unit</label>
|
||||
<select class="form-select" id="ageUnit">
|
||||
<option value="days">days</option>
|
||||
<option value="weeks">weeks</option>
|
||||
<option value="months">months</option>
|
||||
<option value="years">years</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label mb-1">Offset Days</label>
|
||||
<input type="number" min="0" step="1" class="form-control" id="ageOffsetDays">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label mb-1">Sort</label>
|
||||
<input type="number" min="0" step="1" class="form-control" id="sortOrder">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label mb-1">Vaccine</label>
|
||||
<input type="text" class="form-control" id="vaccineName">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label mb-1">Dose</label>
|
||||
<input type="text" class="form-control" id="doseNumber">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label mb-1">Series Doses</label>
|
||||
<input type="text" class="form-control" id="seriesDoses">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">Route</label>
|
||||
<input type="text" class="form-control" id="routeName">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label mb-1">Target Disease</label>
|
||||
<input type="text" class="form-control" id="targetDiseaseName">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">Site</label>
|
||||
<input type="text" class="form-control" id="siteName">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">Status</label>
|
||||
<select class="form-select" id="isActive">
|
||||
<option value="1">Active</option>
|
||||
<option value="0">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label mb-1">Notes</label>
|
||||
<textarea class="form-control" rows="2" id="scheduleNotes"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3" id="uipMasterStatus"></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="btnSaveSchedule">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function esc(value) {
|
||||
return $('<div>').text(value == null ? '' : String(value)).html();
|
||||
}
|
||||
|
||||
function csrfPayload() {
|
||||
var payload = {};
|
||||
payload[$('#uipCsrfName').val()] = $('#uipCsrfHash').val();
|
||||
return payload;
|
||||
}
|
||||
|
||||
function updateCsrf(response) {
|
||||
if (response && response.csrfName && response.csrfHash) {
|
||||
$('#uipCsrfName').val(response.csrfName);
|
||||
$('#uipCsrfHash').val(response.csrfHash);
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(type, message) {
|
||||
var cls = type === 'success' ? 'alert-success' : 'alert-warning';
|
||||
$('#uipMasterStatus').html('<div class="alert ' + cls + ' py-2 mb-0">' + esc(message) + '</div>');
|
||||
}
|
||||
|
||||
function fillModal(item) {
|
||||
$('#scheduleId').val(item.id || '');
|
||||
$('#ageLabel').val(item.age_label || '');
|
||||
$('#ageValue').val(item.age_value || 0);
|
||||
$('#ageUnit').val(item.age_unit || 'days');
|
||||
$('#ageOffsetDays').val(item.age_offset_days || 0);
|
||||
$('#sortOrder').val(item.sort_order || 0);
|
||||
$('#vaccineName').val(item.vaccine_name || '');
|
||||
$('#doseNumber').val(item.dose_number || '');
|
||||
$('#seriesDoses').val(item.series_doses || '');
|
||||
$('#routeName').val(item.route_name || '');
|
||||
$('#targetDiseaseName').val(item.target_disease_name || '');
|
||||
$('#siteName').val(item.site_name || '');
|
||||
$('#isActive').val(String(item.is_active || 0));
|
||||
$('#scheduleNotes').val(item.notes || '');
|
||||
$('#uipMasterStatus').empty();
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('uipEditModal')).show();
|
||||
}
|
||||
|
||||
function saveSchedule() {
|
||||
var scheduleId = parseInt($('#scheduleId').val(), 10) || 0;
|
||||
if (scheduleId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = csrfPayload();
|
||||
payload.age_label = $('#ageLabel').val();
|
||||
payload.age_value = $('#ageValue').val();
|
||||
payload.age_unit = $('#ageUnit').val();
|
||||
payload.age_offset_days = $('#ageOffsetDays').val();
|
||||
payload.sort_order = $('#sortOrder').val();
|
||||
payload.vaccine_name = $('#vaccineName').val();
|
||||
payload.dose_number = $('#doseNumber').val();
|
||||
payload.series_doses = $('#seriesDoses').val();
|
||||
payload.route_name = $('#routeName').val();
|
||||
payload.target_disease_name = $('#targetDiseaseName').val();
|
||||
payload.site_name = $('#siteName').val();
|
||||
payload.is_active = $('#isActive').val();
|
||||
payload.notes = $('#scheduleNotes').val();
|
||||
|
||||
$.ajax({
|
||||
url: '<?= site_url('Immunization/update_schedule') ?>/' + scheduleId,
|
||||
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;
|
||||
}
|
||||
showStatus('success', 'Schedule updated. Reloading master...');
|
||||
window.setTimeout(function () {
|
||||
load_form('<?= site_url('Immunization/schedule_master') ?>', 'UIP Schedule Master');
|
||||
}, 450);
|
||||
}).fail(function (xhr) {
|
||||
updateCsrf(xhr.responseJSON || {});
|
||||
var json = xhr.responseJSON || {};
|
||||
showStatus('warning', json.error_text || json.message || xhr.statusText || 'Save failed');
|
||||
});
|
||||
}
|
||||
|
||||
$('#uipScheduleMasterTable').on('click', '.btnEditSchedule', function () {
|
||||
var raw = $(this).attr('data-item') || '{}';
|
||||
try {
|
||||
fillModal(JSON.parse(raw));
|
||||
} catch (error) {
|
||||
showStatus('warning', 'Unable to open schedule record.');
|
||||
}
|
||||
});
|
||||
$('#btnSaveSchedule').on('click', saveSchedule);
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -379,6 +379,12 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
|
|||
<span>Document Issue </span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Immunization') ?>','Immunization Record')">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
<span>Immunization</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($canPharmacy) { ?>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user