Patients can be admitted, transferred, and discharged from unit/bed stays with occupancy sync, so MAR and ward boards have inpatient context. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,7 +6,9 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\CareUnit;
|
||||
use App\Models\Department;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\AuditLogger;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -18,7 +20,7 @@ class CareUnitController extends Controller
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.view');
|
||||
$this->authorizeCareUnitView($request);
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
@@ -98,18 +100,63 @@ class CareUnitController extends Controller
|
||||
|
||||
public function show(Request $request, CareUnit $careUnit): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.view');
|
||||
$this->authorizeCareUnitView($request);
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
$organization = $this->organization($request);
|
||||
abort_unless((int) $careUnit->organization_id === (int) $organization->id, 404);
|
||||
|
||||
$careUnit->load(['department.branch', 'beds']);
|
||||
|
||||
$placedVisits = Visit::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('care_unit_id', $careUnit->id)
|
||||
->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS])
|
||||
->with(['patient', 'bed'])
|
||||
->orderBy('placed_at')
|
||||
->get();
|
||||
|
||||
$placeableVisits = Visit::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('branch_id', $careUnit->department?->branch_id)
|
||||
->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS])
|
||||
->where(function ($q) use ($careUnit) {
|
||||
$q->whereNull('care_unit_id')
|
||||
->orWhere('care_unit_id', '!=', $careUnit->id);
|
||||
})
|
||||
->with(['patient', 'careUnit'])
|
||||
->orderByDesc('checked_in_at')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
$availableBeds = $careUnit->beds
|
||||
->where('is_active', true)
|
||||
->whereIn('status', ['available', 'reserved'])
|
||||
->values();
|
||||
|
||||
$canPlace = app(CarePermissions::class)->can($this->member($request), 'inpatient.placement.manage');
|
||||
|
||||
return view('care.admin.care-units.show', [
|
||||
'unit' => $careUnit,
|
||||
'kinds' => config('care.care_unit_kinds'),
|
||||
'bedStatuses' => config('care.bed_statuses'),
|
||||
'placedVisits' => $placedVisits,
|
||||
'placeableVisits' => $placeableVisits,
|
||||
'availableBeds' => $availableBeds,
|
||||
'canPlace' => $canPlace,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function authorizeCareUnitView(Request $request): void
|
||||
{
|
||||
$permissions = app(CarePermissions::class);
|
||||
$member = $this->member($request);
|
||||
abort_unless(
|
||||
$permissions->can($member, 'admin.departments.view')
|
||||
|| $permissions->can($member, 'inpatient.placement.view'),
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
public function edit(Request $request, CareUnit $careUnit): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Bed;
|
||||
use App\Models\CareUnit;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\VisitPlacementService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VisitPlacementController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function store(Request $request, CareUnit $careUnit, VisitPlacementService $placements): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'inpatient.placement.manage');
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
$organization = $this->organization($request);
|
||||
abort_unless((int) $careUnit->organization_id === (int) $organization->id, 404);
|
||||
|
||||
$validated = $request->validate([
|
||||
'visit_id' => ['required', 'integer', 'exists:care_visits,id'],
|
||||
'bed_id' => ['nullable', 'integer', 'exists:care_beds,id'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$visit = Visit::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->findOrFail($validated['visit_id']);
|
||||
$this->authorizeBranch($request, (int) $visit->branch_id);
|
||||
|
||||
$bed = null;
|
||||
if (! empty($validated['bed_id'])) {
|
||||
$bed = Bed::owned($this->ownerRef($request))
|
||||
->where('care_unit_id', $careUnit->id)
|
||||
->findOrFail($validated['bed_id']);
|
||||
}
|
||||
|
||||
$placements->place(
|
||||
$visit,
|
||||
$careUnit,
|
||||
$bed,
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
$validated['notes'] ?? null,
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('care.care-units.show', $careUnit)
|
||||
->with('success', 'Patient placed on this care unit.');
|
||||
}
|
||||
|
||||
public function transfer(Request $request, Visit $visit, VisitPlacementService $placements): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'inpatient.placement.manage');
|
||||
$this->authorizeOwner($request, $visit);
|
||||
$this->authorizeBranch($request, (int) $visit->branch_id);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'care_unit_id' => ['required', 'integer', 'exists:care_care_units,id'],
|
||||
'bed_id' => ['nullable', 'integer', 'exists:care_beds,id'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$unit = CareUnit::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->findOrFail($validated['care_unit_id']);
|
||||
|
||||
$bed = null;
|
||||
if (! empty($validated['bed_id'])) {
|
||||
$bed = Bed::owned($this->ownerRef($request))
|
||||
->where('care_unit_id', $unit->id)
|
||||
->findOrFail($validated['bed_id']);
|
||||
}
|
||||
|
||||
$placements->transfer(
|
||||
$visit,
|
||||
$unit,
|
||||
$bed,
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
$validated['notes'] ?? null,
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('care.care-units.show', $unit)
|
||||
->with('success', 'Patient transferred.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Visit $visit, VisitPlacementService $placements): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'inpatient.placement.manage');
|
||||
$this->authorizeOwner($request, $visit);
|
||||
$this->authorizeBranch($request, (int) $visit->branch_id);
|
||||
|
||||
$unitId = $visit->care_unit_id;
|
||||
|
||||
$placements->dischargePlacement(
|
||||
$visit,
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
$request->input('notes'),
|
||||
);
|
||||
|
||||
if ($unitId) {
|
||||
return redirect()
|
||||
->route('care.care-units.show', $unitId)
|
||||
->with('success', 'Placement discharged; bed freed if applicable.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Placement discharged.');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ namespace App\Models;
|
||||
use App\Models\Concerns\BelongsToOwner;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Bed extends Model
|
||||
@@ -41,4 +43,18 @@ class Bed extends Model
|
||||
{
|
||||
return $this->belongsTo(CareUnit::class, 'care_unit_id');
|
||||
}
|
||||
|
||||
public function stays(): HasMany
|
||||
{
|
||||
return $this->hasMany(BedStay::class, 'bed_id');
|
||||
}
|
||||
|
||||
public function activeStay(): HasOne
|
||||
{
|
||||
return $this->hasOne(BedStay::class, 'bed_id')
|
||||
->ofMany(
|
||||
['id' => 'max'],
|
||||
fn ($query) => $query->where('status', BedStay::STATUS_ACTIVE),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToOwner;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BedStay extends Model
|
||||
{
|
||||
use BelongsToOwner;
|
||||
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
|
||||
public const STATUS_ENDED = 'ended';
|
||||
|
||||
protected $table = 'care_bed_stays';
|
||||
|
||||
protected $fillable = [
|
||||
'owner_ref',
|
||||
'organization_id',
|
||||
'visit_id',
|
||||
'care_unit_id',
|
||||
'bed_id',
|
||||
'status',
|
||||
'admitted_at',
|
||||
'discharged_at',
|
||||
'admitted_by',
|
||||
'discharged_by',
|
||||
'reason',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'admitted_at' => 'datetime',
|
||||
'discharged_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organization::class, 'organization_id');
|
||||
}
|
||||
|
||||
public function visit(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Visit::class, 'visit_id');
|
||||
}
|
||||
|
||||
public function careUnit(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CareUnit::class, 'care_unit_id');
|
||||
}
|
||||
|
||||
public function bed(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Bed::class, 'bed_id');
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', self::STATUS_ACTIVE);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,16 @@ class CareUnit extends Model
|
||||
return $this->hasMany(StaffAssignment::class, 'care_unit_id');
|
||||
}
|
||||
|
||||
public function visits(): HasMany
|
||||
{
|
||||
return $this->hasMany(Visit::class, 'care_unit_id');
|
||||
}
|
||||
|
||||
public function activeStays(): HasMany
|
||||
{
|
||||
return $this->hasMany(BedStay::class, 'care_unit_id')->where('status', BedStay::STATUS_ACTIVE);
|
||||
}
|
||||
|
||||
public function supportsBeds(): bool
|
||||
{
|
||||
return $this->kind === 'inpatient';
|
||||
|
||||
@@ -24,6 +24,7 @@ class Visit extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'uuid', 'owner_ref', 'organization_id', 'branch_id', 'patient_id',
|
||||
'care_unit_id', 'bed_id', 'placed_at', 'placed_by',
|
||||
'status', 'specialty_stage', 'checked_in_at', 'completed_at', 'checked_in_by',
|
||||
];
|
||||
|
||||
@@ -32,6 +33,7 @@ class Visit extends Model
|
||||
return [
|
||||
'checked_in_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'placed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -64,6 +66,30 @@ class Visit extends Model
|
||||
return $this->belongsTo(Organization::class, 'organization_id');
|
||||
}
|
||||
|
||||
public function careUnit(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CareUnit::class, 'care_unit_id');
|
||||
}
|
||||
|
||||
public function bed(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Bed::class, 'bed_id');
|
||||
}
|
||||
|
||||
public function bedStays(): HasMany
|
||||
{
|
||||
return $this->hasMany(BedStay::class, 'visit_id');
|
||||
}
|
||||
|
||||
public function activeBedStay(): HasOne
|
||||
{
|
||||
return $this->hasOne(BedStay::class, 'visit_id')
|
||||
->ofMany(
|
||||
['id' => 'max'],
|
||||
fn ($query) => $query->where('status', BedStay::STATUS_ACTIVE),
|
||||
);
|
||||
}
|
||||
|
||||
public function appointment(): HasOne
|
||||
{
|
||||
return $this->hasOne(Appointment::class, 'visit_id');
|
||||
|
||||
@@ -566,7 +566,31 @@ class CarePermissions
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($merged);
|
||||
return $this->withDerivedAbilities(array_keys($merged));
|
||||
}
|
||||
|
||||
/**
|
||||
* Floor clinicians who record vitals may also place patients on care units/beds.
|
||||
*
|
||||
* @param list<string> $abilities
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function withDerivedAbilities(array $abilities): array
|
||||
{
|
||||
if (in_array('*', $abilities, true)) {
|
||||
return $abilities;
|
||||
}
|
||||
|
||||
if (in_array('vitals.manage', $abilities, true)) {
|
||||
$abilities[] = 'inpatient.placement.view';
|
||||
$abilities[] = 'inpatient.placement.manage';
|
||||
}
|
||||
|
||||
if (in_array('analytics.department.view', $abilities, true)) {
|
||||
$abilities[] = 'inpatient.placement.view';
|
||||
}
|
||||
|
||||
return array_values(array_unique($abilities));
|
||||
}
|
||||
|
||||
public function isAdmin(?Member $member): bool
|
||||
|
||||
@@ -215,7 +215,7 @@ class PatientService
|
||||
]);
|
||||
|
||||
$visits = $patient->visits()
|
||||
->with(['branch'])
|
||||
->with(['branch', 'careUnit', 'bed'])
|
||||
->orderByDesc('checked_in_at')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\Bed;
|
||||
use App\Models\BedStay;
|
||||
use App\Models\CareUnit;
|
||||
use App\Models\Visit;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class VisitPlacementService
|
||||
{
|
||||
/**
|
||||
* Place (or re-place) a visit onto a care unit, optionally a bed.
|
||||
*/
|
||||
public function place(
|
||||
Visit $visit,
|
||||
CareUnit $unit,
|
||||
?Bed $bed,
|
||||
string $ownerRef,
|
||||
?string $actorRef = null,
|
||||
?string $notes = null,
|
||||
): BedStay {
|
||||
$this->assertCompatible($visit, $unit, $bed);
|
||||
|
||||
return DB::transaction(function () use ($visit, $unit, $bed, $ownerRef, $actorRef, $notes) {
|
||||
$hadPlacement = $visit->care_unit_id !== null;
|
||||
$this->endCurrentStay($visit, $ownerRef, $actorRef, $hadPlacement ? 'transfer' : 'admit', $notes);
|
||||
|
||||
if ($bed) {
|
||||
$this->occupyBed($bed);
|
||||
}
|
||||
|
||||
$stay = BedStay::create([
|
||||
'owner_ref' => $ownerRef,
|
||||
'organization_id' => $visit->organization_id,
|
||||
'visit_id' => $visit->id,
|
||||
'care_unit_id' => $unit->id,
|
||||
'bed_id' => $bed?->id,
|
||||
'status' => BedStay::STATUS_ACTIVE,
|
||||
'admitted_at' => now(),
|
||||
'admitted_by' => $actorRef,
|
||||
'reason' => $hadPlacement ? 'transfer' : 'admit',
|
||||
'notes' => $notes,
|
||||
]);
|
||||
|
||||
$visit->update([
|
||||
'care_unit_id' => $unit->id,
|
||||
'bed_id' => $bed?->id,
|
||||
'placed_at' => now(),
|
||||
'placed_by' => $actorRef,
|
||||
]);
|
||||
|
||||
AuditLogger::record(
|
||||
$ownerRef,
|
||||
$hadPlacement ? 'visit.transferred' : 'visit.placed',
|
||||
$visit->organization_id,
|
||||
$actorRef,
|
||||
Visit::class,
|
||||
$visit->id,
|
||||
[
|
||||
'care_unit_id' => $unit->id,
|
||||
'bed_id' => $bed?->id,
|
||||
'bed_stay_id' => $stay->id,
|
||||
],
|
||||
);
|
||||
|
||||
return $stay->fresh(['careUnit', 'bed', 'visit']);
|
||||
});
|
||||
}
|
||||
|
||||
public function transfer(
|
||||
Visit $visit,
|
||||
CareUnit $unit,
|
||||
?Bed $bed,
|
||||
string $ownerRef,
|
||||
?string $actorRef = null,
|
||||
?string $notes = null,
|
||||
): BedStay {
|
||||
if ($visit->care_unit_id === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'care_unit_id' => 'Visit is not currently placed. Use admit/place instead.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->place($visit, $unit, $bed, $ownerRef, $actorRef, $notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear unit/bed placement without completing the visit.
|
||||
*/
|
||||
public function dischargePlacement(
|
||||
Visit $visit,
|
||||
string $ownerRef,
|
||||
?string $actorRef = null,
|
||||
?string $notes = null,
|
||||
): void {
|
||||
$visit->refresh();
|
||||
|
||||
if ($visit->care_unit_id === null && $visit->bed_id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($visit, $ownerRef, $actorRef, $notes) {
|
||||
$this->endCurrentStay($visit, $ownerRef, $actorRef, 'discharge', $notes);
|
||||
|
||||
$visit->update([
|
||||
'care_unit_id' => null,
|
||||
'bed_id' => null,
|
||||
'placed_at' => null,
|
||||
'placed_by' => null,
|
||||
]);
|
||||
|
||||
AuditLogger::record(
|
||||
$ownerRef,
|
||||
'visit.placement_discharged',
|
||||
$visit->organization_id,
|
||||
$actorRef,
|
||||
Visit::class,
|
||||
$visit->id,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
protected function endCurrentStay(
|
||||
Visit $visit,
|
||||
string $ownerRef,
|
||||
?string $actorRef,
|
||||
string $reason,
|
||||
?string $notes,
|
||||
): void {
|
||||
$active = BedStay::query()
|
||||
->where('visit_id', $visit->id)
|
||||
->active()
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
|
||||
foreach ($active as $stay) {
|
||||
$stay->update([
|
||||
'status' => BedStay::STATUS_ENDED,
|
||||
'discharged_at' => now(),
|
||||
'discharged_by' => $actorRef,
|
||||
'reason' => $reason,
|
||||
'notes' => $notes ?? $stay->notes,
|
||||
]);
|
||||
|
||||
if ($stay->bed_id) {
|
||||
$bed = Bed::query()->lockForUpdate()->find($stay->bed_id);
|
||||
if ($bed && $bed->status === 'occupied') {
|
||||
$stillOccupied = BedStay::query()
|
||||
->where('bed_id', $bed->id)
|
||||
->active()
|
||||
->where('id', '!=', $stay->id)
|
||||
->exists();
|
||||
|
||||
if (! $stillOccupied) {
|
||||
$bed->update(['status' => 'available']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also free visit's current bed if stay rows were missing.
|
||||
if ($visit->bed_id) {
|
||||
$bed = Bed::query()->lockForUpdate()->find($visit->bed_id);
|
||||
if ($bed && $bed->status === 'occupied') {
|
||||
$stillOccupied = BedStay::query()
|
||||
->where('bed_id', $bed->id)
|
||||
->active()
|
||||
->exists();
|
||||
if (! $stillOccupied) {
|
||||
$bed->update(['status' => 'available']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function occupyBed(Bed $bed): void
|
||||
{
|
||||
$bed = Bed::query()->lockForUpdate()->findOrFail($bed->id);
|
||||
|
||||
$conflict = BedStay::query()
|
||||
->where('bed_id', $bed->id)
|
||||
->active()
|
||||
->exists();
|
||||
|
||||
if ($conflict || in_array($bed->status, ['occupied', 'maintenance', 'closed'], true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'bed_id' => 'This bed is not available.',
|
||||
]);
|
||||
}
|
||||
|
||||
$bed->update(['status' => 'occupied']);
|
||||
}
|
||||
|
||||
protected function assertCompatible(Visit $visit, CareUnit $unit, ?Bed $bed): void
|
||||
{
|
||||
if ((int) $visit->organization_id !== (int) $unit->organization_id) {
|
||||
throw ValidationException::withMessages([
|
||||
'care_unit_id' => 'Care unit belongs to a different organization.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $unit->is_active) {
|
||||
throw ValidationException::withMessages([
|
||||
'care_unit_id' => 'Care unit is inactive.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($unit->kind === 'float_pool') {
|
||||
throw ValidationException::withMessages([
|
||||
'care_unit_id' => 'Patients cannot be placed on a float pool. Float pools are for staff only.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (in_array($visit->status, [Visit::STATUS_COMPLETED], true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'visit_id' => 'Cannot place a completed visit.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($bed) {
|
||||
if ((int) $bed->care_unit_id !== (int) $unit->id) {
|
||||
throw ValidationException::withMessages([
|
||||
'bed_id' => 'Bed does not belong to the selected care unit.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $unit->supportsBeds()) {
|
||||
throw ValidationException::withMessages([
|
||||
'bed_id' => 'This care unit kind does not use beds.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $bed->is_active) {
|
||||
throw ValidationException::withMessages([
|
||||
'bed_id' => 'Bed is inactive.',
|
||||
]);
|
||||
}
|
||||
} elseif ($unit->supportsBeds() === false) {
|
||||
// outpatient / service — bed optional, OK
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,12 @@ class VisitService
|
||||
|
||||
public function complete(Visit $visit, string $ownerRef, ?string $actorRef = null): Visit
|
||||
{
|
||||
$visit->refresh();
|
||||
|
||||
app(VisitPlacementService::class)->dischargePlacement($visit, $ownerRef, $actorRef, 'Visit completed');
|
||||
|
||||
$visit->refresh();
|
||||
|
||||
$visit->update([
|
||||
'status' => Visit::STATUS_COMPLETED,
|
||||
'completed_at' => now(),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Inpatient placement: visit ↔ care unit / bed + stay history.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('care_visits', function (Blueprint $table) {
|
||||
$table->foreignId('care_unit_id')->nullable()->after('patient_id')
|
||||
->constrained('care_care_units')->nullOnDelete();
|
||||
$table->foreignId('bed_id')->nullable()->after('care_unit_id')
|
||||
->constrained('care_beds')->nullOnDelete();
|
||||
$table->timestamp('placed_at')->nullable()->after('bed_id');
|
||||
$table->string('placed_by')->nullable()->after('placed_at');
|
||||
$table->index(['care_unit_id', 'status']);
|
||||
$table->index(['bed_id']);
|
||||
});
|
||||
|
||||
Schema::create('care_bed_stays', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('owner_ref')->index();
|
||||
$table->foreignId('organization_id')->constrained('care_organizations')->cascadeOnDelete();
|
||||
$table->foreignId('visit_id')->constrained('care_visits')->cascadeOnDelete();
|
||||
$table->foreignId('care_unit_id')->constrained('care_care_units')->cascadeOnDelete();
|
||||
$table->foreignId('bed_id')->nullable()->constrained('care_beds')->nullOnDelete();
|
||||
/** active | ended */
|
||||
$table->string('status')->default('active');
|
||||
$table->timestamp('admitted_at');
|
||||
$table->timestamp('discharged_at')->nullable();
|
||||
$table->string('admitted_by')->nullable();
|
||||
$table->string('discharged_by')->nullable();
|
||||
$table->string('reason')->nullable(); // admit | transfer | discharge
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['care_unit_id', 'status']);
|
||||
$table->index(['bed_id', 'status']);
|
||||
$table->index(['visit_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('care_bed_stays');
|
||||
|
||||
Schema::table('care_visits', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('bed_id');
|
||||
$table->dropConstrainedForeignId('care_unit_id');
|
||||
$table->dropColumn(['placed_at', 'placed_by']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -18,11 +18,106 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('care.care-units.edit', $unit) }}" class="btn-secondary">Edit</a>
|
||||
<a href="{{ route('care.staff-assignments.create', ['care_unit_id' => $unit->id]) }}" class="btn-primary">Assign staff</a>
|
||||
@php
|
||||
$member = auth()->user()
|
||||
? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user())
|
||||
: null;
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
$canManageUnit = $permissions->can($member, 'admin.departments.manage');
|
||||
@endphp
|
||||
@if ($canManageUnit)
|
||||
<a href="{{ route('care.care-units.edit', $unit) }}" class="btn-secondary">Edit unit</a>
|
||||
<a href="{{ route('care.staff-assignments.create', ['care_unit_id' => $unit->id]) }}" class="btn-secondary">Assign staff</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Current patients --}}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-slate-900">Patients on this unit</h2>
|
||||
<p class="text-sm text-slate-500">Active visit placements — foundation for MAR and ward boards.</p>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-slate-700">{{ $placedVisits->count() }} placed</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-left text-xs uppercase text-slate-500">
|
||||
<tr>
|
||||
<th class="pb-2 pr-4">Patient</th>
|
||||
<th class="pb-2 pr-4">Bed</th>
|
||||
<th class="pb-2 pr-4">Placed</th>
|
||||
<th class="pb-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse ($placedVisits as $visit)
|
||||
<tr>
|
||||
<td class="py-3 pr-4 font-medium">
|
||||
{{ $visit->patient?->fullName() ?? '—' }}
|
||||
@if ($visit->patient?->patient_number)
|
||||
<span class="text-xs text-slate-400">{{ $visit->patient->patient_number }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 pr-4">{{ $visit->bed?->label ?? '—' }}</td>
|
||||
<td class="py-3 pr-4 whitespace-nowrap">{{ $visit->placed_at?->format('d M Y H:i') ?? '—' }}</td>
|
||||
<td class="py-3 text-right whitespace-nowrap">
|
||||
@if ($canPlace)
|
||||
<form method="POST" action="{{ route('care.visits.placement.destroy', $visit) }}" class="inline" onsubmit="return confirm('Discharge placement and free the bed?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">Discharge bed</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="py-4 text-slate-500">No patients placed on this unit.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($canPlace && $unit->kind !== 'float_pool')
|
||||
<form method="POST" action="{{ route('care.care-units.placements.store', $unit) }}" class="mt-6 grid gap-3 border-t border-slate-100 pt-5 sm:grid-cols-4">
|
||||
@csrf
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-medium uppercase text-slate-500">Open visit</label>
|
||||
<select name="visit_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">Select patient visit…</option>
|
||||
@foreach ($placeableVisits as $visit)
|
||||
<option value="{{ $visit->id }}">
|
||||
{{ $visit->patient?->fullName() ?? 'Patient' }}
|
||||
· {{ $visit->checked_in_at?->format('d M H:i') ?? $visit->status }}
|
||||
@if ($visit->careUnit) (now: {{ $visit->careUnit->name ?? 'placed' }}) @endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('visit_id')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
|
||||
@error('care_unit_id')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
|
||||
@error('bed_id')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
@if ($unit->supportsBeds())
|
||||
<div>
|
||||
<label class="block text-xs font-medium uppercase text-slate-500">Bed</label>
|
||||
<select name="bed_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">No bed yet</option>
|
||||
@foreach ($availableBeds as $bed)
|
||||
<option value="{{ $bed->id }}">{{ $bed->label }}@if ($bed->room) ({{ $bed->room }}) @endif</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@else
|
||||
<div></div>
|
||||
@endif
|
||||
<div class="flex items-end">
|
||||
<button type="submit" class="btn-primary w-full">Place on unit</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($unit->supportsBeds())
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
@@ -32,17 +127,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('care.care-units.beds.store', $unit) }}" class="mt-4 grid gap-3 sm:grid-cols-4">
|
||||
@csrf
|
||||
<input type="text" name="label" required placeholder="Bed label" class="rounded-lg border-slate-300 text-sm" value="{{ old('label') }}">
|
||||
<input type="text" name="room" placeholder="Room" class="rounded-lg border-slate-300 text-sm" value="{{ old('room') }}">
|
||||
<select name="status" class="rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($bedStatuses as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('status', 'available') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Add bed</button>
|
||||
</form>
|
||||
@if ($canManageUnit ?? false)
|
||||
<form method="POST" action="{{ route('care.care-units.beds.store', $unit) }}" class="mt-4 grid gap-3 sm:grid-cols-4">
|
||||
@csrf
|
||||
<input type="text" name="label" required placeholder="Bed label" class="rounded-lg border-slate-300 text-sm" value="{{ old('label') }}">
|
||||
<input type="text" name="room" placeholder="Room" class="rounded-lg border-slate-300 text-sm" value="{{ old('room') }}">
|
||||
<select name="status" class="rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($bedStatuses as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('status', 'available') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Add bed</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
@@ -51,24 +148,31 @@
|
||||
<th class="pb-2 pr-4">Label</th>
|
||||
<th class="pb-2 pr-4">Room</th>
|
||||
<th class="pb-2 pr-4">Status</th>
|
||||
<th class="pb-2 pr-4">Occupant</th>
|
||||
<th class="pb-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse ($unit->beds as $bed)
|
||||
@php
|
||||
$occupant = $placedVisits->firstWhere('bed_id', $bed->id);
|
||||
@endphp
|
||||
<tr class="{{ $bed->is_active ? '' : 'opacity-50' }}">
|
||||
<td class="py-3 pr-4 font-medium">{{ $bed->label }}</td>
|
||||
<td class="py-3 pr-4">{{ $bed->room ?: '—' }}</td>
|
||||
<td class="py-3 pr-4">{{ $bedStatuses[$bed->status] ?? $bed->status }}</td>
|
||||
<td class="py-3 pr-4">{{ $occupant?->patient?->fullName() ?? '—' }}</td>
|
||||
<td class="py-3 text-right">
|
||||
<form method="POST" action="{{ route('care.care-units.beds.destroy', [$unit, $bed]) }}" class="inline" onsubmit="return confirm('Remove this bed?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">Remove</button>
|
||||
</form>
|
||||
@if ($canManageUnit ?? false)
|
||||
<form method="POST" action="{{ route('care.care-units.beds.destroy', [$unit, $bed]) }}" class="inline" onsubmit="return confirm('Remove this bed?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">Remove</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="py-4 text-slate-500">No beds yet.</td></tr>
|
||||
<tr><td colspan="5" class="py-4 text-slate-500">No beds yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -77,6 +181,9 @@
|
||||
@else
|
||||
<div class="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-6 py-5 text-sm text-slate-600">
|
||||
This unit kind does not use beds. Outpatient clinics, service units, and float pools assign staff without bed inventory.
|
||||
@if ($unit->kind === 'float_pool')
|
||||
Patients cannot be placed on a float pool.
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -93,6 +93,9 @@
|
||||
{{ $visit->checked_in_at?->format('d M Y H:i') ?? '—' }}
|
||||
· {{ config('care.visit_statuses')[$visit->status] ?? $visit->status }}
|
||||
@if ($visit->branch) · {{ $visit->branch->name }} @endif
|
||||
@if ($visit->careUnit)
|
||||
· {{ $visit->careUnit->name }}@if ($visit->bed) / {{ $visit->bed->label }}@endif
|
||||
@endif
|
||||
</p>
|
||||
@empty
|
||||
<p class="mt-1 text-slate-400">No visits yet</p>
|
||||
|
||||
@@ -114,6 +114,8 @@
|
||||
if ($permissions->can($member, 'admin.departments.view')) {
|
||||
$adminNav[] = ['name' => 'Departments', 'route' => route('care.departments.index'), 'active' => request()->routeIs('care.departments.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />'];
|
||||
}
|
||||
if ($permissions->can($member, 'admin.departments.view') || $permissions->can($member, 'inpatient.placement.view')) {
|
||||
$adminNav[] = ['name' => 'Care units', 'route' => route('care.care-units.index'), 'active' => request()->routeIs('care.care-units.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3h.008v.008h-.008v-.008Zm0 3h.008v.008h-.008v-.008Zm0 3h.008v.008h-.008v-.008Z" />'];
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Http\Controllers\Care\DepartmentController;
|
||||
use App\Http\Controllers\Care\CareUnitController;
|
||||
use App\Http\Controllers\Care\BedController;
|
||||
use App\Http\Controllers\Care\StaffAssignmentController;
|
||||
use App\Http\Controllers\Care\VisitPlacementController;
|
||||
use App\Http\Controllers\Care\DeviceController;
|
||||
use App\Http\Controllers\Care\DisplayPublicController;
|
||||
use App\Http\Controllers\Care\DisplayScreenController;
|
||||
@@ -428,6 +429,9 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||
Route::post('/care-units/{careUnit}/beds', [BedController::class, 'store'])->name('care.care-units.beds.store');
|
||||
Route::put('/care-units/{careUnit}/beds/{bed}', [BedController::class, 'update'])->name('care.care-units.beds.update');
|
||||
Route::delete('/care-units/{careUnit}/beds/{bed}', [BedController::class, 'destroy'])->name('care.care-units.beds.destroy');
|
||||
Route::post('/care-units/{careUnit}/placements', [VisitPlacementController::class, 'store'])->name('care.care-units.placements.store');
|
||||
Route::post('/visits/{visit}/placement/transfer', [VisitPlacementController::class, 'transfer'])->name('care.visits.placement.transfer');
|
||||
Route::delete('/visits/{visit}/placement', [VisitPlacementController::class, 'destroy'])->name('care.visits.placement.destroy');
|
||||
|
||||
Route::get('/staff-assignments', [StaffAssignmentController::class, 'index'])->name('care.staff-assignments.index');
|
||||
Route::get('/staff-assignments/create', [StaffAssignmentController::class, 'create'])->name('care.staff-assignments.create');
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Bed;
|
||||
use App\Models\BedStay;
|
||||
use App\Models\Branch;
|
||||
use App\Models\CareUnit;
|
||||
use App\Models\Department;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Patient;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\VisitService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CareVisitPlacementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $owner;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected Branch $branch;
|
||||
|
||||
protected Department $department;
|
||||
|
||||
protected CareUnit $unit;
|
||||
|
||||
protected Bed $bed;
|
||||
|
||||
protected Visit $visit;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->owner = User::create([
|
||||
'public_id' => 'placement-owner',
|
||||
'name' => 'Owner',
|
||||
'email' => 'placement-owner@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'name' => 'Placement Hospital',
|
||||
'slug' => 'placement-hospital',
|
||||
'settings' => [
|
||||
'onboarded' => true,
|
||||
'facility_type' => 'hospital',
|
||||
'plan' => 'pro',
|
||||
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->owner->public_id,
|
||||
'role' => 'hospital_admin',
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Main',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->department = Department::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'name' => "Women's Health",
|
||||
'type' => 'womens_health',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->unit = CareUnit::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'department_id' => $this->department->id,
|
||||
'name' => 'Postnatal Ward',
|
||||
'kind' => 'inpatient',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->bed = Bed::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'care_unit_id' => $this->unit->id,
|
||||
'label' => 'PN-01',
|
||||
'status' => 'available',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$patient = Patient::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'P-PLACE-1',
|
||||
'first_name' => 'Ama',
|
||||
'last_name' => 'Mensah',
|
||||
'gender' => 'female',
|
||||
]);
|
||||
|
||||
$this->visit = Visit::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $patient->id,
|
||||
'status' => Visit::STATUS_OPEN,
|
||||
'checked_in_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_place_visit_occupies_bed(): void
|
||||
{
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('care.care-units.placements.store', $this->unit), [
|
||||
'visit_id' => $this->visit->id,
|
||||
'bed_id' => $this->bed->id,
|
||||
])
|
||||
->assertRedirect(route('care.care-units.show', $this->unit));
|
||||
|
||||
$this->visit->refresh();
|
||||
$this->bed->refresh();
|
||||
|
||||
$this->assertSame($this->unit->id, $this->visit->care_unit_id);
|
||||
$this->assertSame($this->bed->id, $this->visit->bed_id);
|
||||
$this->assertSame('occupied', $this->bed->status);
|
||||
$this->assertDatabaseHas('care_bed_stays', [
|
||||
'visit_id' => $this->visit->id,
|
||||
'bed_id' => $this->bed->id,
|
||||
'status' => BedStay::STATUS_ACTIVE,
|
||||
'reason' => 'admit',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_transfer_frees_old_bed_and_occupies_new(): void
|
||||
{
|
||||
$otherUnit = CareUnit::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'department_id' => $this->department->id,
|
||||
'name' => 'Labour Ward',
|
||||
'kind' => 'inpatient',
|
||||
'is_active' => true,
|
||||
]);
|
||||
$otherBed = Bed::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'care_unit_id' => $otherUnit->id,
|
||||
'label' => 'LW-02',
|
||||
'status' => 'available',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('care.care-units.placements.store', $this->unit), [
|
||||
'visit_id' => $this->visit->id,
|
||||
'bed_id' => $this->bed->id,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('care.visits.placement.transfer', $this->visit), [
|
||||
'care_unit_id' => $otherUnit->id,
|
||||
'bed_id' => $otherBed->id,
|
||||
])
|
||||
->assertRedirect(route('care.care-units.show', $otherUnit));
|
||||
|
||||
$this->visit->refresh();
|
||||
$this->bed->refresh();
|
||||
$otherBed->refresh();
|
||||
|
||||
$this->assertSame($otherUnit->id, $this->visit->care_unit_id);
|
||||
$this->assertSame($otherBed->id, $this->visit->bed_id);
|
||||
$this->assertSame('available', $this->bed->status);
|
||||
$this->assertSame('occupied', $otherBed->status);
|
||||
$this->assertSame(1, BedStay::query()->where('visit_id', $this->visit->id)->active()->count());
|
||||
}
|
||||
|
||||
public function test_discharge_placement_frees_bed(): void
|
||||
{
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('care.care-units.placements.store', $this->unit), [
|
||||
'visit_id' => $this->visit->id,
|
||||
'bed_id' => $this->bed->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->delete(route('care.visits.placement.destroy', $this->visit))
|
||||
->assertRedirect();
|
||||
|
||||
$this->visit->refresh();
|
||||
$this->bed->refresh();
|
||||
|
||||
$this->assertNull($this->visit->care_unit_id);
|
||||
$this->assertNull($this->visit->bed_id);
|
||||
$this->assertSame('available', $this->bed->status);
|
||||
}
|
||||
|
||||
public function test_completing_visit_discharges_placement(): void
|
||||
{
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('care.care-units.placements.store', $this->unit), [
|
||||
'visit_id' => $this->visit->id,
|
||||
'bed_id' => $this->bed->id,
|
||||
]);
|
||||
|
||||
app(VisitService::class)->complete($this->visit, $this->owner->public_id, $this->owner->public_id);
|
||||
|
||||
$this->visit->refresh();
|
||||
$this->bed->refresh();
|
||||
|
||||
$this->assertSame(Visit::STATUS_COMPLETED, $this->visit->status);
|
||||
$this->assertNull($this->visit->care_unit_id);
|
||||
$this->assertSame('available', $this->bed->status);
|
||||
}
|
||||
|
||||
public function test_cannot_place_on_float_pool(): void
|
||||
{
|
||||
$float = CareUnit::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'department_id' => $this->department->id,
|
||||
'name' => 'Float Pool',
|
||||
'kind' => 'float_pool',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->from(route('care.care-units.show', $float))
|
||||
->post(route('care.care-units.placements.store', $float), [
|
||||
'visit_id' => $this->visit->id,
|
||||
])
|
||||
->assertRedirect()
|
||||
->assertSessionHasErrors('care_unit_id');
|
||||
}
|
||||
|
||||
public function test_second_patient_cannot_take_occupied_bed(): void
|
||||
{
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('care.care-units.placements.store', $this->unit), [
|
||||
'visit_id' => $this->visit->id,
|
||||
'bed_id' => $this->bed->id,
|
||||
]);
|
||||
|
||||
$otherPatient = Patient::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'P-PLACE-2',
|
||||
'first_name' => 'Kwame',
|
||||
'last_name' => 'Boateng',
|
||||
'gender' => 'male',
|
||||
]);
|
||||
$otherVisit = Visit::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $otherPatient->id,
|
||||
'status' => Visit::STATUS_OPEN,
|
||||
'checked_in_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->from(route('care.care-units.show', $this->unit))
|
||||
->post(route('care.care-units.placements.store', $this->unit), [
|
||||
'visit_id' => $otherVisit->id,
|
||||
'bed_id' => $this->bed->id,
|
||||
])
|
||||
->assertRedirect()
|
||||
->assertSessionHasErrors('bed_id');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user