devsofttech_hms_ci4/app/Controllers/IpdPatient.php
DevSoftTech Server a3c544a53c feat(user-management): enhance user management, permission handling, and charges invoice workflows
- Add UserProfileModel and migration for user profile data
- Enhance User Management views, controller, and doctor/nurse search integration
- Refactor handleAuthError to separate 401 session expiry from 403 access restriction
- Apply granular permission checks to sidebar navigation links
- Open Bed Status view for all authenticated users
- Support GET and POST on charges item routes and exempt billing charges from CSRF desync
2026-10-14 20:08:10 +05:30

1100 lines
45 KiB
PHP

<?php
namespace App\Controllers;
use App\Libraries\NursingScanExtractionService;
use App\Models\BedAssignmentHistoryModel;
use App\Models\BedMasterModel;
use App\Models\IpdBillingModel;
use App\Models\IpdModel;
use App\Models\IpdNursingEntryModel;
use App\Models\NursingBedsideItemModel;
class IpdPatient extends BaseController
{
protected IpdBillingModel $ipdBillingModel;
protected IpdNursingEntryModel $ipdNursingEntryModel;
protected NursingBedsideItemModel $nursingBedsideItemModel;
protected NursingScanExtractionService $nursingScanExtractionService;
protected BedMasterModel $bedMasterModel;
protected BedAssignmentHistoryModel $bedAssignmentModel;
protected IpdModel $ipdModel;
public function __construct()
{
$this->ipdBillingModel = new IpdBillingModel();
$this->ipdNursingEntryModel = new IpdNursingEntryModel();
$this->nursingBedsideItemModel = new NursingBedsideItemModel();
$this->nursingScanExtractionService = new NursingScanExtractionService();
$this->bedMasterModel = new BedMasterModel();
$this->bedAssignmentModel = new BedAssignmentHistoryModel();
$this->ipdModel = new IpdModel();
helper(['common', 'form', 'age']);
}
public function index()
{
$permission = $this->requireAnyPermission([
'ipd_nursing.view',
'billing.access',
'billing.ipd.current-admission',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
$records = $this->ipdBillingModel->getCurrentAdmissions();
$stats = [
'total_beds' => 0,
'available' => 0,
'occupied' => 0,
'reserved' => 0,
'maintenance' => 0,
'cleaning' => 0,
'total_admitted' => count($records),
'nursing_stations' => 0,
'count_ipd' => 0,
'count_daycare' => 0,
'count_emergency' => 0,
];
foreach ($records as $rec) {
$admType = strtolower(trim((string) ($rec->admission_type ?? 'ipd')));
if ($admType === 'emergency') {
$stats['count_emergency']++;
} elseif ($admType === 'daycare') {
$stats['count_daycare']++;
} else {
$stats['count_ipd']++;
}
}
$floors = [];
$wards = [];
$stationModel = new \App\Models\NursingStationModel();
$nursingStations = $stationModel->getActiveStations();
$stats['nursing_stations'] = count($nursingStations);
if ($this->db->tableExists('bed_master') && $this->db->tableExists('ward_master')) {
$builder = $this->db->table('bed_master b')
->select('b.id as bed_id, b.bed_number, b.bed_code, b.bed_status, b.current_ipd_id')
->select('b.has_oxygen, b.has_monitor, b.has_ventilator, b.is_isolation_bed')
->select('w.id as ward_id, w.ward_name, w.floor_number, w.ward_type, w.nursing_station_id')
->join('ward_master w', 'w.id = b.ward_id', 'left')
->where('b.status', 'active')
->orderBy('w.floor_number', 'ASC')
->orderBy('w.ward_name', 'ASC')
->orderBy('b.bed_number', 'ASC');
if ($this->db->tableExists('nursing_station_master')) {
$builder->select('ns.station_name, ns.station_code')
->join('nursing_station_master ns', 'ns.id = w.nursing_station_id', 'left');
} else {
$builder->select('NULL as station_name, NULL as station_code');
}
if ($this->db->tableExists('ipd_master')) {
$builder
->select('i.id as ipd_id, i.ipd_code, i.register_date, i.r_doc_name, i.admission_type')
->join('ipd_master i', 'i.id = b.current_ipd_id AND i.ipd_status = 0', 'left', false);
if ($this->db->tableExists('patient_master')) {
$builder
->select('p.p_code, p.p_fname')
->join('patient_master p', 'p.id = i.p_id', 'left');
}
}
$today = new \DateTime();
foreach ($builder->get()->getResult() as $row) {
$floorNo = (string) ($row->floor_number !== null && $row->floor_number !== '' ? $row->floor_number : 'Ground Floor');
$wardId = (int) ($row->ward_id ?? 0);
$status = (string) ($row->bed_status ?? 'available');
$stats['total_beds']++;
if (array_key_exists($status, $stats)) {
$stats[$status]++;
} elseif (strpos($status, 'maintenance') !== false) {
$stats['maintenance']++;
}
$daysAdmitted = null;
if (! empty($row->register_date)) {
try {
$admitDate = new \DateTime((string) $row->register_date);
$daysAdmitted = (int) $today->diff($admitDate)->days;
} catch (\Throwable $e) {
$daysAdmitted = null;
}
}
$patientName = trim((string) ($row->p_fname ?? ''));
if (! isset($floors[$floorNo])) {
$floors[$floorNo] = [];
}
if (! isset($floors[$floorNo][$wardId])) {
$floors[$floorNo][$wardId] = [
'ward_name' => (string) ($row->ward_name ?? 'Unknown Ward'),
'ward_type' => (string) ($row->ward_type ?? ''),
'station_name' => (string) ($row->station_name ?? 'Unassigned Station'),
'station_code' => (string) ($row->station_code ?? ''),
'beds' => [],
'count' => ['total' => 0, 'available' => 0, 'occupied' => 0],
];
$wards[$wardId] = (string) ($row->ward_name ?? 'Unknown Ward');
}
$floors[$floorNo][$wardId]['beds'][] = [
'bed_id' => (int) ($row->bed_id ?? 0),
'bed_number' => (string) ($row->bed_number !== '' && $row->bed_number !== null
? $row->bed_number : ($row->bed_code ?? '-')),
'bed_code' => (string) ($row->bed_code ?? ''),
'bed_status' => $status,
'patient_name' => $patientName,
'patient_code' => (string) ($row->p_code ?? ''),
'doctor_name' => trim((string) ($row->r_doc_name ?? '')),
'ipd_code' => (string) ($row->ipd_code ?? ''),
'ipd_id' => (int) ($row->ipd_id ?? 0),
'admission_type' => strtolower(trim((string) ($row->admission_type ?? 'ipd'))),
'admit_date' => (string) ($row->register_date ?? ''),
'days_admitted' => $daysAdmitted,
'has_oxygen' => (bool) ($row->has_oxygen ?? false),
'has_monitor' => (bool) ($row->has_monitor ?? false),
'has_ventilator' => (bool) ($row->has_ventilator ?? false),
'is_isolation' => (bool) ($row->is_isolation_bed ?? false),
'station_name' => (string) ($row->station_name ?? ''),
];
$floors[$floorNo][$wardId]['count']['total']++;
if ($status === 'available') {
$floors[$floorNo][$wardId]['count']['available']++;
} elseif ($status === 'occupied') {
$floors[$floorNo][$wardId]['count']['occupied']++;
}
}
}
return view('ipd_nursing/patient_list', [
'records' => $records,
'stats' => $stats,
'floors' => $floors,
'wards' => $wards,
'nursingStations' => $nursingStations,
]);
}
public function workspace(int $ipdId)
{
$permission = $this->requireAnyPermission([
'ipd_nursing.view',
'billing.access',
'billing.ipd.current-admission',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
$panelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
if (empty($panelData)) {
return $this->response->setStatusCode(404)->setBody('IPD not found');
}
$caseMeta = $this->ipdBillingModel->getIpdCaseMeta($ipdId);
$insCompId = (int) ($caseMeta['insurance_id'] ?? 0);
if ($insCompId <= 0) {
$insCompId = 1;
}
$docList = $this->ipdBillingModel->getIpdDoctorList();
$doctorIds = array_map(static fn ($doc) => (int) ($doc->id ?? 0), $docList);
$doctorIds = array_values(array_filter($doctorIds, static fn ($id) => $id > 0));
$person = $panelData['person_info'] ?? null;
$patientId = max(0, (int) ($person->id ?? 0));
if ($patientId <= 0) {
$patientId = $this->getPatientIdFromIpd($ipdId);
}
$historyFields = [
'is_smoking' => 'Smoking',
'is_alcohol' => 'Alcohol',
'is_tobacoo' => 'Tobacco',
'is_drug_abuse' => 'Drug abuse',
'is_hypertesion' => 'Hypertension',
'is_niddm' => 'Type 2 diabetes mellitus (DM)',
'is_hbsag' => 'HBsAg',
'is_hcv' => 'HCV',
'is_hiv_I_II' => 'HIV I & II',
];
$patientHistoryRow = $this->getPatientHistoryFlags($patientId, array_keys($historyFields));
$opdHistorySnapshot = $this->getLatestOpdHistorySnapshot($patientId);
$isFemalePatient = strtolower((string) ($person->xgender ?? '')) === 'female';
// Get current bed assignment with ward info
$currentBed = $this->bedMasterModel
->select('bed_master.*, ward_master.ward_name')
->join('ward_master', 'ward_master.id = bed_master.ward_id', 'left')
->where('bed_master.current_ipd_id', $ipdId)
->first();
// Get available beds for transfer
$availableBeds = $this->bedMasterModel->getAvailableBedsGroupedByWard();
// Get bed assignment history
$bedHistory = $this->bedAssignmentModel->getByIpd($ipdId);
$nurseModel = new \App\Models\NurseModel();
$nurseList = $nurseModel->getActiveNurses();
$defaultNurseId = 0;
$user = function_exists('auth') ? auth()->user() : null;
if ($user && ! empty($user->id)) {
$profile = (new \App\Models\UserProfileModel())->getProfile((int) $user->id);
if (! empty($profile['nurse_id'])) {
$defaultNurseId = (int) $profile['nurse_id'];
}
}
return view('ipd_nursing/workspace', [
'ipd_info' => $panelData['ipd_info'] ?? null,
'person_info' => $person,
'nursing_entries' => $this->ipdNursingEntryModel->getByIpd($ipdId),
'nursing_charge_history' => $this->ipdBillingModel->getNursingChargeHistory($ipdId),
'bedside_items_by_category' => $this->nursingBedsideItemModel->getBillableGroupedByCategory($insCompId),
'doctor_visit_fee_types' => $this->ipdBillingModel->getDoctorVisitFeeTypes(),
'doctor_visit_fee_map' => $this->ipdBillingModel->getDoctorVisitFeeMap($doctorIds),
'doc_list' => $docList,
'nurse_list' => $nurseList,
'default_nurse_id' => $defaultNurseId,
'history_fields' => $historyFields,
'patient_history_row' => $patientHistoryRow,
'opd_history_snapshot' => $opdHistorySnapshot,
'is_female_patient' => $isFemalePatient,
'current_bed' => $currentBed,
'available_beds' => $availableBeds,
'bed_history' => $bedHistory,
]);
}
public function saveAdmissionHistory(int $ipdId)
{
$permission = $this->requireAnyPermission([
'ipd_nursing.record.manage',
'billing.access',
'billing.ipd.current-admission',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
}
$patientId = $this->getPatientIdFromIpd($ipdId);
if ($patientId <= 0) {
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Patient not found']);
}
$historyFieldKeys = [
'is_smoking',
'is_alcohol',
'is_tobacoo',
'is_drug_abuse',
'is_hypertesion',
'is_niddm',
'is_hbsag',
'is_hcv',
'is_hiv_I_II',
];
$saved = false;
if ($this->db->tableExists('patient_master')) {
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
if (in_array('id', $patientFields, true)) {
$historyUpdate = [];
foreach ($historyFieldKeys as $field) {
if (in_array($field, $patientFields, true)) {
$historyUpdate[$field] = (int) ($this->request->getPost($field) ?? 0) === 1 ? 1 : 0;
}
}
if (! empty($historyUpdate)) {
$saved = (bool) $this->db->table('patient_master')
->where('id', $patientId)
->update($historyUpdate) || $saved;
}
}
}
$opdPayload = [
'drug_allergy_status' => trim((string) ($this->request->getPost('drug_allergy_status') ?? '')),
'drug_allergy_details' => trim((string) ($this->request->getPost('drug_allergy_details') ?? '')),
'adr_history' => trim((string) ($this->request->getPost('adr_history') ?? '')),
'current_medications' => trim((string) ($this->request->getPost('current_medications') ?? '')),
'co_morbidities' => trim((string) ($this->request->getPost('co_morbidities') ?? '')),
'women_lmp' => trim((string) ($this->request->getPost('women_lmp') ?? '')),
'women_last_baby' => trim((string) ($this->request->getPost('women_last_baby') ?? '')),
'women_pregnancy_related' => trim((string) ($this->request->getPost('women_pregnancy_related') ?? '')),
'women_related_problems' => trim((string) ($this->request->getPost('women_related_problems') ?? '')),
'hpi_note' => trim((string) ($this->request->getPost('hpi_note') ?? '')),
];
if ($this->db->tableExists('opd_prescription')) {
$rxFields = $this->db->getFieldNames('opd_prescription') ?? [];
if (in_array('p_id', $rxFields, true) && in_array('id', $rxFields, true)) {
$latest = $this->db->table('opd_prescription')
->select('id,Prescriber_Remarks')
->where('p_id', $patientId)
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (is_array($latest) && ! empty($latest['id'])) {
$update = [];
foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history', 'current_medications', 'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems'] as $field) {
if (in_array($field, $rxFields, true)) {
$update[$field] = (string) ($opdPayload[$field] ?? '');
}
}
if (in_array('Prescriber_Remarks', $rxFields, true)) {
$remarks = (string) ($latest['Prescriber_Remarks'] ?? '');
$remarks = $this->upsertLabeledLineInRemarks($remarks, 'Drug Allergy Status', (string) ($opdPayload['drug_allergy_status'] ?? ''));
$remarks = $this->upsertLabeledLineInRemarks($remarks, 'Drug Allergy Details', (string) ($opdPayload['drug_allergy_details'] ?? ''));
$remarks = $this->upsertLabeledLineInRemarks($remarks, 'ADR History', (string) ($opdPayload['adr_history'] ?? ''));
$remarks = $this->upsertLabeledLineInRemarks($remarks, 'Current Medications', (string) ($opdPayload['current_medications'] ?? ''));
$remarks = $this->upsertLabeledLineInRemarks($remarks, 'Co-Morbidities', (string) ($opdPayload['co_morbidities'] ?? ''));
$remarks = $this->upsertLabeledLineInRemarks($remarks, 'HPI Note', (string) ($opdPayload['hpi_note'] ?? ''));
$update['Prescriber_Remarks'] = $remarks;
}
if (! empty($update)) {
$saved = (bool) $this->db->table('opd_prescription')
->where('id', (int) $latest['id'])
->update($update) || $saved;
}
}
}
}
return $this->response->setJSON([
'status' => $saved ? 1 : 0,
'message' => $saved ? 'Admission history updated' : 'No data updated',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function saveNursingEntry(int $ipdId)
{
$permission = $this->requireAnyPermission([
'ipd_nursing.record.manage',
'billing.access',
'billing.ipd.current-admission',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
}
$entryType = (string) ($this->request->getPost('entry_type') ?? '');
if (! in_array($entryType, ['vitals', 'fluid', 'treatment', 'admission'], true)) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Invalid nursing entry type']);
}
$recordedAtInput = (string) ($this->request->getPost('recorded_at') ?? '');
$recordedAt = $recordedAtInput !== ''
? str_replace('T', ' ', $recordedAtInput) . (strlen($recordedAtInput) === 16 ? ':00' : '')
: date('Y-m-d H:i:s');
$user = auth()->user();
$recordedBy = $user->username ?? $user->email ?? 'User';
$recordedById = $user->id ?? null;
$nurseIdPost = (int) ($this->request->getPost('recorded_by_nurse_id') ?? 0);
if ($nurseIdPost <= 0 && $user && ! empty($user->id)) {
$profile = (new \App\Models\UserProfileModel())->getProfile((int) $user->id);
if (! empty($profile['nurse_id'])) {
$nurseIdPost = (int) $profile['nurse_id'];
}
}
if ($nurseIdPost > 0) {
$nurseModel = new \App\Models\NurseModel();
$nurseRow = $nurseModel->getNurseById($nurseIdPost);
if ($nurseRow) {
$recordedBy = ($nurseRow['nurse_code'] ? '[' . $nurseRow['nurse_code'] . '] ' : '') . $nurseRow['name'];
$recordedById = $nurseRow['id'];
}
}
$data = [
'ipd_id' => $ipdId,
'entry_type' => $entryType,
'recorded_at' => $recordedAt,
'shift_name' => '',
'temperature_c' => $this->resolveTemperatureCFromPost(),
'pulse_rate' => $this->request->getPost('pulse_rate') !== null && $this->request->getPost('pulse_rate') !== '' ? (int) $this->request->getPost('pulse_rate') : null,
'resp_rate' => $this->request->getPost('resp_rate') !== null && $this->request->getPost('resp_rate') !== '' ? (int) $this->request->getPost('resp_rate') : null,
'bp_systolic' => $this->request->getPost('bp_systolic') !== null && $this->request->getPost('bp_systolic') !== '' ? (int) $this->request->getPost('bp_systolic') : null,
'bp_diastolic' => $this->request->getPost('bp_diastolic') !== null && $this->request->getPost('bp_diastolic') !== '' ? (int) $this->request->getPost('bp_diastolic') : null,
'spo2' => $this->request->getPost('spo2') !== null && $this->request->getPost('spo2') !== '' ? (int) $this->request->getPost('spo2') : null,
'weight_kg' => $this->request->getPost('weight_kg') !== null && $this->request->getPost('weight_kg') !== '' ? (float) $this->request->getPost('weight_kg') : null,
'fluid_direction' => (string) ($this->request->getPost('fluid_direction') ?? ''),
'fluid_route' => (string) ($this->request->getPost('fluid_route') ?? ''),
'fluid_amount_ml' => $this->request->getPost('fluid_amount_ml') !== null && $this->request->getPost('fluid_amount_ml') !== '' ? (int) $this->request->getPost('fluid_amount_ml') : null,
'treatment_text' => (string) ($this->request->getPost('treatment_text') ?? ''),
'general_note' => (string) ($this->request->getPost('general_note') ?? ''),
'recorded_by' => $recordedBy,
'recorded_by_id' => $recordedById,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
];
if ($entryType === 'vitals') {
$hasVitals = $data['temperature_c'] !== null
|| $data['pulse_rate'] !== null
|| $data['resp_rate'] !== null
|| $data['bp_systolic'] !== null
|| $data['bp_diastolic'] !== null
|| $data['spo2'] !== null
|| $data['weight_kg'] !== null;
if (! $hasVitals) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Enter at least one vital value']);
}
}
if ($entryType === 'fluid' && $data['fluid_amount_ml'] === null) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Fluid amount is required']);
}
if ($entryType === 'treatment' && trim((string) $data['treatment_text']) === '') {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Treatment note is required']);
}
if ($entryType === 'admission') {
$hasVitals = $data['temperature_c'] !== null
|| $data['pulse_rate'] !== null
|| $data['resp_rate'] !== null
|| $data['bp_systolic'] !== null
|| $data['bp_diastolic'] !== null
|| $data['spo2'] !== null
|| $data['weight_kg'] !== null;
$hasComplaint = trim((string) $data['treatment_text']) !== '';
if (! $hasVitals && ! $hasComplaint) {
return $this->response->setStatusCode(422)->setJSON([
'status' => 0,
'message' => 'Enter admission complaints or at least one vital value',
]);
}
}
$entryId = (int) ($this->request->getPost('entry_id') ?? 0);
if ($entryId > 0) {
unset($data['created_at']);
$this->ipdNursingEntryModel->update($entryId, $data);
$msg = 'Nursing entry updated';
} else {
$this->ipdNursingEntryModel->insert($data);
$msg = 'Nursing entry saved';
}
return $this->response->setJSON([
'status' => 1,
'message' => $msg,
]);
}
public function scanNursingPaper(int $ipdId)
{
$permission = $this->requireAnyPermission([
'ipd_nursing.record.manage',
'billing.access',
'billing.ipd.current-admission',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
}
$docType = strtolower(trim((string) ($this->request->getPost('document_type') ?? 'auto')));
if (! in_array($docType, ['auto', 'vitals', 'fluid', 'treatment'], true)) {
$docType = 'auto';
}
$manualText = trim((string) ($this->request->getPost('ocr_text') ?? ''));
$file = $this->request->getFile('scan_file');
$uploadedMeta = null;
if ($file && $file->isValid() && ! $file->hasMoved()) {
$allowed = ['pdf', 'png', 'jpg', 'jpeg', 'webp', 'txt'];
$ext = strtolower((string) $file->getExtension());
if (! in_array($ext, $allowed, true)) {
return $this->response->setStatusCode(422)->setJSON([
'status' => 0,
'message' => 'Only PDF, image, or text files are allowed',
]);
}
$scanRef = 'NSCAN-' . date('YmdHis') . '-' . substr(md5((string) mt_rand()), 0, 6);
$targetDir = WRITEPATH . 'uploads/nursing_scans/' . $ipdId;
if (! is_dir($targetDir)) {
@mkdir($targetDir, 0775, true);
}
$storedName = $scanRef . '.' . $ext;
$file->move($targetDir, $storedName, true);
$uploadedMeta = [
'scan_ref' => $scanRef,
'path' => rtrim($targetDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $storedName,
'mime' => (string) $file->getClientMimeType(),
];
}
if ($manualText === '' && $uploadedMeta === null) {
return $this->response->setStatusCode(422)->setJSON([
'status' => 0,
'message' => 'Upload a scan file or paste OCR text',
]);
}
$parsed = $this->nursingScanExtractionService->extract($docType, $manualText, $uploadedMeta);
$rows = is_array($parsed['rows'] ?? null) ? $parsed['rows'] : [];
return $this->response->setJSON([
'status' => 1,
'message' => empty($rows)
? 'Scan processed. No structured rows detected; please review and edit manually.'
: 'Scan processed. Review extracted rows before saving.',
'scan_ref' => (string) ($parsed['scan_ref'] ?? ''),
'document_type' => $docType,
'rows' => $rows,
'warnings' => $parsed['warnings'] ?? [],
'nabh_requirements' => [
'Each row must have recorded date/time.',
'Each row must include nursing note or measurable values.',
'Vitals rows require at least one vital parameter.',
'Fluid rows require direction and amount in ml.',
'All AI/OCR rows must be nurse-reviewed before save.',
],
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function saveScannedNursingEntries(int $ipdId)
{
$permission = $this->requireAnyPermission([
'ipd_nursing.record.manage',
'billing.access',
'billing.ipd.current-admission',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
}
$rowsJson = (string) ($this->request->getPost('rows_json') ?? '[]');
$rows = json_decode($rowsJson, true);
if (! is_array($rows) || empty($rows)) {
return $this->response->setStatusCode(422)->setJSON([
'status' => 0,
'message' => 'No reviewed rows to save',
]);
}
$scanRef = trim((string) ($this->request->getPost('scan_ref') ?? ''));
$user = auth()->user();
$recordedBy = $user->username ?? $user->email ?? 'User';
$recordedById = $user->id ?? null;
$savedCount = 0;
$errors = [];
foreach ($rows as $idx => $row) {
if (! is_array($row)) {
$errors[] = 'Row ' . ($idx + 1) . ': Invalid payload.';
continue;
}
$entryType = strtolower(trim((string) ($row['entry_type'] ?? '')));
if (! in_array($entryType, ['vitals', 'fluid', 'treatment'], true)) {
$errors[] = 'Row ' . ($idx + 1) . ': Unsupported entry type.';
continue;
}
$recordedAt = $this->normalizeScannedRecordedAt((string) ($row['recorded_at'] ?? ''));
if ($recordedAt === '') {
$errors[] = 'Row ' . ($idx + 1) . ': Recorded At is required.';
continue;
}
$temperatureF = $this->toNullableFloat($row['temperature_f'] ?? null);
$temperatureC = $temperatureF !== null ? round(($temperatureF - 32) * 5 / 9, 2) : null;
$pulse = $this->toNullableInt($row['pulse_rate'] ?? null);
$resp = $this->toNullableInt($row['resp_rate'] ?? null);
$sbp = $this->toNullableInt($row['bp_systolic'] ?? null);
$dbp = $this->toNullableInt($row['bp_diastolic'] ?? null);
$spo2 = $this->toNullableInt($row['spo2'] ?? null);
$weight = $this->toNullableFloat($row['weight_kg'] ?? null);
$fluidAmount = $this->toNullableInt($row['fluid_amount_ml'] ?? null);
$fluidDirection = strtolower(trim((string) ($row['fluid_direction'] ?? '')));
$fluidRoute = trim((string) ($row['fluid_route'] ?? ''));
$treatmentText = trim((string) ($row['treatment_text'] ?? ''));
$generalNote = trim((string) ($row['general_note'] ?? ''));
$confidence = $this->toNullableFloat($row['confidence'] ?? null);
$isCorrected = (int) ($row['is_corrected'] ?? 0) === 1;
$sourceParts = ['Source: Scanned Paper'];
if ($scanRef !== '') {
$sourceParts[] = 'ScanRef: ' . $scanRef;
}
$sourceParts[] = 'ReviewedBy: ' . $recordedBy;
$sourceParts[] = 'ReviewedAt: ' . date('Y-m-d H:i:s');
if ($confidence !== null) {
$sourceParts[] = 'AIConfidence: ' . max(0, min(100, (int) round($confidence * 100))) . '%';
}
$sourceParts[] = 'Corrected: ' . ($isCorrected ? 'Yes' : 'No');
$sourceTag = ' [' . implode(' | ', $sourceParts) . ']';
$generalNote = trim($generalNote . $sourceTag);
if ($entryType === 'vitals') {
$hasVitals = $temperatureC !== null || $pulse !== null || $resp !== null || $sbp !== null || $dbp !== null || $spo2 !== null || $weight !== null;
if (! $hasVitals) {
$errors[] = 'Row ' . ($idx + 1) . ': Vitals row requires at least one vital parameter.';
continue;
}
}
if ($entryType === 'fluid') {
if (! in_array($fluidDirection, ['intake', 'output'], true)) {
$errors[] = 'Row ' . ($idx + 1) . ': Fluid direction must be intake/output.';
continue;
}
if ($fluidAmount === null || $fluidAmount <= 0) {
$errors[] = 'Row ' . ($idx + 1) . ': Fluid amount is required.';
continue;
}
}
if ($entryType === 'treatment' && $treatmentText === '' && $generalNote === $sourceTag) {
$errors[] = 'Row ' . ($idx + 1) . ': Treatment text or note is required.';
continue;
}
$insert = [
'ipd_id' => $ipdId,
'entry_type' => $entryType,
'recorded_at' => $recordedAt,
'shift_name' => $this->resolveShiftName($recordedAt),
'temperature_c' => $temperatureC,
'pulse_rate' => $pulse,
'resp_rate' => $resp,
'bp_systolic' => $sbp,
'bp_diastolic' => $dbp,
'spo2' => $spo2,
'weight_kg' => $weight,
'fluid_direction' => $entryType === 'fluid' ? $fluidDirection : '',
'fluid_route' => $entryType === 'fluid' ? $fluidRoute : '',
'fluid_amount_ml' => $entryType === 'fluid' ? $fluidAmount : null,
'treatment_text' => $entryType === 'treatment' ? $treatmentText : '',
'general_note' => $generalNote,
'recorded_by' => $recordedBy,
'recorded_by_id' => $recordedById,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
];
$this->ipdNursingEntryModel->insert($insert);
$savedCount++;
}
return $this->response->setJSON([
'status' => $savedCount > 0 ? 1 : 0,
'message' => $savedCount > 0
? ('Saved ' . $savedCount . ' scanned nursing row(s).')
: 'No rows saved. Please fix validation errors.',
'saved_count' => $savedCount,
'errors' => $errors,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function nursingChartPrint(int $ipdId)
{
$permission = $this->requireAnyPermission([
'ipd_nursing.view',
'billing.access',
'billing.ipd.current-admission',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
$panelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
if (empty($panelData)) {
return $this->response->setStatusCode(404)->setBody('IPD not found');
}
$chartDate = (string) ($this->request->getGet('date') ?? date('Y-m-d'));
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $chartDate)) {
$chartDate = date('Y-m-d');
}
$nurse = trim((string) ($this->request->getGet('nurse') ?? ''));
$startDateTime = $chartDate . ' 00:00:00';
$endDateTime = $chartDate . ' 23:59:59';
$entries = $this->ipdNursingEntryModel->getByIpdPeriod($ipdId, $startDateTime, $endDateTime, $nurse);
return view('billing/ipd/nursing_chart_print', [
'ipd_info' => $panelData['ipd_info'] ?? null,
'person_info' => $panelData['person_info'] ?? null,
'nursing_entries' => $entries,
'chart_date' => $chartDate,
'chart_nurse' => $nurse,
]);
}
private function requireAnyPermission(array $permissions)
{
if (! function_exists('auth')) {
return null;
}
$user = auth()->user();
if (! $user || ! method_exists($user, 'can')) {
return $this->response->setStatusCode(403)->setBody('Access denied');
}
foreach ($permissions as $permission) {
if ($user->can($permission)) {
return null;
}
}
return $this->response->setStatusCode(403)->setBody('Access denied');
}
private function resolveShiftName(string $recordedAt): string
{
$timePart = substr($recordedAt, 11, 5);
if (! preg_match('/^\d{2}:\d{2}$/', $timePart)) {
return 'Morning';
}
[$hour, $minute] = array_map('intval', explode(':', $timePart));
$minutesFromMidnight = ($hour * 60) + $minute;
if ($minutesFromMidnight >= 360 && $minutesFromMidnight < 840) {
return 'Morning';
}
if ($minutesFromMidnight >= 840 && $minutesFromMidnight < 1320) {
return 'Evening';
}
return 'Night';
}
private function resolveTemperatureCFromPost(): ?float
{
$temperatureF = $this->request->getPost('temperature_f');
if ($temperatureF !== null && $temperatureF !== '') {
return round((((float) $temperatureF) - 32) * 5 / 9, 2);
}
$temperatureC = $this->request->getPost('temperature_c');
if ($temperatureC !== null && $temperatureC !== '') {
return (float) $temperatureC;
}
return null;
}
private function normalizeScannedRecordedAt(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
$candidate = str_replace('T', ' ', $value);
if (preg_match('/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}(:\d{2})?$/', $candidate) === 1) {
return strlen($candidate) === 16 ? $candidate . ':00' : $candidate;
}
$ts = strtotime($value);
if ($ts === false) {
return '';
}
return date('Y-m-d H:i:s', $ts);
}
private function toNullableInt($value): ?int
{
if ($value === null || $value === '') {
return null;
}
if (! is_numeric($value)) {
return null;
}
return (int) $value;
}
private function toNullableFloat($value): ?float
{
if ($value === null || $value === '') {
return null;
}
if (! is_numeric($value)) {
return null;
}
return (float) $value;
}
private function getPatientIdFromIpd(int $ipdId): int
{
if ($ipdId <= 0 || ! $this->db->tableExists('ipd_master')) {
return 0;
}
$fields = $this->db->getFieldNames('ipd_master') ?? [];
if (! in_array('id', $fields, true) || ! in_array('p_id', $fields, true)) {
return 0;
}
$row = $this->db->table('ipd_master')
->select('p_id')
->where('id', $ipdId)
->get(1)
->getRowArray();
return max(0, (int) ($row['p_id'] ?? 0));
}
private function getPatientHistoryFlags(int $patientId, array $keys): array
{
$result = [];
foreach ($keys as $key) {
$result[$key] = 0;
}
if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) {
return $result;
}
$fields = $this->db->getFieldNames('patient_master') ?? [];
if (! in_array('id', $fields, true)) {
return $result;
}
$select = ['id'];
foreach ($keys as $key) {
if (in_array($key, $fields, true)) {
$select[] = $key;
}
}
$row = $this->db->table('patient_master')
->select(implode(',', $select))
->where('id', $patientId)
->get(1)
->getRowArray() ?? [];
foreach ($keys as $key) {
if (isset($row[$key])) {
$result[$key] = (int) $row[$key] === 1 ? 1 : 0;
}
}
return $result;
}
private function extractLabeledField(string $remarks, string $label): string
{
if (preg_match('/^\s*' . preg_quote($label, '/') . '\s*:\s*(.+)$/im', $remarks, $m) !== 1) {
return '';
}
return trim((string) ($m[1] ?? ''));
}
private function getLatestOpdHistorySnapshot(int $patientId): array
{
$empty = [
'drug_allergy_status' => '',
'drug_allergy_details' => '',
'adr_history' => '',
'current_medications' => '',
'co_morbidities' => '',
'women_lmp' => '',
'women_last_baby' => '',
'women_pregnancy_related' => '',
'women_related_problems' => '',
'hpi_note' => '',
];
if ($patientId <= 0 || ! $this->db->tableExists('opd_prescription')) {
return $empty;
}
$fields = $this->db->getFieldNames('opd_prescription') ?? [];
if (! in_array('p_id', $fields, true)) {
return $empty;
}
$row = $this->db->table('opd_prescription')
->where('p_id', $patientId)
->orderBy('id', 'DESC')
->get(1)
->getRowArray() ?? [];
if (empty($row)) {
return $empty;
}
$remarks = trim((string) ($row['Prescriber_Remarks'] ?? ''));
foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history', 'current_medications', 'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems'] as $field) {
$value = trim((string) ($row[$field] ?? ''));
if ($value !== '') {
$empty[$field] = $value;
}
}
if ($empty['co_morbidities'] === '') {
$empty['co_morbidities'] = $this->extractLabeledField($remarks, 'Co-Morbidities');
}
if ($empty['hpi_note'] === '') {
$empty['hpi_note'] = $this->extractLabeledField($remarks, 'HPI Note');
}
if ($empty['drug_allergy_status'] === '') {
$empty['drug_allergy_status'] = $this->extractLabeledField($remarks, 'Drug Allergy Status');
}
if ($empty['drug_allergy_details'] === '') {
$empty['drug_allergy_details'] = $this->extractLabeledField($remarks, 'Drug Allergy Details');
}
if ($empty['adr_history'] === '') {
$empty['adr_history'] = $this->extractLabeledField($remarks, 'ADR History');
}
if ($empty['current_medications'] === '') {
$empty['current_medications'] = $this->extractLabeledField($remarks, 'Current Medications');
}
return $empty;
}
private function upsertLabeledLineInRemarks(string $remarks, string $label, string $value): string
{
$remarks = trim($remarks);
$pattern = '/^\s*' . preg_quote($label, '/') . '\s*:\s*.*$/im';
$remarks = preg_replace($pattern, '', $remarks) ?? $remarks;
$lines = array_filter(array_map(static function (string $line): string {
return trim($line);
}, preg_split('/\R/', $remarks) ?: []), static function (string $line): bool {
return $line !== '';
});
$value = trim($value);
if ($value !== '') {
$lines[] = $label . ': ' . $value;
}
return trim(implode(PHP_EOL, $lines));
}
public function bedTransfer(int $ipdId)
{
$permission = $this->requireAnyPermission([
'ipd_nursing.bed.transfer',
'billing.access',
'billing.ipd.current-admission',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
}
$newBedId = (int) $this->request->getPost('bed_id');
$transferReason = trim((string) $this->request->getPost('transfer_reason'));
$remarks = trim((string) $this->request->getPost('remarks'));
$assignmentDateTime = trim((string) $this->request->getPost('assignment_datetime'));
if ($newBedId <= 0) {
return $this->response->setJSON(['status' => 0, 'message' => 'Please select a bed']);
}
// Validate and format assignment date/time
if (empty($assignmentDateTime)) {
$assignmentDateTime = date('Y-m-d H:i:s');
} else {
// Convert from HTML5 datetime-local format to MySQL datetime
$assignmentDateTime = date('Y-m-d H:i:s', strtotime($assignmentDateTime));
}
try {
// Get current bed assignment
$currentBed = $this->bedMasterModel->where('current_ipd_id', $ipdId)->first();
// Check if new bed is available
$newBed = $this->bedMasterModel->find($newBedId);
if (!$newBed || $newBed->bed_status !== 'available' || $newBed->current_ipd_id !== null) {
return $this->response->setJSON(['status' => 0, 'message' => 'Selected bed is not available']);
}
// Transfer bed
$this->ipdModel->bedAssign([
'ipd_id' => $ipdId,
'bed_id' => $newBedId,
'Fdate' => $assignmentDateTime,
'TDate' => $assignmentDateTime,
'transfer_reason' => $transferReason,
'transfer_from_bed_id' => $currentBed ? $currentBed->id : null,
'remarks' => $remarks,
'assignment_type' => 'transfer',
]);
return $this->response->setJSON([
'status' => 1,
'message' => 'Bed transferred successfully',
'reload' => true,
]);
} catch (\Throwable $e) {
log_message('error', 'Bed transfer failed for IPD #' . $ipdId . ': ' . $e->getMessage());
return $this->response->setJSON(['status' => 0, 'message' => 'Bed transfer failed: ' . $e->getMessage()]);
}
}
}