Files
ladill-care/app/Http/Controllers/Care/AppointmentController.php
T
isaaccladandCursor 0b6cad75a6
Deploy Ladill Care / deploy (push) Successful in 37s
Add hero sections to Patients, Appointments, Queue, and admin index pages.
Introduce shared x-care.page-hero with summary stats so key list views
match the Ladill app hero pattern used across Frontdesk and Link.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 22:57:54 +00:00

258 lines
9.4 KiB
PHP

<?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);
$owner = $this->ownerRef($request);
$appointmentQuery = Appointment::owned($owner)
->where('organization_id', $organization->id)
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope));
$heroStats = [
'today' => (clone $appointmentQuery)->whereDate('scheduled_at', today())->count(),
'scheduled' => (clone $appointmentQuery)->where('status', Appointment::STATUS_SCHEDULED)->count(),
'waiting' => (clone $appointmentQuery)->whereIn('status', [
Appointment::STATUS_CHECKED_IN,
Appointment::STATUS_WAITING,
])->count(),
];
return view('care.appointments.index', [
'organization' => $organization,
'appointments' => $appointments,
'practitioners' => $practitioners,
'statuses' => config('care.appointment_statuses'),
'heroStats' => $heroStats,
]);
}
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'],
]);
}
}