> */ protected array $defaultTimes = [ 'od' => ['08:00'], 'bd' => ['08:00', '20:00'], 'tds' => ['08:00', '14:00', '20:00'], 'qds' => ['06:00', '12:00', '18:00', '22:00'], 'q4h' => ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'], 'q6h' => ['00:00', '06:00', '12:00', '18:00'], 'q8h' => ['06:00', '14:00', '22:00'], 'prn' => [], 'stat' => [], 'custom' => ['08:00'], ]; /** * Sync MAR orders from active/dispensed prescriptions on a visit. * * @return Collection */ public function syncOrdersForVisit(Visit $visit, string $ownerRef): Collection { $prescriptions = Prescription::owned($ownerRef) ->where('visit_id', $visit->id) ->whereIn('status', [Prescription::STATUS_ACTIVE, Prescription::STATUS_DISPENSED]) ->with('items') ->get(); foreach ($prescriptions as $prescription) { foreach ($prescription->items as $item) { if ($item->is_procedure) { continue; } $this->upsertOrderFromItem($visit, $prescription, $item, $ownerRef); } } return MarOrder::owned($ownerRef) ->where('visit_id', $visit->id) ->where('status', MarOrder::STATUS_ACTIVE) ->orderBy('drug_name') ->get(); } public function upsertOrderFromItem( Visit $visit, Prescription $prescription, PrescriptionItem $item, string $ownerRef, ?string $frequencyCode = null, ): MarOrder { $parsed = $this->parseFrequency((string) ($item->frequency ?? ''), $frequencyCode); return MarOrder::query()->updateOrCreate( ['prescription_item_id' => $item->id], [ 'owner_ref' => $ownerRef, 'organization_id' => $visit->organization_id, 'visit_id' => $visit->id, 'patient_id' => $visit->patient_id, 'prescription_id' => $prescription->id, 'drug_name' => $item->name, 'dosage' => $item->dosage, 'route' => $item->route, 'frequency_code' => $parsed['code'], 'times_of_day' => $parsed['times'], 'is_prn' => $parsed['is_prn'], 'status' => MarOrder::STATUS_ACTIVE, 'starts_at' => $visit->placed_at ?? $visit->checked_in_at ?? now(), 'instructions' => $item->instructions, ], ); } /** * Due / overdue / PRN slots for placed patients on a care unit for a calendar day. * * @return list> */ public function unitRoundBoard(CareUnit $unit, string $ownerRef, ?CarbonInterface $day = null): array { $day = Carbon::parse($day ?? now())->startOfDay(); $visits = Visit::owned($ownerRef) ->where('organization_id', $unit->organization_id) ->where('care_unit_id', $unit->id) ->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS]) ->with(['patient', 'bed']) ->get(); $rows = []; foreach ($visits as $visit) { $orders = $this->syncOrdersForVisit($visit, $ownerRef); foreach ($orders as $order) { foreach ($this->dueSlotsForOrder($order, $day) as $slot) { $rows[] = $slot + [ 'visit' => $visit, 'patient' => $visit->patient, 'bed' => $visit->bed, 'order' => $order, ]; } } } usort($rows, function (array $a, array $b) { $aTime = $a['scheduled_for']?->timestamp ?? PHP_INT_MAX; $bTime = $b['scheduled_for']?->timestamp ?? PHP_INT_MAX; if ($aTime === $bTime) { return strcmp((string) ($a['patient']?->fullName() ?? ''), (string) ($b['patient']?->fullName() ?? '')); } return $aTime <=> $bTime; }); return $rows; } /** * @return list */ public function dueSlotsForOrder(MarOrder $order, CarbonInterface $day): array { $day = Carbon::parse($day)->startOfDay(); $end = $day->copy()->endOfDay(); if ($order->status !== MarOrder::STATUS_ACTIVE) { return []; } if ($order->starts_at && $order->starts_at->gt($end)) { return []; } if ($order->ends_at && $order->ends_at->lt($day)) { return []; } $existing = MarAdministration::query() ->where('mar_order_id', $order->id) ->where(function ($q) use ($day, $end) { $q->whereBetween('scheduled_for', [$day, $end]) ->orWhere(function ($q2) use ($day, $end) { $q2->whereNull('scheduled_for') ->whereBetween('recorded_at', [$day, $end]); }); }) ->get(); if ($order->is_prn || $order->frequency_code === 'prn') { $givenToday = $existing->where('status', MarAdministration::STATUS_GIVEN); return [[ 'scheduled_for' => null, 'state' => $givenToday->isNotEmpty() ? 'prn_given' : 'prn_due', 'administration' => $givenToday->sortByDesc('recorded_at')->first(), ]]; } if ($order->frequency_code === 'stat') { $admin = $existing->first(); return [[ 'scheduled_for' => $order->starts_at ?? $day->copy()->setTime(8, 0), 'state' => $admin ? $admin->status : 'due', 'administration' => $admin, ]]; } $times = $order->times_of_day ?: ($this->defaultTimes[$order->frequency_code] ?? ['08:00']); $slots = []; foreach ($times as $time) { [$h, $m] = array_map('intval', explode(':', $time) + [0, 0]); $scheduled = $day->copy()->setTime($h, $m); if ($order->starts_at && $scheduled->lt($order->starts_at)) { continue; } $admin = $existing->first(function (MarAdministration $a) use ($scheduled) { return $a->scheduled_for && $a->scheduled_for->equalTo($scheduled); }); $state = 'due'; if ($admin) { $state = $admin->status; } elseif ($scheduled->lt(now()->subMinutes(30))) { $state = 'overdue'; } $slots[] = [ 'scheduled_for' => $scheduled, 'state' => $state, 'administration' => $admin, ]; } return $slots; } public function recordAdministration( MarOrder $order, string $status, string $ownerRef, ?string $actorRef, ?CarbonInterface $scheduledFor = null, ?string $doseGiven = null, ?string $route = null, ?string $witnessBy = null, ?string $reason = null, ?string $notes = null, ?int $careUnitId = null, ): MarAdministration { if (! in_array($status, [ MarAdministration::STATUS_GIVEN, MarAdministration::STATUS_HELD, MarAdministration::STATUS_REFUSED, MarAdministration::STATUS_MISSED, ], true)) { throw ValidationException::withMessages(['status' => 'Invalid MAR status.']); } if ($order->status !== MarOrder::STATUS_ACTIVE) { throw ValidationException::withMessages(['mar_order_id' => 'This MAR order is not active.']); } if (in_array($status, [MarAdministration::STATUS_HELD, MarAdministration::STATUS_REFUSED, MarAdministration::STATUS_MISSED], true) && blank($reason) && blank($notes)) { throw ValidationException::withMessages([ 'reason' => 'Provide a reason when holding, refusing, or marking missed.', ]); } $visit = $order->visit ?? Visit::query()->findOrFail($order->visit_id); if ($scheduledFor && ! $order->is_prn) { $duplicate = MarAdministration::query() ->where('mar_order_id', $order->id) ->where('scheduled_for', $scheduledFor) ->exists(); if ($duplicate) { throw ValidationException::withMessages([ 'scheduled_for' => 'This dose slot is already recorded.', ]); } } $admin = MarAdministration::create([ 'owner_ref' => $ownerRef, 'organization_id' => $order->organization_id, 'mar_order_id' => $order->id, 'visit_id' => $order->visit_id, 'patient_id' => $order->patient_id, 'care_unit_id' => $careUnitId ?? $visit->care_unit_id, 'scheduled_for' => $scheduledFor, 'recorded_at' => now(), 'status' => $status, 'dose_given' => $doseGiven ?? $order->dosage, 'route' => $route ?? $order->route, 'administered_by' => $actorRef, 'witness_by' => $witnessBy, 'reason' => $reason, 'notes' => $notes, ]); AuditLogger::record( $ownerRef, 'mar.'.$status, $order->organization_id, $actorRef, MarAdministration::class, $admin->id, ['mar_order_id' => $order->id, 'visit_id' => $order->visit_id], ); return $admin; } /** * @return array{code: string, times: list, is_prn: bool} */ public function parseFrequency(string $frequency, ?string $override = null): array { if ($override && isset($this->defaultTimes[$override])) { return [ 'code' => $override, 'times' => $this->defaultTimes[$override], 'is_prn' => $override === 'prn', ]; } $raw = strtolower(trim($frequency)); $map = [ 'od' => 'od', 'once daily' => 'od', 'daily' => 'od', 'once a day' => 'od', '1x' => 'od', 'bd' => 'bd', 'bid' => 'bd', 'twice daily' => 'bd', 'twice a day' => 'bd', '2x' => 'bd', 'tds' => 'tds', 'tid' => 'tds', 'three times' => 'tds', '3x' => 'tds', 'qds' => 'qds', 'qid' => 'qds', 'four times' => 'qds', '4x' => 'qds', 'q4h' => 'q4h', 'every 4' => 'q4h', 'q6h' => 'q6h', 'every 6' => 'q6h', 'q8h' => 'q8h', 'every 8' => 'q8h', 'prn' => 'prn', 'as needed' => 'prn', 'when required' => 'prn', 'stat' => 'stat', 'immediately' => 'stat', 'now' => 'stat', ]; foreach ($map as $needle => $code) { if ($raw === $needle || str_contains($raw, $needle)) { return [ 'code' => $code, 'times' => $this->defaultTimes[$code], 'is_prn' => $code === 'prn', ]; } } return [ 'code' => 'od', 'times' => $this->defaultTimes['od'], 'is_prn' => false, ]; } }