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

417 lines
14 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\Band;
use App\Models\EdiBand;
use App\Models\EdiCategory;
use App\Models\LogOverride;
use App\Models\LogResult;
use App\Models\Round;
use App\Models\EvaluationRun;
use App\Jobs\RecalculateOfficialRanksJob;
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 LogOverrideController 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 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,
];
}
}