Deploy Ladill Care / deploy (push) Successful in 46s
Give lab_manager catalog/ops admin access and blood_bank_manager a branch-level stock register plus specialty admin surface, while keeping technicians on clinical queues. Co-authored-by: Cursor <cursoragent@cursor.com>
150 lines
5.1 KiB
PHP
150 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Care\BloodBank;
|
|
|
|
use App\Models\BloodBankStock;
|
|
use App\Models\Organization;
|
|
use App\Models\SpecialtyClinicalRecord;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class BloodBankInventoryService
|
|
{
|
|
/**
|
|
* Canonical operational products for branch-level stock (thin register, not bag ledger).
|
|
*
|
|
* @return array<string, array{label: string, reorder_level: int}>
|
|
*/
|
|
public function catalogProducts(): array
|
|
{
|
|
return [
|
|
'whole_blood' => ['label' => 'Whole blood', 'reorder_level' => 2],
|
|
'packed_rbc' => ['label' => 'Packed RBC', 'reorder_level' => 2],
|
|
'platelets' => ['label' => 'Platelets', 'reorder_level' => 2],
|
|
'ffp' => ['label' => 'FFP', 'reorder_level' => 2],
|
|
'cryo' => ['label' => 'Cryoprecipitate', 'reorder_level' => 1],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Ensure catalog rows exist for the branch, then return them ordered.
|
|
*
|
|
* @return Collection<int, BloodBankStock>
|
|
*/
|
|
public function ensureStockRows(
|
|
Organization $organization,
|
|
string $ownerRef,
|
|
?int $branchId,
|
|
): Collection {
|
|
foreach ($this->catalogProducts() as $code => $meta) {
|
|
BloodBankStock::query()->firstOrCreate(
|
|
[
|
|
'organization_id' => $organization->id,
|
|
'branch_id' => $branchId,
|
|
'product_code' => $code,
|
|
],
|
|
[
|
|
'owner_ref' => $ownerRef,
|
|
'product_label' => $meta['label'],
|
|
'units_on_hand' => 0,
|
|
'reorder_level' => $meta['reorder_level'],
|
|
],
|
|
);
|
|
}
|
|
|
|
return BloodBankStock::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->when(
|
|
$branchId === null,
|
|
fn ($q) => $q->whereNull('branch_id'),
|
|
fn ($q) => $q->where('branch_id', $branchId),
|
|
)
|
|
->orderBy('product_label')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* @param list<array{product_code: string, units_on_hand: int, reorder_level: int, notes?: ?string}> $rows
|
|
* @return Collection<int, BloodBankStock>
|
|
*/
|
|
public function updateStock(
|
|
Organization $organization,
|
|
string $ownerRef,
|
|
?int $branchId,
|
|
array $rows,
|
|
?string $actorRef = null,
|
|
): Collection {
|
|
$allowed = array_keys($this->catalogProducts());
|
|
|
|
return DB::transaction(function () use ($organization, $ownerRef, $branchId, $rows, $actorRef, $allowed) {
|
|
$this->ensureStockRows($organization, $ownerRef, $branchId);
|
|
|
|
foreach ($rows as $row) {
|
|
$code = (string) ($row['product_code'] ?? '');
|
|
if (! in_array($code, $allowed, true)) {
|
|
continue;
|
|
}
|
|
|
|
$stock = BloodBankStock::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->where('product_code', $code)
|
|
->when(
|
|
$branchId === null,
|
|
fn ($q) => $q->whereNull('branch_id'),
|
|
fn ($q) => $q->where('branch_id', $branchId),
|
|
)
|
|
->first();
|
|
|
|
if (! $stock) {
|
|
continue;
|
|
}
|
|
|
|
$stock->update([
|
|
'units_on_hand' => max(0, (int) ($row['units_on_hand'] ?? 0)),
|
|
'reorder_level' => max(0, (int) ($row['reorder_level'] ?? $stock->reorder_level)),
|
|
'notes' => isset($row['notes']) ? (string) $row['notes'] : $stock->notes,
|
|
'updated_by' => $actorRef,
|
|
]);
|
|
}
|
|
|
|
return $this->ensureStockRows($organization, $ownerRef, $branchId);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* stock: Collection<int, BloodBankStock>,
|
|
* low_stock: Collection<int, BloodBankStock>,
|
|
* low_stock_count: int,
|
|
* total_units: int,
|
|
* recent_notes: Collection<int, SpecialtyClinicalRecord>
|
|
* }
|
|
*/
|
|
public function adminOverview(
|
|
Organization $organization,
|
|
string $ownerRef,
|
|
?int $branchId,
|
|
): array {
|
|
$stock = $this->ensureStockRows($organization, $ownerRef, $branchId);
|
|
$lowStock = $stock->filter(fn (BloodBankStock $row) => $row->isLowStock())->values();
|
|
|
|
$recentNotes = SpecialtyClinicalRecord::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->where('module_key', 'blood_bank')
|
|
->where('record_type', 'inventory_note')
|
|
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
|
->with(['visit.patient'])
|
|
->orderByDesc('recorded_at')
|
|
->limit(8)
|
|
->get();
|
|
|
|
return [
|
|
'stock' => $stock,
|
|
'low_stock' => $lowStock,
|
|
'low_stock_count' => $lowStock->count(),
|
|
'total_units' => (int) $stock->sum('units_on_hand'),
|
|
'recent_notes' => $recentNotes,
|
|
];
|
|
}
|
|
}
|