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
10 changes: 10 additions & 0 deletions docs/modules/program-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ Backend проверяет manager именно этой программы ли

Native adapters Button/Field/Choice/Table, существующие Modal/Icon/Pagination/Alert и Mont сохраняют PROCOLLAB DS. Foundation подключена локально также на desktop; глобальные компоненты не менялись.

## Исправления после dev-приёмки · 17.10.2026

Поиск остаётся в DOM при обновлении списка; завершение навигации предыдущего запроса не заменяет более новый ввод. Кейс и сдача привязаны через Reactive Forms, включая отложенное появление вариантов. Повторный клик по выбранному кружку или строке и клавиша пробела снимают выбор; ссылки сохраняют свои переходы. Подсказка недоступной оценки находится под общим рядом действий.

Кнопки организатора занимают доступную ширину колонок, текст остаётся в одну строку. На 360px более длинной подписи управления выделено больше места. В составе команды используются существующий AvatarComponent и полная почта из приватного API; старый ответ без этих полей поддерживается. В материалах заголовок/описание и действие разнесены, empty/list состояния имеют отдельные отступы.

Проверки: **2122 Vitest / 416 файлов PASS**, dev build, ESLint/Stylelint и 18 API tests на PostgreSQL. Chrome + реальный локальный Django/PostgreSQL: медленный посимвольный поиск в обоих списках с сохранением DOM/focus, кейс после reload и совместно со сдачей, toggle мышью/клавиатурой, геометрия действий, почты/загруженные аватары, материалы CRUD и desktop1440/mobile390/360. Снимки: [кнопки](../qa/program-management-v1/feedback-20261017/entry-desktop.png), [панель действий](../qa/program-management-v1/feedback-20261017/selection-desktop.png), [команда](../qa/program-management-v1/feedback-20261017/team-desktop.png), [материалы](../qa/program-management-v1/feedback-20261017/materials-desktop.png).

