Initial commit

This commit is contained in:
Zdeněk Burda
2026-01-09 21:26:40 +01:00
parent e83aec6dca
commit 41e3ce6f25
404 changed files with 61250 additions and 28 deletions

View File

@@ -0,0 +1,416 @@
<?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,
];
}
}