From b208e59bd02c2beb9c8dadc512668db81f32d816 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 18 Jul 2026 10:15:32 +0000 Subject: [PATCH] Strip healthcare packaging and Care integration from Queue. Queue is general-purpose QMS only; Care runs its own native engine. Co-authored-by: Cursor --- .env.example | 1 - .../Controllers/Api/IntegrationController.php | 20 +- .../Controllers/Qms/SettingsController.php | 6 - .../Middleware/EnsureQueueIntegration.php | 2 +- .../Qms/Industry/IndustryPackageInstaller.php | 6 - app/Support/DemoWorld.php | 94 ++++++- config/industry_packages.php | 134 ---------- config/qms.php | 22 +- resources/views/qms/pro/index.blade.php | 2 +- resources/views/qms/settings/edit.blade.php | 6 +- tests/Feature/CareIntegrationTest.php | 242 ------------------ tests/Feature/DemoSeedCommandTest.php | 2 +- tests/Feature/IndustryPackageTest.php | 65 ++--- tests/Feature/ServicePointRoutingTest.php | 16 +- tests/Feature/SettingsBranchesTeamTest.php | 4 +- tests/Feature/TicketTransferProTest.php | 2 +- 16 files changed, 123 insertions(+), 501 deletions(-) delete mode 100644 tests/Feature/CareIntegrationTest.php diff --git a/.env.example b/.env.example index f6dbbdc..00cdfbc 100644 --- a/.env.example +++ b/.env.example @@ -57,7 +57,6 @@ DEMO_RESET_ON_LOGIN=true # --- Inbound service API keys (sibling Ladill apps calling Queue) --- QUEUE_API_KEY_POS= QUEUE_API_KEY_CRM= -QUEUE_API_KEY_CARE= QUEUE_API_KEY_FRONTDESK= # Optional: platform DB read for SSO user resolution (same host as monolith) diff --git a/app/Http/Controllers/Api/IntegrationController.php b/app/Http/Controllers/Api/IntegrationController.php index 013f991..5e41c4b 100644 --- a/app/Http/Controllers/Api/IntegrationController.php +++ b/app/Http/Controllers/Api/IntegrationController.php @@ -27,7 +27,7 @@ class IntegrationController extends Controller 'data' => [ 'provisioned' => false, 'onboarded' => false, - 'integrations' => ['care' => false, 'frontdesk' => false], + 'integrations' => ['frontdesk' => false], ], ]); } @@ -40,7 +40,6 @@ class IntegrationController extends Controller 'industry' => data_get($organization->settings, 'industry'), 'industry_package' => data_get($organization->settings, 'industry_package.key'), 'integrations' => [ - 'care' => (bool) data_get($organization->settings, 'integrations.care_enabled', false), 'frontdesk' => (bool) data_get($organization->settings, 'integrations.frontdesk_enabled', false), ], ], @@ -61,7 +60,6 @@ class IntegrationController extends Controller ]); $defaultIndustry = match ($caller) { - 'care' => 'healthcare', 'frontdesk' => 'visitor', 'pos' => 'restaurant', default => 'custom', @@ -85,7 +83,6 @@ class IntegrationController extends Controller 'industry' => $industry, 'appointment_mode' => 'hybrid', 'integrations' => [ - 'care_enabled' => false, 'frontdesk_enabled' => false, ], ], @@ -127,19 +124,17 @@ class IntegrationController extends Controller } } - if (in_array($caller, ['care', 'frontdesk'], true)) { + if ($caller === 'frontdesk') { $this->syncIntegrationFlag($organization->fresh(), $caller, true); $organization = $organization->fresh(); } - // Care owns clinical stage graphs; still apply healthcare terminology/package metadata. $packageResult = app(IndustryPackageInstaller::class)->install( $organization->fresh(), $branch, $industry, [ 'actor_ref' => $owner, - 'skip_stages' => $caller === 'care', 'include_optional' => true, ], ); @@ -151,7 +146,6 @@ class IntegrationController extends Controller 'industry' => $industry, 'industry_package' => $packageResult, 'integrations' => [ - 'care' => (bool) data_get($organization->fresh()->settings, 'integrations.care_enabled', false), 'frontdesk' => (bool) data_get($organization->fresh()->settings, 'integrations.frontdesk_enabled', false), ], ], @@ -161,7 +155,6 @@ class IntegrationController extends Controller public function update(Request $request): JsonResponse { $validated = $request->validate([ - 'care_enabled' => ['nullable', 'boolean'], 'frontdesk_enabled' => ['nullable', 'boolean'], ]); @@ -170,9 +163,6 @@ class IntegrationController extends Controller $settings = $organization->settings ?? []; $integrations = $settings['integrations'] ?? []; - if (array_key_exists('care_enabled', $validated) && in_array($caller, ['care', 'crm'], true)) { - $integrations['care_enabled'] = (bool) $validated['care_enabled']; - } if (array_key_exists('frontdesk_enabled', $validated) && in_array($caller, ['frontdesk', 'crm'], true)) { $integrations['frontdesk_enabled'] = (bool) $validated['frontdesk_enabled']; } @@ -183,7 +173,6 @@ class IntegrationController extends Controller return response()->json([ 'data' => [ 'integrations' => [ - 'care' => (bool) ($integrations['care_enabled'] ?? false), 'frontdesk' => (bool) ($integrations['frontdesk_enabled'] ?? false), ], ], @@ -192,14 +181,13 @@ class IntegrationController extends Controller protected function syncIntegrationFlag(Organization $organization, string $caller, bool $enabled): void { - if (! in_array($caller, ['care', 'frontdesk'], true)) { + if ($caller !== 'frontdesk') { return; } $settings = $organization->settings ?? []; $integrations = $settings['integrations'] ?? []; - $key = $caller.'_enabled'; - $integrations[$key] = $enabled; + $integrations['frontdesk_enabled'] = $enabled; $settings['integrations'] = $integrations; $organization->update(['settings' => $settings]); } diff --git a/app/Http/Controllers/Qms/SettingsController.php b/app/Http/Controllers/Qms/SettingsController.php index 610a288..c448955 100644 --- a/app/Http/Controllers/Qms/SettingsController.php +++ b/app/Http/Controllers/Qms/SettingsController.php @@ -72,7 +72,6 @@ class SettingsController extends Controller 'appointment_mode' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.appointment_modes')))], 'industry' => ['nullable', 'string', 'max:64'], 'notifications_enabled' => ['boolean'], - 'care_integration_enabled' => ['nullable', 'boolean'], 'frontdesk_integration_enabled' => ['nullable', 'boolean'], 'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'], 'remove_logo' => ['nullable', 'boolean'], @@ -92,9 +91,6 @@ class SettingsController extends Controller } $settings['notifications_enabled'] = $request->boolean('notifications_enabled', true); $integrations = $settings['integrations'] ?? []; - if ($request->has('care_integration_enabled')) { - $integrations['care_enabled'] = $request->boolean('care_integration_enabled'); - } if ($request->has('frontdesk_integration_enabled')) { $integrations['frontdesk_enabled'] = $request->boolean('frontdesk_integration_enabled'); } @@ -177,8 +173,6 @@ class SettingsController extends Controller if (($result['skipped_reason'] ?? null) === 'plan_queue_limit') { $message .= ' Some stages were skipped because of your plan queue limit — upgrade to Pro for the full template.'; - } elseif (($result['skipped_reason'] ?? null) === 'stages_managed_by_integration') { - $message .= ' Stages are managed by Ladill Care; terminology and announcements were updated.'; } return back()->with('success', $message); diff --git a/app/Http/Middleware/EnsureQueueIntegration.php b/app/Http/Middleware/EnsureQueueIntegration.php index 831faa9..cc902bd 100644 --- a/app/Http/Middleware/EnsureQueueIntegration.php +++ b/app/Http/Middleware/EnsureQueueIntegration.php @@ -12,7 +12,7 @@ class EnsureQueueIntegration public function handle(Request $request, Closure $next): Response { $caller = (string) $request->attributes->get('service_caller', ''); - if (! in_array($caller, ['care', 'frontdesk'], true)) { + if ($caller !== 'frontdesk') { return $next($request); } diff --git a/app/Services/Qms/Industry/IndustryPackageInstaller.php b/app/Services/Qms/Industry/IndustryPackageInstaller.php index d4b538d..72b99cc 100644 --- a/app/Services/Qms/Industry/IndustryPackageInstaller.php +++ b/app/Services/Qms/Industry/IndustryPackageInstaller.php @@ -63,11 +63,6 @@ class IndustryPackageInstaller $replace = (bool) ($options['replace'] ?? false); $actor = $options['actor_ref'] ?? $organization->owner_ref; - // Care (and similar) own clinical stage provisioning — still apply terminology. - if (! $skipStages && data_get($organization->settings, 'integrations.care_enabled') && $key === 'healthcare') { - $skipStages = true; - } - $result = [ 'package' => $key, 'departments' => 0, @@ -81,7 +76,6 @@ class IndustryPackageInstaller return DB::transaction(function () use ($organization, $branch, $package, $key, $skipStages, $includeOptional, $replace, $actor, &$result) { // Replacing a package wipes queues/tickets/points so the new template is clean. - // Care-managed healthcare keeps clinical queues (skip_stages). if ($replace && ! $skipStages) { $this->clearOperationalData($organization); $result['replaced'] = true; diff --git a/app/Support/DemoWorld.php b/app/Support/DemoWorld.php index c2bc1b6..67469b6 100644 --- a/app/Support/DemoWorld.php +++ b/app/Support/DemoWorld.php @@ -186,19 +186,55 @@ final class DemoWorld * account_role?: string * }>> */ + /** + * Care specialty module doctors (Pro / Enterprise). Emails: demo-{tier}-{key}@ladill.com + * + * @var array module key => display name + */ + public const SPECIALTY_DOCTORS = [ + 'emergency' => 'Dr. Kojo Emergency', + 'blood_bank' => 'Dr. Ama Blood Bank', + 'dentistry' => 'Dr. Ama Dental', + 'ophthalmology' => 'Dr. Kofi Eye', + 'physiotherapy' => 'Dr. Efua Physio', + 'maternity' => 'Dr. Abena Maternity', + 'radiology' => 'Dr. Yaw Radiology', + 'cardiology' => 'Dr. Kwesi Cardiology', + 'psychiatry' => 'Dr. Adwoa Psychiatry', + 'pediatrics' => 'Dr. Fiifi Pediatrics', + 'orthopedics' => 'Dr. Kojo Orthopedics', + 'ent' => 'Dr. Esi ENT', + 'oncology' => 'Dr. Mansa Oncology', + 'renal' => 'Dr. Nana Renal', + 'surgery' => 'Dr. Kwadwo Surgery', + 'vaccination' => 'Dr. Afia Vaccination', + 'pathology' => 'Dr. Yaw Pathology', + 'infusion' => 'Dr. Akosua Infusion', + 'dermatology' => 'Dr. Ama Dermatology', + 'podiatry' => 'Dr. Kofi Podiatry', + 'fertility' => 'Dr. Abena Fertility', + 'child_welfare' => 'Dr. Efua Child Welfare', + ]; + public const STAFF = [ 'free' => [ [ 'key' => 'receptionist', 'email' => 'demo-free-receptionist@ladill.com', 'name' => 'Abena Reception (Free)', - 'apps' => ['care', 'frontdesk', 'queue'], + 'apps' => ['care', 'frontdesk'], 'roles' => [ 'care' => 'receptionist', 'frontdesk' => 'receptionist', - 'queue' => 'receptionist', ], ], + [ + 'key' => 'queue', + 'email' => 'demo-free-queue@ladill.com', + 'name' => 'Ama Queue (Free)', + 'apps' => ['queue'], + 'roles' => ['queue' => 'receptionist'], + ], ], 'pro' => [ [ @@ -219,13 +255,19 @@ final class DemoWorld 'key' => 'receptionist', 'email' => 'demo-pro-receptionist@ladill.com', 'name' => 'Abena Reception (Pro)', - 'apps' => ['care', 'frontdesk', 'queue'], + 'apps' => ['care', 'frontdesk'], 'roles' => [ 'care' => 'receptionist', 'frontdesk' => 'receptionist', - 'queue' => 'receptionist', ], ], + [ + 'key' => 'queue', + 'email' => 'demo-pro-queue@ladill.com', + 'name' => 'Ama Queue (Pro)', + 'apps' => ['queue'], + 'roles' => ['queue' => 'receptionist'], + ], [ 'key' => 'pharmacist', 'email' => 'demo-pro-pharmacist@ladill.com', @@ -292,13 +334,19 @@ final class DemoWorld 'key' => 'receptionist', 'email' => 'demo-enterprise-receptionist@ladill.com', 'name' => 'Abena Reception', - 'apps' => ['care', 'frontdesk', 'queue'], + 'apps' => ['care', 'frontdesk'], 'roles' => [ 'care' => 'receptionist', 'frontdesk' => 'receptionist', - 'queue' => 'receptionist', ], ], + [ + 'key' => 'queue', + 'email' => 'demo-enterprise-queue@ladill.com', + 'name' => 'Ama Queue', + 'apps' => ['queue'], + 'roles' => ['queue' => 'receptionist'], + ], [ 'key' => 'pharmacist', 'email' => 'demo-enterprise-pharmacist@ladill.com', @@ -471,7 +519,35 @@ final class DemoWorld */ public static function staff(string $tier): array { - return self::STAFF[self::normalizeTier($tier)] ?? []; + $tier = self::normalizeTier($tier); + $roster = self::STAFF[$tier] ?? []; + if (! in_array($tier, ['pro', 'enterprise'], true)) { + return $roster; + } + + foreach (self::SPECIALTY_DOCTORS as $key => $name) { + $email = sprintf('demo-%s-%s@ladill.com', $tier, $key); + $already = false; + foreach ($roster as $row) { + if (strtolower((string) ($row['email'] ?? '')) === $email) { + $already = true; + break; + } + } + if ($already) { + continue; + } + $suffix = $tier === 'pro' ? ' (Pro)' : ''; + $roster[] = [ + 'key' => $key, + 'email' => $email, + 'name' => $name.$suffix, + 'apps' => ['care'], + 'roles' => ['care' => 'doctor'], + ]; + } + + return $roster; } /** @@ -484,8 +560,8 @@ final class DemoWorld return null; } - foreach (self::STAFF as $roster) { - foreach ($roster as $member) { + foreach (['free', 'pro', 'enterprise'] as $tier) { + foreach (self::staff($tier) as $member) { if (strtolower($member['email']) === $email) { return $member; } diff --git a/config/industry_packages.php b/config/industry_packages.php index 8b2f082..65f87e3 100644 --- a/config/industry_packages.php +++ b/config/industry_packages.php @@ -16,140 +16,6 @@ return [ 'packages' => [ - 'healthcare' => [ - 'label' => 'Healthcare', - 'description' => 'Hospitals and clinics — multi-department patient journeys with doctor-specific service points.', - 'ticket_entity' => 'patient', - 'integrations' => ['care'], - 'display_layout' => 'standard', - 'terminology' => [ - 'ticket' => 'Patient', - 'tickets' => 'Patients', - 'queue' => 'Waiting list', - 'service_point' => 'Room / desk', - 'worker' => 'Clinician', - 'customer' => 'Patient', - 'call_next' => 'Call next patient', - ], - 'announcement' => [ - 'en' => 'Patient :ticket, please proceed to :destination.', - 'fr' => 'Patient :ticket, veuillez vous rendre au :destination.', - 'with_staff' => ':ticket, :staff, :destination.', - ], - 'kpis' => [ - 'avg_wait_time', - 'consultation_time', - 'doctor_utilization', - 'patient_throughput', - ], - 'workflow' => [ - 'name' => 'Patient journey', - 'description' => 'Registration through discharge', - ], - 'departments' => [ - ['code' => 'reception', 'name' => 'Registration', 'type' => 'reception'], - ['code' => 'triage', 'name' => 'Triage', 'type' => 'consultation'], - ['code' => 'consultation', 'name' => 'Consultation', 'type' => 'consultation'], - ['code' => 'laboratory', 'name' => 'Laboratory', 'type' => 'laboratory'], - ['code' => 'imaging', 'name' => 'Imaging', 'type' => 'laboratory'], - ['code' => 'pharmacy', 'name' => 'Pharmacy', 'type' => 'pharmacy'], - ['code' => 'billing', 'name' => 'Billing', 'type' => 'cashier'], - ], - 'stages' => [ - [ - 'code' => 'reception', - 'department' => 'reception', - 'name' => 'Registration', - 'prefix' => 'R', - 'strategy' => 'fifo', - 'routing_mode' => 'shared_pool', - 'routing_strategy' => 'round_robin', - 'optional' => false, - 'service_points' => [ - ['name' => 'Registration Desk 1', 'destination' => 'Registration Desk 1', 'code' => 'RD1'], - ], - ], - [ - 'code' => 'triage', - 'department' => 'triage', - 'name' => 'Triage', - 'prefix' => 'T', - 'strategy' => 'priority', - 'routing_mode' => 'assigned_only', - 'routing_strategy' => 'manual_assignment', - 'optional' => true, - 'service_points' => [ - ['name' => 'Triage Station 1', 'destination' => 'Triage Station 1', 'code' => 'TS1'], - ], - ], - [ - 'code' => 'consultation', - 'department' => 'consultation', - 'name' => 'Consultation', - 'prefix' => 'C', - 'strategy' => 'appointment', - 'routing_mode' => 'assigned_only', - 'routing_strategy' => 'doctor_assignment', - 'optional' => false, - 'service_points' => [ - ['name' => 'Consultation Room 1', 'destination' => 'Consultation Room 1', 'code' => 'CR1'], - ], - ], - [ - 'code' => 'laboratory', - 'department' => 'laboratory', - 'name' => 'Laboratory', - 'prefix' => 'L', - 'strategy' => 'fifo', - 'routing_mode' => 'shared_pool', - 'routing_strategy' => 'least_busy', - 'optional' => true, - 'service_points' => [ - ['name' => 'Sample Collection Desk 1', 'destination' => 'Sample Desk 1', 'code' => 'LD1'], - ], - ], - [ - 'code' => 'imaging', - 'department' => 'imaging', - 'name' => 'Imaging', - 'prefix' => 'I', - 'strategy' => 'fifo', - 'routing_mode' => 'shared_pool', - 'routing_strategy' => 'skill_based', - 'optional' => true, - 'service_points' => [ - ['name' => 'X-Ray', 'destination' => 'X-Ray Room', 'code' => 'XR1'], - ], - ], - [ - 'code' => 'pharmacy', - 'department' => 'pharmacy', - 'name' => 'Pharmacy', - 'prefix' => 'P', - 'strategy' => 'fifo', - 'routing_mode' => 'shared_pool', - 'routing_strategy' => 'least_busy', - 'optional' => false, - 'service_points' => [ - ['name' => 'Pharmacy Counter 1', 'destination' => 'Pharmacy Counter 1', 'code' => 'PC1'], - ], - ], - [ - 'code' => 'billing', - 'department' => 'billing', - 'name' => 'Billing', - 'prefix' => 'B', - 'strategy' => 'fifo', - 'routing_mode' => 'shared_pool', - 'routing_strategy' => 'round_robin', - 'optional' => false, - 'service_points' => [ - ['name' => 'Cashier 1', 'destination' => 'Cashier 1', 'code' => 'CA1'], - ], - ], - ], - ], - 'restaurant' => [ 'label' => 'Restaurant', 'description' => 'Kitchen production queues and pickup — integrates with Ladill POS.', diff --git a/config/qms.php b/config/qms.php index 8d101fc..99e1ffc 100644 --- a/config/qms.php +++ b/config/qms.php @@ -58,14 +58,6 @@ return [ | Missing keys fall back to ticket_priorities above. */ 'ticket_priorities_by_industry' => [ - 'healthcare' => [ - 'emergency' => 'Emergency', - 'vip' => 'VIP', - 'elderly' => 'Elderly', - 'disabled' => 'Disabled', - 'appointment' => 'Appointment', - 'walk_in' => 'Walk-in', - ], 'restaurant' => [ 'emergency' => 'Rush', 'vip' => 'VIP table', @@ -243,7 +235,6 @@ return [ |-------------------------------------------------------------------------- */ 'industry_templates' => [ - 'healthcare' => 'Healthcare', 'restaurant' => 'Restaurant', 'banking' => 'Banking', 'government' => 'Government Office', @@ -264,7 +255,7 @@ return [ 'round_robin' => 'Round robin', 'least_busy' => 'Least busy', 'skill_based' => 'Skill based', - 'doctor_assignment' => 'Doctor assignment', + 'doctor_assignment' => 'Staff assignment', 'manual_assignment' => 'Manual assignment', 'priority_assignment' => 'Priority assignment', 'appointment_based' => 'Appointment based', @@ -407,14 +398,6 @@ return [ | Legacy thin workflow name lists — prefer industry_packages.stages for installs. */ 'workflow_templates' => [ - 'healthcare' => [ - ['name' => 'Registration', 'sort_order' => 1], - ['name' => 'Triage', 'sort_order' => 2], - ['name' => 'Consultation', 'sort_order' => 3], - ['name' => 'Laboratory', 'sort_order' => 4], - ['name' => 'Pharmacy', 'sort_order' => 5], - ['name' => 'Billing', 'sort_order' => 6], - ], 'banking' => [ ['name' => 'Reception', 'sort_order' => 1], ['name' => 'Teller', 'sort_order' => 2], @@ -438,7 +421,6 @@ return [ 'service_api_keys' => array_filter([ 'pos' => env('QUEUE_API_KEY_POS'), 'crm' => env('QUEUE_API_KEY_CRM'), - 'care' => env('QUEUE_API_KEY_CARE'), 'frontdesk' => env('QUEUE_API_KEY_FRONTDESK'), ]), @@ -446,7 +428,7 @@ return [ 'upgrade_banner' => [ 'title' => 'Unlock Queue Pro or Enterprise', - 'description' => 'Unlimited branches, advanced routing, Care & Frontdesk integrations — from GHS 2490/branch/mo.', + 'description' => 'Unlimited branches, advanced routing, Frontdesk & POS integrations — from GHS 2490/branch/mo.', 'route' => 'qms.pro.index', ], diff --git a/resources/views/qms/pro/index.blade.php b/resources/views/qms/pro/index.blade.php index 26ca541..463ddc4 100644 --- a/resources/views/qms/pro/index.blade.php +++ b/resources/views/qms/pro/index.blade.php @@ -100,7 +100,7 @@
  • Advanced routing rules
  • Unlimited kiosks & displays
  • Ladill-provided queue kiosk, display and audio system
  • -
  • Care & Frontdesk API
  • +
  • Frontdesk & POS API
  • Reports & exports
  • diff --git a/resources/views/qms/settings/edit.blade.php b/resources/views/qms/settings/edit.blade.php index 343a2c2..358aad6 100644 --- a/resources/views/qms/settings/edit.blade.php +++ b/resources/views/qms/settings/edit.blade.php @@ -132,12 +132,8 @@ @endif - +
    -