evaluationRunId; } public function handle(): void { $run = EvaluationRun::find($this->evaluationRunId); if (! $run || $run->isCanceled()) { return; } $ruleSet = EvaluationRuleSet::find($run->rule_set_id); if ($ruleSet && $ruleSet->sixhr_ranking_mode) { $this->sixhrRankingMode = strtoupper((string) $ruleSet->sixhr_ranking_mode); } // Krátký lock brání souběžnému přepočtu pořadí nad stejným kolem. $lock = EvaluationLock::acquire( key: "evaluation:official-ranks:round:{$run->round_id}", run: $run, ttl: 300 ); if (! $lock) { return; } try { LogResult::where('evaluation_run_id', $run->id) ->update([ 'rank_overall' => null, 'rank_in_category' => null, 'rank_overall_ok' => null, 'rank_in_category_ok' => null, ]); $results = LogResult::with(['log', 'category', 'powerCategory']) ->where('evaluation_run_id', $run->id) ->get(); // Do pořadí jdou jen logy ve stavu OK a s rozpoznanou kategorií. $eligible = $results->filter(function (LogResult $r) { return $r->status === 'OK' && $this->getCategoryType($r) !== null; }); $allOverall = $eligible->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r)); foreach ($allOverall as $items) { if (EvaluationRun::isCanceledRun($run->id)) { return; } $this->applyRanking($items, 'rank_overall'); } $allPower = $eligible->filter(fn (LogResult $r) => $this->getPowerClass($r) !== null) ->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getPowerClass($r) . '|' . $this->getSixHourBucket($r)); foreach ($allPower as $items) { if (EvaluationRun::isCanceledRun($run->id)) { return; } $this->applyRanking($items, 'rank_in_category'); } $okOnly = $eligible->filter(fn (LogResult $r) => $this->isOkCall($r)); $okOverall = $okOnly->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r)); foreach ($okOverall as $items) { if (EvaluationRun::isCanceledRun($run->id)) { return; } $this->applyRanking($items, 'rank_overall_ok'); } $okPower = $okOnly->filter(fn (LogResult $r) => $this->getPowerClass($r) !== null) ->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getPowerClass($r) . '|' . $this->getSixHourBucket($r)); foreach ($okPower as $items) { if (EvaluationRun::isCanceledRun($run->id)) { return; } $this->applyRanking($items, 'rank_in_category_ok'); } } finally { EvaluationLock::release("evaluation:official-ranks:round:{$run->round_id}", $run); } } protected function applyRanking(Collection $items, string $rankField): void { // Deterministický sort: skóre -> valid QSO -> log_id. $sorted = $items->sort(function (LogResult $a, LogResult $b) { $scoreA = $a->official_score ?? 0; $scoreB = $b->official_score ?? 0; if ($scoreA !== $scoreB) { return $scoreB <=> $scoreA; } $qsoA = $a->valid_qso_count ?? 0; $qsoB = $b->valid_qso_count ?? 0; if ($qsoA !== $qsoB) { return $qsoB <=> $qsoA; } return $a->log_id <=> $b->log_id; })->values(); $lastScore = null; $lastQso = null; $lastRank = 0; foreach ($sorted as $index => $result) { $score = $result->official_score ?? 0; $qso = $result->valid_qso_count ?? 0; if ($score === $lastScore && $qso === $lastQso) { $rank = $lastRank; } else { $rank = $index + 1; } $result->{$rankField} = $rank; $result->sixhr_ranking_bucket = $this->getCategoryBucket($result); $result->save(); $lastScore = $score; $lastQso = $qso; $lastRank = $rank; } } protected function getCategoryType(LogResult $r): ?string { $name = $r->category?->name; if (! $name) { return null; } $lower = mb_strtolower($name); if (str_contains($lower, 'single')) { return 'SINGLE'; } if (str_contains($lower, 'multi')) { return 'MULTI'; } return null; } protected function getCategoryBucket(LogResult $r): ?string { $type = $this->getCategoryType($r); if ($type === null) { return null; } if ($this->getSixHourBucket($r) !== '6H') { return $type; } return $this->sixhrRankingMode === 'IARU' ? 'ALL' : $type; } protected function getPowerClass(LogResult $r): ?string { $name = $r->powerCategory?->name; if (! $name) { return null; } $upper = mb_strtoupper($name); if (in_array($upper, ['LP', 'QRP', 'N'], true)) { return $upper; } return null; } protected function isOkCall(LogResult $r): bool { $call = $this->normalizeCallsign($r->log?->pcall ?? ''); return Str::startsWith($call, ['OK', 'OL']); } protected function normalizeCallsign(string $call): string { $value = mb_strtoupper(trim($call)); $value = preg_replace('/\s+/', '', $value); return $value ?? ''; } protected function getSixHourBucket(LogResult $r): string { $sixh = $r->sixhr_category; if ($sixh === null) { $sixh = $r->log?->sixhr_category; } return $sixh ? '6H' : 'STD'; } }