You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
128 lines
3.8 KiB
128 lines
3.8 KiB
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Models\Application;
|
|
use App\Models\ReviewerScope;
|
|
|
|
/**
|
|
* 评审进程按赛区统计:评委绑定某赛区后可评该赛区全部赛道,
|
|
* 不再按 reviewer_scopes 行数(赛道条数)做分母。
|
|
*/
|
|
final class ReviewAssignmentIndex
|
|
{
|
|
/**
|
|
* @param array<int, true> $globalReviewerIds
|
|
* @param array<string, array<int, true>> $reviewerIdsByLocation
|
|
* @param array<int, array{id: int, name: string}> $reviewers
|
|
*/
|
|
public function __construct(
|
|
private readonly array $globalReviewerIds,
|
|
private readonly array $reviewerIdsByLocation,
|
|
private readonly array $reviewers,
|
|
) {}
|
|
|
|
public static function forCompetition(int $competitionId): self
|
|
{
|
|
$global = [];
|
|
$byLocation = [];
|
|
$reviewers = [];
|
|
|
|
ReviewerScope::query()
|
|
->with('reviewer:id,name')
|
|
->where('competition_id', $competitionId)
|
|
->orderBy('id')
|
|
->get()
|
|
->each(function (ReviewerScope $scope) use (&$global, &$byLocation, &$reviewers): void {
|
|
$reviewerId = (int) $scope->reviewer_id;
|
|
if ($reviewerId <= 0) {
|
|
return;
|
|
}
|
|
|
|
$name = trim((string) ($scope->reviewer?->name ?? ''));
|
|
if ($name === '') {
|
|
$name = '评委#'.$reviewerId;
|
|
}
|
|
$reviewers[$reviewerId] = ['id' => $reviewerId, 'name' => $name];
|
|
|
|
$raw = trim((string) ($scope->event_location ?? ''));
|
|
if ($raw === '') {
|
|
$global[$reviewerId] = true;
|
|
|
|
return;
|
|
}
|
|
|
|
$normalized = SignupOptionCatalog::normalize('event_location', $raw) ?: $raw;
|
|
$byLocation[$normalized][$reviewerId] = true;
|
|
});
|
|
|
|
return new self($global, $byLocation, $reviewers);
|
|
}
|
|
|
|
/**
|
|
* @return list<int>
|
|
*/
|
|
public function reviewerIdsFor(Application $app): array
|
|
{
|
|
$ids = $this->globalReviewerIds;
|
|
$raw = trim((string) ($app->event_location ?? ''));
|
|
if ($raw === '') {
|
|
if ($ids === []) {
|
|
foreach ($this->reviewers as $id => $_) {
|
|
$ids[$id] = true;
|
|
}
|
|
}
|
|
} else {
|
|
$normalized = SignupOptionCatalog::normalize('event_location', $raw) ?: $raw;
|
|
foreach ($this->reviewerIdsByLocation[$normalized] ?? [] as $id => $_) {
|
|
$ids[$id] = true;
|
|
}
|
|
}
|
|
|
|
$out = array_map('intval', array_keys($ids));
|
|
sort($out);
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{id: int, name: string}>
|
|
*/
|
|
public function reviewersFor(Application $app): array
|
|
{
|
|
$out = [];
|
|
foreach ($this->reviewerIdsFor($app) as $id) {
|
|
$out[] = $this->reviewers[$id] ?? ['id' => $id, 'name' => '评委#'.$id];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param list<int> $scoredReviewerIds
|
|
* @return array{required: int, completed: int, fully_completed: bool, progress: string}
|
|
*/
|
|
public function progress(Application $app, array $scoredReviewerIds = []): array
|
|
{
|
|
$assigned = $this->reviewerIdsFor($app);
|
|
$required = count($assigned);
|
|
$scoredSet = [];
|
|
foreach ($scoredReviewerIds as $id) {
|
|
$scoredSet[(int) $id] = true;
|
|
}
|
|
$completed = 0;
|
|
foreach ($assigned as $id) {
|
|
if (isset($scoredSet[$id])) {
|
|
$completed++;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'required' => $required,
|
|
'completed' => $completed,
|
|
'fully_completed' => $required > 0 && $completed >= $required,
|
|
'progress' => $required > 0 ? ($completed.'/'.$required) : '-',
|
|
];
|
|
}
|
|
}
|