middleware('auth:sanctum')->only(['store', 'update', 'destroy']); } /** * Seznam kol (rounds) – stránkovaně. */ public function index(Request $request): JsonResponse { $perPage = (int) $request->get('per_page', 100); $contestId = $request->query('contest_id'); $onlyActive = (bool) $request->query('only_active', false); $includeTests = filter_var($request->query('include_tests', true), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($includeTests === null) { $includeTests = true; } $items = Round::query() ->with([ 'contest', 'bands', 'categories', 'powerCategories', 'ruleSet', ]) ->when($contestId, fn ($q) => $q->where('contest_id', $contestId)) ->when($onlyActive, fn ($q) => $q->where('is_active', true)) ->when(! $includeTests, fn ($q) => $q->where('is_test', false)) ->orderByDesc('start_time') ->orderByDesc('end_time') ->paginate($perPage); return response()->json($items); } /** * Vytvoření nového kola. * Autorizace přes RoundPolicy@create. */ public function store(Request $request): JsonResponse { $this->authorize('create', Round::class); $data = $this->validateData($request); $relations = $this->validateRelations($request); if (! array_key_exists('rule_set_id', $data) || $data['rule_set_id'] === null) { $contestRuleSetId = Contest::where('id', $data['contest_id'])->value('rule_set_id'); $data['rule_set_id'] = $contestRuleSetId; } $round = Round::create($data); $this->syncRelations($round, $relations); $round->load([ 'contest', 'bands', 'categories', 'powerCategories', 'ruleSet', ]); return response()->json($round, 201); } /** * Detail kola. */ public function show(Round $round): JsonResponse { $round->load([ 'contest', 'bands', 'categories', 'powerCategories', 'ruleSet', ]); return response()->json($round); } /** * Aktualizace kola (partial update). * Autorizace přes RoundPolicy@update. */ public function update(Request $request, Round $round): JsonResponse { $this->authorize('update', $round); $data = $this->validateData($request, partial: true); $relations = $this->validateRelations($request); $round->fill($data); $round->save(); $this->syncRelations($round, $relations); $round->load([ 'contest', 'bands', 'categories', 'powerCategories', 'ruleSet', ]); return response()->json($round); } /** * Smazání kola. * Autorizace přes RoundPolicy@delete. */ public function destroy(Round $round): JsonResponse { $this->authorize('delete', $round); $round->delete(); return response()->json(null, 204); } /** * Ručně spustí rebuild deklarovaných výsledků pro kolo. */ public function recalculateClaimed(Request $request, Round $round): JsonResponse { $this->authorize('update', $round); $run = ClaimedRunResolver::createNewForRound($round->id, auth()->id()); RebuildClaimedLogResultsJob::dispatch($run->id)->onQueue('evaluation'); return response()->json([ 'status' => 'queued', 'message' => 'Přepočet deklarovaných výsledků byl spuštěn.', ], 202); } /** * Spustí kompletní vyhodnocovací pipeline pro nové EvaluationRun. */ public function startEvaluation(Request $request, Round $round): JsonResponse { $this->authorize('update', $round); $data = $request->validate([ 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], 'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST,CLAIMED'], 'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'], 'name' => ['sometimes', 'nullable', 'string', 'max:100'], 'is_official' => ['sometimes', 'boolean'], 'scope' => ['sometimes', 'array'], 'scope.band_ids' => ['sometimes', 'array'], 'scope.band_ids.*' => ['integer', 'exists:bands,id'], 'scope.category_ids' => ['sometimes', 'array'], 'scope.category_ids.*' => ['integer', 'exists:categories,id'], 'scope.power_category_ids' => ['sometimes', 'array'], 'scope.power_category_ids.*' => ['integer', 'exists:power_categories,id'], ]); $rulesVersion = $data['rules_version'] ?? 'OFFICIAL'; $resultType = $data['result_type'] ?? ($rulesVersion === 'TEST' ? 'TEST' : 'PRELIMINARY'); $run = EvaluationRun::create([ 'round_id' => $round->id, 'rule_set_id' => $data['rule_set_id'] ?? $round->rule_set_id, 'rules_version' => $rulesVersion, 'result_type' => $rulesVersion === 'CLAIMED' ? null : $resultType, 'name' => $data['name'] ?? 'Vyhodnocení', 'is_official' => $data['is_official'] ?? ($resultType === 'FINAL'), 'scope' => $data['scope'] ?? null, 'status' => 'PENDING', 'created_by_user_id' => auth()->id(), ]); StartEvaluationRunJob::dispatch($run->id)->onQueue('evaluation'); $run->load(['round']); return response()->json($run, 201); } /** * Spustí nový EvaluationRun jako re-run s převzetím override z posledního běhu. */ public function startEvaluationIncremental(Request $request, Round $round): JsonResponse { $this->authorize('update', $round); $data = $request->validate([ 'source_run_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_runs,id'], 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], 'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST'], 'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'], 'name' => ['sometimes', 'nullable', 'string', 'max:100'], 'scope' => ['sometimes', 'array'], 'scope.band_ids' => ['sometimes', 'array'], 'scope.band_ids.*' => ['integer', 'exists:bands,id'], 'scope.category_ids' => ['sometimes', 'array'], 'scope.category_ids.*' => ['integer', 'exists:categories,id'], 'scope.power_category_ids' => ['sometimes', 'array'], 'scope.power_category_ids.*' => ['integer', 'exists:power_categories,id'], ]); $sourceRun = null; if (! empty($data['source_run_id'])) { $sourceRun = EvaluationRun::where('round_id', $round->id) ->where('id', (int) $data['source_run_id']) ->first(); } if (! $sourceRun) { $sourceRun = EvaluationRun::where('round_id', $round->id) ->where('rules_version', '!=', 'CLAIMED') ->orderByDesc('id') ->first(); } $rulesVersion = $data['rules_version'] ?? ($sourceRun?->rules_version ?? 'OFFICIAL'); $resultType = $data['result_type'] ?? ($rulesVersion === 'TEST' ? 'TEST' : 'PRELIMINARY'); $run = EvaluationRun::create([ 'round_id' => $round->id, 'rule_set_id' => $data['rule_set_id'] ?? ($sourceRun?->rule_set_id ?? $round->rule_set_id), 'rules_version' => $rulesVersion, 'result_type' => $resultType, 'name' => $data['name'] ?? 'Vyhodnocení (re-run)', 'is_official' => $resultType === 'FINAL', 'scope' => $data['scope'] ?? ($sourceRun?->scope ?? null), 'status' => 'PENDING', 'created_by_user_id' => auth()->id(), ]); if ($sourceRun) { $this->cloneOverrides($sourceRun->id, $run->id, auth()->id()); } StartEvaluationRunJob::dispatch($run->id)->onQueue('evaluation'); $run->load(['round']); return response()->json($run, 201); } protected function cloneOverrides(int $sourceRunId, int $targetRunId, ?int $userId = null): void { $logOverrides = LogOverride::where('evaluation_run_id', $sourceRunId)->get(); if ($logOverrides->isNotEmpty()) { $rows = $logOverrides->map(function ($override) use ($targetRunId, $userId) { return [ 'evaluation_run_id' => $targetRunId, 'log_id' => $override->log_id, 'forced_log_status' => $override->forced_log_status, 'forced_band_id' => $override->forced_band_id, 'forced_category_id' => $override->forced_category_id, 'forced_power_category_id' => $override->forced_power_category_id, 'forced_sixhr_category' => $override->forced_sixhr_category, 'forced_power_w' => $override->forced_power_w, 'reason' => $override->reason, 'context' => $this->encodeContext($override->context), 'created_by_user_id' => $override->created_by_user_id ?? $userId, 'created_at' => now(), 'updated_at' => now(), ]; })->all(); LogOverride::insert($rows); } $qsoOverrides = QsoOverride::where('evaluation_run_id', $sourceRunId)->get(); if ($qsoOverrides->isNotEmpty()) { $rows = $qsoOverrides->map(function ($override) use ($targetRunId, $userId) { return [ 'evaluation_run_id' => $targetRunId, 'log_qso_id' => $override->log_qso_id, 'forced_matched_log_qso_id' => $override->forced_matched_log_qso_id, 'forced_status' => $override->forced_status, 'forced_points' => $override->forced_points, 'forced_penalty' => $override->forced_penalty, 'reason' => $override->reason, 'context' => $this->encodeContext($override->context), 'created_by_user_id' => $override->created_by_user_id ?? $userId, 'created_at' => now(), 'updated_at' => now(), ]; })->all(); QsoOverride::insert($rows); } } protected function encodeContext(mixed $context): ?string { if ($context === null) { return null; } $encoded = json_encode($context); return $encoded === false ? null : $encoded; } /** * Validace vstupu pro store / update. */ protected function validateData(Request $request, bool $partial = false): array { $required = $partial ? 'sometimes' : 'required'; return $request->validate([ 'contest_id' => [$required, 'integer', 'exists:contests,id'], // name/description – pokud používáš překlady jako u Contest: 'name' => [$required, 'array'], 'name.*' => ['string', 'max:255'], 'description' => ['sometimes', 'nullable', 'array'], 'description.*' => ['string'], 'start_time' => [$required, 'date'], 'end_time' => [$required, 'date', 'after:start_time'], 'logs_deadline' => [$required, 'date'], 'is_active' => ['sometimes', 'boolean'], 'is_test' => ['sometimes', 'boolean'], 'is_sixhr' => ['sometimes', 'boolean'], 'first_check' => ['sometimes', 'nullable', 'date'], 'second_check' => ['sometimes', 'nullable', 'date'], 'unique_qso_check' => ['sometimes', 'nullable', 'date'], 'third_check' => ['sometimes', 'nullable', 'date'], 'fourth_check' => ['sometimes', 'nullable', 'date'], 'prelimitary_results'=> ['sometimes', 'nullable', 'date'], 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], ]); } /** * Validace ID navázaných entit pro belongsToMany vztahy. */ protected function validateRelations(Request $request): array { return $request->validate([ 'band_ids' => ['sometimes', 'array'], 'band_ids.*' => ['integer', 'exists:bands,id'], 'category_ids' => ['sometimes', 'array'], 'category_ids.*' => ['integer', 'exists:categories,id'], 'power_category_ids' => ['sometimes', 'array'], 'power_category_ids.*' => ['integer', 'exists:power_categories,id'], ]); } /** * Sync vazeb pro belongsToMany vztahy. */ protected function syncRelations(Round $round, array $relations): void { if (array_key_exists('band_ids', $relations)) { $round->bands()->sync($relations['band_ids']); } if (array_key_exists('category_ids', $relations)) { $round->categories()->sync($relations['category_ids']); } if (array_key_exists('power_category_ids', $relations)) { $round->powerCategories()->sync($relations['power_category_ids']); } } }