middleware('auth:sanctum')->only(['store', 'update', 'destroy']); } /** * Seznam override záznamů – lze filtrovat podle evaluation_run_id/log_id. */ public function index(Request $request): JsonResponse { $perPage = (int) $request->get('per_page', 100); $query = LogOverride::query() ->with([ 'evaluationRun', 'log', 'forcedBand', 'forcedCategory', 'forcedPowerCategory', 'createdByUser', ]); if ($request->filled('evaluation_run_id')) { $query->where('evaluation_run_id', (int) $request->get('evaluation_run_id')); } if ($request->filled('log_id')) { $query->where('log_id', (int) $request->get('log_id')); } $items = $query->orderByDesc('id')->paginate($perPage); return response()->json($items); } /** * Vytvoření override záznamu. */ public function store(Request $request): JsonResponse { $this->authorize('create', LogOverride::class); $data = $this->validateData($request); $data['context'] = $this->mergeOriginalContext($data['context'] ?? null, $data['evaluation_run_id'], $data['log_id']); if (! isset($data['created_by_user_id']) && $request->user()) { $data['created_by_user_id'] = $request->user()->id; } $item = LogOverride::create($data); $this->applyOverrideToLogResult($item); $statusChanged = array_key_exists('forced_log_status', $data); if ($this->shouldRecalculateRanks($item->evaluation_run_id, $statusChanged)) { RecalculateOfficialRanksJob::dispatch($item->evaluation_run_id)->onQueue('evaluation'); } $item->load([ 'evaluationRun', 'log', 'forcedBand', 'forcedCategory', 'forcedPowerCategory', 'createdByUser', ]); return response()->json($item, 201); } /** * Detail override záznamu. */ public function show(LogOverride $logOverride): JsonResponse { $logOverride->load([ 'evaluationRun', 'log', 'forcedBand', 'forcedCategory', 'forcedPowerCategory', 'createdByUser', ]); return response()->json($logOverride); } /** * Aktualizace override záznamu. */ public function update(Request $request, LogOverride $logOverride): JsonResponse { $this->authorize('update', $logOverride); $data = $this->validateData($request, partial: true); if (! array_key_exists('context', $data)) { $data['context'] = $this->mergeOriginalContext($logOverride->context, $logOverride->evaluation_run_id, $logOverride->log_id); } else { $data['context'] = $this->mergeOriginalContext($data['context'], $logOverride->evaluation_run_id, $logOverride->log_id); } $statusChanged = array_key_exists('forced_log_status', $data); $logOverride->fill($data); $logOverride->save(); $this->applyOverrideToLogResult($logOverride); if ($this->shouldRecalculateRanks($logOverride->evaluation_run_id, $statusChanged)) { RecalculateOfficialRanksJob::dispatch($logOverride->evaluation_run_id)->onQueue('evaluation'); } $logOverride->load([ 'evaluationRun', 'log', 'forcedBand', 'forcedCategory', 'forcedPowerCategory', 'createdByUser', ]); return response()->json($logOverride); } /** * Smazání override záznamu. */ public function destroy(LogOverride $logOverride): JsonResponse { $this->authorize('delete', $logOverride); $log = $logOverride->log; $round = $log ? Round::with(['bands', 'categories', 'powerCategories'])->find($log->round_id) : null; $bandId = $log && $round ? $this->resolveBandId($log, $round) : null; $categoryId = $log && $round ? $this->resolveCategoryId($log, $round) : null; $powerCategoryId = $log?->power_category_id; $logOverride->delete(); LogResult::where('evaluation_run_id', $logOverride->evaluation_run_id) ->where('log_id', $logOverride->log_id) ->update([ 'status' => 'OK', 'band_id' => $bandId, 'category_id' => $categoryId, 'power_category_id' => $powerCategoryId, 'sixhr_category' => $log?->sixhr_category, ]); $statusChanged = $logOverride->forced_log_status && $logOverride->forced_log_status !== 'AUTO'; if ($this->shouldRecalculateRanks($logOverride->evaluation_run_id, $statusChanged)) { RecalculateOfficialRanksJob::dispatch($logOverride->evaluation_run_id)->onQueue('evaluation'); } return response()->json(null, 204); } protected function resolveCategoryId(\App\Models\Log $log, Round $round): ?int { $value = $log->psect; if (! $value) { return null; } $ediCat = EdiCategory::whereRaw('LOWER(value) = ?', [mb_strtolower(trim($value))])->first(); if (! $ediCat) { $ediCat = $this->matchEdiCategoryByRegex($value); } if (! $ediCat) { return null; } $mappedCategoryId = $ediCat->categories()->value('categories.id'); if (! $mappedCategoryId) { return null; } if ($round->categories()->count() === 0) { return $mappedCategoryId; } return $round->categories()->where('categories.id', $mappedCategoryId)->exists() ? $mappedCategoryId : null; } protected function matchEdiCategoryByRegex(string $value): ?EdiCategory { $candidates = EdiCategory::whereNotNull('regex_pattern')->get(); foreach ($candidates as $candidate) { $pattern = $candidate->regex_pattern; if (! $pattern) { continue; } $delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i'; set_error_handler(function () { }); $matched = @preg_match($delimited, $value) === 1; restore_error_handler(); if ($matched) { return $candidate; } } return null; } protected function resolveBandId(\App\Models\Log $log, Round $round): ?int { if (! $log->pband) { return null; } $pbandVal = mb_strtolower(trim($log->pband)); $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); if ($ediBand) { $mappedBandId = $ediBand->bands()->value('bands.id'); if (! $mappedBandId) { return null; } if ($round->bands()->count() === 0) { return $mappedBandId; } return $round->bands()->where('bands.id', $mappedBandId)->exists() ? $mappedBandId : null; } $num = is_numeric($pbandVal) ? (float) $pbandVal : null; if ($num === null && $log->pband) { if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $log->pband, $m)) { $num = (float) str_replace(',', '.', $m[1]); } } if ($num === null) { return null; } $bandMatch = Band::where('edi_band_begin', '<=', $num) ->where('edi_band_end', '>=', $num) ->first(); if (! $bandMatch) { return null; } if ($round->bands()->count() === 0) { return $bandMatch->id; } return $round->bands()->where('bands.id', $bandMatch->id)->exists() ? $bandMatch->id : null; } protected function shouldRecalculateRanks(int $evaluationRunId, bool $statusChanged): bool { $run = EvaluationRun::find($evaluationRunId); if (! $run) { return false; } if ($run->status === 'SUCCEEDED') { return true; } // Ve WAITING_REVIEW_SCORE řešíme jen změny statutu (DQ/IGNORED/OK/CHECK), // aby se pořadí hned přepočítalo bez ručního pokračování pipeline. return $run->status === 'WAITING_REVIEW_SCORE' && $statusChanged; } 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'], 'forced_log_status' => ['sometimes', 'string', 'in:AUTO,OK,CHECK,DQ,IGNORED'], 'forced_band_id' => ['sometimes', 'nullable', 'integer', 'exists:bands,id'], 'forced_category_id' => ['sometimes', 'nullable', 'integer', 'exists:categories,id'], 'forced_power_category_id' => ['sometimes', 'nullable', 'integer', 'exists:power_categories,id'], 'forced_sixhr_category' => ['sometimes', 'nullable', 'boolean'], 'forced_power_w' => ['sometimes', 'nullable', 'integer', 'min:0'], 'reason' => ['sometimes', 'nullable', 'string', 'max:500'], 'context' => ['sometimes', 'nullable', 'array'], 'created_by_user_id' => ['sometimes', 'nullable', 'integer', 'exists:users,id'], ]); } protected function applyOverrideToLogResult(LogOverride $override): void { $data = []; if ($override->forced_log_status && $override->forced_log_status !== 'AUTO') { $data['status'] = $override->forced_log_status; if (in_array($override->forced_log_status, ['DQ', 'IGNORED', 'CHECK'], true)) { $data['rank_overall'] = null; $data['rank_in_category'] = null; $data['rank_overall_ok'] = null; $data['rank_in_category_ok'] = null; $data['status_reason'] = null; $data['official_score'] = 0; $data['penalty_score'] = 0; $data['base_score'] = 0; $data['multiplier_count'] = 0; $data['multiplier_score'] = 0; $data['valid_qso_count'] = 0; $data['dupe_qso_count'] = 0; $data['busted_qso_count'] = 0; $data['other_error_qso_count'] = 0; } } if ($override->forced_band_id !== null) { $data['band_id'] = $override->forced_band_id; } if ($override->forced_category_id !== null) { $data['category_id'] = $override->forced_category_id; } if ($override->forced_power_category_id !== null) { $data['power_category_id'] = $override->forced_power_category_id; } if ($override->forced_sixhr_category !== null) { $data['sixhr_category'] = $override->forced_sixhr_category; } if (! $data) { $this->resetLogResultToSource($override); return; } LogResult::where('evaluation_run_id', $override->evaluation_run_id) ->where('log_id', $override->log_id) ->update($data); } protected function resetLogResultToSource(LogOverride $override): void { $log = $override->log; if (! $log) { return; } $round = Round::with(['bands', 'categories', 'powerCategories'])->find($log->round_id); if (! $round) { return; } $bandId = $this->resolveBandId($log, $round); $categoryId = $this->resolveCategoryId($log, $round); $powerCategoryId = $log->power_category_id; LogResult::where('evaluation_run_id', $override->evaluation_run_id) ->where('log_id', $override->log_id) ->update([ 'status' => 'OK', 'status_reason' => null, 'band_id' => $bandId, 'category_id' => $categoryId, 'power_category_id' => $powerCategoryId, 'sixhr_category' => $log->sixhr_category, ]); } protected function mergeOriginalContext(?array $context, int $evaluationRunId, int $logId): array { $context = $context ?? []; if (isset($context['original']) && is_array($context['original'])) { return $context; } $context['original'] = $this->snapshotLogResult($evaluationRunId, $logId); return $context; } protected function snapshotLogResult(int $evaluationRunId, int $logId): array { $result = LogResult::where('evaluation_run_id', $evaluationRunId) ->where('log_id', $logId) ->first(); if (! $result) { return []; } return [ 'status' => $result->status, 'band_id' => $result->band_id, 'category_id' => $result->category_id, 'power_category_id' => $result->power_category_id, 'sixhr_category' => $result->sixhr_category, ]; } }