middleware('auth:sanctum')->only(['store', 'update', 'destroy']); } /** * Seznam logů – s možností filtrování podle round_id, pcall, processed/accepted. */ public function index(Request $request): JsonResponse { $perPage = (int) $request->get('per_page', 100); $query = Log::query() ->with(['round', 'file']) ->withExists(['logResults as parsed']) ->withExists(['logResults as parsed_claimed' => function ($q) { $q->whereHas('evaluationRun', function ($runQuery) { $runQuery->where('rules_version', 'CLAIMED'); }); }]); if ($request->filled('round_id')) { $query->where('round_id', (int) $request->get('round_id')); } if ($request->filled('pcall')) { $query->where('pcall', $request->get('pcall')); } if ($request->filled('processed')) { $query->where('processed', filter_var($request->get('processed'), FILTER_VALIDATE_BOOL)); } if ($request->filled('accepted')) { $query->where('accepted', filter_var($request->get('accepted'), FILTER_VALIDATE_BOOL)); } $logs = $query ->orderByRaw('parsed_claimed asc, pcall asc') ->paginate($perPage); return response()->json($logs); } /** * Vytvoření logu. * Typicky voláno po úspěšném uploadu / parsování EDI ve službě. * Autorizace přes LogPolicy@create. */ public function store(Request $request): JsonResponse { $this->authorize('create', Log::class); $data = $this->validateData($request); $log = Log::create($data); $log->load(['round', 'file']); return response()->json($log, 201); } /** * Detail jednoho logu včetně vazeb a počtu QSO. */ public function show(Request $request, Log $log): JsonResponse { $includeQsos = $request->boolean('include_qsos', false); $relations = ['round', 'file']; if ($includeQsos) { $relations[] = 'qsos'; } $log->load($relations); return response()->json($log); } /** * QSO tabulka pro log: raw QSO + výsledky vyhodnocení + případné overrides. */ public function qsoTable(Request $request, Log $log): JsonResponse { $evalRunId = $request->filled('evaluation_run_id') ? (int) $request->get('evaluation_run_id') : null; if ($evalRunId) { $run = EvaluationRun::find($evalRunId); if (! $run) { $evalRunId = null; } } if (! $evalRunId) { $run = EvaluationRun::query() ->where('round_id', $log->round_id) ->where('status', 'SUCCEEDED') ->where(function ($q) { $q->whereNull('rules_version') ->orWhere('rules_version', '!=', 'CLAIMED'); }) ->orderByDesc('id') ->first(); $evalRunId = $run?->id; } $qsos = LogQso::query() ->where('log_id', $log->id) ->orderBy('qso_index') ->orderBy('id') ->get([ 'id', 'qso_index', 'time_on', 'dx_call', 'my_rst', 'my_serial', 'dx_rst', 'dx_serial', 'rx_wwl', 'rx_exchange', 'mode_code', 'new_exchange', 'new_wwl', 'new_dxcc', 'duplicate_qso', 'points', ]); $qsoIds = $qsos->pluck('id')->all(); $resultMap = collect(); $overrideMap = collect(); if ($evalRunId && $qsoIds) { $resultMap = QsoResult::query() ->where('evaluation_run_id', $evalRunId) ->whereIn('log_qso_id', $qsoIds) ->get([ 'log_qso_id', 'points', 'penalty_points', 'error_code', 'error_side', 'match_confidence', 'match_type', 'error_flags', 'is_valid', 'is_duplicate', 'is_nil', 'is_busted_call', 'is_busted_rst', 'is_busted_exchange', 'is_time_out_of_window', ]) ->keyBy('log_qso_id'); $overrideMap = QsoOverride::query() ->where('evaluation_run_id', $evalRunId) ->whereIn('log_qso_id', $qsoIds) ->get([ 'id', 'log_qso_id', 'forced_status', 'forced_matched_log_qso_id', 'forced_points', 'forced_penalty', 'reason', ]) ->keyBy('log_qso_id'); } $data = $qsos->map(function (LogQso $qso) use ($resultMap, $overrideMap) { $result = $resultMap->get($qso->id); $override = $overrideMap->get($qso->id); return [ 'id' => $qso->id, 'qso_index' => $qso->qso_index, 'time_on' => $qso->time_on, 'dx_call' => $qso->dx_call, 'my_rst' => $qso->my_rst, 'my_serial' => $qso->my_serial, 'dx_rst' => $qso->dx_rst, 'dx_serial' => $qso->dx_serial, 'rx_wwl' => $qso->rx_wwl, 'rx_exchange' => $qso->rx_exchange, 'mode_code' => $qso->mode_code, 'new_exchange' => $qso->new_exchange, 'new_wwl' => $qso->new_wwl, 'new_dxcc' => $qso->new_dxcc, 'duplicate_qso' => $qso->duplicate_qso, 'points' => $qso->points, 'remarks' => null, 'result' => $result ? [ 'log_qso_id' => $result->log_qso_id, 'points' => $result->points, 'penalty_points' => $result->penalty_points, 'error_code' => $result->error_code, 'error_side' => $result->error_side, 'match_confidence' => $result->match_confidence, 'match_type' => $result->match_type, 'error_flags' => $result->error_flags, 'is_valid' => $result->is_valid, 'is_duplicate' => $result->is_duplicate, 'is_nil' => $result->is_nil, 'is_busted_call' => $result->is_busted_call, 'is_busted_rst' => $result->is_busted_rst, 'is_busted_exchange' => $result->is_busted_exchange, 'is_time_out_of_window' => $result->is_time_out_of_window, ] : null, 'override' => $override ? [ 'id' => $override->id, 'log_qso_id' => $override->log_qso_id, 'forced_status' => $override->forced_status, 'forced_matched_log_qso_id' => $override->forced_matched_log_qso_id, 'forced_points' => $override->forced_points, 'forced_penalty' => $override->forced_penalty, 'reason' => $override->reason, ] : null, ]; }); return response()->json([ 'evaluation_run_id' => $evalRunId, 'data' => $data, ]); } /** * Aktualizace logu (partial update). * Typicky pro ruční úpravu flagů accepted/processed, případně oprav hlavičky. * Autorizace přes LogPolicy@update. */ public function update(Request $request, Log $log): JsonResponse { $this->authorize('update', $log); $data = $this->validateData($request, partial: true); $log->fill($data); $log->save(); $log->load(['round', 'file']); return response()->json($log); } /** * Smazání logu (včetně QSO přes FK ON DELETE CASCADE). * Autorizace přes LogPolicy@delete. */ public function destroy(Log $log): JsonResponse { $this->authorize('delete', $log); // pokud je navázaný soubor, smaž i jeho fyzický obsah a záznam if ($log->file) { if ($log->file->path && Storage::exists($log->file->path)) { Storage::delete($log->file->path); } $log->file->delete(); } $log->delete(); return response()->json(null, 204); } /** * Jednoduchý parser nahraného souboru – aktuálně podporuje EDI. * Pokud jde o EDI, naplní základní pole Logu a uloží raw_header (bez sekce QSORecords). */ public static function parseUploadedFile(Log $log, string $path): void { app(\App\Services\Evaluation\EdiParserService::class)->parseLogFile($log, $path); } /** * Validace vstupu pro store / update. * EDI parser bude typicky volat store/update s již připravenými daty. */ protected function validateData(Request $request, bool $partial = false): array { $required = $partial ? 'sometimes' : 'required'; return $request->validate([ 'round_id' => [$required, 'integer', 'exists:rounds,id'], 'file_id' => ['sometimes', 'nullable', 'integer', 'exists:files,id'], 'accepted' => ['sometimes', 'boolean'], 'processed' => ['sometimes', 'boolean'], 'ip_address' => ['sometimes', 'nullable', 'string', 'max:45'], 'tname' => ['sometimes', 'nullable', 'string', 'max:100'], 'tdate' => ['sometimes', 'nullable', 'string', 'max:50'], 'pcall' => ['sometimes', 'nullable', 'string', 'max:20'], 'pwwlo' => ['sometimes', 'nullable', 'string', 'max:6'], 'pexch' => ['sometimes', 'nullable', 'string', 'max:10'], 'psect' => ['sometimes', 'nullable', 'string', 'max:10'], 'pband' => ['sometimes', 'nullable', 'string', 'max:10'], 'pclub' => ['sometimes', 'nullable', 'string', 'max:50'], 'country_name' => ['sometimes', 'nullable', 'string', 'max:150'], 'operator_name' => ['sometimes', 'nullable', 'string', 'max:100'], 'locator' => ['sometimes', 'nullable', 'string', 'max:6'], 'power_watt' => ['sometimes', 'nullable', 'numeric', 'min:0'], 'power_category' => ['sometimes', 'nullable', 'string', 'max:3'], 'power_category_id' => ['sometimes', 'nullable', 'integer', 'exists:power_categories,id'], 'sixhr_category' => ['sometimes', 'nullable', 'boolean'], 'claimed_qso_count' => ['sometimes', 'nullable', 'integer', 'min:0'], 'claimed_score' => ['sometimes', 'nullable', 'integer', 'min:0'], 'claimed_wwl' => ['sometimes', 'nullable', 'string', 'max:50'], 'claimed_dxcc' => ['sometimes', 'nullable', 'string', 'max:50'], 'remarks' => ['sometimes', 'nullable', 'string', 'max:500'], 'remarks_eval' => ['sometimes', 'nullable', 'string', 'max:500'], 'raw_header' => ['sometimes', 'nullable', 'string'], ]); } }