Files
ladill-care/app/Services/Care/BloodBank/BloodBankInventoryService.php
T
isaaccladandCursor 5d9d333170
Deploy Ladill Care / deploy (push) Successful in 46s
Add Lab and Blood Bank manager roles with admin UIs.
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>
2026-07-19 14:18:47 +00:00

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,
];
}
}