*/ public function rules(): array { $event = $this->route('event'); $resolvedEventId = $this->resolveTargetEventId(); return [ // performances has no organisation_id column (FK-chain via // engagement_id); cross-tenant is caught by the policy in // TimetableMoveController via Gate::authorize('move', ...). 'performance_id' => [ 'required', 'string', 'max:30', Rule::exists('performances', 'id'), ], 'target_stage_id' => [ 'nullable', 'string', 'max:30', Rule::exists('stages', 'id'), new StageActiveOnEvent($resolvedEventId), ], 'target_start_at' => [ 'nullable', 'date_format:Y-m-d H:i:s', 'required_unless:target_stage_id,null', new WithinEventBounds($resolvedEventId), ], 'target_end_at' => [ 'nullable', 'date_format:Y-m-d H:i:s', 'required_unless:target_stage_id,null', 'after:target_start_at', new WithinEventBounds($resolvedEventId), ], 'target_lane' => ['nullable', 'integer', 'min:0', 'max:9'], 'version' => ['required', 'integer', 'min:0'], ]; } public function withValidator(Validator $validator): void { $validator->after(function (Validator $validator): void { // When target_stage_id is non-null, target_lane must be set // (the move algorithm requires a definite lane). if ($this->input('target_stage_id') !== null && $this->input('target_lane') === null) { $validator->errors()->add('target_lane', 'target_lane is verplicht bij een niet-leeg target_stage_id.'); } }); } /** * Resolve the event_id the candidate move lands on so the * StageActiveOnEvent and WithinEventBounds rules can validate * against a concrete event window. * * For flat events: stage.event_id is the answer. * For festivals: walk stage_days for target_stage_id and find the * sub-event whose [start, end] contains target_start_at. */ private function resolveTargetEventId(): ?string { $stageId = $this->input('target_stage_id'); $startAt = $this->input('target_start_at'); if (! is_string($stageId) || ! is_string($startAt)) { return null; } $start = CarbonImmutable::parse($startAt); $stage = Stage::query()->find($stageId); if ($stage === null) { return null; } $match = StageDay::query() ->where('stage_id', $stage->id) ->join('events', 'events.id', '=', 'stage_days.event_id') ->where('events.start_date', '<=', $start->toDateString()) ->where('events.end_date', '>=', $start->toDateString()) ->orderBy('events.start_date', 'desc') ->limit(1) ->value('stage_days.event_id'); return is_string($match) ? $match : $stage->event_id; } }