Give each specialty module a distinct Heroicon-style icon.
Deploy Ladill Care / deploy (push) Successful in 1m2s
Deploy Ladill Care / deploy (push) Successful in 1m2s
Icon names live on the specialty catalog; SVG paths resolve through a shared map and render in sidebar, dashboard, settings, and the specialty shell. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,6 +40,33 @@ class SpecialtyModuleService
|
||||
return $catalog[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heroicon-style icon identifier from the specialty catalog (e.g. bolt, heart).
|
||||
*/
|
||||
public function iconName(string $key): string
|
||||
{
|
||||
$name = (string) ($this->definition($key)['icon'] ?? 'squares-2x2');
|
||||
|
||||
return $name !== '' ? $name : 'squares-2x2';
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline SVG path markup for sidebar / card icons (24×24 outline).
|
||||
*/
|
||||
public function iconSvgPath(string $key): string
|
||||
{
|
||||
$icons = config('care.specialty_icons', []);
|
||||
$name = $this->iconName($key);
|
||||
|
||||
if (is_array($icons) && isset($icons[$name]) && is_string($icons[$name]) && $icons[$name] !== '') {
|
||||
return $icons[$name];
|
||||
}
|
||||
|
||||
return is_array($icons) && isset($icons['squares-2x2']) && is_string($icons['squares-2x2'])
|
||||
? $icons['squares-2x2']
|
||||
: '';
|
||||
}
|
||||
|
||||
public function isDefaultOnPaidPlans(string $key): bool
|
||||
{
|
||||
return (bool) ($this->definition($key)['default_on_paid_plans'] ?? false);
|
||||
|
||||
@@ -46,6 +46,14 @@ if (! function_exists('care_money')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('care_specialty_icon_path')) {
|
||||
/** Inline SVG path markup for a specialty module key (Heroicons-style outline). */
|
||||
function care_specialty_icon_path(string $moduleKey): string
|
||||
{
|
||||
return app(\App\Services\Care\SpecialtyModuleService::class)->iconSvgPath($moduleKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('crm_money')) {
|
||||
/** @deprecated Use care_money() */
|
||||
function crm_money(?int $minor, ?string $currency = null): string
|
||||
|
||||
@@ -86,6 +86,12 @@ return [
|
||||
*/
|
||||
'specialty_modules' => require __DIR__.'/care_specialty_modules.php',
|
||||
|
||||
/*
|
||||
| SVG path markup for specialty module icons (Heroicons-style outline).
|
||||
| Referenced by care.specialty_modules.*.icon identifiers.
|
||||
*/
|
||||
'specialty_icons' => require __DIR__.'/care_specialty_icons.php',
|
||||
|
||||
'audit_actions' => [
|
||||
'organization.created' => 'Organization created',
|
||||
'organization.updated' => 'Organization updated',
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Heroicons-style outline SVG path markup for specialty module icons.
|
||||
* Keys match the `icon` field on care_specialty_modules.php entries.
|
||||
* Paths are 24×24 viewBox, stroke icons (stroke-width 1.5).
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
return [
|
||||
'bolt' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5 14.25 2.25 12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" />',
|
||||
'droplet' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 2.25c-3.75 5.25-6.75 8.625-6.75 12a6.75 6.75 0 1 0 13.5 0c0-3.375-3-6.75-6.75-12Z" />',
|
||||
'face-smile' => '<path stroke-linecap="round" stroke-linejoin="round" d="M15.182 15.182a4.5 4.5 0 0 1-6.364 0M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75h.008v.008H9.75V9.75Zm4.5 0h.008v.008h-.008V9.75Z" />',
|
||||
'eye' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />',
|
||||
'arrow-path' => '<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />',
|
||||
'home' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12 11.2 3.05c.44-.44 1.15-.44 1.59 0L21.75 12M4.5 9.75v10.5a.75.75 0 0 0 .75.75H9.75v-6a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75v6h4.5a.75.75 0 0 0 .75-.75V9.75" />',
|
||||
'camera' => '<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z" />',
|
||||
'heart' => '<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z" />',
|
||||
'chat-bubble-left-right' => '<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-.47-.47-1.1-.734-1.766-.734H9.75a2.25 2.25 0 0 1-2.25-2.25v-6a2.25 2.25 0 0 1 2.25-2.25h6.75c.621 0 1.125.504 1.125 1.125v.636ZM3.75 6.75h10.5A2.25 2.25 0 0 1 16.5 9v6.188c0 .621-.504 1.125-1.125 1.125H9.128a2.25 2.25 0 0 0-1.59.659L4.5 19.5V16.5a.75.75 0 0 0-.75-.75h-.75A2.25 2.25 0 0 1 .75 13.5v-4.5A2.25 2.25 0 0 1 3 6.75h.75Z" />',
|
||||
'user-group' => '<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />',
|
||||
'cube' => '<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />',
|
||||
'speaker-wave' => '<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />',
|
||||
'shield-exclamation' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m0-10.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.25-8.25-3.286Zm0 13.036h.008v.008H12v-.008Z" />',
|
||||
'circle-stack' => '<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />',
|
||||
'wrench-screwdriver' => '<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.6.98-1.312 1.26-2.096a9.004 9.004 0 0 0-9.38-9.38 9.004 9.004 0 0 0-2.096 1.26m8.216 8.216L11.42 15.17" />',
|
||||
'shield-check' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.25-8.25-3.286Z" />',
|
||||
'document-magnifying-glass' => '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-1.231a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />',
|
||||
'beaker' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714a2.25 2.25 0 0 0 .659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 0 1-1.59.659H9.06a2.25 2.25 0 0 1-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 0 1-2.25 2.25H7.25A2.25 2.25 0 0 1 5 17v-2.5" />',
|
||||
'swatch' => '<path stroke-linecap="round" stroke-linejoin="round" d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z" />',
|
||||
'finger-print' => '<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.566 8.268M5.742 6.364A7.465 7.465 0 0 0 4.5 10.5a7.464 7.464 0 0 1-1.15 3.993m1.989 3.559A11.209 11.209 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.635 9.76m6.633-1.13a14.958 14.958 0 0 0 2.252-6.63M12 10.5a14.94 14.94 0 0 0-3.635 9.76m0 0A14.926 14.926 0 0 0 12 21.75c1.258 0 2.48-.17 3.643-.483M8.365 20.26A14.94 14.94 0 0 0 12 10.5" />',
|
||||
'sparkles' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />',
|
||||
'gift' => '<path stroke-linecap="round" stroke-linejoin="round" d="M21 11.25v8.25a1.5 1.5 0 0 1-1.5 1.5H4.5a1.5 1.5 0 0 1-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 1 0 9.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1 1 14.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z" />',
|
||||
'squares-2x2' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />',
|
||||
];
|
||||
@@ -23,6 +23,7 @@ return [
|
||||
'queue_name' => 'Emergency',
|
||||
'queue_prefix' => 'ER',
|
||||
'nav_label' => 'Emergency',
|
||||
'icon' => 'bolt',
|
||||
'queue_keywords' => ['emerg', 'casualty', 'trauma', 'er', 'a&e'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'lab_technician', 'pharmacist', 'receptionist'],
|
||||
@@ -36,6 +37,7 @@ return [
|
||||
'queue_name' => 'Blood Bank',
|
||||
'queue_prefix' => 'BB',
|
||||
'nav_label' => 'Blood Bank',
|
||||
'icon' => 'droplet',
|
||||
'queue_keywords' => ['blood', 'transfusion', 'crossmatch', 'donor'],
|
||||
'access' => 'general',
|
||||
'roles' => ['lab_technician', 'nurse', 'doctor', 'receptionist'],
|
||||
@@ -49,6 +51,7 @@ return [
|
||||
'queue_name' => 'Dentistry',
|
||||
'queue_prefix' => 'DEN',
|
||||
'nav_label' => 'Dentistry',
|
||||
'icon' => 'face-smile',
|
||||
'queue_keywords' => ['dent', 'dental', 'oral'],
|
||||
'access' => 'limited',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
@@ -62,6 +65,7 @@ return [
|
||||
'queue_name' => 'Eye care',
|
||||
'queue_prefix' => 'EYE',
|
||||
'nav_label' => 'Eye care',
|
||||
'icon' => 'eye',
|
||||
'queue_keywords' => ['eye', 'ophthal', 'optom'],
|
||||
'access' => 'limited',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
@@ -75,6 +79,7 @@ return [
|
||||
'queue_name' => 'Physiotherapy',
|
||||
'queue_prefix' => 'PHY',
|
||||
'nav_label' => 'Physiotherapy',
|
||||
'icon' => 'arrow-path',
|
||||
'queue_keywords' => ['physio', 'rehab', 'therapy'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
@@ -87,6 +92,7 @@ return [
|
||||
'queue_name' => 'Maternity',
|
||||
'queue_prefix' => 'MAT',
|
||||
'nav_label' => 'Maternity',
|
||||
'icon' => 'home',
|
||||
'queue_keywords' => ['matern', 'antenatal', 'obstetric'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'pharmacist', 'receptionist'],
|
||||
@@ -99,6 +105,7 @@ return [
|
||||
'queue_name' => 'Radiology',
|
||||
'queue_prefix' => 'RAD',
|
||||
'nav_label' => 'Radiology',
|
||||
'icon' => 'camera',
|
||||
'queue_keywords' => ['radio', 'imaging', 'x-ray', 'xray', 'ultrasound', 'ct', 'mri'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'lab_technician', 'receptionist'],
|
||||
@@ -111,6 +118,7 @@ return [
|
||||
'queue_name' => 'Cardiology',
|
||||
'queue_prefix' => 'CAR',
|
||||
'nav_label' => 'Cardiology',
|
||||
'icon' => 'heart',
|
||||
'queue_keywords' => ['cardio', 'heart', 'ecg', 'echo'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -127,6 +135,7 @@ return [
|
||||
'queue_name' => 'Psychiatry',
|
||||
'queue_prefix' => 'PSY',
|
||||
'nav_label' => 'Psychiatry',
|
||||
'icon' => 'chat-bubble-left-right',
|
||||
'queue_keywords' => ['psych', 'mental', 'behaviour'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -143,6 +152,7 @@ return [
|
||||
'queue_name' => 'Pediatrics',
|
||||
'queue_prefix' => 'PED',
|
||||
'nav_label' => 'Pediatrics',
|
||||
'icon' => 'user-group',
|
||||
'queue_keywords' => ['pedia', 'paedia', 'child', 'infant'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
@@ -155,6 +165,7 @@ return [
|
||||
'queue_name' => 'Orthopedics',
|
||||
'queue_prefix' => 'ORT',
|
||||
'nav_label' => 'Orthopedics',
|
||||
'icon' => 'cube',
|
||||
'queue_keywords' => ['ortho', 'fracture', 'bone', 'joint'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -171,6 +182,7 @@ return [
|
||||
'queue_name' => 'ENT',
|
||||
'queue_prefix' => 'ENT',
|
||||
'nav_label' => 'ENT',
|
||||
'icon' => 'speaker-wave',
|
||||
'queue_keywords' => ['ent', 'ear', 'nose', 'throat', 'otolaryng'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -187,6 +199,7 @@ return [
|
||||
'queue_name' => 'Oncology',
|
||||
'queue_prefix' => 'ONC',
|
||||
'nav_label' => 'Oncology',
|
||||
'icon' => 'shield-exclamation',
|
||||
'queue_keywords' => ['oncol', 'cancer', 'chemo'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -203,6 +216,7 @@ return [
|
||||
'queue_name' => 'Renal Care',
|
||||
'queue_prefix' => 'REN',
|
||||
'nav_label' => 'Renal Care',
|
||||
'icon' => 'circle-stack',
|
||||
'queue_keywords' => ['renal', 'dialysis', 'nephro', 'kidney'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -219,6 +233,7 @@ return [
|
||||
'queue_name' => 'Surgery',
|
||||
'queue_prefix' => 'SUR',
|
||||
'nav_label' => 'Surgery',
|
||||
'icon' => 'wrench-screwdriver',
|
||||
'queue_keywords' => ['surg', 'theatre', 'operat', 'pre-op'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -235,6 +250,7 @@ return [
|
||||
'queue_name' => 'Vaccination',
|
||||
'queue_prefix' => 'VAX',
|
||||
'nav_label' => 'Vaccination',
|
||||
'icon' => 'shield-check',
|
||||
'queue_keywords' => ['vaccin', 'immun', 'epi', 'injection'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
@@ -247,6 +263,7 @@ return [
|
||||
'queue_name' => 'Pathology',
|
||||
'queue_prefix' => 'PATH',
|
||||
'nav_label' => 'Pathology',
|
||||
'icon' => 'document-magnifying-glass',
|
||||
'queue_keywords' => ['pathol', 'histo', 'cytol', 'biopsy'],
|
||||
'access' => 'general',
|
||||
'roles' => ['lab_technician', 'doctor', 'nurse', 'receptionist'],
|
||||
@@ -260,6 +277,7 @@ return [
|
||||
'queue_name' => 'Infusion',
|
||||
'queue_prefix' => 'INF',
|
||||
'nav_label' => 'Infusion',
|
||||
'icon' => 'beaker',
|
||||
'queue_keywords' => ['infusion', 'iv', 'drip', 'day care'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'pharmacist', 'receptionist'],
|
||||
@@ -272,6 +290,7 @@ return [
|
||||
'queue_name' => 'Dermatology',
|
||||
'queue_prefix' => 'DER',
|
||||
'nav_label' => 'Dermatology',
|
||||
'icon' => 'swatch',
|
||||
'queue_keywords' => ['derma', 'skin', 'rash'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -288,6 +307,7 @@ return [
|
||||
'queue_name' => 'Podiatry',
|
||||
'queue_prefix' => 'POD',
|
||||
'nav_label' => 'Podiatry',
|
||||
'icon' => 'finger-print',
|
||||
'queue_keywords' => ['podiat', 'foot', 'diabetic foot'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -304,6 +324,7 @@ return [
|
||||
'queue_name' => 'Fertility',
|
||||
'queue_prefix' => 'FER',
|
||||
'nav_label' => 'Fertility',
|
||||
'icon' => 'sparkles',
|
||||
'queue_keywords' => ['fertil', 'ivf', 'reproduct'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
@@ -320,6 +341,7 @@ return [
|
||||
'queue_name' => 'Child Welfare',
|
||||
'queue_prefix' => 'CWC',
|
||||
'nav_label' => 'Child Welfare',
|
||||
'icon' => 'gift',
|
||||
'queue_keywords' => ['cwc', 'child welfare', 'well baby', 'growth'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
|
||||
@@ -206,17 +206,22 @@
|
||||
<a href="{{ $href }}"
|
||||
class="rounded-2xl border border-slate-200 bg-white px-4 py-4 transition hover:border-indigo-300 hover:shadow-sm">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex min-w-0 items-start gap-2.5">
|
||||
<x-care.specialty-icon :module="$item['key']" boxed />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-slate-900">{{ $item['definition']['nav_label'] ?? $item['definition']['label'] }}</p>
|
||||
<p class="mt-1 text-xs text-slate-500">{{ $item['definition']['description'] ?? '' }}</p>
|
||||
@if ($toSettings)
|
||||
<p class="mt-2 text-[11px] font-medium text-indigo-600">Settings & pricing →</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if (! empty($item['access_level']) && $item['access_level'] !== 'manage' && ! $toSettings)
|
||||
<span class="shrink-0 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-800">
|
||||
{{ $item['access_level'] === 'refer' ? 'View + refer' : 'View' }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-slate-500">{{ $item['definition']['description'] ?? '' }}</p>
|
||||
@if ($toSettings)
|
||||
<p class="mt-2 text-[11px] font-medium text-indigo-600">Settings & pricing →</p>
|
||||
@endif
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
title="{{ $definition['label'] ?? ucfirst($moduleKey) }}"
|
||||
description="{{ $definition['description'] ?? 'Module settings and service prices for '.$organization->name.'.' }}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<x-care.specialty-icon :module="$moduleKey" boxed />
|
||||
<p class="text-sm">
|
||||
<a href="{{ route('care.settings.modules') }}" class="font-medium text-indigo-600 hover:text-indigo-800">← Specialty modules</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (session('success'))
|
||||
<p class="rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">{{ session('success') }}</p>
|
||||
|
||||
@@ -29,12 +29,17 @@
|
||||
<div class="flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition {{ ! empty($enabled[$key]) || $isDefault ? 'ring-1 ring-indigo-200' : '' }}">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<a href="{{ route('care.settings.modules.show', $key) }}" class="min-w-0 flex-1 hover:opacity-90">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<x-care.specialty-icon :module="$key" boxed />
|
||||
<div class="min-w-0">
|
||||
<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
|
||||
<p class="mt-3 text-[11px] font-medium text-indigo-600">Settings & pricing →</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<label class="{{ $isDefault ? '' : 'cursor-pointer' }}" title="Activate module" @click.stop>
|
||||
<input type="checkbox" name="modules[{{ $key }}]" value="1"
|
||||
|
||||
@@ -65,19 +65,25 @@
|
||||
@endif
|
||||
@elseif ($section === 'history')
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<x-care.specialty-icon :module="$moduleKey" boxed />
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Visit history</h1>
|
||||
<h1 class="text-xl font-semibold text-slate-900">{{ $definition['nav_label'] ?? $definition['label'] ?? ucfirst($moduleKey) }} · Visit history</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Completed encounters for this service line.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('care.queue.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">← Back to queue</a>
|
||||
</div>
|
||||
@include('care.specialty.sections.history')
|
||||
@elseif ($section === 'billing')
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<x-care.specialty-icon :module="$moduleKey" boxed />
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Billing</h1>
|
||||
<h1 class="text-xl font-semibold text-slate-900">{{ $definition['nav_label'] ?? $definition['label'] ?? ucfirst($moduleKey) }} · Billing</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Service catalog for this module.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('care.queue.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">← Back to queue</a>
|
||||
</div>
|
||||
@include('care.specialty.sections.billing')
|
||||
@@ -85,9 +91,16 @@
|
||||
{{-- Queue home (overview / visits) — same UI as GP Queue --}}
|
||||
<div class="lg:grid lg:grid-cols-[minmax(0,1fr)_16rem] lg:items-start lg:gap-6">
|
||||
<div class="min-w-0 space-y-6">
|
||||
@php
|
||||
$specialtyLabel = $definition['nav_label'] ?? $definition['label'] ?? ucfirst($moduleKey);
|
||||
@endphp
|
||||
<div class="mb-1 flex items-center gap-2.5">
|
||||
<x-care.specialty-icon :module="$moduleKey" boxed />
|
||||
<p class="text-sm font-semibold text-slate-700">{{ $specialtyLabel }}</p>
|
||||
</div>
|
||||
<x-care.page-hero
|
||||
badge="Waiting · In care · Call next"
|
||||
title="Patient queue"
|
||||
:title="$specialtyLabel.' queue'"
|
||||
description="See who is waiting, start encounters, and keep patient flow moving."
|
||||
:stats="[
|
||||
['value' => number_format($kpis['waiting'] ?? $queue->count()), 'label' => 'Waiting'],
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@props([
|
||||
'module',
|
||||
'boxed' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$path = care_specialty_icon_path((string) $module);
|
||||
@endphp
|
||||
|
||||
@if ($boxed)
|
||||
<div {{ $attributes->merge(['class' => 'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-indigo-50']) }}>
|
||||
<svg class="h-4 w-4 text-indigo-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">{!! $path !!}</svg>
|
||||
</div>
|
||||
@else
|
||||
<svg {{ $attributes->merge(['class' => 'h-4 w-4 shrink-0']) }} fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">{!! $path !!}</svg>
|
||||
@endif
|
||||
@@ -83,7 +83,6 @@
|
||||
$organization,
|
||||
(string) $organization->owner_ref,
|
||||
);
|
||||
$specialtyIcon = '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .846.344 1.657.955 2.247L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-1.659 1.659a2.25 2.25 0 0 1-1.591.659H8.25" />';
|
||||
foreach ($specialtyService->enabledModulesForMember($organization, $member) as $item) {
|
||||
$key = $item['key'];
|
||||
$label = $item['definition']['nav_label'] ?? $item['definition']['label'] ?? $key;
|
||||
@@ -91,7 +90,7 @@
|
||||
'name' => $label,
|
||||
'route' => route('care.specialty.workspace', $key),
|
||||
'active' => request()->routeIs('care.specialty.*') && request()->route('module') === $key,
|
||||
'icon' => $specialtyIcon,
|
||||
'icon' => $specialtyService->iconSvgPath($key),
|
||||
'group' => 'specialty',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -380,4 +380,43 @@ class CareSpecialtyModulesTest extends TestCase
|
||||
$this->assertTrue((bool) ($catalog['emergency']['default_on_paid_plans'] ?? false));
|
||||
$this->assertTrue((bool) ($catalog['blood_bank']['default_on_paid_plans'] ?? false));
|
||||
}
|
||||
|
||||
public function test_each_specialty_module_has_a_unique_icon(): void
|
||||
{
|
||||
$service = app(SpecialtyModuleService::class);
|
||||
$catalog = $service->catalog();
|
||||
$iconNames = [];
|
||||
$iconPaths = [];
|
||||
|
||||
foreach (array_keys($catalog) as $key) {
|
||||
$name = $service->iconName($key);
|
||||
$path = $service->iconSvgPath($key);
|
||||
$this->assertNotSame('', $name, "Module [{$key}] missing icon name");
|
||||
$this->assertNotSame('', $path, "Module [{$key}] missing icon SVG path");
|
||||
$this->assertArrayHasKey($name, config('care.specialty_icons'));
|
||||
$iconNames[$key] = $name;
|
||||
$iconPaths[$key] = $path;
|
||||
}
|
||||
|
||||
$this->assertCount(count($iconNames), array_unique(array_values($iconNames)));
|
||||
$this->assertCount(count($iconPaths), array_unique(array_values($iconPaths)));
|
||||
}
|
||||
|
||||
public function test_modules_settings_renders_distinct_specialty_icons(): void
|
||||
{
|
||||
$response = $this->actingAs($this->owner)
|
||||
->get(route('care.settings.modules'))
|
||||
->assertOk();
|
||||
|
||||
$response->assertSee(care_specialty_icon_path('dentistry'), false);
|
||||
$response->assertSee(care_specialty_icon_path('emergency'), false);
|
||||
$this->assertNotSame(
|
||||
care_specialty_icon_path('dentistry'),
|
||||
care_specialty_icon_path('emergency'),
|
||||
);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.dashboard'))
|
||||
->assertOk();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user