Healthcare management app: patients, appointments, consultations, lab, pharmacy inventory, billing, and reports at care.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Department;
|
||||
use App\Models\Patient;
|
||||
use App\Models\Practitioner;
|
||||
use App\Services\Care\AppointmentService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AppointmentController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected AppointmentService $appointments,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.view');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$appointments = $this->appointments->list(
|
||||
$this->ownerRef($request),
|
||||
$organization->id,
|
||||
$request->only(['status', 'practitioner_id', 'date', 'patient_id']),
|
||||
$branchScope,
|
||||
);
|
||||
|
||||
$practitioners = $this->activePractitioners($request, $organization->id);
|
||||
|
||||
return view('care.appointments.index', [
|
||||
'organization' => $organization,
|
||||
'appointments' => $appointments,
|
||||
'practitioners' => $practitioners,
|
||||
'statuses' => config('care.appointment_statuses'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
return view('care.appointments.create', $this->formData($request, $organization));
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$organization = $this->organization($request);
|
||||
$validated = $this->validatedAppointmentData($request);
|
||||
|
||||
$appointment = $this->appointments->book(
|
||||
$organization,
|
||||
$this->ownerRef($request),
|
||||
$validated,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.appointments.show', $appointment)
|
||||
->with('success', 'Appointment booked.');
|
||||
}
|
||||
|
||||
public function walkInCreate(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
return view('care.appointments.walk-in', $this->formData($request, $organization));
|
||||
}
|
||||
|
||||
public function walkInStore(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$organization = $this->organization($request);
|
||||
$validated = $this->validatedWalkInData($request);
|
||||
|
||||
$appointment = $this->appointments->walkIn(
|
||||
$organization,
|
||||
$this->ownerRef($request),
|
||||
$validated,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.queue.index')
|
||||
->with('success', 'Walk-in added to queue.');
|
||||
}
|
||||
|
||||
public function show(Request $request, Appointment $appointment): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.view');
|
||||
$this->authorizeAppointment($request, $appointment);
|
||||
|
||||
$appointment->load(['patient', 'practitioner', 'branch', 'department', 'visit', 'consultation']);
|
||||
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'appointments.manage');
|
||||
$canConsult = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'consultations.manage');
|
||||
|
||||
return view('care.appointments.show', [
|
||||
'appointment' => $appointment,
|
||||
'statuses' => config('care.appointment_statuses'),
|
||||
'canManage' => $canManage,
|
||||
'canConsult' => $canConsult,
|
||||
]);
|
||||
}
|
||||
|
||||
public function checkIn(Request $request, Appointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$this->authorizeAppointment($request, $appointment);
|
||||
|
||||
$this->appointments->checkIn($appointment, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return redirect()->route('care.queue.index')
|
||||
->with('success', 'Patient checked in and added to queue.');
|
||||
}
|
||||
|
||||
public function cancel(Request $request, Appointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$this->authorizeAppointment($request, $appointment);
|
||||
|
||||
$this->appointments->cancel($appointment, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Appointment cancelled.');
|
||||
}
|
||||
|
||||
public function noShow(Request $request, Appointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$this->authorizeAppointment($request, $appointment);
|
||||
|
||||
$this->appointments->markNoShow($appointment, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Marked as no-show.');
|
||||
}
|
||||
|
||||
protected function authorizeAppointment(Request $request, Appointment $appointment): void
|
||||
{
|
||||
$this->authorizeOwner($request, $appointment);
|
||||
abort_unless($appointment->organization_id === $this->organization($request)->id, 404);
|
||||
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
if ($branchId !== null && $appointment->branch_id !== $branchId) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function formData(Request $request, \App\Models\Organization $organization): array
|
||||
{
|
||||
$ownerRef = $this->ownerRef($request);
|
||||
$branchQuery = Branch::owned($ownerRef)->where('organization_id', $organization->id)->where('is_active', true);
|
||||
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
if ($branchScope !== null) {
|
||||
$branchQuery->where('id', $branchScope);
|
||||
}
|
||||
|
||||
$branches = $branchQuery->orderBy('name')->get();
|
||||
$defaultBranch = $branchScope ?? $branches->first()?->id;
|
||||
|
||||
$patients = Patient::owned($ownerRef)
|
||||
->where('organization_id', $organization->id)
|
||||
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
|
||||
->orderBy('first_name')
|
||||
->limit(200)
|
||||
->get();
|
||||
|
||||
$departments = Department::owned($ownerRef)
|
||||
->whereIn('branch_id', $branches->pluck('id'))
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'defaultBranch' => $defaultBranch,
|
||||
'patients' => $patients,
|
||||
'practitioners' => $this->activePractitioners($request, $organization->id),
|
||||
'departments' => $departments,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, Practitioner>
|
||||
*/
|
||||
protected function activePractitioners(Request $request, int $organizationId)
|
||||
{
|
||||
return Practitioner::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organizationId)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedAppointmentData(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'branch_id' => ['required', 'integer', 'exists:care_branches,id'],
|
||||
'patient_id' => ['required', 'integer', 'exists:care_patients,id'],
|
||||
'practitioner_id' => ['nullable', 'integer', 'exists:care_practitioners,id'],
|
||||
'department_id' => ['nullable', 'integer', 'exists:care_departments,id'],
|
||||
'scheduled_at' => ['required', 'date', 'after:now'],
|
||||
'reason' => ['nullable', 'string', 'max:1000'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedWalkInData(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'branch_id' => ['required', 'integer', 'exists:care_branches,id'],
|
||||
'patient_id' => ['required', 'integer', 'exists:care_patients,id'],
|
||||
'practitioner_id' => ['nullable', 'integer', 'exists:care_practitioners,id'],
|
||||
'department_id' => ['nullable', 'integer', 'exists:care_departments,id'],
|
||||
'reason' => ['nullable', 'string', 'max:1000'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\AuditLog;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class AuditLogController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'audit.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$logs = $this->query($request, $organization)->paginate(50)->withQueryString();
|
||||
|
||||
return view('care.audit.index', [
|
||||
'logs' => $logs,
|
||||
'organization' => $organization,
|
||||
'actions' => config('care.audit_actions'),
|
||||
'canExport' => app(CarePermissions::class)->can($this->member($request), 'audit.export'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'audit.export');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$filename = 'care-audit-'.now()->format('Y-m-d-His').'.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($request, $organization) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fputcsv($handle, ['Time', 'Action', 'Actor', 'Subject', 'Metadata', 'IP']);
|
||||
|
||||
$this->query($request, $organization)->chunk(200, function ($logs) use ($handle) {
|
||||
foreach ($logs as $log) {
|
||||
fputcsv($handle, [
|
||||
$log->created_at?->toDateTimeString(),
|
||||
$log->action,
|
||||
$log->actor_ref ?? '—',
|
||||
$log->subject_type ? class_basename($log->subject_type).' #'.$log->subject_id : '—',
|
||||
json_encode($log->metadata ?? []),
|
||||
$log->ip_address ?? '—',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
fclose($handle);
|
||||
}, $filename, ['Content-Type' => 'text/csv']);
|
||||
}
|
||||
|
||||
protected function query(Request $request, $organization)
|
||||
{
|
||||
return AuditLog::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->when($request->action, fn ($q, $action) => $q->where('action', $action))
|
||||
->when($request->from, fn ($q, $from) => $q->where('created_at', '>=', Carbon::parse($from)->startOfDay()))
|
||||
->when($request->to, fn ($q, $to) => $q->where('created_at', '<=', Carbon::parse($to)->endOfDay()))
|
||||
->when($request->q, function ($q, $search) {
|
||||
$q->where(function ($inner) use ($search) {
|
||||
$inner->where('action', 'like', "%{$search}%")
|
||||
->orWhere('metadata', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->orderByDesc('created_at');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Bill;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\BillService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BillController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected BillService $bills,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.view');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$bills = $this->bills->list(
|
||||
$this->ownerRef($request),
|
||||
$organization->id,
|
||||
$request->only(['status', 'patient_id']),
|
||||
$branchScope,
|
||||
);
|
||||
|
||||
return view('care.bills.index', [
|
||||
'organization' => $organization,
|
||||
'bills' => $bills,
|
||||
'statuses' => config('care.bill_statuses'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function generate(Request $request, Visit $visit): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.manage');
|
||||
$this->authorizeVisit($request, $visit);
|
||||
|
||||
$bill = $this->bills->generateFromVisit($visit, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return redirect()->route('care.bills.show', $bill)
|
||||
->with('success', 'Bill generated from visit.');
|
||||
}
|
||||
|
||||
public function show(Request $request, Bill $bill): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.view');
|
||||
$this->authorizeBill($request, $bill);
|
||||
|
||||
$bill->load(['patient', 'branch', 'visit', 'lineItems', 'payments']);
|
||||
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'bills.manage');
|
||||
$canPay = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'payments.manage');
|
||||
|
||||
return view('care.bills.show', [
|
||||
'bill' => $bill,
|
||||
'statuses' => config('care.bill_statuses'),
|
||||
'lineTypes' => config('care.bill_line_types'),
|
||||
'paymentMethods' => config('care.payment_methods'),
|
||||
'canManage' => $canManage,
|
||||
'canPay' => $canPay,
|
||||
]);
|
||||
}
|
||||
|
||||
public function addLineItem(Request $request, Bill $bill): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.manage');
|
||||
$this->authorizeBill($request, $bill);
|
||||
|
||||
$this->bills->addManualLineItem(
|
||||
$bill,
|
||||
$this->ownerRef($request),
|
||||
$request->validate([
|
||||
'type' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.bill_line_types')))],
|
||||
'description' => ['required', 'string', 'max:255'],
|
||||
'quantity' => ['required', 'integer', 'min:1'],
|
||||
'unit_price_minor' => ['required', 'integer', 'min:0'],
|
||||
]),
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Line item added.');
|
||||
}
|
||||
|
||||
public function applyAdjustments(Request $request, Bill $bill): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.manage');
|
||||
$this->authorizeBill($request, $bill);
|
||||
|
||||
$validated = $request->validate([
|
||||
'discount_minor' => ['nullable', 'integer', 'min:0'],
|
||||
'tax_minor' => ['nullable', 'integer', 'min:0'],
|
||||
]);
|
||||
|
||||
$this->bills->applyAdjustments(
|
||||
$bill,
|
||||
$this->ownerRef($request),
|
||||
(int) ($validated['discount_minor'] ?? 0),
|
||||
(int) ($validated['tax_minor'] ?? 0),
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Bill updated.');
|
||||
}
|
||||
|
||||
public function recordPayment(Request $request, Bill $bill): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'payments.manage');
|
||||
$this->authorizeBill($request, $bill);
|
||||
|
||||
$this->bills->recordPayment(
|
||||
$bill,
|
||||
$this->ownerRef($request),
|
||||
$request->validate([
|
||||
'amount_minor' => ['required', 'integer', 'min:1'],
|
||||
'method' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.payment_methods')))],
|
||||
'reference' => ['nullable', 'string', 'max:100'],
|
||||
'notes' => ['nullable', 'string', 'max:500'],
|
||||
]),
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Payment recorded.');
|
||||
}
|
||||
|
||||
public function void(Request $request, Bill $bill): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.manage');
|
||||
$this->authorizeBill($request, $bill);
|
||||
|
||||
$this->bills->void($bill, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return redirect()->route('care.bills.index')->with('success', 'Bill voided.');
|
||||
}
|
||||
|
||||
public function print(Request $request, Bill $bill): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.view');
|
||||
$this->authorizeBill($request, $bill);
|
||||
|
||||
$bill->load(['patient', 'branch', 'lineItems', 'payments', 'organization']);
|
||||
|
||||
return view('care.bills.print', [
|
||||
'bill' => $bill,
|
||||
'organization' => $this->organization($request),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function authorizeBill(Request $request, Bill $bill): void
|
||||
{
|
||||
$this->authorizeOwner($request, $bill);
|
||||
abort_unless($bill->organization_id === $this->organization($request)->id, 404);
|
||||
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
if ($branchId !== null && $bill->branch_id !== $branchId) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
protected function authorizeVisit(Request $request, Visit $visit): void
|
||||
{
|
||||
$this->authorizeOwner($request, $visit);
|
||||
abort_unless($visit->organization_id === $this->organization($request)->id, 404);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Services\Care\AuditLogger;
|
||||
use App\Services\Care\PlanService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BranchController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->withCount('departments')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.admin.branches.index', compact('branches', 'organization'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||
|
||||
return view('care.admin.branches.create', ['organization' => $this->organization($request)]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$currentCount = Branch::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->count();
|
||||
|
||||
if (! app(PlanService::class)->canAddBranch($organization, $currentCount)) {
|
||||
return back()->withInput()->with('error', 'Your plan allows one branch. Upgrade to Care Pro for unlimited branches.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'code' => ['nullable', 'string', 'max:50'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'phone' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
$branch = Branch::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
...$validated,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'branch.created', $organization->id, $owner, Branch::class, $branch->id);
|
||||
|
||||
return redirect()->route('care.branches.index')->with('success', 'Branch created.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, Branch $branch): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||
$this->authorizeOwner($request, $branch);
|
||||
|
||||
return view('care.admin.branches.edit', compact('branch'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Branch $branch): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||
$this->authorizeOwner($request, $branch);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'code' => ['nullable', 'string', 'max:50'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'phone' => ['nullable', 'string', 'max:50'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$branch->update([
|
||||
...$validated,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
AuditLogger::record(
|
||||
$this->ownerRef($request),
|
||||
'branch.updated',
|
||||
$branch->organization_id,
|
||||
$this->ownerRef($request),
|
||||
Branch::class,
|
||||
$branch->id,
|
||||
);
|
||||
|
||||
return redirect()->route('care.branches.index')->with('success', 'Branch updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care\Concerns;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait ScopesToAccount
|
||||
{
|
||||
protected function ownerRef(Request $request): string
|
||||
{
|
||||
return (string) $request->user()->public_id;
|
||||
}
|
||||
|
||||
protected function organization(Request $request): Organization
|
||||
{
|
||||
$organization = $request->attributes->get('care.organization')
|
||||
?? app(OrganizationResolver::class)->resolveForUser($request->user());
|
||||
|
||||
abort_unless($organization, 404);
|
||||
|
||||
return $organization;
|
||||
}
|
||||
|
||||
protected function member(Request $request): ?Member
|
||||
{
|
||||
return $request->attributes->get('care.member')
|
||||
?? app(OrganizationResolver::class)->memberFor($request->user(), $this->organization($request));
|
||||
}
|
||||
|
||||
protected function authorizeAbility(Request $request, string $ability): void
|
||||
{
|
||||
abort_unless(
|
||||
app(CarePermissions::class)->can($this->member($request), $ability),
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
protected function authorizeOwner(Request $request, Model $model): void
|
||||
{
|
||||
abort_unless($model->getAttribute('owner_ref') === $this->ownerRef($request), 404);
|
||||
}
|
||||
|
||||
protected function scopeToBranch(Request $request, Builder $query, string $column = 'branch_id'): Builder
|
||||
{
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
if ($branchId !== null) {
|
||||
$query->where($column, $branchId);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\InvestigationType;
|
||||
use App\Models\Practitioner;
|
||||
use App\Services\Care\ConsultationService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConsultationController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected ConsultationService $consultations,
|
||||
) {}
|
||||
|
||||
public function show(Request $request, Consultation $consultation): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'consultations.view');
|
||||
$this->authorizeConsultation($request, $consultation);
|
||||
|
||||
$consultation->load([
|
||||
'patient', 'practitioner', 'visit', 'appointment',
|
||||
'vitalSigns', 'diagnoses', 'documents',
|
||||
'investigationRequests.investigationType', 'prescriptions.items',
|
||||
]);
|
||||
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'consultations.manage');
|
||||
$canVitals = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'vitals.manage');
|
||||
$canRequestInvestigations = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'investigations.request');
|
||||
$canPrescribe = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'prescriptions.manage');
|
||||
$canGenerateBill = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'bills.manage');
|
||||
|
||||
$practitioners = Practitioner::owned($this->ownerRef($request))
|
||||
->where('organization_id', $this->organization($request)->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$investigationTypes = InvestigationType::owned($this->ownerRef($request))
|
||||
->where('organization_id', $this->organization($request)->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.consultations.show', [
|
||||
'consultation' => $consultation,
|
||||
'practitioners' => $practitioners,
|
||||
'investigationTypes' => $investigationTypes,
|
||||
'canManage' => $canManage,
|
||||
'canVitals' => $canVitals,
|
||||
'canRequestInvestigations' => $canRequestInvestigations,
|
||||
'canPrescribe' => $canPrescribe,
|
||||
'canGenerateBill' => $canGenerateBill,
|
||||
'isCompleted' => $consultation->status === Consultation::STATUS_COMPLETED,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Consultation $consultation): RedirectResponse
|
||||
{
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'consultations.manage');
|
||||
$canVitals = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'vitals.manage');
|
||||
|
||||
abort_unless($canManage || $canVitals, 403);
|
||||
$this->authorizeConsultation($request, $consultation);
|
||||
abort_if($consultation->status === Consultation::STATUS_COMPLETED, 422);
|
||||
|
||||
$validated = $this->validatedConsultationData($request, $canManage, $canVitals);
|
||||
|
||||
$this->consultations->save(
|
||||
$consultation,
|
||||
$this->ownerRef($request),
|
||||
$validated,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Consultation saved.');
|
||||
}
|
||||
|
||||
public function complete(Request $request, Consultation $consultation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->authorizeConsultation($request, $consultation);
|
||||
|
||||
$this->consultations->complete(
|
||||
$consultation,
|
||||
$this->ownerRef($request),
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.patients.show', $consultation->patient)
|
||||
->with('success', 'Consultation completed.');
|
||||
}
|
||||
|
||||
protected function authorizeConsultation(Request $request, Consultation $consultation): void
|
||||
{
|
||||
$this->authorizeOwner($request, $consultation);
|
||||
$consultation->loadMissing('visit');
|
||||
abort_unless($consultation->visit->organization_id === $this->organization($request)->id, 404);
|
||||
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
if ($branchId !== null && $consultation->visit->branch_id !== $branchId) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedConsultationData(Request $request, bool $canManage, bool $canVitals): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
if ($canManage) {
|
||||
$rules = array_merge($rules, [
|
||||
'symptoms' => ['nullable', 'string', 'max:10000'],
|
||||
'clinical_notes' => ['nullable', 'string', 'max:20000'],
|
||||
'practitioner_id' => ['nullable', 'integer', 'exists:care_practitioners,id'],
|
||||
'diagnoses' => ['nullable', 'array'],
|
||||
'diagnoses.*.code' => ['nullable', 'string', 'max:50'],
|
||||
'diagnoses.*.description' => ['nullable', 'string', 'max:500'],
|
||||
'diagnoses.*.is_primary' => ['nullable', 'boolean'],
|
||||
'diagnoses.*.notes' => ['nullable', 'string', 'max:2000'],
|
||||
'documents' => ['nullable', 'array'],
|
||||
'documents.*' => ['file', 'max:10240', 'mimes:pdf,jpeg,png,jpg,webp'],
|
||||
]);
|
||||
$canVitals = true;
|
||||
}
|
||||
|
||||
if ($canVitals) {
|
||||
$rules['vitals'] = ['nullable', 'array'];
|
||||
$rules['vitals.bp_systolic'] = ['nullable', 'integer', 'min:50', 'max:300'];
|
||||
$rules['vitals.bp_diastolic'] = ['nullable', 'integer', 'min:30', 'max:200'];
|
||||
$rules['vitals.pulse'] = ['nullable', 'integer', 'min:20', 'max:250'];
|
||||
$rules['vitals.temperature'] = ['nullable', 'numeric', 'min:30', 'max:45'];
|
||||
$rules['vitals.weight_kg'] = ['nullable', 'numeric', 'min:0', 'max:500'];
|
||||
$rules['vitals.height_cm'] = ['nullable', 'numeric', 'min:0', 'max:300'];
|
||||
$rules['vitals.spo2'] = ['nullable', 'integer', 'min:50', 'max:100'];
|
||||
$rules['vitals.respiratory_rate'] = ['nullable', 'integer', 'min:5', 'max:80'];
|
||||
}
|
||||
|
||||
return $request->validate($rules);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\ReportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected ReportService $reports,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'dashboard.view');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$branchQuery = Branch::owned($owner)->where('organization_id', $organization->id);
|
||||
$this->scopeToBranch($request, $branchQuery);
|
||||
|
||||
$stats = [
|
||||
'branches' => (clone $branchQuery)->where('is_active', true)->count(),
|
||||
'team_members' => Member::owned($owner)->where('organization_id', $organization->id)->count(),
|
||||
'departments' => $organization->branches()
|
||||
->when(app(OrganizationResolver::class)->branchScope($this->member($request)), function ($q, $branchId) {
|
||||
$q->where('id', $branchId);
|
||||
})
|
||||
->withCount('departments')
|
||||
->get()
|
||||
->sum('departments_count'),
|
||||
];
|
||||
|
||||
$branches = (clone $branchQuery)->withCount('departments')->orderBy('name')->get();
|
||||
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
$operational = $this->reports->dashboardStats($owner, $organization->id, $branchScope);
|
||||
|
||||
return view('care.dashboard', compact('organization', 'stats', 'branches', 'operational'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Department;
|
||||
use App\Services\Care\AuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DepartmentController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.view');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$departments = Department::owned($owner)
|
||||
->whereHas('branch', fn ($q) => $q->where('organization_id', $organization->id))
|
||||
->with('branch')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.admin.departments.index', [
|
||||
'departments' => $departments,
|
||||
'organization' => $organization,
|
||||
'types' => config('care.department_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.admin.departments.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'types' => config('care.department_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'branch_id' => ['required', 'integer', 'exists:care_branches,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.department_types')))],
|
||||
]);
|
||||
|
||||
$branch = Branch::owned($owner)->findOrFail($validated['branch_id']);
|
||||
abort_unless($branch->organization_id === $organization->id, 404);
|
||||
|
||||
$department = Department::create([
|
||||
'owner_ref' => $owner,
|
||||
'branch_id' => $branch->id,
|
||||
'name' => $validated['name'],
|
||||
'type' => $validated['type'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'department.created', $organization->id, $owner, Department::class, $department->id);
|
||||
|
||||
return redirect()->route('care.departments.index')->with('success', 'Department created.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, Department $department): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$this->authorizeOwner($request, $department);
|
||||
|
||||
return view('care.admin.departments.edit', [
|
||||
'department' => $department,
|
||||
'types' => config('care.department_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Department $department): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$this->authorizeOwner($request, $department);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.department_types')))],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$department->update([
|
||||
...$validated,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
AuditLogger::record(
|
||||
$this->ownerRef($request),
|
||||
'department.updated',
|
||||
$department->branch->organization_id,
|
||||
$this->ownerRef($request),
|
||||
Department::class,
|
||||
$department->id,
|
||||
);
|
||||
|
||||
return redirect()->route('care.departments.index')->with('success', 'Department updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Department $department): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$this->authorizeOwner($request, $department);
|
||||
|
||||
$department->load('branch');
|
||||
$organizationId = $department->branch->organization_id;
|
||||
$departmentId = $department->id;
|
||||
|
||||
$department->delete();
|
||||
|
||||
AuditLogger::record(
|
||||
$this->ownerRef($request),
|
||||
'department.deleted',
|
||||
$organizationId,
|
||||
$this->ownerRef($request),
|
||||
Department::class,
|
||||
$departmentId,
|
||||
);
|
||||
|
||||
return redirect()->route('care.departments.index')->with('success', 'Department removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Drug;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\PharmacyService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DrugController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected PharmacyService $pharmacy,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'pharmacy.view');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$drugs = $this->pharmacy->listDrugs(
|
||||
$this->ownerRef($request),
|
||||
$organization->id,
|
||||
$request->only(['q']),
|
||||
$branchScope,
|
||||
);
|
||||
|
||||
$lowStock = $this->pharmacy->lowStock($this->ownerRef($request), $organization->id);
|
||||
$expired = $this->pharmacy->expiredBatches($this->ownerRef($request), $organization->id);
|
||||
|
||||
return view('care.pharmacy.drugs.index', [
|
||||
'organization' => $organization,
|
||||
'drugs' => $drugs,
|
||||
'lowStock' => $lowStock,
|
||||
'expired' => $expired,
|
||||
'canManage' => app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'pharmacy.manage'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'pharmacy.manage');
|
||||
|
||||
return view('care.pharmacy.drugs.create', [
|
||||
'organization' => $this->organization($request),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'pharmacy.manage');
|
||||
|
||||
$drug = $this->pharmacy->createDrug(
|
||||
$this->organization($request),
|
||||
$this->ownerRef($request),
|
||||
$this->validatedDrugData($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.pharmacy.drugs.show', $drug)
|
||||
->with('success', 'Drug added to inventory.');
|
||||
}
|
||||
|
||||
public function show(Request $request, Drug $drug): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'pharmacy.view');
|
||||
$this->authorizeDrug($request, $drug);
|
||||
|
||||
$drug->load(['batches' => fn ($q) => $q->orderBy('expiry_date')]);
|
||||
|
||||
return view('care.pharmacy.drugs.show', [
|
||||
'drug' => $drug,
|
||||
'canManage' => app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'pharmacy.manage'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Request $request, Drug $drug): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'pharmacy.manage');
|
||||
$this->authorizeDrug($request, $drug);
|
||||
|
||||
return view('care.pharmacy.drugs.edit', ['drug' => $drug]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Drug $drug): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'pharmacy.manage');
|
||||
$this->authorizeDrug($request, $drug);
|
||||
|
||||
$this->pharmacy->updateDrug($drug, $this->ownerRef($request), $this->validatedDrugData($request));
|
||||
|
||||
return redirect()->route('care.pharmacy.drugs.show', $drug)
|
||||
->with('success', 'Drug updated.');
|
||||
}
|
||||
|
||||
public function receiveBatch(Request $request, Drug $drug): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'pharmacy.manage');
|
||||
$this->authorizeDrug($request, $drug);
|
||||
|
||||
$this->pharmacy->receiveBatch(
|
||||
$drug,
|
||||
$this->ownerRef($request),
|
||||
$request->validate([
|
||||
'batch_number' => ['required', 'string', 'max:100'],
|
||||
'expiry_date' => ['nullable', 'date', 'after:today'],
|
||||
'quantity_on_hand' => ['required', 'integer', 'min:1'],
|
||||
'cost_minor' => ['nullable', 'integer', 'min:0'],
|
||||
]),
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Stock received.');
|
||||
}
|
||||
|
||||
protected function authorizeDrug(Request $request, Drug $drug): void
|
||||
{
|
||||
$this->authorizeOwner($request, $drug);
|
||||
abort_unless($drug->organization_id === $this->organization($request)->id, 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedDrugData(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'generic_name' => ['nullable', 'string', 'max:255'],
|
||||
'sku' => ['nullable', 'string', 'max:100'],
|
||||
'unit' => ['nullable', 'string', 'max:50'],
|
||||
'unit_price_minor' => ['nullable', 'integer', 'min:0'],
|
||||
'reorder_level' => ['nullable', 'integer', 'min:0'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\InvestigationRequest;
|
||||
use App\Models\InvestigationType;
|
||||
use App\Models\Member;
|
||||
use App\Services\Care\InvestigationService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class InvestigationController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected InvestigationService $investigations,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.view');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$requests = $this->investigations->list(
|
||||
$this->ownerRef($request),
|
||||
$organization->id,
|
||||
$request->only(['status', 'patient_id']),
|
||||
$branchScope,
|
||||
);
|
||||
|
||||
return view('care.lab.requests.index', [
|
||||
'organization' => $organization,
|
||||
'requests' => $requests,
|
||||
'statuses' => config('care.investigation_statuses'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function queue(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->when($branchScope, fn ($q) => $q->where('id', $branchScope))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$branchId = (int) ($request->input('branch_id') ?: $branchScope ?: $branches->first()?->id);
|
||||
$status = $request->input('status');
|
||||
|
||||
$queue = $branchId
|
||||
? $this->investigations->workQueue($this->ownerRef($request), $branchId, $status)
|
||||
: collect();
|
||||
|
||||
return view('care.lab.queue.index', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'branchId' => $branchId,
|
||||
'status' => $status,
|
||||
'queue' => $queue,
|
||||
'statuses' => config('care.investigation_statuses'),
|
||||
'members' => Member::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('role')
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function requestFromConsultation(Request $request, Consultation $consultation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'investigations.request');
|
||||
$this->authorizeConsultation($request, $consultation);
|
||||
|
||||
$validated = $request->validate([
|
||||
'investigation_type_ids' => ['required', 'array', 'min:1'],
|
||||
'investigation_type_ids.*' => ['integer', 'exists:care_investigation_types,id'],
|
||||
'clinical_notes' => ['nullable', 'string', 'max:2000'],
|
||||
'priority' => ['nullable', 'string', 'in:routine,urgent'],
|
||||
]);
|
||||
|
||||
$this->investigations->requestFromConsultation(
|
||||
$consultation,
|
||||
$this->ownerRef($request),
|
||||
$validated['investigation_type_ids'],
|
||||
$validated['clinical_notes'] ?? null,
|
||||
$validated['priority'] ?? 'routine',
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Investigation request(s) submitted.');
|
||||
}
|
||||
|
||||
public function show(Request $request, InvestigationRequest $investigation): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.view');
|
||||
$this->authorizeInvestigation($request, $investigation);
|
||||
|
||||
$investigation->load([
|
||||
'patient', 'investigationType', 'practitioner', 'branch',
|
||||
'result.values', 'result.attachments', 'assignedMember',
|
||||
]);
|
||||
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'lab.manage');
|
||||
$canViewResults = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'lab.results.view');
|
||||
|
||||
return view('care.lab.requests.show', [
|
||||
'investigation' => $investigation,
|
||||
'statuses' => config('care.investigation_statuses'),
|
||||
'canManage' => $canManage,
|
||||
'canViewResults' => $canViewResults,
|
||||
]);
|
||||
}
|
||||
|
||||
public function collectSample(Request $request, InvestigationRequest $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeInvestigation($request, $investigation);
|
||||
|
||||
$validated = $request->validate(['sample_barcode' => ['nullable', 'string', 'max:100']]);
|
||||
|
||||
$this->investigations->collectSample(
|
||||
$investigation,
|
||||
$this->ownerRef($request),
|
||||
$validated['sample_barcode'] ?? null,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Sample collected.');
|
||||
}
|
||||
|
||||
public function startProcessing(Request $request, InvestigationRequest $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeInvestigation($request, $investigation);
|
||||
|
||||
$validated = $request->validate(['assigned_member_id' => ['nullable', 'integer', 'exists:care_members,id']]);
|
||||
|
||||
$this->investigations->startProcessing(
|
||||
$investigation,
|
||||
$this->ownerRef($request),
|
||||
$validated['assigned_member_id'] ?? null,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.lab.requests.show', $investigation)
|
||||
->with('success', 'Processing started.');
|
||||
}
|
||||
|
||||
public function enterResults(Request $request, InvestigationRequest $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeInvestigation($request, $investigation);
|
||||
|
||||
$validated = $request->validate([
|
||||
'value' => ['nullable', 'string', 'max:255'],
|
||||
'result_summary' => ['nullable', 'string', 'max:5000'],
|
||||
'interpretation' => ['nullable', 'string', 'max:5000'],
|
||||
'values' => ['nullable', 'array'],
|
||||
'values.*.parameter' => ['nullable', 'string', 'max:255'],
|
||||
'values.*.value' => ['nullable', 'string', 'max:255'],
|
||||
'attachments' => ['nullable', 'array'],
|
||||
'attachments.*' => ['file', 'max:10240', 'mimes:pdf,jpeg,png,jpg,webp'],
|
||||
]);
|
||||
|
||||
$this->investigations->enterResults(
|
||||
$investigation,
|
||||
$this->ownerRef($request),
|
||||
$validated,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Results submitted for review.');
|
||||
}
|
||||
|
||||
public function approve(Request $request, InvestigationRequest $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeInvestigation($request, $investigation);
|
||||
|
||||
$this->investigations->approve($investigation, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Results approved.');
|
||||
}
|
||||
|
||||
public function deliver(Request $request, InvestigationRequest $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeInvestigation($request, $investigation);
|
||||
|
||||
$this->investigations->deliver($investigation, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Results delivered to patient record.');
|
||||
}
|
||||
|
||||
public function cancel(Request $request, InvestigationRequest $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeInvestigation($request, $investigation);
|
||||
|
||||
$this->investigations->cancel($investigation, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Investigation cancelled.');
|
||||
}
|
||||
|
||||
protected function authorizeConsultation(Request $request, Consultation $consultation): void
|
||||
{
|
||||
$this->authorizeOwner($request, $consultation);
|
||||
$consultation->loadMissing('visit');
|
||||
abort_unless($consultation->visit->organization_id === $this->organization($request)->id, 404);
|
||||
}
|
||||
|
||||
protected function authorizeInvestigation(Request $request, InvestigationRequest $investigation): void
|
||||
{
|
||||
$this->authorizeOwner($request, $investigation);
|
||||
abort_unless($investigation->organization_id === $this->organization($request)->id, 404);
|
||||
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
if ($branchId !== null && $investigation->branch_id !== $branchId) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\InvestigationType;
|
||||
use App\Services\Care\InvestigationService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class InvestigationTypeController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected InvestigationService $investigations,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$types = InvestigationType::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('category')
|
||||
->orderBy('name')
|
||||
->paginate(30);
|
||||
|
||||
return view('care.lab.catalog.index', [
|
||||
'organization' => $organization,
|
||||
'types' => $types,
|
||||
'categories' => config('care.investigation_categories'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
|
||||
return view('care.lab.catalog.create', [
|
||||
'categories' => config('care.investigation_categories'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
|
||||
$type = $this->investigations->createType(
|
||||
$this->organization($request),
|
||||
$this->ownerRef($request),
|
||||
$this->validatedTypeData($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.lab.catalog.index')
|
||||
->with('success', "Investigation type \"{$type->name}\" created.");
|
||||
}
|
||||
|
||||
public function edit(Request $request, InvestigationType $investigationType): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeType($request, $investigationType);
|
||||
|
||||
return view('care.lab.catalog.edit', [
|
||||
'type' => $investigationType,
|
||||
'categories' => config('care.investigation_categories'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, InvestigationType $investigationType): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeType($request, $investigationType);
|
||||
|
||||
$this->investigations->updateType($investigationType, $this->ownerRef($request), $this->validatedTypeData($request));
|
||||
|
||||
return redirect()->route('care.lab.catalog.index')
|
||||
->with('success', 'Investigation type updated.');
|
||||
}
|
||||
|
||||
protected function authorizeType(Request $request, InvestigationType $type): void
|
||||
{
|
||||
$this->authorizeOwner($request, $type);
|
||||
abort_unless($type->organization_id === $this->organization($request)->id, 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedTypeData(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'code' => ['nullable', 'string', 'max:50'],
|
||||
'category' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.investigation_categories')))],
|
||||
'description' => ['nullable', 'string', 'max:2000'],
|
||||
'unit' => ['nullable', 'string', 'max:50'],
|
||||
'reference_low' => ['nullable', 'numeric'],
|
||||
'reference_high' => ['nullable', 'numeric'],
|
||||
'reference_text' => ['nullable', 'string', 'max:255'],
|
||||
'price_minor' => ['nullable', 'integer', 'min:0'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Services\Care\AuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MemberController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$members = Member::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->with('branch')
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
|
||||
return view('care.admin.members.index', [
|
||||
'members' => $members,
|
||||
'organization' => $organization,
|
||||
'roles' => config('care.roles'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.admin.members.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'roles' => config('care.roles'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'user_ref' => ['required', 'string', 'max:255'],
|
||||
'role' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.roles')))],
|
||||
'branch_id' => ['nullable', 'integer', 'exists:care_branches,id'],
|
||||
]);
|
||||
|
||||
$member = Member::updateOrCreate(
|
||||
[
|
||||
'organization_id' => $organization->id,
|
||||
'user_ref' => $validated['user_ref'],
|
||||
],
|
||||
[
|
||||
'owner_ref' => $owner,
|
||||
'role' => $validated['role'],
|
||||
'branch_id' => $validated['branch_id'] ?? null,
|
||||
],
|
||||
);
|
||||
|
||||
AuditLogger::record($owner, 'member.created', $organization->id, $owner, Member::class, $member->id);
|
||||
|
||||
return redirect()->route('care.members.index')->with('success', 'Member saved.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Member $member): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.manage');
|
||||
$this->authorizeOwner($request, $member);
|
||||
|
||||
abort_if($member->user_ref === $this->ownerRef($request), 422, 'You cannot remove yourself.');
|
||||
|
||||
$memberId = $member->id;
|
||||
$organizationId = $member->organization_id;
|
||||
$member->delete();
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'member.deleted', $organizationId, $this->ownerRef($request), Member::class, $memberId);
|
||||
|
||||
return redirect()->route('care.members.index')->with('success', 'Member removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Support\OrganizationBranding;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class OnboardingController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected OrganizationResolver $organizations,
|
||||
) {}
|
||||
|
||||
public function show(Request $request): View|RedirectResponse
|
||||
{
|
||||
if ($this->organizations->isOnboarded($request->user())) {
|
||||
return redirect()->route('care.dashboard');
|
||||
}
|
||||
|
||||
return view('care.onboarding.show', [
|
||||
'user' => $request->user(),
|
||||
'timezones' => timezone_identifiers_list(),
|
||||
'facilityTypes' => [
|
||||
'clinic' => 'Clinic',
|
||||
'hospital' => 'Hospital',
|
||||
'diagnostic' => 'Diagnostic laboratory',
|
||||
'specialist' => 'Specialist practice',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($this->organizations->isOnboarded($request->user())) {
|
||||
return redirect()->route('care.dashboard');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'organization_name' => ['required', 'string', 'max:255'],
|
||||
'facility_type' => ['required', 'string', 'in:clinic,hospital,diagnostic,specialist'],
|
||||
'branch_name' => ['required', 'string', 'max:255'],
|
||||
'branch_address' => ['nullable', 'string', 'max:500'],
|
||||
'branch_phone' => ['nullable', 'string', 'max:50'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
|
||||
]);
|
||||
|
||||
$organization = $this->organizations->completeOnboarding($request->user(), $validated);
|
||||
|
||||
if ($request->hasFile('logo')) {
|
||||
$organization->update([
|
||||
'logo_path' => OrganizationBranding::storeLogo($organization, $request->file('logo')),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('care.dashboard')->with('success', 'Welcome to Ladill Care!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Patient;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\PatientService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PatientController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected PatientService $patients,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'patients.view');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$patients = $this->patients->search(
|
||||
$this->ownerRef($request),
|
||||
$organization->id,
|
||||
$request->only(['q', 'patient_number', 'phone', 'national_id', 'date_of_birth']),
|
||||
$branchScope,
|
||||
);
|
||||
|
||||
return view('care.patients.index', compact('patients', 'organization'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'patients.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.patients.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'genders' => config('care.genders'),
|
||||
'allergySeverities' => config('care.allergy_severities'),
|
||||
'documentTypes' => config('care.document_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'patients.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $this->validatedPatientData($request);
|
||||
|
||||
$patient = $this->patients->create(
|
||||
$organization,
|
||||
$this->ownerRef($request),
|
||||
$validated,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.patients.show', $patient)
|
||||
->with('success', 'Patient registered successfully.');
|
||||
}
|
||||
|
||||
public function show(Request $request, Patient $patient): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'patients.view');
|
||||
$this->authorizePatient($request, $patient);
|
||||
|
||||
$dashboard = $this->patients->dashboard($patient);
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'patients.manage');
|
||||
|
||||
return view('care.patients.show', array_merge($dashboard, [
|
||||
'canManage' => $canManage,
|
||||
'genders' => config('care.genders'),
|
||||
'allergySeverities' => config('care.allergy_severities'),
|
||||
'documentTypes' => config('care.document_types'),
|
||||
]));
|
||||
}
|
||||
|
||||
public function edit(Request $request, Patient $patient): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'patients.manage');
|
||||
$this->authorizePatient($request, $patient);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$patient->load(['allergies', 'conditions', 'familyHistory', 'emergencyContacts', 'insurancePolicies', 'attachments']);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.patients.edit', [
|
||||
'patient' => $patient,
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'genders' => config('care.genders'),
|
||||
'allergySeverities' => config('care.allergy_severities'),
|
||||
'documentTypes' => config('care.document_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Patient $patient): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'patients.manage');
|
||||
$this->authorizePatient($request, $patient);
|
||||
|
||||
$validated = $this->validatedPatientData($request);
|
||||
|
||||
$this->patients->update($patient, $this->ownerRef($request), $validated, $this->ownerRef($request));
|
||||
|
||||
return redirect()->route('care.patients.show', $patient)
|
||||
->with('success', 'Patient record updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Patient $patient): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'patients.manage');
|
||||
$this->authorizePatient($request, $patient);
|
||||
|
||||
$this->patients->delete($patient, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return redirect()->route('care.patients.index')
|
||||
->with('success', 'Patient record archived.');
|
||||
}
|
||||
|
||||
protected function authorizePatient(Request $request, Patient $patient): void
|
||||
{
|
||||
$this->authorizeOwner($request, $patient);
|
||||
abort_unless($patient->organization_id === $this->organization($request)->id, 404);
|
||||
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
if ($branchId !== null && $patient->branch_id !== $branchId) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedPatientData(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'branch_id' => ['nullable', 'integer', 'exists:care_branches,id'],
|
||||
'first_name' => ['required', 'string', 'max:100'],
|
||||
'last_name' => ['required', 'string', 'max:100'],
|
||||
'other_names' => ['nullable', 'string', 'max:100'],
|
||||
'gender' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('care.genders')))],
|
||||
'date_of_birth' => ['nullable', 'date', 'before:today'],
|
||||
'phone' => ['nullable', 'string', 'max:30'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'national_id' => ['nullable', 'string', 'max:50'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'city' => ['nullable', 'string', 'max:100'],
|
||||
'region' => ['nullable', 'string', 'max:100'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
'allergies' => ['nullable', 'array'],
|
||||
'allergies.*.allergen' => ['nullable', 'string', 'max:255'],
|
||||
'allergies.*.severity' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('care.allergy_severities')))],
|
||||
'allergies.*.notes' => ['nullable', 'string', 'max:1000'],
|
||||
'conditions' => ['nullable', 'array'],
|
||||
'conditions.*.condition' => ['nullable', 'string', 'max:255'],
|
||||
'conditions.*.onset_date' => ['nullable', 'date'],
|
||||
'conditions.*.is_chronic' => ['nullable', 'boolean'],
|
||||
'conditions.*.notes' => ['nullable', 'string', 'max:1000'],
|
||||
'family_history' => ['nullable', 'array'],
|
||||
'family_history.*.relation' => ['nullable', 'string', 'max:100'],
|
||||
'family_history.*.condition' => ['nullable', 'string', 'max:255'],
|
||||
'family_history.*.notes' => ['nullable', 'string', 'max:1000'],
|
||||
'emergency_contacts' => ['nullable', 'array'],
|
||||
'emergency_contacts.*.name' => ['nullable', 'string', 'max:255'],
|
||||
'emergency_contacts.*.phone' => ['nullable', 'string', 'max:30'],
|
||||
'emergency_contacts.*.relationship' => ['nullable', 'string', 'max:100'],
|
||||
'emergency_contacts.*.is_primary' => ['nullable', 'boolean'],
|
||||
'insurance' => ['nullable', 'array'],
|
||||
'insurance.*.provider_name' => ['nullable', 'string', 'max:255'],
|
||||
'insurance.*.policy_number' => ['nullable', 'string', 'max:100'],
|
||||
'insurance.*.coverage_type' => ['nullable', 'string', 'max:100'],
|
||||
'insurance.*.expiry_date' => ['nullable', 'date'],
|
||||
'insurance.*.notes' => ['nullable', 'string', 'max:1000'],
|
||||
'attachments' => ['nullable', 'array'],
|
||||
'attachments.*' => ['file', 'max:10240', 'mimes:pdf,jpeg,png,jpg,webp'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\Prescription;
|
||||
use App\Models\Practitioner;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\PharmacyService;
|
||||
use App\Services\Care\PrescriptionService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PrescriptionController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected PrescriptionService $prescriptions,
|
||||
protected PharmacyService $pharmacy,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.view');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$prescriptions = $this->prescriptions->list(
|
||||
$this->ownerRef($request),
|
||||
$organization->id,
|
||||
$request->only(['status', 'patient_id']),
|
||||
$branchScope,
|
||||
);
|
||||
|
||||
return view('care.prescriptions.index', [
|
||||
'organization' => $organization,
|
||||
'prescriptions' => $prescriptions,
|
||||
'statuses' => config('care.prescription_statuses'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function queue(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$queue = $this->prescriptions->pharmacyQueue($this->ownerRef($request), $organization->id);
|
||||
|
||||
$drugs = \App\Models\Drug::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->with(['batches' => fn ($q) => $q->where('quantity_on_hand', '>', 0)->orderBy('expiry_date')])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$canDispense = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'prescriptions.dispense');
|
||||
|
||||
return view('care.prescriptions.queue', [
|
||||
'organization' => $organization,
|
||||
'queue' => $queue,
|
||||
'drugs' => $drugs,
|
||||
'canDispense' => $canDispense,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request, Consultation $consultation): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.manage');
|
||||
$this->authorizeConsultation($request, $consultation);
|
||||
|
||||
$consultation->load(['patient', 'practitioner']);
|
||||
|
||||
$practitioners = Practitioner::owned($this->ownerRef($request))
|
||||
->where('organization_id', $this->organization($request)->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.prescriptions.create', [
|
||||
'consultation' => $consultation,
|
||||
'practitioners' => $practitioners,
|
||||
'routes' => config('care.medication_routes'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, Consultation $consultation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.manage');
|
||||
$this->authorizeConsultation($request, $consultation);
|
||||
|
||||
$prescription = $this->prescriptions->createFromConsultation(
|
||||
$consultation,
|
||||
$this->ownerRef($request),
|
||||
$this->validatedPrescriptionData($request),
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.prescriptions.show', $prescription)
|
||||
->with('success', 'Prescription created.');
|
||||
}
|
||||
|
||||
public function show(Request $request, Prescription $prescription): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.view');
|
||||
$this->authorizePrescription($request, $prescription);
|
||||
|
||||
$prescription->load(['patient', 'practitioner', 'items', 'consultation', 'visit']);
|
||||
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'prescriptions.manage');
|
||||
$canDispense = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'prescriptions.dispense');
|
||||
|
||||
return view('care.prescriptions.show', [
|
||||
'prescription' => $prescription,
|
||||
'statuses' => config('care.prescription_statuses'),
|
||||
'routes' => config('care.medication_routes'),
|
||||
'canManage' => $canManage,
|
||||
'canDispense' => $canDispense,
|
||||
]);
|
||||
}
|
||||
|
||||
public function activate(Request $request, Prescription $prescription): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.manage');
|
||||
$this->authorizePrescription($request, $prescription);
|
||||
|
||||
$this->prescriptions->activate($prescription, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Prescription activated.');
|
||||
}
|
||||
|
||||
public function dispense(Request $request, Prescription $prescription): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.dispense');
|
||||
$this->authorizePrescription($request, $prescription);
|
||||
|
||||
$allocations = $request->input('allocations', []);
|
||||
|
||||
if (! empty($allocations)) {
|
||||
$this->pharmacy->dispensePrescription(
|
||||
$prescription,
|
||||
$this->ownerRef($request),
|
||||
$allocations,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
} else {
|
||||
$this->prescriptions->dispense($prescription, $this->ownerRef($request), $this->ownerRef($request));
|
||||
}
|
||||
|
||||
return redirect()->route('care.prescriptions.queue')
|
||||
->with('success', 'Prescription dispensed.');
|
||||
}
|
||||
|
||||
public function cancel(Request $request, Prescription $prescription): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.manage');
|
||||
$this->authorizePrescription($request, $prescription);
|
||||
|
||||
$this->prescriptions->cancel($prescription, $this->ownerRef($request), $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Prescription cancelled.');
|
||||
}
|
||||
|
||||
protected function authorizeConsultation(Request $request, Consultation $consultation): void
|
||||
{
|
||||
$this->authorizeOwner($request, $consultation);
|
||||
$consultation->loadMissing('visit');
|
||||
abort_unless($consultation->visit->organization_id === $this->organization($request)->id, 404);
|
||||
}
|
||||
|
||||
protected function authorizePrescription(Request $request, Prescription $prescription): void
|
||||
{
|
||||
$this->authorizeOwner($request, $prescription);
|
||||
abort_unless($prescription->organization_id === $this->organization($request)->id, 404);
|
||||
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
if ($branchId !== null) {
|
||||
$prescription->loadMissing('visit');
|
||||
abort_unless($prescription->visit->branch_id === $branchId, 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedPrescriptionData(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'practitioner_id' => ['nullable', 'integer', 'exists:care_practitioners,id'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
'activate' => ['nullable', 'boolean'],
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.is_procedure' => ['nullable', 'boolean'],
|
||||
'items.*.name' => ['required', 'string', 'max:255'],
|
||||
'items.*.dosage' => ['nullable', 'string', 'max:100'],
|
||||
'items.*.frequency' => ['nullable', 'string', 'max:100'],
|
||||
'items.*.duration' => ['nullable', 'string', 'max:100'],
|
||||
'items.*.route' => ['nullable', 'string', 'max:50'],
|
||||
'items.*.quantity' => ['nullable', 'string', 'max:50'],
|
||||
'items.*.instructions' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Practitioner;
|
||||
use App\Services\Care\AppointmentService;
|
||||
use App\Services\Care\ConsultationService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class QueueController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected AppointmentService $appointments,
|
||||
protected ConsultationService $consultations,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.view');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->when($branchScope, fn ($q) => $q->where('id', $branchScope))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$branchId = (int) ($request->input('branch_id') ?: $branchScope ?: $branches->first()?->id);
|
||||
$practitionerId = $request->input('practitioner_id') ? (int) $request->input('practitioner_id') : null;
|
||||
|
||||
$queue = $branchId
|
||||
? $this->appointments->queue($this->ownerRef($request), $branchId, $practitionerId)
|
||||
: collect();
|
||||
|
||||
$inConsultation = Appointment::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('status', Appointment::STATUS_IN_CONSULTATION)
|
||||
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
||||
->when($practitionerId, fn ($q) => $q->where('practitioner_id', $practitionerId))
|
||||
->with(['patient', 'practitioner', 'consultation'])
|
||||
->orderBy('started_at')
|
||||
->get();
|
||||
|
||||
$canManageQueue = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'queue.manage');
|
||||
$canConsult = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'consultations.manage');
|
||||
|
||||
return view('care.queue.index', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'branchId' => $branchId,
|
||||
'practitioners' => Practitioner::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
'practitionerId' => $practitionerId,
|
||||
'queue' => $queue,
|
||||
'inConsultation' => $inConsultation,
|
||||
'canManageQueue' => $canManageQueue,
|
||||
'canConsult' => $canConsult,
|
||||
]);
|
||||
}
|
||||
|
||||
public function start(Request $request, Appointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->authorizeAppointment($request, $appointment);
|
||||
|
||||
$practitionerId = $request->input('practitioner_id')
|
||||
? (int) $request->input('practitioner_id')
|
||||
: $appointment->practitioner_id;
|
||||
|
||||
$this->appointments->startConsultation(
|
||||
$appointment,
|
||||
$this->ownerRef($request),
|
||||
$practitionerId,
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
$consultation = $this->consultations->startFromAppointment(
|
||||
$appointment->fresh(),
|
||||
$this->ownerRef($request),
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return redirect()->route('care.consultations.show', $consultation)
|
||||
->with('success', 'Consultation started.');
|
||||
}
|
||||
|
||||
protected function authorizeAppointment(Request $request, Appointment $appointment): void
|
||||
{
|
||||
$this->authorizeOwner($request, $appointment);
|
||||
abort_unless($appointment->organization_id === $this->organization($request)->id, 404);
|
||||
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
if ($branchId !== null && $appointment->branch_id !== $branchId) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\ReportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected ReportService $reports,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'reports.finance.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.reports.index', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'reports' => config('care.report_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request, string $type): View
|
||||
{
|
||||
$this->authorizeReport($request, $type);
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
$branchId = $request->input('branch_id') ? (int) $request->input('branch_id') : $branchScope;
|
||||
|
||||
[$from, $to] = $this->dateRange($request);
|
||||
|
||||
$data = match ($type) {
|
||||
'patients' => $this->reports->patientsReport($this->ownerRef($request), $organization->id, $from, $to, $branchId),
|
||||
'appointments' => $this->reports->appointmentsReport($this->ownerRef($request), $organization->id, $from, $to, $branchId),
|
||||
'laboratory' => $this->reports->laboratoryReport($this->ownerRef($request), $organization->id, $from, $to, $branchId),
|
||||
'finance' => $this->reports->financeReport($this->ownerRef($request), $organization->id, $from, $to, $branchId),
|
||||
'clinical' => ['diagnoses' => $this->reports->clinicalReport($this->ownerRef($request), $organization->id, $from, $to)],
|
||||
default => abort(404),
|
||||
};
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('care.reports.show', [
|
||||
'type' => $type,
|
||||
'label' => config('care.report_types')[$type] ?? $type,
|
||||
'data' => $data,
|
||||
'from' => $from->toDateString(),
|
||||
'to' => $to->toDateString(),
|
||||
'branchId' => $branchId,
|
||||
'branches' => $branches,
|
||||
'canExport' => app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'reports.finance.export'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request, string $type): StreamedResponse
|
||||
{
|
||||
$this->authorizeReport($request, $type);
|
||||
abort_unless(
|
||||
app(\App\Services\Care\CarePermissions::class)->can($this->member($request), 'reports.finance.export'),
|
||||
403,
|
||||
);
|
||||
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
$branchId = $request->input('branch_id') ? (int) $request->input('branch_id') : $branchScope;
|
||||
[$from, $to] = $this->dateRange($request);
|
||||
|
||||
$data = match ($type) {
|
||||
'patients' => $this->reports->patientsReport($this->ownerRef($request), $organization->id, $from, $to, $branchId),
|
||||
'appointments' => $this->reports->appointmentsReport($this->ownerRef($request), $organization->id, $from, $to, $branchId),
|
||||
'laboratory' => $this->reports->laboratoryReport($this->ownerRef($request), $organization->id, $from, $to, $branchId),
|
||||
'finance' => $this->reports->financeReport($this->ownerRef($request), $organization->id, $from, $to, $branchId),
|
||||
'clinical' => ['diagnoses' => $this->reports->clinicalReport($this->ownerRef($request), $organization->id, $from, $to)],
|
||||
default => abort(404),
|
||||
};
|
||||
|
||||
$filename = "care-report-{$type}-".now()->format('Y-m-d').'.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($data, $type) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
if ($type === 'clinical' && isset($data['diagnoses'])) {
|
||||
fputcsv($handle, ['Diagnosis', 'Count']);
|
||||
foreach ($data['diagnoses'] as $row) {
|
||||
fputcsv($handle, [$row->description, $row->total]);
|
||||
}
|
||||
} else {
|
||||
fputcsv($handle, ['Metric', 'Value']);
|
||||
foreach ($data as $key => $value) {
|
||||
fputcsv($handle, [$key, is_scalar($value) ? $value : json_encode($value)]);
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
}, $filename, ['Content-Type' => 'text/csv']);
|
||||
}
|
||||
|
||||
protected function authorizeReport(Request $request, string $type): void
|
||||
{
|
||||
abort_unless(array_key_exists($type, config('care.report_types')), 404);
|
||||
$this->authorizeAbility($request, 'reports.finance.view');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Carbon, 1: Carbon}
|
||||
*/
|
||||
protected function dateRange(Request $request): array
|
||||
{
|
||||
$from = Carbon::parse($request->input('from', now()->subDays(30)->toDateString()))->startOfDay();
|
||||
$to = Carbon::parse($request->input('to', now()->toDateString()))->endOfDay();
|
||||
|
||||
return [$from, $to];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Services\Care\AuditLogger;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Support\OrganizationBranding;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.view');
|
||||
$organization = $this->organization($request);
|
||||
$canManage = app(CarePermissions::class)->can($this->member($request), 'settings.manage');
|
||||
|
||||
$branchCount = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->count();
|
||||
|
||||
return view('care.settings.edit', [
|
||||
'organization' => $organization,
|
||||
'canManage' => $canManage,
|
||||
'branchCount' => $branchCount,
|
||||
'facilityTypes' => [
|
||||
'clinic' => 'Clinic',
|
||||
'hospital' => 'Hospital',
|
||||
'diagnostic' => 'Diagnostic laboratory',
|
||||
'specialist' => 'Specialist practice',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
'facility_type' => ['required', 'string', 'in:clinic,hospital,diagnostic,specialist'],
|
||||
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
|
||||
'remove_logo' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$settings = $organization->settings ?? [];
|
||||
$settings['onboarded'] = true;
|
||||
$settings['facility_type'] = $validated['facility_type'];
|
||||
|
||||
$logoPath = $organization->logo_path;
|
||||
|
||||
if ($request->boolean('remove_logo')) {
|
||||
OrganizationBranding::deleteStoredLogo($organization);
|
||||
$logoPath = null;
|
||||
}
|
||||
|
||||
if ($request->hasFile('logo')) {
|
||||
$logoPath = OrganizationBranding::storeLogo($organization, $request->file('logo'));
|
||||
}
|
||||
|
||||
$organization->update([
|
||||
'name' => $validated['name'],
|
||||
'timezone' => $validated['timezone'],
|
||||
'logo_path' => $logoPath,
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'organization.updated', $organization->id, $owner, \App\Models\Organization::class, $organization->id);
|
||||
|
||||
return back()->with('success', 'Settings saved.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user