resource; $contexts = $this->resolveContexts($user); return [ 'id' => $this->id, 'first_name' => $this->first_name, 'last_name' => $this->last_name, 'full_name' => $this->full_name, 'date_of_birth' => $this->date_of_birth?->toDateString(), 'email' => $this->email, 'phone' => $this->phone, 'timezone' => $this->timezone, 'locale' => $this->locale, 'avatar' => $this->avatar, 'email_verified_at' => $this->email_verified_at?->toIso8601String(), 'organisations' => $this->whenLoaded('organisations', fn () => $this->organisations->map(fn ($org) => [ 'id' => $org->id, 'name' => $org->name, 'slug' => $org->slug, 'role' => $org->pivot->role, // Forward-compatible array form. The pivot stores a single // role today; B2a emits it as a 1-element array so the // frontend can treat the field as multi-role from day one. // Multi-role pivot resolution is tracked in BACKLOG.md as // TECH-PIVOT-ROLES-MULTI (ARCH discussion, not just a // schema-column expansion). 'roles' => [$org->pivot->role], ]) ), 'app_roles' => $this->getRoleNames()->values()->all(), 'permissions' => $this->getAllPermissions()->pluck('name')->values()->all(), 'portal_events' => $this->whenLoaded('persons', fn () => $this->persons->map(fn (Person $person) => [ 'event_id' => $person->event_id, 'event_name' => $person->event->name, 'event_slug' => $person->event->slug, 'organisation_name' => $person->event->organisation->name, 'person_id' => $person->id, 'person_status' => $person->status, 'start_date' => $person->event->start_date?->toDateString(), 'end_date' => $person->event->end_date?->toDateString(), ]) ), 'mfa' => [ 'enabled' => $this->mfa_enabled, 'method' => $this->mfa_method, 'confirmed_at' => $this->mfa_confirmed_at?->toIso8601String(), 'setup_required' => app(MfaService::class)->isMfaRequired($this->resource) && ! $this->mfa_enabled, ], 'platform' => [ 'is_super_admin' => $user->hasRole('super_admin'), ], 'contexts' => $contexts, ]; } /** * Compute available + default UI contexts for this user. * * - portal: user has at least one Person record (volunteer-side). * - organizer: super_admin OR membership in any Organisation pivot. * * Default precedence: super_admin → organizer; otherwise the first * available context wins (organizer before portal, mirroring the * "familiar context wins on first login" rule from * ARCH-CONSOLIDATION-2026-04 §4.3). When neither context is * available, default falls back to 'portal' so the post-login * landing logic has a safe target to resolve against. * * @return array{available: list, default: string} */ private function resolveContexts(User $user): array { $hasPortal = $user->persons->isNotEmpty(); $hasOrganizer = $user->hasRole('super_admin') || $user->organisations->isNotEmpty(); $available = []; if ($hasPortal) { $available[] = 'portal'; } if ($hasOrganizer) { $available[] = 'organizer'; } $default = match (true) { $user->hasRole('super_admin') => 'organizer', $hasOrganizer => 'organizer', $hasPortal => 'portal', default => 'portal', }; return [ 'available' => $available, 'default' => $default, ]; } }