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); +
+ + +
+ + +
+
+
Current Bed Assignment
+
+
+ +
+
+ Bed Number: bed_number) ?> +
+
+ Bed Code: bed_code) ?> +
+
+ Ward: ward_name ?? 'N/A') ?> +
+
+ Status: + bed_status) ?> +
+
+ +
+ No bed currently assigned to this patient. +
+ +
+
+ +
+
+
Transfer to New Bed
+
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ + + Time when bed assignment takes effect +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+
Bed Assignment History
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BedWardAssigned DateReleased DateDurationTypeRemarks
bed_number ?? '') ?> (bed_code ?? '') ?>)ward_name ?? '') ?>assigned_date ?? '') ?> + released_date): ?> + released_date) ?> + + Current + + + duration_hours) && $history->duration_hours !== null): ?> + duration_hours); + $minutes = round(($history->duration_hours - $hours) * 60); + ?> + h m + released_date): ?> + (ongoing) + + + - + + assignment_type ?? 'admission') ?>remarks ?? '') ?>
No bed assignment history found
+
+
+
+
+
@@ -1438,5 +1604,52 @@ ksort($nurseNames); nursingRefreshBedsideItems(); nursingRefreshDoctorVisitTypes(); nursingRefreshDoctorVisitRate(); + + // Bed Transfer Form Handler + var bedTransferForm = document.getElementById('bed_transfer_form'); + if (bedTransferForm) { + bedTransferForm.addEventListener('submit', function (event) { + event.preventDefault(); + + var bedId = bedTransferForm.querySelector('[name="bed_id"]').value; + var transferReason = bedTransferForm.querySelector('[name="transfer_reason"]').value; + + if (!bedId) { + notify('error', 'Bed Transfer', 'Please select a bed'); + return; + } + + if (!transferReason) { + notify('error', 'Bed Transfer', 'Please select a transfer reason'); + return; + } + + var formData = new window.FormData(bedTransferForm); + + $.ajax({ + url: '', + type: 'POST', + data: formData, + processData: false, + contentType: false, + dataType: 'json' + }).done(function (resp) { + if (!resp || String(resp.status) !== '1') { + notify('error', 'Bed Transfer', (resp && resp.message) ? resp.message : 'Bed transfer failed'); + return; + } + + notify('success', 'Bed Transfer', resp.message || 'Bed transferred successfully'); + + if (resp.reload) { + setTimeout(function () { + window.location.reload(); + }, 1500); + } + }).fail(function () { + notify('error', 'Bed Transfer', 'Failed to transfer bed. Please try again.'); + }); + }); + } })(); diff --git a/fix_bed_management.sql b/fix_bed_management.sql new file mode 100644 index 0000000..e732c88 --- /dev/null +++ b/fix_bed_management.sql @@ -0,0 +1,179 @@ +-- ===================================================== +-- Bed Management Fix SQL Scripts +-- For systems using bed_master table (NEW CI4 system) +-- Run these to fix existing data issues +-- ===================================================== +-- +-- IMPORTANT NOTES: +-- 1. This script is for bed_master table (NEW system) +-- 2. bed_master columns: bed_number, bed_code, ward_id, bed_status, current_ipd_id +-- 3. bed_status values: 'available', 'occupied', 'maintenance', 'blocked', 'reserved', 'cleaning' +-- 4. current_ipd_id: stores the IPD ID when bed is occupied (NULL when available) +-- 5. Always backup your database before running! +-- ===================================================== + +-- ===================================================== +-- Step 1: NOT NEEDED for bed_master system +-- ===================================================== +-- The bed_master table tracks current occupancy via current_ipd_id column +-- No historical backfill needed - just release discharged patients' beds in Step 2 + +-- ===================================================== +-- Step 2: Release beds for discharged patients +-- ===================================================== +-- This is the MAIN FIX - releases beds when patients are discharged + +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; + +-- ===================================================== +-- Step 2b: Fix phantom occupied beds +-- ===================================================== +-- This fixes beds marked as 'occupied' but with no patient assigned +-- These are "phantom" occupied beds that block bed availability + +UPDATE bed_master +SET bed_status = 'available' +WHERE bed_status = 'occupied' +AND current_ipd_id IS NULL; + +-- ===================================================== +-- Step 2c: Sync bed_assignment_history to bed_master.current_ipd_id +-- ===================================================== +-- This syncs active bed assignments from history table to bed_master +-- Fixes cases where bed was assigned in history but current_ipd_id wasn't set + +UPDATE bed_master bm +INNER JOIN bed_assignment_history bah ON bah.bed_id = bm.id +INNER JOIN ipd_master i ON i.id = bah.ipd_id +SET + bm.current_ipd_id = bah.ipd_id, + bm.bed_status = 'occupied' +WHERE i.ipd_status = 0 +AND bah.released_date IS NULL +AND bm.current_ipd_id IS NULL; + +-- ===================================================== +-- Step 3: Update bed_assignment_history (if you use it) +-- ===================================================== +-- This marks bed assignments as released in the history table + +UPDATE bed_assignment_history bah +INNER JOIN ipd_master i ON i.id = bah.ipd_id +SET + bah.released_date = COALESCE(i.discharge_date, NOW()), + bah.release_reason = 'Patient Discharged (Data Fix)' +WHERE i.ipd_status = 1 +AND bah.released_date IS NULL; + +-- ===================================================== +-- Step 4: Verification Queries +-- ===================================================== + +-- 4a. Check beds still marked as occupied for discharged patients +SELECT + bm.id, + bm.bed_number, + bm.bed_code, + bm.bed_status, + bm.current_ipd_id, + i.ipd_code, + i.ipd_status, + w.ward_name, + DATE_FORMAT(i.discharge_date, '%d-%m-%Y') as discharge_date +FROM bed_master bm +INNER JOIN ipd_master i ON i.id = bm.current_ipd_id +LEFT JOIN ward_master w ON w.id = bm.ward_id +WHERE i.ipd_status = 1 +AND bm.current_ipd_id IS NOT NULL; + +-- Expected: Should return 0 rows after fix + +-- 4b. Check currently admitted patients with their beds +SELECT + i.id as ipd_id, + i.ipd_code, + i.ipd_status, + bm.bed_number, + bm.bed_code, + bm.bed_status, + w.ward_name +FROM ipd_master i +LEFT JOIN bed_master bm ON bm.current_ipd_id = i.id +LEFT JOIN ward_master w ON w.id = bm.ward_id +WHERE i.ipd_status = 0 +ORDER BY i.ipd_code; + +-- Expected: All active IPDs should show their assigned beds + +-- 4c. Check current bed status summary +SELECT + COUNT(CASE WHEN bed_status = 'available' THEN 1 END) as available_beds, + COUNT(CASE WHEN bed_status = 'occupied' THEN 1 END) as occupied_beds, + COUNT(CASE WHEN bed_status = 'maintenance' THEN 1 END) as maintenance_beds, + COUNT(CASE WHEN bed_status = 'blocked' THEN 1 END) as blocked_beds, + COUNT(CASE WHEN bed_status = 'reserved' THEN 1 END) as reserved_beds, + COUNT(CASE WHEN bed_status = 'cleaning' THEN 1 END) as cleaning_beds, + COUNT(*) as total_beds +FROM bed_master +WHERE status = 'active'; + +-- 4d. Compare with current admissions count +SELECT COUNT(*) as current_admissions +FROM ipd_master +WHERE ipd_status = 0; + +-- The occupied_beds count should match current_admissions count + +-- 4e. Find beds with status mismatch (occupied but no IPD assigned) +SELECT + bm.id, + bm.bed_number, + bm.bed_code, + bm.bed_status, + bm.current_ipd_id, + w.ward_name +FROM bed_master bm +LEFT JOIN ward_master w ON w.id = bm.ward_id +WHERE bm.bed_status = 'occupied' +AND bm.current_ipd_id IS NULL; + +-- Expected: Should return 0 rows (all occupied beds should have current_ipd_id) + +-- ===================================================== +-- Step 5: Optional - Clean up orphaned bed assignments +-- ===================================================== +-- This removes bed_assignment_history records for IPDs that don't exist +-- Only run if you use bed_assignment_history table + +DELETE bah FROM bed_assignment_history bah +LEFT JOIN ipd_master i ON i.id = bah.ipd_id +WHERE i.id IS NULL; + +-- ===================================================== +-- NOTES: +-- ===================================================== +-- 1. Always backup your database before running these scripts +-- 2. Run Step 4 verification queries before and after to see the improvement +-- 3. Step 2 is the main fix - it releases beds for discharged patients +-- 4. These scripts are safe to run multiple times (idempotent) +-- 5. Monitor the application after running to ensure no issues +-- 6. For bed_master system: focuses on bed_status and current_ipd_id columns +-- 7. If you don't use bed_assignment_history, skip Steps 3 and 5 +-- +-- WHAT THIS FIXES: +-- - Beds marked as 'occupied' for patients who have been discharged +-- - current_ipd_id not cleared when patients leave +-- - Bed count mismatches in bed status dashboard +-- - "Bed Assign" column showing blank because current_ipd_id not set properly +-- +-- AFTER RUNNING: +-- - All discharged patients' beds will be marked as 'available' +-- - current_ipd_id will be NULL for available beds +-- - Bed counts in dashboard will match actual occupancy +-- - New bed assignments will work correctly with CI4 code diff --git a/run_bed_fix.php b/run_bed_fix.php new file mode 100644 index 0000000..f100763 --- /dev/null +++ b/run_bed_fix.php @@ -0,0 +1,189 @@ +connect_error) { + die("Connection failed: " . $db->connect_error); +} + +echo "==============================================\n"; +echo "Bed Management Fix Script\n"; +echo "==============================================\n\n"; + +// Step 0: Check current state BEFORE fix +echo "BEFORE FIX - Current State:\n"; +echo "----------------------------\n"; + +$query = " + SELECT + COUNT(CASE WHEN bed_status = 'available' THEN 1 END) as available_beds, + COUNT(CASE WHEN bed_status = 'occupied' THEN 1 END) as occupied_beds, + COUNT(*) as total_beds + FROM bed_master + WHERE status = 'active' +"; +$result = $db->query($query); +$row = $result->fetch_object(); +echo "Total Beds: {$row->total_beds}\n"; +echo "Available: {$row->available_beds}\n"; +echo "Occupied: {$row->occupied_beds}\n"; + +$query = "SELECT COUNT(*) as current_admissions FROM ipd_master WHERE ipd_status = 0"; +$result = $db->query($query); +$row = $result->fetch_object(); +echo "Current Admissions: {$row->current_admissions}\n"; + +$query = " + SELECT COUNT(*) as problem_beds + FROM bed_master bm + INNER JOIN ipd_master i ON i.id = bm.current_ipd_id + WHERE i.ipd_status = 1 + AND bm.current_ipd_id IS NOT NULL +"; +$result = $db->query($query); +$row = $result->fetch_object(); +echo "Beds stuck with discharged patients: {$row->problem_beds}\n\n"; + +// Step 1: Release beds for discharged patients (MAIN FIX) +echo "Step 1: Releasing beds for discharged patients...\n"; +$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 +"; +$db->query($sql); +$affected = $db->affected_rows; +echo "✓ Released {$affected} beds\n\n"; + +// Step 1b: Fix phantom occupied beds (occupied status but no patient assigned) +echo "Step 1b: Fixing phantom occupied beds (occupied but no current_ipd_id)...\n"; +$sql = " + UPDATE bed_master + SET bed_status = 'available' + WHERE bed_status = 'occupied' + AND current_ipd_id IS NULL +"; +$db->query($sql); +$affected = $db->affected_rows; +echo "✓ Fixed {$affected} phantom occupied beds\n\n"; + +// Step 1c: Sync bed_assignment_history to bed_master.current_ipd_id +echo "Step 1c: Syncing bed assignments from history to bed_master...\n"; +$sql = " + UPDATE bed_master bm + INNER JOIN bed_assignment_history bah ON bah.bed_id = bm.id + INNER JOIN ipd_master i ON i.id = bah.ipd_id + SET + bm.current_ipd_id = bah.ipd_id, + bm.bed_status = 'occupied' + WHERE i.ipd_status = 0 + AND bah.released_date IS NULL + AND bm.current_ipd_id IS NULL +"; +$db->query($sql); +$affected = $db->affected_rows; +echo "✓ Synced {$affected} bed assignments\n\n"; + +// Step 2: Update bed_assignment_history (if table exists) +$tableCheck = $db->query("SHOW TABLES LIKE 'bed_assignment_history'"); +if ($tableCheck->num_rows > 0) { + echo "Step 2: Updating bed_assignment_history...\n"; + $sql = " + UPDATE bed_assignment_history bah + INNER JOIN ipd_master i ON i.id = bah.ipd_id + SET + bah.released_date = COALESCE(i.discharge_date, NOW()), + bah.release_reason = 'Patient Discharged (Data Fix)' + WHERE i.ipd_status = 1 + AND bah.released_date IS NULL + "; + $db->query($sql); + $affected = $db->affected_rows; + echo "✓ Updated {$affected} history records\n\n"; +} else { + echo "Step 2: bed_assignment_history table not found, skipping...\n\n"; +} + +// Step 3: Verify the fix +echo "AFTER FIX - Verification:\n"; +echo "-------------------------\n"; + +$query = " + SELECT + COUNT(CASE WHEN bed_status = 'available' THEN 1 END) as available_beds, + COUNT(CASE WHEN bed_status = 'occupied' THEN 1 END) as occupied_beds, + COUNT(*) as total_beds + FROM bed_master + WHERE status = 'active' +"; +$result = $db->query($query); +$row = $result->fetch_object(); +echo "Total Beds: {$row->total_beds}\n"; +echo "Available: {$row->available_beds}\n"; +echo "Occupied: {$row->occupied_beds}\n"; + +$query = "SELECT COUNT(*) as current_admissions FROM ipd_master WHERE ipd_status = 0"; +$result = $db->query($query); +$row = $result->fetch_object(); +echo "Current Admissions: {$row->current_admissions}\n"; + +$query = " + SELECT COUNT(*) as problem_beds + FROM bed_master bm + INNER JOIN ipd_master i ON i.id = bm.current_ipd_id + WHERE i.ipd_status = 1 + AND bm.current_ipd_id IS NOT NULL +"; +$result = $db->query($query); +$row = $result->fetch_object(); +echo "Beds stuck with discharged patients: {$row->problem_beds}\n\n"; + +// Check for any mismatches +$query = " + SELECT + bm.id, + bm.bed_number, + bm.bed_code, + bm.bed_status, + bm.current_ipd_id + FROM bed_master bm + LEFT JOIN ward_master w ON w.id = bm.ward_id + WHERE bm.bed_status = 'occupied' + AND bm.current_ipd_id IS NULL + LIMIT 5 +"; +$result = $db->query($query); +if ($result->num_rows > 0) { + echo "⚠ Warning: Found beds marked 'occupied' but no current_ipd_id:\n"; + while ($bed = $result->fetch_assoc()) { + echo " - Bed #{$bed['bed_number']} (ID: {$bed['id']})\n"; + } + echo "\n"; +} else { + echo "✓ No status mismatches found\n\n"; +} + +$db->close(); + +echo "==============================================\n"; +echo "Fix completed successfully!\n"; +echo "==============================================\n"; +echo "\nNext steps:\n"; +echo "1. Check bed status at: /setting/admin/bed-status\n"; +echo "2. Test discharge a patient and verify bed is released\n"; +echo "3. Test admitting a patient and verify bed is assigned\n";