diff --git a/app/Http/Controllers/Api/IntegrationController.php b/app/Http/Controllers/Api/IntegrationController.php index c979457..d012c4e 100644 --- a/app/Http/Controllers/Api/IntegrationController.php +++ b/app/Http/Controllers/Api/IntegrationController.php @@ -5,9 +5,10 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Api\Concerns\ScopesApiToAccount; use App\Http\Controllers\Controller; use App\Models\Branch; -use App\Models\Department; use App\Models\Organization; use App\Services\Qms\AuditLogger; +use App\Services\Qms\Industry\IndustryPackageInstaller; +use App\Services\Qms\Industry\IndustryPackageRegistry; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -36,6 +37,8 @@ class IntegrationController extends Controller 'provisioned' => true, 'onboarded' => (bool) data_get($organization->settings, 'onboarded', false), 'organization_name' => $organization->name, + '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), @@ -48,6 +51,7 @@ class IntegrationController extends Controller { $owner = $this->ownerRef($request); $caller = (string) $request->attributes->get('service_caller', ''); + $registry = app(IndustryPackageRegistry::class); $validated = $request->validate([ 'organization_name' => ['nullable', 'string', 'max:255'], @@ -56,8 +60,17 @@ class IntegrationController extends Controller 'industry' => ['nullable', 'string'], ]); + $defaultIndustry = match ($caller) { + 'care' => 'healthcare', + 'frontdesk' => 'visitor', + 'pos' => 'restaurant', + default => 'custom', + }; + $industry = $registry->resolveKey($validated['industry'] ?? $defaultIndustry); + $organization = Organization::owned($owner)->first(); $created = false; + $branch = null; if (! $organization) { $created = true; @@ -69,7 +82,7 @@ class IntegrationController extends Controller 'timezone' => $validated['timezone'] ?? config('app.timezone', 'UTC'), 'settings' => [ 'onboarded' => true, - 'industry' => $validated['industry'] ?? ($caller === 'care' ? 'healthcare' : 'visitor'), + 'industry' => $industry, 'appointment_mode' => 'hybrid', 'integrations' => [ 'care_enabled' => false, @@ -85,25 +98,47 @@ class IntegrationController extends Controller 'is_active' => true, ]); - Department::create([ - 'owner_ref' => $owner, - 'branch_id' => $branch->id, - 'name' => 'Reception', - 'type' => 'reception', - 'is_active' => true, - ]); - AuditLogger::record($owner, 'organization.created', $organization->id, $owner, Organization::class, $organization->id); + } else { + $branch = Branch::query() + ->where('organization_id', $organization->id) + ->where('is_active', true) + ->orderBy('id') + ->first(); + + if (! $branch) { + $branch = Branch::create([ + 'owner_ref' => $owner, + 'organization_id' => $organization->id, + 'name' => $validated['branch_name'] ?? 'Main branch', + 'is_active' => true, + ]); + } } if (in_array($caller, ['care', 'frontdesk'], true)) { $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, + ], + ); + return response()->json([ 'data' => [ 'provisioned' => true, 'organization_name' => $organization->fresh()->name, + '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), diff --git a/app/Http/Controllers/Qms/OnboardingController.php b/app/Http/Controllers/Qms/OnboardingController.php index aa7104c..faddfd2 100644 --- a/app/Http/Controllers/Qms/OnboardingController.php +++ b/app/Http/Controllers/Qms/OnboardingController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Qms; use App\Http\Controllers\Controller; use App\Http\Controllers\Qms\Concerns\ScopesToAccount; +use App\Services\Qms\Industry\IndustryPackageRegistry; use App\Services\Qms\OrganizationResolver; use App\Support\OrganizationBranding; use Illuminate\Http\RedirectResponse; @@ -16,6 +17,7 @@ class OnboardingController extends Controller public function __construct( protected OrganizationResolver $organizations, + protected IndustryPackageRegistry $packages, ) {} public function show(Request $request): View|RedirectResponse @@ -24,10 +26,18 @@ class OnboardingController extends Controller return redirect()->route('qms.dashboard'); } + $industries = []; + foreach ($this->packages->all() as $package) { + $industries[$package->key] = [ + 'label' => $package->label(), + 'description' => $package->description(), + ]; + } + return view('qms.onboarding.show', [ 'user' => $request->user(), 'timezones' => timezone_identifiers_list(), - 'industries' => config('qms.industry_templates'), + 'industries' => $industries, 'appointmentModes' => config('qms.appointment_modes'), ]); } @@ -40,7 +50,7 @@ class OnboardingController extends Controller $validated = $request->validate([ 'organization_name' => ['required', 'string', 'max:255'], - 'industry' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.industry_templates')))], + 'industry' => ['required', 'string', 'in:'.implode(',', array_keys(config('industry_packages.packages', [])))], 'appointment_mode' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.appointment_modes')))], 'branch_name' => ['required', 'string', 'max:255'], 'branch_address' => ['nullable', 'string', 'max:500'], @@ -57,6 +67,11 @@ class OnboardingController extends Controller ]); } - return redirect()->route('qms.dashboard')->with('success', 'Welcome to Ladill Queue!'); + $packageLabel = $this->packages->get($validated['industry'])?->label() ?? 'your industry'; + + return redirect()->route('qms.dashboard')->with( + 'success', + "Welcome to Ladill Queue — {$packageLabel} workflow provisioned." + ); } } diff --git a/app/Http/Controllers/Qms/SettingsController.php b/app/Http/Controllers/Qms/SettingsController.php index 4c59e86..a1158eb 100644 --- a/app/Http/Controllers/Qms/SettingsController.php +++ b/app/Http/Controllers/Qms/SettingsController.php @@ -6,6 +6,8 @@ use App\Http\Controllers\Controller; use App\Http\Controllers\Qms\Concerns\ScopesToAccount; use App\Models\Branch; use App\Services\Qms\AuditLogger; +use App\Services\Qms\Industry\IndustryPackageInstaller; +use App\Services\Qms\Industry\IndustryPackageRegistry; use App\Services\Qms\PlanService; use App\Services\Qms\QmsPermissions; use App\Support\OrganizationBranding; @@ -17,7 +19,7 @@ class SettingsController extends Controller { use ScopesToAccount; - public function edit(Request $request, PlanService $plans): View + public function edit(Request $request, PlanService $plans, IndustryPackageRegistry $packages): View { $this->authorizeAbility($request, 'settings.view'); $organization = $this->organization($request); @@ -29,12 +31,19 @@ class SettingsController extends Controller ->where('organization_id', $organization->id) ->count(); + $packageKey = data_get($organization->settings, 'industry_package.key') + ?? data_get($organization->settings, 'industry') + ?? 'custom'; + $activePackage = $packages->get($packageKey); + return view('qms.settings.edit', [ 'organization' => $organization, 'canManage' => $canManage, 'branchCount' => $branchCount, 'appointmentModes' => config('qms.appointment_modes'), - 'industries' => config('qms.industry_templates'), + 'industries' => $packages->labels(), + 'activePackage' => $activePackage, + 'packageMeta' => data_get($organization->settings, 'industry_package'), 'hasPaidPlan' => $plans->hasPaidPlan($organization), 'isPro' => $plans->isPro($organization), 'isEnterprise' => $plans->isEnterprise($organization), @@ -57,7 +66,7 @@ class SettingsController extends Controller 'name' => ['required', 'string', 'max:255'], 'timezone' => ['required', 'timezone'], 'appointment_mode' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.appointment_modes')))], - 'industry' => ['nullable', 'string'], + 'industry' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('industry_packages.packages', [])))], 'notifications_enabled' => ['boolean'], 'care_integration_enabled' => ['nullable', 'boolean'], 'frontdesk_integration_enabled' => ['nullable', 'boolean'], @@ -67,6 +76,7 @@ class SettingsController extends Controller $settings = $organization->settings ?? []; $settings['appointment_mode'] = $validated['appointment_mode']; + $previousIndustry = $settings['industry'] ?? null; $settings['industry'] = $validated['industry'] ?? $settings['industry'] ?? null; $settings['notifications_enabled'] = $request->boolean('notifications_enabled', true); $integrations = $settings['integrations'] ?? []; @@ -98,6 +108,54 @@ class SettingsController extends Controller AuditLogger::record($owner, 'organization.updated', $organization->id, $owner, \App\Models\Organization::class, $organization->id); - return back()->with('success', 'Settings saved.'); + $industryChanged = $settings['industry'] && $settings['industry'] !== $previousIndustry; + if ($industryChanged) { + $branch = Branch::query() + ->where('organization_id', $organization->id) + ->where('is_active', true) + ->orderBy('id') + ->first(); + if ($branch) { + app(IndustryPackageInstaller::class)->install($organization->fresh(), $branch, $settings['industry'], [ + 'actor_ref' => $owner, + ]); + } + } + + return back()->with('success', $industryChanged + ? 'Settings saved. Industry package re-provisioned.' + : 'Settings saved.'); + } + + public function reinstallPackage(Request $request, IndustryPackageInstaller $installer): RedirectResponse + { + $this->authorizeAbility($request, 'settings.manage'); + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + $branch = Branch::query() + ->where('organization_id', $organization->id) + ->where('is_active', true) + ->orderBy('id') + ->first(); + + if (! $branch) { + return back()->with('error', 'Add a branch before reinstalling the industry package.'); + } + + $key = data_get($organization->settings, 'industry', 'custom'); + $result = $installer->install($organization, $branch, $key, [ + 'actor_ref' => $owner, + 'include_optional' => true, + ]); + + $message = 'Industry package synced.'; + 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/Services/Qms/Industry/IndustryPackage.php b/app/Services/Qms/Industry/IndustryPackage.php new file mode 100644 index 0000000..0f92f43 --- /dev/null +++ b/app/Services/Qms/Industry/IndustryPackage.php @@ -0,0 +1,105 @@ + + * } + */ +class IndustryPackage +{ + /** + * @param array $definition + */ + public function __construct( + public readonly string $key, + public readonly array $definition, + ) {} + + public function label(): string + { + return (string) ($this->definition['label'] ?? $this->key); + } + + public function description(): string + { + return (string) ($this->definition['description'] ?? ''); + } + + public function ticketEntity(): string + { + return (string) ($this->definition['ticket_entity'] ?? 'customer'); + } + + public function displayLayout(): string + { + return (string) ($this->definition['display_layout'] ?? 'standard'); + } + + /** + * @return array + */ + public function terminology(): array + { + return (array) ($this->definition['terminology'] ?? []); + } + + /** + * @return array + */ + public function announcement(): array + { + return (array) ($this->definition['announcement'] ?? []); + } + + /** + * @return list + */ + public function kpis(): array + { + return array_values((array) ($this->definition['kpis'] ?? [])); + } + + /** + * @return array{name: string, description?: string} + */ + public function workflowMeta(): array + { + return (array) ($this->definition['workflow'] ?? ['name' => $this->label().' workflow']); + } + + /** + * @return list + */ + public function departments(): array + { + return array_values((array) ($this->definition['departments'] ?? [])); + } + + /** + * @return list + */ + public function stages(): array + { + return array_values((array) ($this->definition['stages'] ?? [])); + } + + /** + * @return list + */ + public function integrations(): array + { + return array_values((array) ($this->definition['integrations'] ?? [])); + } +} diff --git a/app/Services/Qms/Industry/IndustryPackageInstaller.php b/app/Services/Qms/Industry/IndustryPackageInstaller.php new file mode 100644 index 0000000..ea9ef0c --- /dev/null +++ b/app/Services/Qms/Industry/IndustryPackageInstaller.php @@ -0,0 +1,404 @@ +, + * skipped_reason: string|null + * } + */ + public function install(Organization $organization, Branch $branch, ?string $packageKey = null, array $options = []): array + { + $key = $this->registry->resolveKey($packageKey ?? data_get($organization->settings, 'industry')); + $package = $this->registry->get($key); + if (! $package) { + $package = $this->registry->get('custom'); + $key = 'custom'; + } + + $skipStages = (bool) ($options['skip_stages'] ?? false); + $includeOptional = (bool) ($options['include_optional'] ?? true); + $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, + 'stages' => 0, + 'service_points' => 0, + 'workflow_id' => null, + 'skipped_stages' => [], + 'skipped_reason' => $skipStages ? 'stages_managed_by_integration' : null, + ]; + + return DB::transaction(function () use ($organization, $branch, $package, $key, $skipStages, $includeOptional, $actor, &$result) { + $this->applyPackageSettings($organization, $package); + + $departmentsByCode = []; + foreach ($package->departments() as $deptDef) { + $department = $this->upsertDepartment($organization, $branch, $key, $deptDef); + $departmentsByCode[$deptDef['code']] = $department; + $result['departments']++; + } + + $queuesByCode = []; + if (! $skipStages) { + $maxQueues = $this->plans->maxQueues($organization); + $existingCount = ServiceQueue::query() + ->where('organization_id', $organization->id) + ->whereNull('deleted_at') + ->count(); + + foreach ($package->stages() as $stageDef) { + if (! $includeOptional && ($stageDef['optional'] ?? false)) { + $result['skipped_stages'][] = $stageDef['code']; + continue; + } + + $already = ServiceQueue::query() + ->where('organization_id', $organization->id) + ->where('branch_id', $branch->id) + ->where('settings->external_key', $this->stageExternalKey($key, $stageDef['code'], $branch->id)) + ->exists(); + + if (! $already && $maxQueues !== null && $existingCount >= $maxQueues) { + $result['skipped_stages'][] = $stageDef['code']; + $result['skipped_reason'] = 'plan_queue_limit'; + continue; + } + + $department = $departmentsByCode[$stageDef['department']] ?? null; + $queue = $this->upsertStage($organization, $branch, $department, $key, $stageDef); + if (! $already) { + $existingCount++; + } + $queuesByCode[$stageDef['code']] = $queue; + $result['stages']++; + + foreach ($stageDef['service_points'] ?? [] as $index => $pointDef) { + $this->upsertServicePoint($organization, $branch, $department, $queue, $key, $stageDef['code'], $pointDef, $index); + $result['service_points']++; + } + } + + $workflow = $this->upsertWorkflow($organization, $branch, $package, $key, $queuesByCode); + $result['workflow_id'] = $workflow?->id; + } + + $settings = $organization->settings ?? []; + $settings['industry'] = $key; + $settings['industry_package'] = [ + 'key' => $key, + 'version' => (int) config('industry_packages.version', 1), + 'installed_at' => now()->toIso8601String(), + 'branch_id' => $branch->id, + 'stages_installed' => array_keys($queuesByCode), + 'skipped_stages' => $result['skipped_stages'], + 'skipped_reason' => $result['skipped_reason'], + 'ticket_entity' => $package->ticketEntity(), + 'terminology' => $package->terminology(), + 'announcement' => $package->announcement(), + 'display_layout' => $package->displayLayout(), + 'kpis' => $package->kpis(), + ]; + $organization->update(['settings' => $settings]); + + AuditLogger::record( + $organization->owner_ref, + 'organization.updated', + $organization->id, + $actor, + Organization::class, + $organization->id, + ['industry_package' => $key, 'install' => $result], + ); + + return $result; + }); + } + + protected function applyPackageSettings(Organization $organization, IndustryPackage $package): void + { + // Persisted fully after install; noop placeholder for future hooks. + unset($organization, $package); + } + + /** + * @param array{code: string, name: string, type?: string} $deptDef + */ + protected function upsertDepartment(Organization $organization, Branch $branch, string $packageKey, array $deptDef): Department + { + $externalKey = $this->departmentExternalKey($packageKey, $deptDef['code'], $branch->id); + + $department = Department::query() + ->where('branch_id', $branch->id) + ->where('external_key', $externalKey) + ->first(); + + if (! $department) { + $department = Department::query() + ->where('branch_id', $branch->id) + ->where('name', $deptDef['name']) + ->whereNull('external_key') + ->first(); + } + + if ($department) { + $department->update([ + 'name' => $deptDef['name'], + 'type' => $deptDef['type'] ?? 'general', + 'external_key' => $externalKey, + 'is_active' => true, + ]); + + return $department->fresh(); + } + + return Department::create([ + 'owner_ref' => $organization->owner_ref, + 'branch_id' => $branch->id, + 'name' => $deptDef['name'], + 'type' => $deptDef['type'] ?? 'general', + 'external_key' => $externalKey, + 'is_active' => true, + ]); + } + + /** + * @param array $stageDef + */ + protected function upsertStage( + Organization $organization, + Branch $branch, + ?Department $department, + string $packageKey, + array $stageDef, + ): ServiceQueue { + $externalKey = $this->stageExternalKey($packageKey, $stageDef['code'], $branch->id); + + $queue = ServiceQueue::query() + ->where('organization_id', $organization->id) + ->where('branch_id', $branch->id) + ->where('settings->external_key', $externalKey) + ->first(); + + $settings = [ + 'external_key' => $externalKey, + 'routing_mode' => $stageDef['routing_mode'] ?? 'shared_pool', + 'routing_strategy' => $stageDef['routing_strategy'] ?? 'round_robin', + 'industry_package' => $packageKey, + 'stage_code' => $stageDef['code'], + 'optional' => (bool) ($stageDef['optional'] ?? false), + ]; + + if ($queue) { + $queue->update([ + 'department_id' => $department?->id, + 'name' => $stageDef['name'], + 'prefix' => $stageDef['prefix'], + 'strategy' => $stageDef['strategy'] ?? 'fifo', + 'settings' => array_merge($queue->settings ?? [], $settings), + 'is_active' => true, + ]); + + return $queue->fresh(); + } + + return ServiceQueue::create([ + 'owner_ref' => $organization->owner_ref, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'department_id' => $department?->id, + 'name' => $stageDef['name'], + 'prefix' => $stageDef['prefix'], + 'strategy' => $stageDef['strategy'] ?? 'fifo', + 'settings' => $settings, + 'is_active' => true, + 'is_paused' => false, + 'ticket_sequence' => 0, + ]); + } + + /** + * @param array{name: string, destination?: string, code?: string} $pointDef + */ + protected function upsertServicePoint( + Organization $organization, + Branch $branch, + ?Department $department, + ServiceQueue $queue, + string $packageKey, + string $stageCode, + array $pointDef, + int $index, + ): Counter { + $code = $pointDef['code'] ?? strtoupper(substr($stageCode, 0, 2)).($index + 1); + $externalKey = $this->pointExternalKey($packageKey, $stageCode, $code, $branch->id); + + $counter = Counter::query() + ->where('organization_id', $organization->id) + ->where('branch_id', $branch->id) + ->where('settings->external_key', $externalKey) + ->first(); + + $attributes = [ + 'department_id' => $department?->id, + 'name' => $pointDef['name'], + 'code' => $code, + 'destination' => $pointDef['destination'] ?? $pointDef['name'], + 'status' => 'available', + 'settings' => [ + 'external_key' => $externalKey, + 'industry_package' => $packageKey, + 'stage_code' => $stageCode, + ], + 'is_active' => true, + ]; + + if ($counter) { + $counter->update(array_merge($attributes, [ + 'settings' => array_merge($counter->settings ?? [], $attributes['settings']), + ])); + $counter = $counter->fresh(); + } else { + $counter = Counter::create(array_merge($attributes, [ + 'owner_ref' => $organization->owner_ref, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + ])); + } + + if (! $queue->counters()->where('queue_counters.id', $counter->id)->exists()) { + $queue->counters()->attach($counter->id, ['priority' => $index]); + } + + return $counter; + } + + /** + * @param array $queuesByCode + */ + protected function upsertWorkflow( + Organization $organization, + Branch $branch, + IndustryPackage $package, + string $packageKey, + array $queuesByCode, + ): ?Workflow { + if ($queuesByCode === []) { + return null; + } + + $meta = $package->workflowMeta(); + $externalKey = "industry:{$packageKey}:workflow:{$branch->id}"; + + $workflow = Workflow::query() + ->where('organization_id', $organization->id) + ->where('branch_id', $branch->id) + ->where('settings->external_key', $externalKey) + ->first(); + + if (! $workflow) { + $workflow = Workflow::create([ + 'owner_ref' => $organization->owner_ref, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'name' => $meta['name'] ?? $package->label().' workflow', + 'description' => $meta['description'] ?? null, + 'industry_template' => $packageKey, + 'is_active' => true, + 'settings' => ['external_key' => $externalKey], + ]); + } else { + $workflow->update([ + 'name' => $meta['name'] ?? $workflow->name, + 'description' => $meta['description'] ?? $workflow->description, + 'industry_template' => $packageKey, + 'is_active' => true, + 'settings' => array_merge($workflow->settings ?? [], ['external_key' => $externalKey]), + ]); + } + + $sort = 1; + foreach ($package->stages() as $stageDef) { + $queue = $queuesByCode[$stageDef['code']] ?? null; + if (! $queue) { + continue; + } + + $step = WorkflowStep::query() + ->where('workflow_id', $workflow->id) + ->where('service_queue_id', $queue->id) + ->first(); + + if ($step) { + $step->update([ + 'name' => $stageDef['name'], + 'sort_order' => $sort, + 'settings' => ['stage_code' => $stageDef['code']], + ]); + } else { + WorkflowStep::create([ + 'workflow_id' => $workflow->id, + 'service_queue_id' => $queue->id, + 'name' => $stageDef['name'], + 'sort_order' => $sort, + 'settings' => ['stage_code' => $stageDef['code']], + ]); + } + $sort++; + } + + return $workflow->fresh(['steps']); + } + + public function departmentExternalKey(string $packageKey, string $code, int $branchId): string + { + return "industry:{$packageKey}:dept:{$code}:{$branchId}"; + } + + public function stageExternalKey(string $packageKey, string $code, int $branchId): string + { + return "industry:{$packageKey}:stage:{$code}:{$branchId}"; + } + + public function pointExternalKey(string $packageKey, string $stageCode, string $pointCode, int $branchId): string + { + return "industry:{$packageKey}:point:{$stageCode}:{$pointCode}:{$branchId}"; + } +} diff --git a/app/Services/Qms/Industry/IndustryPackageRegistry.php b/app/Services/Qms/Industry/IndustryPackageRegistry.php new file mode 100644 index 0000000..8324b0b --- /dev/null +++ b/app/Services/Qms/Industry/IndustryPackageRegistry.php @@ -0,0 +1,60 @@ + key => label + */ + public function labels(): array + { + $labels = []; + foreach (array_keys(config('industry_packages.packages', [])) as $key) { + $package = $this->get($key); + if ($package) { + $labels[$key] = $package->label(); + } + } + + return $labels; + } + + public function get(string $key): ?IndustryPackage + { + $definition = config("industry_packages.packages.{$key}"); + if (! is_array($definition)) { + return null; + } + + return new IndustryPackage($key, $definition); + } + + public function has(string $key): bool + { + return $this->get($key) !== null; + } + + /** + * @return list + */ + public function all(): array + { + $packages = []; + foreach (array_keys(config('industry_packages.packages', [])) as $key) { + $package = $this->get($key); + if ($package) { + $packages[] = $package; + } + } + + return $packages; + } + + public function resolveKey(?string $key): string + { + $key = $key ?: 'custom'; + + return $this->has($key) ? $key : 'custom'; + } +} diff --git a/app/Services/Qms/OrganizationResolver.php b/app/Services/Qms/OrganizationResolver.php index 8adb5eb..8bc099b 100644 --- a/app/Services/Qms/OrganizationResolver.php +++ b/app/Services/Qms/OrganizationResolver.php @@ -3,10 +3,11 @@ namespace App\Services\Qms; use App\Models\Branch; -use App\Models\Department; use App\Models\Member; use App\Models\Organization; use App\Models\User; +use App\Services\Qms\Industry\IndustryPackageInstaller; +use App\Services\Qms\Industry\IndustryPackageRegistry; use Illuminate\Support\Str; class OrganizationResolver @@ -69,6 +70,8 @@ class OrganizationResolver public function completeOnboarding(User $user, array $data): Organization { $ref = $user->ownerRef(); + $registry = app(IndustryPackageRegistry::class); + $industry = $registry->resolveKey($data['industry'] ?? 'custom'); $organization = Organization::create([ 'owner_ref' => $ref, @@ -77,7 +80,7 @@ class OrganizationResolver 'timezone' => $data['timezone'] ?? config('app.timezone', 'UTC'), 'settings' => [ 'onboarded' => true, - 'industry' => $data['industry'] ?? 'custom', + 'industry' => $industry, 'appointment_mode' => $data['appointment_mode'] ?? 'hybrid', ], ]); @@ -93,18 +96,15 @@ class OrganizationResolver 'is_active' => true, ]); - Department::create([ - 'owner_ref' => $ref, - 'branch_id' => $branch->id, - 'name' => 'General Reception', - 'type' => 'reception', - 'is_active' => true, - ]); - AuditLogger::record($ref, 'organization.created', $organization->id, $ref, Organization::class, $organization->id); AuditLogger::record($ref, 'branch.created', $organization->id, $ref, Branch::class, $branch->id); - return $organization; + app(IndustryPackageInstaller::class)->install($organization->fresh(), $branch, $industry, [ + 'actor_ref' => $ref, + 'include_optional' => true, + ]); + + return $organization->fresh(); } public function branchScope(?Member $member): ?int diff --git a/app/Services/Qms/VoiceAnnouncementService.php b/app/Services/Qms/VoiceAnnouncementService.php index 081012b..fc62279 100644 --- a/app/Services/Qms/VoiceAnnouncementService.php +++ b/app/Services/Qms/VoiceAnnouncementService.php @@ -11,6 +11,7 @@ class VoiceAnnouncementService public function announceCalled(Ticket $ticket, Counter $counter, ?string $locale = null): VoiceAnnouncement { $locale ??= data_get($ticket->serviceQueue?->settings, 'announcement_locale', 'en'); + $ticket->loadMissing(['organization', 'serviceQueue']); $message = $this->buildMessage($ticket, $counter, $locale); return VoiceAnnouncement::create([ @@ -29,27 +30,37 @@ class VoiceAnnouncementService protected function buildMessage(Ticket $ticket, Counter $counter, string $locale): string { - $templates = config('qms.announcement_templates'); - $template = $templates[$locale] ?? $templates['en']; $destination = $counter->displayDestination(); $staff = trim((string) ($counter->staff_display_name ?? '')); $queueName = trim((string) ($ticket->serviceQueue?->name ?? '')); - // Prefer rich healthcare-style copy when staff is known: - // "A005, Dr. Mensah, Consultation Room 4." - if ($staff !== '') { - $parts = array_values(array_filter([ - $ticket->ticket_number, - $staff, - $queueName !== '' ? $queueName.' '.$destination : $destination, - ])); + $industryAnnouncement = (array) data_get($ticket->organization?->settings, 'industry_package.announcement', []); + $global = config('qms.announcement_templates', []); - return implode(', ', $parts).'.'; + if ($staff !== '') { + $richDestination = $queueName !== '' && ! str_contains(strtolower($destination), strtolower($queueName)) + ? trim($queueName.' '.$destination) + : $destination; + $template = $industryAnnouncement['with_staff'] + ?? $global['with_staff'] + ?? ':ticket, :staff, :destination.'; + + return rtrim(str_replace( + [':ticket', ':staff', ':destination', ':counter'], + [$ticket->ticket_number, $staff, $richDestination, $richDestination], + $template, + ), '.').'.'; } + $template = $industryAnnouncement[$locale] + ?? $industryAnnouncement['en'] + ?? $global[$locale] + ?? $global['en'] + ?? 'Ticket :ticket, please proceed to :destination.'; + return str_replace( - [':ticket', ':counter'], - [$ticket->ticket_number, $destination], + [':ticket', ':destination', ':counter'], + [$ticket->ticket_number, $destination, $destination], $template, ); } diff --git a/config/industry_packages.php b/config/industry_packages.php new file mode 100644 index 0000000..52606e9 --- /dev/null +++ b/config/industry_packages.php @@ -0,0 +1,1247 @@ + 1, + + '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' => 'assigned_only', + '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' => 'assigned_only', + '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' => 'assigned_only', + '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' => 'assigned_only', + '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.', + 'ticket_entity' => 'order', + 'integrations' => ['pos'], + 'display_layout' => 'split', + 'terminology' => [ + 'ticket' => 'Order', + 'tickets' => 'Orders', + 'queue' => 'Kitchen queue', + 'service_point' => 'Station', + 'worker' => 'Cook', + 'customer' => 'Guest', + 'call_next' => 'Bump next order', + ], + 'announcement' => [ + 'en' => 'Order :ticket is ready for pickup.', + 'fr' => 'Commande :ticket prête à emporter.', + 'with_staff' => 'Order :ticket — :destination.', + ], + 'kpis' => [ + 'kitchen_prep_time', + 'order_completion_time', + 'station_utilization', + ], + 'workflow' => [ + 'name' => 'Kitchen production', + 'description' => 'Order through pickup', + ], + 'departments' => [ + ['code' => 'orders', 'name' => 'Orders', 'type' => 'general'], + ['code' => 'kitchen', 'name' => 'Kitchen', 'type' => 'general'], + ['code' => 'packing', 'name' => 'Packing', 'type' => 'general'], + ['code' => 'pickup', 'name' => 'Pickup', 'type' => 'collections'], + ], + 'stages' => [ + [ + 'code' => 'orders', + 'department' => 'orders', + 'name' => 'New orders', + 'prefix' => 'O', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [ + ['name' => 'Expo', 'destination' => 'Expo', 'code' => 'EX1'], + ], + ], + [ + 'code' => 'grill', + 'department' => 'kitchen', + 'name' => 'Grill', + 'prefix' => 'G', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'skill_based', + 'service_points' => [ + ['name' => 'Grill Station', 'destination' => 'Grill', 'code' => 'GR1'], + ], + ], + [ + 'code' => 'fryer', + 'department' => 'kitchen', + 'name' => 'Fryer', + 'prefix' => 'F', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'skill_based', + 'service_points' => [ + ['name' => 'Fryer Station', 'destination' => 'Fryer', 'code' => 'FR1'], + ], + ], + [ + 'code' => 'drinks', + 'department' => 'kitchen', + 'name' => 'Drinks', + 'prefix' => 'D', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'least_busy', + 'service_points' => [ + ['name' => 'Drinks Station', 'destination' => 'Drinks', 'code' => 'DR1'], + ], + ], + [ + 'code' => 'packing', + 'department' => 'packing', + 'name' => 'Packing', + 'prefix' => 'K', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [ + ['name' => 'Packing Bench', 'destination' => 'Packing', 'code' => 'PK1'], + ], + ], + [ + 'code' => 'pickup', + 'department' => 'pickup', + 'name' => 'Pickup', + 'prefix' => 'U', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [ + ['name' => 'Pickup Counter', 'destination' => 'Pickup', 'code' => 'PU1'], + ], + ], + ], + ], + + 'banking' => [ + 'label' => 'Banking', + 'description' => 'Central ticketing with next-available tellers and specialty desks.', + 'ticket_entity' => 'customer', + 'integrations' => [], + 'display_layout' => 'standard', + 'terminology' => [ + 'ticket' => 'Ticket', + 'tickets' => 'Tickets', + 'queue' => 'Queue', + 'service_point' => 'Counter', + 'worker' => 'Teller', + 'customer' => 'Customer', + 'call_next' => 'Call next', + ], + 'announcement' => [ + 'en' => 'Ticket :ticket, please proceed to :destination.', + 'fr' => 'Ticket :ticket, veuillez vous rendre au :destination.', + 'with_staff' => 'Ticket :ticket, please proceed to :destination.', + ], + 'kpis' => [ + 'teller_efficiency', + 'sla_compliance', + 'queue_abandonment', + ], + 'workflow' => [ + 'name' => 'Branch visit', + 'description' => 'Reception through service exit', + ], + 'departments' => [ + ['code' => 'reception', 'name' => 'Reception', 'type' => 'reception'], + ['code' => 'teller', 'name' => 'Teller', 'type' => 'cashier'], + ['code' => 'customer_service', 'name' => 'Customer Service', 'type' => 'customer_service'], + ['code' => 'loans', 'name' => 'Loans', 'type' => 'general'], + ['code' => 'accounts', 'name' => 'Account Opening', 'type' => 'general'], + ['code' => 'forex', 'name' => 'Foreign Exchange', 'type' => 'cashier'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'A', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [ + ['name' => 'Reception Desk', 'destination' => 'Reception', 'code' => 'R1'], + ], + ], + [ + 'code' => 'teller', + 'department' => 'teller', + 'name' => 'Teller', + 'prefix' => 'T', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'least_busy', + 'service_points' => [ + ['name' => 'Teller 1', 'destination' => 'Counter 1', 'code' => 'T1'], + ['name' => 'Teller 2', 'destination' => 'Counter 2', 'code' => 'T2'], + ], + ], + [ + 'code' => 'customer_service', + 'department' => 'customer_service', + 'name' => 'Customer Service', + 'prefix' => 'S', + 'strategy' => 'priority', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'skill_based', + 'service_points' => [ + ['name' => 'Service Desk 1', 'destination' => 'Service Desk 1', 'code' => 'S1'], + ], + ], + [ + 'code' => 'loans', + 'department' => 'loans', + 'name' => 'Loans', + 'prefix' => 'L', + 'strategy' => 'appointment', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'manual_assignment', + 'optional' => true, + 'service_points' => [ + ['name' => 'Loans Desk', 'destination' => 'Loans Desk', 'code' => 'LN1'], + ], + ], + [ + 'code' => 'accounts', + 'department' => 'accounts', + 'name' => 'Account Opening', + 'prefix' => 'N', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'optional' => true, + 'service_points' => [ + ['name' => 'Accounts Desk', 'destination' => 'Accounts Desk', 'code' => 'AC1'], + ], + ], + [ + 'code' => 'forex', + 'department' => 'forex', + 'name' => 'Foreign Exchange', + 'prefix' => 'X', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'optional' => true, + 'service_points' => [ + ['name' => 'Forex Counter', 'destination' => 'Forex', 'code' => 'FX1'], + ], + ], + ], + ], + + 'government' => [ + 'label' => 'Government Office', + 'description' => 'Passport, DVLA, tax, and municipal service workflows.', + 'ticket_entity' => 'application', + 'integrations' => [], + 'display_layout' => 'standard', + 'terminology' => [ + 'ticket' => 'Application', + 'tickets' => 'Applications', + 'queue' => 'Queue', + 'service_point' => 'Window', + 'worker' => 'Officer', + 'customer' => 'Citizen', + 'call_next' => 'Call next', + ], + 'announcement' => [ + 'en' => 'Application :ticket, please proceed to :destination.', + 'with_staff' => 'Application :ticket, please proceed to :destination.', + ], + 'kpis' => ['processing_time', 'applications_completed'], + 'workflow' => ['name' => 'Citizen service', 'description' => 'Reception through collection'], + 'departments' => [ + ['code' => 'reception', 'name' => 'Reception', 'type' => 'reception'], + ['code' => 'verification', 'name' => 'Verification', 'type' => 'verification'], + ['code' => 'processing', 'name' => 'Processing', 'type' => 'general'], + ['code' => 'payment', 'name' => 'Payment', 'type' => 'payments'], + ['code' => 'collection', 'name' => 'Collection', 'type' => 'collections'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'A', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Reception', 'destination' => 'Reception', 'code' => 'R1']], + ], + [ + 'code' => 'verification', + 'department' => 'verification', + 'name' => 'Verification', + 'prefix' => 'V', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Verification Window', 'destination' => 'Window 1', 'code' => 'V1']], + ], + [ + 'code' => 'processing', + 'department' => 'processing', + 'name' => 'Processing', + 'prefix' => 'P', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'least_busy', + 'service_points' => [['name' => 'Processing Desk', 'destination' => 'Processing', 'code' => 'P1']], + ], + [ + 'code' => 'payment', + 'department' => 'payment', + 'name' => 'Payment', + 'prefix' => 'Y', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Payment Counter', 'destination' => 'Payments', 'code' => 'Y1']], + ], + [ + 'code' => 'collection', + 'department' => 'collection', + 'name' => 'Collection', + 'prefix' => 'C', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Collection Window', 'destination' => 'Collection', 'code' => 'C1']], + ], + ], + ], + + 'retail_service' => [ + 'label' => 'Retail Service Centre', + 'description' => 'Phone, electronics, and appliance repair centres.', + 'ticket_entity' => 'device', + 'integrations' => [], + 'display_layout' => 'compact', + 'terminology' => [ + 'ticket' => 'Job', + 'tickets' => 'Jobs', + 'queue' => 'Workbench queue', + 'service_point' => 'Bench', + 'worker' => 'Technician', + 'customer' => 'Customer', + 'call_next' => 'Call next job', + ], + 'announcement' => [ + 'en' => 'Job :ticket, please proceed to :destination.', + 'with_staff' => 'Job :ticket — :destination.', + ], + 'kpis' => ['avg_repair_duration', 'bench_utilization'], + 'workflow' => ['name' => 'Repair journey', 'description' => 'Reception through pickup'], + 'departments' => [ + ['code' => 'reception', 'name' => 'Reception', 'type' => 'reception'], + ['code' => 'diagnosis', 'name' => 'Diagnosis', 'type' => 'technical_support'], + ['code' => 'repair', 'name' => 'Repair', 'type' => 'technical_support'], + ['code' => 'qc', 'name' => 'Quality Check', 'type' => 'verification'], + ['code' => 'pickup', 'name' => 'Pickup', 'type' => 'collections'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'R', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Front Desk', 'destination' => 'Front Desk', 'code' => 'FD1']], + ], + [ + 'code' => 'diagnosis', + 'department' => 'diagnosis', + 'name' => 'Diagnosis', + 'prefix' => 'D', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'skill_based', + 'service_points' => [['name' => 'Diagnosis Bench', 'destination' => 'Diagnosis', 'code' => 'DB1']], + ], + [ + 'code' => 'repair', + 'department' => 'repair', + 'name' => 'Repair', + 'prefix' => 'X', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'skill_based', + 'service_points' => [['name' => 'Repair Bench 1', 'destination' => 'Bench 1', 'code' => 'RB1']], + ], + [ + 'code' => 'qc', + 'department' => 'qc', + 'name' => 'Quality Check', + 'prefix' => 'Q', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'QC Desk', 'destination' => 'QC', 'code' => 'QC1']], + ], + [ + 'code' => 'pickup', + 'department' => 'pickup', + 'name' => 'Pickup', + 'prefix' => 'P', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Pickup Counter', 'destination' => 'Pickup', 'code' => 'PU1']], + ], + ], + ], + + 'vehicle' => [ + 'label' => 'Vehicle Service Centre', + 'description' => 'Inspection, mechanic bays, electrical, wash, and release.', + 'ticket_entity' => 'vehicle', + 'integrations' => [], + 'display_layout' => 'split', + 'terminology' => [ + 'ticket' => 'Vehicle', + 'tickets' => 'Vehicles', + 'queue' => 'Bay queue', + 'service_point' => 'Bay', + 'worker' => 'Technician', + 'customer' => 'Owner', + 'call_next' => 'Call next vehicle', + ], + 'announcement' => [ + 'en' => 'Vehicle :ticket, please proceed to :destination.', + 'with_staff' => 'Vehicle :ticket — :destination.', + ], + 'kpis' => ['avg_repair_duration', 'bay_utilization'], + 'workflow' => ['name' => 'Vehicle service', 'description' => 'Reception through release'], + 'departments' => [ + ['code' => 'reception', 'name' => 'Reception', 'type' => 'reception'], + ['code' => 'inspection', 'name' => 'Inspection', 'type' => 'verification'], + ['code' => 'mechanic', 'name' => 'Mechanic', 'type' => 'technical_support'], + ['code' => 'electrical', 'name' => 'Electrical', 'type' => 'technical_support'], + ['code' => 'wash', 'name' => 'Wash', 'type' => 'general'], + ['code' => 'billing', 'name' => 'Billing', 'type' => 'payments'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'V', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Service Desk', 'destination' => 'Reception', 'code' => 'SD1']], + ], + [ + 'code' => 'inspection', + 'department' => 'inspection', + 'name' => 'Inspection', + 'prefix' => 'I', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Inspection Bay', 'destination' => 'Inspection', 'code' => 'IB1']], + ], + [ + 'code' => 'mechanic', + 'department' => 'mechanic', + 'name' => 'Mechanic', + 'prefix' => 'M', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'skill_based', + 'service_points' => [ + ['name' => 'Bay 1', 'destination' => 'Bay 1', 'code' => 'B1'], + ['name' => 'Bay 2', 'destination' => 'Bay 2', 'code' => 'B2'], + ], + ], + [ + 'code' => 'electrical', + 'department' => 'electrical', + 'name' => 'Electrical', + 'prefix' => 'E', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'skill_based', + 'optional' => true, + 'service_points' => [['name' => 'Electrical Bay', 'destination' => 'Electrical', 'code' => 'EB1']], + ], + [ + 'code' => 'wash', + 'department' => 'wash', + 'name' => 'Wash', + 'prefix' => 'W', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'optional' => true, + 'service_points' => [['name' => 'Wash Bay', 'destination' => 'Wash', 'code' => 'WB1']], + ], + [ + 'code' => 'billing', + 'department' => 'billing', + 'name' => 'Billing', + 'prefix' => 'B', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Cashier', 'destination' => 'Billing', 'code' => 'CA1']], + ], + ], + ], + + 'hospitality' => [ + 'label' => 'Hotel / Hospitality', + 'description' => 'Check-in, concierge, guest services, and checkout.', + 'ticket_entity' => 'guest', + 'integrations' => [], + 'display_layout' => 'compact', + 'terminology' => [ + 'ticket' => 'Guest', + 'tickets' => 'Guests', + 'queue' => 'Front desk queue', + 'service_point' => 'Desk', + 'worker' => 'Agent', + 'customer' => 'Guest', + 'call_next' => 'Call next guest', + ], + 'announcement' => [ + 'en' => 'Guest :ticket, please proceed to :destination.', + 'with_staff' => 'Guest :ticket — :destination.', + ], + 'kpis' => ['checkin_time', 'desk_utilization'], + 'workflow' => ['name' => 'Guest journey', 'description' => 'Arrival through checkout'], + 'departments' => [ + ['code' => 'reception', 'name' => 'Reception', 'type' => 'reception'], + ['code' => 'checkin', 'name' => 'Check-in', 'type' => 'general'], + ['code' => 'concierge', 'name' => 'Concierge', 'type' => 'customer_service'], + ['code' => 'checkout', 'name' => 'Checkout', 'type' => 'cashier'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'H', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Lobby Desk', 'destination' => 'Lobby', 'code' => 'LB1']], + ], + [ + 'code' => 'checkin', + 'department' => 'checkin', + 'name' => 'Check-in', + 'prefix' => 'C', + 'strategy' => 'appointment', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'least_busy', + 'service_points' => [['name' => 'Check-in Desk 1', 'destination' => 'Check-in 1', 'code' => 'CI1']], + ], + [ + 'code' => 'concierge', + 'department' => 'concierge', + 'name' => 'Concierge', + 'prefix' => 'G', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'optional' => true, + 'service_points' => [['name' => 'Concierge Desk', 'destination' => 'Concierge', 'code' => 'CG1']], + ], + [ + 'code' => 'checkout', + 'department' => 'checkout', + 'name' => 'Checkout', + 'prefix' => 'X', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Checkout Desk', 'destination' => 'Checkout', 'code' => 'CO1']], + ], + ], + ], + + 'logistics' => [ + 'label' => 'Logistics', + 'description' => 'Dock allocation, loading, inspection, and dispatch.', + 'ticket_entity' => 'shipment', + 'integrations' => [], + 'display_layout' => 'compact', + 'terminology' => [ + 'ticket' => 'Shipment', + 'tickets' => 'Shipments', + 'queue' => 'Dock queue', + 'service_point' => 'Dock', + 'worker' => 'Handler', + 'customer' => 'Carrier', + 'call_next' => 'Call next shipment', + ], + 'announcement' => [ + 'en' => 'Shipment :ticket, please proceed to :destination.', + 'with_staff' => 'Shipment :ticket — :destination.', + ], + 'kpis' => ['dock_turnaround', 'dispatch_time'], + 'workflow' => ['name' => 'Yard operations', 'description' => 'Arrival through dispatch'], + 'departments' => [ + ['code' => 'arrival', 'name' => 'Arrival', 'type' => 'reception'], + ['code' => 'dock', 'name' => 'Dock Allocation', 'type' => 'general'], + ['code' => 'loading', 'name' => 'Loading', 'type' => 'general'], + ['code' => 'inspection', 'name' => 'Inspection', 'type' => 'verification'], + ['code' => 'dispatch', 'name' => 'Dispatch', 'type' => 'collections'], + ], + 'stages' => [ + [ + 'code' => 'arrival', + 'department' => 'arrival', + 'name' => 'Arrival', + 'prefix' => 'A', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Gate', 'destination' => 'Gate', 'code' => 'GT1']], + ], + [ + 'code' => 'dock', + 'department' => 'dock', + 'name' => 'Dock Allocation', + 'prefix' => 'D', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'least_busy', + 'service_points' => [ + ['name' => 'Dock 1', 'destination' => 'Dock 1', 'code' => 'D1'], + ['name' => 'Dock 2', 'destination' => 'Dock 2', 'code' => 'D2'], + ], + ], + [ + 'code' => 'loading', + 'department' => 'loading', + 'name' => 'Loading', + 'prefix' => 'L', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'manual_assignment', + 'service_points' => [['name' => 'Loading Bay', 'destination' => 'Loading', 'code' => 'LB1']], + ], + [ + 'code' => 'inspection', + 'department' => 'inspection', + 'name' => 'Inspection', + 'prefix' => 'I', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Inspection', 'destination' => 'Inspection', 'code' => 'IN1']], + ], + [ + 'code' => 'dispatch', + 'department' => 'dispatch', + 'name' => 'Dispatch', + 'prefix' => 'X', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Dispatch Office', 'destination' => 'Dispatch', 'code' => 'DX1']], + ], + ], + ], + + 'university' => [ + 'label' => 'University', + 'description' => 'Admissions, finance, registrar, library, and student affairs.', + 'ticket_entity' => 'student', + 'integrations' => [], + 'display_layout' => 'standard', + 'terminology' => [ + 'ticket' => 'Ticket', + 'tickets' => 'Tickets', + 'queue' => 'Queue', + 'service_point' => 'Desk', + 'worker' => 'Officer', + 'customer' => 'Student', + 'call_next' => 'Call next', + ], + 'announcement' => [ + 'en' => 'Ticket :ticket, please proceed to :destination.', + 'with_staff' => 'Ticket :ticket — :destination.', + ], + 'kpis' => ['avg_wait_time', 'desk_throughput'], + 'workflow' => ['name' => 'Student services', 'description' => 'Campus service desks'], + 'departments' => [ + ['code' => 'admissions', 'name' => 'Admissions', 'type' => 'reception'], + ['code' => 'finance', 'name' => 'Finance', 'type' => 'payments'], + ['code' => 'registrar', 'name' => 'Registrar', 'type' => 'general'], + ['code' => 'library', 'name' => 'Library', 'type' => 'general'], + ['code' => 'student_affairs', 'name' => 'Student Affairs', 'type' => 'customer_service'], + ], + 'stages' => [ + [ + 'code' => 'admissions', + 'department' => 'admissions', + 'name' => 'Admissions', + 'prefix' => 'A', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Admissions Desk', 'destination' => 'Admissions', 'code' => 'AD1']], + ], + [ + 'code' => 'finance', + 'department' => 'finance', + 'name' => 'Finance', + 'prefix' => 'F', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Finance Counter', 'destination' => 'Finance', 'code' => 'FN1']], + ], + [ + 'code' => 'registrar', + 'department' => 'registrar', + 'name' => 'Registrar', + 'prefix' => 'R', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Registrar Desk', 'destination' => 'Registrar', 'code' => 'RG1']], + ], + [ + 'code' => 'library', + 'department' => 'library', + 'name' => 'Library', + 'prefix' => 'L', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'optional' => true, + 'service_points' => [['name' => 'Library Desk', 'destination' => 'Library', 'code' => 'LB1']], + ], + [ + 'code' => 'student_affairs', + 'department' => 'student_affairs', + 'name' => 'Student Affairs', + 'prefix' => 'S', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'optional' => true, + 'service_points' => [['name' => 'Student Affairs', 'destination' => 'Student Affairs', 'code' => 'SA1']], + ], + ], + ], + + 'telecom' => [ + 'label' => 'Telecom Service Centre', + 'description' => 'SIM registration, customer care, technical support, and device repair.', + 'ticket_entity' => 'customer', + 'integrations' => [], + 'display_layout' => 'standard', + 'terminology' => [ + 'ticket' => 'Ticket', + 'tickets' => 'Tickets', + 'queue' => 'Queue', + 'service_point' => 'Counter', + 'worker' => 'Agent', + 'customer' => 'Customer', + 'call_next' => 'Call next', + ], + 'announcement' => [ + 'en' => 'Ticket :ticket, please proceed to :destination.', + 'with_staff' => 'Ticket :ticket — :destination.', + ], + 'kpis' => ['avg_wait_time', 'agent_utilization'], + 'workflow' => ['name' => 'Store visit', 'description' => 'Reception through support'], + 'departments' => [ + ['code' => 'reception', 'name' => 'Reception', 'type' => 'reception'], + ['code' => 'sim', 'name' => 'SIM Registration', 'type' => 'general'], + ['code' => 'care', 'name' => 'Customer Care', 'type' => 'customer_service'], + ['code' => 'tech', 'name' => 'Technical Support', 'type' => 'technical_support'], + ['code' => 'repair', 'name' => 'Device Repair', 'type' => 'technical_support'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'A', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Reception', 'destination' => 'Reception', 'code' => 'R1']], + ], + [ + 'code' => 'sim', + 'department' => 'sim', + 'name' => 'SIM Registration', + 'prefix' => 'S', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'SIM Desk', 'destination' => 'SIM Desk', 'code' => 'SM1']], + ], + [ + 'code' => 'care', + 'department' => 'care', + 'name' => 'Customer Care', + 'prefix' => 'C', + 'strategy' => 'priority', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'least_busy', + 'service_points' => [['name' => 'Care Desk 1', 'destination' => 'Care 1', 'code' => 'CD1']], + ], + [ + 'code' => 'tech', + 'department' => 'tech', + 'name' => 'Technical Support', + 'prefix' => 'T', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'skill_based', + 'service_points' => [['name' => 'Tech Desk', 'destination' => 'Tech Support', 'code' => 'TD1']], + ], + [ + 'code' => 'repair', + 'department' => 'repair', + 'name' => 'Device Repair', + 'prefix' => 'D', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'skill_based', + 'optional' => true, + 'service_points' => [['name' => 'Repair Bench', 'destination' => 'Repair', 'code' => 'RB1']], + ], + ], + ], + + 'events' => [ + 'label' => 'Events', + 'description' => 'Registration, badge printing, and hall entry.', + 'ticket_entity' => 'attendee', + 'integrations' => [], + 'display_layout' => 'media', + 'terminology' => [ + 'ticket' => 'Attendee', + 'tickets' => 'Attendees', + 'queue' => 'Line', + 'service_point' => 'Station', + 'worker' => 'Staff', + 'customer' => 'Attendee', + 'call_next' => 'Call next', + ], + 'announcement' => [ + 'en' => 'Attendee :ticket, please proceed to :destination.', + 'with_staff' => 'Attendee :ticket — :destination.', + ], + 'kpis' => ['checkin_rate', 'entry_throughput'], + 'workflow' => ['name' => 'Event entry', 'description' => 'Registration through hall'], + 'departments' => [ + ['code' => 'registration', 'name' => 'Registration', 'type' => 'reception'], + ['code' => 'badges', 'name' => 'Badge Printing', 'type' => 'general'], + ['code' => 'entry', 'name' => 'Hall Entry', 'type' => 'security'], + ], + 'stages' => [ + [ + 'code' => 'registration', + 'department' => 'registration', + 'name' => 'Registration', + 'prefix' => 'E', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Reg Desk 1', 'destination' => 'Registration 1', 'code' => 'RD1']], + ], + [ + 'code' => 'badges', + 'department' => 'badges', + 'name' => 'Badge Printing', + 'prefix' => 'B', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Badge Station', 'destination' => 'Badges', 'code' => 'BD1']], + ], + [ + 'code' => 'entry', + 'department' => 'entry', + 'name' => 'Hall Entry', + 'prefix' => 'H', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Hall Gate', 'destination' => 'Hall Entrance', 'code' => 'HG1']], + ], + ], + ], + + 'church' => [ + 'label' => 'Church', + 'description' => 'Reception, counselling, pastoral sessions, and follow-up.', + 'ticket_entity' => 'member', + 'integrations' => [], + 'display_layout' => 'compact', + 'terminology' => [ + 'ticket' => 'Visitor', + 'tickets' => 'Visitors', + 'queue' => 'Waiting list', + 'service_point' => 'Room', + 'worker' => 'Counsellor', + 'customer' => 'Member', + 'call_next' => 'Call next', + ], + 'announcement' => [ + 'en' => 'Visitor :ticket, please proceed to :destination.', + 'with_staff' => 'Visitor :ticket — :destination.', + ], + 'kpis' => ['session_time', 'followup_rate'], + 'workflow' => ['name' => 'Pastoral care', 'description' => 'Reception through follow-up'], + 'departments' => [ + ['code' => 'reception', 'name' => 'Reception', 'type' => 'reception'], + ['code' => 'counselling', 'name' => 'Counselling', 'type' => 'consultation'], + ['code' => 'pastoral', 'name' => 'Pastoral Session', 'type' => 'consultation'], + ['code' => 'followup', 'name' => 'Follow-up', 'type' => 'customer_service'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'R', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Welcome Desk', 'destination' => 'Welcome Desk', 'code' => 'WD1']], + ], + [ + 'code' => 'counselling', + 'department' => 'counselling', + 'name' => 'Counselling', + 'prefix' => 'C', + 'strategy' => 'appointment', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'manual_assignment', + 'service_points' => [['name' => 'Counselling Room 1', 'destination' => 'Room 1', 'code' => 'CR1']], + ], + [ + 'code' => 'pastoral', + 'department' => 'pastoral', + 'name' => 'Pastoral Session', + 'prefix' => 'P', + 'strategy' => 'appointment', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'manual_assignment', + 'service_points' => [['name' => 'Pastoral Office', 'destination' => 'Pastoral Office', 'code' => 'PO1']], + ], + [ + 'code' => 'followup', + 'department' => 'followup', + 'name' => 'Follow-up', + 'prefix' => 'F', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'optional' => true, + 'service_points' => [['name' => 'Follow-up Desk', 'destination' => 'Follow-up', 'code' => 'FU1']], + ], + ], + ], + + 'visitor' => [ + 'label' => 'Visitor Management', + 'description' => 'General visitor check-in and host queues.', + 'ticket_entity' => 'visitor', + 'integrations' => ['frontdesk'], + 'display_layout' => 'standard', + 'terminology' => [ + 'ticket' => 'Visitor', + 'tickets' => 'Visitors', + 'queue' => 'Queue', + 'service_point' => 'Desk', + 'worker' => 'Host', + 'customer' => 'Visitor', + 'call_next' => 'Call next visitor', + ], + 'announcement' => [ + 'en' => 'Visitor :ticket, please proceed to :destination.', + 'with_staff' => 'Visitor :ticket — :destination.', + ], + 'kpis' => ['avg_wait_time', 'checkin_rate'], + 'workflow' => ['name' => 'Visitor check-in', 'description' => 'Reception through host'], + 'departments' => [ + ['code' => 'reception', 'name' => 'Reception', 'type' => 'reception'], + ['code' => 'host', 'name' => 'Host', 'type' => 'general'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'V', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Reception Desk', 'destination' => 'Reception', 'code' => 'R1']], + ], + [ + 'code' => 'host', + 'department' => 'host', + 'name' => 'Host Meeting', + 'prefix' => 'H', + 'strategy' => 'fifo', + 'routing_mode' => 'assigned_only', + 'routing_strategy' => 'manual_assignment', + 'service_points' => [['name' => 'Meeting Room 1', 'destination' => 'Meeting Room 1', 'code' => 'MR1']], + ], + ], + ], + + 'retail' => [ + 'label' => 'Retail', + 'description' => 'General retail customer service and returns.', + 'ticket_entity' => 'customer', + 'integrations' => ['pos'], + 'display_layout' => 'standard', + 'terminology' => [ + 'ticket' => 'Ticket', + 'tickets' => 'Tickets', + 'queue' => 'Queue', + 'service_point' => 'Counter', + 'worker' => 'Associate', + 'customer' => 'Customer', + 'call_next' => 'Call next', + ], + 'announcement' => [ + 'en' => 'Ticket :ticket, please proceed to :destination.', + 'with_staff' => 'Ticket :ticket — :destination.', + ], + 'kpis' => ['avg_wait_time', 'counter_utilization'], + 'workflow' => ['name' => 'Store service', 'description' => 'Customer service flow'], + 'departments' => [ + ['code' => 'reception', 'name' => 'Customer Service', 'type' => 'customer_service'], + ['code' => 'returns', 'name' => 'Returns', 'type' => 'collections'], + ['code' => 'cashier', 'name' => 'Cashier', 'type' => 'cashier'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Customer Service', + 'prefix' => 'A', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Service Desk 1', 'destination' => 'Service Desk 1', 'code' => 'SD1']], + ], + [ + 'code' => 'returns', + 'department' => 'returns', + 'name' => 'Returns', + 'prefix' => 'R', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'optional' => true, + 'service_points' => [['name' => 'Returns Counter', 'destination' => 'Returns', 'code' => 'RT1']], + ], + [ + 'code' => 'cashier', + 'department' => 'cashier', + 'name' => 'Cashier', + 'prefix' => 'C', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'least_busy', + 'service_points' => [['name' => 'Cashier 1', 'destination' => 'Cashier 1', 'code' => 'CA1']], + ], + ], + ], + + 'custom' => [ + 'label' => 'Custom', + 'description' => 'Start with a blank canvas — add departments, stages, and service points yourself.', + 'ticket_entity' => 'customer', + 'integrations' => [], + 'display_layout' => 'standard', + 'terminology' => [ + 'ticket' => 'Ticket', + 'tickets' => 'Tickets', + 'queue' => 'Queue', + 'service_point' => 'Counter', + 'worker' => 'Staff', + 'customer' => 'Customer', + 'call_next' => 'Call next', + ], + 'announcement' => [ + 'en' => 'Ticket :ticket, please proceed to :destination.', + 'with_staff' => 'Ticket :ticket — :destination.', + ], + 'kpis' => ['avg_wait_time'], + 'workflow' => ['name' => 'Custom workflow', 'description' => 'Build your own stages'], + 'departments' => [ + ['code' => 'reception', 'name' => 'General Reception', 'type' => 'reception'], + ], + 'stages' => [ + [ + 'code' => 'reception', + 'department' => 'reception', + 'name' => 'Reception', + 'prefix' => 'A', + 'strategy' => 'fifo', + 'routing_mode' => 'shared_pool', + 'routing_strategy' => 'round_robin', + 'service_points' => [['name' => 'Counter 1', 'destination' => 'Counter 1', 'code' => 'C1']], + ], + ], + ], + ], +]; diff --git a/config/qms.php b/config/qms.php index 2832ba9..c651cf0 100644 --- a/config/qms.php +++ b/config/qms.php @@ -26,6 +26,10 @@ return [ 'payments' => 'Payments', 'collections' => 'Collections', 'technical_support' => 'Technical Support', + 'security' => 'Security', + 'triage' => 'Triage', + 'imaging' => 'Imaging', + 'billing' => 'Billing', ], 'queue_strategies' => [ @@ -100,22 +104,47 @@ return [ ], 'announcement_templates' => [ - 'en' => 'Ticket :ticket, please proceed to :counter.', - 'fr' => 'Ticket :ticket, veuillez vous rendre au :counter.', - 'tw' => 'Ticket :ticket, mesrɛ kɔ :counter.', + 'en' => 'Ticket :ticket, please proceed to :destination.', + 'fr' => 'Ticket :ticket, veuillez vous rendre au :destination.', + 'tw' => 'Ticket :ticket, mesrɛ kɔ :destination.', + 'with_staff' => ':ticket, :staff, :destination.', ], + /* + |-------------------------------------------------------------------------- + | Industry labels (package definitions live in config/industry_packages.php) + |-------------------------------------------------------------------------- + */ 'industry_templates' => [ 'healthcare' => 'Healthcare', + 'restaurant' => 'Restaurant', 'banking' => 'Banking', 'government' => 'Government Office', - 'visitor' => 'Visitor Management', + 'retail_service' => 'Retail Service Centre', + 'vehicle' => 'Vehicle Service Centre', + 'hospitality' => 'Hotel / Hospitality', + 'logistics' => 'Logistics', 'university' => 'University', - 'retail' => 'Retail', 'telecom' => 'Telecom Service Centre', + 'events' => 'Events', + 'church' => 'Church', + 'visitor' => 'Visitor Management', + 'retail' => 'Retail', 'custom' => 'Custom', ], + 'routing_strategies' => [ + 'round_robin' => 'Round robin', + 'least_busy' => 'Least busy', + 'skill_based' => 'Skill based', + 'doctor_assignment' => 'Doctor assignment', + 'manual_assignment' => 'Manual assignment', + 'priority_assignment' => 'Priority assignment', + 'appointment_based' => 'Appointment based', + 'random' => 'Random', + 'department_rules' => 'Department rules', + ], + 'audit_actions' => [ 'organization.created' => 'Organization created', 'organization.updated' => 'Organization updated', @@ -247,12 +276,17 @@ return [ 'capacity' => 'Capacity limit', ], + /* + | 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' => 'Pharmacy', 'sort_order' => 4], + ['name' => 'Laboratory', 'sort_order' => 4], + ['name' => 'Pharmacy', 'sort_order' => 5], + ['name' => 'Billing', 'sort_order' => 6], ], 'banking' => [ ['name' => 'Reception', 'sort_order' => 1], @@ -260,9 +294,11 @@ return [ ['name' => 'Customer service', 'sort_order' => 3], ], 'government' => [ - ['name' => 'Document check', 'sort_order' => 1], - ['name' => 'Processing', 'sort_order' => 2], - ['name' => 'Collection', 'sort_order' => 3], + ['name' => 'Reception', 'sort_order' => 1], + ['name' => 'Verification', 'sort_order' => 2], + ['name' => 'Processing', 'sort_order' => 3], + ['name' => 'Payment', 'sort_order' => 4], + ['name' => 'Collection', 'sort_order' => 5], ], 'custom' => [], ], diff --git a/resources/views/qms/onboarding/show.blade.php b/resources/views/qms/onboarding/show.blade.php index 743d5fd..0edc12e 100644 --- a/resources/views/qms/onboarding/show.blade.php +++ b/resources/views/qms/onboarding/show.blade.php @@ -1,7 +1,9 @@

Welcome to Ladill Queue

-

Configure your organization to start managing customer flow.

+

+ Choose your industry and we will provision departments, workflow stages, service points, announcements, and dashboards for you. +

@csrf @@ -14,11 +16,14 @@
- + @foreach ($industries as $value => $meta) + @endforeach +

@@ -68,4 +73,18 @@
+ + diff --git a/resources/views/qms/settings/edit.blade.php b/resources/views/qms/settings/edit.blade.php index 47dd8f8..e4e732f 100644 --- a/resources/views/qms/settings/edit.blade.php +++ b/resources/views/qms/settings/edit.blade.php @@ -43,12 +43,22 @@
- + + @if ($activePackage) +

{{ $activePackage->description() }}

+

+ Ticket entity: {{ $activePackage->ticketEntity() }} + · Stages installed: {{ count(data_get($packageMeta, 'stages_installed', [])) }} + @if (data_get($packageMeta, 'skipped_stages')) + · Skipped: {{ implode(', ', data_get($packageMeta, 'skipped_stages', [])) }} + @endif +

+ @endif