From 3a7bd14b2b1588bfda1ce0f39dd076a6a7bfb4b0 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Tue, 14 Jul 2026 21:59:39 +0000 Subject: [PATCH] Add practitioners admin and staff-scoped Care UX. Hospital admins can manage assignable doctors and invite team members from Ladill mailboxes; invited staff only see Care tools they need. Co-authored-by: Cursor --- .../Controllers/Auth/SsoLoginController.php | 5 +- .../Controllers/Care/MemberController.php | 37 +++- .../Care/PractitionerController.php | 195 ++++++++++++++++++ app/Providers/AppServiceProvider.php | 15 ++ app/Services/Care/CarePermissions.php | 1 + app/Services/Identity/IdentityTeamClient.php | 34 ++- app/Support/StaffUx.php | 119 +++++++++++ app/Support/UserProfileMenu.php | 16 +- config/care.php | 4 + .../views/care/admin/members/create.blade.php | 45 +++- .../care/admin/practitioners/create.blade.php | 50 +++++ .../care/admin/practitioners/edit.blade.php | 59 ++++++ .../care/admin/practitioners/index.blade.php | 60 ++++++ resources/views/partials/launcher.blade.php | 50 ++++- resources/views/partials/sidebar.blade.php | 4 + routes/web.php | 8 + storage/framework/cache/data/.gitignore | 0 storage/framework/sessions/.gitignore | 0 storage/framework/views/.gitignore | 0 19 files changed, 684 insertions(+), 18 deletions(-) create mode 100644 app/Http/Controllers/Care/PractitionerController.php create mode 100644 app/Support/StaffUx.php create mode 100644 resources/views/care/admin/practitioners/create.blade.php create mode 100644 resources/views/care/admin/practitioners/edit.blade.php create mode 100644 resources/views/care/admin/practitioners/index.blade.php create mode 100644 storage/framework/cache/data/.gitignore create mode 100644 storage/framework/sessions/.gitignore create mode 100644 storage/framework/views/.gitignore diff --git a/app/Http/Controllers/Auth/SsoLoginController.php b/app/Http/Controllers/Auth/SsoLoginController.php index 1aad786..c914b21 100644 --- a/app/Http/Controllers/Auth/SsoLoginController.php +++ b/app/Http/Controllers/Auth/SsoLoginController.php @@ -284,7 +284,10 @@ class SsoLoginController extends Controller private function resolveLanding(IdentityTeamClient $identity, User $user, string $intended): string { try { - return $identity->postAuthRedirect($user->ownerRef(), $intended); + $access = $identity->appAccess($user->ownerRef(), $intended); + \App\Support\StaffUx::remember($access); + + return $access['url'] !== '' ? $access['url'] : $intended; } catch (\Throwable) { return $intended; } diff --git a/app/Http/Controllers/Care/MemberController.php b/app/Http/Controllers/Care/MemberController.php index d59fecb..a447d19 100644 --- a/app/Http/Controllers/Care/MemberController.php +++ b/app/Http/Controllers/Care/MemberController.php @@ -6,10 +6,12 @@ use App\Http\Controllers\Controller; use App\Http\Controllers\Care\Concerns\ScopesToAccount; use App\Models\Branch; use App\Models\Member; +use App\Models\Practitioner; use App\Services\Care\AuditLogger; use App\Services\Identity\IdentityTeamClient; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Illuminate\View\View; class MemberController extends Controller @@ -43,21 +45,30 @@ class MemberController extends Controller ]); } - public function create(Request $request): View + public function create(Request $request, IdentityTeamClient $identity): View { $this->authorizeAbility($request, 'admin.members.manage'); $organization = $this->organization($request); + $owner = $this->ownerRef($request); - $branches = Branch::owned($this->ownerRef($request)) + $branches = Branch::owned($owner) ->where('organization_id', $organization->id) ->where('is_active', true) ->orderBy('name') ->get(); + $mailboxOptions = []; + try { + $mailboxOptions = $identity->mailboxOptions($owner); + } catch (\Throwable) { + // Identity optional for form rendering. + } + return view('care.admin.members.create', [ 'organization' => $organization, 'branches' => $branches, 'roles' => config('care.roles'), + 'mailboxOptions' => $mailboxOptions, ]); } @@ -71,6 +82,9 @@ class MemberController extends Controller 'email' => ['required', 'email', 'max:255'], 'role' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.roles')))], 'branch_id' => ['nullable', 'integer', 'exists:care_branches,id'], + 'create_practitioner' => ['sometimes', 'boolean'], + 'practitioner_name' => ['nullable', 'string', 'max:255'], + 'specialty' => ['nullable', 'string', 'max:255'], ]); $email = strtolower(trim($validated['email'])); @@ -106,6 +120,25 @@ class MemberController extends Controller AuditLogger::record($owner, 'member.invited', $organization->id, $owner, Member::class, $member->id); + $createPractitioner = $request->boolean('create_practitioner', $validated['role'] === 'doctor'); + if ($createPractitioner) { + $name = trim((string) ($validated['practitioner_name'] ?? '')) ?: Str::headline(Str::before($email, '@')); + Practitioner::query()->firstOrCreate( + [ + 'organization_id' => $organization->id, + 'member_id' => $member->id, + ], + [ + 'owner_ref' => $owner, + 'branch_id' => $validated['branch_id'] ?? null, + 'user_ref' => $email, + 'name' => $name, + 'specialty' => $validated['specialty'] ?? null, + 'is_active' => true, + ], + ); + } + return redirect()->route('care.members.index')->with('success', 'Invitation sent to '.$email.'.'); } diff --git a/app/Http/Controllers/Care/PractitionerController.php b/app/Http/Controllers/Care/PractitionerController.php new file mode 100644 index 0000000..cbd62e4 --- /dev/null +++ b/app/Http/Controllers/Care/PractitionerController.php @@ -0,0 +1,195 @@ +authorizeAbility($request, 'admin.practitioners.view'); + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + $practitioners = Practitioner::owned($owner) + ->where('organization_id', $organization->id) + ->with(['branch', 'department', 'member']) + ->orderBy('name') + ->get(); + + return view('care.admin.practitioners.index', [ + 'practitioners' => $practitioners, + 'organization' => $organization, + 'heroStats' => [ + 'total' => $practitioners->count(), + 'active' => $practitioners->where('is_active', true)->count(), + 'linked' => $practitioners->whereNotNull('member_id')->count(), + ], + ]); + } + + public function create(Request $request): View + { + $this->authorizeAbility($request, 'admin.practitioners.manage'); + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + return view('care.admin.practitioners.create', [ + 'organization' => $organization, + 'branches' => $this->activeBranches($owner, $organization->id), + 'departments' => $this->activeDepartments($owner, $organization->id), + 'members' => $this->linkableMembers($owner, $organization->id), + ]); + } + + public function store(Request $request): RedirectResponse + { + $this->authorizeAbility($request, 'admin.practitioners.manage'); + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + $validated = $this->validated($request, $owner, $organization->id); + + $member = ! empty($validated['member_id']) + ? Member::owned($owner)->where('organization_id', $organization->id)->findOrFail($validated['member_id']) + : null; + + $practitioner = Practitioner::create([ + 'owner_ref' => $owner, + 'organization_id' => $organization->id, + 'branch_id' => $validated['branch_id'] ?? null, + 'department_id' => $validated['department_id'] ?? null, + 'member_id' => $member?->id, + 'user_ref' => $member?->user_ref, + 'name' => $validated['name'], + 'specialty' => $validated['specialty'] ?? null, + 'is_active' => true, + ]); + + AuditLogger::record($owner, 'practitioner.created', $organization->id, $owner, Practitioner::class, $practitioner->id); + + return redirect()->route('care.practitioners.index')->with('success', 'Practitioner added.'); + } + + public function edit(Request $request, Practitioner $practitioner): View + { + $this->authorizeAbility($request, 'admin.practitioners.manage'); + $this->authorizeOwner($request, $practitioner); + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + return view('care.admin.practitioners.edit', [ + 'practitioner' => $practitioner, + 'branches' => $this->activeBranches($owner, $organization->id), + 'departments' => $this->activeDepartments($owner, $organization->id), + 'members' => $this->linkableMembers($owner, $organization->id), + ]); + } + + public function update(Request $request, Practitioner $practitioner): RedirectResponse + { + $this->authorizeAbility($request, 'admin.practitioners.manage'); + $this->authorizeOwner($request, $practitioner); + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + $validated = $this->validated($request, $owner, $organization->id); + + $member = ! empty($validated['member_id']) + ? Member::owned($owner)->where('organization_id', $organization->id)->findOrFail($validated['member_id']) + : null; + + $practitioner->update([ + 'branch_id' => $validated['branch_id'] ?? null, + 'department_id' => $validated['department_id'] ?? null, + 'member_id' => $member?->id, + 'user_ref' => $member?->user_ref, + 'name' => $validated['name'], + 'specialty' => $validated['specialty'] ?? null, + 'is_active' => $request->boolean('is_active', true), + ]); + + AuditLogger::record($owner, 'practitioner.updated', $organization->id, $owner, Practitioner::class, $practitioner->id); + + return redirect()->route('care.practitioners.index')->with('success', 'Practitioner updated.'); + } + + public function destroy(Request $request, Practitioner $practitioner): RedirectResponse + { + $this->authorizeAbility($request, 'admin.practitioners.manage'); + $this->authorizeOwner($request, $practitioner); + + $id = $practitioner->id; + $organizationId = $practitioner->organization_id; + $practitioner->delete(); + + AuditLogger::record($this->ownerRef($request), 'practitioner.deleted', $organizationId, $this->ownerRef($request), Practitioner::class, $id); + + return redirect()->route('care.practitioners.index')->with('success', 'Practitioner removed.'); + } + + /** + * @return array{name: string, specialty?: string|null, branch_id?: int|null, department_id?: int|null, member_id?: int|null} + */ + protected function validated(Request $request, string $owner, int $organizationId): array + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'specialty' => ['nullable', 'string', 'max:255'], + 'branch_id' => ['nullable', 'integer', 'exists:care_branches,id'], + 'department_id' => ['nullable', 'integer', 'exists:care_departments,id'], + 'member_id' => ['nullable', 'integer', 'exists:care_members,id'], + ]); + + if (! empty($validated['branch_id'])) { + $branch = Branch::owned($owner)->findOrFail($validated['branch_id']); + abort_unless($branch->organization_id === $organizationId, 404); + } + + if (! empty($validated['department_id'])) { + $department = Department::owned($owner)->with('branch')->findOrFail($validated['department_id']); + abort_unless($department->branch?->organization_id === $organizationId, 404); + } + + return $validated; + } + + protected function activeBranches(string $owner, int $organizationId) + { + return Branch::owned($owner) + ->where('organization_id', $organizationId) + ->where('is_active', true) + ->orderBy('name') + ->get(); + } + + protected function activeDepartments(string $owner, int $organizationId) + { + return Department::owned($owner) + ->whereHas('branch', fn ($q) => $q->where('organization_id', $organizationId)) + ->where('is_active', true) + ->orderBy('name') + ->get(); + } + + protected function linkableMembers(string $owner, int $organizationId) + { + return Member::owned($owner) + ->where('organization_id', $organizationId) + ->whereIn('role', ['doctor', 'nurse', 'lab_technician', 'pharmacist', 'hospital_admin', 'super_admin']) + ->orderBy('user_ref') + ->get(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f27a0d8..39e424d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -52,5 +52,20 @@ class AppServiceProvider extends ServiceProvider View::composer(['partials.topbar'], function ($view) { $view->with(\App\Support\MobileTopbar::resolve()); }); + + View::composer(['partials.launcher', 'partials.topbar-desktop-widgets', 'components.app-layout'], function () { + $user = auth()->user(); + if (! $user || session()->has(\App\Support\StaffUx::SESSION_KEY)) { + return; + } + + try { + $access = app(\App\Services\Identity\IdentityTeamClient::class) + ->appAccess($user->ownerRef()); + \App\Support\StaffUx::remember($access); + } catch (\Throwable) { + // Leave fail-open defaults until Identity is available. + } + }); } } diff --git a/app/Services/Care/CarePermissions.php b/app/Services/Care/CarePermissions.php index 28cb21f..c8f41af 100644 --- a/app/Services/Care/CarePermissions.php +++ b/app/Services/Care/CarePermissions.php @@ -45,6 +45,7 @@ class CarePermissions protected array $adminAbilities = [ 'admin.branches.view', 'admin.branches.manage', 'admin.departments.view', 'admin.departments.manage', + 'admin.practitioners.view', 'admin.practitioners.manage', 'admin.members.view', 'admin.members.manage', 'settings.view', 'settings.manage', 'audit.view', 'audit.export', diff --git a/app/Services/Identity/IdentityTeamClient.php b/app/Services/Identity/IdentityTeamClient.php index 74d3315..fbb3e2a 100644 --- a/app/Services/Identity/IdentityTeamClient.php +++ b/app/Services/Identity/IdentityTeamClient.php @@ -32,14 +32,40 @@ class IdentityTeamClient return (array) $response->json('data', []); } + /** + * @return array{url: string, apps: list, full_access: bool, show_hub: bool, show_billing: bool} + */ + public function appAccess(string $userPublicId, string $intendedUrl = ''): array + { + $response = $this->request('get', '/identity/team/post-auth-redirect', array_filter([ + 'user' => $userPublicId, + 'redirect' => $intendedUrl !== '' ? $intendedUrl : null, + ])); + + $data = (array) $response->json('data', []); + + return [ + 'url' => (string) ($data['url'] ?? $intendedUrl), + 'apps' => array_values(array_map('strval', (array) ($data['apps'] ?? []))), + 'full_access' => (bool) ($data['full_access'] ?? false), + 'show_hub' => (bool) ($data['show_hub'] ?? ($data['full_access'] ?? false)), + 'show_billing' => (bool) ($data['show_billing'] ?? ($data['full_access'] ?? false)), + ]; + } + public function postAuthRedirect(string $userPublicId, string $intendedUrl): string { - $response = $this->request('get', '/identity/team/post-auth-redirect', [ - 'user' => $userPublicId, - 'redirect' => $intendedUrl, + return $this->appAccess($userPublicId, $intendedUrl)['url']; + } + + /** @return list */ + public function mailboxOptions(string $ownerPublicId): array + { + $response = $this->request('get', '/identity/team/mailbox-options', [ + 'owner' => $ownerPublicId, ]); - return (string) $response->json('data.url', $intendedUrl); + return array_values(array_map('strval', (array) $response->json('data', []))); } /** @return list> */ diff --git a/app/Support/StaffUx.php b/app/Support/StaffUx.php new file mode 100644 index 0000000..65e38af --- /dev/null +++ b/app/Support/StaffUx.php @@ -0,0 +1,119 @@ +user(); + if (! $user) { + return false; + } + + if ($resolved = self::fromTeamAccess($user)) { + return $resolved['show_hub']; + } + + $cached = session(self::SESSION_KEY); + if (is_array($cached) && array_key_exists('show_hub', $cached)) { + return (bool) $cached['show_hub']; + } + + // Unknown in a silo before SSO cache — assume owner (fail open for hub). + return true; + } + + public static function showBilling(?Authenticatable $user = null): bool + { + $user ??= auth()->user(); + if (! $user) { + return false; + } + + if ($resolved = self::fromTeamAccess($user)) { + return $resolved['show_billing']; + } + + $cached = session(self::SESSION_KEY); + if (is_array($cached) && array_key_exists('show_billing', $cached)) { + return (bool) $cached['show_billing']; + } + + return true; + } + + /** @return list|null Null means unrestricted / unknown. */ + public static function allowedAppSlugs(?Authenticatable $user = null): ?array + { + $user ??= auth()->user(); + if (! $user) { + return []; + } + + if ($resolved = self::fromTeamAccess($user)) { + return $resolved['full_access'] ? null : $resolved['apps']; + } + + $cached = session(self::SESSION_KEY); + if (is_array($cached)) { + if (! empty($cached['full_access'])) { + return null; + } + if (array_key_exists('apps', $cached) && is_array($cached['apps'])) { + return array_values(array_map('strval', $cached['apps'])); + } + } + + return null; + } + + /** + * @param array{full_access?: bool, apps?: list, show_hub?: bool, show_billing?: bool} $payload + */ + public static function remember(array $payload): void + { + session([self::SESSION_KEY => [ + 'full_access' => (bool) ($payload['full_access'] ?? false), + 'apps' => array_values(array_map('strval', (array) ($payload['apps'] ?? []))), + 'show_hub' => (bool) ($payload['show_hub'] ?? false), + 'show_billing' => (bool) ($payload['show_billing'] ?? false), + ]]); + } + + /** @return array{full_access: bool, apps: list, show_hub: bool, show_billing: bool}|null */ + private static function fromTeamAccess(Authenticatable $user): ?array + { + $service = 'App\\Services\\Team\\TeamAccessService'; + $userClass = 'App\\Models\\User'; + + if (! class_exists($service) || ! is_a($user, $userClass)) { + return null; + } + + try { + $access = app($service); + + return [ + 'full_access' => $access->hasFullProductAccess($user), + 'apps' => $access->accessibleAppSlugs($user), + 'show_hub' => $access->showProductHub($user), + 'show_billing' => $access->showBilling($user), + ]; + } catch (\Throwable) { + return null; + } + } +} diff --git a/app/Support/UserProfileMenu.php b/app/Support/UserProfileMenu.php index 9461caf..95d6ae3 100644 --- a/app/Support/UserProfileMenu.php +++ b/app/Support/UserProfileMenu.php @@ -21,15 +21,25 @@ class UserProfileMenu return []; } + $showHub = StaffUx::showProductHub($user); + $showBilling = StaffUx::showBilling($user); $items = []; foreach ([ - ['label' => 'Home', 'path' => '', 'host' => 'home'], + ['label' => 'Home', 'path' => '', 'host' => 'home', 'requires_hub' => true], ['label' => 'Profile', 'path' => 'profile'], ['label' => 'Account Settings', 'path' => 'account-settings'], ['label' => 'Dashboard', 'path' => 'dashboard'], - ['label' => 'Billing', 'path' => 'billing'], + ['label' => 'Billing', 'path' => 'billing', 'requires_billing' => true], ] as $link) { + if (! empty($link['requires_hub']) && ! $showHub) { + continue; + } + + if (! empty($link['requires_billing']) && ! $showBilling) { + continue; + } + $href = self::platformUrl($link['host'] ?? 'account', $link['path']); if (self::isCurrentMenuLink($link, $href)) { @@ -43,7 +53,7 @@ class UserProfileMenu ]; } - if (Route::has((string) config('billing.wallet_balance_route', 'wallet.balance'))) { + if ($showBilling && Route::has((string) config('billing.wallet_balance_route', 'wallet.balance'))) { $items[] = ['type' => 'wallet']; } diff --git a/config/care.php b/config/care.php index 1883546..7fa8d59 100644 --- a/config/care.php +++ b/config/care.php @@ -34,9 +34,13 @@ return [ 'department.created' => 'Department created', 'department.updated' => 'Department updated', 'department.deleted' => 'Department deleted', + 'practitioner.created' => 'Practitioner added', + 'practitioner.updated' => 'Practitioner updated', + 'practitioner.deleted' => 'Practitioner removed', 'member.created' => 'Member added', 'member.updated' => 'Member updated', 'member.deleted' => 'Member removed', + 'member.invited' => 'Member invited', 'patient.created' => 'Patient registered', 'patient.updated' => 'Patient record updated', 'patient.deleted' => 'Patient record archived', diff --git a/resources/views/care/admin/members/create.blade.php b/resources/views/care/admin/members/create.blade.php index 1903331..f3116aa 100644 --- a/resources/views/care/admin/members/create.blade.php +++ b/resources/views/care/admin/members/create.blade.php @@ -1,13 +1,28 @@

Invite team member

+

They get access only to Ladill Care (plus Mail if they already have a Ladill mailbox).

-
+ @csrf + @if (! empty($mailboxOptions)) +
+ + +

Or type any email below.

+
+ @endif +
-

They will receive an email to accept and join your Care organization.

@@ -18,9 +33,9 @@
- @foreach ($roles as $value => $label) - + @endforeach
@@ -35,6 +50,28 @@
+
+ +
+
+ + +
+
+ + +
+
+
+
diff --git a/resources/views/care/admin/practitioners/create.blade.php b/resources/views/care/admin/practitioners/create.blade.php new file mode 100644 index 0000000..a132cae --- /dev/null +++ b/resources/views/care/admin/practitioners/create.blade.php @@ -0,0 +1,50 @@ + +
+

Add practitioner

+

Creates a clinical listing for bookings. Invite them under Team if they also need to sign in.

+ +
+ @csrf +
+ + + @error('name')

{{ $message }}

@enderror +
+
+ + +
+
+ + +
+
+ + +
+
+ + +

Link after inviting them under Administration → Team.

+
+ +
+
+
diff --git a/resources/views/care/admin/practitioners/edit.blade.php b/resources/views/care/admin/practitioners/edit.blade.php new file mode 100644 index 0000000..2b12457 --- /dev/null +++ b/resources/views/care/admin/practitioners/edit.blade.php @@ -0,0 +1,59 @@ + +
+

Edit practitioner

+ +
+ @csrf + @method('PUT') +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ @csrf + @method('DELETE') + +
+
+
diff --git a/resources/views/care/admin/practitioners/index.blade.php b/resources/views/care/admin/practitioners/index.blade.php new file mode 100644 index 0000000..6a52ab7 --- /dev/null +++ b/resources/views/care/admin/practitioners/index.blade.php @@ -0,0 +1,60 @@ + +
+ + + Add practitioner + + + + @if (session('success')) +

{{ session('success') }}

+ @endif + +
+ + + + + + + + + + + + + @forelse ($practitioners as $practitioner) + + + + + + + + + @empty + + + + @endforelse + +
NameSpecialtyBranchTeam loginStatus
{{ $practitioner->name }}{{ $practitioner->specialty ?: '—' }}{{ $practitioner->branch?->name ?? 'All branches' }}{{ $practitioner->member?->user_ref ?? '—' }} + + {{ $practitioner->is_active ? 'Active' : 'Inactive' }} + + + Edit +
+ No practitioners yet. Add doctors here so they appear on appointments and the queue. +
+
+
+
diff --git a/resources/views/partials/launcher.blade.php b/resources/views/partials/launcher.blade.php index de21e62..8c0ffa6 100644 --- a/resources/views/partials/launcher.blade.php +++ b/resources/views/partials/launcher.blade.php @@ -2,17 +2,59 @@ // Shared Ladill app launcher — IDENTICAL across every app/service. // Driven by config/ladill_launcher.php (fully-extracted apps only) + icons // from public/images/launcher-icons/. Omits the current app (matched by - // APP_URL host). To replicate elsewhere, copy this file + the config + - // the launcher-icons folder verbatim. + // APP_URL host). Scoped team members only see apps they can access; the + // launcher is hidden entirely when they only have one app (or none). $sidebar = (bool) ($sidebar ?? false); $selfHost = parse_url((string) config('app.url'), PHP_URL_HOST); + $allowedSlugs = \App\Support\StaffUx::allowedAppSlugs(auth()->user()); $launcherApps = array_values(array_filter( config('ladill_launcher.apps', []), - fn (array $app) => parse_url($app['url'], PHP_URL_HOST) !== $selfHost + function (array $app) use ($selfHost, $allowedSlugs) { + if (parse_url($app['url'], PHP_URL_HOST) === $selfHost) { + return false; + } + + if ($allowedSlugs === null) { + return true; + } + + $host = (string) parse_url($app['url'], PHP_URL_HOST); + $root = (string) config('app.platform_domain', 'ladill.com'); + $suffix = '.'.$root; + if (! str_ends_with($host, $suffix)) { + return false; + } + $sub = substr($host, 0, -strlen($suffix)); + + foreach ($allowedSlugs as $slug) { + if ($slug === $sub || str_starts_with($sub, $slug)) { + return true; + } + } + + // Launcher URLs may use marketing hostnames — match by known slug keys. + $slug = match (true) { + str_starts_with($host, 'care.') => 'care', + str_starts_with($host, 'meet.') => 'meet', + str_starts_with($host, 'queue.') => 'queue', + str_starts_with($host, 'frontdesk.') => 'frontdesk', + str_starts_with($host, 'mail.') => 'mail', + str_starts_with($host, 'email.') => 'email', + default => explode('.', $host)[0] ?? null, + }; + + return $slug && in_array($slug, $allowedSlugs, true); + } )); + + // Single-app staff: no launcher (they already are in their only app). + if ($allowedSlugs !== null && count($allowedSlugs) <= 1) { + $launcherApps = []; + } + $launcherOverflows = count($launcherApps) > 15; @endphp -@if (! empty($launcherApps)) +@if (! empty($launcherApps) && \App\Support\StaffUx::showProductHub(auth()->user()))
']; } + if ($permissions->can($member, 'admin.practitioners.view')) { + $adminNav[] = ['name' => 'Practitioners', 'route' => route('care.practitioners.index'), 'active' => request()->routeIs('care.practitioners.*'), + 'icon' => '']; + } if ($permissions->can($member, 'admin.members.view')) { $adminNav[] = ['name' => 'Team', 'route' => route('care.members.index'), 'active' => request()->routeIs('care.members.*'), 'icon' => '']; diff --git a/routes/web.php b/routes/web.php index 9fc2925..d7effcb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -18,6 +18,7 @@ use App\Http\Controllers\Care\MemberController; use App\Http\Controllers\Care\OnboardingController; use App\Http\Controllers\Care\PatientController; use App\Http\Controllers\Care\PatientMessageController; +use App\Http\Controllers\Care\PractitionerController; use App\Http\Controllers\Care\PrescriptionController; use App\Http\Controllers\Care\QueueController; use App\Http\Controllers\Care\ProController; @@ -164,6 +165,13 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::put('/departments/{department}', [DepartmentController::class, 'update'])->name('care.departments.update'); Route::delete('/departments/{department}', [DepartmentController::class, 'destroy'])->name('care.departments.destroy'); + Route::get('/practitioners', [PractitionerController::class, 'index'])->name('care.practitioners.index'); + Route::get('/practitioners/create', [PractitionerController::class, 'create'])->name('care.practitioners.create'); + Route::post('/practitioners', [PractitionerController::class, 'store'])->name('care.practitioners.store'); + Route::get('/practitioners/{practitioner}/edit', [PractitionerController::class, 'edit'])->name('care.practitioners.edit'); + Route::put('/practitioners/{practitioner}', [PractitionerController::class, 'update'])->name('care.practitioners.update'); + Route::delete('/practitioners/{practitioner}', [PractitionerController::class, 'destroy'])->name('care.practitioners.destroy'); + Route::get('/members', [MemberController::class, 'index'])->name('care.members.index'); Route::get('/members/create', [MemberController::class, 'create'])->name('care.members.create'); Route::post('/members', [MemberController::class, 'store'])->name('care.members.store'); diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..e69de29