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
29 changes: 22 additions & 7 deletions docs/modules/program.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,8 @@ DI-биндинги (`infrastructure/di/program/`):
- `GET /programs/:programId/project-analytics/assignments/?scope=all|completed|pending` — массив назначений.
- `GET /programs/:programId/project-analytics/assignments/:assignmentId/scores/` — поля назначения и все критерии в `scores`.

Backend API в этом PR не изменялся. Данные проходят через существующий
`CamelcaseInterceptor`: `assignment_id → assignmentId`, `criteria_total → criteriaTotal`,
`criteria_scored → criteriaScored`, `waiting_seconds → waitingSeconds`,
Данные проходят через существующий
`CamelcaseInterceptor`: `assignment_id → assignmentId`, `waiting_seconds → waitingSeconds`,
`delayed_experts → delayedExperts`. Важно: установленный camelcase-keys преобразует
`overdue_24h → overdue24H` и `overdue_48h → overdue48H` (заглавная H).
Ручного преобразования SLA-полей нет.
Expand All @@ -375,8 +374,23 @@ Use cases возвращают `Result` с `ProgramAnalyticsError`; сырой
- `in_progress` — «В процессе»;
- `completed` — «Выполнено».

Прогресс — например, «2 из 5 критериев»; при отсутствии критериев — «Нет критериев»,
для несданного проекта — «—». В open-режиме показываются реальные назначения,
Таблица назначений содержит только «Эксперт», «Проект», «Статус», «Ожидание».
Количество оценённых критериев остаётся внутренней деталью расчёта статуса
на backend. Angular не использует эти счётчики ни в списке, ни в детализации;
массив `scores` с критериями и фактическими оценками сохраняется.
«Завершили: X из Y» в разделе «Работы ожидают оценивания» по-прежнему считает
завершённые назначения экспертов, а не критерии одного назначения.

Все три scope используют один `assignmentTable`. Локальные модификаторы
`analytics-drilldown-body--assignments` и `analytics-drilldown__table--assignments`
применяются только при `view === "assignments"`. Backlog, scores detail и таблицы
внимания сохраняют прежнее оформление; shared `app-modal` не меняется.
Desktop-окно имеет ширину `min(980px, calc(100vw - 32px))`, padding 28 px и колонки
27/31/29/13%. На экранах уже 1000 px назначения представлены карточками, статус
и действие находятся на отдельных строках. Escape, focus trap и возврат фокуса
используют прежний lifecycle.

В open-режиме показываются реальные назначения,
но frontend не синтезирует задержки экспертов. В distributed-режиме
«Частично оценено» означает, что хотя бы один назначенный эксперт полностью оценил
проект, но не все назначенные эксперты завершили оценивание.
Expand Down Expand Up @@ -435,14 +449,15 @@ bottom-up destroy Angular: дочерний modal может detach до cleanup
проверяется DestroyRef владельца view, без изменения shared primitive.
Таймеров, polling, MutationObserver и document-global Escape handler нет.

Desktop: таблица внутри модалки шириной до 880px. Mobile/tablet: stacked cards,
Desktop: назначения — таблица в модалке шириной до 980px; для остальных view
сохранено прежнее оформление. Mobile/tablet: stacked cards,
перенос длинных имён и названий, ограничение высоты с вертикальным скроллом.
Сохранены Mont, токены и существующие zero states аналитики.

### Проверка

