Expand specialty modules; pin Emergency and Blood Bank on Pro.
Deploy Ladill Care / deploy (push) Successful in 1m37s
Deploy Ladill Care / deploy (push) Successful in 1m37s
Add Cardiology through Child Welfare to the specialty catalog, keep Emergency and Blood Bank always enabled on Pro/Enterprise sidebars, and generate matching demo specialty doctors. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,6 +21,11 @@ class SettingsModulesController extends Controller
|
||||
$member = $this->member($request);
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)->can($member, 'settings.manage');
|
||||
|
||||
if ($modules->canManage($organization)) {
|
||||
$modules->ensureDefaultModulesProvisioned($organization, $this->ownerRef($request));
|
||||
$organization->refresh();
|
||||
}
|
||||
|
||||
return view('care.settings.modules', [
|
||||
'organization' => $organization,
|
||||
'canManage' => $canManage,
|
||||
@@ -46,9 +51,13 @@ class SettingsModulesController extends Controller
|
||||
$desired = [];
|
||||
foreach ($keys as $key) {
|
||||
$desired[$key] = $request->boolean("modules.{$key}");
|
||||
if ($modules->isDefaultOnPaidPlans($key)) {
|
||||
$desired[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $modules->sync($organization, $this->ownerRef($request), $desired);
|
||||
$modules->ensureDefaultModulesProvisioned($organization, $this->ownerRef($request));
|
||||
$result = $modules->sync($organization->fresh(), $this->ownerRef($request), $desired);
|
||||
|
||||
if ($result['errors'] !== []) {
|
||||
return back()->with('error', 'Some modules could not be updated: '.implode(' ', $result['errors']));
|
||||
|
||||
@@ -802,11 +802,28 @@ class DemoTenantSeeder
|
||||
}
|
||||
|
||||
$reasons = [
|
||||
'emergency' => ['Chest pain', 'Trauma review', 'Acute fever'],
|
||||
'blood_bank' => ['Cross-match request', 'Transfusion review', 'Blood product issue'],
|
||||
'dentistry' => ['Toothache', 'Dental cleaning', 'Extraction review'],
|
||||
'ophthalmology' => ['Blurry vision', 'Eye exam', 'Contact lens fitting'],
|
||||
'physiotherapy' => ['Back pain rehab', 'Post-injury physio', 'Mobility session'],
|
||||
'maternity' => ['Antenatal visit', 'Pregnancy check', 'Postnatal review'],
|
||||
'radiology' => ['X-ray referral', 'Ultrasound', 'Imaging review'],
|
||||
'cardiology' => ['Chest pain review', 'ECG follow-up', 'Hypertension clinic'],
|
||||
'psychiatry' => ['Mental health review', 'Counselling session', 'Medication review'],
|
||||
'pediatrics' => ['Well-child visit', 'Fever in child', 'Growth check'],
|
||||
'orthopedics' => ['Fracture follow-up', 'Joint pain', 'Cast review'],
|
||||
'ent' => ['Ear infection', 'Sore throat', 'Sinus review'],
|
||||
'oncology' => ['Chemo day-care', 'Oncology review', 'Symptom management'],
|
||||
'renal' => ['Dialysis session', 'Nephrology review', 'Fluid assessment'],
|
||||
'surgery' => ['Pre-op assessment', 'Surgical review', 'Wound check'],
|
||||
'vaccination' => ['Routine immunization', 'Catch-up vaccine', 'Travel vaccine'],
|
||||
'pathology' => ['Biopsy review', 'Histology result', 'Cytology sample'],
|
||||
'infusion' => ['IV infusion', 'Iron infusion', 'Day-care therapy'],
|
||||
'dermatology' => ['Skin rash', 'Mole review', 'Dermatology procedure'],
|
||||
'podiatry' => ['Diabetic foot care', 'Nail procedure', 'Foot pain'],
|
||||
'fertility' => ['Fertility consult', 'Cycle review', 'IVF follow-up'],
|
||||
'child_welfare' => ['Growth monitoring', 'Immunization CWC', 'Well-baby visit'],
|
||||
];
|
||||
|
||||
$count = 0;
|
||||
|
||||
@@ -41,15 +41,69 @@ class SpecialtyModuleService
|
||||
return $catalog[$key] ?? null;
|
||||
}
|
||||
|
||||
public function isDefaultOnPaidPlans(string $key): bool
|
||||
{
|
||||
return (bool) ($this->definition($key)['default_on_paid_plans'] ?? false);
|
||||
}
|
||||
|
||||
public function isEnabled(Organization $organization, string $key): bool
|
||||
{
|
||||
if (! $this->definition($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) data_get($organization->settings, "specialty_modules.{$key}", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function defaultKeysForPaidPlans(): array
|
||||
{
|
||||
$keys = [];
|
||||
foreach (array_keys($this->catalog()) as $key) {
|
||||
if ($this->isDefaultOnPaidPlans($key)) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provision Emergency / Blood Bank (and any other default_on_paid_plans modules).
|
||||
*/
|
||||
public function ensureDefaultModulesProvisioned(Organization $organization, string $ownerRef): void
|
||||
{
|
||||
if (! $this->plans->hasPaidPlan($organization)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->defaultKeysForPaidPlans() as $key) {
|
||||
$provisioned = data_get(
|
||||
$organization->settings,
|
||||
"specialty_module_provisioning.{$key}.active",
|
||||
);
|
||||
if ($provisioned) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->activate($organization->fresh(), $ownerRef, $key);
|
||||
$organization->refresh();
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('specialty_module.default_provision_failed', [
|
||||
'key' => $key,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
@@ -116,6 +170,11 @@ class SpecialtyModuleService
|
||||
}
|
||||
|
||||
if ($member && $member->role === 'doctor') {
|
||||
// Default Pro surfaces (Emergency, Blood Bank) are available to all doctors.
|
||||
if ($this->isDefaultOnPaidPlans($key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->doctorAssignedToModule($organization, $member, $key);
|
||||
}
|
||||
|
||||
@@ -187,9 +246,23 @@ class SpecialtyModuleService
|
||||
|
||||
foreach ($this->catalog() as $key => $definition) {
|
||||
$want = (bool) ($desired[$key] ?? false);
|
||||
if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
|
||||
$want = true;
|
||||
}
|
||||
$have = $this->isEnabled($organization, $key);
|
||||
|
||||
if ($want === $have) {
|
||||
// Still ensure defaults are provisioned even when already "enabled".
|
||||
if ($want && $this->isDefaultOnPaidPlans($key)
|
||||
&& ! data_get($organization->settings, "specialty_module_provisioning.{$key}.active")) {
|
||||
try {
|
||||
$this->activate($organization, $ownerRef, $key);
|
||||
$organization->refresh();
|
||||
} catch (\Throwable $e) {
|
||||
$errors[] = ($definition['label'] ?? $key).': '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -274,6 +347,10 @@ class SpecialtyModuleService
|
||||
throw new \InvalidArgumentException("Unknown specialty module [{$key}].");
|
||||
}
|
||||
|
||||
if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
|
||||
throw new \RuntimeException(($this->definition($key)['label'] ?? $key).' stays enabled on Pro and Enterprise.');
|
||||
}
|
||||
|
||||
$settings = $organization->settings ?? [];
|
||||
$modules = is_array($settings['specialty_modules'] ?? null) ? $settings['specialty_modules'] : [];
|
||||
$modules[$key] = false;
|
||||
|
||||
+61
-73
@@ -186,6 +186,36 @@ final class DemoWorld
|
||||
* account_role?: string
|
||||
* }>>
|
||||
*/
|
||||
/**
|
||||
* Care specialty module doctors (Pro / Enterprise). Emails: demo-{tier}-{key}@ladill.com
|
||||
*
|
||||
* @var array<string, string> 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' => [
|
||||
[
|
||||
@@ -272,41 +302,6 @@ final class DemoWorld
|
||||
'meet' => 'member',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'dentistry',
|
||||
'email' => 'demo-pro-dentistry@ladill.com',
|
||||
'name' => 'Dr. Ama Dental (Pro)',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
[
|
||||
'key' => 'ophthalmology',
|
||||
'email' => 'demo-pro-ophthalmology@ladill.com',
|
||||
'name' => 'Dr. Kofi Eye (Pro)',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
[
|
||||
'key' => 'physiotherapy',
|
||||
'email' => 'demo-pro-physiotherapy@ladill.com',
|
||||
'name' => 'Dr. Efua Physio (Pro)',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
[
|
||||
'key' => 'maternity',
|
||||
'email' => 'demo-pro-maternity@ladill.com',
|
||||
'name' => 'Dr. Abena Maternity (Pro)',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
[
|
||||
'key' => 'radiology',
|
||||
'email' => 'demo-pro-radiology@ladill.com',
|
||||
'name' => 'Dr. Yaw Radiology (Pro)',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
],
|
||||
'enterprise' => [
|
||||
[
|
||||
@@ -381,41 +376,6 @@ final class DemoWorld
|
||||
'events' => 'member',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'dentistry',
|
||||
'email' => 'demo-enterprise-dentistry@ladill.com',
|
||||
'name' => 'Dr. Ama Dental',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
[
|
||||
'key' => 'ophthalmology',
|
||||
'email' => 'demo-enterprise-ophthalmology@ladill.com',
|
||||
'name' => 'Dr. Kofi Eye',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
[
|
||||
'key' => 'physiotherapy',
|
||||
'email' => 'demo-enterprise-physiotherapy@ladill.com',
|
||||
'name' => 'Dr. Efua Physio',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
[
|
||||
'key' => 'maternity',
|
||||
'email' => 'demo-enterprise-maternity@ladill.com',
|
||||
'name' => 'Dr. Abena Maternity',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
[
|
||||
'key' => 'radiology',
|
||||
'email' => 'demo-enterprise-radiology@ladill.com',
|
||||
'name' => 'Dr. Yaw Radiology',
|
||||
'apps' => ['care'],
|
||||
'roles' => ['care' => 'doctor'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -541,7 +501,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -554,8 +542,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;
|
||||
}
|
||||
|
||||
+18
-57
@@ -25,70 +25,31 @@ return [
|
||||
'dental' => 'Dental',
|
||||
'ophthalmology' => 'Ophthalmology / Eye care',
|
||||
'physiotherapy' => 'Physiotherapy',
|
||||
'blood_bank' => 'Blood Bank',
|
||||
'cardiology' => 'Cardiology',
|
||||
'psychiatry' => 'Psychiatry',
|
||||
'pediatrics' => 'Pediatrics',
|
||||
'orthopedics' => 'Orthopedics',
|
||||
'ent' => 'ENT',
|
||||
'oncology' => 'Oncology',
|
||||
'renal' => 'Renal Care',
|
||||
'surgery' => 'Surgery',
|
||||
'vaccination' => 'Vaccination & Immunization',
|
||||
'pathology' => 'Pathology',
|
||||
'infusion' => 'Infusion Centre',
|
||||
'dermatology' => 'Dermatology',
|
||||
'podiatry' => 'Podiatry',
|
||||
'fertility' => 'Fertility',
|
||||
'child_welfare' => 'Child Welfare Clinic',
|
||||
],
|
||||
|
||||
/*
|
||||
| Specialty practice modules (Settings → Modules).
|
||||
| Pro-gated via plans.*.features specialty_modules — expansions beyond core
|
||||
| outpatient/lab/pharmacy belong on paid plans, matching queue_integration.
|
||||
| See config/care_specialty_modules.php (default_on_paid_plans for Emergency / Blood Bank).
|
||||
*/
|
||||
'specialty_modules' => [
|
||||
'dentistry' => [
|
||||
'label' => 'Dentistry',
|
||||
'description' => 'Dental chairs, oral consults, and dentistry waiting lists.',
|
||||
'department_type' => 'dental',
|
||||
'department_name' => 'Dentistry',
|
||||
'queue_name' => 'Dentistry',
|
||||
'queue_prefix' => 'DEN',
|
||||
'nav_label' => 'Dentistry',
|
||||
'queue_keywords' => ['dent', 'dental', 'oral'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'ophthalmology' => [
|
||||
'label' => 'Eye care',
|
||||
'description' => 'Ophthalmology and optometry clinics with dedicated queues.',
|
||||
'department_type' => 'ophthalmology',
|
||||
'department_name' => 'Eye care',
|
||||
'queue_name' => 'Eye care',
|
||||
'queue_prefix' => 'EYE',
|
||||
'nav_label' => 'Eye care',
|
||||
'queue_keywords' => ['eye', 'ophthal', 'optom'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'physiotherapy' => [
|
||||
'label' => 'Physiotherapy',
|
||||
'description' => 'Rehab sessions and physiotherapy treatment queues.',
|
||||
'department_type' => 'physiotherapy',
|
||||
'department_name' => 'Physiotherapy',
|
||||
'queue_name' => 'Physiotherapy',
|
||||
'queue_prefix' => 'PHY',
|
||||
'nav_label' => 'Physiotherapy',
|
||||
'queue_keywords' => ['physio', 'rehab', 'therapy'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'maternity' => [
|
||||
'label' => 'Maternity / Antenatal',
|
||||
'description' => 'Antenatal clinics, maternity wards, and related queues.',
|
||||
'department_type' => 'maternity',
|
||||
'department_name' => 'Maternity',
|
||||
'queue_name' => 'Maternity',
|
||||
'queue_prefix' => 'MAT',
|
||||
'nav_label' => 'Maternity',
|
||||
'queue_keywords' => ['matern', 'antenatal', 'obstetric'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'radiology' => [
|
||||
'label' => 'Radiology',
|
||||
'description' => 'Imaging requests and radiology service counters.',
|
||||
'department_type' => 'radiology',
|
||||
'department_name' => 'Radiology',
|
||||
'queue_name' => 'Radiology',
|
||||
'queue_prefix' => 'RAD',
|
||||
'nav_label' => 'Radiology',
|
||||
'queue_keywords' => ['radio', 'imaging', 'x-ray', 'xray', 'ultrasound', 'ct', 'mri'],
|
||||
'roles' => ['doctor', 'lab_technician', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
],
|
||||
'specialty_modules' => require __DIR__.'/care_specialty_modules.php',
|
||||
|
||||
'audit_actions' => [
|
||||
'organization.created' => 'Organization created',
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Specialty practice modules (Settings → Modules).
|
||||
* Pro-gated via plans.*.features specialty_modules.
|
||||
*
|
||||
* default_on_paid_plans: always on for Pro/Enterprise (sidebar + access).
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
return [
|
||||
'emergency' => [
|
||||
'label' => 'Emergency',
|
||||
'description' => 'Emergency department triage, resus, and urgent care queues.',
|
||||
'department_type' => 'emergency',
|
||||
'department_name' => 'Emergency',
|
||||
'queue_name' => 'Emergency',
|
||||
'queue_prefix' => 'ER',
|
||||
'nav_label' => 'Emergency',
|
||||
'queue_keywords' => ['emerg', 'casualty', 'trauma', 'er', 'a&e'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
'default_on_paid_plans' => true,
|
||||
],
|
||||
'blood_bank' => [
|
||||
'label' => 'Blood Bank',
|
||||
'description' => 'Blood products, cross-match requests, and transfusion counters.',
|
||||
'department_type' => 'blood_bank',
|
||||
'department_name' => 'Blood Bank',
|
||||
'queue_name' => 'Blood Bank',
|
||||
'queue_prefix' => 'BB',
|
||||
'nav_label' => 'Blood Bank',
|
||||
'queue_keywords' => ['blood', 'transfusion', 'crossmatch', 'donor'],
|
||||
'roles' => ['doctor', 'nurse', 'lab_technician', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
'default_on_paid_plans' => true,
|
||||
],
|
||||
'dentistry' => [
|
||||
'label' => 'Dentistry',
|
||||
'description' => 'Dental chairs, oral consults, and dentistry waiting lists.',
|
||||
'department_type' => 'dental',
|
||||
'department_name' => 'Dentistry',
|
||||
'queue_name' => 'Dentistry',
|
||||
'queue_prefix' => 'DEN',
|
||||
'nav_label' => 'Dentistry',
|
||||
'queue_keywords' => ['dent', 'dental', 'oral'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'ophthalmology' => [
|
||||
'label' => 'Eye care',
|
||||
'description' => 'Ophthalmology and optometry clinics with dedicated queues.',
|
||||
'department_type' => 'ophthalmology',
|
||||
'department_name' => 'Eye care',
|
||||
'queue_name' => 'Eye care',
|
||||
'queue_prefix' => 'EYE',
|
||||
'nav_label' => 'Eye care',
|
||||
'queue_keywords' => ['eye', 'ophthal', 'optom'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'physiotherapy' => [
|
||||
'label' => 'Physiotherapy',
|
||||
'description' => 'Rehab sessions and physiotherapy treatment queues.',
|
||||
'department_type' => 'physiotherapy',
|
||||
'department_name' => 'Physiotherapy',
|
||||
'queue_name' => 'Physiotherapy',
|
||||
'queue_prefix' => 'PHY',
|
||||
'nav_label' => 'Physiotherapy',
|
||||
'queue_keywords' => ['physio', 'rehab', 'therapy'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'maternity' => [
|
||||
'label' => 'Maternity / Antenatal',
|
||||
'description' => 'Antenatal clinics, maternity wards, and related queues.',
|
||||
'department_type' => 'maternity',
|
||||
'department_name' => 'Maternity',
|
||||
'queue_name' => 'Maternity',
|
||||
'queue_prefix' => 'MAT',
|
||||
'nav_label' => 'Maternity',
|
||||
'queue_keywords' => ['matern', 'antenatal', 'obstetric'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'radiology' => [
|
||||
'label' => 'Radiology',
|
||||
'description' => 'Imaging requests and radiology service counters.',
|
||||
'department_type' => 'radiology',
|
||||
'department_name' => 'Radiology',
|
||||
'queue_name' => 'Radiology',
|
||||
'queue_prefix' => 'RAD',
|
||||
'nav_label' => 'Radiology',
|
||||
'queue_keywords' => ['radio', 'imaging', 'x-ray', 'xray', 'ultrasound', 'ct', 'mri'],
|
||||
'roles' => ['doctor', 'lab_technician', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'cardiology' => [
|
||||
'label' => 'Cardiology',
|
||||
'description' => 'Heart clinics, ECG follow-up, and cardiology waiting lists.',
|
||||
'department_type' => 'cardiology',
|
||||
'department_name' => 'Cardiology',
|
||||
'queue_name' => 'Cardiology',
|
||||
'queue_prefix' => 'CAR',
|
||||
'nav_label' => 'Cardiology',
|
||||
'queue_keywords' => ['cardio', 'heart', 'ecg', 'echo'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'psychiatry' => [
|
||||
'label' => 'Psychiatry',
|
||||
'description' => 'Mental health consultations and psychiatry clinic queues.',
|
||||
'department_type' => 'psychiatry',
|
||||
'department_name' => 'Psychiatry',
|
||||
'queue_name' => 'Psychiatry',
|
||||
'queue_prefix' => 'PSY',
|
||||
'nav_label' => 'Psychiatry',
|
||||
'queue_keywords' => ['psych', 'mental', 'behaviour'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'pediatrics' => [
|
||||
'label' => 'Pediatrics',
|
||||
'description' => 'Child health clinics and pediatric consultation queues.',
|
||||
'department_type' => 'pediatrics',
|
||||
'department_name' => 'Pediatrics',
|
||||
'queue_name' => 'Pediatrics',
|
||||
'queue_prefix' => 'PED',
|
||||
'nav_label' => 'Pediatrics',
|
||||
'queue_keywords' => ['pedia', 'paedia', 'child', 'infant'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'orthopedics' => [
|
||||
'label' => 'Orthopedics',
|
||||
'description' => 'Bones, joints, fracture clinics, and orthopedic queues.',
|
||||
'department_type' => 'orthopedics',
|
||||
'department_name' => 'Orthopedics',
|
||||
'queue_name' => 'Orthopedics',
|
||||
'queue_prefix' => 'ORT',
|
||||
'nav_label' => 'Orthopedics',
|
||||
'queue_keywords' => ['ortho', 'fracture', 'bone', 'joint'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'ent' => [
|
||||
'label' => 'ENT',
|
||||
'description' => 'Ear, nose, and throat clinics with dedicated queues.',
|
||||
'department_type' => 'ent',
|
||||
'department_name' => 'ENT',
|
||||
'queue_name' => 'ENT',
|
||||
'queue_prefix' => 'ENT',
|
||||
'nav_label' => 'ENT',
|
||||
'queue_keywords' => ['ent', 'ear', 'nose', 'throat', 'otolaryng'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'oncology' => [
|
||||
'label' => 'Oncology',
|
||||
'description' => 'Cancer care clinics, chemo day-care, and oncology queues.',
|
||||
'department_type' => 'oncology',
|
||||
'department_name' => 'Oncology',
|
||||
'queue_name' => 'Oncology',
|
||||
'queue_prefix' => 'ONC',
|
||||
'nav_label' => 'Oncology',
|
||||
'queue_keywords' => ['oncol', 'cancer', 'chemo'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'renal' => [
|
||||
'label' => 'Renal Care',
|
||||
'description' => 'Dialysis, nephrology clinics, and renal care queues.',
|
||||
'department_type' => 'renal',
|
||||
'department_name' => 'Renal Care',
|
||||
'queue_name' => 'Renal Care',
|
||||
'queue_prefix' => 'REN',
|
||||
'nav_label' => 'Renal Care',
|
||||
'queue_keywords' => ['renal', 'dialysis', 'nephro', 'kidney'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'surgery' => [
|
||||
'label' => 'Surgery',
|
||||
'description' => 'Surgical OPD, pre-op assessment, and theatre-linked queues.',
|
||||
'department_type' => 'surgery',
|
||||
'department_name' => 'Surgery',
|
||||
'queue_name' => 'Surgery',
|
||||
'queue_prefix' => 'SUR',
|
||||
'nav_label' => 'Surgery',
|
||||
'queue_keywords' => ['surg', 'theatre', 'operat', 'pre-op'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'vaccination' => [
|
||||
'label' => 'Vaccination & Immunization',
|
||||
'description' => 'Immunization clinics, EPI schedules, and vaccine queues.',
|
||||
'department_type' => 'vaccination',
|
||||
'department_name' => 'Vaccination',
|
||||
'queue_name' => 'Vaccination',
|
||||
'queue_prefix' => 'VAX',
|
||||
'nav_label' => 'Vaccination',
|
||||
'queue_keywords' => ['vaccin', 'immun', 'epi', 'injection'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'pathology' => [
|
||||
'label' => 'Pathology',
|
||||
'description' => 'Histopathology, cytology, and pathology service counters.',
|
||||
'department_type' => 'pathology',
|
||||
'department_name' => 'Pathology',
|
||||
'queue_name' => 'Pathology',
|
||||
'queue_prefix' => 'PATH',
|
||||
'nav_label' => 'Pathology',
|
||||
'queue_keywords' => ['pathol', 'histo', 'cytol', 'biopsy'],
|
||||
'roles' => ['doctor', 'lab_technician', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'infusion' => [
|
||||
'label' => 'Infusion Centre',
|
||||
'description' => 'IV therapy, infusion chairs, and day-care infusion queues.',
|
||||
'department_type' => 'infusion',
|
||||
'department_name' => 'Infusion Centre',
|
||||
'queue_name' => 'Infusion',
|
||||
'queue_prefix' => 'INF',
|
||||
'nav_label' => 'Infusion',
|
||||
'queue_keywords' => ['infusion', 'iv', 'drip', 'day care'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'dermatology' => [
|
||||
'label' => 'Dermatology',
|
||||
'description' => 'Skin clinics, dermatology procedures, and specialty queues.',
|
||||
'department_type' => 'dermatology',
|
||||
'department_name' => 'Dermatology',
|
||||
'queue_name' => 'Dermatology',
|
||||
'queue_prefix' => 'DER',
|
||||
'nav_label' => 'Dermatology',
|
||||
'queue_keywords' => ['derma', 'skin', 'rash'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'podiatry' => [
|
||||
'label' => 'Podiatry',
|
||||
'description' => 'Foot clinics, diabetic foot care, and podiatry queues.',
|
||||
'department_type' => 'podiatry',
|
||||
'department_name' => 'Podiatry',
|
||||
'queue_name' => 'Podiatry',
|
||||
'queue_prefix' => 'POD',
|
||||
'nav_label' => 'Podiatry',
|
||||
'queue_keywords' => ['podiat', 'foot', 'diabetic foot'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'fertility' => [
|
||||
'label' => 'Fertility',
|
||||
'description' => 'Fertility / IVF clinics and reproductive health queues.',
|
||||
'department_type' => 'fertility',
|
||||
'department_name' => 'Fertility',
|
||||
'queue_name' => 'Fertility',
|
||||
'queue_prefix' => 'FER',
|
||||
'nav_label' => 'Fertility',
|
||||
'queue_keywords' => ['fertil', 'ivf', 'reproduct'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
'child_welfare' => [
|
||||
'label' => 'Child Welfare Clinic',
|
||||
'description' => 'Well-child visits, growth monitoring, and CWC queues.',
|
||||
'department_type' => 'child_welfare',
|
||||
'department_name' => 'Child Welfare Clinic',
|
||||
'queue_name' => 'Child Welfare',
|
||||
'queue_prefix' => 'CWC',
|
||||
'nav_label' => 'Child Welfare',
|
||||
'queue_keywords' => ['cwc', 'child welfare', 'well baby', 'growth'],
|
||||
'roles' => ['doctor', 'nurse', 'receptionist', 'hospital_admin', 'super_admin'],
|
||||
],
|
||||
];
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
@if (! $canUseSpecialtyModules)
|
||||
<x-settings.card title="Pro feature" description="Specialty modules require Care Pro or Enterprise.">
|
||||
<p class="text-sm text-slate-600">Unlock dentistry, eye care, physiotherapy, maternity, and radiology practice pages with matching departments and queue stubs.</p>
|
||||
<p class="text-sm text-slate-600">Unlock specialty practice pages with matching departments and queue stubs. Emergency and Blood Bank are included by default on Pro and Enterprise.</p>
|
||||
<a href="{{ route('care.pro.index') }}" class="btn-primary mt-4 inline-flex text-sm">View plans</a>
|
||||
</x-settings.card>
|
||||
@else
|
||||
@@ -21,18 +21,25 @@
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach ($catalog as $key => $module)
|
||||
<label class="flex cursor-pointer flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 {{ ! empty($enabled[$key]) ? 'ring-1 ring-indigo-200' : '' }}">
|
||||
@php $isDefault = ! empty($module['default_on_paid_plans']); @endphp
|
||||
<label class="flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition {{ $isDefault ? '' : 'cursor-pointer hover:border-indigo-200' }} {{ ! empty($enabled[$key]) || $isDefault ? 'ring-1 ring-indigo-200' : '' }}">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-slate-900">{{ $module['label'] }}</p>
|
||||
<p class="mt-1 text-xs text-slate-500">{{ $module['description'] }}</p>
|
||||
@if ($isDefault)
|
||||
<p class="mt-2 text-[11px] font-medium uppercase tracking-wide text-indigo-600">Included on Pro / Enterprise</p>
|
||||
@endif
|
||||
</div>
|
||||
<input type="checkbox" name="modules[{{ $key }}]" value="1"
|
||||
@checked(old("modules.{$key}", ! empty($enabled[$key])))
|
||||
@disabled(! $canManage)
|
||||
@checked(old("modules.{$key}", ! empty($enabled[$key]) || $isDefault))
|
||||
@disabled(! $canManage || $isDefault)
|
||||
class="mt-0.5 rounded border-slate-300">
|
||||
@if ($isDefault)
|
||||
<input type="hidden" name="modules[{{ $key }}]" value="1">
|
||||
@endif
|
||||
</div>
|
||||
<p class="mt-3 text-[11px] uppercase tracking-wide text-slate-400">
|
||||
Dept · {{ $module['department_name'] ?? $module['label'] }}
|
||||
|
||||
@@ -74,10 +74,34 @@
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />'];
|
||||
}
|
||||
|
||||
// Specialty modules (Settings → Modules) — no separate Service Queues nav;
|
||||
// Ladill Queue embeds on clinical / pharmacy / lab / specialty pages.
|
||||
$specialtyModules = app(\App\Services\Care\SpecialtyModuleService::class)
|
||||
->enabledModulesForMember($organization, $member);
|
||||
// Always-on Pro/Enterprise surfaces first, then other specialty modules.
|
||||
$specialtyService = app(\App\Services\Care\SpecialtyModuleService::class);
|
||||
if ($organization) {
|
||||
$specialtyService->ensureDefaultModulesProvisioned(
|
||||
$organization,
|
||||
(string) $organization->owner_ref,
|
||||
);
|
||||
$organization->refresh();
|
||||
}
|
||||
$specialtyModules = $specialtyService->enabledModulesForMember($organization, $member);
|
||||
$pinnedKeys = $specialtyService->defaultKeysForPaidPlans();
|
||||
usort($specialtyModules, function (array $a, array $b) use ($pinnedKeys): int {
|
||||
$aPin = in_array($a['key'], $pinnedKeys, true) ? 0 : 1;
|
||||
$bPin = in_array($b['key'], $pinnedKeys, true) ? 0 : 1;
|
||||
if ($aPin !== $bPin) {
|
||||
return $aPin <=> $bPin;
|
||||
}
|
||||
$aOrder = array_search($a['key'], $pinnedKeys, true);
|
||||
$bOrder = array_search($b['key'], $pinnedKeys, true);
|
||||
if ($aOrder !== false || $bOrder !== false) {
|
||||
return ($aOrder === false ? 99 : $aOrder) <=> ($bOrder === false ? 99 : $bOrder);
|
||||
}
|
||||
|
||||
return strcasecmp(
|
||||
(string) ($a['definition']['nav_label'] ?? $a['definition']['label'] ?? $a['key']),
|
||||
(string) ($b['definition']['nav_label'] ?? $b['definition']['label'] ?? $b['key']),
|
||||
);
|
||||
});
|
||||
foreach ($specialtyModules as $item) {
|
||||
$nav[] = [
|
||||
'name' => $item['definition']['nav_label'] ?? $item['definition']['label'],
|
||||
|
||||
@@ -342,4 +342,49 @@ class CareSpecialtyModulesTest extends TestCase
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_emergency_and_blood_bank_are_default_on_pro(): void
|
||||
{
|
||||
Http::fake();
|
||||
$service = app(SpecialtyModuleService::class);
|
||||
|
||||
$this->assertTrue($service->isEnabled($this->organization, 'emergency'));
|
||||
$this->assertTrue($service->isEnabled($this->organization, 'blood_bank'));
|
||||
$this->assertFalse($service->isEnabled($this->organization, 'cardiology'));
|
||||
|
||||
$service->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id);
|
||||
|
||||
$this->assertDatabaseHas('care_departments', [
|
||||
'branch_id' => $this->branch->id,
|
||||
'type' => 'emergency',
|
||||
'is_active' => true,
|
||||
]);
|
||||
$this->assertDatabaseHas('care_departments', [
|
||||
'branch_id' => $this->branch->id,
|
||||
'type' => 'blood_bank',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.specialty.show', 'emergency'))
|
||||
->assertOk()
|
||||
->assertSee('Emergency');
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$service->deactivate($this->organization->fresh(), $this->owner->public_id, 'emergency');
|
||||
}
|
||||
|
||||
public function test_catalog_includes_expanded_specialty_modules(): void
|
||||
{
|
||||
$catalog = app(SpecialtyModuleService::class)->catalog();
|
||||
foreach ([
|
||||
'emergency', 'blood_bank', 'cardiology', 'psychiatry', 'pediatrics', 'orthopedics',
|
||||
'ent', 'oncology', 'renal', 'surgery', 'vaccination', 'pathology', 'infusion',
|
||||
'dermatology', 'podiatry', 'fertility', 'child_welfare',
|
||||
] as $key) {
|
||||
$this->assertArrayHasKey($key, $catalog);
|
||||
}
|
||||
$this->assertTrue((bool) ($catalog['emergency']['default_on_paid_plans'] ?? false));
|
||||
$this->assertTrue((bool) ($catalog['blood_bank']['default_on_paid_plans'] ?? false));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user