fix(assessments): enable engine by default and wire into live UI
Deploy Ladill Care / deploy (push) Successful in 54s
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:
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user