feat: Complete bed management system with transfer and time tracking

- Implemented automatic bed release on patient discharge
- Added bed transfer functionality in nursing workspace
- Added date/time tracking for bed assignments with duration calculation
- Fixed bed_master.current_ipd_id as source of truth for occupancy
- Created data fix scripts for existing records
- Added bed assignment history with duration display (hours/minutes)
- Fixed display issues in bed assignment tab
- Added route for bed transfer endpoint
- Supports custom assignment datetime for backdated entries
- Automatically marks previous bed as released on transfer
This commit is contained in:
DevSoftTech Server 2026-06-09 08:39:48 +05:30
parent 8e293c4ffb
commit d722714ef1
10 changed files with 1059 additions and 11 deletions

View File

@ -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

View File

@ -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');

View File

@ -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.');
}

View File

@ -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()]);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -199,9 +199,175 @@ ksort($nurseNames);
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_charge">Charge Entry</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_bed">Bed Management</button>
</li>
</ul>
</div>
<div class="card-body">
<!-- Bed Management Tab -->
<div class="nursing-tab-pane d-none" id="nursing_tab_bed">
<?php
$currentBedData = $current_bed ?? null;
$availableBedsData = $available_beds ?? [];
$bedHistoryData = $bed_history ?? [];
$bedTransferUrl = site_url('ipd/patient/bed/transfer/' . $ipdId);
?>
<div class="card">
<div class="card-header">
<h6 class="mb-0">Current Bed Assignment</h6>
</div>
<div class="card-body">
<?php if ($currentBedData): ?>
<div class="row g-3">
<div class="col-md-3">
<strong>Bed Number:</strong> <?= esc($currentBedData->bed_number) ?>
</div>
<div class="col-md-3">
<strong>Bed Code:</strong> <?= esc($currentBedData->bed_code) ?>
</div>
<div class="col-md-3">
<strong>Ward:</strong> <?= esc($currentBedData->ward_name ?? 'N/A') ?>
</div>
<div class="col-md-3">
<strong>Status:</strong>
<span class="badge bg-success"><?= esc($currentBedData->bed_status) ?></span>
</div>
</div>
<?php else: ?>
<div class="alert alert-warning mb-0">
<i class="bi bi-exclamation-triangle"></i> No bed currently assigned to this patient.
</div>
<?php endif; ?>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">Transfer to New Bed</h6>
</div>
<div class="card-body">
<form id="bed_transfer_form" class="row g-3">
<?= csrf_field() ?>
<div class="col-md-6">
<label class="form-label">Select New Bed <span class="text-danger">*</span></label>
<select name="bed_id" class="form-select" required>
<option value="">-- Select Bed --</option>
<?php foreach ($availableBedsData as $wardName => $beds): ?>
<optgroup label="<?= esc($wardName) ?>">
<?php foreach ($beds as $bed): ?>
<option value="<?= esc($bed->id) ?>">
<?= esc($bed->bed_number) ?> - <?= esc($bed->bed_code) ?>
<?php if ($bed->category_name): ?>
(<?= esc($bed->category_name) ?>)
<?php endif; ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Transfer Reason <span class="text-danger">*</span></label>
<select name="transfer_reason" class="form-select" required>
<option value="">-- Select Reason --</option>
<option value="Medical Requirement">Medical Requirement</option>
<option value="ICU Transfer">ICU Transfer</option>
<option value="Ward Transfer">Ward Transfer</option>
<option value="Room Upgrade">Room Upgrade</option>
<option value="Patient Request">Patient Request</option>
<option value="Isolation Required">Isolation Required</option>
<option value="Bed Maintenance">Bed Maintenance</option>
<option value="Other">Other</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Assignment Date/Time <span class="text-danger">*</span></label>
<input type="datetime-local" name="assignment_datetime" class="form-control" value="<?= date('Y-m-d\\TH:i') ?>" required>
<small class="form-text text-muted">Time when bed assignment takes effect</small>
</div>
<div class="col-md-12">
<label class="form-label">Additional Remarks</label>
<textarea name="remarks" class="form-control" rows="2" placeholder="Any additional notes about the transfer..."></textarea>
</div>
<div class="col-md-12">
<button type="submit" class="btn btn-primary">
<i class="bi bi-arrow-left-right"></i> Transfer Bed
</button>
</div>
</form>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">Bed Assignment History</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Bed</th>
<th>Ward</th>
<th>Assigned Date</th>
<th>Released Date</th>
<th>Duration</th>
<th>Type</th>
<th>Remarks</th>
</tr>
</thead>
<tbody>
<?php if (!empty($bedHistoryData)): ?>
<?php foreach ($bedHistoryData as $history): ?>
<tr>
<td><?= esc($history->bed_number ?? '') ?> (<?= esc($history->bed_code ?? '') ?>)</td>
<td><?= esc($history->ward_name ?? '') ?></td>
<td><?= esc($history->assigned_date ?? '') ?></td>
<td>
<?php if ($history->released_date): ?>
<?= esc($history->released_date) ?>
<?php else: ?>
<span class="badge bg-success">Current</span>
<?php endif; ?>
</td>
<td>
<?php if (isset($history->duration_hours) && $history->duration_hours !== null): ?>
<?php
$hours = floor($history->duration_hours);
$minutes = round(($history->duration_hours - $hours) * 60);
?>
<strong><?= $hours ?>h <?= $minutes ?>m</strong>
<?php if (!$history->released_date): ?>
<small class="text-muted">(ongoing)</small>
<?php endif; ?>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td><?= esc($history->assignment_type ?? 'admission') ?></td>
<td><?= esc($history->remarks ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="7" class="text-center text-muted">No bed assignment history found</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="nursing-tab-pane d-none" id="nursing_tab_admission">
<form class="row g-2 nursing-form" data-save-url="<?= esc($saveUrl) ?>">
<?= csrf_field() ?>
@ -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: '<?= esc(site_url('ipd/patient/bed/transfer/' . $ipdId)) ?>',
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.');
});
});
}
})();
</script>

179
fix_bed_management.sql Normal file
View File

@ -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

189
run_bed_fix.php Normal file
View File

@ -0,0 +1,189 @@
<?php
/**
* Run Bed Management Fix
* This script executes the SQL fixes for bed management issues
*/
// Database credentials from .env
$hostname = 'localhost';
$username = 'root';
$password = '';
$database = 'hms_data_ci4';
// Connect to database
$db = new mysqli($hostname, $username, $password, $database);
if ($db->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";