*/ use AuthenticatesWithLdap, HasFactory, Notifiable; /** * Pending values for the "roles" / "custom_field_values" virtual * attributes (see getAttribute()/setAttribute() below) — neither is a * real column anymore, so a plain write can't land in $attributes; it's * stashed here until saved() flushes it into role_user / user_field_values. */ protected ?array $pendingRoleKeys = null; protected ?array $pendingFieldValues = null; protected static function booted(): void { static::saved(function (User $user) { if ($user->pendingRoleKeys !== null) { $ids = Role::query()->whereIn('key', $user->pendingRoleKeys)->pluck('id'); $user->roleAssignments()->sync($ids); $user->unsetRelation('roleAssignments'); $user->pendingRoleKeys = null; } if ($user->pendingFieldValues !== null) { $user->syncFieldValues($user->pendingFieldValues); $user->unsetRelation('fieldValues'); $user->pendingFieldValues = null; } }); } /** * Get an attribute's value. Overridden (rather than using an Eloquent * "Attribute" get/set mutator) because "roles" and "custom_field_values" * no longer back a real column at all — there's nothing for the * mutator's return value to be written into. */ public function getAttribute($key) { if ($key === 'roles') { return $this->pendingRoleKeys ?? $this->roleAssignments->pluck('key')->all(); } if ($key === 'custom_field_values') { return $this->pendingFieldValues ?? $this->fieldValues->pluck('value', 'user_field_id')->all(); } return parent::getAttribute($key); } public function setAttribute($key, $value) { if ($key === 'roles') { $this->pendingRoleKeys = $value ?? []; return $this; } if ($key === 'custom_field_values') { $this->pendingFieldValues = $value ?? []; return $this; } return parent::setAttribute($key, $value); } protected function syncFieldValues(array $values): void { $keep = []; foreach ($values as $fieldId => $value) { if ($value === null || $value === '') { continue; } $keep[] = $fieldId; UserFieldValue::query()->updateOrCreate( ['user_id' => $this->id, 'user_field_id' => $fieldId], ['value' => is_bool($value) ? ($value ? '1' : '0') : (string) $value] ); } UserFieldValue::query()->where('user_id', $this->id)->whereNotIn('user_field_id', $keep)->delete(); } protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; } /** * The roles this user actually holds, stored via the role_user pivot — * see the roles virtual attribute above for the plain-array reader/writer * that everything else in the app uses instead of this relation directly. */ public function roleAssignments(): BelongsToMany { return $this->belongsToMany(Role::class); } /** * Filters to users holding the given role — replaces the old * whereJsonContains('roles', $key) now that roles live in role_user. */ public function scopeWithRole(Builder $query, string $roleKey): Builder { return $query->whereHas('roleAssignments', fn (Builder $q) => $q->where('key', $roleKey)); } /** * A user can hold several roles at once (e.g. operator + admin) — each * grants access to its matching area independently. */ public function hasRole(string $role): bool { return in_array($role, $this->roles ?? [], true); } public function isClient(): bool { return $this->hasRole('client'); } public function isOperator(): bool { return $this->hasRole('operator'); } public function isAdmin(): bool { return $this->hasRole('admin'); } /** * Where to land a user with (possibly) several roles right after login — * always the client area, even for admins/operators, since every account * carries the "client" role by default (see AssignDefaultRole) and staff * switch into their other areas afterwards via the role switcher in the * header rather than landing there automatically. */ public function defaultArea(): string { return match (true) { $this->isClient() => '/client', $this->isAdmin() => '/admin', $this->isOperator() => '/operator', default => '/client', }; } public function teams(): BelongsToMany { return $this->belongsToMany(Team::class, 'team_user'); } public function ticketsAsCustomer(): HasMany { return $this->hasMany(Ticket::class, 'customer_id'); } public function watchedTickets(): BelongsToMany { return $this->belongsToMany(Ticket::class, 'ticket_watchers'); } public function ticketsAssigned(): HasMany { return $this->hasMany(Ticket::class, 'assignee_id'); } public function savedQueueViews(): HasMany { return $this->hasMany(SavedQueueView::class); } /** * The admin-defined "user field" values, stored one row per field in * user_field_values — see the custom_field_values virtual attribute * above for the plain array-by-field-id reader/writer everything else * in the app uses instead of this relation directly. */ public function fieldValues(): HasMany { return $this->hasMany(UserFieldValue::class); } /** * The admin-defined "user field" values that actually have something in * them, label + display value, for showing next to a reporter/user * elsewhere in the app (mirrors Ticket::customFieldEntries()). */ public function customFieldEntries(): array { $values = $this->custom_field_values ?? []; return UserField::query()->orderBy('sort_order')->get() ->filter(fn (UserField $field) => array_key_exists($field->id, $values) && $values[$field->id] !== '' && $values[$field->id] !== null) ->map(fn (UserField $field) => [ 'label' => $field->label, 'value' => $field->type === 'checkbox' ? ($values[$field->id] ? 'Tak' : 'Nie') : (string) $values[$field->id], ]) ->values() ->all(); } }