393 lines
14 KiB
PHP
393 lines
14 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Models\Round;
|
||
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;
|
||
use App\Jobs\RebuildClaimedLogResultsJob;
|
||
use App\Services\Evaluation\ClaimedRunResolver;
|
||
use App\Models\EvaluationRun;
|
||
use App\Jobs\StartEvaluationRunJob;
|
||
use App\Models\Contest;
|
||
use App\Models\LogOverride;
|
||
use App\Models\QsoOverride;
|
||
|
||
class RoundController 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 kol (rounds) – stránkovaně.
|
||
*/
|
||
public function index(Request $request): JsonResponse
|
||
{
|
||
$perPage = (int) $request->get('per_page', 100);
|
||
$contestId = $request->query('contest_id');
|
||
$onlyActive = (bool) $request->query('only_active', false);
|
||
$includeTests = filter_var($request->query('include_tests', true), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||
if ($includeTests === null) {
|
||
$includeTests = true;
|
||
}
|
||
|
||
$items = Round::query()
|
||
->with([
|
||
'contest',
|
||
'bands',
|
||
'categories',
|
||
'powerCategories',
|
||
'ruleSet',
|
||
])
|
||
->when($contestId, fn ($q) => $q->where('contest_id', $contestId))
|
||
->when($onlyActive, fn ($q) => $q->where('is_active', true))
|
||
->when(! $includeTests, fn ($q) => $q->where('is_test', false))
|
||
->orderByDesc('start_time')
|
||
->orderByDesc('end_time')
|
||
->paginate($perPage);
|
||
|
||
return response()->json($items);
|
||
}
|
||
|
||
/**
|
||
* Vytvoření nového kola.
|
||
* Autorizace přes RoundPolicy@create.
|
||
*/
|
||
public function store(Request $request): JsonResponse
|
||
{
|
||
$this->authorize('create', Round::class);
|
||
|
||
$data = $this->validateData($request);
|
||
$relations = $this->validateRelations($request);
|
||
|
||
if (! array_key_exists('rule_set_id', $data) || $data['rule_set_id'] === null) {
|
||
$contestRuleSetId = Contest::where('id', $data['contest_id'])->value('rule_set_id');
|
||
$data['rule_set_id'] = $contestRuleSetId;
|
||
}
|
||
|
||
$round = Round::create($data);
|
||
|
||
$this->syncRelations($round, $relations);
|
||
|
||
$round->load([
|
||
'contest',
|
||
'bands',
|
||
'categories',
|
||
'powerCategories',
|
||
'ruleSet',
|
||
]);
|
||
|
||
return response()->json($round, 201);
|
||
}
|
||
|
||
/**
|
||
* Detail kola.
|
||
*/
|
||
public function show(Round $round): JsonResponse
|
||
{
|
||
$round->load([
|
||
'contest',
|
||
'bands',
|
||
'categories',
|
||
'powerCategories',
|
||
'ruleSet',
|
||
]);
|
||
|
||
return response()->json($round);
|
||
}
|
||
|
||
/**
|
||
* Aktualizace kola (partial update).
|
||
* Autorizace přes RoundPolicy@update.
|
||
*/
|
||
public function update(Request $request, Round $round): JsonResponse
|
||
{
|
||
$this->authorize('update', $round);
|
||
|
||
$data = $this->validateData($request, partial: true);
|
||
$relations = $this->validateRelations($request);
|
||
|
||
$round->fill($data);
|
||
$round->save();
|
||
|
||
$this->syncRelations($round, $relations);
|
||
|
||
$round->load([
|
||
'contest',
|
||
'bands',
|
||
'categories',
|
||
'powerCategories',
|
||
'ruleSet',
|
||
]);
|
||
|
||
return response()->json($round);
|
||
}
|
||
|
||
/**
|
||
* Smazání kola.
|
||
* Autorizace přes RoundPolicy@delete.
|
||
*/
|
||
public function destroy(Round $round): JsonResponse
|
||
{
|
||
$this->authorize('delete', $round);
|
||
|
||
$round->delete();
|
||
|
||
return response()->json(null, 204);
|
||
}
|
||
|
||
/**
|
||
* Ručně spustí rebuild deklarovaných výsledků pro kolo.
|
||
*/
|
||
public function recalculateClaimed(Request $request, Round $round): JsonResponse
|
||
{
|
||
$this->authorize('update', $round);
|
||
|
||
$run = ClaimedRunResolver::createNewForRound($round->id, auth()->id());
|
||
RebuildClaimedLogResultsJob::dispatch($run->id)->onQueue('evaluation');
|
||
|
||
return response()->json([
|
||
'status' => 'queued',
|
||
'message' => 'Přepočet deklarovaných výsledků byl spuštěn.',
|
||
], 202);
|
||
}
|
||
|
||
/**
|
||
* Spustí kompletní vyhodnocovací pipeline pro nové EvaluationRun.
|
||
*/
|
||
public function startEvaluation(Request $request, Round $round): JsonResponse
|
||
{
|
||
$this->authorize('update', $round);
|
||
|
||
$data = $request->validate([
|
||
'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'],
|
||
'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST,CLAIMED'],
|
||
'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'],
|
||
'name' => ['sometimes', 'nullable', 'string', 'max:100'],
|
||
'is_official' => ['sometimes', 'boolean'],
|
||
'scope' => ['sometimes', 'array'],
|
||
'scope.band_ids' => ['sometimes', 'array'],
|
||
'scope.band_ids.*' => ['integer', 'exists:bands,id'],
|
||
'scope.category_ids' => ['sometimes', 'array'],
|
||
'scope.category_ids.*' => ['integer', 'exists:categories,id'],
|
||
'scope.power_category_ids' => ['sometimes', 'array'],
|
||
'scope.power_category_ids.*' => ['integer', 'exists:power_categories,id'],
|
||
]);
|
||
|
||
$rulesVersion = $data['rules_version'] ?? 'OFFICIAL';
|
||
$resultType = $data['result_type']
|
||
?? ($rulesVersion === 'TEST' ? 'TEST' : 'PRELIMINARY');
|
||
|
||
$run = EvaluationRun::create([
|
||
'round_id' => $round->id,
|
||
'rule_set_id' => $data['rule_set_id'] ?? $round->rule_set_id,
|
||
'rules_version' => $rulesVersion,
|
||
'result_type' => $rulesVersion === 'CLAIMED' ? null : $resultType,
|
||
'name' => $data['name'] ?? 'Vyhodnocení',
|
||
'is_official' => $data['is_official'] ?? ($resultType === 'FINAL'),
|
||
'scope' => $data['scope'] ?? null,
|
||
'status' => 'PENDING',
|
||
'created_by_user_id' => auth()->id(),
|
||
]);
|
||
|
||
StartEvaluationRunJob::dispatch($run->id)->onQueue('evaluation');
|
||
|
||
$run->load(['round']);
|
||
|
||
return response()->json($run, 201);
|
||
}
|
||
|
||
/**
|
||
* Spustí nový EvaluationRun jako re-run s převzetím override z posledního běhu.
|
||
*/
|
||
public function startEvaluationIncremental(Request $request, Round $round): JsonResponse
|
||
{
|
||
$this->authorize('update', $round);
|
||
|
||
$data = $request->validate([
|
||
'source_run_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_runs,id'],
|
||
'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'],
|
||
'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST'],
|
||
'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'],
|
||
'name' => ['sometimes', 'nullable', 'string', 'max:100'],
|
||
'scope' => ['sometimes', 'array'],
|
||
'scope.band_ids' => ['sometimes', 'array'],
|
||
'scope.band_ids.*' => ['integer', 'exists:bands,id'],
|
||
'scope.category_ids' => ['sometimes', 'array'],
|
||
'scope.category_ids.*' => ['integer', 'exists:categories,id'],
|
||
'scope.power_category_ids' => ['sometimes', 'array'],
|
||
'scope.power_category_ids.*' => ['integer', 'exists:power_categories,id'],
|
||
]);
|
||
|
||
$sourceRun = null;
|
||
if (! empty($data['source_run_id'])) {
|
||
$sourceRun = EvaluationRun::where('round_id', $round->id)
|
||
->where('id', (int) $data['source_run_id'])
|
||
->first();
|
||
}
|
||
if (! $sourceRun) {
|
||
$sourceRun = EvaluationRun::where('round_id', $round->id)
|
||
->where('rules_version', '!=', 'CLAIMED')
|
||
->orderByDesc('id')
|
||
->first();
|
||
}
|
||
|
||
$rulesVersion = $data['rules_version']
|
||
?? ($sourceRun?->rules_version ?? 'OFFICIAL');
|
||
$resultType = $data['result_type']
|
||
?? ($rulesVersion === 'TEST' ? 'TEST' : 'PRELIMINARY');
|
||
|
||
$run = EvaluationRun::create([
|
||
'round_id' => $round->id,
|
||
'rule_set_id' => $data['rule_set_id'] ?? ($sourceRun?->rule_set_id ?? $round->rule_set_id),
|
||
'rules_version' => $rulesVersion,
|
||
'result_type' => $resultType,
|
||
'name' => $data['name'] ?? 'Vyhodnocení (re-run)',
|
||
'is_official' => $resultType === 'FINAL',
|
||
'scope' => $data['scope'] ?? ($sourceRun?->scope ?? null),
|
||
'status' => 'PENDING',
|
||
'created_by_user_id' => auth()->id(),
|
||
]);
|
||
|
||
if ($sourceRun) {
|
||
$this->cloneOverrides($sourceRun->id, $run->id, auth()->id());
|
||
}
|
||
|
||
StartEvaluationRunJob::dispatch($run->id)->onQueue('evaluation');
|
||
|
||
$run->load(['round']);
|
||
|
||
return response()->json($run, 201);
|
||
}
|
||
|
||
protected function cloneOverrides(int $sourceRunId, int $targetRunId, ?int $userId = null): void
|
||
{
|
||
$logOverrides = LogOverride::where('evaluation_run_id', $sourceRunId)->get();
|
||
if ($logOverrides->isNotEmpty()) {
|
||
$rows = $logOverrides->map(function ($override) use ($targetRunId, $userId) {
|
||
return [
|
||
'evaluation_run_id' => $targetRunId,
|
||
'log_id' => $override->log_id,
|
||
'forced_log_status' => $override->forced_log_status,
|
||
'forced_band_id' => $override->forced_band_id,
|
||
'forced_category_id' => $override->forced_category_id,
|
||
'forced_power_category_id' => $override->forced_power_category_id,
|
||
'forced_sixhr_category' => $override->forced_sixhr_category,
|
||
'forced_power_w' => $override->forced_power_w,
|
||
'reason' => $override->reason,
|
||
'context' => $this->encodeContext($override->context),
|
||
'created_by_user_id' => $override->created_by_user_id ?? $userId,
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
];
|
||
})->all();
|
||
LogOverride::insert($rows);
|
||
}
|
||
|
||
$qsoOverrides = QsoOverride::where('evaluation_run_id', $sourceRunId)->get();
|
||
if ($qsoOverrides->isNotEmpty()) {
|
||
$rows = $qsoOverrides->map(function ($override) use ($targetRunId, $userId) {
|
||
return [
|
||
'evaluation_run_id' => $targetRunId,
|
||
'log_qso_id' => $override->log_qso_id,
|
||
'forced_matched_log_qso_id' => $override->forced_matched_log_qso_id,
|
||
'forced_status' => $override->forced_status,
|
||
'forced_points' => $override->forced_points,
|
||
'forced_penalty' => $override->forced_penalty,
|
||
'reason' => $override->reason,
|
||
'context' => $this->encodeContext($override->context),
|
||
'created_by_user_id' => $override->created_by_user_id ?? $userId,
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
];
|
||
})->all();
|
||
QsoOverride::insert($rows);
|
||
}
|
||
}
|
||
|
||
protected function encodeContext(mixed $context): ?string
|
||
{
|
||
if ($context === null) {
|
||
return null;
|
||
}
|
||
$encoded = json_encode($context);
|
||
return $encoded === false ? null : $encoded;
|
||
}
|
||
|
||
/**
|
||
* Validace vstupu pro store / update.
|
||
*/
|
||
protected function validateData(Request $request, bool $partial = false): array
|
||
{
|
||
$required = $partial ? 'sometimes' : 'required';
|
||
|
||
return $request->validate([
|
||
'contest_id' => [$required, 'integer', 'exists:contests,id'],
|
||
|
||
// name/description – pokud používáš překlady jako u Contest:
|
||
'name' => [$required, 'array'],
|
||
'name.*' => ['string', 'max:255'],
|
||
'description' => ['sometimes', 'nullable', 'array'],
|
||
'description.*' => ['string'],
|
||
|
||
'start_time' => [$required, 'date'],
|
||
'end_time' => [$required, 'date', 'after:start_time'],
|
||
'logs_deadline' => [$required, 'date'],
|
||
|
||
'is_active' => ['sometimes', 'boolean'],
|
||
'is_test' => ['sometimes', 'boolean'],
|
||
'is_sixhr' => ['sometimes', 'boolean'],
|
||
|
||
'first_check' => ['sometimes', 'nullable', 'date'],
|
||
'second_check' => ['sometimes', 'nullable', 'date'],
|
||
'unique_qso_check' => ['sometimes', 'nullable', 'date'],
|
||
'third_check' => ['sometimes', 'nullable', 'date'],
|
||
'fourth_check' => ['sometimes', 'nullable', 'date'],
|
||
'prelimitary_results'=> ['sometimes', 'nullable', 'date'],
|
||
'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Validace ID navázaných entit pro belongsToMany vztahy.
|
||
*/
|
||
protected function validateRelations(Request $request): array
|
||
{
|
||
return $request->validate([
|
||
'band_ids' => ['sometimes', 'array'],
|
||
'band_ids.*' => ['integer', 'exists:bands,id'],
|
||
|
||
'category_ids' => ['sometimes', 'array'],
|
||
'category_ids.*' => ['integer', 'exists:categories,id'],
|
||
|
||
'power_category_ids' => ['sometimes', 'array'],
|
||
'power_category_ids.*' => ['integer', 'exists:power_categories,id'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Sync vazeb pro belongsToMany vztahy.
|
||
*/
|
||
protected function syncRelations(Round $round, array $relations): void
|
||
{
|
||
if (array_key_exists('band_ids', $relations)) {
|
||
$round->bands()->sync($relations['band_ids']);
|
||
}
|
||
|
||
if (array_key_exists('category_ids', $relations)) {
|
||
$round->categories()->sync($relations['category_ids']);
|
||
}
|
||
|
||
if (array_key_exists('power_category_ids', $relations)) {
|
||
$round->powerCategories()->sync($relations['power_category_ids']);
|
||
}
|
||
}
|
||
}
|