From 9ccf1eacebf5c086cbe4ef8b6b1f85bf86a5f116 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 18:00:28 +0200 Subject: [PATCH] =?UTF-8?q?feat(timetable):=20Artist=20domain=20=E2=80=94?= =?UTF-8?q?=207=20enums=20+=209=20Eloquent=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enums under App\Enums\Artist\ (PascalCase per FormBuilder convention, snake_case wire values per RFC): - ArtistEngagementStatus (D9, 9 states + Dutch labels) - BumaHandledBy (D26) - FeeType, PaymentStatus - AdvanceSectionType, AdvanceSectionSubmissionStatus, AdvanceSubmissionStatus Models: - Artist (org-scoped, slug-unique-per-org via creating boot hook) - ArtistEngagement (per-event booking, denorm organisation_id) - Genre, Stage (event-scoped, ordered scope), StageDay (Pivot, int PK) - Performance (engagement-scoped, isParked() helper) - AdvanceSection, AdvanceSubmission, ArtistContact (primary scope) OrganisationScope wired: - Direct organisation_id: Artist, Genre, ArtistEngagement - FK-chain via tenantScopeStrategy(): Stage→Event, Performance→Engagement, AdvanceSection→Engagement, AdvanceSubmission→Section→Engagement, ArtistContact→Artist, StageDay→Stage→Event Soft-deletes: Artist, ArtistEngagement, Performance (per RFC §5.4). LogsActivity baseline (logFillable+dontSubmitEmptyLogs) on all business models — actual mutation surfaces wire LogOptions in Session 2+. Inverse relations added on Organisation, Event, Company. companies.handles_buma cast added. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Artist/AdvanceSectionSubmissionStatus.php | 20 +++ api/app/Enums/Artist/AdvanceSectionType.php | 20 +++ .../Enums/Artist/AdvanceSubmissionStatus.php | 17 +++ .../Enums/Artist/ArtistEngagementStatus.php | 39 ++++++ api/app/Enums/Artist/BumaHandledBy.php | 26 ++++ api/app/Enums/Artist/FeeType.php | 26 ++++ api/app/Enums/Artist/PaymentStatus.php | 26 ++++ api/app/Models/AdvanceSection.php | 79 +++++++++++ api/app/Models/AdvanceSubmission.php | 70 ++++++++++ api/app/Models/Artist.php | 107 +++++++++++++++ api/app/Models/ArtistContact.php | 69 ++++++++++ api/app/Models/ArtistEngagement.php | 123 ++++++++++++++++++ api/app/Models/Company.php | 13 ++ api/app/Models/Event.php | 17 ++- api/app/Models/Genre.php | 59 +++++++++ api/app/Models/Organisation.php | 15 +++ api/app/Models/Performance.php | 81 ++++++++++++ api/app/Models/Stage.php | 76 +++++++++++ api/app/Models/StageDay.php | 51 ++++++++ 19 files changed, 933 insertions(+), 1 deletion(-) create mode 100644 api/app/Enums/Artist/AdvanceSectionSubmissionStatus.php create mode 100644 api/app/Enums/Artist/AdvanceSectionType.php create mode 100644 api/app/Enums/Artist/AdvanceSubmissionStatus.php create mode 100644 api/app/Enums/Artist/ArtistEngagementStatus.php create mode 100644 api/app/Enums/Artist/BumaHandledBy.php create mode 100644 api/app/Enums/Artist/FeeType.php create mode 100644 api/app/Enums/Artist/PaymentStatus.php create mode 100644 api/app/Models/AdvanceSection.php create mode 100644 api/app/Models/AdvanceSubmission.php create mode 100644 api/app/Models/Artist.php create mode 100644 api/app/Models/ArtistContact.php create mode 100644 api/app/Models/ArtistEngagement.php create mode 100644 api/app/Models/Genre.php create mode 100644 api/app/Models/Performance.php create mode 100644 api/app/Models/Stage.php create mode 100644 api/app/Models/StageDay.php diff --git a/api/app/Enums/Artist/AdvanceSectionSubmissionStatus.php b/api/app/Enums/Artist/AdvanceSectionSubmissionStatus.php new file mode 100644 index 00000000..7e69606e --- /dev/null +++ b/api/app/Enums/Artist/AdvanceSectionSubmissionStatus.php @@ -0,0 +1,20 @@ + 'Concept', + self::Requested => 'Aangevraagd', + self::Option => 'Optie', + self::Offered => 'Aanbod uit', + self::Confirmed => 'Bevestigd', + self::Contracted => 'Gecontracteerd', + self::Cancelled => 'Geannuleerd', + self::Rejected => 'Afgewezen', + self::Declined => 'Bedankt', + }; + } +} diff --git a/api/app/Enums/Artist/BumaHandledBy.php b/api/app/Enums/Artist/BumaHandledBy.php new file mode 100644 index 00000000..0a8e79d1 --- /dev/null +++ b/api/app/Enums/Artist/BumaHandledBy.php @@ -0,0 +1,26 @@ + 'Organisatie', + self::BookingAgency => 'Boekingsagent', + self::NotApplicable => 'Niet van toepassing', + }; + } +} diff --git a/api/app/Enums/Artist/FeeType.php b/api/app/Enums/Artist/FeeType.php new file mode 100644 index 00000000..8db3dea2 --- /dev/null +++ b/api/app/Enums/Artist/FeeType.php @@ -0,0 +1,26 @@ + 'Vaste fee', + self::DoorSplit => 'Door split', + self::GuaranteePlusSplit => 'Garantie + split', + }; + } +} diff --git a/api/app/Enums/Artist/PaymentStatus.php b/api/app/Enums/Artist/PaymentStatus.php new file mode 100644 index 00000000..6abad00f --- /dev/null +++ b/api/app/Enums/Artist/PaymentStatus.php @@ -0,0 +1,26 @@ + 'Geen betaling', + self::DepositPaid => 'Aanbetaling voldaan', + self::PaidInFull => 'Volledig voldaan', + }; + } +} diff --git a/api/app/Models/AdvanceSection.php b/api/app/Models/AdvanceSection.php new file mode 100644 index 00000000..a41b1bc1 --- /dev/null +++ b/api/app/Models/AdvanceSection.php @@ -0,0 +1,79 @@ + ArtistEngagement::class, 'fk' => 'engagement_id']; + } + + protected $fillable = [ + 'engagement_id', + 'name', + 'type', + 'is_open', + 'open_from', + 'open_to', + 'sort_order', + 'submission_status', + 'last_submitted_at', + 'last_submitted_by', + 'submission_diff', + ]; + + protected function casts(): array + { + return [ + 'type' => AdvanceSectionType::class, + 'submission_status' => AdvanceSectionSubmissionStatus::class, + 'is_open' => 'boolean', + 'open_from' => 'datetime', + 'open_to' => 'datetime', + 'sort_order' => 'integer', + 'last_submitted_at' => 'datetime', + 'submission_diff' => 'array', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function engagement(): BelongsTo + { + return $this->belongsTo(ArtistEngagement::class, 'engagement_id'); + } + + public function submissions(): HasMany + { + return $this->hasMany(AdvanceSubmission::class); + } +} diff --git a/api/app/Models/AdvanceSubmission.php b/api/app/Models/AdvanceSubmission.php new file mode 100644 index 00000000..cefe1a9b --- /dev/null +++ b/api/app/Models/AdvanceSubmission.php @@ -0,0 +1,70 @@ + AdvanceSection::class, 'fk' => 'advance_section_id']; + } + + protected $fillable = [ + 'advance_section_id', + 'submitted_by_name', + 'submitted_by_email', + 'submitted_at', + 'status', + 'reviewed_by', + 'reviewed_at', + 'data', + ]; + + protected function casts(): array + { + return [ + 'status' => AdvanceSubmissionStatus::class, + 'submitted_at' => 'datetime', + 'reviewed_at' => 'datetime', + 'data' => 'array', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function section(): BelongsTo + { + return $this->belongsTo(AdvanceSection::class, 'advance_section_id'); + } + + public function reviewer(): BelongsTo + { + return $this->belongsTo(User::class, 'reviewed_by'); + } +} diff --git a/api/app/Models/Artist.php b/api/app/Models/Artist.php new file mode 100644 index 00000000..783cc5b1 --- /dev/null +++ b/api/app/Models/Artist.php @@ -0,0 +1,107 @@ +slug)) { + $artist->slug = $artist->generateUniqueSlug($artist->name); + } + }); + } + + protected $fillable = [ + 'organisation_id', + 'name', + 'slug', + 'default_genre_id', + 'default_draw', + 'star_rating', + 'home_base_country', + 'agent_company_id', + 'notes', + ]; + + protected function casts(): array + { + return [ + 'default_draw' => 'integer', + 'star_rating' => 'integer', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + private function generateUniqueSlug(string $name): string + { + $base = Str::slug($name); + $slug = $base; + $suffix = 2; + + while ( + self::withoutGlobalScope(OrganisationScope::class) + ->withTrashed() + ->where('organisation_id', $this->organisation_id) + ->where('slug', $slug) + ->exists() + ) { + $slug = "{$base}-{$suffix}"; + $suffix++; + } + + return $slug; + } + + public function organisation(): BelongsTo + { + return $this->belongsTo(Organisation::class); + } + + public function defaultGenre(): BelongsTo + { + return $this->belongsTo(Genre::class, 'default_genre_id'); + } + + public function agentCompany(): BelongsTo + { + return $this->belongsTo(Company::class, 'agent_company_id'); + } + + public function contacts(): HasMany + { + return $this->hasMany(ArtistContact::class); + } + + public function engagements(): HasMany + { + return $this->hasMany(ArtistEngagement::class); + } +} diff --git a/api/app/Models/ArtistContact.php b/api/app/Models/ArtistContact.php new file mode 100644 index 00000000..8a4a0382 --- /dev/null +++ b/api/app/Models/ArtistContact.php @@ -0,0 +1,69 @@ + Artist::class, 'fk' => 'artist_id']; + } + + protected $fillable = [ + 'artist_id', + 'name', + 'email', + 'phone', + 'role', + 'is_primary', + 'receives_briefing', + 'receives_infosheet', + ]; + + protected function casts(): array + { + return [ + 'is_primary' => 'boolean', + 'receives_briefing' => 'boolean', + 'receives_infosheet' => 'boolean', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function artist(): BelongsTo + { + return $this->belongsTo(Artist::class); + } + + public function scopePrimary(Builder $query): Builder + { + return $query->where('is_primary', true); + } +} diff --git a/api/app/Models/ArtistEngagement.php b/api/app/Models/ArtistEngagement.php new file mode 100644 index 00000000..dbb58428 --- /dev/null +++ b/api/app/Models/ArtistEngagement.php @@ -0,0 +1,123 @@ + ArtistEngagementStatus::class, + 'fee_type' => FeeType::class, + 'buma_handled_by' => BumaHandledBy::class, + 'payment_status' => PaymentStatus::class, + 'buma_applicable' => 'boolean', + 'vat_applicable' => 'boolean', + 'deal_breakdown' => 'array', + 'deposit_due_date' => 'date', + 'balance_due_date' => 'date', + 'crew_count' => 'integer', + 'guests_count' => 'integer', + 'requested_at' => 'datetime', + 'option_expires_at' => 'datetime', + 'advance_open_from' => 'datetime', + 'advance_open_to' => 'datetime', + 'advancing_completed_count' => 'integer', + 'advancing_total_count' => 'integer', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function organisation(): BelongsTo + { + return $this->belongsTo(Organisation::class); + } + + public function artist(): BelongsTo + { + return $this->belongsTo(Artist::class); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + public function projectLeader(): BelongsTo + { + return $this->belongsTo(User::class, 'project_leader_id'); + } + + public function performances(): HasMany + { + return $this->hasMany(Performance::class, 'engagement_id'); + } + + public function advanceSections(): HasMany + { + return $this->hasMany(AdvanceSection::class, 'engagement_id'); + } +} diff --git a/api/app/Models/Company.php b/api/app/Models/Company.php index 06b1bf0f..1899fc39 100644 --- a/api/app/Models/Company.php +++ b/api/app/Models/Company.php @@ -28,6 +28,7 @@ final class Company extends Model 'organisation_id', 'name', 'type', + 'handles_buma', 'kvk_number', 'contact_first_name', 'contact_last_name', @@ -35,6 +36,13 @@ final class Company extends Model 'contact_phone', ]; + protected function casts(): array + { + return [ + 'handles_buma' => 'boolean', + ]; + } + public function getContactFullNameAttribute(): ?string { if (! $this->contact_first_name) { @@ -54,6 +62,11 @@ final class Company extends Model return $this->hasMany(Person::class); } + public function artistsAsAgent(): HasMany + { + return $this->hasMany(Artist::class, 'agent_company_id'); + } + /** @param Builder $query */ public function scopeOrdered(Builder $query): Builder { diff --git a/api/app/Models/Event.php b/api/app/Models/Event.php index 7c1ab402..c0d4f57d 100644 --- a/api/app/Models/Event.php +++ b/api/app/Models/Event.php @@ -27,7 +27,7 @@ final class Event extends Model protected static function booted(): void { - static::addGlobalScope(new OrganisationScope()); + self::addGlobalScope(new OrganisationScope); } /** @var array> Allowed status transitions */ @@ -153,6 +153,21 @@ final class Event extends Model ->orderBy('name'); } + public function stages(): HasMany + { + return $this->hasMany(Stage::class); + } + + public function artistEngagements(): HasMany + { + return $this->hasMany(ArtistEngagement::class); + } + + public function performances(): HasMany + { + return $this->hasMany(Performance::class); + } + // ----- Status State Machine ----- public function canTransitionTo(string $newStatus): bool diff --git a/api/app/Models/Genre.php b/api/app/Models/Genre.php new file mode 100644 index 00000000..070f8e09 --- /dev/null +++ b/api/app/Models/Genre.php @@ -0,0 +1,59 @@ + 'integer', + 'is_active' => 'boolean', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function organisation(): BelongsTo + { + return $this->belongsTo(Organisation::class); + } + + public function artists(): HasMany + { + return $this->hasMany(Artist::class, 'default_genre_id'); + } +} diff --git a/api/app/Models/Organisation.php b/api/app/Models/Organisation.php index 4b5c9cb1..04704fe9 100644 --- a/api/app/Models/Organisation.php +++ b/api/app/Models/Organisation.php @@ -100,4 +100,19 @@ final class Organisation extends Model { return $this->hasMany(EmailLog::class); } + + public function artists(): HasMany + { + return $this->hasMany(Artist::class); + } + + public function genres(): HasMany + { + return $this->hasMany(Genre::class); + } + + public function artistEngagements(): HasMany + { + return $this->hasMany(ArtistEngagement::class); + } } diff --git a/api/app/Models/Performance.php b/api/app/Models/Performance.php new file mode 100644 index 00000000..639775e2 --- /dev/null +++ b/api/app/Models/Performance.php @@ -0,0 +1,81 @@ + ArtistEngagement::class, 'fk' => 'engagement_id']; + } + + protected $fillable = [ + 'engagement_id', + 'event_id', + 'stage_id', + 'lane', + 'start_at', + 'end_at', + 'version', + 'notes', + ]; + + protected function casts(): array + { + return [ + 'lane' => 'integer', + 'start_at' => 'datetime', + 'end_at' => 'datetime', + 'version' => 'integer', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function engagement(): BelongsTo + { + return $this->belongsTo(ArtistEngagement::class, 'engagement_id'); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + public function stage(): BelongsTo + { + return $this->belongsTo(Stage::class); + } + + public function isParked(): bool + { + return $this->stage_id === null; + } +} diff --git a/api/app/Models/Stage.php b/api/app/Models/Stage.php new file mode 100644 index 00000000..7bd60a68 --- /dev/null +++ b/api/app/Models/Stage.php @@ -0,0 +1,76 @@ + Event::class, 'fk' => 'event_id']; + } + + protected $fillable = [ + 'event_id', + 'name', + 'color', + 'capacity', + 'sort_order', + ]; + + protected function casts(): array + { + return [ + 'capacity' => 'integer', + 'sort_order' => 'integer', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + public function stageDays(): HasMany + { + return $this->hasMany(StageDay::class); + } + + public function performances(): HasMany + { + return $this->hasMany(Performance::class); + } + + public function scopeOrdered(Builder $query): Builder + { + return $query->orderBy('sort_order')->orderBy('created_at'); + } +} diff --git a/api/app/Models/StageDay.php b/api/app/Models/StageDay.php new file mode 100644 index 00000000..0619f372 --- /dev/null +++ b/api/app/Models/StageDay.php @@ -0,0 +1,51 @@ + Stage::class, 'fk' => 'stage_id']; + } + + protected $fillable = [ + 'stage_id', + 'event_id', + ]; + + public function stage(): BelongsTo + { + return $this->belongsTo(Stage::class); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } +}