- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
292 lines
8.6 KiB
PHP
Executable File
292 lines
8.6 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Enums\CateringStatus;
|
|
use App\Enums\EventStatus;
|
|
use App\Enums\EventType;
|
|
use App\Enums\ParticipantStatus;
|
|
use App\Enums\UserRole;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class Event extends Model
|
|
{
|
|
use SoftDeletes;
|
|
protected $fillable = [
|
|
'team_id',
|
|
'type',
|
|
'title',
|
|
'start_at',
|
|
'end_at',
|
|
'status',
|
|
'location_name',
|
|
'address_text',
|
|
'location_lat',
|
|
'location_lng',
|
|
'description_html',
|
|
'min_players',
|
|
'min_catering',
|
|
'min_timekeepers',
|
|
'opponent',
|
|
'score_home',
|
|
'score_away',
|
|
];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'type' => EventType::class,
|
|
'status' => EventStatus::class,
|
|
'start_at' => 'datetime',
|
|
'end_at' => 'datetime',
|
|
'location_lat' => 'float',
|
|
'location_lng' => 'float',
|
|
'min_players' => 'integer',
|
|
'min_catering' => 'integer',
|
|
'min_timekeepers' => 'integer',
|
|
'score_home' => 'integer',
|
|
'score_away' => 'integer',
|
|
];
|
|
}
|
|
|
|
public function hasCoordinates(): bool
|
|
{
|
|
return $this->location_lat !== null && $this->location_lng !== null;
|
|
}
|
|
|
|
public function hasScore(): bool
|
|
{
|
|
return $this->type->isGameType() && ($this->score_home !== null || $this->score_away !== null);
|
|
}
|
|
|
|
public function scoreDisplay(): ?string
|
|
{
|
|
if (!$this->hasScore()) {
|
|
return null;
|
|
}
|
|
|
|
return ($this->score_home ?? '?') . ':' . ($this->score_away ?? '?');
|
|
}
|
|
|
|
public function team(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Team::class);
|
|
}
|
|
|
|
public function creator(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'created_by')->withTrashed();
|
|
}
|
|
|
|
public function updater(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'updated_by')->withTrashed();
|
|
}
|
|
|
|
public function deletedByUser(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'deleted_by')->withTrashed();
|
|
}
|
|
|
|
public function isRestorable(): bool
|
|
{
|
|
return $this->trashed() && $this->deleted_at->diffInDays(now()) <= 30;
|
|
}
|
|
|
|
public function participants(): HasMany
|
|
{
|
|
return $this->hasMany(EventParticipant::class);
|
|
}
|
|
|
|
public function caterings(): HasMany
|
|
{
|
|
return $this->hasMany(EventCatering::class);
|
|
}
|
|
|
|
public function timekeepers(): HasMany
|
|
{
|
|
return $this->hasMany(EventTimekeeper::class);
|
|
}
|
|
|
|
public function comments(): HasMany
|
|
{
|
|
return $this->hasMany(Comment::class);
|
|
}
|
|
|
|
public function faqs(): BelongsToMany
|
|
{
|
|
return $this->belongsToMany(Faq::class, 'event_faq');
|
|
}
|
|
|
|
public function files(): BelongsToMany
|
|
{
|
|
return $this->belongsToMany(File::class, 'event_file')->withPivot('created_at');
|
|
}
|
|
|
|
/**
|
|
* Check if all set minimums are met.
|
|
* Returns: true = all met, false = at least one not met, null = no minimums set.
|
|
*/
|
|
public function minimumsStatus(): ?bool
|
|
{
|
|
$hasAny = false;
|
|
$allMet = true;
|
|
|
|
if ($this->min_players !== null) {
|
|
$hasAny = true;
|
|
if ($this->type === EventType::Meeting) {
|
|
// Für Besprechungen: User mit Zusage zählen (user_id-basiert)
|
|
$count = $this->participants
|
|
->where('status', ParticipantStatus::Yes)
|
|
->whereNotNull('user_id')
|
|
->count();
|
|
} else {
|
|
$count = $this->participants->where('status', ParticipantStatus::Yes)->count();
|
|
}
|
|
if ($count < $this->min_players) {
|
|
$allMet = false;
|
|
}
|
|
}
|
|
|
|
// Catering/Zeitnehmer nur für Typen die es unterstützen
|
|
if ($this->type->hasCatering() && $this->min_catering !== null) {
|
|
$hasAny = true;
|
|
$cateringYes = $this->caterings_yes_count
|
|
?? $this->caterings->where('status', CateringStatus::Yes)->count();
|
|
if ($cateringYes < $this->min_catering) {
|
|
$allMet = false;
|
|
}
|
|
}
|
|
|
|
if ($this->type->hasTimekeepers() && $this->min_timekeepers !== null) {
|
|
$hasAny = true;
|
|
$timekeeperYes = $this->timekeepers_yes_count
|
|
?? $this->timekeepers->where('status', CateringStatus::Yes)->count();
|
|
if ($timekeeperYes < $this->min_timekeepers) {
|
|
$allMet = false;
|
|
}
|
|
}
|
|
|
|
return $hasAny ? $allMet : null;
|
|
}
|
|
|
|
/**
|
|
* Add missing active team players as participants (idempotent).
|
|
* For meetings, delegates to syncMeetingParticipants().
|
|
*/
|
|
public function syncParticipants(int $userId): void
|
|
{
|
|
if ($this->type === EventType::Meeting) {
|
|
$this->syncMeetingParticipants($userId);
|
|
return;
|
|
}
|
|
|
|
$activePlayerIds = $this->team->activePlayers()->pluck('id');
|
|
$existingPlayerIds = $this->participants()->pluck('player_id');
|
|
|
|
$missingPlayerIds = $activePlayerIds->diff($existingPlayerIds);
|
|
|
|
if ($missingPlayerIds->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$now = now();
|
|
$records = $missingPlayerIds->map(fn ($playerId) => [
|
|
'event_id' => $this->id,
|
|
'player_id' => $playerId,
|
|
'status' => ParticipantStatus::Unknown->value,
|
|
'set_by_user_id' => $userId,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
])->toArray();
|
|
|
|
$this->participants()->insert($records);
|
|
}
|
|
|
|
/**
|
|
* Sync meeting participants: eligible users for this event's team.
|
|
* Eligible = parents with children in team + coaches (excluding admin ID 1).
|
|
*/
|
|
public function syncMeetingParticipants(int $setByUserId): void
|
|
{
|
|
$teamId = $this->team_id;
|
|
|
|
// Users with active children in this team
|
|
$parentUserIds = DB::table('parent_player')
|
|
->join('players', 'parent_player.player_id', '=', 'players.id')
|
|
->where('players.team_id', $teamId)
|
|
->whereNull('players.deleted_at')
|
|
->where('players.is_active', true)
|
|
->join('users', 'parent_player.parent_id', '=', 'users.id')
|
|
->whereNull('users.deleted_at')
|
|
->where('users.is_active', true)
|
|
->pluck('parent_player.parent_id')
|
|
->unique();
|
|
|
|
// Coaches — nur die dem Team zugeordneten
|
|
$coachIds = User::where('role', UserRole::Coach->value)
|
|
->where('is_active', true)
|
|
->whereNull('deleted_at')
|
|
->whereHas('coachTeams', fn ($q) => $q->where('teams.id', $teamId))
|
|
->pluck('id');
|
|
|
|
// Admins aus Meeting-Teilnehmern ausschließen (rollenbasiert statt ID-basiert, V12)
|
|
$adminIds = User::where('role', UserRole::Admin)->pluck('id');
|
|
$eligibleUserIds = $parentUserIds->merge($coachIds)->unique()->diff($adminIds);
|
|
|
|
$existingUserIds = $this->participants()->whereNotNull('user_id')->pluck('user_id');
|
|
$missingUserIds = $eligibleUserIds->diff($existingUserIds);
|
|
|
|
if ($missingUserIds->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$now = now();
|
|
$records = $missingUserIds->map(fn ($userId) => [
|
|
'event_id' => $this->id,
|
|
'player_id' => null,
|
|
'user_id' => $userId,
|
|
'status' => ParticipantStatus::Unknown->value,
|
|
'set_by_user_id' => $setByUserId,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
])->toArray();
|
|
|
|
$this->participants()->insert($records);
|
|
}
|
|
|
|
/**
|
|
* Sync participants for all future events of a team.
|
|
*/
|
|
public static function syncParticipantsForTeam(int $teamId, int $userId): void
|
|
{
|
|
$futureEvents = static::where('team_id', $teamId)
|
|
->where('start_at', '>=', now())
|
|
->get();
|
|
|
|
foreach ($futureEvents as $event) {
|
|
$event->syncParticipants($userId);
|
|
}
|
|
}
|
|
|
|
public function scopePublished($query)
|
|
{
|
|
return $query->whereIn('status', [EventStatus::Published, EventStatus::Cancelled]);
|
|
}
|
|
|
|
public function scopeUpcoming($query)
|
|
{
|
|
return $query->where('start_at', '>=', now())->orderBy('start_at');
|
|
}
|
|
|
|
public function scopeForTeam($query, int $teamId)
|
|
{
|
|
return $query->where('team_id', $teamId);
|
|
}
|
|
}
|