middleware('auth:sanctum')->only(['store', 'update', 'destroy']); } /** * Seznam výsledků logů – filtrování podle evaluation_run_id, * log_id, band_id, category_id, status. */ public function index(Request $request): JsonResponse { $perPage = (int) $request->get('per_page', 100); $statusParam = $request->get('status'); $isClaimedRequest = $statusParam === 'CLAIMED'; $query = LogResult::query() ->with([ 'evaluationRun.ruleSet:id,sixhr_ranking_mode', 'log', 'band:id,name,order', 'category:id,name,order', 'powerCategory:id,name,order', ]); if ($request->filled('evaluation_run_id')) { $query->where('evaluation_run_id', (int) $request->get('evaluation_run_id')); } if ($request->filled('round_id')) { $roundId = (int) $request->get('round_id'); $query->whereHas('log', function ($q) use ($roundId) { $q->where('round_id', $roundId); }); if (! $request->filled('evaluation_run_id') && $request->filled('result_type')) { $round = Round::find($roundId); $resultType = strtoupper((string) $request->get('result_type')); $selectedRunId = null; if ($round) { if ($resultType === 'FINAL') { $selectedRunId = $round->official_evaluation_run_id; } elseif ($resultType === 'PRELIMINARY') { $selectedRunId = $round->preliminary_evaluation_run_id; } elseif ($resultType === 'TEST') { $selectedRunId = $round->test_evaluation_run_id; } elseif ($resultType === 'AUTO') { $selectedRunId = $round->official_evaluation_run_id ?? $round->preliminary_evaluation_run_id; } } if ($selectedRunId) { $query->where('evaluation_run_id', $selectedRunId); } else { $query->whereRaw('1=0'); } } if (! $request->filled('evaluation_run_id') && $isClaimedRequest) { $latestClaimedRunId = EvaluationRun::where('round_id', $roundId) ->where('rules_version', 'CLAIMED') ->orderByDesc('id') ->value('id'); if ($latestClaimedRunId) { $query->where('evaluation_run_id', $latestClaimedRunId); } } } if ($request->filled('log_id')) { $query->where('log_id', (int) $request->get('log_id')); } if ($request->filled('band_id')) { $query->where('band_id', (int) $request->get('band_id')); } if ($request->filled('category_id')) { $query->where('category_id', (int) $request->get('category_id')); } if ($request->filled('status') && ! $isClaimedRequest) { $query->where('status', $statusParam); } if ($request->boolean('only_ok', false)) { $pcallExpr = "UPPER(REPLACE(TRIM(pcall), ' ', ''))"; $query->whereHas('log', function ($q) use ($pcallExpr) { $q->where(function ($sub) use ($pcallExpr) { $sub->whereRaw("{$pcallExpr} LIKE ?", ['OK%']) ->orWhereRaw("{$pcallExpr} LIKE ?", ['OL%']) ->orWhereRaw("{$pcallExpr} LIKE ?", ['%/OK%']) ->orWhereRaw("{$pcallExpr} LIKE ?", ['%/OL%']); }); }); } // implicitně řadit podle oficiálního skóre $items = $query ->orderByDesc('official_score') ->paginate($perPage); return response()->json($items); } /** * Vytvoření záznamu výsledku logu. * Typicky voláno vyhodnocovačem, ne přímo z UI. */ public function store(Request $request): JsonResponse { $this->authorize('create', LogResult::class); $data = $this->validateData($request); $result = LogResult::create($data); $result->load([ 'evaluationRun.ruleSet:id,sixhr_ranking_mode', 'log', 'band:id,name,order', 'category:id,name,order', 'powerCategory:id,name,order', ]); return response()->json($result, 201); } /** * Detail jednoho výsledku. */ public function show(LogResult $logResult): JsonResponse { $logResult->load([ 'evaluationRun.ruleSet:id,sixhr_ranking_mode', 'log', 'band:id,name,order', 'category:id,name,order', 'powerCategory:id,name,order', ]); return response()->json($logResult); } /** * Aktualizace výsledku (partial). * Typicky pro ruční korekci statutu / poznámky. */ public function update(Request $request, LogResult $logResult): JsonResponse { $this->authorize('update', $logResult); $data = $this->validateData($request, partial: true); $logResult->fill($data); $logResult->save(); $logResult->load([ 'evaluationRun.ruleSet:id,sixhr_ranking_mode', 'log', 'band:id,name,order', 'category:id,name,order', 'powerCategory:id,name,order', ]); return response()->json($logResult); } /** * Smazání výsledku. */ public function destroy(LogResult $logResult): JsonResponse { $this->authorize('delete', $logResult); $logResult->delete(); return response()->json(null, 204); } /** * Validace pro store / update. */ protected function validateData(Request $request, bool $partial = false): array { $required = $partial ? 'sometimes' : 'required'; return $request->validate([ 'evaluation_run_id' => [$required, 'integer', 'exists:evaluation_runs,id'], 'log_id' => [$required, 'integer', 'exists:logs,id'], 'band_id' => ['sometimes', 'nullable', 'integer', 'exists:bands,id'], 'category_id' => ['sometimes', 'nullable', 'integer', 'exists:categories,id'], 'power_category_id' => ['sometimes', 'nullable', 'integer', 'exists:power_categories,id'], 'claimed_qso_count' => ['sometimes', 'nullable', 'integer', 'min:0'], 'claimed_score' => ['sometimes', 'nullable', 'integer', 'min:0'], 'valid_qso_count' => ['sometimes', 'integer', 'min:0'], 'dupe_qso_count' => ['sometimes', 'integer', 'min:0'], 'busted_qso_count' => ['sometimes', 'integer', 'min:0'], 'other_error_qso_count' => ['sometimes', 'integer', 'min:0'], 'total_qso_count' => ['sometimes', 'integer', 'min:0'], 'discarded_qso_count' => ['sometimes', 'integer', 'min:0'], 'discarded_points' => ['sometimes', 'integer'], 'discarded_qso_percent' => ['sometimes', 'numeric', 'min:0'], 'unique_qso_count' => ['sometimes', 'integer', 'min:0'], 'official_score' => ['sometimes', 'integer'], 'penalty_score' => ['sometimes', 'integer'], 'base_score' => ['sometimes', 'integer'], 'multiplier_count' => ['sometimes', 'integer', 'min:0'], 'multiplier_score' => ['sometimes', 'integer'], 'score_per_qso' => ['sometimes', 'numeric', 'min:0'], 'rank_overall' => ['sometimes', 'nullable', 'integer', 'min:1'], 'rank_in_category' => ['sometimes', 'nullable', 'integer', 'min:1'], 'status' => ['sometimes', 'string', 'max:20'], 'status_reason' => ['sometimes', 'nullable', 'string'], ]); } }