feat: add OT workflow and allow zero payments
This commit is contained in:
parent
670f6d3f1f
commit
d289bb5be1
|
|
@ -221,6 +221,12 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'ipd_discharge.view' => 'Can search, preview, and print IPD discharge summaries',
|
||||
'ipd_discharge.manage' => 'Can create and update IPD discharge summaries and personal section templates',
|
||||
'ipd_discharge.master.manage' => 'Can manage shared discharge surgery, course, dietary, and ICD masters',
|
||||
'ipd_ot.view' => 'Can view IPD Surgery/OT and specialty pre-operative examinations',
|
||||
'ipd_ot.examination.manage' => 'Can create, complete, and revise specialty pre-operative examinations',
|
||||
'ipd_ot.request.manage' => 'Can create and update OT surgery requests',
|
||||
'ipd_ot.schedule.manage' => 'Can schedule and reschedule OT surgery cases',
|
||||
'ipd_ot.status.manage' => 'Can record patient calls, safety checks, and OT status changes',
|
||||
'ipd_ot.postop.manage' => 'Can record signed post-operative assessments and handover',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -256,6 +262,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'abdm.*',
|
||||
'ipd_nursing.*',
|
||||
'ipd_discharge.*',
|
||||
'ipd_ot.*',
|
||||
],
|
||||
'admin' => [
|
||||
'admin.access',
|
||||
|
|
@ -284,6 +291,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'abdm.*',
|
||||
'ipd_nursing.*',
|
||||
'ipd_discharge.*',
|
||||
'ipd_ot.*',
|
||||
],
|
||||
'developer' => [
|
||||
'admin.access',
|
||||
|
|
@ -311,6 +319,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'abdm.*',
|
||||
'ipd_nursing.*',
|
||||
'ipd_discharge.*',
|
||||
'ipd_ot.*',
|
||||
],
|
||||
'doctor' => [
|
||||
'opd.doctor-panel.access',
|
||||
|
|
@ -322,6 +331,12 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'ipd_nursing.view',
|
||||
'ipd_discharge.view',
|
||||
'ipd_discharge.manage',
|
||||
'ipd_ot.view',
|
||||
'ipd_ot.examination.manage',
|
||||
'ipd_ot.request.manage',
|
||||
'ipd_ot.schedule.manage',
|
||||
'ipd_ot.status.manage',
|
||||
'ipd_ot.postop.manage',
|
||||
],
|
||||
'nurse' => [
|
||||
'ipd_nursing.view',
|
||||
|
|
@ -329,6 +344,10 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'ipd_nursing.bed.transfer',
|
||||
'ipd_nursing.charge.manage',
|
||||
'ipd_discharge.view',
|
||||
'ipd_ot.view',
|
||||
'ipd_ot.request.manage',
|
||||
'ipd_ot.status.manage',
|
||||
'ipd_ot.postop.manage',
|
||||
],
|
||||
'pharmacy_admin' => [
|
||||
'pharmacy.*',
|
||||
|
|
|
|||
62
app/Config/IpdOtForms.php
Normal file
62
app/Config/IpdOtForms.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class IpdOtForms extends BaseConfig
|
||||
{
|
||||
public array $examinationForms = [
|
||||
'ophthalmology_preop_v1' => [
|
||||
'title' => 'Pre-operative Ophthalmology Examination',
|
||||
'schema_version' => 1,
|
||||
'department_aliases' => [
|
||||
'ophthalmology',
|
||||
'ophthalmic',
|
||||
'ophthalmic surgery',
|
||||
'eye',
|
||||
'eye department',
|
||||
'eye surgery',
|
||||
'department of eye',
|
||||
'department of ophthalmology',
|
||||
'ophthalmology department',
|
||||
'ophthalmology eye',
|
||||
],
|
||||
'columns' => [
|
||||
'od' => 'Right Eye (OD)',
|
||||
'os' => 'Left Eye (OS)',
|
||||
],
|
||||
'rows' => [
|
||||
'visual_acuity' => 'Visual Acuity',
|
||||
'refraction_findings' => 'Refraction Findings',
|
||||
'slit_lamp_examination' => 'Slit Lamp Examination',
|
||||
'fundus_examination' => 'Fundus Examination',
|
||||
'intraocular_pressure' => 'Intraocular Pressure',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function formsForDepartment(string $departmentName): array
|
||||
{
|
||||
$normalizedDepartment = $this->normalizeDepartmentName($departmentName);
|
||||
$matches = [];
|
||||
foreach ($this->examinationForms as $formKey => $form) {
|
||||
$aliases = array_map(
|
||||
fn ($alias): string => $this->normalizeDepartmentName((string) $alias),
|
||||
$form['department_aliases'] ?? []
|
||||
);
|
||||
if ($normalizedDepartment !== '' && in_array($normalizedDepartment, $aliases, true)) {
|
||||
$matches[(string) $formKey] = $form;
|
||||
}
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
private function normalizeDepartmentName(string $name): string
|
||||
{
|
||||
$name = strtolower(trim($name));
|
||||
$name = (string) preg_replace('/[^a-z0-9]+/', ' ', $name);
|
||||
return trim((string) preg_replace('/\s+/', ' ', $name));
|
||||
}
|
||||
}
|
||||
|
|
@ -448,6 +448,17 @@ $routes->post('setup/db-tools/prepare-filesystem', 'Setup::prepareFilesystem');
|
|||
$routes->post('setup/db-tools/ensure-admin', 'Setup::ensureAdminLogin');
|
||||
$routes->post('setup/db-tools/complete', 'Setup::complete');
|
||||
|
||||
$routes->get('ipd/ot/(:num)', 'IpdOt::panel/$1', ['filter' => 'permission:ipd_ot.view,ipd_ot.examination.manage']);
|
||||
$routes->get('ipd/ot/(:num)/examination/(:segment)', 'IpdOt::examination/$1/$2', ['filter' => 'permission:ipd_ot.view,ipd_ot.examination.manage']);
|
||||
$routes->post('ipd/ot/(:num)/examination/(:segment)', 'IpdOt::saveExamination/$1/$2', ['filter' => 'permission:ipd_ot.examination.manage']);
|
||||
$routes->get('ipd/ot/(:num)/request', 'IpdOt::requestForm/$1', ['filter' => 'permission:ipd_ot.request.manage']);
|
||||
$routes->post('ipd/ot/(:num)/requests', 'IpdOt::createCase/$1', ['filter' => 'permission:ipd_ot.request.manage']);
|
||||
$routes->get('ot/queue', 'IpdOt::queue', ['filter' => 'permission:ipd_ot.view,ipd_ot.schedule.manage,ipd_ot.status.manage,ipd_ot.postop.manage']);
|
||||
$routes->get('ot/cases/(:num)', 'IpdOt::caseForm/$1', ['filter' => 'permission:ipd_ot.view,ipd_ot.schedule.manage,ipd_ot.status.manage,ipd_ot.postop.manage']);
|
||||
$routes->post('ot/cases/(:num)', 'IpdOt::updateCase/$1', ['filter' => 'permission:ipd_ot.schedule.manage,ipd_ot.status.manage']);
|
||||
$routes->get('ot/cases/(:num)/postop', 'IpdOt::postopForm/$1', ['filter' => 'permission:ipd_ot.view,ipd_ot.postop.manage']);
|
||||
$routes->post('ot/cases/(:num)/postop', 'IpdOt::savePostop/$1', ['filter' => 'permission:ipd_ot.postop.manage']);
|
||||
|
||||
|
||||
$routes->group('billing', function($routes) {
|
||||
$routes->get('ipd', 'Billing\\Ipd::index');
|
||||
|
|
|
|||
597
app/Controllers/IpdOt.php
Normal file
597
app/Controllers/IpdOt.php
Normal file
|
|
@ -0,0 +1,597 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\ClinicalAuditTrail;
|
||||
use App\Models\IpdBillingModel;
|
||||
use App\Models\IpdOtCaseModel;
|
||||
use App\Models\IpdPreopExaminationModel;
|
||||
use Config\IpdOtForms;
|
||||
|
||||
class IpdOt extends BaseController
|
||||
{
|
||||
private IpdBillingModel $ipdBillingModel;
|
||||
private IpdPreopExaminationModel $examinationModel;
|
||||
private IpdOtCaseModel $caseModel;
|
||||
private IpdOtForms $otConfig;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ipdBillingModel = new IpdBillingModel();
|
||||
$this->examinationModel = new IpdPreopExaminationModel();
|
||||
$this->caseModel = new IpdOtCaseModel();
|
||||
$this->otConfig = config('IpdOtForms');
|
||||
helper(['form']);
|
||||
}
|
||||
|
||||
public function panel(int $ipdId)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.view', 'ipd_ot.examination.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$context = $this->getAdmissionContext($ipdId);
|
||||
if ($context === []) {
|
||||
return $this->response->setStatusCode(404)->setBody('IPD admission not found.');
|
||||
}
|
||||
|
||||
$forms = $this->otConfig->formsForDepartment((string) $context['department_name']);
|
||||
foreach ($forms as $formKey => &$form) {
|
||||
$form['key'] = $formKey;
|
||||
$form['record'] = $this->examinationModel->findForAdmission($ipdId, $formKey);
|
||||
}
|
||||
unset($form);
|
||||
|
||||
$cases = $this->caseModel->where('ipd_id', $ipdId)->orderBy('id', 'DESC')->findAll();
|
||||
|
||||
return view('ipd_ot/panel', [
|
||||
'ipd_id' => $ipdId,
|
||||
'department_name' => $context['department_name'],
|
||||
'forms' => $forms,
|
||||
'can_manage' => $this->userCan('ipd_ot.examination.manage'),
|
||||
'cases' => $cases,
|
||||
'can_request' => $this->userCan('ipd_ot.request.manage'),
|
||||
'can_status' => $this->userCan('ipd_ot.status.manage'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function queue()
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.view', 'ipd_ot.schedule.manage', 'ipd_ot.status.manage', 'ipd_ot.postop.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
$date = trim((string) ($this->request->getGet('date') ?? date('Y-m-d')));
|
||||
$departmentId = max(0, (int) ($this->request->getGet('department_id') ?? 0));
|
||||
$status = trim((string) ($this->request->getGet('status') ?? ''));
|
||||
$builder = $this->db->table('ipd_ot_cases c')
|
||||
->select('c.*, i.ipd_code, p.p_code, p.p_fname, p.mphone1')
|
||||
->join('ipd_master i', 'i.id = c.ipd_id')
|
||||
->join('patient_master p', 'p.id = c.patient_id', 'left');
|
||||
if ($date !== '') {
|
||||
$builder->where('COALESCE(DATE(c.scheduled_start_at), c.requested_date)', $date, false);
|
||||
}
|
||||
if ($departmentId > 0) {
|
||||
$builder->where('c.department_id', $departmentId);
|
||||
}
|
||||
if ($status !== '') {
|
||||
$builder->where('c.status', $status);
|
||||
}
|
||||
return view('ipd_ot/queue', [
|
||||
'rows' => $builder->orderBy('COALESCE(c.scheduled_start_at, CONCAT(c.requested_date, " ", COALESCE(c.requested_time, "00:00:00")))', 'ASC', false)->get()->getResultArray(),
|
||||
'departments' => $this->db->table('hc_department')->select('iId, vName')->where('iId >', 0)->orderBy('vName')->get()->getResultArray(),
|
||||
'filter_date' => $date,
|
||||
'filter_department_id' => $departmentId,
|
||||
'filter_status' => $status,
|
||||
'can_schedule' => $this->userCan('ipd_ot.schedule.manage'),
|
||||
'can_status' => $this->userCan('ipd_ot.status.manage'),
|
||||
'can_postop' => $this->userCan('ipd_ot.postop.manage'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function requestForm(int $ipdId)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.request.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
$context = $this->getAdmissionContext($ipdId);
|
||||
if ($context === []) {
|
||||
return $this->response->setStatusCode(404)->setBody('IPD admission not found.');
|
||||
}
|
||||
return view('ipd_ot/request_form', ['ipd_id' => $ipdId, 'department_name' => $context['department_name'], 'doctors' => $this->ipdBillingModel->getIpdDoctorList()]);
|
||||
}
|
||||
|
||||
public function createCase(int $ipdId)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.request.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
$context = $this->getAdmissionContext($ipdId);
|
||||
if ($context === []) {
|
||||
return $this->jsonResult(false, 'IPD admission not found.', [], 404);
|
||||
}
|
||||
$procedureName = trim((string) ($this->request->getPost('procedure_name') ?? ''));
|
||||
$requestedDate = trim((string) ($this->request->getPost('requested_date') ?? ''));
|
||||
if ($procedureName === '' || ! $this->validDate($requestedDate)) {
|
||||
return $this->jsonResult(false, 'Procedure and valid requested date are required.', [], 422);
|
||||
}
|
||||
$surgeonId = max(0, (int) ($this->request->getPost('surgeon_id') ?? 0));
|
||||
$userId = (int) (auth()->user()->id ?? 0);
|
||||
$data = [
|
||||
'request_no' => 'OT' . date('ymdHis') . str_pad((string) $ipdId, 4, '0', STR_PAD_LEFT),
|
||||
'ipd_id' => $ipdId, 'patient_id' => (int) $context['patient_id'],
|
||||
'department_id' => (int) $context['department_id'], 'department_name_snapshot' => (string) $context['department_name'],
|
||||
'procedure_name' => mb_substr($procedureName, 0, 255),
|
||||
'procedure_side' => $this->allowedValue((string) $this->request->getPost('procedure_side'), ['not_applicable', 'left', 'right', 'bilateral'], 'not_applicable'),
|
||||
'priority' => $this->allowedValue((string) $this->request->getPost('priority'), ['routine', 'urgent', 'emergency'], 'routine'),
|
||||
'requested_date' => $requestedDate,
|
||||
'requested_time' => $this->validTime((string) $this->request->getPost('requested_time')) ? (string) $this->request->getPost('requested_time') : null,
|
||||
'requested_notes' => mb_substr(trim((string) $this->request->getPost('requested_notes')), 0, 5000),
|
||||
'surgeon_id' => $surgeonId, 'surgeon_name_snapshot' => $this->doctorName($surgeonId),
|
||||
'status' => 'requested', 'call_status' => 'not_called', 'created_by' => $userId, 'updated_by' => $userId,
|
||||
];
|
||||
$this->db->transBegin();
|
||||
$this->db->query('SELECT id FROM ipd_master WHERE id = ? FOR UPDATE', [$ipdId]);
|
||||
if ($this->caseModel->where('ipd_id', $ipdId)->whereNotIn('status', ['completed', 'cancelled'])->countAllResults() > 0) {
|
||||
$this->db->transRollback();
|
||||
return $this->jsonResult(false, 'This admission already has an open surgery request.', [], 422);
|
||||
}
|
||||
if ($this->caseModel->insert($data) === false) {
|
||||
$this->db->transRollback();
|
||||
return $this->jsonResult(false, 'Unable to create surgery request.', [], 500);
|
||||
}
|
||||
$caseId = (int) $this->caseModel->getInsertID();
|
||||
if (! $this->appendEvent($caseId, 'requested', '', 'requested', null, null, '', (string) $data['requested_notes'])) {
|
||||
$this->db->transRollback();
|
||||
return $this->jsonResult(false, 'Unable to record surgery request history.', [], 500);
|
||||
}
|
||||
$this->db->transCommit();
|
||||
return $this->jsonResult(true, 'Surgery request created.', ['id' => $caseId]);
|
||||
}
|
||||
|
||||
public function caseForm(int $caseId)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.view', 'ipd_ot.schedule.manage', 'ipd_ot.status.manage', 'ipd_ot.postop.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
$case = $this->caseModel->find($caseId);
|
||||
if (! is_array($case)) {
|
||||
return $this->response->setStatusCode(404)->setBody('OT case not found.');
|
||||
}
|
||||
$status = (string) $case['status'];
|
||||
return view('ipd_ot/case_form', [
|
||||
'case' => $case,
|
||||
'doctors' => $this->ipdBillingModel->getIpdDoctorList(),
|
||||
'can_schedule' => $this->userCan('ipd_ot.schedule.manage') && in_array($status, ['requested', 'scheduled', 'called'], true),
|
||||
'can_status' => $this->userCan('ipd_ot.status.manage') && in_array($status, ['requested', 'scheduled', 'called', 'in_progress'], true),
|
||||
'can_postop' => $this->userCan('ipd_ot.postop.manage') && in_array($status, ['in_progress', 'completed'], true),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateCase(int $caseId)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.schedule.manage', 'ipd_ot.status.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
$case = $this->caseModel->find($caseId);
|
||||
if (! is_array($case)) {
|
||||
return $this->jsonResult(false, 'OT case not found.', [], 404);
|
||||
}
|
||||
$action = trim((string) ($this->request->getPost('action') ?? ''));
|
||||
$reason = trim((string) ($this->request->getPost('reason') ?? ''));
|
||||
$userId = (int) (auth()->user()->id ?? 0);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$updates = ['updated_by' => $userId];
|
||||
$fromStatus = (string) $case['status'];
|
||||
$toStatus = $fromStatus;
|
||||
$oldSchedule = $case['scheduled_start_at'] ?? null;
|
||||
$newSchedule = $oldSchedule;
|
||||
|
||||
if (in_array($action, ['schedule', 'reschedule'], true)) {
|
||||
if (! $this->userCan('ipd_ot.schedule.manage')) {
|
||||
return $this->jsonResult(false, 'Scheduling permission is required.', [], 403);
|
||||
}
|
||||
$scheduledAt = str_replace('T', ' ', trim((string) $this->request->getPost('scheduled_start_at')));
|
||||
$canInitialSchedule = $action === 'schedule' && $fromStatus === 'requested' && empty($oldSchedule);
|
||||
$canReschedule = $action === 'reschedule' && in_array($fromStatus, ['scheduled', 'called'], true) && ! empty($oldSchedule);
|
||||
if (! $canInitialSchedule && ! $canReschedule) {
|
||||
return $this->jsonResult(false, 'This OT case cannot be scheduled from its current status.', [], 422);
|
||||
}
|
||||
if (! $this->validDateTime($scheduledAt) || ($action === 'reschedule' && $reason === '')) {
|
||||
return $this->jsonResult(false, $action === 'reschedule' ? 'Valid date/time and reschedule reason are required.' : 'Valid surgery date and time are required.', [], 422);
|
||||
}
|
||||
$surgeonId = max(0, (int) ($this->request->getPost('surgeon_id') ?? $case['surgeon_id']));
|
||||
$anesthetistId = max(0, (int) ($this->request->getPost('anesthetist_id') ?? $case['anesthetist_id']));
|
||||
$updates += ['scheduled_start_at' => $scheduledAt, 'surgeon_id' => $surgeonId, 'surgeon_name_snapshot' => $this->doctorName($surgeonId), 'anesthetist_id' => $anesthetistId, 'anesthetist_name_snapshot' => $this->doctorName($anesthetistId), 'status' => 'scheduled', 'call_status' => 'not_called', 'called_at' => null, 'called_by' => 0, 'confirmed_at' => null, 'confirmed_by' => 0];
|
||||
$toStatus = 'scheduled';
|
||||
$newSchedule = $scheduledAt;
|
||||
} elseif ($action === 'call') {
|
||||
if (! $this->userCan('ipd_ot.status.manage')) {
|
||||
return $this->jsonResult(false, 'Status permission is required.', [], 403);
|
||||
}
|
||||
$callStatus = $this->allowedValue((string) $this->request->getPost('call_status'), ['called', 'confirmed', 'not_reachable', 'declined'], 'called');
|
||||
if ($callStatus === 'declined' && $reason === '') {
|
||||
return $this->jsonResult(false, 'Reason is required when the patient declines.', [], 422);
|
||||
}
|
||||
$updates += ['call_status' => $callStatus, 'called_at' => $now, 'called_by' => $userId];
|
||||
if ($callStatus === 'confirmed') {
|
||||
$updates += ['confirmed_at' => $now, 'confirmed_by' => $userId];
|
||||
}
|
||||
} elseif ($action === 'status') {
|
||||
if (! $this->userCan('ipd_ot.status.manage')) {
|
||||
return $this->jsonResult(false, 'Status permission is required.', [], 403);
|
||||
}
|
||||
$nextStatus = $this->allowedValue((string) $this->request->getPost('status'), ['requested', 'scheduled', 'called', 'in_progress', 'completed', 'cancelled'], '');
|
||||
$allowedTransitions = ['requested' => ['cancelled'], 'scheduled' => ['called', 'in_progress', 'cancelled'], 'called' => ['scheduled', 'in_progress', 'cancelled'], 'in_progress' => ['completed']];
|
||||
if (! in_array($nextStatus, $allowedTransitions[$fromStatus] ?? [], true)) {
|
||||
return $this->jsonResult(false, 'Invalid OT status transition.', [], 422);
|
||||
}
|
||||
if ($nextStatus === 'cancelled' && $reason === '') {
|
||||
return $this->jsonResult(false, 'Cancellation reason is required.', [], 422);
|
||||
}
|
||||
$safetyFields = ['consent_verified', 'site_side_verified', 'allergy_verified', 'npo_verified', 'investigations_verified', 'anesthesia_clearance', 'who_sign_in', 'who_time_out', 'who_sign_out'];
|
||||
foreach ($safetyFields as $field) {
|
||||
$updates[$field] = (int) ($this->request->getPost($field) ?? 0) === 1 ? 1 : 0;
|
||||
}
|
||||
$updates['blood_availability'] = $this->allowedValue((string) ($this->request->getPost('blood_availability') ?? $case['blood_availability']), ['not_required', 'arranged', 'available'], 'not_required');
|
||||
if ($nextStatus === 'in_progress') {
|
||||
foreach (['consent_verified', 'site_side_verified', 'allergy_verified', 'npo_verified', 'investigations_verified', 'anesthesia_clearance', 'who_sign_in'] as $required) {
|
||||
if ((int) $updates[$required] !== 1) {
|
||||
return $this->jsonResult(false, 'Complete all pre-operative safety checks and WHO sign-in first.', [], 422);
|
||||
}
|
||||
}
|
||||
$updates['actual_start_at'] = $now;
|
||||
$updates['who_time_out'] = 0;
|
||||
$updates['who_sign_out'] = 0;
|
||||
}
|
||||
if ($nextStatus === 'completed') {
|
||||
if ((int) $updates['who_time_out'] !== 1 || (int) $updates['who_sign_out'] !== 1) {
|
||||
return $this->jsonResult(false, 'WHO time-out and sign-out are required before completion.', [], 422);
|
||||
}
|
||||
$updates['actual_end_at'] = $now;
|
||||
}
|
||||
$updates['status'] = $nextStatus;
|
||||
$toStatus = $nextStatus;
|
||||
} else {
|
||||
return $this->jsonResult(false, 'Unsupported OT action.', [], 422);
|
||||
}
|
||||
$this->db->transBegin();
|
||||
$caseUpdated = $this->db->table('ipd_ot_cases')
|
||||
->where('id', $caseId)
|
||||
->where('status', $fromStatus)
|
||||
->where('lock_version', (int) ($case['lock_version'] ?? 0))
|
||||
->update($updates + ['lock_version' => (int) ($case['lock_version'] ?? 0) + 1, 'updated_at' => $now]);
|
||||
if (! $caseUpdated) {
|
||||
$this->db->transRollback();
|
||||
return $this->jsonResult(false, 'Unable to update OT case and history.', [], 500);
|
||||
}
|
||||
if ($this->db->affectedRows() === 0) {
|
||||
$currentCase = $this->caseModel->select('status, lock_version')->find($caseId);
|
||||
if ((string) ($currentCase['status'] ?? '') !== $fromStatus || (int) ($currentCase['lock_version'] ?? -1) !== (int) ($case['lock_version'] ?? 0)) {
|
||||
$this->db->transRollback();
|
||||
return $this->jsonResult(false, 'The OT case changed while you were editing it. Reload and try again.', [], 409);
|
||||
}
|
||||
}
|
||||
if (! $this->appendEvent($caseId, $action, $fromStatus, $toStatus, $oldSchedule, $newSchedule, $reason, trim((string) $this->request->getPost('notes')))) {
|
||||
$this->db->transRollback();
|
||||
return $this->jsonResult(false, 'Unable to update OT case history.', [], 500);
|
||||
}
|
||||
$this->db->transCommit();
|
||||
return $this->jsonResult(true, 'OT case updated.', ['id' => $caseId]);
|
||||
}
|
||||
|
||||
public function postopForm(int $caseId)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.view', 'ipd_ot.postop.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
$case = $this->caseModel->find($caseId);
|
||||
if (! is_array($case)) {
|
||||
return $this->response->setStatusCode(404)->setBody('OT case not found.');
|
||||
}
|
||||
return view('ipd_ot/postop_form', ['case' => $case, 'assessments' => $this->db->table('ipd_ot_postop_assessments')->where('case_id', $caseId)->orderBy('observed_at', 'DESC')->get()->getResultArray(), 'can_manage' => $this->userCan('ipd_ot.postop.manage')]);
|
||||
}
|
||||
|
||||
public function savePostop(int $caseId)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.postop.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
$case = $this->caseModel->find($caseId);
|
||||
if (! is_array($case) || ! in_array((string) $case['status'], ['in_progress', 'completed'], true)) {
|
||||
return $this->jsonResult(false, 'Post-operative assessment is available after surgery starts.', [], 422);
|
||||
}
|
||||
$observedAt = str_replace('T', ' ', trim((string) $this->request->getPost('observed_at')));
|
||||
if (! $this->validDateTime($observedAt)) {
|
||||
return $this->jsonResult(false, 'Valid observation date and time are required.', [], 422);
|
||||
}
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$consciousness = trim((string) $this->request->getPost('consciousness'));
|
||||
$airwayBreathing = trim((string) $this->request->getPost('airway_breathing'));
|
||||
$bp = trim((string) $this->request->getPost('bp'));
|
||||
$pulse = trim((string) $this->request->getPost('pulse'));
|
||||
$spo2 = trim((string) $this->request->getPost('spo2'));
|
||||
$disposition = trim((string) $this->request->getPost('disposition'));
|
||||
$handoverNotes = trim((string) $this->request->getPost('handover_notes'));
|
||||
if ($consciousness === '' || $airwayBreathing === '' || $bp === '' || $pulse === '' || $spo2 === '' || $disposition === '' || $handoverNotes === '') {
|
||||
return $this->jsonResult(false, 'Consciousness, airway/breathing, BP, pulse, SpO2, disposition, and handover notes are required.', [], 422);
|
||||
}
|
||||
if ($observedAt > $now || (! empty($case['actual_start_at']) && $observedAt < (string) $case['actual_start_at'])) {
|
||||
return $this->jsonResult(false, 'Observation time must be between surgery start and the current time.', [], 422);
|
||||
}
|
||||
$this->db->transBegin();
|
||||
$assessmentSaved = $this->db->table('ipd_ot_postop_assessments')->insert([
|
||||
'case_id' => $caseId, 'observed_at' => $observedAt,
|
||||
'consciousness' => mb_substr($consciousness, 0, 80), 'airway_breathing' => mb_substr($airwayBreathing, 0, 255),
|
||||
'bp' => mb_substr($bp, 0, 30), 'pulse' => mb_substr($pulse, 0, 20), 'spo2' => mb_substr($spo2, 0, 20), 'temperature' => mb_substr(trim((string) $this->request->getPost('temperature')), 0, 20),
|
||||
'pain_score' => max(0, min(10, (int) $this->request->getPost('pain_score'))), 'bleeding_wound_drain' => mb_substr(trim((string) $this->request->getPost('bleeding_wound_drain')), 0, 5000), 'nausea_vomiting' => mb_substr(trim((string) $this->request->getPost('nausea_vomiting')), 0, 100),
|
||||
'complications' => mb_substr(trim((string) $this->request->getPost('complications')), 0, 5000), 'interventions' => mb_substr(trim((string) $this->request->getPost('interventions')), 0, 5000), 'disposition' => mb_substr($disposition, 0, 100), 'handover_notes' => mb_substr($handoverNotes, 0, 5000),
|
||||
'recorded_by' => (int) (auth()->user()->id ?? 0), 'signed_at' => $now, 'created_at' => $now,
|
||||
]);
|
||||
if (! $assessmentSaved
|
||||
|| ! $this->appendEvent($caseId, 'postop_assessment', (string) $case['status'], (string) $case['status'], $case['scheduled_start_at'] ?? null, $case['scheduled_start_at'] ?? null, '', 'Post-operative assessment signed.')) {
|
||||
$this->db->transRollback();
|
||||
return $this->jsonResult(false, 'Unable to sign post-operative assessment and history.', [], 500);
|
||||
}
|
||||
$this->db->transCommit();
|
||||
return $this->jsonResult(true, 'Post-operative assessment signed.');
|
||||
}
|
||||
|
||||
public function examination(int $ipdId, string $formKey)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.view', 'ipd_ot.examination.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$resolved = $this->resolveRequestedForm($ipdId, $formKey);
|
||||
if (isset($resolved['error'])) {
|
||||
return $this->response->setStatusCode((int) $resolved['status'])->setBody((string) $resolved['error']);
|
||||
}
|
||||
|
||||
$record = $this->examinationModel->findForAdmission($ipdId, $formKey);
|
||||
$values = json_decode((string) ($record['payload_json'] ?? ''), true);
|
||||
if (! is_array($values)) {
|
||||
$values = [];
|
||||
}
|
||||
|
||||
return view('ipd_ot/ophthalmology_preop', [
|
||||
'ipd_id' => $ipdId,
|
||||
'form_key' => $formKey,
|
||||
'form' => $resolved['form'],
|
||||
'department_name' => $resolved['context']['department_name'],
|
||||
'record' => $record,
|
||||
'values' => $values,
|
||||
'can_manage' => $this->userCan('ipd_ot.examination.manage'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveExamination(int $ipdId, string $formKey)
|
||||
{
|
||||
if ($response = $this->requireAnyPermission(['ipd_ot.examination.manage'])) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$resolved = $this->resolveRequestedForm($ipdId, $formKey);
|
||||
if (isset($resolved['error'])) {
|
||||
return $this->jsonResult(false, (string) $resolved['error'], [], (int) $resolved['status']);
|
||||
}
|
||||
|
||||
$form = $resolved['form'];
|
||||
$context = $resolved['context'];
|
||||
$postedValues = $this->request->getPost('values');
|
||||
$postedValues = is_array($postedValues) ? $postedValues : [];
|
||||
$values = $this->sanitizeValues($postedValues, $form);
|
||||
$status = strtolower(trim((string) ($this->request->getPost('status') ?? 'draft')));
|
||||
$status = $status === 'completed' ? 'completed' : 'draft';
|
||||
$editReason = trim((string) ($this->request->getPost('edit_reason') ?? ''));
|
||||
$existing = $this->examinationModel->findForAdmission($ipdId, $formKey);
|
||||
|
||||
if ($status === 'completed' && ! $this->hasAnyFinding($values)) {
|
||||
return $this->jsonResult(false, 'Enter at least one examination finding before completing the form.', [], 422);
|
||||
}
|
||||
|
||||
$payloadJson = (string) json_encode($values, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
$hasChanged = $existing === []
|
||||
|| (string) ($existing['payload_json'] ?? '') !== $payloadJson
|
||||
|| (string) ($existing['status'] ?? 'draft') !== $status;
|
||||
|
||||
if (! $hasChanged) {
|
||||
return $this->jsonResult(true, 'No changes to save.', ['id' => (int) ($existing['id'] ?? 0)]);
|
||||
}
|
||||
if ((string) ($existing['status'] ?? '') === 'completed' && $editReason === '') {
|
||||
return $this->jsonResult(false, 'Edit reason is required when changing a completed examination.', [], 422);
|
||||
}
|
||||
|
||||
$userId = (int) (auth()->user()->id ?? 0);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$saveData = [
|
||||
'ipd_id' => $ipdId,
|
||||
'patient_id' => (int) $context['patient_id'],
|
||||
'department_id' => (int) $context['department_id'],
|
||||
'department_name_snapshot' => (string) $context['department_name'],
|
||||
'form_key' => $formKey,
|
||||
'schema_version' => (int) ($form['schema_version'] ?? 1),
|
||||
'episode_no' => 1,
|
||||
'payload_json' => $payloadJson,
|
||||
'status' => $status,
|
||||
'examined_by' => $status === 'completed' ? $userId : (int) ($existing['examined_by'] ?? 0),
|
||||
'examined_at' => $status === 'completed' ? $now : ($existing['examined_at'] ?? null),
|
||||
'updated_by' => $userId,
|
||||
];
|
||||
|
||||
$this->db->transStart();
|
||||
if ($existing !== []) {
|
||||
$revisionNo = (int) ($this->db->table('ipd_preop_examination_updates')
|
||||
->selectMax('revision_no', 'revision_no')
|
||||
->where('examination_id', (int) $existing['id'])
|
||||
->get()
|
||||
->getRowArray()['revision_no'] ?? 0) + 1;
|
||||
$this->db->table('ipd_preop_examination_updates')->insert([
|
||||
'examination_id' => (int) $existing['id'],
|
||||
'revision_no' => $revisionNo,
|
||||
'payload_json' => (string) ($existing['payload_json'] ?? ''),
|
||||
'status' => (string) ($existing['status'] ?? 'draft'),
|
||||
'examined_by' => (int) ($existing['examined_by'] ?? 0),
|
||||
'examined_at' => $existing['examined_at'] ?? null,
|
||||
'edit_reason' => $editReason,
|
||||
'updated_by' => $userId,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$this->examinationModel->update((int) $existing['id'], $saveData);
|
||||
$recordId = (int) $existing['id'];
|
||||
} else {
|
||||
$saveData['created_by'] = $userId;
|
||||
$this->examinationModel->insert($saveData);
|
||||
$recordId = (int) $this->examinationModel->getInsertID();
|
||||
}
|
||||
$this->db->transComplete();
|
||||
|
||||
if (! $this->db->transStatus()) {
|
||||
return $this->jsonResult(false, 'Unable to save the examination.', [], 500);
|
||||
}
|
||||
|
||||
try {
|
||||
(new ClinicalAuditTrail())->logChangedFields(
|
||||
'ipd_preop_examination',
|
||||
$recordId,
|
||||
['payload' => json_decode((string) ($existing['payload_json'] ?? ''), true), 'status' => $existing['status'] ?? null],
|
||||
['payload' => $values, 'status' => $status],
|
||||
$userId
|
||||
);
|
||||
} catch (\Throwable $exception) {
|
||||
log_message('error', 'Unable to write pre-operative examination audit: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return $this->jsonResult(true, 'Pre-operative examination saved.', ['id' => $recordId]);
|
||||
}
|
||||
|
||||
private function resolveRequestedForm(int $ipdId, string $formKey): array
|
||||
{
|
||||
$context = $this->getAdmissionContext($ipdId);
|
||||
if ($context === []) {
|
||||
return ['error' => 'IPD admission not found.', 'status' => 404];
|
||||
}
|
||||
|
||||
$forms = $this->otConfig->formsForDepartment((string) $context['department_name']);
|
||||
if (! isset($forms[$formKey])) {
|
||||
return ['error' => 'This examination is not configured for the admission department.', 'status' => 403];
|
||||
}
|
||||
|
||||
return ['context' => $context, 'form' => $forms[$formKey]];
|
||||
}
|
||||
|
||||
private function appendEvent(int $caseId, string $eventType, string $fromStatus, string $toStatus, $oldSchedule, $newSchedule, string $reason, string $notes): bool
|
||||
{
|
||||
return $this->db->table('ipd_ot_case_events')->insert(['case_id' => $caseId, 'event_type' => $eventType, 'from_status' => $fromStatus, 'to_status' => $toStatus, 'old_scheduled_at' => $oldSchedule ?: null, 'new_scheduled_at' => $newSchedule ?: null, 'reason' => mb_substr($reason, 0, 255), 'notes' => mb_substr($notes, 0, 5000), 'actor_id' => (int) (auth()->user()->id ?? 0), 'created_at' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
private function doctorName(int $doctorId): string
|
||||
{
|
||||
if ($doctorId <= 0) {
|
||||
return '';
|
||||
}
|
||||
$row = $this->db->table('doctor_master')->select("TRIM(CONCAT_WS(' ', p_fname, p_mname, p_lname)) AS name", false)->where('id', $doctorId)->get(1)->getRowArray();
|
||||
return trim((string) ($row['name'] ?? ''));
|
||||
}
|
||||
|
||||
private function allowedValue(string $value, array $allowed, string $default): string
|
||||
{
|
||||
$value = strtolower(trim($value));
|
||||
return in_array($value, $allowed, true) ? $value : $default;
|
||||
}
|
||||
|
||||
private function validDate(string $value): bool
|
||||
{
|
||||
$date = \DateTime::createFromFormat('Y-m-d', $value);
|
||||
return $date !== false && $date->format('Y-m-d') === $value;
|
||||
}
|
||||
|
||||
private function validTime(string $value): bool
|
||||
{
|
||||
return preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/', trim($value)) === 1;
|
||||
}
|
||||
|
||||
private function validDateTime(string $value): bool
|
||||
{
|
||||
$value = trim($value);
|
||||
foreach (['Y-m-d H:i', 'Y-m-d H:i:s'] as $format) {
|
||||
$date = \DateTime::createFromFormat($format, $value);
|
||||
if ($date !== false && $date->format($format) === $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getAdmissionContext(int $ipdId): array
|
||||
{
|
||||
$row = $this->db->table('ipd_master i')
|
||||
->select('i.id, i.p_id, i.dept_id, d.vName AS department_name')
|
||||
->join('hc_department d', 'd.iId = i.dept_id', 'left')
|
||||
->where('i.id', $ipdId)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if ($row === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'patient_id' => (int) ($row['p_id'] ?? 0),
|
||||
'department_id' => (int) ($row['dept_id'] ?? 0),
|
||||
'department_name' => trim((string) ($row['department_name'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
private function sanitizeValues(array $postedValues, array $form): array
|
||||
{
|
||||
$values = [];
|
||||
foreach (($form['rows'] ?? []) as $rowKey => $unusedLabel) {
|
||||
foreach (($form['columns'] ?? []) as $columnKey => $unusedColumnLabel) {
|
||||
$value = trim((string) ($postedValues[$rowKey][$columnKey] ?? ''));
|
||||
$values[(string) $rowKey][(string) $columnKey] = mb_substr($value, 0, 5000);
|
||||
}
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
private function hasAnyFinding(array $values): bool
|
||||
{
|
||||
foreach ($values as $rowValues) {
|
||||
foreach ($rowValues as $value) {
|
||||
if (trim((string) $value) !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function requireAnyPermission(array $permissions)
|
||||
{
|
||||
foreach ($permissions as $permission) {
|
||||
if ($this->userCan($permission)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $this->response->setStatusCode(403)->setBody('Access denied');
|
||||
}
|
||||
|
||||
private function userCan(string $permission): bool
|
||||
{
|
||||
if (! function_exists('auth')) {
|
||||
return false;
|
||||
}
|
||||
$user = auth()->user();
|
||||
return $user !== null && method_exists($user, 'can') && $user->can($permission);
|
||||
}
|
||||
|
||||
private function jsonResult(bool $success, string $message, array $extra = [], int $statusCode = 200)
|
||||
{
|
||||
return $this->response->setStatusCode($statusCode)->setJSON(array_merge([
|
||||
'update' => $success ? 1 : 0,
|
||||
'error_text' => $message,
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
], $extra));
|
||||
}
|
||||
}
|
||||
|
|
@ -98,8 +98,8 @@ class Payment extends BaseController
|
|||
{
|
||||
$paymentId = (int) $this->request->getPost('pay_id');
|
||||
$rawAmount = trim((string) $this->request->getPost('change_value'));
|
||||
if (! is_numeric($rawAmount) || (float) $rawAmount <= 0) {
|
||||
return $this->error('Enter a valid amount greater than zero.', 422);
|
||||
if (! is_numeric($rawAmount) || (float) $rawAmount < 0) {
|
||||
return $this->error('Enter a valid amount of zero or greater.', 422);
|
||||
}
|
||||
|
||||
$payment = $this->paymentModel->findPayment($paymentId);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use App\Libraries\RoleRegistry;
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateIpdPreopExaminations extends Migration
|
||||
{
|
||||
private const PERMISSIONS = [
|
||||
'ipd_ot.view',
|
||||
'ipd_ot.examination.manage',
|
||||
];
|
||||
|
||||
public function up()
|
||||
{
|
||||
if (! $this->tableExists('ipd_preop_examinations')) {
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'ipd_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'patient_id' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'department_id' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'department_name_snapshot' => ['type' => 'VARCHAR', 'constraint' => 150, 'default' => ''],
|
||||
'form_key' => ['type' => 'VARCHAR', 'constraint' => 100],
|
||||
'schema_version' => ['type' => 'INT', 'unsigned' => true, 'default' => 1],
|
||||
'episode_no' => ['type' => 'INT', 'unsigned' => true, 'default' => 1],
|
||||
'payload_json' => ['type' => 'LONGTEXT', 'null' => true],
|
||||
'status' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'draft'],
|
||||
'examined_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'examined_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'created_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'updated_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey(['ipd_id', 'form_key', 'episode_no'], 'uq_ipd_preop_episode');
|
||||
$this->forge->addKey(['department_id', 'status'], false, false, 'idx_ipd_preop_department_status');
|
||||
$this->forge->createTable('ipd_preop_examinations', true, ['ENGINE' => 'InnoDB']);
|
||||
}
|
||||
|
||||
if (! $this->tableExists('ipd_preop_examination_updates')) {
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'BIGINT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'examination_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'revision_no' => ['type' => 'INT', 'unsigned' => true],
|
||||
'payload_json' => ['type' => 'LONGTEXT', 'null' => true],
|
||||
'status' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'draft'],
|
||||
'examined_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'examined_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'edit_reason' => ['type' => 'VARCHAR', 'constraint' => 255, 'default' => ''],
|
||||
'updated_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'updated_at' => ['type' => 'DATETIME'],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey(['examination_id', 'revision_no'], 'uq_ipd_preop_revision');
|
||||
$this->forge->createTable('ipd_preop_examination_updates', true, ['ENGINE' => 'InnoDB']);
|
||||
}
|
||||
|
||||
$this->grantPermissions();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->tableExists('auth_permissions_users')) {
|
||||
$this->db->table('auth_permissions_users')->whereIn('permission', self::PERMISSIONS)->delete();
|
||||
}
|
||||
if ($this->tableExists('auth_role_permissions')) {
|
||||
$this->db->table('auth_role_permissions')->whereIn('permission', self::PERMISSIONS)->delete();
|
||||
}
|
||||
if ($this->tableExists('auth_roles')) {
|
||||
(new RoleRegistry($this->db))->publish();
|
||||
}
|
||||
|
||||
$this->forge->dropTable('ipd_preop_examination_updates', true);
|
||||
$this->forge->dropTable('ipd_preop_examinations', true);
|
||||
}
|
||||
|
||||
private function grantPermissions(): void
|
||||
{
|
||||
if (! $this->tableExists('auth_roles') || ! $this->tableExists('auth_role_permissions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$roleGrants = [
|
||||
'superadmin' => self::PERMISSIONS,
|
||||
'admin' => self::PERMISSIONS,
|
||||
'developer' => self::PERMISSIONS,
|
||||
'doctor' => self::PERMISSIONS,
|
||||
'nurse' => ['ipd_ot.view'],
|
||||
];
|
||||
|
||||
foreach ($roleGrants as $alias => $permissions) {
|
||||
$role = $this->db->table('auth_roles')->select('id')->where('alias', $alias)->get(1)->getRowArray();
|
||||
if ($role === null) {
|
||||
continue;
|
||||
}
|
||||
foreach ($permissions as $permission) {
|
||||
$exists = $this->db->table('auth_role_permissions')
|
||||
->where('role_id', (int) $role['id'])
|
||||
->where('permission', $permission)
|
||||
->countAllResults();
|
||||
if ($exists === 0) {
|
||||
$this->db->table('auth_role_permissions')->insert([
|
||||
'role_id' => (int) $role['id'],
|
||||
'permission' => $permission,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->tableExists('auth_permissions_users')) {
|
||||
$managerIds = array_column(
|
||||
$this->db->table('auth_permissions_users')->select('user_id')->where('permission', 'users.manage-admins')->get()->getResultArray(),
|
||||
'user_id'
|
||||
);
|
||||
foreach ($managerIds as $userId) {
|
||||
foreach (self::PERMISSIONS as $permission) {
|
||||
$exists = $this->db->table('auth_permissions_users')
|
||||
->where('user_id', (int) $userId)
|
||||
->where('permission', $permission)
|
||||
->countAllResults();
|
||||
if ($exists === 0) {
|
||||
$this->db->table('auth_permissions_users')->insert([
|
||||
'user_id' => (int) $userId,
|
||||
'permission' => $permission,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(new RoleRegistry($this->db))->publish();
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
{
|
||||
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray() !== null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use App\Libraries\RoleRegistry;
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateIpdOtWorkflow extends Migration
|
||||
{
|
||||
private const PERMISSIONS = [
|
||||
'ipd_ot.request.manage',
|
||||
'ipd_ot.schedule.manage',
|
||||
'ipd_ot.status.manage',
|
||||
'ipd_ot.postop.manage',
|
||||
];
|
||||
|
||||
public function up()
|
||||
{
|
||||
if (! $this->tableExists('ipd_ot_cases')) {
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'request_no' => ['type' => 'VARCHAR', 'constraint' => 30],
|
||||
'ipd_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'patient_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'department_id' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'department_name_snapshot' => ['type' => 'VARCHAR', 'constraint' => 150, 'default' => ''],
|
||||
'procedure_name' => ['type' => 'VARCHAR', 'constraint' => 255],
|
||||
'procedure_side' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => 'not_applicable'],
|
||||
'priority' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'routine'],
|
||||
'requested_date' => ['type' => 'DATE'],
|
||||
'requested_time' => ['type' => 'TIME', 'null' => true],
|
||||
'requested_notes' => ['type' => 'TEXT', 'null' => true],
|
||||
'scheduled_start_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'surgeon_id' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'surgeon_name_snapshot' => ['type' => 'VARCHAR', 'constraint' => 180, 'default' => ''],
|
||||
'anesthetist_id' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'anesthetist_name_snapshot' => ['type' => 'VARCHAR', 'constraint' => 180, 'default' => ''],
|
||||
'status' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => 'requested'],
|
||||
'call_status' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => 'not_called'],
|
||||
'called_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'called_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'confirmed_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'confirmed_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'consent_verified' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'site_side_verified' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'allergy_verified' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'npo_verified' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'investigations_verified' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'anesthesia_clearance' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'blood_availability' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => 'not_required'],
|
||||
'who_sign_in' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'who_time_out' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'who_sign_out' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'actual_start_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'actual_end_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'lock_version' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'created_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'updated_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey('request_no', 'uq_ipd_ot_request_no');
|
||||
$this->forge->addKey(['scheduled_start_at', 'department_id', 'status'], false, false, 'idx_ipd_ot_queue');
|
||||
$this->forge->addKey(['ipd_id', 'status'], false, false, 'idx_ipd_ot_admission');
|
||||
$this->forge->createTable('ipd_ot_cases', true, ['ENGINE' => 'InnoDB']);
|
||||
}
|
||||
|
||||
if (! $this->tableExists('ipd_ot_case_events')) {
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'BIGINT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'case_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'event_type' => ['type' => 'VARCHAR', 'constraint' => 40],
|
||||
'from_status' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => ''],
|
||||
'to_status' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => ''],
|
||||
'old_scheduled_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'new_scheduled_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'reason' => ['type' => 'VARCHAR', 'constraint' => 255, 'default' => ''],
|
||||
'notes' => ['type' => 'TEXT', 'null' => true],
|
||||
'actor_id' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'created_at' => ['type' => 'DATETIME'],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey(['case_id', 'created_at'], false, false, 'idx_ipd_ot_case_events');
|
||||
$this->forge->createTable('ipd_ot_case_events', true, ['ENGINE' => 'InnoDB']);
|
||||
}
|
||||
|
||||
if (! $this->tableExists('ipd_ot_postop_assessments')) {
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'BIGINT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'case_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'supersedes_id' => ['type' => 'BIGINT', 'unsigned' => true, 'default' => 0],
|
||||
'observed_at' => ['type' => 'DATETIME'],
|
||||
'consciousness' => ['type' => 'VARCHAR', 'constraint' => 80, 'default' => ''],
|
||||
'airway_breathing' => ['type' => 'VARCHAR', 'constraint' => 255, 'default' => ''],
|
||||
'bp' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => ''],
|
||||
'pulse' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => ''],
|
||||
'spo2' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => ''],
|
||||
'temperature' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => ''],
|
||||
'pain_score' => ['type' => 'TINYINT', 'unsigned' => true, 'default' => 0],
|
||||
'bleeding_wound_drain' => ['type' => 'TEXT', 'null' => true],
|
||||
'nausea_vomiting' => ['type' => 'VARCHAR', 'constraint' => 100, 'default' => ''],
|
||||
'complications' => ['type' => 'TEXT', 'null' => true],
|
||||
'interventions' => ['type' => 'TEXT', 'null' => true],
|
||||
'disposition' => ['type' => 'VARCHAR', 'constraint' => 100, 'default' => ''],
|
||||
'handover_notes' => ['type' => 'TEXT', 'null' => true],
|
||||
'edit_reason' => ['type' => 'VARCHAR', 'constraint' => 255, 'default' => ''],
|
||||
'recorded_by' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
|
||||
'signed_at' => ['type' => 'DATETIME'],
|
||||
'created_at' => ['type' => 'DATETIME'],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey(['case_id', 'observed_at'], false, false, 'idx_ipd_ot_postop_case');
|
||||
$this->forge->createTable('ipd_ot_postop_assessments', true, ['ENGINE' => 'InnoDB']);
|
||||
}
|
||||
|
||||
$this->grantPermissions();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->tableExists('auth_permissions_users')) {
|
||||
$this->db->table('auth_permissions_users')->whereIn('permission', self::PERMISSIONS)->delete();
|
||||
}
|
||||
if ($this->tableExists('auth_role_permissions')) {
|
||||
$this->db->table('auth_role_permissions')->whereIn('permission', self::PERMISSIONS)->delete();
|
||||
}
|
||||
if ($this->tableExists('auth_roles')) {
|
||||
(new RoleRegistry($this->db))->publish();
|
||||
}
|
||||
$this->forge->dropTable('ipd_ot_postop_assessments', true);
|
||||
$this->forge->dropTable('ipd_ot_case_events', true);
|
||||
$this->forge->dropTable('ipd_ot_cases', true);
|
||||
}
|
||||
|
||||
private function grantPermissions(): void
|
||||
{
|
||||
if (! $this->tableExists('auth_roles') || ! $this->tableExists('auth_role_permissions')) {
|
||||
return;
|
||||
}
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$roleGrants = [
|
||||
'superadmin' => self::PERMISSIONS,
|
||||
'admin' => self::PERMISSIONS,
|
||||
'developer' => self::PERMISSIONS,
|
||||
'doctor' => self::PERMISSIONS,
|
||||
'nurse' => ['ipd_ot.request.manage', 'ipd_ot.status.manage', 'ipd_ot.postop.manage'],
|
||||
];
|
||||
foreach ($roleGrants as $alias => $permissions) {
|
||||
$role = $this->db->table('auth_roles')->select('id')->where('alias', $alias)->get(1)->getRowArray();
|
||||
if ($role === null) {
|
||||
continue;
|
||||
}
|
||||
foreach ($permissions as $permission) {
|
||||
$exists = $this->db->table('auth_role_permissions')->where('role_id', (int) $role['id'])->where('permission', $permission)->countAllResults();
|
||||
if ($exists === 0) {
|
||||
$this->db->table('auth_role_permissions')->insert(['role_id' => (int) $role['id'], 'permission' => $permission, 'created_at' => $now]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->tableExists('auth_permissions_users')) {
|
||||
$managerIds = array_column($this->db->table('auth_permissions_users')->select('user_id')->where('permission', 'users.manage-admins')->get()->getResultArray(), 'user_id');
|
||||
foreach ($managerIds as $userId) {
|
||||
foreach (self::PERMISSIONS as $permission) {
|
||||
$exists = $this->db->table('auth_permissions_users')->where('user_id', (int) $userId)->where('permission', $permission)->countAllResults();
|
||||
if ($exists === 0) {
|
||||
$this->db->table('auth_permissions_users')->insert(['user_id' => (int) $userId, 'permission' => $permission, 'created_at' => $now]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(new RoleRegistry($this->db))->publish();
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
{
|
||||
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray() !== null;
|
||||
}
|
||||
}
|
||||
23
app/Models/IpdOtCaseModel.php
Normal file
23
app/Models/IpdOtCaseModel.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class IpdOtCaseModel extends Model
|
||||
{
|
||||
protected $table = 'ipd_ot_cases';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = [
|
||||
'request_no', 'ipd_id', 'patient_id', 'department_id', 'department_name_snapshot',
|
||||
'procedure_name', 'procedure_side', 'priority', 'requested_date', 'requested_time',
|
||||
'requested_notes', 'scheduled_start_at', 'surgeon_id', 'surgeon_name_snapshot',
|
||||
'anesthetist_id', 'anesthetist_name_snapshot', 'status', 'call_status', 'called_at',
|
||||
'called_by', 'confirmed_at', 'confirmed_by', 'consent_verified', 'site_side_verified',
|
||||
'allergy_verified', 'npo_verified', 'investigations_verified', 'anesthesia_clearance',
|
||||
'blood_availability', 'who_sign_in', 'who_time_out', 'who_sign_out',
|
||||
'actual_start_at', 'actual_end_at', 'lock_version', 'created_by', 'updated_by',
|
||||
];
|
||||
}
|
||||
36
app/Models/IpdPreopExaminationModel.php
Normal file
36
app/Models/IpdPreopExaminationModel.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class IpdPreopExaminationModel extends Model
|
||||
{
|
||||
protected $table = 'ipd_preop_examinations';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = [
|
||||
'ipd_id',
|
||||
'patient_id',
|
||||
'department_id',
|
||||
'department_name_snapshot',
|
||||
'form_key',
|
||||
'schema_version',
|
||||
'episode_no',
|
||||
'payload_json',
|
||||
'status',
|
||||
'examined_by',
|
||||
'examined_at',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
public function findForAdmission(int $ipdId, string $formKey, int $episodeNo = 1): array
|
||||
{
|
||||
return $this->where('ipd_id', $ipdId)
|
||||
->where('form_key', $formKey)
|
||||
->where('episode_no', $episodeNo)
|
||||
->first() ?? [];
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,10 @@ $dischargeSummaryUrl = site_url('Ipd_discharge/preview_discharge_report/' . $ipd
|
|||
$dischargeSummaryCreateUrl = site_url('Ipd_discharge/ipd_select/' . $ipdId);
|
||||
$bedsideChargeUrl = site_url('billing/ipd/bedside-charge/add/' . $ipdId);
|
||||
$doctorVisitChargeUrl = site_url('billing/ipd/doctor-visit/add/' . $ipdId);
|
||||
$otPanelUrl = site_url('ipd/ot/' . $ipdId);
|
||||
$authUser = function_exists('auth') ? auth()->user() : null;
|
||||
$canViewIpdOt = $authUser && method_exists($authUser, 'can')
|
||||
&& ($authUser->can('ipd_ot.view') || $authUser->can('ipd_ot.examination.manage'));
|
||||
$today = date('Y-m-d');
|
||||
$csrfName = csrf_token();
|
||||
$csrfHash = csrf_hash();
|
||||
|
|
@ -193,6 +197,11 @@ ksort($nurseNames);
|
|||
<li class="nav-item" role="presentation">
|
||||
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_treatment">Treatment / Procedure Note</button>
|
||||
</li>
|
||||
<?php if ($canViewIpdOt) : ?>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_ot" data-url="<?= esc($otPanelUrl, 'attr') ?>">Surgery / OT</button>
|
||||
</li>
|
||||
<?php endif ?>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_scan">Scan Paper -> Auto Fill</button>
|
||||
</li>
|
||||
|
|
@ -519,6 +528,12 @@ ksort($nurseNames);
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($canViewIpdOt) : ?>
|
||||
<div class="nursing-tab-pane d-none" id="nursing_tab_ot">
|
||||
<div id="tab_ot_content">Loading...</div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="nursing-tab-pane d-none" id="nursing_tab_scan">
|
||||
<div class="alert alert-warning py-2 mb-3">
|
||||
OCR/AI extraction is assistive only. Nurse review is mandatory before save (NABH documentation requirement).
|
||||
|
|
@ -1334,6 +1349,11 @@ ksort($nurseNames);
|
|||
if (targetPane) {
|
||||
targetPane.classList.remove('d-none');
|
||||
}
|
||||
|
||||
var contentUrl = button.getAttribute('data-url');
|
||||
if (contentUrl && targetId === 'nursing_tab_ot') {
|
||||
load_form_div(contentUrl, 'tab_ot_content');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
28
app/Views/ipd_ot/case_form.php
Normal file
28
app/Views/ipd_ot/case_form.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/** @var array<string, mixed> $case */
|
||||
/** @var array<int, object> $doctors */
|
||||
/** @var bool $can_schedule */
|
||||
/** @var bool $can_status */
|
||||
/** @var bool $can_postop */
|
||||
$caseId = (int) ($case['id'] ?? 0);
|
||||
$status = (string) ($case['status'] ?? 'requested');
|
||||
?>
|
||||
<h5><?= esc((string) ($case['request_no'] ?? 'OT Case')) ?> - <?= esc((string) ($case['procedure_name'] ?? '')) ?></h5>
|
||||
<p class="small text-muted mb-3"><?= esc((string) ($case['department_name_snapshot'] ?? '')) ?> | Status: <?= esc(ucwords(str_replace('_', ' ', $status))) ?> | Call: <?= esc(ucwords(str_replace('_', ' ', (string) ($case['call_status'] ?? '')))) ?></p>
|
||||
<?php if ($can_schedule) : ?>
|
||||
<form class="ipd-ot-ajax-form border rounded p-3 mb-3" action="<?= site_url('ot/cases/' . $caseId) ?>" method="post" data-refresh="queue"><h6>Schedule / Reschedule</h6><?= csrf_field() ?><input type="hidden" name="action" value="<?= empty($case['scheduled_start_at']) ? 'schedule' : 'reschedule' ?>"><div class="row g-2">
|
||||
<div class="col-md-4"><label class="form-label">Date & Time</label><input type="datetime-local" name="scheduled_start_at" class="form-control" value="<?= esc(str_replace(' ', 'T', substr((string) ($case['scheduled_start_at'] ?? ''), 0, 16)), 'attr') ?>" required></div>
|
||||
<div class="col-md-4"><label class="form-label">Surgeon</label><select name="surgeon_id" class="form-select"><option value="0">Select</option><?php foreach ($doctors as $doctor) : ?><option value="<?= (int) ($doctor->id ?? 0) ?>" <?= (int) ($doctor->id ?? 0) === (int) ($case['surgeon_id'] ?? 0) ? 'selected' : '' ?>><?= esc((string) ($doctor->DocSpecName ?? '')) ?></option><?php endforeach ?></select></div>
|
||||
<div class="col-md-4"><label class="form-label">Anesthetist</label><select name="anesthetist_id" class="form-select"><option value="0">Select</option><?php foreach ($doctors as $doctor) : ?><option value="<?= (int) ($doctor->id ?? 0) ?>" <?= (int) ($doctor->id ?? 0) === (int) ($case['anesthetist_id'] ?? 0) ? 'selected' : '' ?>><?= esc((string) ($doctor->DocSpecName ?? '')) ?></option><?php endforeach ?></select></div>
|
||||
<?php if (! empty($case['scheduled_start_at'])) : ?><div class="col-md-9"><label class="form-label">Reason for date/time modification</label><input type="text" name="reason" class="form-control" maxlength="255" required></div><?php endif ?><div class="col-md-3 d-flex align-items-end"><button class="btn btn-primary w-100" type="submit"><?= empty($case['scheduled_start_at']) ? 'Schedule' : 'Reschedule' ?></button></div></div><div class="ipd-ot-form-notice mt-2"></div></form>
|
||||
<?php endif ?>
|
||||
<?php if ($can_status) : ?>
|
||||
<form class="ipd-ot-ajax-form border rounded p-3 mb-3" action="<?= site_url('ot/cases/' . $caseId) ?>" method="post" data-refresh="queue"><h6>Patient Call Confirmation</h6><?= csrf_field() ?><input type="hidden" name="action" value="call"><div class="row g-2"><div class="col-md-4"><select name="call_status" class="form-select"><option value="called">Called</option><option value="confirmed">Confirmed</option><option value="not_reachable">Not reachable</option><option value="declined">Declined</option></select></div><div class="col-md-5"><input type="text" name="reason" class="form-control" placeholder="Reason/remarks (required if declined)"></div><div class="col-md-3"><button class="btn btn-outline-primary w-100">Update Call</button></div></div><div class="ipd-ot-form-notice mt-2"></div></form>
|
||||
<form class="ipd-ot-ajax-form border rounded p-3" action="<?= site_url('ot/cases/' . $caseId) ?>" method="post" data-refresh="queue"><h6>Safety Checklist & Status</h6><?= csrf_field() ?><input type="hidden" name="action" value="status"><div class="row g-2">
|
||||
<?php foreach (['consent_verified'=>'Consent','site_side_verified'=>'Patient/Site/Side','allergy_verified'=>'Allergy','npo_verified'=>'NPO/Fasting','investigations_verified'=>'Investigations','anesthesia_clearance'=>'Anesthesia Clearance','who_sign_in'=>'WHO Sign-in','who_time_out'=>'WHO Time-out','who_sign_out'=>'WHO Sign-out'] as $field=>$label) : ?><div class="col-md-4"><label class="form-check"><input class="form-check-input" type="checkbox" name="<?= $field ?>" value="1" <?= ! empty($case[$field]) ? 'checked' : '' ?>><span class="form-check-label"><?= esc($label) ?></span></label></div><?php endforeach ?>
|
||||
<div class="col-md-4"><label class="form-label">Blood</label><select class="form-select" name="blood_availability"><option value="not_required">Not required</option><option value="arranged" <?= ($case['blood_availability'] ?? '') === 'arranged' ? 'selected' : '' ?>>Arranged</option><option value="available" <?= ($case['blood_availability'] ?? '') === 'available' ? 'selected' : '' ?>>Available</option></select></div>
|
||||
<div class="col-md-4"><label class="form-label">Next Status</label><select class="form-select" name="status"><?php $statusOptions = ['requested'=>['cancelled'],'scheduled'=>['called','in_progress','cancelled'],'called'=>['scheduled','in_progress','cancelled'],'in_progress'=>['completed']][$status] ?? []; foreach ($statusOptions as $next) : ?><option value="<?= $next ?>"><?= esc(ucwords(str_replace('_',' ',$next))) ?></option><?php endforeach ?></select></div><div class="col-md-4"><label class="form-label">Reason/Notes</label><input class="form-control" name="reason" placeholder="Required for cancellation"></div><div class="col-12 text-end"><button class="btn btn-success">Update Status</button></div></div><div class="ipd-ot-form-notice mt-2"></div></form>
|
||||
<?php endif ?>
|
||||
<?php if ($can_postop) : ?><div class="mt-3"><button type="button" class="btn btn-outline-secondary ipd-ot-postop-open" data-case-id="<?= $caseId ?>">Post-operative Assessment</button></div><?php endif ?>
|
||||
<?= view('ipd_ot/workflow_script') ?>
|
||||
<script>$(document).off('click.otPostop','.ipd-ot-postop-open').on('click.otPostop','.ipd-ot-postop-open',function(){load_form_div('<?= site_url('ot/cases/' . $caseId . '/postop') ?>',$(this).closest('.modal-body').attr('id'));});</script>
|
||||
107
app/Views/ipd_ot/ophthalmology_preop.php
Normal file
107
app/Views/ipd_ot/ophthalmology_preop.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
/** @var int $ipd_id */
|
||||
/** @var string $form_key */
|
||||
/** @var array<string, mixed> $form */
|
||||
/** @var string $department_name */
|
||||
/** @var array<string, mixed> $record */
|
||||
/** @var array<string, array<string, string>> $values */
|
||||
/** @var bool $can_manage */
|
||||
$isCompleted = (string) ($record['status'] ?? '') === 'completed';
|
||||
?>
|
||||
<form id="ipd_ophthalmology_preop_form" action="<?= site_url('ipd/ot/' . (int) $ipd_id . '/examination/' . rawurlencode($form_key)) ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||||
<div>
|
||||
<h5 class="mb-1"><?= esc((string) ($form['title'] ?? 'Pre-operative Examination')) ?></h5>
|
||||
<div class="text-muted small"><?= esc($department_name) ?></div>
|
||||
</div>
|
||||
<?php if ($isCompleted) : ?>
|
||||
<span class="badge bg-success">Completed</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<div id="ipd_ot_exam_notice"></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:34%;">Parameter</th>
|
||||
<?php foreach (($form['columns'] ?? []) as $columnLabel) : ?>
|
||||
<th><?= esc((string) $columnLabel) ?></th>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (($form['rows'] ?? []) as $rowKey => $rowLabel) : ?>
|
||||
<tr>
|
||||
<th><?= esc((string) $rowLabel) ?></th>
|
||||
<?php foreach (($form['columns'] ?? []) as $columnKey => $columnLabel) : ?>
|
||||
<td>
|
||||
<textarea class="form-control form-control-sm" name="values[<?= esc((string) $rowKey, 'attr') ?>][<?= esc((string) $columnKey, 'attr') ?>]" rows="2" <?= $can_manage ? '' : 'readonly' ?>><?= esc((string) ($values[$rowKey][$columnKey] ?? '')) ?></textarea>
|
||||
</td>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($can_manage) : ?>
|
||||
<?php if ($isCompleted) : ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="ipd_ot_edit_reason">Reason for editing completed examination</label>
|
||||
<input type="text" class="form-control" id="ipd_ot_edit_reason" name="edit_reason" maxlength="255" required>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-outline-primary" name="status" value="draft">Save Draft</button>
|
||||
<button type="submit" class="btn btn-success" name="status" value="completed">Complete Examination</button>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var form = document.getElementById('ipd_ophthalmology_preop_form');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(form).off('submit.ipdOtSave').on('submit.ipdOtSave', function(event) {
|
||||
event.preventDefault();
|
||||
var submitter = event.originalEvent && event.originalEvent.submitter ? event.originalEvent.submitter : null;
|
||||
var formData = new window.FormData(form);
|
||||
formData.set('status', submitter ? String(submitter.value || 'draft') : 'draft');
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
}).then(function(response) {
|
||||
return response.json().then(function(data) {
|
||||
return {ok: response.ok, data: data};
|
||||
});
|
||||
}).then(function(result) {
|
||||
var data = result.data || {};
|
||||
var notice = document.getElementById('ipd_ot_exam_notice');
|
||||
if (data.csrfName && data.csrfHash) {
|
||||
var csrf = form.querySelector('input[name="' + data.csrfName + '"]');
|
||||
if (csrf) {
|
||||
csrf.value = data.csrfHash;
|
||||
}
|
||||
}
|
||||
if (!result.ok || parseInt(data.update || 0, 10) !== 1) {
|
||||
notice.innerHTML = '<div class="alert alert-danger">' + $('<div>').text(data.error_text || 'Unable to save examination.').html() + '</div>';
|
||||
return;
|
||||
}
|
||||
notice.innerHTML = '<div class="alert alert-success">' + $('<div>').text(data.error_text || 'Examination saved.').html() + '</div>';
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('ipdOtExamModal')).hide();
|
||||
if (typeof window.refreshIpdOtPanel === 'function') {
|
||||
window.refreshIpdOtPanel();
|
||||
}
|
||||
}).catch(function() {
|
||||
document.getElementById('ipd_ot_exam_notice').innerHTML = '<div class="alert alert-danger">Unable to save examination.</div>';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
120
app/Views/ipd_ot/panel.php
Normal file
120
app/Views/ipd_ot/panel.php
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
/** @var int $ipd_id */
|
||||
/** @var string $department_name */
|
||||
/** @var array<string, array<string, mixed>> $forms */
|
||||
/** @var bool $can_manage */
|
||||
/** @var array<int, array<string, mixed>> $cases */
|
||||
/** @var bool $can_request */
|
||||
/** @var bool $can_status */
|
||||
?>
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||||
<div>
|
||||
<h5 class="mb-1">Surgery / OT</h5>
|
||||
<div class="text-muted small">Department: <?= esc($department_name !== '' ? $department_name : 'Not assigned') ?></div>
|
||||
</div>
|
||||
<?php if ($can_request) : ?>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="ipd_ot_new_request">New Surgery Request</button>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<?php if (! empty($cases)) : ?>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-sm table-bordered align-middle mb-0">
|
||||
<thead class="table-light"><tr><th>Request</th><th>Procedure</th><th>Requested/Scheduled</th><th>Status</th><th>Call</th><th style="width:90px;">Action</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($cases as $case) : ?>
|
||||
<tr>
|
||||
<td><?= esc((string) ($case['request_no'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($case['procedure_name'] ?? '')) ?></td>
|
||||
<td><?= esc((string) (($case['scheduled_start_at'] ?? '') ?: ($case['requested_date'] ?? ''))) ?></td>
|
||||
<td><span class="badge bg-info text-dark"><?= esc(ucwords(str_replace('_', ' ', (string) ($case['status'] ?? 'requested')))) ?></span></td>
|
||||
<td><?= esc(ucwords(str_replace('_', ' ', (string) ($case['call_status'] ?? 'not_called')))) ?></td>
|
||||
<td><button type="button" class="btn btn-sm btn-outline-primary ipd-ot-case-open" data-case-id="<?= (int) ($case['id'] ?? 0) ?>">Open</button></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (empty($forms)) : ?>
|
||||
<div class="alert alert-info mb-0">No specialty pre-operative examination is configured for this department.</div>
|
||||
<?php else : ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Pre-operative Examination</th>
|
||||
<th style="width:140px;">Status</th>
|
||||
<th style="width:110px;">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($forms as $form) : ?>
|
||||
<?php $recordStatus = (string) ($form['record']['status'] ?? 'Not started'); ?>
|
||||
<tr>
|
||||
<td><?= esc((string) ($form['title'] ?? 'Specialty Examination')) ?></td>
|
||||
<td>
|
||||
<span class="badge <?= $recordStatus === 'completed' ? 'bg-success' : ($recordStatus === 'draft' ? 'bg-warning text-dark' : 'bg-secondary') ?>">
|
||||
<?= esc(ucfirst($recordStatus)) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-primary ipd-ot-open-exam" data-form-key="<?= esc((string) $form['key'], 'attr') ?>">
|
||||
<?= $can_manage ? 'Fill' : 'View' ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="modal fade" id="ipdOtExamModal" tabindex="-1" aria-labelledby="ipdOtExamModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ipdOtExamModalLabel">Specialty Examination</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="ipdOtExamModalBody">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="ipdOtWorkflowModal" tabindex="-1" aria-labelledby="ipdOtWorkflowModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content">
|
||||
<div class="modal-header"><h5 class="modal-title" id="ipdOtWorkflowModalLabel">OT Management</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div>
|
||||
<div class="modal-body" id="ipdOtWorkflowModalBody">Loading...</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var panelUrl = '<?= site_url('ipd/ot/' . (int) $ipd_id) ?>';
|
||||
var examBaseUrl = panelUrl + '/examination/';
|
||||
|
||||
window.refreshIpdOtPanel = function() {
|
||||
load_form_div(panelUrl, 'tab_ot_content');
|
||||
};
|
||||
|
||||
$(document).off('click.ipdOtExam', '.ipd-ot-open-exam').on('click.ipdOtExam', '.ipd-ot-open-exam', function() {
|
||||
var formKey = String($(this).data('form-key') || '');
|
||||
if (formKey === '') {
|
||||
return;
|
||||
}
|
||||
$('#ipdOtExamModalBody').html('Loading...');
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('ipdOtExamModal')).show();
|
||||
load_form_div(examBaseUrl + encodeURIComponent(formKey), 'ipdOtExamModalBody');
|
||||
});
|
||||
|
||||
function openWorkflow(url) {
|
||||
$('#ipdOtWorkflowModalBody').html('Loading...');
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('ipdOtWorkflowModal')).show();
|
||||
load_form_div(url, 'ipdOtWorkflowModalBody');
|
||||
}
|
||||
$(document).off('click.ipdOtRequest', '#ipd_ot_new_request').on('click.ipdOtRequest', '#ipd_ot_new_request', function() { openWorkflow(panelUrl + '/request'); });
|
||||
$(document).off('click.ipdOtCase', '.ipd-ot-case-open').on('click.ipdOtCase', '.ipd-ot-case-open', function() { openWorkflow('<?= site_url('ot/cases') ?>/' + parseInt($(this).data('case-id') || 0, 10)); });
|
||||
})();
|
||||
</script>
|
||||
13
app/Views/ipd_ot/postop_form.php
Normal file
13
app/Views/ipd_ot/postop_form.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
/** @var array<string, mixed> $case */
|
||||
/** @var array<int, array<string, mixed>> $assessments */
|
||||
/** @var bool $can_manage */
|
||||
$caseId=(int)($case['id']??0);
|
||||
?>
|
||||
<h5>Post-operative Assessment & Handover</h5><p class="small text-muted"><?= esc((string)($case['procedure_name']??'')) ?></p>
|
||||
<?php if ($can_manage) : ?><form class="ipd-ot-ajax-form" action="<?= site_url('ot/cases/'.$caseId.'/postop') ?>" method="post" data-refresh="queue"><?= csrf_field() ?><div class="row g-2">
|
||||
<div class="col-md-3"><label class="form-label">Observed At</label><input type="datetime-local" class="form-control" name="observed_at" value="<?= date('Y-m-d\TH:i') ?>" required></div><div class="col-md-3"><label class="form-label">Consciousness</label><input class="form-control" name="consciousness"></div><div class="col-md-6"><label class="form-label">Airway/Breathing</label><input class="form-control" name="airway_breathing"></div>
|
||||
<div class="col-md-2"><label class="form-label">BP</label><input class="form-control" name="bp"></div><div class="col-md-2"><label class="form-label">Pulse</label><input class="form-control" name="pulse"></div><div class="col-md-2"><label class="form-label">SpO2</label><input class="form-control" name="spo2"></div><div class="col-md-2"><label class="form-label">Temperature</label><input class="form-control" name="temperature"></div><div class="col-md-2"><label class="form-label">Pain (0-10)</label><input type="number" min="0" max="10" class="form-control" name="pain_score"></div><div class="col-md-2"><label class="form-label">Nausea/Vomiting</label><input class="form-control" name="nausea_vomiting"></div>
|
||||
<div class="col-md-6"><label class="form-label">Bleeding/Wound/Drain</label><textarea class="form-control" name="bleeding_wound_drain"></textarea></div><div class="col-md-6"><label class="form-label">Complications</label><textarea class="form-control" name="complications"></textarea></div><div class="col-md-6"><label class="form-label">Interventions</label><textarea class="form-control" name="interventions"></textarea></div><div class="col-md-6"><label class="form-label">Disposition</label><input class="form-control" name="disposition" placeholder="PACU / ICU / Ward"></div><div class="col-12"><label class="form-label">Handover Notes</label><textarea class="form-control" name="handover_notes" required></textarea></div><div class="col-12 text-end"><button class="btn btn-success">Sign Assessment</button></div></div><div class="ipd-ot-form-notice mt-2"></div></form><?php endif ?>
|
||||
<?php if (!empty($assessments)) : ?><hr><h6>Signed Assessments</h6><div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr><th>Observed</th><th>Vitals</th><th>Pain</th><th>Disposition/Handover</th></tr></thead><tbody><?php foreach($assessments as $row): ?><tr><td><?= esc((string)$row['observed_at']) ?></td><td>BP <?= esc((string)$row['bp']) ?>, Pulse <?= esc((string)$row['pulse']) ?>, SpO2 <?= esc((string)$row['spo2']) ?></td><td><?= (int)$row['pain_score'] ?></td><td><?= esc((string)$row['disposition']) ?><br><?= esc((string)$row['handover_notes']) ?></td></tr><?php endforeach ?></tbody></table></div><?php endif ?>
|
||||
<?= view('ipd_ot/workflow_script') ?>
|
||||
31
app/Views/ipd_ot/queue.php
Normal file
31
app/Views/ipd_ot/queue.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/** @var array<int, array<string, mixed>> $rows */
|
||||
/** @var array<int, array<string, mixed>> $departments */
|
||||
/** @var string $filter_date */
|
||||
/** @var int $filter_department_id */
|
||||
/** @var string $filter_status */
|
||||
?>
|
||||
<section class="content"><div class="card"><div class="card-header"><h5 class="mb-0">OT Surgery Queue</h5></div><div class="card-body">
|
||||
<form class="row g-2 mb-3" id="ipd_ot_queue_filter">
|
||||
<div class="col-md-3"><label class="form-label">Date</label><input type="date" class="form-control" name="date" value="<?= esc($filter_date, 'attr') ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label">Department</label><select class="form-select" name="department_id"><option value="0">All departments</option><?php foreach ($departments as $department) : ?><option value="<?= (int) $department['iId'] ?>" <?= (int) $department['iId'] === $filter_department_id ? 'selected' : '' ?>><?= esc((string) $department['vName']) ?></option><?php endforeach ?></select></div>
|
||||
<div class="col-md-3"><label class="form-label">Status</label><select class="form-select" name="status"><option value="">All statuses</option><?php foreach (['requested','scheduled','called','in_progress','completed','cancelled'] as $status) : ?><option value="<?= $status ?>" <?= $filter_status === $status ? 'selected' : '' ?>><?= esc(ucwords(str_replace('_', ' ', $status))) ?></option><?php endforeach ?></select></div>
|
||||
<div class="col-md-3 d-flex align-items-end"><button type="submit" class="btn btn-primary">Apply</button></div>
|
||||
</form>
|
||||
<div class="table-responsive"><table class="table table-bordered table-sm align-middle"><thead class="table-light"><tr><th>Date/Time</th><th>Patient</th><th>Department</th><th>Procedure</th><th>Surgeon</th><th>Status</th><th>Call</th><th>Action</th></tr></thead><tbody>
|
||||
<?php if (empty($rows)) : ?><tr><td colspan="8" class="text-center text-muted">No surgery cases for selected filters.</td></tr><?php endif ?>
|
||||
<?php foreach ($rows as $row) : ?><tr>
|
||||
<td><?= esc((string) (($row['scheduled_start_at'] ?? '') ?: (($row['requested_date'] ?? '') . ' ' . ($row['requested_time'] ?? '')))) ?></td>
|
||||
<td><strong><?= esc((string) ($row['p_fname'] ?? '')) ?></strong><br><small><?= esc((string) ($row['ipd_code'] ?? '')) ?> / <?= esc((string) ($row['p_code'] ?? '')) ?></small></td>
|
||||
<td><?= esc((string) ($row['department_name_snapshot'] ?? '')) ?></td><td><?= esc((string) ($row['procedure_name'] ?? '')) ?></td><td><?= esc((string) ($row['surgeon_name_snapshot'] ?? '')) ?></td>
|
||||
<td><?= esc(ucwords(str_replace('_', ' ', (string) ($row['status'] ?? '')))) ?></td><td><?= esc(ucwords(str_replace('_', ' ', (string) ($row['call_status'] ?? '')))) ?></td>
|
||||
<td><button type="button" class="btn btn-sm btn-outline-primary ipd-ot-queue-open" data-case-id="<?= (int) ($row['id'] ?? 0) ?>">Open</button></td>
|
||||
</tr><?php endforeach ?>
|
||||
</tbody></table></div>
|
||||
</div></div></section>
|
||||
<div class="modal fade" id="ipdOtQueueModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">OT Case</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="ipdOtQueueModalBody"></div></div></div></div>
|
||||
<script>(function(){
|
||||
$('#ipd_ot_queue_filter').off('submit.otQueue').on('submit.otQueue',function(e){e.preventDefault();var query=new window.URLSearchParams(new window.FormData(this)).toString();load_form('<?= site_url('ot/queue') ?>?'+query,'OT Surgery Queue');});
|
||||
$(document).off('click.otQueue','.ipd-ot-queue-open').on('click.otQueue','.ipd-ot-queue-open',function(){var id=parseInt($(this).data('case-id')||0,10);bootstrap.Modal.getOrCreateInstance(document.getElementById('ipdOtQueueModal')).show();load_form_div('<?= site_url('ot/cases') ?>/'+id,'ipdOtQueueModalBody');});
|
||||
window.refreshIpdOtQueue=function(){load_form('<?= site_url('ot/queue') ?>?'+$('#ipd_ot_queue_filter').serialize(),'OT Surgery Queue');};
|
||||
})();</script>
|
||||
22
app/Views/ipd_ot/request_form.php
Normal file
22
app/Views/ipd_ot/request_form.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
/** @var int $ipd_id */
|
||||
/** @var string $department_name */
|
||||
/** @var array<int, object> $doctors */
|
||||
?>
|
||||
<form class="ipd-ot-ajax-form" action="<?= site_url('ipd/ot/' . $ipd_id . '/requests') ?>" method="post" data-refresh="panel">
|
||||
<?= csrf_field() ?>
|
||||
<h5>New Surgery Request</h5>
|
||||
<p class="text-muted small">Department: <?= esc($department_name) ?></p>
|
||||
<div class="ipd-ot-form-notice"></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8"><label class="form-label">Planned Procedure</label><input type="text" name="procedure_name" class="form-control" maxlength="255" required></div>
|
||||
<div class="col-md-4"><label class="form-label">Side/Site</label><select name="procedure_side" class="form-select"><option value="not_applicable">Not applicable</option><option value="left">Left</option><option value="right">Right</option><option value="bilateral">Bilateral</option></select></div>
|
||||
<div class="col-md-3"><label class="form-label">Priority</label><select name="priority" class="form-select"><option value="routine">Routine</option><option value="urgent">Urgent</option><option value="emergency">Emergency</option></select></div>
|
||||
<div class="col-md-3"><label class="form-label">Requested Date</label><input type="date" name="requested_date" class="form-control" value="<?= date('Y-m-d') ?>" required></div>
|
||||
<div class="col-md-2"><label class="form-label">Preferred Time</label><input type="time" name="requested_time" class="form-control"></div>
|
||||
<div class="col-md-4"><label class="form-label">Surgeon</label><select name="surgeon_id" class="form-select"><option value="0">Select later</option><?php foreach ($doctors as $doctor) : ?><option value="<?= (int) ($doctor->id ?? 0) ?>"><?= esc((string) ($doctor->DocSpecName ?? $doctor->p_fname ?? '')) ?></option><?php endforeach ?></select></div>
|
||||
<div class="col-12"><label class="form-label">Clinical/Scheduling Notes</label><textarea name="requested_notes" class="form-control" rows="3"></textarea></div>
|
||||
<div class="col-12 text-end"><button type="submit" class="btn btn-primary">Create Request</button></div>
|
||||
</div>
|
||||
</form>
|
||||
<?= view('ipd_ot/workflow_script') ?>
|
||||
3
app/Views/ipd_ot/workflow_script.php
Normal file
3
app/Views/ipd_ot/workflow_script.php
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<script>(function(){
|
||||
$(document).off('submit.ipdOtWorkflow','.ipd-ot-ajax-form').on('submit.ipdOtWorkflow','.ipd-ot-ajax-form',function(event){event.preventDefault();var form=this;var notice=$(form).find('.ipd-ot-form-notice');fetch(form.action,{method:'POST',body:new window.FormData(form),headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(response){return response.json().then(function(data){return{ok:response.ok,data:data};});}).then(function(result){var data=result.data||{};if(data.csrfName&&data.csrfHash){$(form).find('input[name="'+data.csrfName+'"]').val(data.csrfHash);}if(!result.ok||parseInt(data.update||0,10)!==1){notice.html('<div class="alert alert-danger py-2">'+$('<div>').text(data.error_text||'Unable to save.').html()+'</div>');return;}notice.html('<div class="alert alert-success py-2">'+$('<div>').text(data.error_text||'Saved.').html()+'</div>');if(form.dataset.refresh==='panel'&&window.refreshIpdOtPanel){window.refreshIpdOtPanel();}if(form.dataset.refresh==='queue'&&window.refreshIpdOtQueue){window.refreshIpdOtQueue();}}).catch(function(){notice.html('<div class="alert alert-danger py-2">Unable to save.</div>');});});
|
||||
})();</script>
|
||||
|
|
@ -29,9 +29,13 @@ $canIpdNursing = $user && method_exists($user, 'can')
|
|||
$canIpdDischarge = $user && method_exists($user, 'can')
|
||||
? ($user->can('ipd_discharge.view') || $user->can('ipd_discharge.manage') || $user->can('ipd_discharge.*'))
|
||||
: false;
|
||||
$canIpdOt = $user && method_exists($user, 'can')
|
||||
? ($user->can('ipd_ot.view') || $user->can('ipd_ot.request.manage') || $user->can('ipd_ot.schedule.manage') || $user->can('ipd_ot.status.manage') || $user->can('ipd_ot.postop.manage') || $user->can('ipd_ot.*'))
|
||||
: false;
|
||||
if ($user && method_exists($user, 'inGroup') && $user->inGroup('superadmin', 'admin', 'developer')) {
|
||||
$canIpdNursing = true;
|
||||
$canIpdDischarge = true;
|
||||
$canIpdOt = true;
|
||||
}
|
||||
|
||||
$canBilling = false;
|
||||
|
|
@ -329,7 +333,7 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
|
|||
</ul>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($canIpdNursing || $canIpdDischarge) { ?>
|
||||
<?php if ($canIpdNursing || $canIpdDischarge || $canIpdOt) { ?>
|
||||
<li class="nav-heading">In-Patient & Nursing Care</li>
|
||||
<?php if ($canIpdNursing) { ?>
|
||||
<li class="nav-item">
|
||||
|
|
@ -339,6 +343,14 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
|
|||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($canIpdOt) { ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('ot/queue') ?>','OT Surgery Queue')">
|
||||
<i class="bi bi-calendar2-week"></i>
|
||||
<span>OT Surgery Queue</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($canIpdDischarge) { ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Ipd_discharge/search_patient') ?>','Discharge Management')">
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ $locked = ($cashStatus !== '' && $cashStatus !== 'open')
|
|||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label for="payment-edit-amount" class="form-label">Correct Amount</label>
|
||||
<input class="form-control" type="number" min="0.01" step="0.01" id="payment-edit-amount" value="<?= esc(number_format((float) ($payment['amount'] ?? 0), 2, '.', '')) ?>" <?= $locked ? 'disabled' : '' ?>>
|
||||
<input class="form-control" type="number" min="0" step="0.01" id="payment-edit-amount" value="<?= esc(number_format((float) ($payment['amount'] ?? 0), 2, '.', '')) ?>" <?= $locked ? 'disabled' : '' ?>>
|
||||
</div>
|
||||
<div class="col-md-3 d-grid"><button class="btn btn-primary" id="payment-edit-change-amount" <?= $locked ? 'disabled' : '' ?>>Change Amount</button></div>
|
||||
</div>
|
||||
|
|
|
|||
27
tests/unit/IpdOtConfigTest.php
Normal file
27
tests/unit/IpdOtConfigTest.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Config\IpdOtForms;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class IpdOtConfigTest extends TestCase
|
||||
{
|
||||
public function testOphthalmologyAliasesResolvePreoperativeForm(): void
|
||||
{
|
||||
$config = new IpdOtForms();
|
||||
|
||||
foreach (['Ophthalmology', 'EYE DEPARTMENT', 'Department-of-Ophthalmology', 'Ophthalmology Department'] as $departmentName) {
|
||||
$forms = $config->formsForDepartment($departmentName);
|
||||
$this->assertArrayHasKey('ophthalmology_preop_v1', $forms);
|
||||
}
|
||||
}
|
||||
|
||||
public function testUnrelatedDepartmentDoesNotReceiveOphthalmologyForm(): void
|
||||
{
|
||||
$config = new IpdOtForms();
|
||||
|
||||
$this->assertSame([], $config->formsForDepartment('Orthopedics'));
|
||||
$this->assertSame([], $config->formsForDepartment(''));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user