Backend dependency: [API #780](https://github.com/PROCOLLAB-github/api/pull/780), additive `members[].email/avatar`; существующие права сохранены, миграций нет. Live авторизованная сессия организатора и реальный Selectel transport не проверялись.

## Verification · 17.10.2026

52 теста Vitest в семи файлах: management facade, компонент, HTTP contract, утилиты дат/статусов, ProgramRepository, ProgramShellInfoService и существующий FileService. Проверены отмена устаревших запросов и сброс приватных данных при 403, выбор, dirty confirmation, версия записи, загрузка multipart через FileService и кеш после сохранения.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ export interface ManagementTeam {
projectId: number;
programLinkId: number;
name: string;
members: { id: number; name: string; role: string }[];
members: {
id: number;
name: string;
role: string;
email?: string | null;
avatar?: string | null;
}[];
}
export interface ManagementEvaluation {
projectId: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ <h1>Управление программой</h1>
<h2>{{ requestError() }}</h2>
<button type="button" appButton="secondary" (click)="state.retry()">Повторить</button>
</section>
} @else if (state.pending()) {
} @else if (state.pending() && !state.program()) {
<section class="management__card" aria-busy="true" aria-live="polite">
Загружаем данные программы…
</section>
} @else if (state.program(); as program) {
@if (section() === "participants" || section() === "projects") {
<section class="management__card">
<section class="management__card" [attr.aria-busy]="state.pending()">
<h2>{{ section() === "participants" ? "Участники" : "Проекты" }}</h2>
<p class="management__muted">
{{
Expand Down Expand Up @@ -65,7 +65,10 @@ <h2>{{ section() === "participants" ? "Участники" : "Проекты" }}
@if (section() === "projects") {
<div class="management__filters">
<label
>Кейс<select [value]="currentCase()" (change)="changeCase($any($event.target).value)">
>Кейс<select
[formControl]="caseFilter"
(change)="changeCase($any($event.target).value)"
>
<option value="all">Все кейсы</option>
<option value="without_case">Без выбранного кейса</option>
@for (option of state.projects()?.caseOptions ?? []; track option) {
Expand All @@ -76,7 +79,7 @@ <h2>{{ section() === "participants" ? "Участники" : "Проекты" }}
@if (program.isCompetitive) {
<label
>Сдача решения<select
[value]="state.context?.query?.submission ?? 'all'"
[formControl]="submissionFilter"
(change)="changeQuery({ submission: $any($event.target).value, offset: 0 })"
>
<option value="all">Все проекты</option>
Expand All @@ -102,33 +105,43 @@ <h2>{{ section() === "participants" ? "Участники" : "Проекты" }}
>Перейти к оценке</a
>
} @else {
<div>
<button type="button" appButton="primary" disabled>Перейти к оценке</button>
<p class="management__muted">
{{
program.isCompetitive
? "Сначала нужно сдать решение."
: "Программа не предусматривает оценивание."
}}
</p>
</div>
<button
type="button"
appButton="primary"
disabled
aria-describedby="evaluation-unavailable"
>
Перейти к оценке
</button>
}
<button
type="button"
appButton="icon"
class="management__clear-selection"
aria-label="Снять выбор проекта"
(click)="state.clearSelection()"
>
<i appIcon icon="cross" appSquare="12" aria-hidden="true"></i>
</button>
@if (!selected.submitted || !program.isCompetitive) {
<p id="evaluation-unavailable" class="management__muted management__selection-hint">
{{
program.isCompetitive
? "Сначала нужно сдать решение."
: "Программа не предусматривает оценивание."
}}
</p>
}
</section>
} @else if (count()) {
<p class="management__muted">
Выберите один проект, чтобы посмотреть команду или перейти к оценке.
</p>
}
}
@if (!count()) {
@if (state.pending()) {
<p class="management__list-loading" role="status">Обновляем список…</p>
} @else if (!count()) {
<div class="management__empty" role="status">
<h3>
{{
Expand Down Expand Up @@ -230,7 +243,9 @@ <h3>
[value]="project.id"
[checked]="state.selected()?.id === project.id"
[attr.aria-label]="'Выбрать проект «' + project.name + '»'"
(change)="select(project)"
(click)="select(project)"
(keydown.space)="$event.preventDefault(); select(project)"
(keydown.enter)="$event.preventDefault(); select(project)"
/></label>
</td>
<td class="management__name" data-label="Проект">
Expand Down Expand Up @@ -486,11 +501,15 @@ <h2>Календарь программы</h2>
</form>
} @else {
<section class="management__card">
<h2>Материалы программы</h2>
<p class="management__muted">Документы и полезные ссылки для участников.</p>
<button type="button" appButton="primary" (click)="editMaterial('new')">
Добавить материал
</button>
<header class="management__materials-heading">
<div>
<h2>Материалы программы</h2>
<p class="management__muted">Документы и полезные ссылки для участников.</p>
</div>
<button type="button" appButton="primary" (click)="editMaterial('new')">
Добавить материал
</button>
</header>
@for (material of program.materials; track material.id) {
<article class="management__material">
<h3>
Expand Down Expand Up @@ -530,9 +549,12 @@ <h3>
</div>
</article>
} @empty {
<p class="management__empty">
В программе пока нет материалов. Добавьте файл или ссылку.
</p>
<div class="management__materials-empty">
<h3>В программе пока нет материалов</h3>
<p class="management__muted">
Добавьте файл или ссылку, чтобы участники могли открыть их со страницы программы.
</p>
</div>
}
</section>
}
Expand Down Expand Up @@ -576,8 +598,12 @@ <h3>{{ state.selected()?.name }}</h3>
<p>Участников: {{ team.members.length }}</p>
@for (member of team.members; track member.id) {
<div class="management__team-member">
<p>{{ member.name }}</p>
<p class="management__muted">{{ member.role }}</p>
<app-avatar [url]="href(member.avatar ?? null) ?? undefined" [size]="44" />
<div class="management__team-person">
<p class="management__team-name">{{ member.name }}</p>
<p class="management__muted">{{ member.role }}</p>
<p class="management__team-email">{{ member.email || "Почта не указана" }}</p>
</div>
</div>
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@
position: sticky;
top: 0;
z-index: 2;
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto 44px;
gap: 12px;
align-items: center;
padding: 12px;
Expand All @@ -227,6 +228,21 @@
}
}

&__clear-selection {
grid-row: 1;
grid-column: 4;
}

p.management__selection-hint {
grid-column: 2 / 4;
margin: 0;
}

&__list-loading {
padding: 28px 0;
color: var(--grey-for-text);
}

&__primary-link {
display: inline-flex;
align-items: center;
Expand Down Expand Up @@ -306,6 +322,22 @@
}
}

&__materials-heading {
display: flex;
gap: 24px;
align-items: center;
justify-content: space-between;

> button {
flex-shrink: 0;
}
}

&__materials-empty {
max-width: 480px;
padding: 32px 0 12px;
}

&__material {
display: grid;
gap: 8px;
Expand Down Expand Up @@ -381,8 +413,34 @@
}

.management__team-member {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 16px 0;
border-top: 1px solid var(--medium-grey-for-outline);

app-avatar {
flex-shrink: 0;
}
}

.management__team-person {
min-width: 0;
overflow-wrap: anywhere;

p {
margin: 0 0 4px;
}

.management__team-name {
font-weight: 600;
}

.management__team-email {
margin-bottom: 0;
font-size: 12px;
line-height: 18px;
}
}

.management__dialog {
Expand Down Expand Up @@ -510,8 +568,7 @@
grid-template-columns: minmax(0, 1fr) 44px;

> button:not([aria-label]),
> a,
> div:has(button) {
> a {
grid-column: 1 / -1;
width: 100%;
}
Expand All @@ -527,6 +584,20 @@
grid-column: 1;
}

p.management__selection-hint {
grid-column: 1 / -1;
}

&__materials-heading {
display: grid;
gap: 20px;
justify-content: start;

> button {
justify-self: start;
}
}

&__inline > button {
min-height: 44px;
}
Expand Down
Loading