diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 499ff7d..44c9be2 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1556,3 +1556,22 @@ $routes->get('Document_Patient/index', 'DoctorDocument::index'); $routes->get('Document_Patient/workspace', 'DoctorDocument::workspace'); $routes->get('Document_Patient/health_document_fhir_preview/(:num)', 'DoctorDocument::health_document_fhir_preview/$1'); +/* + * -------------------------------------------------------------------- + * Dedicated PWA Apps & RESTful API Routes (Isolated Architecture) + * -------------------------------------------------------------------- + */ + +// Dedicated PWA Web App Endpoint (Serves React PWA without touching standard CI4 web views) +$routes->get('app/nursing', 'Api\v1\NursingApi::pwaIndex'); +$routes->get('app/nursing/(:any)', 'Api\v1\NursingApi::pwaIndex'); + +// Dedicated RESTful JSON API v1 Group for React PWAs & Mobile Apps +$routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static function ($routes) { + // Nursing PWA APIs + $routes->get('nursing/beds', 'NursingApi::beds'); + $routes->get('nursing/nurses', 'NursingApi::nurses'); + $routes->get('nursing/workspace/(:num)', 'NursingApi::workspace/$1'); + $routes->post('nursing/entry/save/(:num)', 'NursingApi::saveEntry/$1'); +}); + diff --git a/app/Controllers/Api/v1/NursingApi.php b/app/Controllers/Api/v1/NursingApi.php new file mode 100644 index 0000000..3439a9e --- /dev/null +++ b/app/Controllers/Api/v1/NursingApi.php @@ -0,0 +1,149 @@ +bedMasterModel = new BedMasterModel(); + $this->bedAssignmentModel = new BedAssignmentHistoryModel(); + $this->ipdNursingEntryModel = new IpdNursingEntryModel(); + $this->nursingStationModel = new NursingStationModel(); + $this->nurseModel = new NurseModel(); + } + + /** + * Serves the React PWA app entry point at /app/nursing + */ + public function pwaIndex() + { + $pwaPath = FCPATH . 'App/Nursing/index.html'; + if (file_exists($pwaPath)) { + return $this->response->setBody(file_get_contents($pwaPath)); + } + + return $this->response->setJSON([ + 'app_name' => 'Nursing Care PWA App Gateway', + 'status' => 'ready', + 'api_base_url' => base_url('api/v1/nursing/'), + 'message' => 'React PWA build target directory /App/Nursing is active. API endpoints are ready for integration.', + 'endpoints' => [ + 'beds' => base_url('api/v1/nursing/beds'), + 'workspace' => base_url('api/v1/nursing/workspace/{ipdId}'), + 'save_entry' => base_url('api/v1/nursing/entry/save/{ipdId}'), + 'nurses' => base_url('api/v1/nursing/nurses'), + ] + ]); + } + + /** + * GET api/v1/nursing/beds + */ + public function beds() + { + $records = $this->bedMasterModel->getAllWithRelations(); + $nursingStations = $this->nursingStationModel->getActiveStations(); + + return $this->response->setJSON([ + 'status' => 1, + 'data' => [ + 'beds' => $records, + 'nursing_stations' => $nursingStations, + ] + ]); + } + + /** + * GET api/v1/nursing/nurses + */ + public function nurses() + { + return $this->response->setJSON([ + 'status' => 1, + 'data' => $this->nurseModel->getActiveNurses() + ]); + } + + /** + * GET api/v1/nursing/workspace/(:num) + */ + public function workspace(int $ipdId) + { + $entries = $this->ipdNursingEntryModel->getByIpd($ipdId); + + return $this->response->setJSON([ + 'status' => 1, + 'ipd_id' => $ipdId, + 'entries' => $entries, + ]); + } + + /** + * POST api/v1/nursing/entry/save/(:num) + */ + public function saveEntry(int $ipdId) + { + $post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []); + $entryType = (string) ($post['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) ($post['recorded_at'] ?? ''); + $recordedAt = $recordedAtInput !== '' + ? str_replace('T', ' ', $recordedAtInput) . (strlen($recordedAtInput) === 16 ? ':00' : '') + : date('Y-m-d H:i:s'); + + $data = [ + 'ipd_id' => $ipdId, + 'entry_type' => $entryType, + 'recorded_at' => $recordedAt, + 'shift_name' => '', + 'temperature_c' => isset($post['temperature_f']) && $post['temperature_f'] !== '' ? (((float)$post['temperature_f'] - 32) * 5 / 9) : null, + 'pulse_rate' => isset($post['pulse_rate']) && $post['pulse_rate'] !== '' ? (int) $post['pulse_rate'] : null, + 'resp_rate' => isset($post['resp_rate']) && $post['resp_rate'] !== '' ? (int) $post['resp_rate'] : null, + 'bp_systolic' => isset($post['bp_systolic']) && $post['bp_systolic'] !== '' ? (int) $post['bp_systolic'] : null, + 'bp_diastolic' => isset($post['bp_diastolic']) && $post['bp_diastolic'] !== '' ? (int) $post['bp_diastolic'] : null, + 'spo2' => isset($post['spo2']) && $post['spo2'] !== '' ? (int) $post['spo2'] : null, + 'weight_kg' => isset($post['weight_kg']) && $post['weight_kg'] !== '' ? (float) $post['weight_kg'] : null, + 'fluid_direction' => (string) ($post['fluid_direction'] ?? ''), + 'fluid_route' => (string) ($post['fluid_route'] ?? ''), + 'fluid_amount_ml' => isset($post['fluid_amount_ml']) && $post['fluid_amount_ml'] !== '' ? (int) $post['fluid_amount_ml'] : null, + 'treatment_text' => (string) ($post['treatment_text'] ?? ''), + 'general_note' => (string) ($post['general_note'] ?? ''), + 'recorded_by' => (string) ($post['recorded_by'] ?? 'Staff'), + 'recorded_by_id' => isset($post['recorded_by_id']) ? (int) $post['recorded_by_id'] : null, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + + $entryId = (int) ($post['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, + ]); + } +}