authorizeAbility($request, 'consultations.view'); $this->authorizeConsultation($request, $consultation); $consultation->load([ 'patient', 'practitioner', 'visit', 'appointment', 'vitalSigns', 'diagnoses', 'documents', 'investigationRequests.investigationType', 'prescriptions.items', ]); $permissions = app(CarePermissions::class); $member = $this->member($request); $organization = $this->organization($request); $canManage = $permissions->can($member, 'consultations.manage'); $canVitals = $permissions->can($member, 'vitals.manage'); $canRequestInvestigations = $permissions->can($member, 'investigations.request'); $canPrescribe = $permissions->can($member, 'prescriptions.manage'); $canGenerateBill = $permissions->can($member, 'bills.manage'); $assessmentsEnabled = app(CareFeatures::class)->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE); $canViewAssessments = $assessmentsEnabled && $permissions->can($member, 'assessments.view'); $canCaptureAssessment = $assessmentsEnabled && ( $permissions->can($member, 'assessments.capture') || $permissions->can($member, 'assessments.manage') ); $consultationAssessments = collect(); $startableTemplates = collect(); $universalTemplate = null; $universalAssessment = null; $canCaptureUniversal = false; $canManagePathways = $assessmentsEnabled && $permissions->can($member, 'pathways.manage'); $pathwaySuggestions = collect(); $activePathways = collect(); if ($canViewAssessments) { $consultationAssessments = Assessment::query() ->owned($this->ownerRef($request)) ->where('consultation_id', $consultation->id) ->with(['template', 'answers.question']) ->orderByDesc('created_at') ->get(); $universalTemplate = AssessmentTemplate::currentSystemByCode('universal_intake'); if ($universalTemplate) { $universalAssessment = $consultationAssessments ->first(fn (Assessment $a) => $a->template_id === $universalTemplate->id) ?? Assessment::query() ->owned($this->ownerRef($request)) ->where('patient_id', $consultation->patient_id) ->where('template_id', $universalTemplate->id) ->whereIn('status', [Assessment::STATUS_DRAFT, Assessment::STATUS_COMPLETED]) ->with(['template', 'answers.question']) ->orderByDesc('created_at') ->first(); } if ($canCaptureAssessment) { $startableTemplates = AssessmentTemplate::query() ->system() ->current() ->orderBy('category') ->orderBy('name') ->get() // Universal has a dedicated CTA; keep other templates in the general list. ->reject(fn (AssessmentTemplate $t) => $t->code === 'universal_intake') ->values(); if ($universalTemplate) { try { app(\App\Services\Care\AssessmentService::class) ->assertCaptureAllowed($member, $universalTemplate); $canCaptureUniversal = true; } catch (\Throwable) { $canCaptureUniversal = false; } } } $pathwayService = app(PathwayService::class); $activePathways = $pathwayService->activeFor($consultation->patient) ->load(['pathway.templates', 'assessments.template', 'assessments.score']); // Suggestions from **persisted** diagnoses only (after save). $pathwaySuggestions = $pathwayService->suggest( $consultation->patient, $consultation->diagnoses, ); // Instruments for active pathways (e.g. stroke → NIHSS, mRS) with draft/complete status. $pathwayInstruments = collect(); foreach ($activePathways as $patientPathway) { foreach ($patientPathway->pathway->templates as $binding) { $template = AssessmentTemplate::currentSystemByCode($binding->template_code); $assessment = $patientPathway->assessments ->first(fn (Assessment $a) => $a->template?->code === $binding->template_code) ?? $consultationAssessments ->first(fn (Assessment $a) => $a->template?->code === $binding->template_code) ?? ($template ? Assessment::query() ->owned($this->ownerRef($request)) ->where('patient_id', $consultation->patient_id) ->where('template_id', $template->id) ->with(['template', 'score']) ->orderByDesc('created_at') ->first() : null); $pathwayInstruments->push([ 'pathway_code' => $patientPathway->pathway->code, 'pathway_name' => $patientPathway->pathway->name, 'template_code' => $binding->template_code, 'template_name' => $template?->name ?? $binding->template_code, 'required' => $binding->is_required_on_activation, 'phase' => $binding->phase, 'template' => $template, 'assessment' => $assessment, 'pack_missing' => $template === null, ]); } } } $practitioners = Practitioner::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->where('is_active', true) ->orderBy('name') ->get(); $investigationTypes = InvestigationType::owned($this->ownerRef($request)) ->where('organization_id', $organization->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, 'assessmentsEnabled' => $assessmentsEnabled, 'canViewAssessments' => $canViewAssessments, 'canCaptureAssessment' => $canCaptureAssessment, 'consultationAssessments' => $consultationAssessments, 'startableTemplates' => $startableTemplates, 'assessmentStatuses' => config('care.assessment_statuses'), 'universalTemplate' => $universalTemplate, 'universalAssessment' => $universalAssessment, 'canCaptureUniversal' => $canCaptureUniversal, 'canManagePathways' => $canManagePathways, 'pathwaySuggestions' => $pathwaySuggestions, 'activePathways' => $activePathways, 'pathwayInstruments' => $pathwayInstruments ?? collect(), ]); } 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), ); $message = 'Consultation saved.'; if ($canManage && $request->boolean('suggest_pathways')) { $consultation->load('diagnoses'); $count = $consultation->diagnoses->filter(fn ($d) => filled($d->description) || filled($d->code))->count(); $message = $count > 0 ? 'Diagnoses saved — review pathway suggestions below.' : 'Consultation saved. Add and save diagnoses to see pathway suggestions.'; } return back()->with('success', $message); } 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 */ 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); } }