Files
isaaccladandCursor 656f402e8f
Deploy Ladill Care / deploy (push) Successful in 38s
Restrict lab catalog pricing to admins only.
Split catalog CRUD onto lab.catalog.manage so technicians keep lab.manage for queue and results workflow without setting investigation prices.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 18:58:57 +00:00

240 lines
11 KiB
PHP

<?php
namespace App\Http\Controllers\Care;
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Http\Controllers\Controller;
use App\Models\Branch;
use App\Models\Organization;
use App\Services\Billing\PlatformEmailClient;
use App\Services\Billing\PlatformSmsClient;
use App\Services\Care\AppointmentPatientNotifier;
use App\Services\Care\AuditLogger;
use App\Services\Care\CareFeatures;
use App\Services\Care\CarePermissions;
use App\Services\Care\CareQueueBridge;
use App\Services\Care\CareQueueContexts;
use App\Services\Care\CareQueueProvisioner;
use App\Services\Care\PlanService;
use App\Services\Care\Workflow\WorkflowTemplateInstaller;
use App\Services\Care\Workflow\WorkflowTemplateRegistry;
use App\Services\Messaging\MessagingCredentialsService;
use App\Support\OrganizationBranding;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SettingsController extends Controller
{
use ScopesToAccount;
public function edit(
Request $request,
MessagingCredentialsService $credentials,
PlatformEmailClient $platformEmail,
PlatformSmsClient $platformSms,
): View {
$this->authorizeAbility($request, 'settings.view');
$organization = $this->organization($request);
$member = $this->member($request);
$permissions = app(CarePermissions::class);
$canManage = $permissions->can($member, 'settings.manage');
$credential = $credentials->forOrganization($organization);
$branchCount = Branch::owned($this->ownerRef($request))
->where('organization_id', $organization->id)
->count();
$plans = app(PlanService::class);
// Suite platform keys (zero-config) OR optional product credentials.
$suiteEmailReady = $platformEmail->isConfigured();
$suiteSmsReady = $platformSms->isConfigured();
$careFeatures = app(CareFeatures::class);
return view('care.settings.edit', [
'organization' => $organization,
'canManage' => $canManage,
'branchCount' => $branchCount,
'canViewBranches' => $permissions->can($member, 'admin.branches.view'),
'canViewTeam' => $permissions->can($member, 'admin.members.view'),
'canViewDevices' => $permissions->can($member, 'devices.view'),
'canViewDisplays' => $permissions->can($member, 'displays.view'),
'canManageLabCatalog' => $permissions->can($member, 'lab.catalog.manage'),
'hasBranchesFeature' => $plans->hasFeature($organization, 'branches'),
'hasClinicalDevicesFeature' => $plans->hasFeature($organization, 'clinical_devices'),
'canUseQueueIntegration' => $plans->canUseQueueIntegration($organization),
'queueIntegrationEnabled' => (bool) data_get($organization->settings, 'queue_integration_enabled'),
'canUseSpecialtyModules' => $plans->canUseSpecialtyModules($organization),
'assessmentsEngineEnabled' => $careFeatures->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE),
'workflowEngineEnabled' => $careFeatures->enabled($organization, CareFeatures::WORKFLOW_ENGINE),
'financialGatesEnabled' => $careFeatures->enabled($organization, CareFeatures::FINANCIAL_GATES),
'hasBillingFeature' => $plans->hasFeature($organization, 'billing'),
'messagingSmsReady' => $suiteSmsReady || $credential->hasValidSms(),
'messagingEmailReady' => $suiteEmailReady || $credential->hasValidBird(),
'suiteMessagingReady' => $suiteEmailReady || $suiteSmsReady,
'messagingSettingsUrl' => function_exists('ladill_account_url')
? ladill_account_url('/account/settings/messaging')
: (string) config('smtp.messaging_settings_url', 'https://account.ladill.com/account/settings/messaging'),
'facilityTypes' => app(WorkflowTemplateRegistry::class)->categoryOptions(),
]);
}
public function update(Request $request): RedirectResponse
{
$this->authorizeAbility($request, 'settings.manage');
$organization = $this->organization($request);
$owner = $this->ownerRef($request);
$plans = app(PlanService::class);
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'timezone' => ['required', 'timezone'],
'facility_type' => [
'required',
'string',
'in:'.implode(',', array_keys(app(WorkflowTemplateRegistry::class)->categories())),
],
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
'remove_logo' => ['nullable', 'boolean'],
'queue_integration_enabled' => ['nullable', 'boolean'],
'assessments_engine' => ['nullable', 'boolean'],
'workflow_engine' => ['nullable', 'boolean'],
'financial_gates' => ['nullable', 'boolean'],
'notify_appointment_booked_sms' => ['nullable', 'boolean'],
'notify_appointment_booked_email' => ['nullable', 'boolean'],
'notify_video_scheduled_sms' => ['nullable', 'boolean'],
'notify_video_scheduled_email' => ['nullable', 'boolean'],
]);
$settings = $organization->settings ?? [];
$settings['onboarded'] = true;
$settings['facility_type'] = $validated['facility_type'];
$settings['facility_category'] = $validated['facility_type'];
$settings['modules'] = app(WorkflowTemplateRegistry::class)
->modulesForCategory($validated['facility_type']);
$rollout = is_array($settings['rollout'] ?? null) ? $settings['rollout'] : [];
// Explicit save so opt-out is stored (engine defaults ON when unset).
$rollout[CareFeatures::ASSESSMENTS_ENGINE] = $request->boolean('assessments_engine');
$rollout[CareFeatures::WORKFLOW_ENGINE] = $request->boolean('workflow_engine');
$rollout[CareFeatures::FINANCIAL_GATES] = $request->boolean('financial_gates')
&& $rollout[CareFeatures::WORKFLOW_ENGINE]
&& $plans->hasFeature($organization, 'billing');
$settings['rollout'] = $rollout;
if ($rollout[CareFeatures::ASSESSMENTS_ENGINE]) {
app(CareFeatures::class)->ensureAssessmentCatalog();
}
// Queue integration is Pro/Enterprise only — free plans cannot enable it.
$wantsQueue = $request->boolean('queue_integration_enabled');
if ($wantsQueue && ! $plans->canUseQueueIntegration($organization)) {
return redirect()
->route('care.pro.index')
->with('upsell', 'In-app service queues are part of Care Pro. Upgrade to enable ticket numbers and Call next.');
}
if (! $plans->canUseQueueIntegration($organization)) {
$wantsQueue = false;
}
$settings['queue_integration_enabled'] = $wantsQueue;
$queueWasEnabled = (bool) data_get($organization->settings, 'queue_integration_enabled');
$queueJustEnabled = $wantsQueue && ! $queueWasEnabled;
$settings[AppointmentPatientNotifier::SETTING_BOOKED_SMS] = $request->boolean('notify_appointment_booked_sms');
$settings[AppointmentPatientNotifier::SETTING_BOOKED_EMAIL] = $request->boolean('notify_appointment_booked_email');
$settings[AppointmentPatientNotifier::SETTING_VIDEO_SMS] = $request->boolean('notify_video_scheduled_sms');
$settings[AppointmentPatientNotifier::SETTING_VIDEO_EMAIL] = $request->boolean('notify_video_scheduled_email');
$logoPath = $organization->logo_path;
if ($request->boolean('remove_logo')) {
OrganizationBranding::deleteStoredLogo($organization);
$logoPath = null;
}
if ($request->hasFile('logo')) {
$logoPath = OrganizationBranding::storeLogo($organization, $request->file('logo'));
}
$organization->update([
'name' => $validated['name'],
'timezone' => $validated['timezone'],
'logo_path' => $logoPath,
'settings' => $settings,
]);
if ($rollout[CareFeatures::WORKFLOW_ENGINE] && ! $organization->facilityWorkflows()->where('is_active', true)->exists()) {
$branch = Branch::owned($owner)
->where('organization_id', $organization->id)
->orderBy('id')
->first();
if ($branch) {
app(WorkflowTemplateInstaller::class)->install(
$organization->fresh(),
$branch,
(string) ($settings['workflow_template'] ?? config('care_workflows.default_template', 'legacy_clinic')),
['category' => $settings['facility_category'] ?? $settings['facility_type'] ?? 'clinic'],
);
}
}
// Queue department sync + waiting-ticket backfill — run after the redirect.
if ($wantsQueue) {
$organizationId = (int) $organization->id;
$backfillTickets = $queueJustEnabled;
dispatch(function () use ($organizationId, $owner, $backfillTickets) {
$organization = Organization::query()->find($organizationId);
if (! $organization) {
return;
}
try {
app(CareQueueProvisioner::class)->provisionOrganization($organization);
} catch (\Throwable) {
return;
}
if (! $backfillTickets) {
return;
}
try {
$bridge = app(CareQueueBridge::class);
$branches = Branch::owned($owner)
->where('organization_id', $organization->id)
->where('is_active', true)
->pluck('id');
foreach ($branches as $branchId) {
try {
$bridge->syncMissingAppointmentTickets($organization, (int) $branchId, 50);
} catch (\Throwable) {
// Continue other branches.
}
foreach (array_keys(CareQueueContexts::departments()) as $context) {
if (in_array($context, [
CareQueueContexts::RECEPTION,
CareQueueContexts::CONSULTATION,
CareQueueContexts::TRIAGE,
], true)) {
continue;
}
try {
$bridge->syncMissingTickets($organization, $context, (int) $branchId, 25);
} catch (\Throwable) {
// Continue other contexts/branches.
}
}
}
} catch (\Throwable) {
// Best-effort; role pages will lazy-ensure on next use.
}
})->afterResponse();
}
AuditLogger::record($owner, 'organization.updated', $organization->id, $owner, Organization::class, $organization->id);
return back()->with('success', 'Settings saved.');
}
}