feat(timetable): API resources + LaneResolver helper
Six resources under app/Http/Resources/Api/V1/Artist/ matching
FormSubmissionResource conventions (final class, @mixin model,
optional()->toIso8601String, whenLoaded relationships).
GenreResource — id, name, color, sort_order, is_active
ArtistResource — master + lifetime/upcoming engagement counts
computed lazily from the engagements relation
ArtistContactResource — paired with ArtistResource.contacts
ArtistEngagementResource — full deal block with the RFC D26 Buma/VAT
formulas computed live in `computed.*`:
buma_amount = fee × buma_pct/100
IFF Organisation handles BUMA
vat_grondslag = fee + (buma when Organisation)
vat_amount = vat_grondslag × vat_pct/100
when vat_applicable
total_cost = fee + buma + vat + Σ breakdown
Frontend (Session 5) ports the same formula.
StageResource — adds stage_days as a flat array of event_ids
(not nested Event resources, to keep payload
light)
PerformanceResource — `lane` (raw, persisted), `lane_resolved`
(computed per D19), `warnings` (overlap +
B2B at minimum; capacity-warn refined later)
LaneResolver under app/Services/Artist/ is the pure-logic helper that
PerformanceResource calls. Greedy lowest-non-conflicting lane
assignment over the (stage_id, event_id) cohort sorted by start_at
then by raw lane (so cascade-bumped rows stay where they were
visually). Frontend port lands in Session 4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
109
api/app/Http/Resources/Api/V1/Artist/PerformanceResource.php
Normal file
109
api/app/Http/Resources/Api/V1/Artist/PerformanceResource.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\Api\V1\Artist;
|
||||
|
||||
use App\Models\Performance;
|
||||
use App\Services\Artist\LaneResolver;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Performance
|
||||
*/
|
||||
final class PerformanceResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'engagement_id' => $this->engagement_id,
|
||||
'event_id' => $this->event_id,
|
||||
'stage_id' => $this->stage_id,
|
||||
'lane' => (int) $this->lane,
|
||||
'lane_resolved' => $this->resolveLane(),
|
||||
'start_at' => optional($this->start_at)->toIso8601String(),
|
||||
'end_at' => optional($this->end_at)->toIso8601String(),
|
||||
'version' => (int) $this->version,
|
||||
'notes' => $this->notes,
|
||||
'warnings' => $this->computeWarnings(),
|
||||
'engagement' => ArtistEngagementResource::make($this->whenLoaded('engagement')),
|
||||
'stage' => StageResource::make($this->whenLoaded('stage')),
|
||||
'created_at' => optional($this->created_at)->toIso8601String(),
|
||||
'updated_at' => optional($this->updated_at)->toIso8601String(),
|
||||
'deleted_at' => optional($this->deleted_at)->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed via LaneResolver over the (stage, sub-event) cohort.
|
||||
* For parked performances (stage_id = null) the persisted lane is
|
||||
* surfaced as-is — the wachtrij is a flat list, not a lane grid.
|
||||
*/
|
||||
private function resolveLane(): int
|
||||
{
|
||||
if ($this->stage_id === null) {
|
||||
return (int) $this->lane;
|
||||
}
|
||||
|
||||
$cohort = Performance::query()
|
||||
->where('stage_id', $this->stage_id)
|
||||
->where('event_id', $this->event_id)
|
||||
->get();
|
||||
|
||||
$resolved = app(LaneResolver::class)->resolve($cohort);
|
||||
|
||||
return $resolved[(string) $this->id] ?? (int) $this->lane;
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC v0.2 D5 / D6 / D25 — overlap, B2B, capacity warnings.
|
||||
* Naive implementation for Session 2; refined as the timetable
|
||||
* frontend lands in Session 4.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function computeWarnings(): array
|
||||
{
|
||||
$warnings = [];
|
||||
|
||||
if ($this->stage_id === null) {
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
$start = CarbonImmutable::instance($this->start_at);
|
||||
$end = CarbonImmutable::instance($this->end_at);
|
||||
|
||||
$peers = Performance::query()
|
||||
->where('stage_id', $this->stage_id)
|
||||
->where('event_id', $this->event_id)
|
||||
->where('id', '!=', $this->id)
|
||||
->get();
|
||||
|
||||
foreach ($peers as $other) {
|
||||
$oStart = CarbonImmutable::instance($other->start_at);
|
||||
$oEnd = CarbonImmutable::instance($other->end_at);
|
||||
|
||||
if ($start < $oEnd && $oStart < $end && (int) $other->lane === (int) $this->lane) {
|
||||
$warnings[] = 'overlap';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($peers as $other) {
|
||||
$oEnd = CarbonImmutable::instance($other->end_at);
|
||||
$oStart = CarbonImmutable::instance($other->start_at);
|
||||
if ($oEnd->equalTo($start) || $oStart->equalTo($end)) {
|
||||
$warnings[] = 'b2b';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($warnings));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user