Regression tests покрывают adapter/repository/use cases/facade/interceptor,
scope/status/progress, nullable ожидание, критерии, задержки, смену программы,
scope/status, отсутствие прогресса, nullable ожидание, критерии, задержки, смену программы,
отмену запросов и настоящий CDK Overlay lifecycle. Focus tests не подменяют
attachment событием openChange и не добавляют ручной detectChanges после клика.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ describe("CamelcaseInterceptor", () => {
assignments: [
{
assignment_id: 17,
criteria_total: 5,
criteria_scored: 2,
waiting_seconds: 108000,
expert: { full_name: "Иван Иванов", user_id: 123 },
scores: [{ criterion_id: 1, min_value: 0, is_scored: true }],
Expand All @@ -129,8 +127,6 @@ describe("CamelcaseInterceptor", () => {
assignments: [
{
assignmentId: 17,
criteriaTotal: 5,
criteriaScored: 2,
waitingSeconds: 108000,
expert: { fullName: "Иван Иванов", userId: 123 },
scores: [{ criterionId: 1, minValue: 0, isScored: true }],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export function assignment(
},
project: { id: 55, name: "Проект А" },
status: "completed",
criteriaTotal: 5,
criteriaScored: 5,
assignedAt: "2026-09-01T10:00:00Z",
projectSubmitted: true,
projectSubmittedAt: "2026-09-01T12:00:00Z",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,15 @@ export interface ProgramAnalyticsAssignmentProject {
name: string;
}

/** Ожидание вычисляет backend на момент запроса. null не означает нулевое ожидание. */
/**
* Статус вычисляет backend без публикации внутренних счётчиков критериев.
* Ожидание фиксируется на момент запроса; null не означает нулевое ожидание.
*/
export interface ProgramAnalyticsAssignment {
assignmentId: number;
expert: ProgramAnalyticsAssignmentExpert;
project: ProgramAnalyticsAssignmentProject;
status: ProgramAnalyticsAssignmentStatus;
criteriaTotal: number;
criteriaScored: number;
assignedAt: string;
projectSubmitted: boolean;
projectSubmittedAt: string | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<!-- @format -->
<app-modal
[open]="state.open()"
bodyClass="analytics-drilldown-body"
[bodyClass]="
state.view() === 'assignments'
? 'analytics-drilldown-body analytics-drilldown-body--assignments'
: 'analytics-drilldown-body'
"
(openChange)="onOpenChange($event)"
>
<section
Expand Down Expand Up @@ -411,13 +415,15 @@ <h3 class="analytics-drilldown__section">Ещё не сданы</h3>
</app-modal>

<ng-template #assignmentTable let-assignments>
<table class="analytics-drilldown__table">
<table
class="analytics-drilldown__table"
[class.analytics-drilldown__table--assignments]="state.view() === 'assignments'"
>
<thead>
<tr>
<th scope="col">Эксперт</th>
<th scope="col">Проект</th>
<th scope="col">Статус</th>
<th scope="col">Прогресс</th>
<th scope="col">Ожидание</th>
</tr>
</thead>
Expand All @@ -433,24 +439,25 @@ <h3 class="analytics-drilldown__section">Ещё не сданы</h3>
</td>
<td data-label="Проект">{{ item.project.name }}</td>
<td data-label="Статус">
<span
[class]="'analytics-drilldown__badge analytics-drilldown__badge--' + item.status"
>{{ statusLabel(item.status) }}</span
>
@if (item.status === "completed") {
<button
type="button"
class="analytics-drilldown__action"
(click)="state.loadAssignmentScores(item.assignmentId)"
[attr.aria-label]="
'Посмотреть оценку: ' + item.project.name + ', ' + item.expert.fullName
"
<span class="analytics-drilldown__assignment-status">
<span
[class]="'analytics-drilldown__badge analytics-drilldown__badge--' + item.status"
>{{ statusLabel(item.status) }}</span
>
Посмотреть оценку
</button>
}
@if (item.status === "completed") {
<button
type="button"
class="analytics-drilldown__action"
(click)="state.loadAssignmentScores(item.assignmentId)"
[attr.aria-label]="
'Посмотреть оценку: ' + item.project.name + ', ' + item.expert.fullName
"
>
Посмотреть оценку
</button>
}
</span>
</td>
<td data-label="Прогресс">{{ progress(item) }}</td>
<td data-label="Ожидание">{{ waiting(item.waitingSeconds, item.status) }}</td>
</tr>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@
overflow-y: auto;
}

// Тот же шаблон используется в backlog: оформление меняется только у трёх списков назначений.
.modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments {
width: min(980px, calc(100vw - 32px));
padding: 16px;

@include responsive.apply-desktop {
padding: 28px;
}

.analytics-drilldown__header {
align-items: center;
margin-bottom: 24px;

h2 {
font-size: 24px;
line-height: 30px;
}
}

.analytics-drilldown__close {
display: grid;
place-items: center;
padding: 0;
line-height: 1;

&:hover {
background: var(--light-gray);
}
}
}

.analytics-drilldown {
width: 100%;
min-width: 0;
Expand Down Expand Up @@ -313,4 +344,114 @@
}
}
}

&__table--assignments {
tr {
display: grid;
gap: 12px;
padding: 14px;
}

td {
padding: 0;
line-height: 22px;
}

td::before {
margin-bottom: 4px;
}

.analytics-drilldown__identity {
gap: 12px;
font-weight: 500;
}

.analytics-drilldown__assignment-status {
display: flex;
flex-flow: column wrap;
gap: 12px 14px;
align-items: flex-start;
}

.analytics-drilldown__badge {
display: inline-flex;
gap: 6px;
align-items: center;
min-height: 28px;
padding: 4px 9px;
border-radius: 8px;

&::before {
flex: 0 0 6px;
width: 6px;
height: 6px;
content: "";
background: currentcolor;
border-radius: 50%;
}
}

.analytics-drilldown__action {
padding: 4px 0;
font-size: 13px;
font-weight: 500;
line-height: 20px;
}

@include responsive.apply-desktop {
border-spacing: 0;
border-collapse: separate;

.analytics-drilldown__assignment-status {
flex-direction: row;
align-items: center;
}

tr {
display: table-row;
height: 72px;
padding: 0;
}

th,
td {
padding: 16px 12px;
vertical-align: middle;
}

thead tr {
height: auto;
}

th {
padding-block: 13px;
color: var(--dark-grey);
background: var(--light-gray);
border-bottom: 0;

&:first-child {
width: 27%;
border-radius: 8px 0 0 8px;
}

&:nth-child(2) {
width: 31%;
}

&:nth-child(3) {
width: 29%;
}

&:last-child {
width: 13%;
border-radius: 0 8px 8px 0;
}
}

th:nth-child(3),
td:nth-child(3) {
padding-inline: 8px;
}
}
}
}
Loading