middleware('auth:sanctum')->only(['store', 'update', 'destroy']); } /** * Seznam evaluation runů – filtrování podle round_id, is_official. */ public function index(Request $request): JsonResponse { $perPage = (int) $request->get('per_page', 100); // Seznam běhů slouží hlavně pro monitoring na RoundDetailPage. $query = EvaluationRun::query() ->with(['round']); if ($request->filled('round_id')) { $query->where('round_id', (int) $request->get('round_id')); } if ($request->filled('is_official')) { $query->where( 'is_official', filter_var($request->get('is_official'), FILTER_VALIDATE_BOOL) ); } if ($request->filled('result_type')) { $query->where('result_type', $request->get('result_type')); } $items = $query ->orderByDesc('created_at') ->paginate($perPage); return response()->json($items); } /** * Vytvoření nového evaluation runu. * Typicky před samotným spuštěním vyhodnocovače. */ public function store(Request $request): JsonResponse { $this->authorize('create', EvaluationRun::class); $data = $this->validateData($request); // Samotné spuštění pipeline zajišťuje StartEvaluationRunJob. $run = EvaluationRun::create($data); $run->load(['round']); return response()->json($run, 201); } /** * Detail jednoho evaluation runu včetně vazeb a výsledků. */ public function show(EvaluationRun $evaluationRun): JsonResponse { // Detail běhu včetně výsledků je náročný – používej s rozumem (paging/limit). $evaluationRun->load([ 'round', 'logResults', 'qsoResults', ]); return response()->json($evaluationRun); } /** * Aktualizace evaluation runu (např. změna názvu, poznámky, * příznaku is_official). */ public function update(Request $request, EvaluationRun $evaluationRun): JsonResponse { $this->authorize('update', $evaluationRun); $data = $this->validateData($request, partial: true); $evaluationRun->fill($data); $evaluationRun->save(); $evaluationRun->load(['round']); return response()->json($evaluationRun); } /** * Označí běh jako TEST/PRELIMINARY/FINAL a aktualizuje ukazatele v kole. */ public function setResultType(Request $request, EvaluationRun $evaluationRun): JsonResponse { $this->authorize('update', $evaluationRun); $data = $request->validate([ 'result_type' => ['required', 'string', 'in:TEST,PRELIMINARY,FINAL'], ]); $resultType = $data['result_type']; $evaluationRun->update([ 'result_type' => $resultType, 'is_official' => $resultType === 'FINAL', ]); $round = $evaluationRun->round; if ($round) { if ($resultType === 'FINAL') { $round->official_evaluation_run_id = $evaluationRun->id; $round->preliminary_evaluation_run_id = null; $round->test_evaluation_run_id = null; } elseif ($resultType === 'PRELIMINARY') { $round->preliminary_evaluation_run_id = $evaluationRun->id; $round->official_evaluation_run_id = null; $round->test_evaluation_run_id = null; } elseif ($resultType === 'TEST') { $round->test_evaluation_run_id = $evaluationRun->id; $round->official_evaluation_run_id = null; $round->preliminary_evaluation_run_id = null; } $round->save(); } $evaluationRun->load(['round']); return response()->json($evaluationRun); } /** * Smazání evaluation runu (včetně log_results / qso_results přes FK). */ public function destroy(EvaluationRun $evaluationRun): JsonResponse { $this->authorize('delete', $evaluationRun); $evaluationRun->delete(); return response()->json(null, 204); } /** * Zruší běh vyhodnocení (pokud je stále aktivní). */ public function cancel(EvaluationRun $evaluationRun): JsonResponse { $this->authorize('update', $evaluationRun); // Cancel je povolený jen pro běhy, které ještě neskončily. $activeStatuses = ['PENDING', 'RUNNING', 'WAITING_REVIEW_INPUT', 'WAITING_REVIEW_MATCH', 'WAITING_REVIEW_SCORE']; if (! in_array($evaluationRun->status, $activeStatuses, true)) { return response()->json([ 'message' => 'Běh nelze zrušit v aktuálním stavu.', ], 409); } $evaluationRun->update([ 'status' => 'CANCELED', 'finished_at' => now(), ]); if ($evaluationRun->batch_id) { $batch = Bus::findBatch($evaluationRun->batch_id); if ($batch) { $batch->cancel(); } } EvaluationRunEvent::create([ 'evaluation_run_id' => $evaluationRun->id, 'level' => 'warning', 'message' => 'Vyhodnocení bylo zrušeno uživatelem.', 'context' => [ 'step' => 'cancel', 'round_id' => $evaluationRun->round_id, 'user_id' => auth()->id(), ], ]); // Uvolní lock, aby mohl běh navázat nebo se spustit nový. EvaluationLock::where('evaluation_run_id', $evaluationRun->id)->delete(); return response()->json([ 'status' => 'canceled', ], 200); } /** * Vrátí poslední události běhu vyhodnocení. */ public function events(EvaluationRun $evaluationRun, Request $request): JsonResponse { $this->authorize('update', $evaluationRun); $limit = (int) $request->get('limit', 10); if ($limit < 1) { $limit = 1; } elseif ($limit > 100) { $limit = 100; } $minLevel = $request->get('min_level'); $levels = ['debug', 'info', 'warning', 'error']; if (! in_array($minLevel, $levels, true)) { $minLevel = null; } $events = $evaluationRun->events() ->when($minLevel, function ($query) use ($minLevel, $levels) { $query->whereIn('level', array_slice($levels, array_search($minLevel, $levels, true))); }) ->orderByDesc('id') ->limit($limit) ->get(); return response()->json($events); } /** * Pokračuje v běhu vyhodnocení po manuální kontrole. */ public function resume(Request $request, EvaluationRun $evaluationRun): JsonResponse { $this->authorize('update', $evaluationRun); if ($evaluationRun->isCanceled()) { return response()->json([ 'message' => 'Běh byl zrušen.', ], 409); } $ok = app(EvaluationCoordinator::class)->resume($evaluationRun, [ 'rebuild_working_set' => $request->boolean('rebuild_working_set'), ]); if ($ok) { return response()->json([ 'status' => 'queued', ], 202); } return response()->json([ 'message' => 'Běh není ve stavu čekání na kontrolu.', ], 409); } /** * Validace vstupů pro store / update. */ protected function validateData(Request $request, bool $partial = false): array { $required = $partial ? 'sometimes' : 'required'; return $request->validate([ 'round_id' => [$required, 'integer', 'exists:rounds,id'], 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], 'name' => ['sometimes', 'nullable', 'string', 'max:100'], 'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST,CLAIMED'], 'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'], 'is_official' => ['sometimes', 'boolean'], 'notes' => ['sometimes', 'nullable', 'string'], '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'], ]); } }