417 lines
14 KiB
PHP
417 lines
14 KiB
PHP
<?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,
|
||
];
|
||
}
|
||
}
|