fix(assessments): enable engine by default and wire into live UI
Deploy Ladill Care / deploy (push) Successful in 54s

The assessment engine was built but opt-in and hidden: flag defaulted
off, no sidebar entry, and no settings toggle. Default assessments_engine
on, auto-seed catalog when empty, add Settings toggle and Assessments
nav, and surface guidance from the patients list.
This commit is contained in:
isaacclad
2026-07-16 23:05:48 +00:00
parent 2ce4bc8993
commit f6780b9958
13 changed files with 120 additions and 4 deletions
@@ -250,6 +250,7 @@ class AssessmentController extends Controller
$this->features->enabled($this->organization($request), CareFeatures::ASSESSMENTS_ENGINE),
404,
);
$this->features->ensureAssessmentCatalog();
}
protected function authorizeCapture(Request $request): void
@@ -164,6 +164,7 @@ class PathwayController extends Controller
$this->features->enabled($this->organization($request), CareFeatures::ASSESSMENTS_ENGINE),
404,
);
$this->features->ensureAssessmentCatalog();
}
protected function authorizePatient(Request $request, Patient $patient): void
@@ -294,6 +294,7 @@ class AssessmentController extends Controller
$this->features->enabled($this->organization($request), CareFeatures::ASSESSMENTS_ENGINE),
404,
);
$this->features->ensureAssessmentCatalog();
}
protected function authorizeCapture(Request $request): void
@@ -47,7 +47,11 @@ class ConsultationController extends Controller
$canPrescribe = $permissions->can($member, 'prescriptions.manage');
$canGenerateBill = $permissions->can($member, 'bills.manage');
$assessmentsEnabled = app(CareFeatures::class)->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE);
$careFeatures = app(CareFeatures::class);
$assessmentsEnabled = $careFeatures->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE);
if ($assessmentsEnabled) {
$careFeatures->ensureAssessmentCatalog();
}
$canViewAssessments = $assessmentsEnabled && $permissions->can($member, 'assessments.view');
$canCaptureAssessment = $assessmentsEnabled && (
$permissions->can($member, 'assessments.capture')
@@ -94,6 +94,7 @@ class OutcomeController extends Controller
$this->features->enabled($this->organization($request), CareFeatures::ASSESSMENTS_ENGINE),
404,
);
$this->features->ensureAssessmentCatalog();
}
protected function authorizePatient(Request $request, Patient $patient): void
@@ -149,6 +149,7 @@ class PathwayController extends Controller
$this->features->enabled($org, CareFeatures::ASSESSMENTS_ENGINE),
404,
);
$this->features->ensureAssessmentCatalog();
}
protected function authorizePatient(Request $request, Patient $patient): void
@@ -102,7 +102,11 @@ class PatientController extends Controller
$suiteEmailReady = app(\App\Services\Billing\PlatformEmailClient::class)->isConfigured();
$suiteSmsReady = app(\App\Services\Billing\PlatformSmsClient::class)->isConfigured();
$assessmentsEnabled = app(CareFeatures::class)->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE);
$careFeatures = app(CareFeatures::class);
$assessmentsEnabled = $careFeatures->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE);
if ($assessmentsEnabled) {
$careFeatures->ensureAssessmentCatalog();
}
$canViewAssessments = $assessmentsEnabled && $permissions->can($member, 'assessments.view');
$canCaptureAssessment = $assessmentsEnabled && (
$permissions->can($member, 'assessments.capture')
@@ -7,6 +7,7 @@ use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Models\Branch;
use App\Services\Care\AppointmentPatientNotifier;
use App\Services\Care\AuditLogger;
use App\Services\Care\CareFeatures;
use App\Services\Care\CarePermissions;
use App\Services\Care\PlanService;
use App\Services\Billing\PlatformEmailClient;
@@ -45,6 +46,8 @@ class SettingsController extends Controller
$suiteEmailReady = $platformEmail->isConfigured();
$suiteSmsReady = $platformSms->isConfigured();
$careFeatures = app(CareFeatures::class);
return view('care.settings.edit', [
'organization' => $organization,
'canManage' => $canManage,
@@ -53,6 +56,7 @@ class SettingsController extends Controller
'canViewTeam' => $permissions->can($member, 'admin.members.view'),
'hasBranchesFeature' => $plans->hasFeature($organization, 'branches'),
'canUseQueueIntegration' => $plans->canUseQueueIntegration($organization),
'assessmentsEngineEnabled' => $careFeatures->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE),
'messagingSmsReady' => $suiteSmsReady || $credential->hasValidSms(),
'messagingEmailReady' => $suiteEmailReady || $credential->hasValidBird(),
'suiteMessagingReady' => $suiteEmailReady || $suiteSmsReady,
@@ -82,6 +86,7 @@ class SettingsController extends Controller
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
'remove_logo' => ['nullable', 'boolean'],
'queue_integration_enabled' => ['nullable', 'boolean'],
'assessments_engine' => ['nullable', 'boolean'],
'notify_appointment_booked_sms' => ['nullable', 'boolean'],
'notify_appointment_booked_email' => ['nullable', 'boolean'],
'notify_video_scheduled_sms' => ['nullable', 'boolean'],
@@ -92,6 +97,14 @@ class SettingsController extends Controller
$settings['onboarded'] = true;
$settings['facility_type'] = $validated['facility_type'];
$rollout = is_array($settings['rollout'] ?? null) ? $settings['rollout'] : [];
// Explicit save so opt-out is stored (engine defaults ON when unset).
$rollout[CareFeatures::ASSESSMENTS_ENGINE] = $request->boolean('assessments_engine');
$settings['rollout'] = $rollout;
if ($rollout[CareFeatures::ASSESSMENTS_ENGINE]) {
app(CareFeatures::class)->ensureAssessmentCatalog();
}
// Queue integration is Pro/Enterprise only — free plans cannot enable it.
$wantsQueue = $request->boolean('queue_integration_enabled');
if ($wantsQueue && ! $plans->canUseQueueIntegration($organization)) {
+46 -1
View File
@@ -2,7 +2,12 @@
namespace App\Services\Care;
use App\Models\AssessmentTemplate;
use App\Models\Organization;
use Database\Seeders\AssessmentTemplateSeeder;
use Database\Seeders\ClinicalPathwaySeeder;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
/**
* Product rollout flags stored under organization settings.rollout.*
@@ -16,12 +21,25 @@ class CareFeatures
public const ASSESSMENT_REQUIRED_ON_COMPLETE = 'assessment_required_on_complete';
/**
* Flags that default ON when unset (opt-out). Others default OFF (opt-in).
*
* @var list<string>
*/
protected array $defaultOn = [
self::ASSESSMENTS_ENGINE,
];
public function enabled(Organization $organization, string $flag): bool
{
$settings = $organization->settings ?? [];
$rollout = is_array($settings['rollout'] ?? null) ? $settings['rollout'] : [];
return (bool) ($rollout[$flag] ?? false);
if (array_key_exists($flag, $rollout)) {
return (bool) $rollout[$flag];
}
return in_array($flag, $this->defaultOn, true);
}
/**
@@ -37,4 +55,31 @@ class CareFeatures
return $organization->fresh();
}
/**
* Ensure platform assessment/pathway catalog exists (idempotent seed).
* Call when the engine is enabled so consult/patient UI has templates.
*/
public function ensureAssessmentCatalog(): void
{
if (! Schema::hasTable('care_assessment_templates')) {
return;
}
if (AssessmentTemplate::query()->whereNull('organization_id')->exists()) {
return;
}
Artisan::call('db:seed', [
'--class' => AssessmentTemplateSeeder::class,
'--force' => true,
]);
if (Schema::hasTable('care_clinical_pathways')) {
Artisan::call('db:seed', [
'--class' => ClinicalPathwaySeeder::class,
'--force' => true,
]);
}
}
}
+12 -1
View File
@@ -23,13 +23,24 @@
@endif
</x-care.page-hero>
@if (request('focus') === 'assessments')
<div class="rounded-2xl border border-sky-100 bg-sky-50 px-4 py-3 text-sm text-sky-900">
<p class="font-medium">Clinical assessments</p>
<p class="mt-1 text-sky-800/90">
Open a patient chart for assessments, pathways, and outcomes or start a visit from
<a href="{{ route('care.queue.index') }}" class="font-medium underline">Queue</a>
and use <strong>Universal intake</strong> / <strong>Clinical pathways</strong> on the consultation.
</p>
</div>
@endif
<form method="GET" class="flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
<input type="search" name="q" value="{{ request('q') }}" placeholder="Name, phone, patient ID, national ID…"
class="min-w-[200px] flex-1 rounded-lg border-slate-300 text-sm">
<input type="date" name="date_of_birth" value="{{ request('date_of_birth') }}"
class="rounded-lg border-slate-300 text-sm" title="Date of birth">
<button type="submit" class="btn-primary">Search</button>
@if (request()->hasAny(['q', 'date_of_birth', 'phone', 'national_id', 'patient_number']))
@if (request()->hasAny(['q', 'date_of_birth', 'phone', 'national_id', 'patient_number', 'focus']))
<a href="{{ route('care.patients.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Clear</a>
@endif
</form>
@@ -89,6 +89,21 @@
@endif
@if ($canManage)
<x-settings.card title="Clinical assessments" description="Layered intake, disease pathways (stroke, diabetes, COPD, …), scoring instruments, and outcome tracking on consultations and patient charts.">
<label class="flex items-start gap-3 text-sm">
<input type="checkbox" name="assessments_engine" value="1"
@checked(old('assessments_engine', $assessmentsEngineEnabled ?? true))
class="mt-0.5 rounded border-slate-300">
<span>
<span class="font-medium text-slate-800">Enable clinical assessments engine</span>
<span class="mt-0.5 block text-xs text-slate-500">
Shows universal intake, pathway suggestions, and disease forms on consultations.
Open a patient consultation (Queue) or patient chart Assessments / Pathways / Outcomes.
</span>
</span>
</label>
</x-settings.card>
<x-settings.card title="Messaging" description="Patient email and SMS use suite messaging by default — no Bird or SMS API keys required.">
<p class="text-sm text-slate-600">
Set a Ladill mailbox (From address) and optional SMS sender ID under Account Messaging.
@@ -30,6 +30,23 @@
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.25 4.5h13.5a.75.75 0 0 1 .75.75v15.75a.75.75 0 0 1-.75.75H5.25a.75.75 0 0 1-.75-.75V5.25a.75.75 0 0 1 .75-.75Z" />'];
}
// Clinical assessments (layered intake / pathways) — on by default; toggle in Settings.
$assessmentsEngineOn = $organization
&& app(\App\Services\Care\CareFeatures::class)->enabled(
$organization,
\App\Services\Care\CareFeatures::ASSESSMENTS_ENGINE,
);
if ($assessmentsEngineOn && $permissions->can($member, 'assessments.view') && $permissions->can($member, 'patients.view')) {
$nav[] = [
'name' => 'Assessments',
'route' => route('care.patients.index', ['focus' => 'assessments']),
'active' => request()->routeIs('care.assessments.*')
|| request()->routeIs('care.pathways.*')
|| request()->routeIs('care.outcomes.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />',
];
}
// Lab, pharmacy, billing, and Queue service counters are Pro/Enterprise modules.
if (! empty($hasPaidPlan)) {
if ($permissions->can($member, 'lab.view')) {
+2
View File
@@ -91,6 +91,7 @@ class CareAssessmentApiTest extends TestCase
public function test_api_lists_templates_and_404_when_flag_off(): void
{
// Engine defaults ON when rollout flag is unset.
$this->getJson('/api/v1/assessment-templates')
->assertOk()
->assertJsonPath('data.0.code', fn ($c) => is_string($c));
@@ -100,6 +101,7 @@ class CareAssessmentApiTest extends TestCase
'rollout' => [CareFeatures::ASSESSMENTS_ENGINE => false],
]),
]);
$this->organization->refresh();
$this->getJson('/api/v1/assessment-templates')->assertNotFound();
}