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, ); $owner = $this->ownerRef($request); $patientQuery = Patient::owned($owner) ->where('organization_id', $organization->id) ->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope)); $heroStats = [ 'total' => (clone $patientQuery)->count(), 'new_this_month' => (clone $patientQuery)->where('created_at', '>=', now()->startOfMonth())->count(), 'with_visits' => (clone $patientQuery)->whereHas('appointments')->count(), ]; return view('care.patients.index', compact('patients', 'organization', 'heroStats')); } 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'); $credential = app(\App\Services\Messaging\MessagingCredentialsService::class) ->forOrganization($this->organization($request)); return view('care.patients.show', array_merge($dashboard, [ 'canManage' => $canManage, 'messagingSmsReady' => $credential->hasValidSms(), 'messagingEmailReady' => $credential->hasValidBird(), '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 */ 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'], ]); } }