'datetime', 'password' => 'hashed', 'role' => UserRole::class, 'is_active' => 'boolean', 'last_login_at' => 'datetime', 'dsgvo_accepted_at' => 'datetime', 'dsgvo_notice_accepted_at' => 'datetime', ]; } public function sendPasswordResetNotification($token): void { $this->notify(new ResetPasswordNotification($token)); } public function isAdmin(): bool { return $this->role === UserRole::Admin; } public function isCoach(): bool { return $this->role === UserRole::Coach; } public function isParentRep(): bool { return $this->role === UserRole::ParentRep; } public function isStaff(): bool { return in_array($this->role, [UserRole::Admin, UserRole::Coach]); } public function canAccessAdminPanel(): bool { return $this->isStaff() || $this->isParentRep(); } public function canViewActivityLog(): bool { return $this->id === 1 && $this->isAdmin(); } public function children(): BelongsToMany { return $this->belongsToMany(Player::class, 'parent_player', 'parent_id', 'player_id') ->withPivot('relationship_label', 'created_at'); } /** * Team-IDs, auf die der User Zugriff hat (über seine Kinder). * Direkte DB-Query ohne Model-Hydration. */ public function accessibleTeamIds(): Collection { return $this->children()->distinct()->pluck('players.team_id'); } public function comments(): HasMany { return $this->hasMany(Comment::class); } public function caterings(): HasMany { return $this->hasMany(EventCatering::class); } public function createdInvitations(): HasMany { return $this->hasMany(Invitation::class, 'created_by'); } public function coachTeams(): BelongsToMany { return $this->belongsToMany(Team::class, 'team_user') ->withPivot('created_at'); } public function scopeActive($query) { return $query->where('is_active', true); } public function getAvatarUrl(): ?string { if ($this->profile_picture) { return asset('storage/' . $this->profile_picture); } return null; } public function getInitials(): string { $parts = explode(' ', trim($this->name)); if (count($parts) >= 2) { return mb_strtoupper(mb_substr($parts[0], 0, 1) . mb_substr(end($parts), 0, 1)); } return mb_strtoupper(mb_substr($this->name, 0, 2)); } public function isRestorable(): bool { return $this->trashed() && $this->deleted_at->diffInDays(now()) < 7; } public function isDsgvoRestricted(): bool { if ($this->role !== UserRole::User) { return false; } return !$this->isDsgvoConfirmed(); } public function needsDsgvoBanner(): bool { if ($this->role !== UserRole::User) { return false; } return !$this->isDsgvoConfirmed(); } public function dsgvoBannerState(): ?string { if ($this->role !== UserRole::User) { return null; } if ($this->dsgvo_consent_file === null) { return 'upload_required'; } if ($this->dsgvo_accepted_at === null) { return 'pending_confirmation'; } return null; } public function hasDsgvoConsent(): bool { return $this->dsgvo_consent_file !== null; } public function isDsgvoConfirmed(): bool { return $this->dsgvo_accepted_at !== null; } public function dsgvoAcceptedBy(): BelongsTo { return $this->belongsTo(User::class, 'dsgvo_accepted_by')->withTrashed(); } public function getOrphanedChildren(): Collection { return $this->children() ->withCount('parents') ->get() ->filter(fn (Player $child) => $child->parents_count <= 1); } }