evaluationRunId; } public function handle(): void { $run = EvaluationRun::find($this->evaluationRunId); if (! $run) { return; } // Zabraňuje souběžnému přepočtu pro stejné kolo (claimed scoreboard). $lock = EvaluationLock::acquire( key: "evaluation:claimed-ranks:round:{$run->round_id}", run: $run, ttl: 300 ); if (! $lock) { return; } try { // Vynuluje pořadí, aby staré hodnoty neovlivnily nový přepočet. LogResult::where('evaluation_run_id', $run->id) ->update([ 'rank_overall' => null, 'rank_in_category' => null, 'rank_overall_ok' => null, 'rank_in_category_ok' => null, ]); // Načte všechny deklarované výsledky včetně vazeb pro kategorii a výkon. $results = LogResult::with(['log', 'category', 'powerCategory']) ->where('evaluation_run_id', $run->id) ->get(); // Do pořadí vstupují jen logy se statusem OK a kategorií SINGLE/MULTI. $eligible = $results->filter(function (LogResult $r) { return $r->status === 'OK' && $this->getCategoryType($r) !== null; }); // Celkové pořadí: podle pásma + SINGLE/MULTI + 6H/standard. $allOverall = $eligible->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r)); foreach ($allOverall as $items) { $this->applyRanking($items, 'rank_overall'); } // Pořadí výkonových kategorií: pásmo + SINGLE/MULTI + výkon (jen LP/QRP/N) + 6H/standard. $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) { $this->applyRanking($items, 'rank_in_category'); } // Česká podmnožina (OK/OL) pro národní pořadí. $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) { $this->applyRanking($items, 'rank_overall_ok'); } // České pořadí výkonových kategorií: stejné jako power, ale jen OK/OL a 6H/standard. $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) { $this->applyRanking($items, 'rank_in_category_ok'); } } finally { EvaluationLock::release("evaluation:claimed-ranks:round:{$run->round_id}", $run); } } protected function applyRanking(Collection $items, string $rankField): void { // Řazení podle claimed_score (desc), pak QSO (desc), pak log_id (asc) kvůli stabilitě. $sorted = $items->sort(function (LogResult $a, LogResult $b) { $scoreA = $a->claimed_score ?? 0; $scoreB = $b->claimed_score ?? 0; if ($scoreA !== $scoreB) { return $scoreB <=> $scoreA; } $qsoA = $a->claimed_qso_count ?? 0; $qsoB = $b->claimed_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->claimed_score ?? 0; $qso = $result->claimed_qso_count ?? 0; // Shodný výsledek (stejné skóre + QSO) = stejné pořadí. if ($score === $lastScore && $qso === $lastQso) { $rank = $lastRank; } else { $rank = $index + 1; } $result->{$rankField} = $rank; $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; } return $this->getSixHourBucket($r) === '6H' ? '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'; } }