Add Lab and Blood Bank manager roles with admin UIs.
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>
This commit is contained in:
isaacclad
2026-07-19 14:18:47 +00:00
co-authored by Cursor
parent 64e3f75159
commit 5d9d333170
26 changed files with 869 additions and 17 deletions
@@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Care;
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Http\Controllers\Controller;
use App\Services\Care\BloodBank\BloodBankAnalyticsService;
use App\Services\Care\BloodBank\BloodBankInventoryService;
use App\Services\Care\BranchContext;
use App\Services\Care\CarePermissions;
use App\Services\Care\SpecialtyModuleService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class BloodBankAdminController extends Controller
{
use ScopesToAccount;
public function __construct(
protected BloodBankInventoryService $inventory,
protected BloodBankAnalyticsService $analytics,
protected BranchContext $branchContext,
) {}
public function index(
Request $request,
SpecialtyModuleService $modules,
): View {
$this->authorizeAbility($request, 'blood_bank.manage');
$organization = $this->organization($request);
abort_unless($modules->isEnabled($organization, 'blood_bank'), 404);
$member = $this->member($request);
$branches = $this->branchContext->branches($request, $organization, $member);
$branchId = $this->branchContext->resolve($request, $organization, $member);
$branchScope = $branchId > 0 ? $branchId : null;
$overview = $this->inventory->adminOverview(
$organization,
$this->ownerRef($request),
$branchScope,
);
$report = $this->analytics->report(
$organization,
$this->ownerRef($request),
$branchScope,
);
return view('care.blood-bank.admin.index', [
'organization' => $organization,
'branches' => $branches,
'branchId' => $branchId,
'canSwitchBranch' => $this->branchContext->canSwitch($member),
'overview' => $overview,
'report' => $report,
'isAdmin' => app(CarePermissions::class)->isAdmin($member),
]);
}
public function updateStock(
Request $request,
SpecialtyModuleService $modules,
): RedirectResponse {
$this->authorizeAbility($request, 'blood_bank.manage');
$organization = $this->organization($request);
abort_unless($modules->isEnabled($organization, 'blood_bank'), 404);
$member = $this->member($request);
$branchId = $this->branchContext->resolve($request, $organization, $member);
$branchScope = $branchId > 0 ? $branchId : null;
$validated = $request->validate([
'stock' => ['required', 'array'],
'stock.*.product_code' => ['required', 'string', 'max:64'],
'stock.*.units_on_hand' => ['required', 'integer', 'min:0', 'max:99999'],
'stock.*.reorder_level' => ['required', 'integer', 'min:0', 'max:9999'],
'stock.*.notes' => ['nullable', 'string', 'max:2000'],
]);
$this->inventory->updateStock(
$organization,
$this->ownerRef($request),
$branchScope,
array_values($validated['stock']),
$this->actorRef($request),
);
return redirect()
->route('care.blood-bank.admin.index', array_filter([
'branch_id' => $branchId > 0 ? $branchId : null,
]))
->with('success', 'Blood Bank operational inventory updated.');
}
}
@@ -8,6 +8,7 @@ use App\Models\SpecialtyClinicalRecord;
use App\Models\Visit;
use App\Services\Care\BloodBank\BloodBankAnalyticsService;
use App\Services\Care\BloodBank\BloodBankWorkflowService;
use App\Services\Care\CarePermissions;
use App\Services\Care\SpecialtyClinicalRecordService;
use App\Services\Care\SpecialtyModuleService;
use App\Services\Care\SpecialtyShellService;
@@ -38,13 +39,40 @@ class BloodBankWorkspaceController extends Controller
$this->authorizeBranch($request, (int) $visit->branch_id);
}
/**
* Doctors use consultations.manage; Blood Bank managers use blood_bank.manage.
*/
protected function authorizeBloodBankClinical(Request $request): void
{
$permissions = app(CarePermissions::class);
$member = $this->member($request);
abort_unless(
$permissions->can($member, 'blood_bank.manage')
|| $permissions->can($member, 'consultations.manage'),
403,
);
}
protected function authorizeBloodBankViewAbility(Request $request): void
{
$permissions = app(CarePermissions::class);
$member = $this->member($request);
abort_unless(
$permissions->can($member, 'blood_bank.view')
|| $permissions->can($member, 'blood_bank.manage')
|| $permissions->can($member, 'consultations.view')
|| $permissions->can($member, 'consultations.manage'),
403,
);
}
public function setStage(
Request $request,
Visit $visit,
SpecialtyModuleService $modules,
SpecialtyVisitStageService $stages,
): RedirectResponse {
$this->authorizeAbility($request, 'consultations.manage');
$this->authorizeBloodBankClinical($request);
$this->assertBloodBankManage($request, $modules);
$this->assertVisit($request, $visit);
@@ -79,7 +107,7 @@ class BloodBankWorkspaceController extends Controller
SpecialtyShellService $shell,
BloodBankWorkflowService $workflow,
): RedirectResponse {
$this->authorizeAbility($request, 'consultations.manage');
$this->authorizeBloodBankClinical($request);
$this->assertBloodBankManage($request, $modules);
$this->assertVisit($request, $visit);
@@ -187,7 +215,7 @@ class BloodBankWorkspaceController extends Controller
SpecialtyVisitStageService $stages,
BloodBankWorkflowService $workflow,
): RedirectResponse {
$this->authorizeAbility($request, 'consultations.manage');
$this->authorizeBloodBankClinical($request);
$this->assertBloodBankManage($request, $modules);
$this->assertVisit($request, $visit);
@@ -271,7 +299,7 @@ class BloodBankWorkspaceController extends Controller
SpecialtyShellService $shell,
BloodBankAnalyticsService $analytics,
): View {
$this->authorizeAbility($request, 'consultations.view');
$this->authorizeBloodBankViewAbility($request);
$this->assertBloodBankAccess($request, $modules);
$organization = $this->organization($request);
@@ -300,7 +328,7 @@ class BloodBankWorkspaceController extends Controller
SpecialtyModuleService $modules,
SpecialtyClinicalRecordService $clinical,
): View {
$this->authorizeAbility($request, 'consultations.view');
$this->authorizeBloodBankViewAbility($request);
$this->assertBloodBankAccess($request, $modules);
$this->assertVisit($request, $visit);
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\Care;
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Http\Controllers\Controller;
use App\Models\InvestigationRequest;
use App\Models\InvestigationType;
use App\Services\Care\BranchContext;
use App\Services\Care\CarePermissions;
use App\Services\Care\InvestigationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class LabAdminController extends Controller
{
use ScopesToAccount;
public function __construct(
protected InvestigationService $investigations,
protected BranchContext $branchContext,
) {}
public function index(Request $request): View
{
$this->authorizeAbility($request, 'lab.catalog.manage');
$organization = $this->organization($request);
$member = $this->member($request);
$owner = $this->ownerRef($request);
$branches = $this->branchContext->branches($request, $organization, $member);
$branchId = $this->branchContext->resolve($request, $organization, $member);
$branchScope = $branchId > 0 ? $branchId : null;
$catalogCount = InvestigationType::query()
->where('organization_id', $organization->id)
->count();
$activeCatalogCount = InvestigationType::query()
->where('organization_id', $organization->id)
->where('is_active', true)
->count();
$queueCounts = InvestigationRequest::owned($owner)
->where('organization_id', $organization->id)
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
->whereIn('status', [
InvestigationRequest::STATUS_PENDING,
InvestigationRequest::STATUS_SAMPLE_COLLECTED,
InvestigationRequest::STATUS_IN_PROGRESS,
InvestigationRequest::STATUS_AWAITING_REVIEW,
])
->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->pluck('total', 'status');
$openQueue = (int) $queueCounts->sum();
$queue = $branchScope
? $this->investigations->workQueue($owner, $branchScope)
: collect();
return view('care.lab.admin.index', [
'organization' => $organization,
'branches' => $branches,
'branchId' => $branchId,
'canSwitchBranch' => $this->branchContext->canSwitch($member),
'catalogCount' => $catalogCount,
'activeCatalogCount' => $activeCatalogCount,
'openQueue' => $openQueue,
'queueCounts' => $queueCounts,
'recentQueue' => $queue->take(8),
'statuses' => config('care.investigation_statuses'),
'isAdmin' => app(CarePermissions::class)->isAdmin($member),
]);
}
}
@@ -31,7 +31,7 @@ class MemberController extends Controller
->get();
$adminRoles = ['super_admin', 'hospital_admin'];
$clinicalRoles = ['doctor', 'nurse', 'lab_technician', 'pharmacist'];
$clinicalRoles = ['doctor', 'nurse', 'lab_technician', 'lab_manager', 'pharmacist', 'blood_bank_manager'];
$heroStats = [
'total' => $members->count(),
@@ -210,7 +210,7 @@ class PractitionerController extends Controller
{
return Member::owned($owner)
->where('organization_id', $organizationId)
->whereIn('role', ['doctor', 'nurse', 'lab_technician', 'pharmacist', 'hospital_admin', 'super_admin'])
->whereIn('role', ['doctor', 'nurse', 'lab_technician', 'lab_manager', 'pharmacist', 'blood_bank_manager', 'hospital_admin', 'super_admin'])
->orderBy('user_ref')
->get();
}
@@ -60,7 +60,8 @@ class ServiceQueueController extends Controller
return match ($member?->role) {
'cashier' => 'care.bills.index',
'pharmacist' => 'care.prescriptions.queue',
'lab_technician' => 'care.lab.queue.index',
'lab_technician', 'lab_manager' => 'care.lab.queue.index',
'blood_bank_manager' => 'care.blood-bank.admin.index',
'receptionist', 'nurse' => 'care.appointments.index',
default => 'care.dashboard',
};
@@ -670,7 +670,10 @@ class SpecialtyModuleController extends Controller
// Ability-based for every role (doctor, nurse, receptionist, lab, pharmacist, …).
// Module canManage alone is not enough — flags must match what mutate routes authorize.
$canConsult = $permissions->can($member, 'consultations.manage') && $canManageSpecialty;
$canAdvanceStage = $canConsult;
$canAdvanceStage = $canConsult
|| ($module === 'blood_bank'
&& $canManageSpecialty
&& $permissions->can($member, 'blood_bank.manage'));
$canStartConsultation = $canConsult;
$canCallNext = $canManageSpecialty;
$canManageClinical = $canManageSpecialty;