Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ExpertWorkspace {
requiredEvaluators: number;
managedEvaluation: boolean;
submittedProjects: number;
projectsMissingExperts?: number;
missingAssignments: number;
readyProjects: number;
experts: ExpertRow[];
Expand Down Expand Up @@ -49,6 +50,8 @@ export interface AllocationPlan {
export interface QuotaPreview {
revision: string;
requiredEvaluators: number;
submittedProjects?: number;
projectsMissingExperts?: number;
readyBefore: number;
readyAfter: number;
missingAssignments: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface ProgramAnalyticsOverview {
evaluationStatus: {
mode: ProgramEvaluationMode;
maxEvaluationsPerProject: number | null;
requiredEvaluators?: number | null;
assignments: {
total: number;
pending: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,11 @@ <h2 class="analytics-card__title">Статус оценивания</h2>
</div>
<app-tooltip
[text]="
overview.evaluationStatus.mode === 'open'
? 'Проект считается оценённым после первой оценки эксперта.'
: 'Статус проекта определяется выполнением назначений экспертов.'
overview.evaluationStatus.requiredEvaluators != null
? 'Статус зависит от числа полных оценок. Нужное число задаётся в разделе «Эксперты».'
: overview.evaluationStatus.mode === 'open'
? 'Проект считается оценённым после первой оценки эксперта.'
: 'Статус проекта определяется выполнением назначений экспертов.'
"
[isVisible]="isTooltipVisible('evaluation-status')"
[useOverlay]="true"
Expand All @@ -264,14 +266,28 @@ <h2 class="analytics-card__title">Статус оценивания</h2>
categoryLabel="Показатель"
definitionsLabel="Данные и определения показателей"
/>
@if (overview.evaluationStatus.maxEvaluationsPerProject !== null) {
@if (
overview.evaluationStatus.requiredEvaluators != null ||
overview.evaluationStatus.maxEvaluationsPerProject != null
) {
<div class="evaluation__limit" data-testid="evaluation-limit">
<span>
<span class="text-body-10">Максимум экспертов на проект:</span>
<strong>{{ overview.evaluationStatus.maxEvaluationsPerProject }}</strong>
<span class="text-body-10">{{
overview.evaluationStatus.requiredEvaluators != null
? "Оценок для каждого проекта:"
: "Максимум экспертов на проект:"
}}</span>
<strong>{{
overview.evaluationStatus.requiredEvaluators ??
overview.evaluationStatus.maxEvaluationsPerProject
}}</strong>
</span>
<app-tooltip
text="Максимальное количество экспертов, которые могут оценить один проект."
[text]="
overview.evaluationStatus.requiredEvaluators != null
? 'Проект полностью оценён, когда это число разных экспертов сохранили оценки по всем критериям. Настраивается в разделе «Эксперты».'
: 'Максимальное количество экспертов, которые могут оценить один проект.'
"
[isVisible]="isTooltipVisible('evaluation-limit')"
[useOverlay]="true"
(click)="toggleTooltip('evaluation-limit')"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,19 @@ describe("ProgramAnalyticsComponent", () => {
).toHaveLength(2);
});

it("показывает обязательное число оценок вместо прежнего максимума в управляемом режиме", () => {
const model = overview();
model.evaluationStatus.requiredEvaluators = 1;
model.evaluationStatus.maxEvaluationsPerProject = 3;
data.set(model);
const fixture = TestBed.createComponent(ProgramAnalyticsComponent);
fixture.detectChanges();
const limit = fixture.nativeElement.querySelector('[data-testid="evaluation-limit"]');
expect(limit.textContent).toContain("Оценок для каждого проекта:");
expect(limit.querySelector("strong").textContent.trim()).toBe("1");
expect(limit.textContent).not.toContain("Максимум");
});

it("не выдаёт отсутствие новой метрики посещения за ноль", () => {
const model = overview();
delete model.participantFunnel.visitedParticipants;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ export class ProgramAnalyticsComponent implements OnInit {
"evaluation-partial",
"Частично оценено",
status.projects.partiallyEvaluated,
"Хотя бы один назначенный эксперт полностью оценил проект, но не все назначенные эксперты завершили оценивание.",
status.requiredEvaluators != null
? "Есть полная оценка хотя бы одного эксперта, но нужное число оценок ещё не получено."
: "Хотя бы один назначенный эксперт полностью оценил проект, но не все назначенные эксперты завершили оценивание.",
),
);
}
Expand All @@ -298,9 +300,11 @@ export class ProgramAnalyticsComponent implements OnInit {
"evaluation-evaluated",
"Оценено",
status.projects.evaluated,
status.mode === "open"
? "Проекты, получившие хотя бы одну оценку эксперта."
: "Проекты, по которым выполнены все назначения экспертов.",
status.requiredEvaluators != null
? "Проекты, получившие нужное число полных оценок от разных экспертов."
: status.mode === "open"
? "Проекты, получившие хотя бы одну оценку эксперта."
: "Проекты, по которым выполнены все назначения экспертов.",
),
);
return metrics;
Expand Down Expand Up @@ -477,6 +481,8 @@ export class ProgramAnalyticsComponent implements OnInit {
}

private awaitingEvaluationTooltip(mode: "open" | "distributed"): string {
if (this.data()?.evaluationStatus.requiredEvaluators != null)
return "Сданные проекты, которые ещё не получили нужное число полных оценок от разных экспертов.";
return mode === "open"
? "Сданные проекты, которые ещё не получили ни одной оценки эксперта."
: "Сданные проекты без выполненных назначений или с выполненной только частью назначений.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,40 @@ <h2>Эксперты</h2>
@if (snapshot(); as state) {
<section class="experts__card experts__quota">
<div>
<strong>На каждый проект: {{ state.requiredEvaluators }} оценщика</strong>
<strong>Оценок для каждого проекта: {{ state.requiredEvaluators }}</strong>
<p>Каждую оценку выставляет отдельный эксперт.</p>
<p>
{{ state.submittedProjects }} сданных проектов · не хватает
{{ state.missingAssignments }} назначений
Сдано проектов: {{ state.submittedProjects }} · Полностью оценено:
{{ state.readyProjects }}
</p>
@if (state.missingAssignments) {
<p>
Ещё нужно выбрать экспертов{{
state.projectsMissingExperts === undefined
? " для проектов."
: " для " +
state.projectsMissingExperts +
" из " +
state.submittedProjects +
" проектов."
}}
Можно распределить автоматически или назначить вручную.
</p>
} @else if (state.submittedProjects) {
<p>Для всех сданных проектов выбран нужный состав экспертов.</p>
}
</div>
<button type="button" appButton="secondary" (click)="open('quota')">Изменить</button>
</section>
}
<section class="experts__card" [attr.aria-busy]="loading()">
@if (deliveryRefreshPaused()) {
<p appAlert="warning">
Отправка письма занимает больше времени. Можно проверить статус снова или отправить
приглашение повторно.
<button type="button" appButton="text" (click)="load()">Обновить статус</button>
</p>
}
<div class="experts__filters">
<label
>Поиск по имени или email<input
Expand Down Expand Up @@ -137,7 +161,14 @@ <h2>Эксперты</h2>
</div>
</td>
<td class="experts__email">{{ expert.email }}</td>
<td>{{ label(expert) }}</td>
<td>
{{ label(expert) }}
@if (expert.status === "pending") {
<p class="experts__delivery">
{{ deliveryLabel(expert) }}
</p>
}
</td>
<td data-label="Назначено">{{ expert.assigned ?? "—" }}</td>
<td data-label="Оценено">{{ expert.completed ?? "—" }}</td>
<td data-label="Осталось">{{ expert.remaining ?? "—" }}</td>
Expand Down Expand Up @@ -233,9 +264,12 @@ <h2 id="experts-dialog-title">{{ dialogTitle() }}</h2>
</footer>
}
@case ("quota") {
<p>Проект готов, когда получено нужное число полных оценок от разных экспертов.</p>
<p>
Проект считается полностью оценённым, когда нужное число разных экспертов сохранили свои
оценки по всем критериям.
</p>
<label
>Требуется оценщиков<input
>Экспертов для оценки каждого проекта<input
type="number"
min="1"
max="1000"
Expand All @@ -250,15 +284,22 @@ <h2 id="experts-dialog-title">{{ dialogTitle() }}</h2>
<div class="experts__selection">
<div>
<strong
>После пересчёта готово {{ preview.readyAfter }} проектов вместо
{{ preview.readyBefore }}</strong
>Полностью оценённых проектов после изменения: {{ preview.readyAfter }}.</strong
>
<p>Не хватает {{ preview.missingAssignments }} назначений.</p>
<p>Сейчас полностью оценено: {{ preview.readyBefore }}.</p>
@if (preview.projectsMissingExperts !== undefined) {
<p>
Проекты, для которых ещё нужно выбрать экспертов:
{{ preview.projectsMissingExperts }}.
</p>
}
</div>
</div>
<p>
Выставленные оценки сохранятся. Избыточные назначения ({{ preview.surplusAssignments }})
можно снять вручную, если оценок ещё нет.
Сохранённые оценки и выбранные эксперты останутся.
@if (preview.surplusAssignments) {
Лишних экспертов можно снять с проектов вручную, если они ещё не сохраняли оценки.
}
</p>
<button type="button" appButton="primary" (click)="saveQuota()" [disabled]="busy()">
Сохранить и пересчитать
Expand All @@ -282,20 +323,20 @@ <h2 id="experts-dialog-title">{{ dialogTitle() }}</h2>
</p>
} @else {
<p>
Добавим {{ preview.assignments.length }} назначений. Текущие назначения и сохранённые
оценки остаются.
Проверьте, кто будет оценивать проекты. Изменения применятся после подтверждения. Уже
выбранные эксперты и сохранённые оценки останутся.
</p>
}
<h3>Нагрузка экспертов</h3>
<div class="experts__scroll">
@for (expert of preview.load; track expert.expertId) {
<div class="experts__load">
<strong>{{ expert.name }}</strong
><span>Осталось: {{ expert.before }} → {{ expert.after }}</span>
><span>Проектов осталось оценить: {{ expert.before }} → {{ expert.after }}</span>
</div>
}
</div>
<h3>Назначения по проектам</h3>
<h3>Кто будет оценивать проекты</h3>
<div class="experts__scroll">
@for (assignment of preview.assignments; track $index) {
<div class="experts__load">
Expand All @@ -304,7 +345,15 @@ <h3>Назначения по проектам</h3>
</div>
}
@for (shortage of preview.shortages; track shortage.projectId) {
<p>{{ shortage.projectName }} — не хватает {{ shortage.missing }} экспертов</p>
<p>
{{ shortage.projectName }} — нужно выбрать ещё {{ shortage.missing }}
{{ shortage.missing | pluralize: ["эксперта", "экспертов", "экспертов"] }}
</p>
}
@if (!preview.assignments.length && !preview.shortages.length) {
<p>
Для всех проектов уже выбран нужный состав экспертов. Добавлять никого не нужно.
</p>
}
</div>
<footer class="experts__actions">
Expand All @@ -314,7 +363,7 @@ <h3>Назначения по проектам</h3>
(click)="applyAllocation()"
[disabled]="busy() || !!preview.shortages.length || !preview.assignments.length"
>
Применить {{ preview.assignments.length }} назначений</button
Подтвердить распределение</button
><button
type="button"
appButton="secondary"
Expand All @@ -325,19 +374,27 @@ <h3>Назначения по проектам</h3>
</button>
</footer>
} @else {
<p>
Система дополнит назначения до {{ snapshot()?.requiredEvaluators }} оценщиков на каждый
проект.
</p>
<label
>Проекты для распределения<select appField>
<option>Все сданные · {{ snapshot()?.submittedProjects }} проектов</option>
</select></label
>
<h3>Эксперты программы</h3>
<p>Выбрано {{ chosenExperts().size }} · приглашённые не участвуют</p>
<p>Выберите экспертов — система предложит, кто оценит каждый сданный проект.</p>
<div class="experts__scope">
<strong
>Экспертов на проект: {{ snapshot()?.requiredEvaluators }} · Сданных проектов:
{{ snapshot()?.submittedProjects }}</strong
>
@if (snapshot()?.projectsMissingExperts !== undefined) {
@if (snapshot()?.projectsMissingExperts) {
<p>
Нужно подобрать экспертов для {{ snapshot()?.projectsMissingExperts }} из
{{ snapshot()?.submittedProjects }} проектов.
</p>
} @else {
<p>Для всех сданных проектов уже выбран нужный состав экспертов.</p>
}
}
</div>
<h3>1. Выберите экспертов</h3>
<p>Выбрано: {{ chosenExperts().size }}. Приглашённые появятся здесь после принятия.</p>
<div class="experts__scroll">
@for (expert of snapshot()?.experts; track expert.id) {
@for (expert of joined(); track expert.id) {
<label class="experts__choice"
><input
type="checkbox"
Expand All @@ -348,26 +405,26 @@ <h3>Эксперты программы</h3>
/><span
><strong>{{ expert.name || expert.email }}</strong
><span>{{ expert.email }}</span
><span>{{
expert.status === "joined"
? "Осталось оценить: " + expert.remaining
: "Приглашение ещё не принято"
}}</span></span
><span>Проектов осталось оценить: {{ expert.remaining }}</span></span
></label
>
} @empty {
<p>Пока никто не принял приглашение. Пригласите экспертов и дождитесь принятия.</p>
}
</div>
<p>
Учитываем оставшуюся нагрузку и текущие назначения. Проекты собственной команды
недоступны.
Учитываем нагрузку и сохраняем текущие назначения. Эксперт не получает проект своей
команды.
</p>
<h3>2. Проверьте результат</h3>
<p>Сначала покажем план. Назначим проекты после вашего подтверждения.</p>
<button
type="button"
appButton="primary"
(click)="previewAllocation()"
[disabled]="busy() || !chosenExperts().size || !snapshot()?.submittedProjects"
>
Посмотреть распределение
Подобрать экспертов
</button>
}
}
Expand Down
Loading