diff --git a/BED_MANAGEMENT_FIX_SUMMARY.md b/BED_MANAGEMENT_FIX_SUMMARY.md new file mode 100644 index 0000000..0c9a067 --- /dev/null +++ b/BED_MANAGEMENT_FIX_SUMMARY.md @@ -0,0 +1,227 @@ +# Bed Management Fix Summary - bed_master System + +## System Overview + +Your hospital management system uses the **NEW bed_master table** for bed management (not the legacy hc_bed_master). + +### Database Schema + +**bed_master table columns:** +- `id` - Primary key +- `bed_number` - Bed number/name +- `bed_code` - Unique bed code +- `ward_id` - Reference to ward_master +- `bed_status` - Status: 'available', 'occupied', 'maintenance', 'blocked', 'reserved', 'cleaning' +- `current_ipd_id` - IPD ID of patient currently occupying this bed (NULL when available) +- `status` - Record status: 'active', 'inactive' + +### How It Works + +1. **Bed Assignment**: When a patient is admitted, `bed_master.current_ipd_id` is set to the IPD ID, and `bed_status` is set to 'occupied' +2. **Bed Release**: When a patient is discharged, `current_ipd_id` is cleared (set to NULL) and `bed_status` is set to 'available' +3. **Bed Status Dashboard**: Queries `bed_master` joining with `ipd_master` on `current_ipd_id` to show current occupancy + +## Issues Identified + +### 1. Beds Not Released on Discharge +**Problem**: When patients are discharged (ipd_status = 1), beds remain occupied: +- `bed_master.bed_status` still shows 'occupied' +- `bed_master.current_ipd_id` still has the IPD ID instead of NULL + +**Impact**: Bed status dashboard shows 5 occupied beds but only 1 current admission, causing confusion and preventing bed reuse for new patients. + +### 2. Blank "Bed Assign" Column +**Problem**: The Current Admission view may show blank in the "Bed No [Type]" column. + +**Root Cause**: The query looks for beds where `current_ipd_id` matches the IPD, but if discharge didn't clear this field, the join fails. + +## Solutions Implemented + +### Fix 1: Add Bed Release Logic + +**File**: `app/Models/IpdModel.php` + +Added new method `releaseBed(int $ipdId)` that: +1. Clears `current_ipd_id` (sets to NULL) in `bed_master` table +2. Sets `bed_status = 'available'` in `bed_master` table +3. Updates `bed_assignment_history` with release date and reason (if that table is used) +4. Also handles legacy `hc_bed_master` for backward compatibility (if it exists) + +### Fix 2: Call Bed Release on Discharge + +**File**: `app/Controllers/Billing/Ipd.php` + +Modified the `updateDischargeStatus()` method to call `releaseBed()` when ipd_status is set to 1. + +### Fix 3: Update Bed Assignment Logic + +**File**: `app/Models/IpdModel.php` + +Updated `bedAssign()` method to: +1. Set `bed_master.current_ipd_id` to the IPD ID when assigning +2. Set `bed_master.bed_status` to 'occupied' +3. Clear any previous bed assigned to the same IPD (in case of bed transfer) + +## Additional Recommendations + +### 1. Run the SQL Fix to Clean Up Existing Data + +For existing IPDs where beds weren't released properly, run the SQL script `fix_bed_management.sql`: + +**Main fix (Step 2):** +```sql +UPDATE bed_master bm +INNER JOIN ipd_master i ON i.id = bm.current_ipd_id +SET + bm.bed_status = 'available', + bm.current_ipd_id = NULL +WHERE i.ipd_status = 1 +AND bm.current_ipd_id IS NOT NULL; +``` + +This releases all beds for patients who have been discharged. + +### 2. Verify the Fix + +After running the SQL, check the verification queries (Step 4) to confirm: +- All discharged patients' beds show as available +- Occupied bed count matches current admission count +- No beds have status mismatch + +### 3. Monitor Bed Management + +Use these pages to monitor: +- `/setting/admin/bed-status` - View current bed occupancy by ward/floor +- `/setting/admin/bed-management` - Manage bed master data +- `/billing/ipd/current-admission` - View patients with their assigned beds + +## Testing Checklist + +1. **Test Bed Assignment**: + - Admit a new patient + - Assign a bed + - Verify `bed_master.bed_status = 'occupied'` + - Verify `bed_master.current_ipd_id` = IPD ID + +2. **Test Discharge**: + - Discharge a patient + - Verify `bed_master.bed_status = 'available'` + - Verify `bed_master.current_ipd_id IS NULL` + +3. **Test Current Admission View**: + - Open `/billing/ipd/current-admission` + - Verify "Bed No [Type]" column shows bed information + - Verify all admitted patients show their bed assignments + +4. **Test Bed Status Panel**: + - Open `/setting/admin/bed-status` + - Verify Available count is correct + - Verify Occupied count matches current admissions + - Click on occupied beds to see patient details + +## Files Modified + +1. **app/Models/IpdModel.php** + - Updated `releaseBed()` method - releases beds on discharge + - Updated `bedAssign()` method - sets current_ipd_id when assigning + +2. **app/Controllers/Billing/Ipd.php** + - Modified `updateDischargeStatus()` to call bed release + +3. **fix_bed_management.sql** + - SQL script to fix existing data (discharged patients' beds) + +4. **BED_MANAGEMENT_FIX_SUMMARY.md** + - This documentation file + +## Database Schema Reference + +### Tables Used + +1. **bed_master** (Primary table - NEW system) + - `bed_status`: 'available' | 'occupied' | 'maintenance' | 'blocked' | 'reserved' | 'cleaning' + - `current_ipd_id`: IPD ID using this bed (NULL = available) + - `bed_number`: Bed identifier + - `bed_code`: Unique code + - `ward_id`: Reference to ward_master + - `status`: 'active' | 'inactive' + +2. **bed_assignment_history** (Optional tracking table) + - Records all bed assignments with dates + - `released_date`: When bed was freed + - `release_reason`: Why bed was freed + - Provides audit trail for bed usage + +3. **ipd_master** (Patient admission records) + - `ipd_status`: 0=admitted, 1=discharged + - `register_date`: Admission date + - `discharge_date`: Discharge date + +4. **ward_master** (Ward/room information) + - `ward_name`: Ward/room name + - `floor_number`: Floor level + - `ward_type`: Type of ward + +## How Bed Management Works Now + +### Bed Assignment Flow: +1. Patient admitted to IPD +2. Bed selected from available beds +3. `IpdModel::bedAssign()` called: + - Creates entry in `bed_assignment_history` + - Sets `bed_master.current_ipd_id` = IPD ID + - Sets `bed_master.bed_status` = 'occupied' + +### Bed Release Flow: +1. Patient discharged (ipd_status = 1) +2. `IpdModel::releaseBed()` called automatically: + - Sets `bed_master.current_ipd_id` = NULL + - Sets `bed_master.bed_status` = 'available' + - Updates `bed_assignment_history.released_date` + +### Bed Status Dashboard Query: +```sql +SELECT b.*, i.ipd_code, i.register_date, p.p_fname +FROM bed_master b +LEFT JOIN ipd_master i ON i.id = b.current_ipd_id AND i.ipd_status = 0 +LEFT JOIN patient_master p ON p.id = i.p_id +WHERE b.status = 'active' +``` + +## Implementation Notes + +- Legacy `hc_bed_master` support kept for backward compatibility (optional) +- Main system now uses `bed_master` exclusively +- `current_ipd_id` is the key field linking beds to patients +- Bed status transitions: available → occupied → available + +## Support + +If issues persist after running the fix: +1. Check `ipd_master.ipd_status` (0=admitted, 1=discharged) +2. Check `bed_master.current_ipd_id` values (should be NULL for available beds) +3. Check `bed_master.bed_status` values (should match occupancy) +4. Review application logs for errors in IpdModel methods +5. Run the verification queries from Step 4 in the SQL script +6. Check `/setting/admin/bed-status` dashboard for accurate counts + +## Quick Troubleshooting + +**Problem**: Beds still show occupied after discharge +- **Solution**: Run Step 2 of fix_bed_management.sql + +**Problem**: "Bed Assign" column is blank +- **Solution**: Ensure bed_master.current_ipd_id is set during admission + +**Problem**: Bed count mismatch +- **Solution**: Run verification queries (Step 4) to identify discrepancies + +**Problem**: Cannot assign bed (shows as occupied) +- **Solution**: Check if current_ipd_id is stuck with old IPD - run fix SQL + +## Contact + +For additional support, check: +- Application logs in `writable/logs/` +- Database for constraint violations +- CI4 debug toolbar for query errors diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 83bc903..67f8984 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -531,6 +531,7 @@ $routes->post('ipd/patient/history/save/(:num)', 'IpdPatient::saveAdmissionHisto $routes->post('ipd/patient/nursing/scan/(:num)', 'IpdPatient::scanNursingPaper/$1'); $routes->post('ipd/patient/nursing/scan-save/(:num)', 'IpdPatient::saveScannedNursingEntries/$1'); $routes->get('ipd/patient/nursing/print/(:num)', 'IpdPatient::nursingChartPrint/$1'); +$routes->post('ipd/patient/bed/transfer/(:num)', 'IpdPatient::bedTransfer/$1'); $routes->get('nursing-bedside-items', 'Setting\\NursingBedsideItems::index'); $routes->get('nursing-bedside-items/(:num)', 'Setting\\NursingBedsideItems::get/$1'); diff --git a/app/Controllers/Billing/Ipd.php b/app/Controllers/Billing/Ipd.php index 771e044..84b1454 100644 --- a/app/Controllers/Billing/Ipd.php +++ b/app/Controllers/Billing/Ipd.php @@ -871,6 +871,11 @@ class Ipd extends BaseController $this->ipdEditModel->updateIpd($update, $ipdId); $this->ipdEditModel->calculateIPD($ipdId); + // Release bed when discharging patient + if (isset($update['ipd_status']) && $update['ipd_status'] == 1) { + $this->ipdEditModel->releaseBed($ipdId); + } + return $this->buildDischargeProcessResponse($ipdId, 'Discharge status updated.'); } diff --git a/app/Controllers/IpdPatient.php b/app/Controllers/IpdPatient.php index c98981a..06967b5 100644 --- a/app/Controllers/IpdPatient.php +++ b/app/Controllers/IpdPatient.php @@ -3,7 +3,10 @@ namespace App\Controllers; use App\Libraries\NursingScanExtractionService; +use App\Models\BedAssignmentHistoryModel; +use App\Models\BedMasterModel; use App\Models\IpdBillingModel; +use App\Models\IpdModel; use App\Models\IpdNursingEntryModel; use App\Models\NursingBedsideItemModel; @@ -13,6 +16,9 @@ class IpdPatient extends BaseController protected IpdNursingEntryModel $ipdNursingEntryModel; protected NursingBedsideItemModel $nursingBedsideItemModel; protected NursingScanExtractionService $nursingScanExtractionService; + protected BedMasterModel $bedMasterModel; + protected BedAssignmentHistoryModel $bedAssignmentModel; + protected IpdModel $ipdModel; public function __construct() { @@ -20,6 +26,9 @@ class IpdPatient extends BaseController $this->ipdNursingEntryModel = new IpdNursingEntryModel(); $this->nursingBedsideItemModel = new NursingBedsideItemModel(); $this->nursingScanExtractionService = new NursingScanExtractionService(); + $this->bedMasterModel = new BedMasterModel(); + $this->bedAssignmentModel = new BedAssignmentHistoryModel(); + $this->ipdModel = new IpdModel(); helper(['common', 'form', 'age']); } @@ -85,6 +94,18 @@ class IpdPatient extends BaseController $opdHistorySnapshot = $this->getLatestOpdHistorySnapshot($patientId); $isFemalePatient = strtolower((string) ($person->xgender ?? '')) === 'female'; + // Get current bed assignment with ward info + $currentBed = $this->bedMasterModel + ->select('bed_master.*, ward_master.ward_name') + ->join('ward_master', 'ward_master.id = bed_master.ward_id', 'left') + ->where('bed_master.current_ipd_id', $ipdId) + ->first(); + + // Get available beds for transfer + $availableBeds = $this->bedMasterModel->getAvailableBedsGroupedByWard(); + // Get bed assignment history + $bedHistory = $this->bedAssignmentModel->getByIpd($ipdId); + return view('ipd_nursing/workspace', [ 'ipd_info' => $panelData['ipd_info'] ?? null, 'person_info' => $person, @@ -98,6 +119,9 @@ class IpdPatient extends BaseController 'patient_history_row' => $patientHistoryRow, 'opd_history_snapshot' => $opdHistorySnapshot, 'is_female_patient' => $isFemalePatient, + 'current_bed' => $currentBed, + 'available_beds' => $availableBeds, + 'bed_history' => $bedHistory, ]); } @@ -822,4 +846,69 @@ class IpdPatient extends BaseController return trim(implode(PHP_EOL, $lines)); } + + public function bedTransfer(int $ipdId) + { + $permission = $this->requireAnyPermission([ + 'billing.access', + 'billing.ipd.current-admission', + 'billing.ipd.invoice', + ]); + if ($permission) { + return $permission; + } + + if (!$this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']); + } + + $newBedId = (int) $this->request->getPost('bed_id'); + $transferReason = trim((string) $this->request->getPost('transfer_reason')); + $remarks = trim((string) $this->request->getPost('remarks')); + $assignmentDateTime = trim((string) $this->request->getPost('assignment_datetime')); + + if ($newBedId <= 0) { + return $this->response->setJSON(['status' => 0, 'message' => 'Please select a bed']); + } + + // Validate and format assignment date/time + if (empty($assignmentDateTime)) { + $assignmentDateTime = date('Y-m-d H:i:s'); + } else { + // Convert from HTML5 datetime-local format to MySQL datetime + $assignmentDateTime = date('Y-m-d H:i:s', strtotime($assignmentDateTime)); + } + + try { + // Get current bed assignment + $currentBed = $this->bedMasterModel->where('current_ipd_id', $ipdId)->first(); + + // Check if new bed is available + $newBed = $this->bedMasterModel->find($newBedId); + if (!$newBed || $newBed->bed_status !== 'available' || $newBed->current_ipd_id !== null) { + return $this->response->setJSON(['status' => 0, 'message' => 'Selected bed is not available']); + } + + // Transfer bed + $this->ipdModel->bedAssign([ + 'ipd_id' => $ipdId, + 'bed_id' => $newBedId, + 'Fdate' => $assignmentDateTime, + 'TDate' => $assignmentDateTime, + 'transfer_reason' => $transferReason, + 'transfer_from_bed_id' => $currentBed ? $currentBed->id : null, + 'remarks' => $remarks, + 'assignment_type' => 'transfer', + ]); + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'Bed transferred successfully', + 'reload' => true, + ]); + } catch (\Throwable $e) { + log_message('error', 'Bed transfer failed for IPD #' . $ipdId . ': ' . $e->getMessage()); + return $this->response->setJSON(['status' => 0, 'message' => 'Bed transfer failed: ' . $e->getMessage()]); + } + } } diff --git a/app/Models/BedAssignmentHistoryModel.php b/app/Models/BedAssignmentHistoryModel.php index 07ce185..56ead63 100644 --- a/app/Models/BedAssignmentHistoryModel.php +++ b/app/Models/BedAssignmentHistoryModel.php @@ -30,16 +30,43 @@ class BedAssignmentHistoryModel extends Model ->join('bed_master', 'bed_master.id = bed_assignment_history.bed_id', 'left') ->join('ward_master', 'ward_master.id = bed_assignment_history.ward_id', 'left') ->orderBy('bed_assignment_history.id', 'DESC') - ->findAll(); + ->get() + ->getResult(); } public function getByIpd(int $ipdId): array { - return $this->select('bed_assignment_history.*, bed_master.bed_code, bed_master.bed_number, ward_master.ward_name') + $results = $this->select('bed_assignment_history.*, bed_master.bed_code, bed_master.bed_number, ward_master.ward_name') ->join('bed_master', 'bed_master.id = bed_assignment_history.bed_id', 'left') ->join('ward_master', 'ward_master.id = bed_assignment_history.ward_id', 'left') ->where('bed_assignment_history.ipd_id', $ipdId) ->orderBy('bed_assignment_history.id', 'DESC') - ->findAll(); + ->get() + ->getResult(); + + // Calculate duration for each record + foreach ($results as $record) { + $record->duration_hours = $this->calculateDurationHours($record->assigned_date, $record->released_date); + } + + return $results; + } + + /** + * Calculate duration in hours between two dates + */ + private function calculateDurationHours(?string $startDate, ?string $endDate): ?float + { + if (empty($startDate)) { + return null; + } + + $start = new \DateTime($startDate); + $end = $endDate ? new \DateTime($endDate) : new \DateTime(); + + $interval = $start->diff($end); + $hours = ($interval->days * 24) + $interval->h + ($interval->i / 60); + + return round($hours, 1); } } diff --git a/app/Models/BedMasterModel.php b/app/Models/BedMasterModel.php index 10be772..b488d34 100644 --- a/app/Models/BedMasterModel.php +++ b/app/Models/BedMasterModel.php @@ -35,4 +35,28 @@ class BedMasterModel extends Model ->orderBy('bed_master.id', 'DESC') ->findAll(); } + + public function getAvailableBedsGroupedByWard(): array + { + $beds = $this->select('bed_master.*, ward_master.ward_name, bed_category_master.category_name') + ->join('ward_master', 'ward_master.id = bed_master.ward_id', 'left') + ->join('bed_category_master', 'bed_category_master.id = bed_master.bed_category_id', 'left') + ->where('bed_master.status', 'active') + ->where('bed_master.bed_status', 'available') + ->where('bed_master.current_ipd_id IS NULL') + ->orderBy('ward_master.ward_name', 'ASC') + ->orderBy('bed_master.bed_number', 'ASC') + ->findAll(); + + $grouped = []; + foreach ($beds as $bed) { + $wardName = $bed->ward_name ?? 'Unassigned'; + if (!isset($grouped[$wardName])) { + $grouped[$wardName] = []; + } + $grouped[$wardName][] = $bed; + } + + return $grouped; + } } diff --git a/app/Models/IpdModel.php b/app/Models/IpdModel.php index ddf0a36..73808c4 100644 --- a/app/Models/IpdModel.php +++ b/app/Models/IpdModel.php @@ -193,15 +193,40 @@ class IpdModel extends Model $assignedBy = (int) ($this->getUserIdentity()['id'] ?? 0); - $inserted = $this->db->table('bed_assignment_history')->insert([ + // Prepare insert data with optional fields + $insertData = [ 'ipd_id' => $ipdId, 'bed_id' => $bedId, - 'assignment_type' => 'admission', + 'assignment_type' => $data['assignment_type'] ?? 'admission', 'assigned_date' => $fromDate, 'discharged_date' => $toDate, 'assigned_by' => $assignedBy > 0 ? $assignedBy : null, 'ward_id' => $wardId, - ]); + ]; + + // Add transfer-specific fields if provided + if (isset($data['transfer_reason']) && !empty($data['transfer_reason'])) { + $insertData['transfer_reason'] = $data['transfer_reason']; + } + if (isset($data['transfer_from_bed_id']) && $data['transfer_from_bed_id'] > 0) { + $insertData['transfer_from_bed_id'] = (int) $data['transfer_from_bed_id']; + } + if (isset($data['remarks']) && !empty($data['remarks'])) { + $insertData['remarks'] = $data['remarks']; + } + + // If this is a transfer, mark previous bed assignment as released + if (($data['assignment_type'] ?? 'admission') === 'transfer') { + $this->db->table('bed_assignment_history') + ->where('ipd_id', $ipdId) + ->where('released_date IS NULL') + ->update([ + 'released_date' => $fromDate, + 'release_reason' => 'Transferred to another bed', + ]); + } + + $inserted = $this->db->table('bed_assignment_history')->insert($insertData); if (! $inserted) { return 0; @@ -225,6 +250,37 @@ class IpdModel extends Model return 0; } + // Update bed_master table (new system) + if ($this->db->tableExists('bed_master')) { + $bedFields = $this->db->getFieldNames('bed_master') ?? []; + + // Release any previous bed assigned to this IPD + if (in_array('current_ipd_id', $bedFields) && in_array('bed_status', $bedFields)) { + $this->db->table('bed_master') + ->where('current_ipd_id', $ipdId) + ->update([ + 'current_ipd_id' => null, + 'bed_status' => 'available' + ]); + } + + // Assign new bed + $bedUpdate = []; + if (in_array('bed_status', $bedFields)) { + $bedUpdate['bed_status'] = 'occupied'; + } + if (in_array('current_ipd_id', $bedFields)) { + $bedUpdate['current_ipd_id'] = $ipdId; + } + + if (!empty($bedUpdate)) { + $this->db->table('bed_master') + ->where('id', $bedId) + ->update($bedUpdate); + } + } + + // Legacy hc_bed_master support (if needed for backward compatibility) if ($this->db->tableExists('hc_bed_master') && in_array('bed_used_p_id', $this->db->getFieldNames('hc_bed_master') ?? [], true)) { $this->db->table('hc_bed_master') ->where('bed_used_p_id', $ipdId) @@ -235,13 +291,51 @@ class IpdModel extends Model ->update(['bed_used_p_id' => $ipdId]); } - if ($this->db->tableExists('bed_master') && in_array('bed_status', $this->db->getFieldNames('bed_master') ?? [], true)) { - $this->db->table('bed_master') - ->where('id', $bedId) - ->update(['bed_status' => 'occupied']); + return $insertId; + } + + public function releaseBed(int $ipdId): void + { + if ($ipdId <= 0) { + return; } - return $insertId; + // Release bed in bed_master table (new system) + if ($this->db->tableExists('bed_master')) { + $bedFields = $this->db->getFieldNames('bed_master') ?? []; + $bedUpdate = []; + + if (in_array('bed_status', $bedFields)) { + $bedUpdate['bed_status'] = 'available'; + } + if (in_array('current_ipd_id', $bedFields)) { + $bedUpdate['current_ipd_id'] = null; + } + + if (!empty($bedUpdate)) { + $this->db->table('bed_master') + ->where('current_ipd_id', $ipdId) + ->update($bedUpdate); + } + } + + // Legacy hc_bed_master support (if needed for backward compatibility) + if ($this->db->tableExists('hc_bed_master') && in_array('bed_used_p_id', $this->db->getFieldNames('hc_bed_master') ?? [], true)) { + $this->db->table('hc_bed_master') + ->where('bed_used_p_id', $ipdId) + ->update(['bed_used_p_id' => 0]); + } + + // Update bed_assignment_history to mark beds as released + if ($this->db->tableExists('bed_assignment_history') && in_array('released_date', $this->db->getFieldNames('bed_assignment_history') ?? [], true)) { + $this->db->table('bed_assignment_history') + ->where('ipd_id', $ipdId) + ->where('released_date IS NULL') + ->update([ + 'released_date' => date('Y-m-d H:i:s'), + 'release_reason' => 'Patient Discharged', + ]); + } } public function calculateIPD(int $ipdNo): void diff --git a/app/Views/ipd_nursing/workspace.php b/app/Views/ipd_nursing/workspace.php index 72b3b1e..6cc5996 100644 --- a/app/Views/ipd_nursing/workspace.php +++ b/app/Views/ipd_nursing/workspace.php @@ -199,9 +199,175 @@ ksort($nurseNames);
| Bed | +Ward | +Assigned Date | +Released Date | +Duration | +Type | +Remarks | +
|---|---|---|---|---|---|---|
| = esc($history->bed_number ?? '') ?> (= esc($history->bed_code ?? '') ?>) | += esc($history->ward_name ?? '') ?> | += esc($history->assigned_date ?? '') ?> | ++ released_date): ?> + = esc($history->released_date) ?> + + Current + + | ++ duration_hours) && $history->duration_hours !== null): ?> + duration_hours); + $minutes = round(($history->duration_hours - $hours) * 60); + ?> + = $hours ?>h = $minutes ?>m + released_date): ?> + (ongoing) + + + - + + | += esc($history->assignment_type ?? 'admission') ?> | += esc($history->remarks ?? '') ?> | +
| No bed assignment history found | +||||||