Files
vkv/app/Http/Controllers/LogController.php
Zdeněk Burda 41e3ce6f25 Initial commit
2026-01-09 21:26:40 +01:00

345 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers;
use App\Models\Log;
use App\Models\LogQso;
use App\Models\EvaluationRun;
use App\Models\QsoOverride;
use App\Models\QsoResult;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class LogController extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
public function __construct()
{
// zápisové operace jen pro přihlášené
$this->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'],
]);
}
}