From 1aadb2da7ed12c26577f8c2ff90ad1b9879077aa Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 20:24:02 +0300 Subject: [PATCH 01/47] feat(programs): team tab and solution invitation flow --- docs/contracts/program-solutions-v1/README.md | 274 ++++++++++++++++- .../program-solutions-v1/examples.json | 41 ++- .../program-solutions-v1/openapi.json | 6 +- docs/product/program-solutions-fe04-fe05.md | 19 ++ package.json | 3 +- .../solution-invitation-inbox.service.spec.ts | 134 +++++++++ .../solution-invitation-inbox.service.ts | 144 +++++++++ .../solution/solution-team-contract.spec.ts | 178 ++++++++++++ .../use-cases/solution-team.use-cases.ts | 95 ++++++ .../solution/use-cases/solution.use-cases.ts | 8 +- .../ports/solution.repository.port.ts | 57 ++++ .../solution/solution-contract.fixture.ts | 13 + .../solution/solution-contract.model.ts | 40 ++- .../solution/solution-http.adapter.ts | 97 ++++++ .../solution/solution.repository.ts | 44 ++- .../ui/pages/office/nav/nav.component.html | 1 + .../app/ui/pages/office/nav/nav.component.ts | 6 +- .../app/ui/pages/office/office.component.html | 20 +- .../ui/pages/office/office.component.spec.ts | 13 + .../app/ui/pages/office/office.component.ts | 17 +- .../solution/solution-team.component.html | 225 ++++++++++++++ .../solution/solution-team.component.scss | 55 ++++ .../solution/solution-team.component.spec.ts | 248 ++++++++++++++++ .../solution/solution-team.component.ts | 275 ++++++++++++++++++ .../program/solution/solution.component.html | 30 +- .../program/solution/solution.component.scss | 24 +- .../solution/solution.component.spec.ts | 12 + .../program/solution/solution.component.ts | 45 ++- .../member-invite-form.component.html | 71 +++++ .../member-invite-form.component.ts | 58 ++++ .../participant-picker.component.ts | 3 + .../project-invite-role-input.component.html | 2 +- .../project-invite-role-input.component.ts | 1 + ...project-member-invite-modal.component.html | 89 +----- .../project-member-invite-modal.component.ts | 22 +- ...tion-member-invite-modal.component.spec.ts | 183 ++++++++++++ .../solution-member-invite-modal.component.ts | 152 ++++++++++ ...olution-invitation-inbox.component.spec.ts | 97 ++++++ .../solution-invitation-inbox.component.ts | 98 +++++++ .../invite-manage-card.component.html | 62 ++-- .../invite-manage-card.component.ts | 36 ++- tools/generate-solution-contract.py | 17 +- tools/verify-solution-contract-types.ts | 64 ++++ 43 files changed, 2881 insertions(+), 198 deletions(-) create mode 100644 docs/product/program-solutions-fe04-fe05.md create mode 100644 projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts create mode 100644 projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.ts create mode 100644 projects/social_platform/src/app/api/solution/solution-team-contract.spec.ts create mode 100644 projects/social_platform/src/app/api/solution/use-cases/solution-team.use-cases.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html create mode 100644 projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.scss create mode 100644 projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts create mode 100644 projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.html create mode 100644 projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.ts create mode 100644 projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.ts create mode 100644 projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.ts create mode 100644 tools/verify-solution-contract-types.ts diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index 91faad638..46c23d51f 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -1,9 +1,275 @@ -# C00 1.0.0 для FE01 +# C00 Контракт решения в программе -Неизмененные `openapi.json` и `examples.json` из пакета C00 backend. JSON на проводе в snake_case. Источник требований: https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175 . Дизайн D00 v3 утвержден 24 октября 2026 года. +Версия 1.1.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений. Остальные команды остаются задачами BE04–BE06. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. -`python tools/generate-solution-contract.py` генерирует 47 доменных типов и 43 независимых сценария в `domain/solution/`. После генерации выполнить scoped Prettier этих двух файлов. Фикстуры используются только тестами; production mock provider не включается. +Изменение 1.1.0: добавлен необязательный `permissions.can_leave`, вычисляемый сервером для собственного выхода принятого участника. При отсутствии поля клиент считает его false. Существующие поля, URL и payload команд не меняются; C00 1.0.0 остается совместимым подмножеством. Капитан, сданная заявка и выключенный режим дают false. -Изменение контракта требует синхронного изменения копий backend/frontend и повторной проверки wire boundary. Реализация FE01 ограничена контекстом, созданием и чтением; все сценарии C00 не означают реализацию будущих команд. +Источники: ТЗ «Решение вместо проекта в программах PROCOLLAB» и действующие правила ветки dev, проверенные на backend b7d9f57 и frontend bc1f961b. Старые API и обычные проекты сохраняют свои контракты. + +- [OpenAPI 3.1](openapi.json) — URL, схемы, обязательность, статусы и примеры. +- [Независимые сценарии для моков](examples.json) — request и response по operation_id. +- [Проверка контракта](validate_contract.py) — проверка OpenAPI и примеров. +- [Продуктовое ТЗ](https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175). + +## Порядок разработки + +C00 фиксирует техническую основу. D00 v3 явно согласован пользователем 24 октября 2026 года в чате. BE01 и FE01 реализуются параллельно по C00. Для следующих задач сохраняется согласованный дизайн и порядок зависимостей ТЗ. + +## Решения C00 + +Одна основная сущность ProgramSolution одновременно представляет заявку команды и ее решение. Команда состоит из принадлежащих заявке записей участников. Самостоятельная Team, обязательный Project, отдельная Application поверх решения и процесс повторной сдачи не вводятся. + +Решение принадлежит одной программе. Формат формы — case_championship или hackathon. У программы одна общая форма; разные поля по кейсам не входят в v1. Кейс или задача представлены полем select с purpose:case, без новой сущности Case. Поля шаблона копируются в конфигурацию программы. + +Создание доступно по текущим правилам участника и дедлайна, включая существующие административные исключения. Повторное создание капитаном возвращает его заявку с HTTP 200; новое — 201. До первого включения все изменяющие endpoints проверяют технический режим программы. Наличие серверного кода при выключенном режиме не дает создать решение. + +## Модель хранения + +| Объект | Данные и связи | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| ProgramSolution | id, program FK, captain FK User, name, version bigint, status draft/submitted, submitted_at nullable, submission_snapshot JSON nullable, created_at, updated_at | +| ProgramSolutionMember | solution FK, user FK, role nullable, specialization nullable, joined_at | +| SolutionInvitation | solution FK, recipient FK User, role nullable, specialization nullable, motivational_letter nullable, status pending/accepted/declined, version bigint, created_at, updated_at | +| ProgramSolutionAnswer | solution FK, field FK PartnerProgramField, value JSON; одна запись на пару | +| Конфигурация формы программы | template nullable до настройки; определения PartnerProgramField расширяются type, purpose, position, validation; revision считается по всей конфигурации | +| Профиль участника программы | Существующая регистрация сохраняется. Текущая заявка определяется по принятому составу или капитанству, не только по старому profile.project | +| UserFile | Существующий файл с первичным ключом link. Новый File ID не изобретается | + +Капитан также имеет одну запись ProgramSolutionMember. При создании она добавляется в той же транзакции. При передаче капитанства меняется captain FK на уже принятого участника; прежний капитан остается участником. is_captain вычисляется сравнением user.id и captain_id. Дублировать капитана в численности нельзя. + +Ограничения БД: unique(solution,user) для участников; unique(solution,field) для ответов; частичный unique(solution,recipient) для pending приглашений; положительные версии; submitted_at и snapshot заполнены только в submitted. FK ссылаются на реальное решение без фиктивного Project. + +Инвариант «капитан входит в состав» и соответствие поля программе проверяются сервисом в транзакции. Их нельзя выразить обычным CHECK по строке. Создание дублей и запрет участия в двух командах активной программы обеспечиваются сериализацией через программу и повторными проверками всех поддерживаемых способов записи. Не объявляем динамическую exclusivity ограничением unique(user,program) навсегда. + +Физически новые приглашения хранятся в SolutionInvitation, а старые Invite не получают фиктивный project FK. Общая бизнес policy используется через адаптер владельца. Операции отзыва сохраняют действующую семантику удаления: pending приглашение удаляется; статус revoked не добавляется. accepted и declined остаются историческими строками. + +## Ревизии + +ProgramSolution.version начинается с 1 и увеличивается при каждом успешном изменении содержимого, состава или приглашений и при сдаче. Изменение разрешений из-за времени или роли и изменение формы организатором само по себе версию решения не повышают. + +SolutionInvitation.version начинается с 1 и повышается при редактировании и принятии либо отклонении. Отзыв удаляет строку. Ревизия приглашения не заменяет актуальные проверки состава. + +Form.revision — непрозрачный 64-символьный SHA-256 в hex от канонической конфигурации: template и определения в порядке position с ID, типом, назначением, подписью, подсказкой, обязательностью, options и validation. Используется существующий механизм хеша; одинаковая конфигурация дает одинаковую ревизию. Клиент сравнивает строки и не вычисляет хеш. Сам hash не является номером версии документа 1.0.0. + +- PATCH решения и submit требуют expected_version и expected_form_revision. +- Команды состава требуют expected_solution_version. +- Создание приглашения требует expected_solution_version. +- Редактирование приглашения требует expected_version приглашения и expected_solution_version. +- Accept и decline требуют только expected_version приглашения: получатель до вступления не должен читать закрытую заявку. +- DELETE приглашения передает обе ожидаемые версии в query. DELETE участника — expected_solution_version в query. +- PUT формы требует expected_revision. + +Состояние и права всегда повторно проверяются внутри транзакции. Ошибка ревизии — 409 version_conflict либо form_revision_conflict без записи. Клиент перечитывает объект, сохраняет локальные правки и предлагает повторить осмысленное действие. Автоматически повторять submit или принятие приглашения после конфликта нельзя. + +## Общие правила API + +API base URL берется из настроек окружения. Новые пути добавляются к тому же base URL, что /programs/ и /projects/. Отдельный /v1 в URL не добавляется; версия пакета контракта — 1.0.0. + +JWT Bearer и текущие правила аутентификации сохраняются. ID — положительные JSON integer не больше 2^53−1. Даты — ISO 8601 UTC с Z. Пагинация: limit от 1 до 100, default 20; offset от 0. Ответ: count, next, previous, results. + +Тело запроса строгое: неизвестные поля отклоняются; поля пользователя, роли авторизации, project_id, team_id и submitted не принимаются в командах, где они не определены. Клиенты должны терпимо читать дополнительные поля ответов при совместимом расширении контракта. + +Все несекретные публичные подписи имен полей берутся из определения формы. Поля ответа запроса могут возвращать только данные разрешенной заявки. Смена ID в URL не переносит объект в другую программу. + +## Endpoints + +| Метод и путь | Назначение | +| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | +| GET /programs/{program_id}/solution-context/ | Режим программы, сроки, доступность создания и текущая заявка | +| GET /programs/{program_id}/solutions/ | Доступный пользователю список решений | +| POST /programs/{program_id}/solutions/ | Создать решение с начальным названием либо вернуть существующее | +| GET /programs/{program_id}/solutions/{solution_id}/ | Полная разрешенная заявка: team, form, answers, submission, permissions, readiness | +| PATCH /programs/{program_id}/solutions/{solution_id}/ | Частично сохранить название и ответы черновика | +| POST /programs/{program_id}/solutions/{solution_id}/submit/ | Сдать сохраненную заявку | +| DELETE /programs/{program_id}/solutions/{solution_id}/members/{user_id}/ | Капитан удаляет участника | +| POST /programs/{program_id}/solutions/{solution_id}/leave/ | Участник выходит | +| POST /programs/{program_id}/solutions/{solution_id}/captain/ | Передача капитанства | +| GET и POST /programs/{program_id}/solutions/{solution_id}/invitations/ | Капитан читает приглашения заявки или создает приглашение | +| GET /solution-invitations/ | Входящие приглашения текущего пользователя | +| GET /solution-invitations/{invitation_id}/ | Безопасное представление для капитана и получателя | +| PATCH и DELETE /solution-invitations/{invitation_id}/ | Изменить или отозвать pending приглашение капитаном | +| POST /solution-invitations/{invitation_id}/accept/ | Получатель принимает приглашение | +| POST /solution-invitations/{invitation_id}/decline/ | Получатель отклоняет приглашение | +| GET /programs/{program_id}/solution-form/ | Текущая форма, доступная участнику программы | +| GET и PUT /programs/{program_id}/management/solution-form/ | Организатор читает или атомарно меняет форму | +| GET /solution-form-templates/ | Прототипы двух шаблонов | +| POST /files/ | Существующая multipart загрузка, ответ 201 {url} | + +Списки solutions используют search и submission=all/submitted/not_submitted. В неконкурсной программе submission, отличный от all, возвращает 400. Списки приглашений используют status=pending/accepted/declined, default pending. Общие списки и контекст сортируются детерминированно по created_at и ID согласно текущему интерфейсу, без случайного выбора первой заявки. + +Контекст существует и в legacy режиме. current_application — null либо ссылка с kind:solution и solution_id либо kind:legacy_project с project_id и program_link_id. Это discriminated union для навигации, а не новая сущность. Несогласованные несколько текущих заявок дают 409 ambiguous_application. + +blocked_reason в контексте: null, already_has_application, legacy_mode, form_not_configured, not_program_member, submission_closed или already_in_program_team. Порядок выбора: текущая заявка, legacy режим, права, закрытая подача, неготовая форма. Детали участия в чужой команде не раскрываются постороннему. + +## Чтение и полномочия + +Капитан и принятые участники читают заявку. Организатор, эксперт и прочие зрители получают доступ в пределах текущей политики программы, назначений и публикации работ. Это не дает прав изменения или сдачи за капитана. + +permissions в Solution вычисляются для текущего пользователя: can_edit, can_manage_team, can_invite, can_submit и can_leave. У участника и эксперта нет прав редактирования, управления командой, приглашения и сдачи. Принятый участник может получить can_leave для собственного выхода. can_submit означает, что капитану разрешена команда сдачи по состоянию и срокам; неполнота полей отражается отдельно в readiness. Это позволяет нажать действие и увидеть причины неготовности. + +readiness содержит is_ready и issues. Для неконкурсных заявок и зрителей вне своей команды возвращается null. Для своей команды выводятся актуальные требования без раскрытия чужих персональных данных. Разрешение на действие не вычисляется на фронте по роли или часам браузера. + +Приглашение до принятия раскрывает program, безопасную ссылку solution, капитана, получателя и текст приглашения. Ответов формы, состава чужой команды, регистрационных анкет и оценок в нем нет. Входящий список содержит только приглашения текущему пользователю; список приглашений заявки доступен капитану. Административное чтение сохраняет действующий отдельный объем полномочий и не подразумевается этим endpoint. + +Если объект вне видимости вызывающего, возвращается 404. Если объект видим, но действие запрещено, — 403. Для новых API 401, 403 и 404 также имеют общий Error envelope. Старые endpoints не переводятся на него автоматически. + +## Команда и приглашения + +### Пользовательский путь из действующего проекта + +Заявка имеет две вкладки: «Решение» и «Команда». На первой — настроенные поля и материалы, на второй — строки принятого состава и отдельный список «Ожидают подтверждения». Это разделы одного ProgramSolution. Переключение сохраняет локальный ввод; общее сохранение и сдача относятся к обеим вкладкам. При ошибке сдачи открывается нужная вкладка. + +Источник текущего UI исправлен после предоставленных пользователем скриншотов: локальный ref origin/master, commit 8a064835, компоненты project-member-invite-modal, participant-picker, project-invite-role-input и project-team-step. Старый checkout f6f867c6 с inline формой по ссылке не является источником целевого сценария. Ссылка на профиль не вводится. + +«Пригласить участника» открывает модальное окно. В нем поиск по имени или фамилии от двух символов, нормализация пробелов, задержка 300 мс и отмена предыдущего запроса. Переиспользуется существующий GET `/auth/public-users/` с `user_type=1`, `fullname`, `partner_program=solution.program_id`, `limit=8`, `offset=0` через GetMembersUseCase и MemberHttpAdapter. program_link_id не подставляется вместо ID программы. Ответ использует действующую модель User и карточку имени, возраста, специальности и навыков; новый каталог API не вводится. + +Получатель выбирается из результата. Капитан, принятый участник и уже приглашенный человек недоступны с объяснением причины. Выбор заменяет список карточкой «Выбранный участник», которую можно очистить. В createInvitation передается существующий `recipient_user_id` выбранного пользователя; текст поиска не является выбранным ID. + +Роль обязательна в форме: после нормализации пробелов от 1 до 128 символов. Сохраняются 13 подсказок ProjectRoleSuggestions, их фильтрация и свободный ввод. Отправка активна только при валидных получателе и роли; pending блокирует повтор. Ошибка оставляет выбор и роль, успех закрывает окно и добавляет ожидающее приглашение. Отмена и новое открытие сбрасывают временную форму; закрытие прекращает обновление из старого запроса. + +В ожидающих приглашениях сохраняются видимые действия изменения роли и отзыва с подтверждением. Прежнее описание об отсутствии видимой кнопки редактирования относилось к старому checkout и исправлено. После сдачи применяются ограничения C00: редактирование роли и принятие запрещены, отзыв и отклонение доступны. Получатель использует существующие входящие приглашения и принять/отклонить, ссылка адаптируется к Solution. + +Схемы C00 для создания по recipient_user_id, изменения и отзыва уже поддерживают этот путь и не менялись. Nullable role на уровне API сохранен для совместимости с legacy моделью; UI требует заполненную нормализованную роль. Существующие проектные endpoints сохраняют работу; решения используют свой контракт. Ошибки показываются безопасным сообщением без raw backend body. Черновик заявки не отправляет приглашение автоматически. + +Принятый состав представлен team.members. accepted_count равен количеству уникальных членов, включая капитана. pending_count — уникальные pending получатели. reserved_count — количество уникальных пользователей в объединении принятого состава и pending получателей; при штатных данных равно сумме двух счетчиков. + +При создании и принятии приглашения проверяются регистрация, отсутствие в принятом составе, запрет второй команды активной конкурсной программы и максимум с резервами. При принятии свой резерв заменяется принятым участием и не увеличивает число занятых мест второй раз. + +На submit минимум и максимум проверяются по принятому составу, как сейчас. Не принявшие приглашение пользователи не закрывают минимум. Новая заявка допустима с одним капитаном. NULL отключает соответствующую границу. Источник границ — текущие параметры legacy_team_min_size и legacy_team_max_size через адаптер, без импорта параметров из независимого nextgen процесса. + +Exclusivity действует в конкурсной программе до datetime_finished. В другой программе и после завершения программы ложного конфликта быть не должно. При сосуществовании старых заявок и новых решений проверяются обе структуры; pending приглашения сами по себе не считаются принятым участием во второй команде. + +После submit состав и приглашения нельзя создавать, менять или принимать. Pending приглашения разрешено отклонить или отозвать, что изменяет служебную version и счетчики, но не снимок сданного принятого состава. Зафиксированная работа не размораживается. + +remove и transfer возвращают MutationReceipt с новой solution_version; клиент перечитывает доступное решение. Leave возвращает 204, поскольку вышедший пользователь может утратить право чтения. Accept возвращает обновленное приглашение, ссылку заявки и новую solution_version; decline возвращает application:null и solution_version:null. Удаление капитана и его выход запрещены до передачи капитанства. Прямого POST members для обхода приглашения нет. + +## Ответы и формы + +Ответ представлен как {field_id,value}. Тип берется из определения поля, а не из переданного клиентом type: + +| Тип | JSON value | +| ------------------- | --------------------------------- | +| text, textarea, url | string | +| select, radio | Точная строка из options | +| number | JSON number, включая 0 | +| checkbox | boolean, включая false | +| file | {url: URL существующего UserFile} | + +PATCH с value:null очищает ответ. Пустая строка и строка из пробелов также очищают текст, ссылку и выбор в черновике. В ответе GET очищенные поля отсутствуют в answers, не содержат null. Дубликат field_id в запросе отклоняется целиком. Сохраненный текст не меняется автоматически, кроме нормализации, предусмотренной текущим полем; проверка пустоты использует trim. + +Проверка типов, вариантов и ограничений выполняется при каждом непустом сохранении. Required применяется при submit. Число 0 и false удовлетворяют обязательности ответа; отдельное согласие с требованием true в v1 не добавляется. Системное название ограничено 256 символами, создается по текущему правилу начального названия и обязательно при сдаче. + +Типы: text, textarea, url, number, select, checkbox, file. radio сохраняется для существующих определений и переноса; новый конструктор не предлагает его создание. purpose:case допустим только для select и в единственном экземпляре; сервер сохраняет системное name=case. Подпись «Задача или трек» в хакатоне не меняет эту семантику. Системные назначения, кроме other, уникальны в форме. presentation требует file; repository, demo и video требуют url; run_instructions требует textarea. Любой поддерживаемый тип можно использовать с purpose:other. + +position — уникальные целые числа от 0; PUT нормализует порядок в последовательность. options хранятся точными строками, пустые и дубли после trim без учета регистра запрещены. Используемые значения нельзя переименовать или удалить. Select в сохраняемой конфигурации требует хотя бы один вариант; шаблон возвращает пустые options как заготовку для организатора. + +До первого PUT конфигурация может иметь template:null, fields:[], is_configured:false. Это блокирует создание новых решений с кодом form_not_configured. GET формы при этом допустим организатору и зарегистрированному участнику, а не генерирует автоматически выбранный шаблон. + +## Настройка организатором + +PUT management/solution-form полностью заменяет определения с expected_revision. id:null означает новое поле, пропуск существующего ID — удаление. Чужой ID и повтор одного ID отклоняются. Системное name не редактируется клиентом. + +После непустых ответов нельзя удалить поле или сменить его тип. Использованные варианты нельзя удалить или переименовать. Допустимо добавить поля, менять подпись, подсказку, порядок и required. Ограничения сужаются только если им соответствуют все сохраненные непустые ответы. Смена template после has_answers:true запрещена. Новая required настройка влияет только на несданные работы. + +В FormManagement fields_usage содержит возможность удаления и смены типа, а affected_draft_count — количество черновиков, которые после применения конфигурации не готовы по новым обязательным полям. При GET это количество по текущей конфигурации; после PUT — по сохраненной. Фронт не вычисляет счетчик по загруженной странице заявок. + +Начальные шаблоны копируются в конкретную программу. В КЧ требуются выбор, краткое описание и PDF, в хакатоне — выбор, краткое описание, описание реализации, репозиторий и инструкция запуска. Демо, видео и дополнительные материалы необязательны; в хакатоне также необязательна презентация. Иные поля добавляет организатор. Это стартовые требования, не отдельные классы решения. + +## Файлы + +POST /files/ принимает multipart file и возвращает ровно существующий {url}. На привязке file поля сервер ищет UserFile по link и проверяет право использования: собственная загрузка капитана либо файл, уже принадлежащий этой заявке, в том числе из переноса. Произвольная внешняя ссылка не становится file ответом. Внешние материалы добавляются через поле url. + +Один file ответ содержит один файл. Верхний предел привязки к решению в v1 — 100 MiB (104857600 bytes); поле может задать меньший предел. Существующий общий upload сохраняет свои ограничения для остальных потребителей. Фронт проверяет размер по определению поля до загрузки, а backend повторяет проверку при привязке UserFile. Для PDF в шаблоне КЧ — 20 MiB и extension pdf. Для дополнительных архивов задается отдельное поле; начальный предел 50 MiB. Проверяются фактический размер и разрешенный тип, а не только строка имени. Старые заведомо импортированные материалы не блокируют чтение истории. + +Снятие ответа не удаляет бинарный файл автоматически. Удаление UserFile, на который ссылается сданное решение, должно отклоняться защитой file_in_use в файловом сервисе. Действующие успешные upload ответы остаются прежними. Доступ к бинарным данным CDN остается в существующей модели; контракт не обещает новую выдачу подписанных ссылок. + +## Сдача + +Состояния хранения: draft и submitted. Статусы оценивания и публикации остаются производными действующего процесса. + +Последовательность фронта: + +1. Сохранить текущие ответы PATCH с обеими ожидаемыми ревизиями. +2. Получить новую Solution.version, не использовать старую версию до PATCH. +3. Выполнить текущее подтверждение сдачи. +4. POST submit с новой expected_version и actual expected_form_revision. +5. При успехе показать полученное представление submitted; при конфликте перечитать данные без автоматической повторной сдачи. + +PATCH и submit — две отдельные транзакции. Если submit отклонен, ранее успешно сохраненный черновик остается сохраненным; частично сданного состояния не возникает. + +Под блокировками повторяются проверки капитана, конкурсности, состояния, дедлайна, состава, выбора, всех required и ограничений. Успех записывает status, submitted_at, version и submission_snapshot атомарно. Снимок включает название, form definitions, form revision, answers, капитана и принятый состав. Pending приглашения в состав снимка не входят. + +Для draft GET возвращает текущую form и is_snapshot:false. Для submitted — form и answers из снимка с is_snapshot:true. Новый required, новая подпись или порядок не меняют сданный просмотр. Состояние таймера submission_open при этом вычисляется на момент GET и не является частью исторических требований. + +После сдачи повторный submit дает 409 already_submitted независимо от старой expected_version, после проверки доступа и роли. Не вводятся повторная сдача, отзыв сдачи или возврат в draft. При сетевом обрыве клиент делает GET и устанавливает фактический статус. + +Дедлайн: отдельная дата подачи либо дата окончания регистрации. Сравнение сохраняет текущую включенную границу: серверное now <= deadline. После закрытия подачи нельзя создавать и сдавать, но несданный черновик можно сохранять, как в действующем API. Конкурсность отдельно управляет submit: в неконкурсной программе status остается draft, submission.required:false, readiness:null, can_submit:false; работа сохраняется без искусственной сдачи. + +## Ошибки + +Новый Error содержит обязательные code, detail, errors и meta. errors — массив {path,code,field_id,detail}; field_id:null для системной ошибки. meta несет current_version или current_form_revision только при наличии права чтения. + +| HTTP | Коды | +| ---- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| 400 | validation_error, invalid_answers, required_program_fields, case_required, case_unavailable, invalid_form | +| 401 | authentication_required | +| 403 | not_program_member, not_solution_captain, not_invite_recipient, permission_denied | +| 404 | not_found | +| 409 | solution_mode_unavailable, form_not_configured, version_conflict, form_revision_conflict, already_in_program_team, ambiguous_application | +| 409 | team_frozen, team_min_size, team_max_size, already_solution_member, duplicate_pending_invite, invite_already_processed | +| 409 | captain_cannot_leave, captain_cannot_be_removed, captain_already_selected, not_solution_member | +| 409 | submission_closed, already_submitted, not_competitive, field_in_use, template_locked | + +Это HTTP контракты новых endpoints. Существующие статусы старых /projects/, /invites/ и submit связи не меняются. Повторное действие с удаленным приглашением дает 404. Повтор accept/decline сохраненной обработанной строки — 409 invite_already_processed. + +Приоритет проверок: аутентификация → существование и видимость → право действия → финальное состояние → ожидаемые ревизии → валидация текущих бизнес правил и значений → запись. На уже сданной работе submit дает already_submitted, edit дает team_frozen; на уже обработанном приглашении accept/decline дают invite_already_processed. Внутри блока нарушений дедлайн проверяется перед составом и полями. Синтаксическая проверка запроса может выполняться до загрузки объекта. + +## Транзакции и совместимость + +Для новых writers порядок блокировок: PartnerProgram → ProgramSolution → затронутые SolutionInvitation в порядке ID → определения формы и профиль/записи участников в согласованном порядке. Config writer начинает с Program и блокирует затронутые решения в порядке ID перед определениями. Все writers работают в транзакции и повторяют проверки после блокировок. Фактический порядок внутренних таблиц фиксируется совместно в BE01–BE04; инверсия между endpoint запрещена. + +Legacy порядок Project → Program → program links не меняется молча. Новый writer не пытается получить блокировку старого Project, уже удерживая Program. Проверка старого состава выполняется чтением под общей сериализацией Program. Переход программы на единственный источник записи осуществляется отдельно в BE10 с остановкой старых writers и явной таблицей соответствий. + +После commit отправляются существующие уведомления и события; повторный submit или ошибка не создает их снова. Начисление оценок и выдача результата не входят в submit транзакцию. + +Не подменять старые поля partner_program/project/program_link_id новыми числовыми ID. Старый и новый ID могут совпасть по значению, поэтому тип ссылки всегда задан контекстом или kind. current_application для новой навигации берется из solution-context; старые endpoints остаются в legacy adapter. + +## Карта потребителей + +| Текущий участок | Изменение | Задача | +| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| Project и PartnerProgramProject, profile.project | Владение данными переносится в ProgramSolution; профиль регистрации сохраняется | BE01, BE08, FE01 | +| Collaborator и projects.team_policy/team_service | Общая policy, новый владелец участников; счетчики без дубля капитана | BE02, FE04 | +| Invite и текущий inbox | Новые приглашения через SolutionInvitation и /solution-invitations/; старые /invites/ работают прежним способом | BE03, BE08, FE05 | +| PartnerProgramField и program field values | Definitions переиспользуются; ответы нового решения — typed JSON | BE04, BE05, FE02, FE03 | +| files.UserFile и /files/ | Текущий upload, проверка привязки и защита использования после сдачи | BE05, BE06, FE03 | +| Link-scoped submit | Новая команда submit с теми же правилами и снимком | BE06, FE06 | +| ProjectExpertAssignment, ProjectScore и результаты | Привязка к решению без смены критериев и формул | BE07, FE07 | +| Management, role widgets, case analytics, exports | Чтение решения; признак материала через purpose; нет двойного учета | BE08, BE09, FE08 | +| Уведомления и глубокие ссылки | Явная ссылка kind:solution с program_id и solution_id | BE08, FE05, FE08 | +| История программ | legacy adapter и таблица old link → new solution | BE10 | +| Чаты и вакансии программного Project | Не создаются автоматически для нового решения; сохраненный доступ к существующим связанным объектам проверяется до переноса | C00 карта, BE08, BE10 | + +Чаты и вакансии не являются зависимостью формы, приглашения и submit нового решения. Полный перенос их функциональности на новую сущность оформляется отдельно; существующие объекты старых программ не удаляются и не теряют маршрут. Если пилотной программе нужны эти функции, их готовность проверяется до выбора этой программы для REL01. + +Контракт оценивания, аналитики и массовых административных действий расширяется в BE07–BE09, без изменения C00 модели владения. Их точные payload не объявляются определенными схемой Solution. Это отдельные endpoints, не произвольные поля в answer JSON. + +## Параллельная разработка + +Backend реализует операции по operationId из openapi.json. Frontend использует те же схемы и examples.json; сценарии независимы, не описывают изменяемую in-memory базу. Сначала можно собрать FE01 и экранные моки FE02–FE06, пока идут BE01–BE06. + +Фикстуры содержат успешные и ошибочные состояния, включая неполный и готовый черновик, поданную работу, неконкурсный режим, закрытый дедлайн, приглашения, конфликты ревизий, 0 и false. Даты и ID фиктивные; моки с таймером используют собственные контролируемые часы, а не текущую дату компьютера. + +Изменение имен полей, required, статусов или формы ответа требует синхронного обновления этого описания, OpenAPI и примеров. Добавление совместимых endpoints или полей — minor версия; ломающее изменение согласуется до интеграции. Номер ревизии формы не используется как версия API. + +## Проверка пакета + +Для проверки нужны dev-зависимости jsonschema и openapi-spec-validator (`poetry install`). Они не нужны для обработки запросов продукта. + +```text +python validate_contract.py +``` + +Скрипт проверяет корректность OpenAPI, все локальные refs и схемы, примеры request/response, уникальность operationId, базовые инварианты состава и snapshot. Реальные ответы BE01 дополнительно проверяются JSON Schema в `partner_programs.tests.test_solution_foundation`. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json index 4d3a86d5a..ecb794717 100644 --- a/docs/contracts/program-solutions-v1/examples.json +++ b/docs/contracts/program-solutions-v1/examples.json @@ -1,5 +1,5 @@ { - "contract_version": "1.0.0", + "contract_version": "1.1.0", "description": "Независимые примеры, не последовательность операций; фиктивные данные.", "scenarios": [ { @@ -224,7 +224,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": true + "can_submit": true, + "can_leave": false }, "readiness": { "is_ready": false, @@ -424,7 +425,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": true + "can_submit": true, + "can_leave": false }, "readiness": { "is_ready": true, @@ -617,7 +619,8 @@ "can_edit": false, "can_manage_team": false, "can_invite": false, - "can_submit": false + "can_submit": false, + "can_leave": false }, "readiness": { "is_ready": true, @@ -804,7 +807,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": false + "can_submit": false, + "can_leave": false }, "readiness": null } @@ -988,7 +992,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": false + "can_submit": false, + "can_leave": false }, "readiness": { "is_ready": false, @@ -1154,7 +1159,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": true + "can_submit": true, + "can_leave": false }, "readiness": { "is_ready": false, @@ -1367,7 +1373,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": true + "can_submit": true, + "can_leave": false }, "readiness": { "is_ready": false, @@ -1587,7 +1594,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": true + "can_submit": true, + "can_leave": false }, "readiness": { "is_ready": true, @@ -1784,7 +1792,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": true + "can_submit": true, + "can_leave": false }, "readiness": { "is_ready": false, @@ -1988,7 +1997,8 @@ "can_edit": false, "can_manage_team": false, "can_invite": false, - "can_submit": false + "can_submit": false, + "can_leave": false }, "readiness": { "is_ready": true, @@ -3218,7 +3228,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": true + "can_submit": true, + "can_leave": false }, "readiness": { "is_ready": true, @@ -3811,7 +3822,8 @@ "can_edit": false, "can_manage_team": false, "can_invite": false, - "can_submit": false + "can_submit": false, + "can_leave": true }, "readiness": { "is_ready": false, @@ -4011,7 +4023,8 @@ "can_edit": false, "can_manage_team": false, "can_invite": false, - "can_submit": false + "can_submit": false, + "can_leave": false }, "readiness": null } diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json index f61c50d2a..045e53ba8 100644 --- a/docs/contracts/program-solutions-v1/openapi.json +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -3,7 +3,7 @@ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "PROCOLLAB Program Solutions C00", - "version": "1.0.0", + "version": "1.1.0", "description": "Контракт для реализации и моков. Новые endpoints пока не реализованы. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ @@ -6647,6 +6647,10 @@ }, "can_submit": { "type": "boolean" + }, + "can_leave": { + "type": "boolean", + "description": "Собственный выход принятого участника: false для капитана, сданного решения и отключенного режима. Необязательное совместимое поле; при отсутствии клиент считает false." } }, "required": [ diff --git a/docs/product/program-solutions-fe04-fe05.md b/docs/product/program-solutions-fe04-fe05.md new file mode 100644 index 000000000..cd0649878 --- /dev/null +++ b/docs/product/program-solutions-fe04-fe05.md @@ -0,0 +1,19 @@ + + +# FE04–FE05: команда и приглашения решения + +Реализация согласованного D00 v3 и C00 1.1.0. Команда принадлежит заявке ProgramSolution, отдельный Project или Team не создается. Вторая вкладка показывает принятых участников и ожидающие приглашения. Управление составом, собственный выход и передача капитанства используют серверные разрешения и ожидаемую версию решения. + +Окно приглашения переиспользует существующее представление проекта: поиск имени/фамилии от двух символов через каталог участников конкретной программы, выбранный пользователь, 13 подсказок ролей и свободный ввод. ProjectMemberInviteModal остается проектным адаптером; общий MemberInviteForm не подставляет фиктивный projectId. Ошибка сохраняет выбор, роль и запрос. Участник появляется в составе только после принятия. + +Входящие приглашения добавлены в существующие уведомления desktop/mobile. Карточка переиспользует нейтральное представление InviteManageCard и показывает программу и роль. Принятие открывает ссылку Solution только из успешного серверного ответа. Список и активные запросы очищаются при смене пользователя; опрос останавливается при уничтожении Office. Участнику не раскрывается приватный список pending-получателей команды. + +Обновление команды не уничтожает страницу или локальные поля решения. Старые ответы запросов отменяются при смене программы/заявки. Конфликт ревизии обновляет серверные права; изменяемая роль и выбранный получатель сохраняются для осмысленного повтора. Замороженная заявка оставляет только разрешенный отзыв pending; получатель может отклонить его. + +Исправлен генератор C00: required-only ветви anyOf больше не стирают обязательные поля PATCH-команд до unknown. Отдельные положительные и отрицательные TypeScript-примеры входят в test:ci через test:solution-contract. Необязательный can_leave при отсутствии считается false; остальные URL и payload C00 сохранены. + +Проверено: 2512 тестов в 459 файлах, строгая компиляция примеров контрактов, ESLint без ошибок (6 прежних предупреждений), Stylelint двух измененных SCSS, development build. Canonical LF проверка Prettier 3.8.3 соответствует package-lock и не нашла ошибок в 1906 отслеживаемых файлах; новые файлы форматировались отдельно. Старые тесты проектных приглашений и карточек сохранены. + +Интеграция: реальная локальная PostgreSQL/Django и JWT, два тестовых пользователя; поиск, создание pending, редактирование роли, принятие через уведомления, отзыв, передача капитанства и выход. Проверены desktop и 390 px, отсутствие горизонтального overflow и JavaScript-ошибок. Визуальная сверка использует Mont, строки/иконки существующего проекта и согласованные вкладки. Встроенный CUA недоступен из-за ошибки записи kernel assets, поэтому применен локальный Playwright с Edge. + +Форма, сохранение ответов, общие действия сдачи и организаторские поля продолжаются следующими FE02/FE03/FE06. Эта поставка сама по себе не завершает всю новую сущность. diff --git a/package.json b/package.json index 9e71d1564..1299b7f58 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "watch": "ng build --watch social_platform --configuration=development", "build:sprite": "svg-sprite -s --dest=projects/social_platform/src/assets/icons \"projects/social_platform/src/assets/icons/svg/**/*.svg\"", "test": "ng test", - "test:ci": "vitest run", + "test:ci": "vitest run && npm run test:solution-contract", + "test:solution-contract": "tsc --noEmit --strict --skipLibCheck --target ES2022 --module ES2022 --moduleResolution bundler --types node tools/verify-solution-contract-types.ts", "lint:ts": "eslint projects/**/*.ts", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts b/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts new file mode 100644 index 000000000..69baf16e2 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts @@ -0,0 +1,134 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { of, Subject } from "rxjs"; +import { fail, ok } from "@domain/shared/result.type"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { + SolutionInvitation, + SolutionInvitationActionResult, + SolutionInvitationCreated, +} from "@domain/solution/solution-contract.model"; +import { + DecideSolutionInvitationUseCase, + GetMySolutionInvitationsUseCase, +} from "../use-cases/solution-team.use-cases"; +import { SolutionInvitationInboxService } from "./solution-invitation-inbox.service"; + +const source = ( + solutionContractScenarios.find(s => s.name === "create_invitation")! + .response as SolutionInvitationCreated +).invitation; +const invitation: SolutionInvitation = { + ...source, + permissions: { canEdit: false, canRevoke: false, canAccept: true, canDecline: true }, +}; +const page = { count: 1, next: null, previous: null, results: [invitation] }; + +describe("Входящие приглашения решений", () => { + const get = { execute: vi.fn() }, + decide = { execute: vi.fn() }; + let inbox: SolutionInvitationInboxService; + beforeEach(() => { + vi.useFakeTimers(); + get.execute.mockReset().mockReturnValue(of(ok(page))); + decide.execute.mockReset(); + TestBed.configureTestingModule({ + providers: [ + SolutionInvitationInboxService, + { provide: GetMySolutionInvitationsUseCase, useValue: get }, + { provide: DecideSolutionInvitationUseCase, useValue: decide }, + ], + }); + inbox = TestBed.inject(SolutionInvitationInboxService); + }); + afterEach(() => { + inbox.bindUser(null); + vi.clearAllTimers(); + vi.useRealTimers(); + }); + + it("deduplicates desktop/mobile binding and polls only while a user is bound", async () => { + inbox.bindUser(invitation.recipient.id); + inbox.bindUser(invitation.recipient.id); + await vi.advanceTimersByTimeAsync(0); + expect(get.execute).toHaveBeenCalledTimes(1); + expect(inbox.count()).toBe(1); + await vi.advanceTimersByTimeAsync(30000); + expect(get.execute).toHaveBeenCalledTimes(2); + inbox.bindUser(null); + expect(inbox.items()).toEqual([]); + await vi.advanceTimersByTimeAsync(30000); + expect(get.execute).toHaveBeenCalledTimes(2); + }); + + it("user change cancels old reads and cannot show another user's invitation", async () => { + const old = new Subject>>(); + get.execute + .mockReturnValueOnce(old) + .mockReturnValue( + of( + ok({ + count: 1, + next: null, + previous: null, + results: [{ ...invitation, id: 702, recipient: { ...invitation.recipient, id: 21 } }], + }), + ), + ); + inbox.bindUser(invitation.recipient.id); + await vi.advanceTimersByTimeAsync(0); + inbox.bindUser(21); + await vi.advanceTimersByTimeAsync(0); + old.next(ok(page)); + expect(inbox.items().map(row => [row.id, row.recipient.id])).toEqual([[702, 21]]); + }); + + it("accept remains pending until server success and then synchronizes without optimistic membership", async () => { + inbox.bindUser(invitation.recipient.id); + await vi.advanceTimersByTimeAsync(0); + const pending = new Subject>>(); + decide.execute.mockReturnValue(pending); + const received = vi.fn(); + inbox.action(invitation, "accept").subscribe(received); + expect(inbox.items()).toHaveLength(1); + expect(received).not.toHaveBeenCalled(); + expect(inbox.pendingId()).toBe(invitation.id); + inbox.action(invitation, "accept").subscribe(); + expect(decide.execute).toHaveBeenCalledTimes(1); + get.execute.mockReturnValue(of(ok({ count: 0, next: null, previous: null, results: [] }))); + pending.next( + ok({ + invitation: { ...invitation, status: "accepted", version: 2 }, + application: invitation.solution, + solutionVersion: 3, + }), + ); + pending.complete(); + expect(inbox.items()).toEqual([]); + expect(inbox.pendingId()).toBeNull(); + expect(received).toHaveBeenCalledWith(expect.objectContaining({ ok: true })); + }); + + it("error keeps the invitation and its reason after a successful background refresh", async () => { + inbox.bindUser(invitation.recipient.id); + await vi.advanceTimersByTimeAsync(0); + decide.execute.mockReturnValue( + of( + fail({ + code: "already_in_program_team", + detail: "Вы уже состоите в другой команде.", + errors: [], + meta: {}, + }), + ), + ); + const received = vi.fn(); + inbox.action(invitation, "accept").subscribe(received); + expect(inbox.items()).toHaveLength(1); + expect(inbox.error()).toBe("Вы уже состоите в другой команде."); + expect(received).toHaveBeenCalledWith(expect.objectContaining({ ok: false })); + inbox.refresh(); + expect(inbox.error()).toBe(""); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.ts b/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.ts new file mode 100644 index 000000000..f3f2d0fff --- /dev/null +++ b/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.ts @@ -0,0 +1,144 @@ +/** @format */ + +import { computed, DestroyRef, inject, Injectable, signal } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { + defer, + distinctUntilChanged, + finalize, + map, + merge, + Observable, + of, + Subject, + switchMap, + takeUntil, + tap, + timer, +} from "rxjs"; +import { + SolutionApiError, + SolutionInvitation, + SolutionInvitationActionResult, +} from "@domain/solution/solution-contract.model"; +import { fail, ok, Result } from "@domain/shared/result.type"; +import { + DecideSolutionInvitationUseCase, + GetMySolutionInvitationsUseCase, +} from "../use-cases/solution-team.use-cases"; + +/** Один входящий список для desktop/mobile; состояние меняется вместе с пользователем. */ +@Injectable() +export class SolutionInvitationInboxService { + readonly items = signal([]); + readonly loading = signal(false); + private readonly loadError = signal(""); + private readonly actionError = signal(""); + readonly error = computed(() => this.actionError() || this.loadError()); + readonly pendingId = signal(null); + readonly count = computed(() => this.items().length); + private readonly userChanges = new Subject(); + private readonly refreshes = new Subject(); + private readonly cancelActions = new Subject(); + private readonly get = inject(GetMySolutionInvitationsUseCase); + private readonly decide = inject(DecideSolutionInvitationUseCase); + private readonly destroyRef = inject(DestroyRef); + private currentUser: number | null = null; + + constructor() { + this.userChanges + .pipe( + distinctUntilChanged(), + switchMap(userId => { + this.cancelActions.next(); + this.currentUser = userId; + this.items.set([]); + this.loadError.set(""); + this.actionError.set(""); + this.loading.set(false); + this.pendingId.set(null); + if (userId === null) return of(null); + return merge(timer(0, 30000), this.refreshes).pipe( + switchMap(() => { + this.loading.set(true); + return this.readAll().pipe(finalize(() => this.loading.set(false))); + }), + ); + }), + takeUntilDestroyed(), + ) + .subscribe(result => { + if (!result) return; + if (result.ok) { + this.items.set(result.value); + this.loadError.set(""); + } else this.loadError.set(result.error.detail); + }); + } + + bindUser(userId: number | null): void { + this.userChanges.next(userId); + } + refresh(clearAction = true): void { + if (clearAction) this.actionError.set(""); + this.refreshes.next(); + } + + private readAll(offset = 0): Observable> { + return this.get.execute({ status: "pending", limit: 100, offset }).pipe( + switchMap(result => { + if (!result.ok) return of(fail(result.error)); + if (!result.value.next) return of(ok(result.value.results)); + if (!result.value.results.length) + return of( + fail({ + code: "invalid_response", + detail: "Не удалось обновить приглашения.", + errors: [], + meta: {}, + }), + ); + return this.readAll(offset + result.value.results.length).pipe( + map(next => (next.ok ? ok([...result.value.results, ...next.value]) : fail(next.error))), + ); + }), + ); + } + + action( + invitation: SolutionInvitation, + decision: "accept" | "decline", + ): Observable> { + return defer(() => { + const allowed = + decision === "accept" + ? invitation.permissions.canAccept + : invitation.permissions.canDecline; + if (!allowed || this.pendingId() !== null || this.currentUser !== invitation.recipient.id) + return of( + fail({ + code: "forbidden", + detail: "Действие с приглашением сейчас недоступно.", + errors: [], + meta: {}, + }), + ); + this.pendingId.set(invitation.id); + this.actionError.set(""); + return this.decide.execute(invitation.id, decision, invitation.version).pipe( + takeUntil(this.cancelActions), + takeUntilDestroyed(this.destroyRef), + tap(result => { + if (result.ok) { + this.items.update(items => items.filter(item => item.id !== invitation.id)); + this.refresh(false); + } else { + this.actionError.set(result.error.detail); + this.refresh(false); + } + }), + finalize(() => this.pendingId.set(null)), + ); + }); + } +} diff --git a/projects/social_platform/src/app/api/solution/solution-team-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-team-contract.spec.ts new file mode 100644 index 000000000..f8245607f --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-team-contract.spec.ts @@ -0,0 +1,178 @@ +/** @format */ + +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom } from "rxjs"; +import snakecaseKeys from "snakecase-keys"; +import { CamelcaseInterceptor } from "@core/lib/interceptors/camelcase.interceptor"; +import { API_URL } from "@core/lib/providers"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { SolutionRepository } from "@infrastructure/repository/solution/solution.repository"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { + CreateSolutionInvitationUseCase, + DecideSolutionInvitationUseCase, + GetMySolutionInvitationsUseCase, + GetSolutionInvitationsUseCase, + LeaveSolutionUseCase, + RemoveSolutionMemberUseCase, + RevokeSolutionInvitationUseCase, + TransferSolutionCaptainUseCase, + UpdateSolutionInvitationUseCase, +} from "./use-cases/solution-team.use-cases"; + +describe("C00 team and invitation wire contracts", () => { + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: API_URL, useValue: "https://local.test" }, + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: LoggerService, useValue: { warn: vi.fn() } }, + { provide: SolutionRepositoryPort, useExisting: SolutionRepository }, + ], + }), + ); + afterEach(() => TestBed.inject(HttpTestingController).verify()); + + it("sends selected recipient ID and role without Project/Team IDs", async () => { + const result = firstValueFrom( + TestBed.inject(CreateSolutionInvitationUseCase).execute(12, 301, { + expectedSolutionVersion: 8, + recipientUserId: 9, + role: "Аналитик", + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/301/invitations/", + ); + expect(request.request.method).toBe("POST"); + expect(request.request.body).toEqual({ + expected_solution_version: 8, + recipient_user_id: 9, + role: "Аналитик", + }); + const expected = solutionContractScenarios.find(s => s.name === "create_invitation")!.response; + request.flush(snakecaseKeys(expected as object, { deep: true })); + expect(await result).toEqual({ ok: true, value: expected }); + }); + + it("preserves both invitation and solution versions in PATCH and DELETE query", async () => { + const updated = firstValueFrom( + TestBed.inject(UpdateSolutionInvitationUseCase).execute(701, { + expectedVersion: 1, + expectedSolutionVersion: 8, + role: null, + }), + ); + const patch = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/solution-invitations/701/", + ); + expect(patch.request.method).toBe("PATCH"); + expect(patch.request.body).toEqual({ + expected_version: 1, + expected_solution_version: 8, + role: null, + }); + patch.flush({ invitation: { id: 701, version: 2 }, solution_version: 9 }); + expect(await updated).toEqual({ + ok: true, + value: { invitation: { id: 701, version: 2 }, solutionVersion: 9 }, + }); + const revoked = firstValueFrom( + TestBed.inject(RevokeSolutionInvitationUseCase).execute(701, 2, 9), + ); + const remove = TestBed.inject(HttpTestingController).expectOne(r => + r.url.endsWith("/solution-invitations/701/"), + ); + expect(remove.request.method).toBe("DELETE"); + expect(remove.request.params.get("expected_version")).toBe("2"); + expect(remove.request.params.get("expected_solution_version")).toBe("9"); + remove.flush(null, { status: 204, statusText: "No Content" }); + expect((await revoked).ok).toBe(true); + }); + + it.each(["accept", "decline"] as const)( + "%s needs only the invitation version and does not retry POST", + async decision => { + const result = firstValueFrom( + TestBed.inject(DecideSolutionInvitationUseCase).execute(701, decision, 2), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + `https://local.test/solution-invitations/701/${decision}/`, + ); + expect(request.request.body).toEqual({ expected_version: 2 }); + request.flush( + { code: "team_frozen", detail: "Команда зафиксирована.", errors: [], meta: {} }, + { status: 409, statusText: "Conflict" }, + ); + expect(await result).toMatchObject({ ok: false, error: { code: "team_frozen" } }); + TestBed.inject(HttpTestingController).expectNone(() => true); + }, + ); + + it("routes remove/leave/transfer by exact program, solution and user IDs", async () => { + const removed = firstValueFrom( + TestBed.inject(RemoveSolutionMemberUseCase).execute(12, 301, 9, 8), + ); + const remove = TestBed.inject(HttpTestingController).expectOne(r => + r.url.endsWith("/programs/12/solutions/301/members/9/"), + ); + expect(remove.request.params.get("expected_solution_version")).toBe("8"); + remove.flush({ solution_id: 301, solution_version: 9, updated_at: "2026-10-24T10:00:00Z" }); + expect(await removed).toMatchObject({ ok: true, value: { solutionVersion: 9 } }); + const transferred = firstValueFrom( + TestBed.inject(TransferSolutionCaptainUseCase).execute(12, 301, { + expectedSolutionVersion: 9, + newCaptainUserId: 10, + }), + ); + const transfer = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/301/captain/", + ); + expect(transfer.request.body).toEqual({ + expected_solution_version: 9, + new_captain_user_id: 10, + }); + transfer.flush({ solution_id: 301, solution_version: 10, updated_at: "2026-10-24T10:00:01Z" }); + expect((await transferred).ok).toBe(true); + const left = firstValueFrom( + TestBed.inject(LeaveSolutionUseCase).execute(12, 301, { expectedSolutionVersion: 10 }), + ); + const leave = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/301/leave/", + ); + expect(leave.request.body).toEqual({ expected_solution_version: 10 }); + leave.flush(null, { status: 204, statusText: "No Content" }); + expect((await left).ok).toBe(true); + }); + + it("uses independent inbox and captain list with status/pagination", async () => { + const inbox = firstValueFrom( + TestBed.inject(GetMySolutionInvitationsUseCase).execute({ + status: "pending", + limit: 8, + offset: 16, + }), + ); + const incoming = TestBed.inject(HttpTestingController).expectOne( + r => r.url === "https://local.test/solution-invitations/", + ); + expect(incoming.request.params.get("offset")).toBe("16"); + expect(incoming.request.params.get("status")).toBe("pending"); + incoming.flush({ count: 0, next: null, previous: null, results: [] }); + expect((await inbox).ok).toBe(true); + const pending = firstValueFrom( + TestBed.inject(GetSolutionInvitationsUseCase).execute(12, 301, { status: "accepted" }), + ); + const outgoing = TestBed.inject(HttpTestingController).expectOne(r => + r.url.endsWith("/programs/12/solutions/301/invitations/"), + ); + expect(outgoing.request.params.get("status")).toBe("accepted"); + outgoing.flush({ count: 0, next: null, previous: null, results: [] }); + expect((await pending).ok).toBe(true); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-team.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-team.use-cases.ts new file mode 100644 index 000000000..38a4cc70e --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-team.use-cases.ts @@ -0,0 +1,95 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { + SolutionInvitationQuery, + SolutionRepositoryPort, +} from "@domain/solution/ports/solution.repository.port"; +import { + SolutionCaptainCommand, + SolutionTeamCommand, + SolutionCreateInvitation, + SolutionUpdateInvitation, +} from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class RemoveSolutionMemberUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, userId: number, version: number) { + return solutionRequestResult(() => + this.repository.removeMember(programId, solutionId, userId, version), + ); + } +} +@Injectable({ providedIn: "root" }) +export class LeaveSolutionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: SolutionTeamCommand) { + return solutionRequestResult(() => this.repository.leave(programId, solutionId, command)); + } +} +@Injectable({ providedIn: "root" }) +export class TransferSolutionCaptainUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: SolutionCaptainCommand) { + return solutionRequestResult(() => + this.repository.transferCaptain(programId, solutionId, command), + ); + } +} +@Injectable({ providedIn: "root" }) +export class GetSolutionInvitationsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, query?: SolutionInvitationQuery) { + return solutionRequestResult(() => this.repository.invitations(programId, solutionId, query)); + } +} +@Injectable({ providedIn: "root" }) +export class CreateSolutionInvitationUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: SolutionCreateInvitation) { + return solutionRequestResult(() => + this.repository.createInvitation(programId, solutionId, command), + ); + } +} +@Injectable({ providedIn: "root" }) +export class GetMySolutionInvitationsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(query?: SolutionInvitationQuery) { + return solutionRequestResult(() => this.repository.myInvitations(query)); + } +} +@Injectable({ providedIn: "root" }) +export class GetSolutionInvitationUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(invitationId: number) { + return solutionRequestResult(() => this.repository.invitation(invitationId)); + } +} +@Injectable({ providedIn: "root" }) +export class UpdateSolutionInvitationUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(invitationId: number, command: SolutionUpdateInvitation) { + return solutionRequestResult(() => this.repository.updateInvitation(invitationId, command)); + } +} +@Injectable({ providedIn: "root" }) +export class RevokeSolutionInvitationUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(invitationId: number, version: number, solutionVersion: number) { + return solutionRequestResult(() => + this.repository.revokeInvitation(invitationId, version, solutionVersion), + ); + } +} +@Injectable({ providedIn: "root" }) +export class DecideSolutionInvitationUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(invitationId: number, decision: "accept" | "decline", version: number) { + return solutionRequestResult(() => + this.repository.decideInvitation(invitationId, decision, version), + ); + } +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts index 45847098d..335926e05 100644 --- a/projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts +++ b/projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts @@ -7,7 +7,7 @@ import { CreateSolutionCommand } from "@domain/solution/solution-contract.model" import { fail, ok } from "@domain/shared/result.type"; import { solutionError } from "../solution-error"; -function requestResult(request: () => Observable) { +export function solutionRequestResult(request: () => Observable) { return defer(request).pipe( map(value => ok(value)), catchError(error => of(fail(solutionError(error)))), @@ -18,7 +18,7 @@ function requestResult(request: () => Observable) { export class GetSolutionContextUseCase { private readonly repository = inject(SolutionRepositoryPort); execute(programId: number) { - return requestResult(() => this.repository.context(programId)); + return solutionRequestResult(() => this.repository.context(programId)); } } @@ -26,7 +26,7 @@ export class GetSolutionContextUseCase { export class CreateSolutionUseCase { private readonly repository = inject(SolutionRepositoryPort); execute(programId: number, command: CreateSolutionCommand = {}) { - return requestResult(() => this.repository.create(programId, command)); + return solutionRequestResult(() => this.repository.create(programId, command)); } } @@ -34,6 +34,6 @@ export class CreateSolutionUseCase { export class GetSolutionUseCase { private readonly repository = inject(SolutionRepositoryPort); execute(programId: number, solutionId: number) { - return requestResult(() => this.repository.get(programId, solutionId)); + return solutionRequestResult(() => this.repository.get(programId, solutionId)); } } diff --git a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts index da5e9f6e7..1c2aa436c 100644 --- a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts +++ b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts @@ -6,6 +6,15 @@ import { ProgramSolution, SolutionContext, SolutionPage, + SolutionCaptainCommand, + SolutionTeamCommand, + SolutionMutationReceipt, + SolutionInvitation, + SolutionInvitationPage, + SolutionCreateInvitation, + SolutionUpdateInvitation, + SolutionInvitationCreated, + SolutionInvitationActionResult, } from "../solution-contract.model"; export abstract class SolutionRepositoryPort { @@ -13,8 +22,56 @@ export abstract class SolutionRepositoryPort { abstract create(programId: number, command: CreateSolutionCommand): Observable; abstract get(programId: number, solutionId: number): Observable; abstract list(programId: number, query?: SolutionListQuery): Observable; + abstract removeMember( + programId: number, + solutionId: number, + userId: number, + expectedSolutionVersion: number, + ): Observable; + abstract leave( + programId: number, + solutionId: number, + command: SolutionTeamCommand, + ): Observable; + abstract transferCaptain( + programId: number, + solutionId: number, + command: SolutionCaptainCommand, + ): Observable; + abstract invitations( + programId: number, + solutionId: number, + query?: SolutionInvitationQuery, + ): Observable; + abstract createInvitation( + programId: number, + solutionId: number, + command: SolutionCreateInvitation, + ): Observable; + abstract myInvitations(query?: SolutionInvitationQuery): Observable; + abstract invitation(invitationId: number): Observable; + abstract updateInvitation( + invitationId: number, + command: SolutionUpdateInvitation, + ): Observable; + abstract revokeInvitation( + invitationId: number, + expectedVersion: number, + expectedSolutionVersion: number, + ): Observable; + abstract decideInvitation( + invitationId: number, + decision: "accept" | "decline", + expectedVersion: number, + ): Observable; } +export type SolutionInvitationQuery = { + status?: "pending" | "accepted" | "declined"; + limit?: number; + offset?: number; +}; + export type SolutionListQuery = { search?: string; submission?: "all" | "submitted" | "not_submitted"; diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts index 4b1a90f52..01e3ba3bd 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts @@ -227,6 +227,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: false, @@ -422,6 +423,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: true, @@ -610,6 +612,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: false, canInvite: false, canSubmit: false, + canLeave: false, }, readiness: { isReady: true, @@ -792,6 +795,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: false, + canLeave: false, }, readiness: null, }, @@ -971,6 +975,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: false, + canLeave: false, }, readiness: { isReady: false, @@ -1132,6 +1137,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: false, @@ -1340,6 +1346,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: false, @@ -1555,6 +1562,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: true, @@ -1747,6 +1755,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: false, @@ -1946,6 +1955,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: false, canInvite: false, canSubmit: false, + canLeave: false, }, readiness: { isReady: true, @@ -3149,6 +3159,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: true, @@ -3727,6 +3738,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: false, canInvite: false, canSubmit: false, + canLeave: true, }, readiness: { isReady: false, @@ -3922,6 +3934,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: false, canInvite: false, canSubmit: false, + canLeave: false, }, readiness: null, }, diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts index 946f7e56e..533a7d36f 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts @@ -1,6 +1,6 @@ /** @format */ -// Сгенерировано tools/generate-solution-contract.py из C00 1.0.0. +// Сгенерировано tools/generate-solution-contract.py из C00 1.1.0. // camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются. export type SolutionUser = { @@ -191,6 +191,7 @@ export type SolutionPermissions = { canManageTeam: boolean; canInvite: boolean; canSubmit: boolean; + canLeave?: boolean; }; export type SolutionSubmission = { @@ -237,7 +238,19 @@ export type CreateSolutionCommand = { name?: string; }; -export type UpdateSolutionCommand = unknown | unknown; +export type UpdateSolutionCommand = + | { + expectedVersion: number; + expectedFormRevision: string; + name: string; + answers?: Array; + } + | { + expectedVersion: number; + expectedFormRevision: string; + name?: string; + answers: Array; + }; export type SubmitSolutionCommand = { expectedVersion: number; @@ -284,7 +297,28 @@ export type SolutionCreateInvitation = { motivationalLetter?: string | null; }; -export type SolutionUpdateInvitation = unknown | unknown | unknown; +export type SolutionUpdateInvitation = + | { + expectedVersion: number; + expectedSolutionVersion: number; + role: string | null; + specialization?: string | null; + motivationalLetter?: string | null; + } + | { + expectedVersion: number; + expectedSolutionVersion: number; + role?: string | null; + specialization: string | null; + motivationalLetter?: string | null; + } + | { + expectedVersion: number; + expectedSolutionVersion: number; + role?: string | null; + specialization?: string | null; + motivationalLetter: string | null; + }; export type SolutionInvitationAction = { expectedVersion: number; diff --git a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts index 3efb0303d..34b3dbe24 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts @@ -8,9 +8,19 @@ import { ProgramSolution, SolutionContext, SolutionPage, + SolutionCaptainCommand, + SolutionTeamCommand, + SolutionMutationReceipt, + SolutionInvitation, + SolutionInvitationPage, + SolutionCreateInvitation, + SolutionUpdateInvitation, + SolutionInvitationCreated, + SolutionInvitationActionResult, } from "@domain/solution/solution-contract.model"; import { SolutionListQuery, + SolutionInvitationQuery, validSolutionId, } from "@domain/solution/ports/solution.repository.port"; @@ -45,4 +55,91 @@ export class SolutionHttpAdapter { } return this.api.get(this.path(programId), params); } + + private solutionPath(programId: number, solutionId: number): string { + if (!validSolutionId(solutionId)) throw new RangeError("Invalid solution ID"); + return `${this.path(programId)}${solutionId}/`; + } + + private invitationPath(invitationId: number): string { + if (!validSolutionId(invitationId)) throw new RangeError("Invalid invitation ID"); + return `/solution-invitations/${invitationId}/`; + } + + private invitationQuery(query: SolutionInvitationQuery = {}): HttpParams { + let params = new HttpParams(); + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) params = params.set(key, value); + } + return params; + } + + removeMember( + programId: number, + solutionId: number, + userId: number, + expectedSolutionVersion: number, + ) { + if (!validSolutionId(userId)) throw new RangeError("Invalid member ID"); + return this.api.delete( + `${this.solutionPath(programId, solutionId)}members/${userId}/`, + new HttpParams().set("expected_solution_version", expectedSolutionVersion), + ); + } + + leave(programId: number, solutionId: number, command: SolutionTeamCommand) { + return this.api.post(`${this.solutionPath(programId, solutionId)}leave/`, command); + } + + transferCaptain(programId: number, solutionId: number, command: SolutionCaptainCommand) { + return this.api.post( + `${this.solutionPath(programId, solutionId)}captain/`, + command, + ); + } + + invitations(programId: number, solutionId: number, query?: SolutionInvitationQuery) { + return this.api.get( + `${this.solutionPath(programId, solutionId)}invitations/`, + this.invitationQuery(query), + ); + } + + createInvitation(programId: number, solutionId: number, command: SolutionCreateInvitation) { + return this.api.post( + `${this.solutionPath(programId, solutionId)}invitations/`, + command, + ); + } + + myInvitations(query?: SolutionInvitationQuery) { + return this.api.get( + "/solution-invitations/", + this.invitationQuery(query), + ); + } + + invitation(invitationId: number) { + return this.api.get(this.invitationPath(invitationId)); + } + + updateInvitation(invitationId: number, command: SolutionUpdateInvitation) { + return this.api.patch(this.invitationPath(invitationId), command); + } + + revokeInvitation(invitationId: number, expectedVersion: number, expectedSolutionVersion: number) { + return this.api.delete( + this.invitationPath(invitationId), + new HttpParams() + .set("expected_version", expectedVersion) + .set("expected_solution_version", expectedSolutionVersion), + ); + } + + decideInvitation(invitationId: number, decision: "accept" | "decline", expectedVersion: number) { + return this.api.post( + `${this.invitationPath(invitationId)}${decision}/`, + { expectedVersion }, + ); + } } diff --git a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts index 62471c294..db8ec4ab0 100644 --- a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts @@ -4,8 +4,15 @@ import { inject, Injectable } from "@angular/core"; import { SolutionRepositoryPort, SolutionListQuery, + SolutionInvitationQuery, } from "@domain/solution/ports/solution.repository.port"; -import { CreateSolutionCommand } from "@domain/solution/solution-contract.model"; +import { + CreateSolutionCommand, + SolutionTeamCommand, + SolutionCaptainCommand, + SolutionCreateInvitation, + SolutionUpdateInvitation, +} from "@domain/solution/solution-contract.model"; import { SolutionHttpAdapter } from "../../adapters/solution/solution-http.adapter"; @Injectable({ providedIn: "root" }) @@ -23,4 +30,39 @@ export class SolutionRepository implements SolutionRepositoryPort { list(programId: number, query?: SolutionListQuery) { return this.http.list(programId, query); } + removeMember( + programId: number, + solutionId: number, + userId: number, + expectedSolutionVersion: number, + ) { + return this.http.removeMember(programId, solutionId, userId, expectedSolutionVersion); + } + leave(programId: number, solutionId: number, command: SolutionTeamCommand) { + return this.http.leave(programId, solutionId, command); + } + transferCaptain(programId: number, solutionId: number, command: SolutionCaptainCommand) { + return this.http.transferCaptain(programId, solutionId, command); + } + invitations(programId: number, solutionId: number, query?: SolutionInvitationQuery) { + return this.http.invitations(programId, solutionId, query); + } + createInvitation(programId: number, solutionId: number, command: SolutionCreateInvitation) { + return this.http.createInvitation(programId, solutionId, command); + } + myInvitations(query?: SolutionInvitationQuery) { + return this.http.myInvitations(query); + } + invitation(invitationId: number) { + return this.http.invitation(invitationId); + } + updateInvitation(invitationId: number, command: SolutionUpdateInvitation) { + return this.http.updateInvitation(invitationId, command); + } + revokeInvitation(invitationId: number, expectedVersion: number, expectedSolutionVersion: number) { + return this.http.revokeInvitation(invitationId, expectedVersion, expectedSolutionVersion); + } + decideInvitation(invitationId: number, decision: "accept" | "decline", expectedVersion: number) { + return this.http.decideInvitation(invitationId, decision, expectedVersion); + } } diff --git a/projects/social_platform/src/app/ui/pages/office/nav/nav.component.html b/projects/social_platform/src/app/ui/pages/office/nav/nav.component.html index 986e48d2c..015cde881 100644 --- a/projects/social_platform/src/app/ui/pages/office/nav/nav.component.html +++ b/projects/social_platform/src/app/ui/pages/office/nav/nav.component.html @@ -163,6 +163,7 @@

уведомления

+
    @for (invite of invites(); track invite.id) {
  • diff --git a/projects/social_platform/src/app/ui/pages/office/nav/nav.component.ts b/projects/social_platform/src/app/ui/pages/office/nav/nav.component.ts index 3c72a81c5..f951ee621 100644 --- a/projects/social_platform/src/app/ui/pages/office/nav/nav.component.ts +++ b/projects/social_platform/src/app/ui/pages/office/nav/nav.component.ts @@ -38,6 +38,8 @@ import { A11yModule } from "@angular/cdk/a11y"; import { Overlay } from "@angular/cdk/overlay"; import { desktop } from "@utils/responsive"; import { NotificationListComponent } from "@ui/widgets/notification-list/notification-list.component"; +import { SolutionInvitationInboxComponent } from "@ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component"; +import { SolutionInvitationInboxService } from "@api/solution/facades/solution-invitation-inbox.service"; /** * Компонент навигационного меню @@ -84,6 +86,7 @@ import { NotificationListComponent } from "@ui/widgets/notification-list/notific AsyncPipe, A11yModule, NotificationListComponent, + SolutionInvitationInboxComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -95,6 +98,7 @@ export class NavComponent implements OnInit, OnDestroy { private menuTrigger?: HTMLElement; readonly invites = this.inviteInfoService.invites; + readonly solutionInbox = inject(SolutionInvitationInboxService); readonly logout = output(); constructor( @@ -182,7 +186,7 @@ export class NavComponent implements OnInit, OnDestroy { * Возвращает true если есть приглашения со статусом null (не принято/не отклонено) */ get hasInvites(): boolean { - return this.invites().some(i => i.isAccepted === null); + return this.invites().some(i => i.isAccepted === null) || this.solutionInbox.count() > 0; } /** diff --git a/projects/social_platform/src/app/ui/pages/office/office.component.html b/projects/social_platform/src/app/ui/pages/office/office.component.html index 03663f2bc..d8481b2eb 100644 --- a/projects/social_platform/src/app/ui/pages/office/office.component.html +++ b/projects/social_platform/src/app/ui/pages/office/office.component.html @@ -43,16 +43,28 @@ @if (profile() !== undefined && invites() !== undefined) { + > + + }
diff --git a/projects/social_platform/src/app/ui/pages/office/office.component.spec.ts b/projects/social_platform/src/app/ui/pages/office/office.component.spec.ts index 9f8529638..fd7acd345 100644 --- a/projects/social_platform/src/app/ui/pages/office/office.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/office/office.component.spec.ts @@ -15,6 +15,7 @@ import { ChatUnreadStateService } from "@api/chat/chat-unread-state.service"; import { ProgramShellInfoService } from "@api/program/facades/program-shell-info.service"; import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; import { NotificationService } from "@ui/services/notification/notification.service"; +import { SolutionInvitationInboxService } from "@api/solution/facades/solution-invitation-inbox.service"; describe("OfficeComponent", () => { let component: OfficeComponent; @@ -96,10 +97,22 @@ describe("OfficeComponent", () => { OfficeUIInfoService, AuthUIInfoService, AuthRegisterService, + SolutionInvitationInboxService, ], }, add: { providers: [ + { + provide: SolutionInvitationInboxService, + useValue: { + bindUser: vi.fn(), + count: signal(0), + loading: signal(false), + error: signal(""), + items: signal([]), + pendingId: signal(null), + }, + }, { provide: OfficeInfoService, useValue: officeInfoServiceSpy }, { provide: OfficeUIInfoService, useValue: officeUIInfoServiceSpy }, { provide: AuthUIInfoService, useValue: authUIInfoServiceSpy }, diff --git a/projects/social_platform/src/app/ui/pages/office/office.component.ts b/projects/social_platform/src/app/ui/pages/office/office.component.ts index 250d7fc90..5c936c414 100644 --- a/projects/social_platform/src/app/ui/pages/office/office.component.ts +++ b/projects/social_platform/src/app/ui/pages/office/office.component.ts @@ -7,6 +7,7 @@ import { signal, inject, computed, + effect, ChangeDetectionStrategy, } from "@angular/core"; import { RouterLink, RouterOutlet } from "@angular/router"; @@ -28,6 +29,8 @@ import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; import { AsyncPipe } from "@angular/common"; import { NotificationService } from "@ui/services/notification/notification.service"; import { NotificationListComponent } from "@ui/widgets/notification-list/notification-list.component"; +import { SolutionInvitationInboxComponent } from "@ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component"; +import { SolutionInvitationInboxService } from "@api/solution/facades/solution-invitation-inbox.service"; /** Корневой компонент рабочего пространства с навигацией и управлением состоянием. */ @Component({ @@ -47,12 +50,20 @@ import { NotificationListComponent } from "@ui/widgets/notification-list/notific ProgramSidebarCardComponent, AsyncPipe, NotificationListComponent, + SolutionInvitationInboxComponent, + ], + providers: [ + OfficeInfoService, + OfficeUIInfoService, + AuthUIInfoService, + AuthRegisterService, + SolutionInvitationInboxService, ], - providers: [OfficeInfoService, OfficeUIInfoService, AuthUIInfoService, AuthRegisterService], changeDetection: ChangeDetectionStrategy.OnPush, }) export class OfficeComponent implements OnInit { readonly notificationService = inject(NotificationService); + readonly solutionInbox = inject(SolutionInvitationInboxService); private readonly destroyRef = inject(DestroyRef); private readonly officeInfoService = inject(OfficeInfoService); private readonly officeUIInfoService = inject(OfficeUIInfoService); @@ -85,6 +96,10 @@ export class OfficeComponent implements OnInit { protected currentYear = signal(new Date().getFullYear()); + constructor() { + effect(() => this.solutionInbox.bindUser(this.profile()?.id ?? null)); + } + ngOnInit(): void { this.notificationService.start(); this.destroyRef.onDestroy(() => this.notificationService.stop()); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html new file mode 100644 index 000000000..3f76766c5 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html @@ -0,0 +1,225 @@ + +
+ @if (busy()) { +

Обновляем команду…

+ } +
+
+

Команда заявки

+

Управляйте участниками команды, приглашайте новых и назначайте роли.

+
+ @if (solution().permissions.canInvite) { + + } + @if (solution().permissions.canLeave) { + + } +
+ @if (solution().team.isFrozen) { +

Состав команды зафиксирован после сдачи.

+ } + @if (error() && !editing()) { +

{{ error() }}

+ } +

Участники команды · {{ solution().team.acceptedCount }}

+
    + @for (member of solution().team.members; track member.user.id) { +
  • + +
    + {{ member.user.firstName }} {{ member.user.lastName }}{{ member.isCaptain ? "Капитан команды" : "Участник команды" + }}{{ member.user.id === currentUser()?.id ? " · Вы" : "" }} +
    + {{ member.role || "Роль не указана" }}В команде +
    + @if (solution().permissions.canManageTeam && !member.isCaptain) { + + + } +
    +
  • + } +
+

Ожидают подтверждения · {{ solution().team.pendingCount }}

+

Участники появятся в команде, когда примут приглашение.

+ @if (currentUser()?.id === solution().captainId) { + @if (loading()) { +

Загружаем приглашения…

+ } @else if (listError()) { +

{{ listError() }}

+ + } @else { +
    + @for (invitation of pending(); track invitation.id) { +
  • + +
    + {{ invitation.recipient.firstName }} {{ invitation.recipient.lastName }}Отправлено {{ invitation.createdAt | date: "dd.MM.yyyy" }} +
    + {{ invitation.role || "Роль не указана" }}Приглашение отправлено +
    + @if (invitation.permissions.canEdit) { + + } + @if (invitation.permissions.canRevoke) { + + } +
    +
  • + } @empty { +
  • Нет ожидающих приглашений.
  • + } +
+ } + } @else { +

Список отправленных приглашений доступен капитану.

+ } +
+@if (inviting()) { + +} +@if (editing()) { + + + @if (error()) { +

{{ error() }}

+ } +
+ +
+
+} diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.scss b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.scss new file mode 100644 index 000000000..c704e5324 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.scss @@ -0,0 +1,55 @@ +/** @format */ +@use "styles/ui-foundation"; +@use "../../projects/edit/components/project-team-step/team-member-row"; + +h2 { + margin: 0 0 8px; + font-size: 18px; + font-weight: 500; +} + +h3 { + margin: 24px 0 8px; + font-size: 14px; + font-weight: 500; +} + +p { + line-height: 1.5; +} + +.muted { + color: var(--grey-for-text); +} + +.team-header { + display: flex; + flex-wrap: wrap; + gap: 20px; + align-items: start; + justify-content: space-between; +} + +.team-header p { + margin: 0; + font-size: 12px; +} + +.members { + padding: 0; + margin: 0; + list-style: none; +} + +.empty { + padding: 20px 0; + font-size: 12px; + color: var(--grey-for-text); + border-bottom: 1px solid var(--medium-grey-for-outline); +} + +.team-header .ui-button, +section > .ui-button, +.invite-actions .ui-button { + @include ui-foundation.button; +} diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.spec.ts new file mode 100644 index 000000000..3bef4f293 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.spec.ts @@ -0,0 +1,248 @@ +/** @format */ + +import { signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { of, Subject } from "rxjs"; +import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; +import { + GetSolutionInvitationsUseCase, + LeaveSolutionUseCase, + RemoveSolutionMemberUseCase, + RevokeSolutionInvitationUseCase, + TransferSolutionCaptainUseCase, + UpdateSolutionInvitationUseCase, +} from "@api/solution/use-cases/solution-team.use-cases"; +import { + ProgramSolution, + SolutionInvitation, + SolutionInvitationCreated, + SolutionMutationReceipt, +} from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { fail, ok } from "@domain/shared/result.type"; +import { SolutionTeamComponent } from "./solution-team.component"; + +const base = solutionContractScenarios.find(s => s.name === "created_solo")! + .response as ProgramSolution; +const invited = ( + solutionContractScenarios.find(s => s.name === "create_invitation")! + .response as SolutionInvitationCreated +).invitation; +const member = { + user: { id: 9, firstName: "Анна", lastName: "Смирнова", avatarUrl: null }, + role: "Аналитик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, +}; +const receipt = { + solutionId: base.id, + solutionVersion: 2, + updatedAt: "2026-10-24T10:00:00Z", +} satisfies SolutionMutationReceipt; + +describe("Команда внутри заявки", () => { + const profile = signal({ id: base.captainId }); + const list = { execute: vi.fn() }, + remove = { execute: vi.fn() }, + leave = { execute: vi.fn() }, + transfer = { execute: vi.fn() }, + update = { execute: vi.fn() }, + revoke = { execute: vi.fn() }; + const pending: SolutionInvitation = { + ...invited, + recipient: { ...invited.recipient, id: 21 }, + permissions: { canEdit: true, canRevoke: true, canAccept: false, canDecline: false }, + }; + beforeEach(() => { + profile.set({ id: base.captainId }); + list.execute + .mockReset() + .mockReturnValue(of(ok({ count: 1, next: null, previous: null, results: [pending] }))); + for (const command of [remove, transfer, update, revoke]) + command.execute.mockReset().mockReturnValue(of(ok(receipt))); + leave.execute.mockReset().mockReturnValue(of(ok(undefined))); + vi.spyOn(window, "confirm").mockReturnValue(true); + TestBed.configureTestingModule({ + imports: [SolutionTeamComponent], + providers: [ + { provide: ProfileInfoService, useValue: { profile } }, + { provide: GetSolutionInvitationsUseCase, useValue: list }, + { provide: RemoveSolutionMemberUseCase, useValue: remove }, + { provide: LeaveSolutionUseCase, useValue: leave }, + { provide: TransferSolutionCaptainUseCase, useValue: transfer }, + { provide: UpdateSolutionInvitationUseCase, useValue: update }, + { provide: RevokeSolutionInvitationUseCase, useValue: revoke }, + ], + }); + }); + afterEach(() => vi.restoreAllMocks()); + async function setup(changes: Partial = {}) { + const fixture = TestBed.createComponent(SolutionTeamComponent); + fixture.componentRef.setInput("solution", { + ...structuredClone(base), + team: { + ...base.team, + members: [...base.team.members, member], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + }, + ...changes, + }); + fixture.detectChanges(); + await fixture.whenStable(); + return fixture; + } + + it("renders accepted and pending rows and uses the program-scoped list", async () => { + const fixture = await setup(); + expect(fixture.nativeElement.textContent).toContain("Участники команды · 2"); + expect(fixture.nativeElement.textContent).toContain("Ожидают подтверждения · 1"); + expect(fixture.nativeElement.textContent).toContain("Аналитик"); + expect(list.execute).toHaveBeenCalledWith(base.programId, base.id, { + status: "pending", + limit: 100, + offset: 0, + }); + }); + + it("remove confirms, sends exact versions, blocks double action and refreshes after receipt", async () => { + const pendingRequest = new Subject>>(); + remove.execute.mockReturnValue(pendingRequest); + const fixture = await setup(); + const component = fixture.componentInstance; + const changed = vi.fn(); + component.changed.subscribe(changed); + component.remove(member); + component.remove(member); + expect(remove.execute).toHaveBeenCalledTimes(1); + expect(remove.execute).toHaveBeenCalledWith(base.programId, base.id, 9, base.version); + expect(component.busy()).toBe(true); + pendingRequest.next(ok(receipt)); + pendingRequest.complete(); + expect(changed).toHaveBeenCalledTimes(1); + expect(component.busy()).toBe(false); + }); + + it("transfer updates permissions from the refreshed server object", async () => { + const fixture = await setup(); + const component = fixture.componentInstance; + component.transfer(member); + expect(transfer.execute).toHaveBeenCalledWith(base.programId, base.id, { + expectedSolutionVersion: base.version, + newCaptainUserId: 9, + }); + const current = component.solution(); + fixture.componentRef.setInput("solution", { + ...current, + version: 2, + captainId: 9, + permissions: { + ...current.permissions, + canManageTeam: false, + canInvite: false, + canLeave: true, + }, + team: { + ...current.team, + members: current.team.members.map(row => ({ ...row, isCaptain: row.user.id === 9 })), + }, + }); + await fixture.whenStable(); + expect(fixture.nativeElement.textContent).toContain("Выйти из команды"); + expect( + fixture.nativeElement.querySelector('[aria-label="Исключить участника: Анна Смирнова"]'), + ).toBeNull(); + expect(component.pending()).toEqual([]); + }); + + it("regular member sees no private pending identities and can leave only with server permission", async () => { + profile.set({ id: 9 }); + const fixture = await setup({ + permissions: { ...base.permissions, canManageTeam: false, canInvite: false, canLeave: true }, + }); + const component = fixture.componentInstance; + const left = vi.fn(); + component.left.subscribe(left); + expect(list.execute).not.toHaveBeenCalled(); + expect(fixture.nativeElement.textContent).toContain( + "Список отправленных приглашений доступен капитану", + ); + component.leave(); + expect(left).toHaveBeenCalledTimes(1); + expect(leave.execute).toHaveBeenCalledWith(base.programId, base.id, { + expectedSolutionVersion: base.version, + }); + }); + + it("submitted captain cannot edit accepted team or role, but can revoke pending", async () => { + list.execute.mockReturnValue( + of( + ok({ + count: 1, + next: null, + previous: null, + results: [{ ...pending, permissions: { ...pending.permissions, canEdit: false } }], + }), + ), + ); + const fixture = await setup({ + permissions: { ...base.permissions, canManageTeam: false, canInvite: false, canLeave: false }, + team: { ...base.team, members: [...base.team.members, member], isFrozen: true }, + }); + const component = fixture.componentInstance; + component.remove(member); + component.transfer(member); + component.leave(); + expect(remove.execute).not.toHaveBeenCalled(); + expect(transfer.execute).not.toHaveBeenCalled(); + expect(leave.execute).not.toHaveBeenCalled(); + component.revoke(component.pending()[0]); + expect(revoke.execute).toHaveBeenCalledWith(pending.id, pending.version, base.version); + }); + + it("list error is not an empty list and blocks inviting until retry succeeds", async () => { + list.execute.mockReturnValueOnce( + of( + fail({ + code: "network", + detail: "Не удалось загрузить приглашения.", + errors: [], + meta: {}, + }), + ), + ); + const fixture = await setup(); + expect(fixture.nativeElement.textContent).toContain("Не удалось загрузить приглашения"); + expect(fixture.nativeElement.textContent).not.toContain("Нет ожидающих приглашений"); + fixture.componentInstance.reloadInvitations(); + await fixture.whenStable(); + expect(fixture.componentInstance.pending()).toHaveLength(1); + }); + + it("changing program cancels old invitation reads and cannot render their late result", async () => { + const old = new Subject< + ReturnType< + typeof ok<{ count: number; next: null; previous: null; results: SolutionInvitation[] }> + > + >(); + list.execute + .mockReturnValueOnce(old) + .mockReturnValue(of(ok({ count: 0, next: null, previous: null, results: [] }))); + const fixture = await setup(); + fixture.componentRef.setInput("solution", { + ...fixture.componentInstance.solution(), + id: 999, + programId: 13, + }); + await fixture.whenStable(); + old.next(ok({ count: 1, next: null, previous: null, results: [pending] })); + expect(fixture.componentInstance.pending()).toEqual([]); + expect(list.execute).toHaveBeenLastCalledWith(13, 999, { + status: "pending", + limit: 100, + offset: 0, + }); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts new file mode 100644 index 000000000..ab6671dda --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts @@ -0,0 +1,275 @@ +/** @format */ + +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + effect, + inject, + input, + output, + signal, + untracked, +} from "@angular/core"; +import { DatePipe } from "@angular/common"; +import { FormControl } from "@angular/forms"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { finalize, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs"; +import { ButtonDirective, IconComponent } from "@uilib"; +import { AvatarComponent } from "@ui/primitives/avatar/avatar.component"; +import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; +import { + GetSolutionInvitationsUseCase, + RemoveSolutionMemberUseCase, + LeaveSolutionUseCase, + TransferSolutionCaptainUseCase, + UpdateSolutionInvitationUseCase, + RevokeSolutionInvitationUseCase, +} from "@api/solution/use-cases/solution-team.use-cases"; +import { + ProgramSolution, + SolutionApiError, + SolutionInvitation, + SolutionMember, +} from "@domain/solution/solution-contract.model"; +import { fail, ok, Result } from "@domain/shared/result.type"; +import { inviteRoleValidator } from "@api/invite/project-invite-form"; +import { normalizeInviteText } from "@domain/invite/project-role-suggestions"; +import { ProjectInviteDialogComponent } from "@ui/widgets/project-invite/project-invite-dialog.component"; +import { ProjectInviteRoleInputComponent } from "@ui/widgets/project-invite/project-invite-role-input.component"; +import { SolutionMemberInviteModalComponent } from "@ui/widgets/project-invite/solution-member-invite-modal.component"; + +@Component({ + selector: "app-solution-team", + imports: [ + DatePipe, + ButtonDirective, + IconComponent, + AvatarComponent, + ProjectInviteDialogComponent, + ProjectInviteRoleInputComponent, + SolutionMemberInviteModalComponent, + ], + templateUrl: "./solution-team.component.html", + styleUrl: "./solution-team.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionTeamComponent { + readonly solution = input.required(); + readonly refreshing = input(false); + readonly changed = output(); + readonly left = output(); + readonly currentUser = inject(ProfileInfoService).profile; + readonly pending = signal([]); + readonly loading = signal(false); + readonly listError = signal(""); + readonly error = signal(""); + private readonly pendingCommand = signal(false); + readonly busy = computed(() => this.pendingCommand() || this.refreshing()); + readonly inviting = signal(false); + readonly editing = signal(null); + readonly role = new FormControl("", { nonNullable: true, validators: [inviteRoleValidator] }); + trigger: HTMLElement | null = null; + private readonly list = inject(GetSolutionInvitationsUseCase); + private readonly removeUseCase = inject(RemoveSolutionMemberUseCase); + private readonly leaveUseCase = inject(LeaveSolutionUseCase); + private readonly transferUseCase = inject(TransferSolutionCaptainUseCase); + private readonly updateUseCase = inject(UpdateSolutionInvitationUseCase); + private readonly revokeUseCase = inject(RevokeSolutionInvitationUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly listRevision = signal(0); + private readonly changedContext = new Subject(); + private contextKey = ""; + + constructor() { + effect(onCleanup => { + const solution = this.solution(); + const key = solution.programId + ":" + solution.id; + this.listRevision(); + const owner = this.currentUser()?.id === solution.captainId; + if (key !== this.contextKey) { + this.contextKey = key; + this.changedContext.next(); + this.inviting.set(false); + this.editing.set(null); + this.error.set(""); + } + this.pending.set([]); + this.listError.set(""); + this.loading.set(false); + if (!owner) { + this.editing.set(null); + this.inviting.set(false); + return; + } + this.loading.set(true); + const subscription = this.allPending(solution.programId, solution.id).subscribe(result => { + this.loading.set(false); + if (result.ok) { + this.pending.set(result.value); + const editing = untracked(this.editing); + if (editing) + this.editing.set(result.value.find(invitation => invitation.id === editing.id) || null); + } else this.listError.set(result.error.detail); + }); + onCleanup(() => subscription.unsubscribe()); + }); + } + + private allPending( + programId: number, + solutionId: number, + offset = 0, + ): Observable> { + return this.list.execute(programId, solutionId, { status: "pending", limit: 100, offset }).pipe( + switchMap(result => { + if (!result.ok) return of(fail(result.error)); + const page = result.value; + if (!page.next) return of(ok(page.results)); + if (!page.results.length) + return of( + fail({ + code: "invalid_response", + detail: "Не удалось обновить приглашения. Повторите загрузку.", + errors: [], + meta: {}, + }), + ); + return this.allPending(programId, solutionId, offset + page.results.length).pipe( + map(next => (next.ok ? ok([...page.results, ...next.value]) : fail(next.error))), + ); + }), + ); + } + + reloadInvitations(): void { + this.listRevision.update(value => value + 1); + } + + openInvite(event: Event): void { + if (!this.solution().permissions.canInvite || this.busy() || this.loading() || this.listError()) + return; + this.trigger = event.currentTarget as HTMLElement; + this.inviting.set(true); + } + + remove(member: SolutionMember): void { + const solution = this.solution(); + if (this.busy() || !solution.permissions.canManageTeam || member.isCaptain) return; + if ( + !confirm( + `Вы точно хотите исключить участника ${member.user.firstName} ${member.user.lastName}?`, + ) + ) + return; + this.command( + this.removeUseCase.execute(solution.programId, solution.id, member.user.id, solution.version), + ); + } + + transfer(member: SolutionMember): void { + const solution = this.solution(); + if (this.busy() || !solution.permissions.canManageTeam || member.isCaptain) return; + if ( + !confirm( + `Передать капитанство участнику ${member.user.firstName} ${member.user.lastName}? Вы останетесь в команде.`, + ) + ) + return; + this.command( + this.transferUseCase.execute(solution.programId, solution.id, { + expectedSolutionVersion: solution.version, + newCaptainUserId: member.user.id, + }), + ); + } + + leave(): void { + const solution = this.solution(); + if (this.busy() || !solution.permissions.canLeave || !confirm("Выйти из команды заявки?")) + return; + this.command( + this.leaveUseCase.execute(solution.programId, solution.id, { + expectedSolutionVersion: solution.version, + }), + () => this.left.emit(), + true, + ); + } + + edit(invitation: SolutionInvitation, event: Event): void { + if (this.busy() || !invitation.permissions.canEdit) return; + this.trigger = event.currentTarget as HTMLElement; + this.role.reset(invitation.role || ""); + this.error.set(""); + this.editing.set(invitation); + } + + saveRole(): void { + const invitation = this.editing(); + this.role.markAsTouched(); + if ( + this.busy() || + this.role.invalid || + !invitation?.permissions.canEdit || + !this.solution().permissions.canManageTeam + ) + return; + this.command( + this.updateUseCase.execute(invitation.id, { + expectedVersion: invitation.version, + expectedSolutionVersion: this.solution().version, + role: normalizeInviteText(this.role.value), + }), + () => this.editing.set(null), + ); + } + + revoke(invitation: SolutionInvitation): void { + if ( + this.busy() || + !invitation.permissions.canRevoke || + !confirm( + `Отозвать приглашение участнику ${invitation.recipient.firstName} ${invitation.recipient.lastName}?`, + ) + ) + return; + this.command( + this.revokeUseCase.execute(invitation.id, invitation.version, this.solution().version), + ); + } + + private command( + request: Observable>, + after?: () => void, + leaving = false, + ): void { + this.pendingCommand.set(true); + this.error.set(""); + request + .pipe( + takeUntil(this.changedContext), + takeUntilDestroyed(this.destroyRef), + finalize(() => this.pendingCommand.set(false)), + ) + .subscribe(result => { + if (!result.ok) { + this.error.set(result.error.detail); + if ( + [ + "version_conflict", + "not_solution_captain", + "team_frozen", + "solution_mode_unavailable", + "invite_already_processed", + ].includes(result.error.code) + ) + this.changed.emit(); + return; + } + after?.(); + if (!leaving) this.changed.emit(); + }); + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html index 99c036ace..3d019e9a4 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html @@ -35,26 +35,16 @@

{{ item.name }}

> @if (teamTab()) { -
-

Команда заявки

-

Участники команды · {{ item.team.acceptedCount }}

-
    - @for (member of item.team.members; track member.user.id) { -
  • - {{ member.user.firstName }} {{ member.user.lastName - }}{{ - member.isCaptain ? "Капитан" : "Участник команды" - }} - {{ member.role || "Роль не указана" }} - В команде -
  • - } @empty { -
  • В команде пока нет участников.
  • - } -
-
+ @if (teamRefreshError()) { +

{{ teamRefreshError() }}

+ + } + } @else {

Данные решения

diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss index 9ea57f006..185baf05a 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss @@ -51,20 +51,34 @@ p { nav { display: flex; - gap: 32px; + gap: 4px; + width: min(100%, 432px); + padding: 4px; margin-bottom: 28px; - border-bottom: 1px solid var(--medium-grey-for-outline); + background: var(--medium-grey-for-outline); + border-radius: 24px; } nav a { - padding: 12px 4px; + display: flex; + flex: 1; + gap: 8px; + align-items: center; + justify-content: center; + min-height: 40px; + padding: 8px 12px; color: var(--black); - border-bottom: 2px solid transparent; + border-radius: 20px; } nav a[aria-current] { color: var(--accent); - border-bottom-color: var(--accent); + background: var(--white, #fff); +} + +nav a:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; } dl { diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts index 76cf0e167..72fbccf34 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts @@ -1,6 +1,9 @@ /** @format */ import { TestBed } from "@angular/core/testing"; +import { signal } from "@angular/core"; +import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; import { provideRouter, Router } from "@angular/router"; import { RouterTestingHarness } from "@angular/router/testing"; import { Observable, of, Subject } from "rxjs"; @@ -51,6 +54,15 @@ describe("Solution route foundation", () => { { provide: GetSolutionUseCase, useValue: get }, { provide: CreateSolutionUseCase, useValue: create }, { provide: GetSolutionContextUseCase, useValue: getContext }, + { provide: ProfileInfoService, useValue: { profile: signal({ id: 7 }) } }, + { + provide: SolutionRepositoryPort, + useValue: { + invitations: vi + .fn() + .mockReturnValue(of({ count: 0, next: null, previous: null, results: [] })), + }, + }, ], }); }); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts index 661989be0..73e9de1ec 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts @@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, Component, inject, signal } from "@angular/core"; import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { combineLatest, EMPTY, map, startWith, Subject, switchMap } from "rxjs"; +import { combineLatest, EMPTY, finalize, map, startWith, Subject, switchMap } from "rxjs"; import { GetSolutionContextUseCase, CreateSolutionUseCase, @@ -15,12 +15,13 @@ import { ProgramSolution, SolutionApiError } from "@domain/solution/solution-con import { validSolutionId } from "@domain/solution/ports/solution.repository.port"; import { ButtonDirective } from "@uilib"; import { DatePipe } from "@angular/common"; +import { SolutionTeamComponent } from "./solution-team.component"; /** FE01: read surface and route foundation. Editing/team commands follow in FE03–FE06. */ @Component({ selector: "app-program-solution", standalone: true, - imports: [RouterLink, ButtonDirective, DatePipe], + imports: [RouterLink, ButtonDirective, DatePipe, SolutionTeamComponent], templateUrl: "./solution.component.html", styleUrl: "./solution.component.scss", changeDetection: ChangeDetectionStrategy.OnPush, @@ -36,14 +37,51 @@ export class ProgramSolutionComponent { readonly error = signal(null); readonly teamTab = signal(false); readonly programId = signal(0); + readonly refreshingTeam = signal(false); + readonly teamRefreshError = signal(""); readonly routes = AppRoutes; private readonly retry = new Subject(); + private readonly teamRefresh = new Subject<{ programId: number; solutionId: number } | null>(); reload(): void { this.retry.next(); } + refreshTeam(): void { + const solution = this.solution(); + if (solution) this.teamRefresh.next({ programId: solution.programId, solutionId: solution.id }); + } + + teamLeft(): void { + void this.router.navigate([AppRoutes.program.detail(this.programId())]); + } + constructor() { + this.teamRefresh + .pipe( + switchMap(context => { + if (!context) return EMPTY; + this.refreshingTeam.set(true); + this.teamRefreshError.set(""); + return this.get.execute(context.programId, context.solutionId).pipe( + map(result => ({ context, result })), + finalize(() => this.refreshingTeam.set(false)), + ); + }), + takeUntilDestroyed(), + ) + .subscribe(({ context, result }) => { + if (this.programId() !== context.programId || this.solution()?.id !== context.solutionId) + return; + if (result.ok) { + this.solution.set(result.value); + return; + } + if (["not_found", "forbidden", "authentication_required"].includes(result.error.code)) { + this.solution.set(null); + this.showError(result.error); + } else this.teamRefreshError.set(result.error.detail); + }); this.route.queryParamMap .pipe(takeUntilDestroyed()) .subscribe(params => this.teamTab.set(params.get("tab") === "team")); @@ -57,6 +95,9 @@ export class ProgramSolutionComponent { solutionId: params.find(p => p.has("solutionId"))?.get("solutionId"), })), switchMap(({ programId, solutionId }) => { + this.teamRefresh.next(null); + this.refreshingTeam.set(false); + this.teamRefreshError.set(""); this.solution.set(null); this.error.set(null); this.loading.set(true); diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.html b/projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.html new file mode 100644 index 000000000..93a2a28ad --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.html @@ -0,0 +1,71 @@ + + +
+ + +
+ + + @if (error()) { + + } +
+ + +
+
diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.ts b/projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.ts new file mode 100644 index 000000000..8fb81a839 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.ts @@ -0,0 +1,58 @@ +/** @format */ + +import { + ChangeDetectionStrategy, + Component, + ElementRef, + input, + output, + ViewChild, +} from "@angular/core"; +import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { ButtonDirective, StateComponent } from "@uilib"; +import { User } from "@domain/auth/user.model"; +import { InviteSearchState } from "@api/invite/facades/invite-participant-search.facade"; +import { ParticipantPickerComponent } from "./participant-picker.component"; +import { ProjectInviteDialogComponent } from "./project-invite-dialog.component"; +import { ProjectInviteRoleInputComponent } from "./project-invite-role-input.component"; + +let nextSearchId = 0; + +/** Представление существующего окна: владелец и API остаются во внешнем адаптере. */ +@Component({ + selector: "app-member-invite-form", + imports: [ + ReactiveFormsModule, + ButtonDirective, + StateComponent, + ParticipantPickerComponent, + ProjectInviteDialogComponent, + ProjectInviteRoleInputComponent, + ], + templateUrl: "./member-invite-form.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MemberInviteFormComponent { + readonly query = input.required>(); + readonly role = input.required>(); + readonly state = input.required(); + readonly selected = input(null); + readonly unavailableReason = input.required<(id: number) => string | null>(); + readonly busy = input(false); + readonly valid = input(false); + readonly error = input(""); + readonly trigger = input(null); + readonly description = input("Найдите участника и укажите его роль в проекте"); + readonly roleLabel = input("Роль в проекте"); + readonly closed = output(); + readonly chosen = output(); + readonly cleared = output(); + readonly submitted = output(); + readonly searchId = "team-invite-search-" + ++nextSearchId; + @ViewChild("searchInput") private searchInput!: ElementRef; + + clear(): void { + this.cleared.emit(); + this.searchInput.nativeElement.focus(); + } +} diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/participant-picker.component.ts b/projects/social_platform/src/app/ui/widgets/project-invite/participant-picker.component.ts index 2c79405c6..e03dbf89d 100644 --- a/projects/social_platform/src/app/ui/widgets/project-invite/participant-picker.component.ts +++ b/projects/social_platform/src/app/ui/widgets/project-invite/participant-picker.component.ts @@ -101,10 +101,13 @@ export class ParticipantPickerComponent { readonly collaborators = input([]); readonly invites = input([]); readonly busy = input(false); + readonly unavailableReason = input<((id: number) => string | null) | null>(null); readonly chosen = output(); readonly cleared = output(); reason(id: number): string | null { + const reason = this.unavailableReason(); + if (reason) return reason(id); return inviteCandidateReason(id, this.leaderId(), this.collaborators(), this.invites()); } } diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-role-input.component.html b/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-role-input.component.html index f9b5bb03d..05def59e6 100644 --- a/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-role-input.component.html +++ b/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-role-input.component.html @@ -1,6 +1,6 @@
- + >(); readonly readonly = input(false); + readonly label = input("Роль в проекте"); readonly id = "invite-role-" + ++nextRoleId; readonly opened = signal(false); readonly query = signal(""); diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.html b/projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.html index 3e92674cc..71db30e82 100644 --- a/projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.html +++ b/projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.html @@ -1,79 +1,16 @@ - -
- - -
- - - @if (ui.inviteSubmitError(); as error) { - - } -
- - -
-
+ (submitted)="team.submitInvite(projectId(), project(), programLinkId())" +/> diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.ts b/projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.ts index b1ca2b7d5..ae8aabef7 100644 --- a/projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.ts +++ b/projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.ts @@ -1,19 +1,16 @@ /** @format */ -import { ButtonDirective, StateComponent } from "@uilib"; /** @format */ import { ChangeDetectionStrategy, Component, DestroyRef, effect, - ElementRef, inject, input, output, - ViewChild, } from "@angular/core"; -import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { FormControl } from "@angular/forms"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Project } from "@domain/project/project.model"; import { User } from "@domain/auth/user.model"; @@ -22,20 +19,11 @@ import { isLoading } from "@domain/shared/async-state"; import { InviteParticipantSearchFacade } from "@api/invite/facades/invite-participant-search.facade"; import { ProjectTeamUIService } from "@api/project/facades/edit/ui/project-team-ui.service"; import { ProjectTeamService } from "@api/project/facades/edit/project-team.service"; -import { ProjectInviteDialogComponent } from "./project-invite-dialog.component"; -import { ProjectInviteRoleInputComponent } from "./project-invite-role-input.component"; -import { ParticipantPickerComponent } from "./participant-picker.component"; +import { MemberInviteFormComponent } from "./member-invite-form.component"; @Component({ selector: "app-project-member-invite-modal", - imports: [ - StateComponent, - ButtonDirective, - ReactiveFormsModule, - ProjectInviteDialogComponent, - ProjectInviteRoleInputComponent, - ParticipantPickerComponent, - ], + imports: [MemberInviteFormComponent], providers: [InviteParticipantSearchFacade], templateUrl: "./project-member-invite-modal.component.html", changeDetection: ChangeDetectionStrategy.OnPush, @@ -51,7 +39,8 @@ export class ProjectMemberInviteModalComponent { readonly search = inject(InviteParticipantSearchFacade); readonly query = new FormControl("", { nonNullable: true }); readonly isLoading = isLoading; - @ViewChild("searchInput") private searchInput!: ElementRef; + readonly candidateReason = (id: number) => + inviteCandidateReason(id, this.project()?.leader, this.ui.collaborators(), this.ui.invites()); constructor() { const destroyRef = inject(DestroyRef); @@ -76,6 +65,5 @@ export class ProjectMemberInviteModalComponent { clear(): void { this.ui.selectRecipient(null); - this.searchInput.nativeElement.focus(); } } diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.spec.ts b/projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.spec.ts new file mode 100644 index 000000000..2c06fe197 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.spec.ts @@ -0,0 +1,183 @@ +/** @format */ + +import { provideZonelessChangeDetection, signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { of, Subject } from "rxjs"; +import { InviteParticipantSearchFacade } from "@api/invite/facades/invite-participant-search.facade"; +import { CreateSolutionInvitationUseCase } from "@api/solution/use-cases/solution-team.use-cases"; +import { User } from "@domain/auth/user.model"; +import { + ProgramSolution, + SolutionApiError, + SolutionInvitation, + SolutionInvitationCreated, +} from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { fail, ok, Result } from "@domain/shared/result.type"; +import { SolutionMemberInviteModalComponent } from "./solution-member-invite-modal.component"; + +const candidate = { + id: 42, + firstName: "Иван", + lastName: "Тест", + personal: { avatar: "", birthday: "2000-01-01", speciality: "Разработка" }, + relations: { skills: [] }, +} as unknown as User; +const base = solutionContractScenarios.find(s => s.name === "created_solo")! + .response as ProgramSolution; +const response = solutionContractScenarios.find(s => s.name === "create_invitation")! + .response as SolutionInvitationCreated; + +describe("Приглашение в решение через существующее окно", () => { + const create = { execute: vi.fn() }; + const search = { + state: signal({ status: "initial", users: [] } as { + status: "initial" | "success"; + users: User[]; + }), + search: vi.fn(), + }; + beforeEach(() => { + create.execute.mockReset().mockReturnValue(of(ok(response))); + search.search.mockReset(); + search.state.set({ status: "initial", users: [] }); + TestBed.configureTestingModule({ + imports: [SolutionMemberInviteModalComponent], + providers: [ + provideZonelessChangeDetection(), + { provide: CreateSolutionInvitationUseCase, useValue: create }, + ], + }).overrideComponent(SolutionMemberInviteModalComponent, { + remove: { providers: [InviteParticipantSearchFacade] }, + add: { providers: [{ provide: InviteParticipantSearchFacade, useValue: search }] }, + }); + }); + + async function setup(invitations: SolutionInvitation[] = []) { + const fixture = TestBed.createComponent(SolutionMemberInviteModalComponent); + fixture.componentRef.setInput("solution", structuredClone(base)); + fixture.componentRef.setInput("invitations", invitations); + await fixture.whenStable(); + await new Promise(resolve => setTimeout(resolve, 20)); + await fixture.whenStable(); + return fixture; + } + + it("uses program ID for name search and a selected user ID for sending", async () => { + const fixture = await setup(); + const component = fixture.componentInstance; + component.query.setValue("Ива"); + expect(search.search).toHaveBeenLastCalledWith("Ива", base.programId); + component.select(candidate); + component.form.controls.role.setValue(" Backend разработчик "); + component.submit(); + expect(create.execute).toHaveBeenCalledWith(base.programId, base.id, { + expectedSolutionVersion: base.version, + recipientUserId: 42, + role: "Backend разработчик", + }); + }); + + it("blocks empty recipient and whitespace role, including manual submit", async () => { + const fixture = await setup(); + const component = fixture.componentInstance; + component.form.controls.role.setValue("Аналитик"); + component.submit(); + component.select(candidate); + component.form.controls.role.setValue(" "); + component.submit(); + expect(create.execute).not.toHaveBeenCalled(); + expect(component.form.controls.role.hasError("required")).toBe(true); + }); + + it("does not select captain, accepted member or pending recipient", async () => { + const invitation = { + ...response.invitation, + recipient: { ...response.invitation.recipient, id: 42 }, + }; + const fixture = await setup([invitation]); + const component = fixture.componentInstance; + component.select({ ...candidate, id: base.captainId }); + expect(component.selected()).toBeNull(); + expect(component.reason(base.captainId)).toBe("Капитан команды"); + component.select(candidate); + expect(component.selected()).toBeNull(); + expect(component.reason(42)).toBe("Приглашение уже отправлено"); + }); + + it("pending blocks duplicate, error preserves selected ID/query/role and allows deliberate retry", async () => { + const pending = new Subject>(); + create.execute.mockReturnValue(pending); + const fixture = await setup(); + const component = fixture.componentInstance; + component.query.setValue("Ива"); + component.select(candidate); + component.form.controls.role.setValue("Аналитик"); + component.submit(); + component.submit(); + expect(create.execute).toHaveBeenCalledTimes(1); + expect(component.busy()).toBe(true); + pending.next( + fail({ + code: "version_conflict", + detail: "Заявка изменилась.", + errors: [], + meta: { currentVersion: 2 }, + }), + ); + pending.complete(); + await fixture.whenStable(); + expect(component.error()).toBe("Заявка изменилась."); + expect(component.selected()?.id).toBe(42); + expect(component.query.value).toBe("Ива"); + expect(component.form.controls.role.value).toBe("Аналитик"); + expect(component.busy()).toBe(false); + expect(document.querySelector(".project-invite")?.textContent).toContain("Заявка изменилась."); + }); + + it("success emits only pending invitation and close; does not fabricate team membership", async () => { + const fixture = await setup(); + const component = fixture.componentInstance; + const created = vi.fn(), + closed = vi.fn(); + component.created.subscribe(created); + component.closed.subscribe(closed); + component.select(candidate); + component.form.controls.role.setValue("Аналитик"); + component.submit(); + expect(created).toHaveBeenCalledWith(response); + expect(closed).toHaveBeenCalledTimes(1); + expect(component.solution().team.members).toEqual(base.team.members); + expect(response.invitation.status).toBe("pending"); + }); + + it("closing destroys subscriptions and a late response cannot update a newly opened dialog", async () => { + const pending = new Subject>(); + create.execute.mockReturnValue(pending); + const fixture = await setup(); + const component = fixture.componentInstance; + const created = vi.fn(); + component.created.subscribe(created); + component.select(candidate); + component.form.controls.role.setValue("Аналитик"); + component.submit(); + fixture.destroy(); + pending.next(ok(response)); + pending.complete(); + expect(created).not.toHaveBeenCalled(); + const reopened = await setup(); + expect(reopened.componentInstance.selected()).toBeNull(); + expect(reopened.componentInstance.form.controls.role.value).toBe(""); + }); + + it("clearing selected participant returns focus to name search", async () => { + const fixture = await setup(); + fixture.componentInstance.select(candidate); + await fixture.whenStable(); + const clear = document.querySelector(".clear-recipient")!; + clear.click(); + await fixture.whenStable(); + expect(fixture.componentInstance.selected()).toBeNull(); + expect(document.activeElement?.getAttribute("type")).toBe("search"); + }); +}); diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.ts b/projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.ts new file mode 100644 index 000000000..bb691f169 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.ts @@ -0,0 +1,152 @@ +/** @format */ + +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + effect, + inject, + input, + output, + signal, +} from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { finalize, Subject, takeUntil } from "rxjs"; +import { User } from "@domain/auth/user.model"; +import { + ProgramSolution, + SolutionInvitation, + SolutionInvitationCreated, +} from "@domain/solution/solution-contract.model"; +import { normalizeInviteText } from "@domain/invite/project-role-suggestions"; +import { createProjectInviteForm } from "@api/invite/project-invite-form"; +import { InviteParticipantSearchFacade } from "@api/invite/facades/invite-participant-search.facade"; +import { CreateSolutionInvitationUseCase } from "@api/solution/use-cases/solution-team.use-cases"; +import { MemberInviteFormComponent } from "./member-invite-form.component"; + +@Component({ + selector: "app-solution-member-invite-modal", + imports: [MemberInviteFormComponent], + providers: [InviteParticipantSearchFacade], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionMemberInviteModalComponent { + readonly solution = input.required(); + readonly invitations = input([]); + readonly trigger = input(null); + readonly closed = output(); + readonly created = output(); + readonly refreshRequired = output(); + readonly search = inject(InviteParticipantSearchFacade); + readonly form = createProjectInviteForm(); + readonly query = new FormControl("", { nonNullable: true }); + readonly selected = signal(null); + readonly busy = signal(false); + readonly error = signal(""); + private readonly create = inject(CreateSolutionInvitationUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly changedContext = new Subject(); + private contextKey = ""; + + readonly reason = (id: number): string | null => { + if (id === this.solution().captainId) return "Капитан команды"; + if (this.solution().team.members.some(member => member.user.id === id)) return "Уже в команде"; + if ( + this.invitations().some(invite => invite.recipient.id === id && invite.status === "pending") + ) + return "Приглашение уже отправлено"; + return null; + }; + + constructor() { + effect(() => { + const selected = this.selected(); + this.invitations(); + const reason = selected ? this.reason(selected.id) : null; + if (reason) this.error.set(reason); + }); + this.query.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.search.search(value, this.solution().programId)); + effect(() => { + const solution = this.solution(); + const key = solution.programId + ":" + solution.id; + if (key !== this.contextKey) { + this.contextKey = key; + this.changedContext.next(); + this.form.reset({ recipientId: null, role: "" }); + this.query.reset("", { emitEvent: false }); + this.selected.set(null); + this.error.set(""); + } + this.search.search(this.query.value, solution.programId); + }); + } + + select(user: User): void { + if (this.busy() || this.reason(user.id)) return; + this.selected.set(user); + this.form.controls.recipientId.setValue(user.id); + } + clear(): void { + this.selected.set(null); + this.form.controls.recipientId.setValue(null); + } + submit(): void { + const solution = this.solution(); + const id = this.selected()?.id; + if (this.busy() || !solution.permissions.canInvite) return; + this.form.markAllAsTouched(); + if (this.form.invalid || !id || this.form.controls.recipientId.value !== id || this.reason(id)) + return; + this.busy.set(true); + this.error.set(""); + this.create + .execute(solution.programId, solution.id, { + expectedSolutionVersion: solution.version, + recipientUserId: id, + role: normalizeInviteText(this.form.controls.role.value), + }) + .pipe( + takeUntil(this.changedContext), + takeUntilDestroyed(this.destroyRef), + finalize(() => this.busy.set(false)), + ) + .subscribe(result => { + if (!result.ok) { + this.error.set(result.error.detail); + if ( + [ + "version_conflict", + "not_solution_captain", + "team_frozen", + "solution_mode_unavailable", + "duplicate_pending_invite", + ].includes(result.error.code) + ) + this.refreshRequired.emit(); + return; + } + this.created.emit(result.value); + this.closed.emit(); + }); + } +} diff --git a/projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.spec.ts b/projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.spec.ts new file mode 100644 index 000000000..961f0cf94 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.spec.ts @@ -0,0 +1,97 @@ +/** @format */ + +import { signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { provideRouter, Router } from "@angular/router"; +import { of } from "rxjs"; +import { SolutionInvitationInboxService } from "@api/solution/facades/solution-invitation-inbox.service"; +import { SolutionInvitationCreated } from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { fail, ok } from "@domain/shared/result.type"; +import { SolutionInvitationInboxComponent } from "./solution-invitation-inbox.component"; + +const created = ( + solutionContractScenarios.find(s => s.name === "create_invitation")! + .response as SolutionInvitationCreated +).invitation; +const invitation = { + ...created, + permissions: { canAccept: true, canDecline: true, canEdit: false, canRevoke: false }, +}; + +describe("Получатель приглашения в решение", () => { + const inbox = { + items: signal([invitation]), + loading: signal(false), + error: signal(""), + pendingId: signal(null), + action: vi.fn(), + refresh: vi.fn(), + }; + beforeEach(() => { + inbox.action.mockReset(); + inbox.items.set([invitation]); + inbox.pendingId.set(null); + TestBed.configureTestingModule({ + imports: [SolutionInvitationInboxComponent], + providers: [provideRouter([]), { provide: SolutionInvitationInboxService, useValue: inbox }], + }); + }); + + it("shows program/role without a Project link and navigates only after accepted application", () => { + const fixture = TestBed.createComponent(SolutionInvitationInboxComponent); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toContain(invitation.program.name); + expect(fixture.nativeElement.querySelector('a[href*="/office/projects/"]')).toBeNull(); + const navigate = vi.spyOn(TestBed.inject(Router), "navigate").mockResolvedValue(true); + inbox.action.mockReturnValue( + of( + ok({ + invitation: { ...invitation, status: "accepted" }, + application: invitation.solution, + solutionVersion: 3, + }), + ), + ); + (Array.from(fixture.nativeElement.querySelectorAll("button")) as HTMLButtonElement[]) + .find(button => button.textContent?.trim() === "принять")! + .click(); + expect(navigate).toHaveBeenCalledWith( + [ + `/office/program/${invitation.solution.programId}/solutions/${invitation.solution.solutionId}`, + ], + { queryParams: { tab: "team" } }, + ); + }); + + it("does not report success or navigate on rejected acceptance", () => { + const fixture = TestBed.createComponent(SolutionInvitationInboxComponent); + fixture.detectChanges(); + const navigate = vi.spyOn(TestBed.inject(Router), "navigate").mockResolvedValue(true), + handled = vi.fn(); + fixture.componentInstance.handled.subscribe(handled); + inbox.action.mockReturnValue( + of(fail({ code: "team_frozen", detail: "Состав зафиксирован.", errors: [], meta: {} })), + ); + (Array.from(fixture.nativeElement.querySelectorAll("button")) as HTMLButtonElement[]) + .find(button => button.textContent?.trim() === "принять")! + .click(); + expect(navigate).not.toHaveBeenCalled(); + expect(handled).not.toHaveBeenCalled(); + }); + + it("keeps acceptance disabled for a frozen solution while allowing decline", () => { + inbox.items.set([ + { ...invitation, permissions: { ...invitation.permissions, canAccept: false } }, + ]); + const fixture = TestBed.createComponent(SolutionInvitationInboxComponent); + fixture.detectChanges(); + const buttons = Array.from( + fixture.nativeElement.querySelectorAll("button"), + ) as HTMLButtonElement[]; + expect(buttons.find(button => button.textContent?.trim() === "принять")!.disabled).toBe(true); + expect(buttons.find(button => button.textContent?.trim() === "отклонить")!.disabled).toBe( + false, + ); + }); +}); diff --git a/projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.ts b/projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.ts new file mode 100644 index 000000000..2e19e0025 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.ts @@ -0,0 +1,98 @@ +/** @format */ + +import { ChangeDetectionStrategy, Component, DestroyRef, inject, output } from "@angular/core"; +import { Router } from "@angular/router"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ButtonDirective, InviteManageCardComponent } from "@uilib"; +import { SolutionInvitationInboxService } from "@api/solution/facades/solution-invitation-inbox.service"; +import { SolutionInvitation } from "@domain/solution/solution-contract.model"; +import { solutionApplicationRoute } from "@api/solution/solution-navigation"; + +@Component({ + selector: "app-solution-invitation-inbox", + imports: [InviteManageCardComponent, ButtonDirective], + template: ` + @if (inbox.loading() && !inbox.items().length) { +

Загружаем приглашения в команды…

+ } + @if (inbox.error()) { +

{{ inbox.error() }}

+ + } +
    + @for (invitation of inbox.items(); track invitation.id) { +
  • + +
  • + } +
+ `, + styles: ` + :host { + display: block; + min-width: 0; + } + ul { + padding: 0; + margin: 0; + list-style: none; + } + li { + margin-bottom: 12px; + } + p { + font-size: 12px; + line-height: 1.5; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionInvitationInboxComponent { + readonly inbox = inject(SolutionInvitationInboxService); + readonly handled = output(); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + + notice(invitation: SolutionInvitation) { + return { + id: invitation.id, + senderId: invitation.captain.id, + senderName: invitation.captain.firstName + " " + invitation.captain.lastName, + avatarUrl: invitation.captain.avatarUrl, + resourceName: invitation.solution.name, + resourceLabel: "команду заявки", + context: invitation.program.name, + role: invitation.role, + canAccept: invitation.permissions.canAccept && this.inbox.pendingId() === null, + canDecline: invitation.permissions.canDecline && this.inbox.pendingId() === null, + pending: this.inbox.pendingId() === invitation.id, + }; + } + + answer(invitation: SolutionInvitation, decision: "accept" | "decline"): void { + this.inbox + .action(invitation, decision) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + if (!result.ok) return; + this.handled.emit(); + if (result.value.application) { + const route = solutionApplicationRoute(result.value.application); + void this.router.navigate([route.path], { + queryParams: { ...route.queryParams, tab: "team" }, + }); + } + }); + } +} diff --git a/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.html b/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.html index b59c32131..6cfd54e34 100644 --- a/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.html +++ b/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.html @@ -1,64 +1,44 @@ - -@if (invite()) { -
+@if (view(); as item) { +
- +
- - {{ invite().sender.firstName }} {{ invite().sender.lastName }}
приглашает вас в проект
- "{{ invite().project.name | truncate: 20 }}" - - - - - - - - - + {{ item.senderName }}
приглашает вас в {{ item.resourceLabel }}
+ @if (item.resourceRoute) { + "{{ item.resourceName | truncate: 20 }}" + } @else { + "{{ item.resourceName }}" + } + @if (item.context) { +

В программе «{{ item.context }}»

+ } + @if (item.role) { +

Роль: {{ item.role }}

+ }
- -
- отклонить принять{{ item.pending ? "Обрабатываем…" : "принять" }} - - -
} diff --git a/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.ts b/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.ts index 8c4fad88e..65f853ca2 100644 --- a/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.ts +++ b/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, + computed, EventEmitter, input, Input, @@ -16,6 +17,21 @@ import { RouterLink } from "@angular/router"; import { AvatarComponent } from "../../primitives/avatar/avatar.component"; import { TruncatePipe } from "@corelib"; +export interface InvitationNotice { + id: number; + senderId: number; + senderName: string; + avatarUrl?: string | null; + resourceName: string; + resourceLabel: string; + resourceRoute?: (string | number)[]; + context?: string; + role?: string | null; + canAccept: boolean; + canDecline: boolean; + pending?: boolean; +} + /** * Компонент карточки управления приглашением * @@ -43,7 +59,25 @@ import { TruncatePipe } from "@corelib"; }) export class InviteManageCardComponent implements OnInit { /** Данные приглашения для отображения */ - readonly invite = input.required(); + readonly invite = input(null); + readonly notice = input(null); + readonly view = computed(() => { + const notice = this.notice(); + if (notice) return notice; + const invite = this.invite(); + if (!invite) return null; + return { + id: invite.id, + senderId: invite.sender.id, + senderName: invite.sender.firstName + " " + invite.sender.lastName, + avatarUrl: invite.sender.personal.avatar, + resourceName: invite.project.name, + resourceLabel: "проект", + resourceRoute: ["/office/projects", invite.project.id], + canAccept: true, + canDecline: true, + }; + }); /** Событие принятия приглашения (передает ID приглашения) */ readonly accept = output(); diff --git a/tools/generate-solution-contract.py b/tools/generate-solution-contract.py index fdb7a7f6e..87ab73f80 100644 --- a/tools/generate-solution-contract.py +++ b/tools/generate-solution-contract.py @@ -30,6 +30,21 @@ def ts(schema): return " | ".join(json.dumps(v, ensure_ascii=False) for v in schema["enum"]) for key, separator in [("anyOf", " | "), ("oneOf", " | "), ("allOf", " & ")]: if key in schema: + if schema.get("properties"): + base = {k: v for k, v in schema.items() if k != key} + if key in ("anyOf", "oneOf"): + # required-only ветви уточняют общий объект, а не заменяют его unknown. + variants = [] + for branch in schema[key]: + if set(branch) <= {"required", "properties", "type"}: + combined = {**base, **branch, + "properties": {**base["properties"], **branch.get("properties", {})}, + "required": sorted(set(base.get("required", [])) | set(branch.get("required", [])))} + variants.append(ts(combined)) + else: + variants.append("(" + ts(base) + ") & (" + ts(branch) + ")") + return separator.join("(" + variant + ")" for variant in variants) + return "(" + ts(base) + ") & " + separator.join("(" + ts(v) + ")" for v in schema[key]) return separator.join("(" + ts(v) + ")" for v in schema[key]) kind = schema.get("type") if isinstance(kind, list): @@ -45,7 +60,7 @@ def ts(schema): return {"string": "string", "integer": "number", "number": "number", "boolean": "boolean", "null": "null"}.get(kind, "unknown") -types = "// Сгенерировано tools/generate-solution-contract.py из C00 1.0.0.\n// camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются.\n\n" +types = f"// Сгенерировано tools/generate-solution-contract.py из C00 {spec['info']['version']}.\n// camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются.\n\n" types += "\n\n".join("export type " + name(k) + " = " + ts(v) + ";" for k, v in spec["components"]["schemas"].items()) + "\n" model = APP / "domain/solution/solution-contract.model.ts" model.parent.mkdir(parents=True, exist_ok=True) diff --git a/tools/verify-solution-contract-types.ts b/tools/verify-solution-contract-types.ts new file mode 100644 index 000000000..0726d13fe --- /dev/null +++ b/tools/verify-solution-contract-types.ts @@ -0,0 +1,64 @@ +/** @format */ + +import type { + SolutionUpdateInvitation, + UpdateSolutionCommand, +} from "../projects/social_platform/src/app/domain/solution/solution-contract.model"; + +// Независимые примеры C00: у обеих команд обязательны ревизии и хотя бы одно изменение. +const role = { + expectedVersion: 1, + expectedSolutionVersion: 2, + role: "Аналитик", +} satisfies SolutionUpdateInvitation; +const cleared = { + expectedVersion: 1, + expectedSolutionVersion: 2, + role: null, +} satisfies SolutionUpdateInvitation; +const text = { + expectedVersion: 1, + expectedSolutionVersion: 2, + motivationalLetter: "Текст", +} satisfies SolutionUpdateInvitation; +const name = { + expectedVersion: 1, + expectedFormRevision: "revision", + name: "Решение", +} satisfies UpdateSolutionCommand; +const answers = { + expectedVersion: 1, + expectedFormRevision: "revision", + answers: [{ fieldId: 1, value: false }], +} satisfies UpdateSolutionCommand; + +// @ts-expect-error Требуется хотя бы одно изменяемое поле приглашения. +const emptyInvite: SolutionUpdateInvitation = { expectedVersion: 1, expectedSolutionVersion: 2 }; +// @ts-expect-error Ревизия решения обязательна. +const missingVersion: SolutionUpdateInvitation = { expectedVersion: 1, role: "Аналитик" }; +const invalidRole: SolutionUpdateInvitation = { + expectedVersion: 1, + expectedSolutionVersion: 2, + // @ts-expect-error Роль не является числом. + role: 5, +}; +// @ts-expect-error Требуется имя или список изменяемых ответов. +const emptySolution: UpdateSolutionCommand = { + expectedVersion: 1, + expectedFormRevision: "revision", +}; +// @ts-expect-error Отсутствует обязательная ревизия формы. +const missingRevision: UpdateSolutionCommand = { expectedVersion: 1, name: "Решение" }; + +void [ + role, + cleared, + text, + name, + answers, + emptyInvite, + missingVersion, + invalidRole, + emptySolution, + missingRevision, +]; From 4305126d872358e25fd81da06f304121a7005e43 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 21:09:10 +0300 Subject: [PATCH 02/47] feat(programs): solution form builder and preview --- docs/contracts/program-solutions-v1/README.md | 4 +- .../program-solutions-v1/openapi.json | 2 +- docs/product/program-solutions-fe02.md | 15 + .../solution-invitation-inbox.service.spec.ts | 22 +- .../solution/solution-form-contract.spec.ts | 94 ++++ .../use-cases/solution-form.use-cases.ts | 35 ++ .../program/program-management.model.ts | 1 + .../ports/solution.repository.port.ts | 11 + .../solution/solution-form-rebase.spec.ts | 58 ++ .../domain/solution/solution-form-rebase.ts | 90 +++ .../app/domain/solution/solution-values.ts | 5 + .../solution/solution-http.adapter.ts | 21 + .../solution/solution.repository.ts | 13 + .../management/management.component.html | 22 +- .../management/management.component.ts | 4 + .../solution-form.component.html | 334 +++++++++++ .../solution-form.component.scss | 25 + .../solution-form.component.spec.ts | 164 ++++++ .../solution-form/solution-form.component.ts | 517 ++++++++++++++++++ .../solution-fields.component.html | 135 +++++ .../solution-fields.component.scss | 94 ++++ .../solution-fields.component.ts | 61 +++ .../app/utils/solution-field-controls.spec.ts | 38 ++ .../src/app/utils/solution-field-controls.ts | 57 ++ 24 files changed, 1801 insertions(+), 21 deletions(-) create mode 100644 docs/product/program-solutions-fe02.md create mode 100644 projects/social_platform/src/app/api/solution/solution-form-contract.spec.ts create mode 100644 projects/social_platform/src/app/api/solution/use-cases/solution-form.use-cases.ts create mode 100644 projects/social_platform/src/app/domain/solution/solution-form-rebase.spec.ts create mode 100644 projects/social_platform/src/app/domain/solution/solution-form-rebase.ts create mode 100644 projects/social_platform/src/app/domain/solution/solution-values.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html create mode 100644 projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.scss create mode 100644 projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts create mode 100644 projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html create mode 100644 projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss create mode 100644 projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts create mode 100644 projects/social_platform/src/app/utils/solution-field-controls.spec.ts create mode 100644 projects/social_platform/src/app/utils/solution-field-controls.ts diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index 46c23d51f..c7afab985 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -2,7 +2,7 @@ # C00 Контракт решения в программе -Версия 1.1.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений. Остальные команды остаются задачами BE04–BE06. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. +Версия 1.1.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны. Сохранение ответов и сдача остаются задачами BE05–BE06. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. Изменение 1.1.0: добавлен необязательный `permissions.can_leave`, вычисляемый сервером для собственного выхода принятого участника. При отсутствии поля клиент считает его false. Существующие поля, URL и payload команд не меняются; C00 1.0.0 остается совместимым подмножеством. Капитан, сданная заявка и выключенный режим дают false. @@ -45,6 +45,8 @@ C00 фиксирует техническую основу. D00 v3 явно со Физически новые приглашения хранятся в SolutionInvitation, а старые Invite не получают фиктивный project FK. Общая бизнес policy используется через адаптер владельца. Операции отзыва сохраняют действующую семантику удаления: pending приглашение удаляется; статус revoked не добавляется. accepted и declined остаются историческими строками. +BE04 добавляет nullable JSON-колонку PartnerProgramField.solution_options для точного хранения вариантов, включая символ |. При null чтение использует прежний options; старые определения автоматически не переписываются. Новая форма сохраняется целиком через solution-form. Прежний project-form не изменяет определения нового конструктора, в том числе после технического выключения режима. + ## Ревизии ProgramSolution.version начинается с 1 и увеличивается при каждом успешном изменении содержимого, состава или приглашений и при сдаче. Изменение разрешений из-за времени или роли и изменение формы организатором само по себе версию решения не повышают. diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json index 045e53ba8..907543709 100644 --- a/docs/contracts/program-solutions-v1/openapi.json +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -4,7 +4,7 @@ "info": { "title": "PROCOLLAB Program Solutions C00", "version": "1.1.0", - "description": "Контракт для реализации и моков. Новые endpoints пока не реализованы. API base URL задается окружением. snake_case на проводе. Старые API не меняются." + "description": "Контракт решений в программах для реализации и моков. Endpoints подключаются поэтапно по BE01–BE06. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ { diff --git a/docs/product/program-solutions-fe02.md b/docs/product/program-solutions-fe02.md new file mode 100644 index 000000000..f3cc6089b --- /dev/null +++ b/docs/product/program-solutions-fe02.md @@ -0,0 +1,15 @@ + + +# FE02: настройка формы решения организатором + +В существующих настройках «Кейсы и поля» новый storage mode открывает конструктор решения. Отсутствие нового поля в management DTO сохраняет прежний проектный интерфейс. Используются текущие Mont, компоненты управления программой, модальные окна и guard несохраненных изменений. + +Доступны явное копирование шаблонов КЧ/хакатона, порядок и редактирование полей, тип, назначение, обязательность, варианты, подсказка, текстовые/числовые/файловые ограничения и предпросмотр. Поля с ответами нельзя удалить или сменить их тип; шаблон блокируется по has_answers. Счетчик затронутых черновиков и полномочия на изменение типа/удаление приходят с сервера. Название заявки и команда остаются вне конструктора. + +Отказ сервера сохраняет локальные значения. При конфликте ревизии GET объединяет локальные правки с актуальными неизмененными свойствами, добавленными полями, вариантами и порядком другого организатора. Измененные локально поля, удаленные на сервере, остаются видимыми и блокируют сохранение до явного разрешения. Повторного PUT автоматически нет. Смена программы отменяет запросы; отложенные подтверждения проверяют контекст. + +Добавлен общий renderer решения и helpers контролов для следующего FE03: ни один кейс не выбирается автоматически, значения 0/false сохранены, null отличается от ответа. Предпросмотр не загружает файлы; реальная загрузка и сохранение ответов продолжаются BE05/FE03. Данные сданной формы читает тот же renderer в readonly режиме. + +Проверки: полный test:ci прошел — 2530 тестов в 463 файлах и строгие TypeScript-примеры C00. Development build, ESLint без ошибок (6 прежних предупреждений) и Stylelint прошли. Реальный локальный Django/PostgreSQL/JWT сценарий сохраняет шаблон КЧ с точными вариантами, проверяет returned revision/affected_draft_count и пустой выбор в предпросмотре. Desktop и 390 px проверены визуально; нет горизонтального overflow и ошибок JavaScript. Конфликты, поздние ответы, смена программы, 0/false и отмена локальных изменений покрыты отдельными проверками. + +Этот блок не завершает полный выпуск новой сущности: редактор ответов, сдача, оценивание и общий список решений остаются следующими этапами. diff --git a/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts b/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts index 69baf16e2..fa0c832c8 100644 --- a/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts +++ b/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts @@ -64,18 +64,16 @@ describe("Входящие приглашения решений", () => { it("user change cancels old reads and cannot show another user's invitation", async () => { const old = new Subject>>(); - get.execute - .mockReturnValueOnce(old) - .mockReturnValue( - of( - ok({ - count: 1, - next: null, - previous: null, - results: [{ ...invitation, id: 702, recipient: { ...invitation.recipient, id: 21 } }], - }), - ), - ); + get.execute.mockReturnValueOnce(old).mockReturnValue( + of( + ok({ + count: 1, + next: null, + previous: null, + results: [{ ...invitation, id: 702, recipient: { ...invitation.recipient, id: 21 } }], + }), + ), + ); inbox.bindUser(invitation.recipient.id); await vi.advanceTimersByTimeAsync(0); inbox.bindUser(21); diff --git a/projects/social_platform/src/app/api/solution/solution-form-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-form-contract.spec.ts new file mode 100644 index 000000000..70681531e --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-form-contract.spec.ts @@ -0,0 +1,94 @@ +/** @format */ + +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom } from "rxjs"; +import snakecaseKeys from "snakecase-keys"; +import { CamelcaseInterceptor } from "@core/lib/interceptors/camelcase.interceptor"; +import { API_URL } from "@core/lib/providers"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { SolutionRepository } from "@infrastructure/repository/solution/solution.repository"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { SolutionFormWrite } from "@domain/solution/solution-contract.model"; +import { + GetSolutionFormUseCase, + GetManagedSolutionFormUseCase, + GetSolutionTemplatesUseCase, + SaveManagedSolutionFormUseCase, +} from "./use-cases/solution-form.use-cases"; + +describe("C00 wire формы решения", () => { + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: API_URL, useValue: "https://local.test" }, + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: LoggerService, useValue: { warn: vi.fn() } }, + { provide: SolutionRepositoryPort, useExisting: SolutionRepository }, + ], + }), + ); + afterEach(() => TestBed.inject(HttpTestingController).verify()); + it("reads participant/manager forms and templates through the exact C00 routes", async () => { + for (const [useCase, path, scenario] of [ + [GetSolutionFormUseCase, "/programs/12/solution-form/", "form"], + [GetManagedSolutionFormUseCase, "/programs/12/management/solution-form/", "form_management"], + [GetSolutionTemplatesUseCase, "/solution-form-templates/", "templates"], + ] as const) { + const source = + useCase === GetSolutionTemplatesUseCase + ? TestBed.inject(GetSolutionTemplatesUseCase).execute() + : useCase === GetManagedSolutionFormUseCase + ? TestBed.inject(GetManagedSolutionFormUseCase).execute(12) + : TestBed.inject(GetSolutionFormUseCase).execute(12); + const result = firstValueFrom(source), + request = TestBed.inject(HttpTestingController).expectOne("https://local.test" + path); + expect(request.request.method).toBe("GET"); + const expected = solutionContractScenarios.find(item => item.name === scenario)!.response; + request.flush(snakecaseKeys(expected as object, { deep: true })); + expect(await result).toEqual({ ok: true, value: expected }); + } + }); + it("sends exact expected_revision, field IDs and snake_case validation in PUT", async () => { + const command = solutionContractScenarios.find(item => item.name === "save_form")! + .request as SolutionFormWrite; + const result = firstValueFrom( + TestBed.inject(SaveManagedSolutionFormUseCase).execute(12, command), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/management/solution-form/", + ); + expect(request.request.method).toBe("PUT"); + expect(request.request.body).toEqual(snakecaseKeys(command, { deep: true })); + const expected = solutionContractScenarios.find(item => item.name === "save_form")!.response; + request.flush(snakecaseKeys(expected as object, { deep: true })); + expect(await result).toEqual({ ok: true, value: expected }); + }); + it("reports a revision conflict without retrying or discarding the command", async () => { + const command = solutionContractScenarios.find(item => item.name === "save_form")! + .request as SolutionFormWrite; + const result = firstValueFrom( + TestBed.inject(SaveManagedSolutionFormUseCase).execute(12, command), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/management/solution-form/", + ); + request.flush( + { + code: "form_revision_conflict", + detail: "Форма изменилась", + errors: [], + meta: { current_form_revision: "b".repeat(64) }, + }, + { status: 409, statusText: "Conflict" }, + ); + expect((await result).ok).toBe(false); + TestBed.inject(HttpTestingController).expectNone( + "https://local.test/programs/12/management/solution-form/", + ); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-form.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-form.use-cases.ts new file mode 100644 index 000000000..2af9d4b3e --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-form.use-cases.ts @@ -0,0 +1,35 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { SolutionFormWrite } from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class GetSolutionFormUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number) { + return solutionRequestResult(() => this.repository.form(programId)); + } +} +@Injectable({ providedIn: "root" }) +export class GetManagedSolutionFormUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number) { + return solutionRequestResult(() => this.repository.managedForm(programId)); + } +} +@Injectable({ providedIn: "root" }) +export class SaveManagedSolutionFormUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionFormWrite) { + return solutionRequestResult(() => this.repository.saveManagedForm(programId, command)); + } +} +@Injectable({ providedIn: "root" }) +export class GetSolutionTemplatesUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute() { + return solutionRequestResult(() => this.repository.templates()); + } +} diff --git a/projects/social_platform/src/app/domain/program/program-management.model.ts b/projects/social_platform/src/app/domain/program/program-management.model.ts index bff4d240d..1c8d75466 100644 --- a/projects/social_platform/src/app/domain/program/program-management.model.ts +++ b/projects/social_platform/src/app/domain/program/program-management.model.ts @@ -49,6 +49,7 @@ export interface ManagementDates { datetimeFinished: string; } export interface ProgramManagement extends ManagementBasic, ManagementDates { + solutionStorageMode?: "legacy_project" | "solution"; id: number; updatedAt: string; isCompetitive: boolean; diff --git a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts index 1c2aa436c..caeb1ccd8 100644 --- a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts +++ b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts @@ -15,9 +15,20 @@ import { SolutionUpdateInvitation, SolutionInvitationCreated, SolutionInvitationActionResult, + SolutionForm, + SolutionFormWrite, + SolutionFormManagement, + SolutionTemplates, } from "../solution-contract.model"; export abstract class SolutionRepositoryPort { + abstract form(programId: number): Observable; + abstract managedForm(programId: number): Observable; + abstract saveManagedForm( + programId: number, + command: SolutionFormWrite, + ): Observable; + abstract templates(): Observable; abstract context(programId: number): Observable; abstract create(programId: number, command: CreateSolutionCommand): Observable; abstract get(programId: number, solutionId: number): Observable; diff --git a/projects/social_platform/src/app/domain/solution/solution-form-rebase.spec.ts b/projects/social_platform/src/app/domain/solution/solution-form-rebase.spec.ts new file mode 100644 index 000000000..24480773c --- /dev/null +++ b/projects/social_platform/src/app/domain/solution/solution-form-rebase.spec.ts @@ -0,0 +1,58 @@ +/** @format */ + +import { rebaseSolutionFields } from "./solution-form-rebase"; +import { solutionContractScenarios } from "./solution-contract.fixture"; +import { SolutionForm } from "./solution-contract.model"; +const form = solutionContractScenarios.find(s => s.name === "form")!.response as SolutionForm; +describe("Разрешение конфликта формы", () => { + it("preserves a remote reorder when only the local label changed", () => { + const base = structuredClone(form.fields), + local = base.map(field => ({ ...structuredClone(field), clientKey: field.id })); + local[0].label = "Локальная подпись"; + const remote = [...structuredClone(base)].reverse(); + const result = rebaseSolutionFields(base, local, remote); + expect(result.fields.map(field => field.id)).toEqual(remote.map(field => field.id)); + expect(result.fields.find(field => field.id === base[0].id)!.label).toBe("Локальная подпись"); + }); + it("keeps local labels, remote help, added choices and new remote fields", () => { + const base = structuredClone(form.fields), + local = base.map(field => ({ ...structuredClone(field), clientKey: field.id })); + local[0].label = "Моя задача"; + local[0].options.push("Мой трек"); + const remote = structuredClone(base); + remote[0].helpText = "Подсказка коллеги"; + remote[0].options.push("Трек коллеги"); + remote.push({ ...remote[1], id: 9999 }); + const result = rebaseSolutionFields(base, local, remote); + expect(result.fields[0].label).toBe("Моя задача"); + expect(result.fields[0].helpText).toBe("Подсказка коллеги"); + expect(result.fields[0].options).toContain("Трек коллеги"); + expect(result.fields[0].options).toContain("Мой трек"); + expect(result.fields.some(field => field.id === 9999)).toBe(true); + }); + it("does not hide a locally edited field that was deleted remotely", () => { + const base = structuredClone(form.fields), + local = base.map(field => ({ ...structuredClone(field), clientKey: field.id })); + local[1].label = "Мой ввод"; + const result = rebaseSolutionFields( + base, + local, + base.filter(field => field.id !== base[1].id), + ); + expect(result.missing).toContain(base[1].id); + expect(result.fields.some(field => field.label === "Мой ввод")).toBe(true); + }); + it("drops unedited remote deletions and preserves local deletion", () => { + const base = structuredClone(form.fields), + local = base + .filter((_, index) => index !== 2) + .map(field => ({ ...structuredClone(field), clientKey: field.id })); + const result = rebaseSolutionFields( + base, + local, + base.filter((_, index) => index !== 1), + ); + expect(result.fields.some(field => field.id === base[1].id)).toBe(false); + expect(result.fields.some(field => field.id === base[2].id)).toBe(false); + }); +}); diff --git a/projects/social_platform/src/app/domain/solution/solution-form-rebase.ts b/projects/social_platform/src/app/domain/solution/solution-form-rebase.ts new file mode 100644 index 000000000..d059d9135 --- /dev/null +++ b/projects/social_platform/src/app/domain/solution/solution-form-rebase.ts @@ -0,0 +1,90 @@ +/** @format */ + +import { SolutionField, SolutionFieldWrite } from "./solution-contract.model"; + +export type EditableSolutionField = SolutionFieldWrite & { clientKey: number }; +const equal = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b); +export function fieldWrite( + field: SolutionFieldWrite, + position = field.position, +): SolutionFieldWrite { + return { + id: field.id, + type: field.type, + purpose: field.purpose, + label: field.label, + helpText: field.helpText, + required: field.required, + position, + options: [...field.options], + validation: { ...field.validation, allowedExtensions: [...field.validation.allowedExtensions] }, + }; +} +/** Сохраняет локальный ввод и добавления другого организатора; удаленные ID требуют явного решения. */ +export function rebaseSolutionFields( + base: SolutionField[], + local: EditableSolutionField[], + remote: SolutionField[], +) { + const old = new Map(base.map(field => [field.id, field])), + latest = new Map(remote.map(field => [field.id, field])); + const missing: number[] = []; + const fields: EditableSolutionField[] = []; + for (const localField of local) { + if (localField.id === null) { + fields.push(localField); + continue; + } + const before = old.get(localField.id), + current = latest.get(localField.id); + if (!current) { + if (before && equal(fieldWrite(before), fieldWrite(localField))) continue; + missing.push(localField.id); + fields.push(localField); + continue; + } + if (!before) { + fields.push(localField); + continue; + } + const changes = Object.fromEntries( + Object.entries(fieldWrite(localField)).filter( + ([key, value]) => key !== "position" && !equal(value, before[key as keyof SolutionField]), + ), + ); + const validation = { ...current.validation }; + for (const key of Object.keys(localField.validation) as (keyof typeof validation)[]) { + if (!equal(localField.validation[key], before.validation[key])) + Object.assign(validation, { [key]: localField.validation[key] }); + } + const options = equal(localField.options, before.options) + ? current.options + : [ + ...localField.options, + ...current.options.filter( + value => !before.options.includes(value) && !localField.options.includes(value), + ), + ]; + fields.push({ ...current, ...changes, options, validation, clientKey: localField.clientKey }); + } + for (const current of remote) + if (!old.has(current.id) && !fields.some(field => field.id === current.id)) + fields.push({ ...current, clientKey: current.id }); + const localIds = new Set(local.map(field => field.id)); + const originalOrder = base.filter(field => localIds.has(field.id)).map(field => field.id); + const editedOrder = local + .filter(field => field.id !== null && old.has(field.id)) + .map(field => field.id); + const insertedBeforeExisting = local.some( + (field, index) => + field.id === null && + local.slice(index + 1).some(next => next.id !== null && old.has(next.id)), + ); + if (equal(originalOrder, editedOrder) && !insertedBeforeExisting) { + const rank = new Map(remote.map((field, index) => [field.id, index])); + fields.sort( + (a, b) => (rank.get(a.id ?? 0) ?? remote.length) - (rank.get(b.id ?? 0) ?? remote.length), + ); + } + return { fields, missing }; +} diff --git a/projects/social_platform/src/app/domain/solution/solution-values.ts b/projects/social_platform/src/app/domain/solution/solution-values.ts new file mode 100644 index 000000000..387d1fe03 --- /dev/null +++ b/projects/social_platform/src/app/domain/solution/solution-values.ts @@ -0,0 +1,5 @@ +/** @format */ + +export function hasSolutionAnswer(value: unknown): boolean { + return value !== null && value !== undefined && !(typeof value === "string" && !value.trim()); +} diff --git a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts index 34b3dbe24..222da6033 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts @@ -17,6 +17,10 @@ import { SolutionUpdateInvitation, SolutionInvitationCreated, SolutionInvitationActionResult, + SolutionForm, + SolutionFormWrite, + SolutionFormManagement, + SolutionTemplates, } from "@domain/solution/solution-contract.model"; import { SolutionListQuery, @@ -33,6 +37,23 @@ export class SolutionHttpAdapter { return `/programs/${programId}/solutions/`; } + private formPath(programId: number, management = false): string { + if (!validSolutionId(programId)) throw new RangeError("Invalid program ID"); + return `/programs/${programId}/${management ? "management/" : ""}solution-form/`; + } + form(programId: number) { + return this.api.get(this.formPath(programId)); + } + managedForm(programId: number) { + return this.api.get(this.formPath(programId, true)); + } + saveManagedForm(programId: number, command: SolutionFormWrite) { + return this.api.put(this.formPath(programId, true), command); + } + templates() { + return this.api.get("/solution-form-templates/"); + } + context(programId: number) { this.path(programId); return this.api.get(`/programs/${programId}/solution-context/`); diff --git a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts index db8ec4ab0..cf9afeb7b 100644 --- a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts @@ -12,12 +12,25 @@ import { SolutionCaptainCommand, SolutionCreateInvitation, SolutionUpdateInvitation, + SolutionFormWrite, } from "@domain/solution/solution-contract.model"; import { SolutionHttpAdapter } from "../../adapters/solution/solution-http.adapter"; @Injectable({ providedIn: "root" }) export class SolutionRepository implements SolutionRepositoryPort { private readonly http = inject(SolutionHttpAdapter); + form(programId: number) { + return this.http.form(programId); + } + managedForm(programId: number) { + return this.http.managedForm(programId); + } + saveManagedForm(programId: number, command: SolutionFormWrite) { + return this.http.saveManagedForm(programId, command); + } + templates() { + return this.http.templates(); + } context(programId: number) { return this.http.context(programId); } diff --git a/projects/social_platform/src/app/ui/pages/program/management/management.component.html b/projects/social_platform/src/app/ui/pages/program/management/management.component.html index 49f9a7497..35724345c 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/management.component.html +++ b/projects/social_platform/src/app/ui/pages/program/management/management.component.html @@ -643,13 +643,21 @@

{{ evaluation.name }}

> } @if (settingsTab() === "project-form") { - + @if (program.solutionStorageMode === "solution") { + + } @else { + + } } @else if (settingsTab() === "criteria") { (null); readonly saved = signal(false, { equal: () => false }); readonly projectForm = viewChild(ManagementProjectFormComponent); + readonly solutionForm = viewChild(ManagementSolutionFormComponent); private readonly managementLayout = viewChild>("managementLayout"); readonly floatingPosition = signal<{ center: number; width: number } | null>(null); readonly projectFormStatus = signal({ dirty: false, busy: false }); @@ -1118,6 +1121,7 @@ export class ProgramManagementComponent implements OnInit { this.materialForm.markAsPristine(); this.editingMaterial.set(null); this.projectForm()?.discard(); + this.solutionForm()?.discard(); this.projectFormStatus.set({ dirty: false, busy: false }); this.criteriaForm()?.discard(); this.criteriaStatus.set({ dirty: false, busy: false }); diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html new file mode 100644 index 000000000..fdf453c32 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html @@ -0,0 +1,334 @@ + + +
+ @if (loading()) { +

Загружаем форму решения…

+ } + @if (error()) { +
+ {{ error() }} + +
+ } + @if (notice()) { +

{{ notice() }}

+ } + @if (snapshot(); as current) { +
+
+

Форма решения

+

+ Настройте общие поля для всех команд программы. Команда и название заявки остаются + системными данными. +

+
+ +
+
+

Формат программы

+ @if (templateError()) { +
+ {{ templateError() }} + +
+ } +
+ + +
+ @if (current.hasAnswers) { +

+ Шаблон нельзя менять после появления ответов. Подписи, порядок и требования полей можно + настроить. +

+ } @else { +

+ Шаблон копируется в эту программу. Добавьте названия кейсов или задач и сохраните форму. +

+ } +
+
+
+

Поля решения

+ +
+ @for (field of fields(); track field.clientKey; let index = $index) { +
+
+

{{ field.label }}{{ field.required ? " *" : "" }}

+

+ {{ typeLabels[field.type] }} / {{ purposeLabels[field.purpose] }} + @if (usage(field); as use) { + / Ответов: {{ use.answersCount }} + } +

+ @if (missingIds().includes(field.id ?? 0)) { +

+ Поле удалено другим организатором. Локальные данные сохранены; уберите поле или + добавьте его заново. +

+ } +
+
+ +
+
+ } @empty { +

Выберите начальный шаблон, чтобы добавить поля решения.

+ } +

+ Поля с ответами нельзя удалить или изменить их тип. Использованные варианты выбора защищены + от удаления. Новые обязательные поля действуют только для черновиков. +

+
+
+

+ Черновиков, которым не хватает обязательных ответов: {{ current.affectedDraftCount }}. + Сданные решения сохраняют свою форму. +

+
+ +
+
+ } +
+ +
+
+

Настройка поля

+ +
+ + + + @if (editor.controls.type.disabled) { +

Тип закреплен назначением поля или сохраненными ответами.

+ } + + + @if (editor.controls.type.value === "select" || editor.controls.type.value === "radio") { +
+ Варианты выбора * +
+ @for (option of editor.controls.options.controls; track option; let index = $index) { +
+ +
+ } +
+ +
+ } + @if (["text", "textarea", "url"].includes(editor.controls.type.value)) { +
+ +
+ } + @if (editor.controls.type.value === "number") { +
+ +
+ } + @if (editor.controls.type.value === "file") { + +

+ Один файл на поле, не более 100 МиБ. Пустой список расширений сохраняет общий список + разрешенных типов загрузки. +

+ } + @if (editorError()) { +

{{ editorError() }}

+ } + @if (editor.invalid && editor.touched) { +

Проверьте обязательные поля и ограничения.

+ } +
+ +
+
+
+ +
+
+

Предпросмотр решения

+ +
+ +
+
diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.scss b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.scss new file mode 100644 index 000000000..3a5b64a62 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.scss @@ -0,0 +1,25 @@ +/** @format */ + +@use "../project-form/project-form.component"; + +.constraints { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.template-choice { + align-items: end; + margin-top: 16px; +} + +.template-choice label { + flex: 1; + min-width: 180px; +} + +@media (max-width: 600px) { + .constraints { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.spec.ts new file mode 100644 index 000000000..f748e426e --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.spec.ts @@ -0,0 +1,164 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { of, Subject } from "rxjs"; +import { + GetManagedSolutionFormUseCase, + GetSolutionTemplatesUseCase, + SaveManagedSolutionFormUseCase, +} from "@api/solution/use-cases/solution-form.use-cases"; +import { + SolutionFormManagement, + SolutionTemplates, +} from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { fail, ok } from "@domain/shared/result.type"; +import { ManagementSolutionFormComponent } from "./solution-form.component"; + +const base = solutionContractScenarios.find(s => s.name === "form_management")! + .response as SolutionFormManagement; +const templates = solutionContractScenarios.find(s => s.name === "templates")! + .response as SolutionTemplates; +describe("Конструктор формы решения", () => { + const read = { execute: vi.fn() }, + save = { execute: vi.fn() }, + list = { execute: vi.fn() }; + beforeEach(() => { + read.execute.mockReset().mockReturnValue(of(ok(structuredClone(base)))); + save.execute.mockReset().mockReturnValue(of(ok(structuredClone(base)))); + list.execute.mockReset().mockReturnValue(of(ok(structuredClone(templates)))); + TestBed.configureTestingModule({ + imports: [ManagementSolutionFormComponent], + providers: [ + { provide: GetManagedSolutionFormUseCase, useValue: read }, + { provide: SaveManagedSolutionFormUseCase, useValue: save }, + { provide: GetSolutionTemplatesUseCase, useValue: list }, + ], + }); + }); + async function setup() { + const fixture = TestBed.createComponent(ManagementSolutionFormComponent); + fixture.componentRef.setInput("programId", base.form.programId); + fixture.componentRef.setInput("confirmDiscard", () => Promise.resolve(true)); + fixture.detectChanges(); + await fixture.whenStable(); + return fixture; + } + it("renders server usage, locks used types and keeps system case", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + expect(fixture.nativeElement.textContent).toContain("Форма решения"); + expect(component.templateChoice.disabled).toBe(true); + const used = component.fields().find(field => component.usage(field)?.answersCount)!; + expect(component.canDelete(used)).toBe(false); + component.edit(used); + expect(component.editor.controls.type.disabled).toBe(true); + expect(component.editor.controls.purpose.disabled).toBe(true); + component.remove(used); + expect(component.fields().some(field => field.id === used.id)).toBe(true); + }); + it("copies a template locally with null API IDs and no automatic save", async () => { + read.execute.mockReturnValue( + of(ok({ ...structuredClone(base), hasAnswers: false, fieldsUsage: [] })), + ); + const fixture = await setup(), + component = fixture.componentInstance; + component.templateChoice.setValue("hackathon"); + await component.applyTemplate(); + expect(component.fields().every(field => field.id === null)).toBe(true); + expect(component.fields().some(field => field.purpose === "repository" && field.required)).toBe( + true, + ); + expect(save.execute).not.toHaveBeenCalled(); + expect(component.dirty()).toBe(true); + }); + it("sends exact form revision and excludes client keys from PUT", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + component.edit(component.fields()[1]); + component.editor.controls.label.setValue("Измененная подпись"); + component.applyField(); + component.save(); + const [programId, command] = save.execute.mock.calls[0]; + expect(programId).toBe(base.form.programId); + expect(command.expectedRevision).toBe(base.form.revision); + expect(command.fields[1].label).toBe("Измененная подпись"); + expect(command.fields.every((field: Record) => !("clientKey" in field))).toBe( + true, + ); + }); + it("keeps local edits on failure and merges fresh fields on revision conflict", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + component.edit(component.fields()[1]); + component.editor.controls.label.setValue("Моя подпись"); + component.applyField(); + const added = { + ...structuredClone(base.form.fields[1]), + id: 9999, + position: 10, + label: "Добавлено коллегой", + }; + read.execute.mockReturnValue( + of( + ok({ + ...structuredClone(base), + form: { ...base.form, revision: "b".repeat(64), fields: [...base.form.fields, added] }, + }), + ), + ); + save.execute.mockReturnValue( + of( + fail({ code: "form_revision_conflict", detail: "Форма изменилась", errors: [], meta: {} }), + ), + ); + component.save(); + await fixture.whenStable(); + expect(component.fields().find(field => field.id === base.form.fields[1].id)!.label).toBe( + "Моя подпись", + ); + expect(component.fields().some(field => field.id === 9999)).toBe(true); + expect(component.snapshot()?.form.revision).toBe("b".repeat(64)); + expect(save.execute).toHaveBeenCalledTimes(1); + expect(component.dirty()).toBe(true); + }); + it("new program cancels old reads and cannot render their late result", async () => { + const old = new Subject>>(); + read.execute.mockReturnValueOnce(old); + const fixture = await setup(), + component = fixture.componentInstance; + fixture.componentRef.setInput("programId", 13); + fixture.detectChanges(); + await fixture.whenStable(); + old.next( + ok({ + ...structuredClone(base), + form: { ...base.form, programId: 12, revision: "c".repeat(64) }, + }), + ); + expect(read.execute).toHaveBeenLastCalledWith(13); + expect(component.snapshot()?.form.revision).not.toBe("c".repeat(64)); + }); + it("preview never selects the first case and does not upload", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + component.openPreview(); + const caseField = component.previewFields().find(field => field.purpose === "case")!; + expect(component.previewGroup().get(String(caseField.id))?.value).toBeNull(); + expect(save.execute).not.toHaveBeenCalled(); + }); + it("discard resets changes and the parent dirty state without sending requests", async () => { + const fixture = await setup(), + component = fixture.componentInstance, + states: unknown[] = []; + component.statusChange.subscribe(state => states.push(state)); + component.move(1, 1); + fixture.detectChanges(); + expect(component.dirty()).toBe(true); + component.discard(); + fixture.detectChanges(); + expect(component.dirty()).toBe(false); + expect(save.execute).not.toHaveBeenCalled(); + expect(states).toContainEqual({ dirty: false, busy: false }); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts new file mode 100644 index 000000000..9a3d03b2c --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts @@ -0,0 +1,517 @@ +/** @format */ + +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + computed, + effect, + inject, + input, + output, + signal, + untracked, +} from "@angular/core"; +import { + FormArray, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, + ValidatorFn, +} from "@angular/forms"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { finalize, Subject, takeUntil } from "rxjs"; +import { + AlertDirective, + ButtonDirective, + ChoiceDirective, + FieldDirective, + IconComponent, +} from "@uilib"; +import { + GetManagedSolutionFormUseCase, + GetSolutionTemplatesUseCase, + SaveManagedSolutionFormUseCase, +} from "@api/solution/use-cases/solution-form.use-cases"; +import { + SolutionField, + SolutionFieldType, + SolutionFormManagement, + SolutionFormWrite, + SolutionPurpose, + SolutionTemplate, + SolutionTemplateName, +} from "@domain/solution/solution-contract.model"; +import { + EditableSolutionField, + fieldWrite, + rebaseSolutionFields, +} from "@domain/solution/solution-form-rebase"; +import { solutionFieldsGroup } from "@utils/solution-field-controls"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; +import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fields.component"; + +const emptyValidation = () => ({ + minLength: null as number | null, + maxLength: null as number | null, + minimum: null as number | null, + maximum: null as number | null, + allowedExtensions: [] as string[], + maxFileSizeBytes: null as number | null, +}); +const integerConstraint: ValidatorFn = control => + control.value === null || Number.isInteger(control.value) ? null : { integer: true }; +const purposeTypes: Partial> = { + case: "select", + presentation: "file", + repository: "url", + demo: "url", + video: "url", + run_instructions: "textarea", +}; + +@Component({ + selector: "app-management-solution-form", + imports: [ + ReactiveFormsModule, + AlertDirective, + ButtonDirective, + ChoiceDirective, + FieldDirective, + IconComponent, + ModalComponent, + SolutionFieldsComponent, + ], + templateUrl: "./solution-form.component.html", + styleUrl: "./solution-form.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ManagementSolutionFormComponent { + readonly programId = input.required(); + readonly confirmDiscard = input.required<() => Promise>(); + readonly statusChange = output<{ dirty: boolean; busy: boolean }>(); + private readonly getForm = inject(GetManagedSolutionFormUseCase); + private readonly saveForm = inject(SaveManagedSolutionFormUseCase); + private readonly getTemplates = inject(GetSolutionTemplatesUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly cancelRead = new Subject(); + private readonly cancelSave = new Subject(); + private context = 0; + private clientKey = -1; + readonly snapshot = signal(null); + readonly fields = signal([]); + readonly template = signal(null); + readonly templates = signal([]); + readonly templateError = signal(""); + readonly templateLoading = signal(false); + readonly loading = signal(false); + readonly saving = signal(false); + readonly error = signal(""); + readonly notice = signal(""); + readonly missingIds = signal([]); + readonly editing = signal(null); + readonly editorDirty = signal(false); + readonly editorError = signal(""); + readonly previewOpen = signal(false); + readonly previewFields = signal([]); + readonly previewGroup = signal(solutionFieldsGroup([])); + readonly templateChoice = new FormControl(null); + readonly typeLabels: Record = { + text: "Короткий текст", + textarea: "Многострочный текст", + url: "Ссылка", + number: "Число", + select: "Выбор из списка", + checkbox: "Да или нет", + file: "Файл", + radio: "Радио (существующее поле)", + }; + readonly purposeLabels: Record = { + other: "Дополнительное поле", + case: "Кейс или задача", + presentation: "Презентация", + repository: "Репозиторий", + demo: "Демо", + video: "Видео", + run_instructions: "Инструкция запуска", + }; + readonly types: SolutionFieldType[] = [ + "text", + "textarea", + "url", + "number", + "select", + "checkbox", + "file", + ]; + readonly purposes = Object.keys(this.purposeLabels) as SolutionPurpose[]; + readonly editor = new FormGroup({ + label: new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.pattern(/\S/), Validators.maxLength(256)], + }), + type: new FormControl("text", { nonNullable: true }), + purpose: new FormControl("other", { nonNullable: true }), + helpText: new FormControl("", { nonNullable: true, validators: Validators.maxLength(4000) }), + required: new FormControl(false, { nonNullable: true }), + options: new FormArray>([]), + minLength: new FormControl(null, [Validators.min(0), integerConstraint]), + maxLength: new FormControl(null, [Validators.min(0), integerConstraint]), + minimum: new FormControl(null), + maximum: new FormControl(null), + extensions: new FormControl("", { nonNullable: true }), + maxFileSizeMiB: new FormControl(null, [ + Validators.min(1 / 1048576), + Validators.max(100), + ]), + }); + readonly dirty = computed(() => { + const original = this.snapshot(); + return ( + !!original && + (JSON.stringify(this.writeBody()) !== + JSON.stringify({ + expectedRevision: original.form.revision, + template: original.form.template, + fields: original.form.fields.map((field, index) => fieldWrite(field, index)), + }) || + this.editorDirty()) + ); + }); + constructor() { + effect(() => { + const id = this.programId(); + untracked(() => { + if (id !== this.context) { + this.cancelSave.next(); + this.snapshot.set(null); + this.fields.set([]); + this.editing.set(null); + this.editorDirty.set(false); + this.context = id; + } + this.load(false); + }); + }); + effect(() => + this.statusChange.emit({ dirty: this.dirty(), busy: this.loading() || this.saving() }), + ); + this.loadTemplates(); + this.editor.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => this.editorDirty.set(this.editor.dirty)); + this.editor.controls.purpose.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(purpose => this.configurePurpose(purpose)); + } + loadTemplates() { + if (this.templateLoading()) return; + this.templateLoading.set(true); + this.templateError.set(""); + this.getTemplates + .execute() + .pipe( + takeUntilDestroyed(this.destroyRef), + finalize(() => this.templateLoading.set(false)), + ) + .subscribe(result => { + if (result.ok) this.templates.set(result.value.results); + else this.templateError.set(result.error.detail); + }); + } + private writeBody(): SolutionFormWrite { + return { + expectedRevision: this.snapshot()?.form.revision ?? "", + template: this.template() ?? "case_championship", + fields: this.fields().map((field, index) => fieldWrite(field, index)), + }; + } + private load(keepLocal: boolean) { + this.cancelRead.next(); + this.loading.set(true); + const id = this.programId(); + this.getForm + .execute(id) + .pipe( + takeUntil(this.cancelRead), + takeUntilDestroyed(this.destroyRef), + finalize(() => this.loading.set(false)), + ) + .subscribe(result => { + if (id !== this.context) return; + if (!result.ok) { + if (["forbidden", "not_found", "authentication_required"].includes(result.error.code)) { + this.snapshot.set(null); + this.fields.set([]); + this.editing.set(null); + this.editorDirty.set(false); + } + this.error.set(result.error.detail); + return; + } + const before = this.snapshot(); + if (keepLocal && before) { + const rebased = rebaseSolutionFields( + before.form.fields, + this.fields(), + result.value.form.fields, + ); + this.fields.set(rebased.fields); + this.missingIds.set(rebased.missing); + if (this.template() === before.form.template) + this.template.set(result.value.form.template); + this.snapshot.set(result.value); + this.notice.set( + "Форма обновлена. Ваши правки сохранены локально. Проверьте их и сохраните еще раз.", + ); + } else { + this.snapshot.set(result.value); + this.discard(); + } + if (result.value.hasAnswers) this.templateChoice.disable({ emitEvent: false }); + else this.templateChoice.enable({ emitEvent: false }); + }); + } + reload() { + if (this.saving()) return; + if (!this.snapshot()) this.load(false); + else this.load(true); + } + discard() { + const snapshot = this.snapshot(); + this.fields.set( + snapshot?.form.fields.map(field => ({ ...structuredClone(field), clientKey: field.id })) ?? + [], + ); + this.template.set(snapshot?.form.template ?? null); + this.templateChoice.setValue(this.template(), { emitEvent: false }); + this.editing.set(null); + this.editorDirty.set(false); + this.error.set(""); + this.notice.set(""); + this.missingIds.set([]); + } + async cancel() { + const context = this.context; + if ((!this.dirty() || (await this.confirmDiscard()())) && context === this.context) + this.discard(); + } + async applyTemplate() { + const key = this.templateChoice.value, + template = this.templates().find(template => template.key === key); + const context = this.context; + if (!template || this.saving() || this.loading() || this.snapshot()?.hasAnswers) return; + if (this.dirty() && !(await this.confirmDiscard()())) return; + if (context !== this.context) return; + this.template.set(template.key); + this.fields.set( + template.fields.map(field => ({ ...structuredClone(field), clientKey: this.clientKey-- })), + ); + this.editing.set(null); + this.editorDirty.set(false); + this.missingIds.set([]); + this.error.set(""); + this.notice.set(""); + } + usage(field: EditableSolutionField) { + return this.snapshot()?.fieldsUsage.find(usage => usage.fieldId === field.id); + } + canDelete(field: EditableSolutionField) { + return ( + this.missingIds().includes(field.id ?? 0) || + (field.purpose !== "case" && (field.id === null || this.usage(field)?.canDelete === true)) + ); + } + remove(field: EditableSolutionField) { + if (this.saving() || !this.canDelete(field)) return; + this.fields.update(fields => fields.filter(row => row.clientKey !== field.clientKey)); + this.missingIds.update(ids => ids.filter(id => id !== field.id)); + } + move(index: number, offset: number) { + if (this.saving()) return; + const fields = [...this.fields()], + target = index + offset; + if (target < 0 || target >= fields.length) return; + [fields[index], fields[target]] = [fields[target], fields[index]]; + this.fields.set(fields); + } + edit(field?: EditableSolutionField) { + if (this.saving()) return; + this.editing.set(field?.clientKey ?? this.clientKey--); + this.editorError.set(""); + this.editor.enable({ emitEvent: false }); + this.editor.controls.options.clear({ emitEvent: false }); + for (const option of field?.options ?? []) + this.editor.controls.options.push( + new FormControl(option, { + nonNullable: true, + validators: [Validators.required, Validators.pattern(/\S/), Validators.maxLength(1000)], + }), + { emitEvent: false }, + ); + const validation = field?.validation ?? emptyValidation(); + this.editor.reset( + { + label: field?.label ?? "", + type: field?.type ?? "text", + purpose: field?.purpose ?? "other", + helpText: field?.helpText ?? "", + required: field?.required ?? false, + minLength: validation.minLength, + maxLength: validation.maxLength, + minimum: validation.minimum, + maximum: validation.maximum, + extensions: validation.allowedExtensions.join(", "), + maxFileSizeMiB: + validation.maxFileSizeBytes === null ? null : validation.maxFileSizeBytes / 1048576, + }, + { emitEvent: false }, + ); + if (field && field.id !== null && this.usage(field)?.canChangeType === false) { + this.editor.controls.type.disable({ emitEvent: false }); + this.editor.controls.purpose.disable({ emitEvent: false }); + } + this.configurePurpose(field?.purpose ?? "other"); + this.editor.markAsPristine(); + this.editorDirty.set(false); + } + private configurePurpose(purpose: SolutionPurpose) { + const type = purposeTypes[purpose]; + const source = this.fields().find(field => field.clientKey === this.editing()); + const used = source && source.id !== null && this.usage(source)?.canChangeType === false; + if (type) { + this.editor.controls.type.setValue(type, { emitEvent: false }); + this.editor.controls.type.disable({ emitEvent: false }); + } else if (!used) this.editor.controls.type.enable({ emitEvent: false }); + if (purpose === "case") { + this.editor.controls.required.setValue(true, { emitEvent: false }); + this.editor.controls.required.disable({ emitEvent: false }); + } else this.editor.controls.required.enable({ emitEvent: false }); + } + addOption() { + this.editor.controls.options.push( + new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.pattern(/\S/), Validators.maxLength(1000)], + }), + ); + this.editor.markAsDirty(); + this.editorDirty.set(true); + } + removeOption(index: number) { + this.editor.controls.options.removeAt(index); + this.editor.markAsDirty(); + this.editorDirty.set(true); + } + applyField() { + this.editor.markAllAsTouched(); + if (this.editor.invalid) return; + const value = this.editor.getRawValue(), + choice = ["select", "radio"].includes(value.type); + if ( + choice && + (!value.options.length || + new Set(value.options.map(option => option.trim().toLocaleLowerCase())).size !== + value.options.length) + ) { + this.editorError.set("Добавьте неповторяющиеся варианты выбора."); + return; + } + const source = this.fields().find(field => field.clientKey === this.editing()); + const validation = emptyValidation(); + if (["text", "textarea", "url"].includes(value.type)) + Object.assign(validation, { minLength: value.minLength, maxLength: value.maxLength }); + if (value.type === "number") + Object.assign(validation, { minimum: value.minimum, maximum: value.maximum }); + if (value.type === "file") + Object.assign(validation, { + allowedExtensions: value.extensions + .split(",") + .map(extension => extension.trim().toLowerCase()) + .filter(Boolean), + maxFileSizeBytes: + value.maxFileSizeMiB === null ? null : Math.round(value.maxFileSizeMiB * 1048576), + }); + if ( + (validation.minLength !== null && + validation.maxLength !== null && + validation.minLength > validation.maxLength) || + (validation.minimum !== null && + validation.maximum !== null && + validation.minimum > validation.maximum) + ) { + this.editorError.set("Минимум не должен превышать максимум."); + return; + } + const field: EditableSolutionField = { + id: source?.id ?? null, + clientKey: this.editing()!, + label: value.label, + type: value.type, + purpose: value.purpose, + helpText: value.helpText, + required: value.required, + position: source?.position ?? this.fields().length, + options: choice ? value.options : [], + validation, + }; + this.fields.update(fields => + source + ? fields.map(row => (row.clientKey === source.clientKey ? field : row)) + : [...fields, field], + ); + this.editing.set(null); + this.editorDirty.set(false); + } + async closeEditor() { + const context = this.context; + if (!this.editorDirty() || (await this.confirmDiscard()())) { + if (context !== this.context) return; + this.editing.set(null); + this.editorDirty.set(false); + } + } + openPreview() { + const fields = this.fields().map((field, index) => ({ + ...fieldWrite(field, index), + id: field.id ?? field.clientKey, + })); + this.previewFields.set(fields); + this.previewGroup.set(solutionFieldsGroup(fields)); + this.previewOpen.set(true); + } + save() { + if ( + this.saving() || + this.loading() || + !this.snapshot() || + !this.template() || + this.editing() !== null || + this.missingIds().length + ) + return; + this.saving.set(true); + this.error.set(""); + this.notice.set(""); + const id = this.context; + this.saveForm + .execute(id, this.writeBody()) + .pipe( + takeUntil(this.cancelSave), + takeUntilDestroyed(this.destroyRef), + finalize(() => this.saving.set(false)), + ) + .subscribe(result => { + if (id !== this.context) return; + if (result.ok) { + this.snapshot.set(result.value); + this.discard(); + this.notice.set("Форма решения сохранена."); + } else { + this.error.set(result.error.detail); + if (result.error.code === "form_revision_conflict") this.load(true); + } + }); + } +} diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html new file mode 100644 index 000000000..53c55fca3 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html @@ -0,0 +1,135 @@ + + +
+ @for (field of fields(); track field.id) { +
+ + @if (readonly()) { + @if (fileLink(field); as link) { + Открыть файл + } @else { +

{{ labelValue(field) }}

+ } + } @else { + @switch (field.type) { + @case ("textarea") { + + } + @case ("select") { + + } + @case ("radio") { + + } + @case ("checkbox") { +
+ {{ labelValue(field) }} +
+ } + @case ("file") { + @if (fileLink(field); as link) { + Открыть файл + } + +
+ + {{ field.validation.allowedExtensions.join(", ").toUpperCase() || "Один файл" }}, + до {{ (field.validation.maxFileSizeBytes ?? 104857600) / 1048576 }} МиБ +
+ @if (preview()) { + Загрузка файла доступна в заявке участника. + } + } + @case ("number") { + + } + @default { + + } + } + } +

{{ field.helpText }}

+ @if (errors()[field.id]) { + + } @else if (!readonly() && attempted() && field.required && !hasAnswer(value(field))) { + + } @else if ( + !readonly() && control(field)?.invalid && (attempted() || control(field)?.touched) + ) { + + } +
+ } +
diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss new file mode 100644 index 000000000..75509c0bd --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss @@ -0,0 +1,94 @@ +/** @format */ + +@use "styles/ui-foundation"; + +:host { + display: block; + min-width: 0; +} + +.solution-fields { + display: grid; + gap: 24px; +} + +fieldset { + display: grid; + gap: 8px; + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +label { + font-size: 14px; + line-height: 1.5; +} + +.ui-field { + @include ui-foundation.control; + + width: 100%; +} + +textarea.ui-field { + min-height: 120px; + resize: vertical; + border-radius: 12px; +} + +.file-upload { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} + +.ui-choice { + @include ui-foundation.choice; +} + +.ui-button { + @include ui-foundation.button; +} + +.boolean-answer { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} + +.answer { + margin: 0; + line-height: 1.5; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.help, +small { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: var(--grey-for-text); +} + +.error { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: var(--error, var(--ui-error-text)); +} + +a { + color: var(--accent); + overflow-wrap: anywhere; +} + +input[type="file"] { + width: 100%; + min-height: 44px; + font: inherit; +} diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts new file mode 100644 index 000000000..064171be7 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts @@ -0,0 +1,61 @@ +/** @format */ + +import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; +import { FormGroup, ReactiveFormsModule } from "@angular/forms"; +import { ButtonDirective, FieldDirective, ChoiceDirective, IconComponent } from "@uilib"; +import { SolutionField } from "@domain/solution/solution-contract.model"; +import { hasSolutionAnswer } from "@domain/solution/solution-values"; + +@Component({ + selector: "app-solution-fields", + imports: [ReactiveFormsModule, ButtonDirective, FieldDirective, ChoiceDirective, IconComponent], + templateUrl: "./solution-fields.component.html", + styleUrl: "./solution-fields.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionFieldsComponent { + readonly fields = input.required(); + readonly group = input.required(); + readonly readonly = input(false); + readonly preview = input(false); + readonly busy = input(false); + readonly attempted = input(false); + readonly errors = input>({}); + readonly upload = output<{ fieldId: number; file: File }>(); + readonly clearFile = output(); + readonly hasAnswer = hasSolutionAnswer; + control(field: SolutionField) { + return this.group().get(String(field.id)); + } + value(field: SolutionField): unknown { + return this.control(field)?.value; + } + labelValue(field: SolutionField): string { + const value = this.value(field); + if (!hasSolutionAnswer(value)) return "Ответ не указан"; + if (typeof value === "boolean") return value ? "Да" : "Нет"; + if (value && typeof value === "object" && "url" in value) return String(value.url); + return String(value); + } + fileLink(field: SolutionField): string | null { + const value = this.value(field); + if (!value || typeof value !== "object" || !("url" in value) || typeof value.url !== "string") + return null; + try { + const url = new URL(value.url); + return ["http:", "https:"].includes(url.protocol) ? url.href : null; + } catch { + return null; + } + } + fileChanged(field: SolutionField, event: Event) { + const input = event.target as HTMLInputElement, + file = input.files?.[0]; + input.value = ""; + if (file && !this.busy() && !this.readonly() && !this.preview()) + this.upload.emit({ fieldId: field.id, file }); + } + accept(field: SolutionField) { + return field.validation.allowedExtensions.map(extension => "." + extension).join(","); + } +} diff --git a/projects/social_platform/src/app/utils/solution-field-controls.spec.ts b/projects/social_platform/src/app/utils/solution-field-controls.spec.ts new file mode 100644 index 000000000..22b642e1f --- /dev/null +++ b/projects/social_platform/src/app/utils/solution-field-controls.spec.ts @@ -0,0 +1,38 @@ +/** @format */ + +import { solutionFieldsGroup } from "./solution-field-controls"; +import { hasSolutionAnswer } from "@domain/solution/solution-values"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { SolutionForm } from "@domain/solution/solution-contract.model"; +const form = solutionContractScenarios.find(s => s.name === "form")!.response as SolutionForm; +describe("Значения полей решения", () => { + it("distinguishes missing from zero and false", () => { + expect(hasSolutionAnswer(0)).toBe(true); + expect(hasSolutionAnswer(false)).toBe(true); + for (const value of [null, undefined, "", " "]) expect(hasSolutionAnswer(value)).toBe(false); + }); + it("does not choose a case implicitly", () => { + const group = solutionFieldsGroup(form.fields); + expect(group.get(String(form.fields[0].id))!.value).toBeNull(); + }); + it("restores exact false and zero values without required-checkbox coercion", () => { + const number = form.fields.find(field => field.type === "number")!, + checkbox = form.fields.find(field => field.type === "checkbox")!; + const group = solutionFieldsGroup( + [number, checkbox], + [ + { fieldId: number.id, value: 0 }, + { fieldId: checkbox.id, value: false }, + ], + ); + expect(group.get(String(number.id))!.value).toBe(0); + expect(group.get(String(checkbox.id))!.value).toBe(false); + expect(group.valid).toBe(true); + }); + it("retains invalid values for correction instead of silently replacing the selection", () => { + const field = form.fields[0], + group = solutionFieldsGroup([field], [{ fieldId: field.id, value: "Удаленный выбор" }]); + expect(group.get(String(field.id))!.value).toBe("Удаленный выбор"); + expect(group.valid).toBe(false); + }); +}); diff --git a/projects/social_platform/src/app/utils/solution-field-controls.ts b/projects/social_platform/src/app/utils/solution-field-controls.ts new file mode 100644 index 000000000..6c79061c6 --- /dev/null +++ b/projects/social_platform/src/app/utils/solution-field-controls.ts @@ -0,0 +1,57 @@ +/** @format */ + +import { FormControl, FormGroup, ValidatorFn, Validators } from "@angular/forms"; +import { SolutionField, SolutionAnswerValue } from "@domain/solution/solution-contract.model"; +import { hasSolutionAnswer } from "@domain/solution/solution-values"; + +export type SolutionControlValue = SolutionAnswerValue | null; +export function solutionFieldControl(field: SolutionField, value: SolutionControlValue = null) { + const rules: ValidatorFn[] = []; + const validation = field.validation; + if (["text", "textarea", "url"].includes(field.type)) { + if (validation.minLength !== null) rules.push(Validators.minLength(validation.minLength)); + if (validation.maxLength !== null) rules.push(Validators.maxLength(validation.maxLength)); + } + if (field.type === "number") { + if (validation.minimum !== null) rules.push(Validators.min(validation.minimum)); + if (validation.maximum !== null) rules.push(Validators.max(validation.maximum)); + } + if (["select", "radio"].includes(field.type)) { + rules.push(control => + !hasSolutionAnswer(control.value) || field.options.includes(control.value) + ? null + : { choice: true }, + ); + } + if (field.type === "url") + rules.push(control => { + if (!hasSolutionAnswer(control.value)) return null; + try { + const url = new URL(control.value); + return ["http:", "https:"].includes(url.protocol) && + !url.username && + !url.password && + !/\s/.test(control.value) + ? null + : { url: true }; + } catch { + return { url: true }; + } + }); + return new FormControl(value, rules); +} +export function solutionFieldsGroup( + fields: SolutionField[], + answers: { fieldId: number; value: SolutionAnswerValue }[] = [], +) { + const group = new FormGroup>>({}); + for (const field of fields) + group.addControl( + String(field.id), + solutionFieldControl( + field, + answers.find(answer => answer.fieldId === field.id)?.value ?? null, + ), + ); + return group; +} From 9a40d0fa8397767412c6a0498caecc9552b67234 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 21:56:44 +0300 Subject: [PATCH 03/47] feat(programs): edit solution drafts and attach original files --- docs/contracts/program-solutions-v1/README.md | 4 +- .../program-solutions-v1/examples.json | 2 +- .../program-solutions-v1/openapi.json | 16 +- docs/product/program-solutions-fe03.md | 15 + .../facades/solution-draft.facade.spec.ts | 156 +++++++++ .../solution/facades/solution-draft.facade.ts | 305 ++++++++++++++++++ .../solution/solution-draft-contract.spec.ts | 92 ++++++ .../use-cases/solution-draft.use-cases.ts | 21 ++ .../ports/solution.repository.port.ts | 8 + .../solution/solution-contract.model.ts | 2 +- .../solution/solution-http.adapter.ts | 10 + .../solution/solution.repository.ts | 7 + .../solution-form/solution-form.component.ts | 6 +- .../program/solution/solution.component.html | 128 +++++++- .../program/solution/solution.component.scss | 55 ++-- .../solution/solution.component.spec.ts | 89 ++++- .../program/solution/solution.component.ts | 103 +++++- .../app/ui/routes/program/detail.routes.ts | 4 + .../ui/routes/program/solution-leave.guard.ts | 6 + .../solution-fields.component.html | 14 +- .../solution-fields.component.scss | 3 +- .../solution-fields.component.ts | 1 + 22 files changed, 980 insertions(+), 67 deletions(-) create mode 100644 docs/product/program-solutions-fe03.md create mode 100644 projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts create mode 100644 projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts create mode 100644 projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts create mode 100644 projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts create mode 100644 projects/social_platform/src/app/ui/routes/program/solution-leave.guard.ts diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index c7afab985..ce8134869 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -2,7 +2,9 @@ # C00 Контракт решения в программе -Версия 1.1.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны. Сохранение ответов и сдача остаются задачами BE05–BE06. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. +Версия 1.2.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов. Сдача остается задачей BE06. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. + +Изменение 1.2.0: общий POST `/files/` принимает необязательный query-параметр `preserve_original=true`. Он сохраняет исходные байты и формат изображения для материалов решения. Без параметра остается прежнее преобразование изображений в WebP. Multipart содержит только `file`, успешный ответ по-прежнему `{url}`. Сервер хранит фактический тип содержимого внутри UserFile и проверяет его при привязке к решению; новые публичные поля ответа не добавляются. Клиент с опцией оригинала внедряется после backend с ее поддержкой. Изменение 1.1.0: добавлен необязательный `permissions.can_leave`, вычисляемый сервером для собственного выхода принятого участника. При отсутствии поля клиент считает его false. Существующие поля, URL и payload команд не меняются; C00 1.0.0 остается совместимым подмножеством. Капитан, сданная заявка и выключенный режим дают false. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json index ecb794717..d88b4b22c 100644 --- a/docs/contracts/program-solutions-v1/examples.json +++ b/docs/contracts/program-solutions-v1/examples.json @@ -1,5 +1,5 @@ { - "contract_version": "1.1.0", + "contract_version": "1.2.0", "description": "Независимые примеры, не последовательность операций; фиктивные данные.", "scenarios": [ { diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json index 907543709..2241f2664 100644 --- a/docs/contracts/program-solutions-v1/openapi.json +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -3,7 +3,7 @@ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "PROCOLLAB Program Solutions C00", - "version": "1.1.0", + "version": "1.2.0", "description": "Контракт решений в программах для реализации и моков. Endpoints подключаются поэтапно по BE01–BE06. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ @@ -5795,7 +5795,19 @@ } } } - } + }, + "parameters": [ + { + "name": "preserve_original", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "description": "true сохраняет исходные байты и формат материалов решения без перекодирования изображений; без параметра общий upload работает как прежде." + } + ] } } }, diff --git a/docs/product/program-solutions-fe03.md b/docs/product/program-solutions-fe03.md new file mode 100644 index 000000000..e65277c70 --- /dev/null +++ b/docs/product/program-solutions-fe03.md @@ -0,0 +1,15 @@ + + +# FE03: редактор и просмотр решения + +Первая вкладка рендерит настроенную форму без проектных разделов, вторая сохраняет согласованный сценарий команды и приглашений. Scoped facade держит ввод на уровне заявки, поэтому переключение вкладок и обновление состава не сбрасывают ответы. Общее действие «Сохранить черновик» доступно из обеих вкладок. Незаполненные обязательные поля допустимы до сдачи; кейс не выбирается автоматически. Значения 0 и false отличаются от отсутствия ответа. + +PATCH отправляет обе актуальные ревизии и только измененные ответы; нет автоматического повторения записи. После version/form conflict выполняется GET: локальные изменения сохраняются по стабильным ID, неизмененные поля получают серверные данные. Удаленный или изменивший тип локальный ответ остается видимым и требует явного разрешения перед сохранением. При потере прав показывается серверный просмотр и доступная для копирования локальная версия. Переход к другой заявке отменяет старые запросы. Route guard и beforeunload защищают несохраненный ввод; переключение вкладок не вызывает подтверждения. + +Файлы загружаются multipart через общий /files/?preserve_original=true по совместимому C00 1.2.0. Клиент проверяет размер/расширение до upload, сервер повторяет проверку при привязке. Результат upload сначала локален и привязывается при сохранении черновика. Очистка снимает только ответ; исходный файл не удаляется. Ошибка upload не теряет остальные поля. + +Проверено тестами: 18 целевых проверок facade, wire и маршрутов; неполная форма, 0/false, актуальные версии, отсутствие авто-PATCH, конфликт, измененные поля, потеря права, поздний ответ старой заявки, ошибки файлов, вкладка команды и защита выхода. Общий test:ci прошел: 2544 теста в 465 файлах и строгие TypeScript-примеры C00. Development build, Stylelint измененных SCSS, Prettier 3.8.3 (1940 файлов) прошли. ESLint: 0 ошибок, 6 прежних предупреждений. + +Проверено в браузере: реальные Django/PostgreSQL/JWT, формы КЧ и хакатона, сохранение из вкладки команды, реальный конфликт двух вкладок с повторным явным сохранением, PDF и оригинальные PNG байт в байт, очистка без удаления бинарного файла, reload с 0/false и mobile 390 px. Ошибок JavaScript и горизонтального overflow нет. Внешний CDN заменен локальным адаптером только для проверки; upload и привязка выполнялись через реальные endpoints. Mont, segmented tabs и существующие controls сохранены. + +Представление сданного снимка и полный шаг подтверждения проходят интеграцию в BE06/FE06. Публикация, эксперт и управление еще не завершены; этот блок не означает готовность всей новой сущности. diff --git a/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts new file mode 100644 index 000000000..7fefb9197 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts @@ -0,0 +1,156 @@ +/** @format */ +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom, of, Subject } from "rxjs"; +import { ProgramSolution, SolutionApiError } from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { fail, ok, Result } from "@domain/shared/result.type"; +import { + UpdateSolutionUseCase, + UploadSolutionFileUseCase, +} from "../use-cases/solution-draft.use-cases"; +import { SolutionDraftFacade } from "./solution-draft.facade"; +const original = solutionContractScenarios.find(s => s.name === "created_solo")! + .response as ProgramSolution; +const source = () => structuredClone(original); +const conflict: SolutionApiError = { + code: "version_conflict", + detail: "Заявка изменилась", + errors: [], + meta: {}, +}; + +describe("Solution draft editing", () => { + const update = { execute: vi.fn() }, + files = { execute: vi.fn() }; + let draft: SolutionDraftFacade; + beforeEach(() => { + update.execute.mockReset(); + files.execute.mockReset(); + TestBed.configureTestingModule({ + providers: [ + SolutionDraftFacade, + { provide: UpdateSolutionUseCase, useValue: update }, + { provide: UploadSolutionFileUseCase, useValue: files }, + ], + }); + draft = TestBed.inject(SolutionDraftFacade); + draft.hydrate(source()); + }); + it("saves an incomplete draft with exact versions, zero and false without choosing a case", async () => { + draft.group().get("1004")!.setValue(0); + draft.group().get("1005")!.setValue(false); + expect(draft.group().get("1001")!.value).toBeNull(); + expect(draft.dirty()).toBe(true); + const saved = { + ...source(), + version: 2, + answers: [ + { fieldId: 1004, value: 0 }, + { fieldId: 1005, value: false }, + ], + }; + update.execute.mockReturnValue(of(ok(saved))); + expect((await firstValueFrom(draft.save())).ok).toBe(true); + expect(update.execute).toHaveBeenCalledWith(12, 301, { + expectedVersion: 1, + expectedFormRevision: original.form.revision, + name: original.name, + answers: saved.answers, + }); + expect(draft.dirty()).toBe(false); + expect(draft.attempted()).toBe(false); + }); + it("keeps local changes through a team refresh while incorporating untouched server answers", () => { + draft.name.setValue("Мое решение"); + draft.group().get("1002")!.setValue("Локальный ответ"); + const incoming = { ...source(), version: 2, answers: [{ fieldId: 1004, value: 12 }] }; + draft.hydrate(incoming); + expect(draft.name.value).toBe("Мое решение"); + expect(draft.group().get("1002")!.value).toBe("Локальный ответ"); + expect(draft.group().get("1004")!.value).toBe(12); + expect(draft.source()?.version).toBe(2); + expect(draft.hydrate(source())).toBe(false); + expect(draft.source()?.version).toBe(2); + }); + it("retains values on conflict, refreshes the base and only retries on explicit save", async () => { + draft.group().get("1002")!.setValue("Ответ"); + update.execute.mockReturnValueOnce(of(fail(conflict))); + expect((await firstValueFrom(draft.save())).ok).toBe(false); + draft.hydrate({ ...source(), version: 3 }); + draft.reconciled(); + expect(draft.group().get("1002")!.value).toBe("Ответ"); + expect(update.execute).toHaveBeenCalledTimes(1); + update.execute.mockReturnValue( + of(ok({ ...source(), version: 4, answers: [{ fieldId: 1002, value: "Ответ" }] })), + ); + await firstValueFrom(draft.save()); + expect(update.execute.mock.lastCall?.[2].expectedVersion).toBe(3); + }); + it("keeps removed or retyped local answers visible and blocks save until explicitly resolved", async () => { + draft.group().get("1002")!.setValue("Не терять"); + const next = source(); + next.form.fields = next.form.fields.filter(field => field.id !== 1002); + next.form.revision = "b".repeat(64); + draft.hydrate(next); + expect(draft.orphaned()).toEqual([ + { fieldId: 1002, label: "Краткое описание", value: "Не терять" }, + ]); + expect((await firstValueFrom(draft.save())).ok).toBe(false); + expect(update.execute).not.toHaveBeenCalled(); + draft.resolveLocalCopy(1002); + expect(draft.orphaned()).toEqual([]); + }); + it("does not display unsaved values as authoritative after losing edit permission", () => { + draft.group().get("1002")!.setValue("Локальное"); + const next = source(); + next.version = 2; + next.permissions.canEdit = false; + draft.hydrate(next); + expect(draft.canEdit()).toBe(false); + expect(draft.displayGroup().get("1002")!.value).toBeNull(); + expect(draft.group().get("1002")!.value).toBe("Локальное"); + expect(draft.dirty()).toBe(true); + }); + it("cancels an old save on route context change and never applies its late response", () => { + const response = new Subject>(); + update.execute.mockReturnValue(response); + draft.name.setValue("A"); + const completed = vi.fn(); + draft.save().subscribe({ complete: completed }); + expect(draft.busy()).toBe(true); + draft.hydrate({ ...source(), id: 999, programId: 13, name: "Другая заявка" }); + response.next(ok({ ...source(), name: "Старое" })); + expect(completed).toHaveBeenCalled(); + expect(draft.source()?.id).toBe(999); + expect(draft.name.value).toBe("Другая заявка"); + expect(draft.busy()).toBe(false); + }); + it("validates file constraints before upload and preserves other input on upload failure", () => { + draft.group().get("1002")!.setValue("Описание"); + draft.upload(1003, new File(["x"], "x.zip")); + expect(files.execute).not.toHaveBeenCalled(); + expect(draft.fieldErrors()[1003]).toContain("тип"); + files.execute.mockReturnValue( + of(fail({ ...conflict, code: "network", detail: "Нет соединения" })), + ); + draft.upload(1003, new File(["x"], "x.pdf")); + expect(draft.group().get("1002")!.value).toBe("Описание"); + expect(draft.fieldErrors()[1003]).toBe("Нет соединения"); + expect(draft.busy()).toBe(false); + }); + it("uploads then binds only on draft save; clearing an answer never deletes the binary", async () => { + files.execute.mockReturnValue(of(ok({ url: "https://cdn.test/file.pdf" }))); + draft.upload(1003, new File(["%PDF-"], "file.PDF")); + expect(update.execute).not.toHaveBeenCalled(); + expect(draft.group().get("1003")!.value).toEqual({ url: "https://cdn.test/file.pdf" }); + draft.hydrate( + { ...source(), answers: [{ fieldId: 1003, value: { url: "https://cdn.test/file.pdf" } }] }, + true, + ); + draft.clearFile(1003); + update.execute.mockReturnValue(of(ok({ ...source(), version: 2 }))); + await firstValueFrom(draft.save()); + expect(update.execute.mock.lastCall?.[2].answers).toEqual([{ fieldId: 1003, value: null }]); + expect(files.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts new file mode 100644 index 000000000..a267ec2cd --- /dev/null +++ b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts @@ -0,0 +1,305 @@ +/** @format */ + +import { DestroyRef, Injectable, computed, inject, signal } from "@angular/core"; +import { FormControl, Validators } from "@angular/forms"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { finalize, of, Subject, takeUntil, tap } from "rxjs"; +import { + ProgramSolution, + SolutionApiError, + UpdateSolutionCommand, +} from "@domain/solution/solution-contract.model"; +import { fail } from "@domain/shared/result.type"; +import { hasSolutionAnswer } from "@domain/solution/solution-values"; +import { solutionFieldsGroup, SolutionControlValue } from "@utils/solution-field-controls"; +import { + UpdateSolutionUseCase, + UploadSolutionFileUseCase, +} from "../use-cases/solution-draft.use-cases"; + +export type LocalSolutionAnswer = { fieldId: number; label: string; value: SolutionControlValue }; +const equal = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b); + +@Injectable() +export class SolutionDraftFacade { + private readonly update = inject(UpdateSolutionUseCase); + private readonly files = inject(UploadSolutionFileUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly contextChanged = new Subject(); + private readonly controlsChanged = new Subject(); + private key = ""; + private readonly tick = signal(0); + readonly source = signal(null); + readonly group = signal(solutionFieldsGroup([])); + readonly name = new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.maxLength(256)], + }); + readonly saving = signal(false); + readonly uploading = signal(null); + readonly busy = computed(() => this.saving() || this.uploading() !== null); + readonly attempted = signal(false); + readonly fieldErrors = signal>({}); + readonly nameError = signal(""); + readonly error = signal(null); + readonly notice = signal(""); + readonly orphaned = signal([]); + readonly canEdit = computed(() => this.source()?.permissions.canEdit === true); + readonly displayGroup = computed(() => + this.canEdit() + ? this.group() + : solutionFieldsGroup(this.source()?.form.fields ?? [], this.source()?.answers ?? []), + ); + readonly dirty = computed(() => { + this.tick(); + const source = this.source(); + if (!source) return false; + return ( + this.name.value !== source.name || + this.orphaned().length > 0 || + source.form.fields.some( + field => + !equal( + this.group().get(String(field.id))?.value ?? null, + this.serverValue(source, field.id), + ), + ) + ); + }); + readonly localChanges = computed(() => { + this.tick(); + const source = this.source(); + if (!source) return []; + return source.form.fields.flatMap(field => { + const value = this.group().get(String(field.id))?.value ?? null; + return equal(value, this.serverValue(source, field.id)) + ? [] + : [{ fieldId: field.id, label: field.label, value }]; + }); + }); + constructor() { + this.name.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.tick.update(value => value + 1); + this.nameError.set(""); + }); + } + private serverValue(source: ProgramSolution, fieldId: number): SolutionControlValue { + return source.answers.find(answer => answer.fieldId === fieldId)?.value ?? null; + } + clear() { + this.contextChanged.next(); + this.controlsChanged.next(); + this.key = ""; + this.source.set(null); + this.group.set(solutionFieldsGroup([])); + this.name.reset("", { emitEvent: false }); + this.orphaned.set([]); + this.error.set(null); + this.fieldErrors.set({}); + this.nameError.set(""); + this.notice.set(""); + this.attempted.set(false); + this.tick.update(value => value + 1); + } + hydrate(incoming: ProgramSolution, saved = false): boolean { + const previous = this.source(), + key = incoming.programId + ":" + incoming.id; + if (previous && this.key === key && incoming.version < previous.version) return false; + if (!saved && incoming === previous) return true; + if (this.key !== key) { + this.clear(); + this.key = key; + } + this.controlsChanged.next(); + const preserve = + !saved && previous !== null && this.key === previous.programId + ":" + previous.id; + const group = solutionFieldsGroup(incoming.form.fields, incoming.answers); + const orphans = new Map((preserve ? this.orphaned() : []).map(item => [item.fieldId, item])); + if (preserve) { + for (const oldField of previous.form.fields) { + const control = this.group().get(String(oldField.id)); + if (!control) continue; + if (equal(control.value, this.serverValue(previous, oldField.id))) continue; + const nextField = incoming.form.fields.find(field => field.id === oldField.id); + if (!nextField || nextField.type !== oldField.type) { + if (hasSolutionAnswer(control.value)) + orphans.set(oldField.id, { + fieldId: oldField.id, + label: oldField.label, + value: control.value, + }); + continue; + } + const next = group.get(String(oldField.id))!; + next.setValue(control.value, { emitEvent: false }); + next.markAsDirty(); + if (control.touched) next.markAsTouched(); + } + } + if (!preserve || this.name.value === previous.name) + this.name.reset(incoming.name, { emitEvent: false }); + this.source.set(incoming); + this.group.set(group); + this.orphaned.set([...orphans.values()]); + for (const field of incoming.form.fields) { + group + .get(String(field.id))! + .valueChanges.pipe(takeUntil(this.controlsChanged), takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.fieldErrors.update(errors => { + const next = { ...errors }; + delete next[field.id]; + return next; + }); + this.tick.update(value => value + 1); + }); + } + if (saved) { + this.error.set(null); + this.fieldErrors.set({}); + this.nameError.set(""); + this.attempted.set(false); + this.notice.set("Черновик сохранен."); + } + this.tick.update(value => value + 1); + return true; + } + discard() { + const source = this.source(); + if (source) this.hydrate(source, true); + this.notice.set(""); + } + resolveLocalCopy(fieldId: number) { + this.orphaned.update(items => items.filter(item => item.fieldId !== fieldId)); + } + reconciled() { + if (!this.dirty()) this.error.set(null); + this.notice.set( + this.dirty() + ? "Данные обновлены. Ваши правки сохранены локально; проверьте их и повторите действие." + : "Данные обновлены.", + ); + } + private localError(code: string, detail: string): SolutionApiError { + return { code, detail, errors: [], meta: {} }; + } + private report(error: SolutionApiError) { + this.error.set(error); + this.nameError.set(""); + const fields: Record = {}; + for (const issue of error.errors) { + if (issue.fieldId !== null) fields[issue.fieldId] = issue.detail; + else if (issue.path === "name") this.nameError.set(issue.detail); + } + this.fieldErrors.set(fields); + } + save() { + const source = this.source(); + this.attempted.set(true); + this.name.markAsTouched(); + this.group().markAllAsTouched(); + this.notice.set(""); + if (!source || !this.canEdit() || this.busy()) + return of(fail(this.localError("permission_denied", "Сохранение сейчас недоступно."))); + if (this.name.invalid || this.group().invalid || this.orphaned().length) { + const error = this.localError( + "invalid_answers", + "Проверьте поля решения и несохраненные ответы измененной формы.", + ); + this.report(error); + if (this.name.invalid) this.nameError.set("Укажите название не длиннее 256 символов."); + return of(fail(error)); + } + const command: UpdateSolutionCommand = { + expectedVersion: source.version, + expectedFormRevision: source.form.revision, + name: this.name.value, + answers: source.form.fields + .filter( + field => + !equal(this.group().get(String(field.id))!.value, this.serverValue(source, field.id)), + ) + .map(field => ({ fieldId: field.id, value: this.group().get(String(field.id))!.value })), + }; + const key = this.key; + this.saving.set(true); + this.error.set(null); + return this.update.execute(source.programId, source.id, command).pipe( + takeUntil(this.contextChanged), + takeUntilDestroyed(this.destroyRef), + tap(result => { + if (key !== this.key) return; + if (result.ok) this.hydrate(result.value, true); + else this.report(result.error); + }), + finalize(() => this.saving.set(false)), + ); + } + clearFile(fieldId: number) { + if (!this.canEdit() || this.busy()) return; + const control = this.group().get(String(fieldId)); + control?.setValue(null); + control?.markAsDirty(); + this.tick.update(value => value + 1); + } + upload(fieldId: number, file: File) { + const source = this.source(), + field = source?.form.fields.find(field => field.id === fieldId); + if (!source || !field || field.type !== "file" || !this.canEdit() || this.busy()) return; + const maximum = Math.min(field.validation.maxFileSizeBytes ?? 104857600, 104857600); + const extension = file.name.includes(".") ? file.name.split(".").pop()!.toLowerCase() : ""; + let detail = ""; + if (file.size > maximum) detail = "Файл превышает допустимый размер."; + else if ( + field.validation.allowedExtensions.length && + !field.validation.allowedExtensions.includes(extension) + ) + detail = "Выберите разрешенный тип файла."; + if (detail) { + this.fieldErrors.update(errors => ({ ...errors, [fieldId]: detail })); + return; + } + this.uploading.set(fieldId); + this.error.set(null); + this.notice.set(""); + const key = this.key; + this.files + .execute(file) + .pipe( + takeUntil(this.contextChanged), + takeUntilDestroyed(this.destroyRef), + finalize(() => this.uploading.set(null)), + ) + .subscribe(result => { + if (key !== this.key) return; + if (!result.ok) { + this.fieldErrors.update(errors => ({ ...errors, [fieldId]: result.error.detail })); + return; + } + const current = this.source()?.form.fields.find(next => next.id === fieldId); + if (!this.canEdit() || !current || current.type !== "file") { + this.orphaned.update(items => [ + ...items.filter(item => item.fieldId !== fieldId), + { fieldId, label: field.label, value: result.value }, + ]); + this.error.set( + this.localError( + "form_revision_conflict", + "Форма или права изменились. Загруженный файл сохранен в локальном ответе.", + ), + ); + return; + } + const control = this.group().get(String(fieldId))!; + control.setValue(result.value); + control.markAsDirty(); + this.tick.update(value => value + 1); + this.notice.set("Файл загружен. Сохраните черновик, чтобы добавить его в решение."); + }); + } + valueText(value: SolutionControlValue): string { + if (value === null) return "Ответ очищен"; + if (typeof value === "boolean") return value ? "Да" : "Нет"; + return typeof value === "object" ? value.url : String(value); + } +} diff --git a/projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts new file mode 100644 index 000000000..e398dec5d --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts @@ -0,0 +1,92 @@ +/** @format */ +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom } from "rxjs"; +import snakecaseKeys from "snakecase-keys"; +import { CamelcaseInterceptor } from "@core/lib/interceptors/camelcase.interceptor"; +import { API_URL } from "@core/lib/providers"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { SolutionRepository } from "@infrastructure/repository/solution/solution.repository"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { ProgramSolution } from "@domain/solution/solution-contract.model"; +import { + UpdateSolutionUseCase, + UploadSolutionFileUseCase, +} from "./use-cases/solution-draft.use-cases"; + +describe("C00 draft and original upload wire", () => { + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: API_URL, useValue: "https://local.test" }, + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: LoggerService, useValue: { warn: vi.fn() } }, + { provide: SolutionRepositoryPort, useExisting: SolutionRepository }, + ], + }), + ); + afterEach(() => TestBed.inject(HttpTestingController).verify()); + it("PATCH uses both expected versions and preserves null, zero, false and exact option strings", async () => { + const command = { + expectedVersion: 2, + expectedFormRevision: "a".repeat(64), + name: "Решение", + answers: [ + { fieldId: 1, value: null }, + { fieldId: 2, value: 0 }, + { fieldId: 3, value: false }, + { fieldId: 4, value: "API_v2" }, + ], + }; + const result = firstValueFrom(TestBed.inject(UpdateSolutionUseCase).execute(12, 301, command)); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/301/", + ); + expect(request.request.method).toBe("PATCH"); + expect(request.request.body).toEqual(snakecaseKeys(command, { deep: true })); + const expected = solutionContractScenarios.find(s => s.name === "created_solo")! + .response as ProgramSolution; + request.flush(snakecaseKeys(expected, { deep: true })); + expect(await result).toEqual({ ok: true, value: expected }); + }); + it("posts a single multipart file with optional preserve_original and the old url response", async () => { + const file = new File(["image"], "image.png", { type: "image/png" }); + const result = firstValueFrom(TestBed.inject(UploadSolutionFileUseCase).execute(file)); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/files/?preserve_original=true", + ); + expect(request.request.method).toBe("POST"); + expect(request.request.body).toBeInstanceOf(FormData); + expect([...request.request.body.keys()]).toEqual(["file"]); + expect(request.request.body.get("file")).toMatchObject({ + name: file.name, + size: file.size, + type: file.type, + }); + request.flush({ url: "https://cdn.test/original.png" }); + expect(await result).toEqual({ ok: true, value: { url: "https://cdn.test/original.png" } }); + }); + it("does not automatically retry a failed write", async () => { + const result = firstValueFrom( + TestBed.inject(UpdateSolutionUseCase).execute(12, 301, { + expectedVersion: 1, + expectedFormRevision: "a".repeat(64), + answers: [], + }), + ); + TestBed.inject(HttpTestingController) + .expectOne("https://local.test/programs/12/solutions/301/") + .flush( + { code: "version_conflict", detail: "Обновите заявку", errors: [], meta: {} }, + { status: 409, statusText: "Conflict" }, + ); + expect((await result).ok).toBe(false); + TestBed.inject(HttpTestingController).expectNone( + "https://local.test/programs/12/solutions/301/", + ); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts new file mode 100644 index 000000000..7b46b9301 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts @@ -0,0 +1,21 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { UpdateSolutionCommand } from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class UpdateSolutionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: UpdateSolutionCommand) { + return solutionRequestResult(() => this.repository.update(programId, solutionId, command)); + } +} +@Injectable({ providedIn: "root" }) +export class UploadSolutionFileUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(file: File) { + return solutionRequestResult(() => this.repository.upload(file)); + } +} diff --git a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts index caeb1ccd8..ce140557a 100644 --- a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts +++ b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts @@ -19,9 +19,17 @@ import { SolutionFormWrite, SolutionFormManagement, SolutionTemplates, + UpdateSolutionCommand, + SolutionUploadResponse, } from "../solution-contract.model"; export abstract class SolutionRepositoryPort { + abstract update( + programId: number, + solutionId: number, + command: UpdateSolutionCommand, + ): Observable; + abstract upload(file: File): Observable; abstract form(programId: number): Observable; abstract managedForm(programId: number): Observable; abstract saveManagedForm( diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts index 533a7d36f..6fb32a41a 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts @@ -1,6 +1,6 @@ /** @format */ -// Сгенерировано tools/generate-solution-contract.py из C00 1.1.0. +// Сгенерировано tools/generate-solution-contract.py из C00 1.2.0. // camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются. export type SolutionUser = { diff --git a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts index 222da6033..5e7612c24 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts @@ -21,6 +21,8 @@ import { SolutionFormWrite, SolutionFormManagement, SolutionTemplates, + UpdateSolutionCommand, + SolutionUploadResponse, } from "@domain/solution/solution-contract.model"; import { SolutionListQuery, @@ -31,6 +33,14 @@ import { @Injectable({ providedIn: "root" }) export class SolutionHttpAdapter { private readonly api = inject(ApiService); + update(programId: number, solutionId: number, command: UpdateSolutionCommand) { + return this.api.patch(this.solutionPath(programId, solutionId), command); + } + upload(file: File) { + const body = new FormData(); + body.append("file", file, file.name); + return this.api.post("/files/?preserve_original=true", body); + } private path(programId: number): string { if (!validSolutionId(programId)) throw new RangeError("Invalid program ID"); diff --git a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts index cf9afeb7b..858af3fd4 100644 --- a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts @@ -13,12 +13,19 @@ import { SolutionCreateInvitation, SolutionUpdateInvitation, SolutionFormWrite, + UpdateSolutionCommand, } from "@domain/solution/solution-contract.model"; import { SolutionHttpAdapter } from "../../adapters/solution/solution-http.adapter"; @Injectable({ providedIn: "root" }) export class SolutionRepository implements SolutionRepositoryPort { private readonly http = inject(SolutionHttpAdapter); + update(programId: number, solutionId: number, command: UpdateSolutionCommand) { + return this.http.update(programId, solutionId, command); + } + upload(file: File) { + return this.http.upload(file); + } form(programId: number) { return this.http.form(programId); } diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts index 9a3d03b2c..ef5327237 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts @@ -241,7 +241,11 @@ export class ManagementSolutionFormComponent { .subscribe(result => { if (id !== this.context) return; if (!result.ok) { - if (["forbidden", "not_found", "authentication_required"].includes(result.error.code)) { + if ( + ["forbidden", "permission_denied", "not_found", "authentication_required"].includes( + result.error.code, + ) + ) { this.snapshot.set(null); this.fields.set([]); this.editing.set(null); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html index 3d019e9a4..8748d723a 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html @@ -41,24 +41,128 @@

{{ item.name }}

} } @else { -
+

Данные решения

-
- @for (field of item.form.fields; track field.id) { -
-
{{ field.label }}{{ field.required ? " *" : "" }}
-
{{ answer(field.id) }}
-
- } @empty { -

Организатор еще не настроил поля решения.

- } -
+ @if (draft.canEdit()) { +

+ Заполняйте решение постепенно. Обязательные поля понадобятся при сдаче. +

+
+ + + @if (draft.nameError()) { + + } +
+ } @else { +

+ {{ + item.submission.status === "submitted" + ? "Решение сдано. Данные и состав команды зафиксированы." + : "Редактировать решение может капитан команды." + }} +

+ } + + @if (!item.form.fields.length) { +

Организатор еще не настроил поля решения.

+ } + @if (!draft.canEdit() && draft.dirty()) { + + } + @if (draft.orphaned().length) { + + }
} +
+ @if (teamRefreshError() && !teamTab()) { +

{{ teamRefreshError() }}

+ + } + @if (draft.error(); as problem) { + + } + @if (draft.notice()) { +

{{ draft.notice() }}

+ } + @if (draft.uploading() !== null) { +

Загружаем файл…

+ } + @if (draft.dirty() && !draft.canEdit()) { +

+ Права изменились. Ваши несохраненные правки остались в этой вкладке браузера. +

+ } +
+ @if (draft.canEdit()) { + + } + @if (draft.dirty()) { + + } +
+
} diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss index 185baf05a..3fd2cdffc 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss @@ -81,42 +81,43 @@ nav a:focus-visible { outline-offset: 2px; } -dl { - margin: 0; +.solution-name { + display: grid; + gap: 8px; + margin: 24px 0; } -dl > div { - padding: 16px 0; - border-bottom: 1px solid var(--medium-grey-for-outline); +.solution-name .ui-field { + @include ui-foundation.control; + + width: 100%; } -dt { - margin-bottom: 8px; +.error { + color: var(--error, var(--ui-error-text)); } -dd { - margin: 0; - color: var(--grey-for-text); - overflow-wrap: anywhere; - white-space: pre-wrap; +.local-answers { + padding: 20px 0; + border-top: 1px solid var(--medium-grey-for-outline); } -.members { - padding: 0; - list-style: none; +.local-answers p { + overflow-wrap: anywhere; + white-space: pre-wrap; } -.members li { - display: grid; - grid-template-columns: 1.5fr 1fr 1fr; - gap: 24px; - padding: 20px 0; - border-bottom: 1px solid var(--medium-grey-for-outline); +.draft-actions { + padding-top: 20px; + margin-top: 24px; + border-top: 1px solid var(--medium-grey-for-outline); } -.name small { - display: block; - margin-top: 6px; +.draft-actions__buttons { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; } .ui-button { @@ -128,8 +129,8 @@ dd { flex-direction: column; } - .members li { - grid-template-columns: 1fr; - gap: 8px; + .draft-actions__buttons { + flex-direction: column; + align-items: stretch; } } diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts index 72fbccf34..c1aaa339d 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts @@ -16,6 +16,8 @@ import { ProgramSolution, SolutionContext } from "@domain/solution/solution-cont import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; import { fail, ok } from "@domain/shared/result.type"; import { ProgramSolutionComponent } from "./solution.component"; +import { UpdateSolutionUseCase } from "@api/solution/use-cases/solution-draft.use-cases"; +import { canLeaveSolution } from "@ui/routes/program/solution-leave.guard"; const solution = solutionContractScenarios.find(s => s.name === "created_solo")! .response as ProgramSolution; @@ -35,10 +37,12 @@ describe("Solution route foundation", () => { const get = { execute: vi.fn() }; const create = { execute: vi.fn() }; const getContext = { execute: vi.fn() }; + const update = { execute: vi.fn() }; beforeEach(() => { get.execute.mockReset().mockReturnValue(of(ok(solution))); create.execute.mockReset().mockReturnValue(of(ok(solution))); getContext.execute.mockReset().mockReturnValue(of(ok(context))); + update.execute.mockReset(); TestBed.configureTestingModule({ providers: [ provideRouter([ @@ -46,14 +50,23 @@ describe("Solution route foundation", () => { path: "office/program/:programId", children: [ { path: "solutions/new", component: ProgramSolutionComponent }, - { path: "solutions/:solutionId/edit", component: ProgramSolutionComponent }, - { path: "solutions/:solutionId", component: ProgramSolutionComponent }, + { + path: "solutions/:solutionId/edit", + component: ProgramSolutionComponent, + canDeactivate: [canLeaveSolution], + }, + { + path: "solutions/:solutionId", + component: ProgramSolutionComponent, + canDeactivate: [canLeaveSolution], + }, ], }, ]), { provide: GetSolutionUseCase, useValue: get }, { provide: CreateSolutionUseCase, useValue: create }, { provide: GetSolutionContextUseCase, useValue: getContext }, + { provide: UpdateSolutionUseCase, useValue: update }, { provide: ProfileInfoService, useValue: { profile: signal({ id: 7 }) } }, { provide: SolutionRepositoryPort, @@ -145,4 +158,76 @@ describe("Solution route foundation", () => { expect(get.execute).toHaveBeenLastCalledWith(13, 99); expect(cancelled).toBe(true); }); + + it("keeps input across tabs and a team refresh, then saves from the Team tab with the fresh version", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88/edit", + ProgramSolutionComponent, + ); + component.draft.group().get("1002")!.setValue("Ответ из вкладки решения"); + const confirmation = vi.spyOn(window, "confirm").mockReturnValue(false); + await TestBed.inject(Router).navigateByUrl("/office/program/12/solutions/88/edit?tab=team"); + get.execute.mockReturnValue(of(ok({ ...solution, version: 2 }))); + component.refreshTeam(); + harness.detectChanges(); + expect(confirmation).not.toHaveBeenCalled(); + expect(component.draft.group().get("1002")!.value).toBe("Ответ из вкладки решения"); + const saved = { + ...solution, + version: 3, + answers: [{ fieldId: 1002, value: "Ответ из вкладки решения" }], + }; + update.execute.mockReturnValue(of(ok(saved))); + component.saveDraft(); + harness.detectChanges(); + expect(update.execute.mock.lastCall?.[2].expectedVersion).toBe(2); + expect(component.draft.dirty()).toBe(false); + expect(harness.routeNativeElement?.textContent).toContain("Черновик сохранен"); + confirmation.mockRestore(); + }); + + it("refreshes a conflict without automatically saving again or losing the edited answer", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + component.draft.group().get("1002")!.setValue("Несохраненное"); + update.execute.mockReturnValue( + of( + fail({ code: "form_revision_conflict", detail: "Форма изменилась", errors: [], meta: {} }), + ), + ); + get.execute.mockReturnValue( + of(ok({ ...solution, version: 2, form: { ...solution.form, revision: "b".repeat(64) } })), + ); + component.saveDraft(); + await harness.fixture.whenStable(); + harness.detectChanges(); + expect(update.execute).toHaveBeenCalledTimes(1); + expect(component.draft.group().get("1002")!.value).toBe("Несохраненное"); + expect(component.draft.source()?.form.revision).toBe("b".repeat(64)); + expect(component.draft.dirty()).toBe(true); + }); + + it("blocks leaving or reload with unsaved input, while an explicit discard restores server data", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + component.draft.name.setValue("Местное имя"); + const confirmation = vi.spyOn(window, "confirm").mockReturnValue(false); + await TestBed.inject(Router).navigateByUrl("/office/program/13/solutions/99"); + expect(TestBed.inject(Router).url).toBe("/office/program/12/solutions/88"); + const event = { preventDefault: vi.fn(), returnValue: undefined }; + component.onBeforeUnload(event as unknown as BeforeUnloadEvent); + expect(event.preventDefault).toHaveBeenCalled(); + confirmation.mockReturnValue(true); + component.discardDraft(); + expect(component.draft.name.value).toBe(solution.name); + expect(component.draft.dirty()).toBe(false); + confirmation.mockRestore(); + }); }); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts index 73e9de1ec..09da03236 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts @@ -1,6 +1,7 @@ /** @format */ -import { ChangeDetectionStrategy, Component, inject, signal } from "@angular/core"; +import { ChangeDetectionStrategy, Component, HostListener, inject, signal } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { combineLatest, EMPTY, finalize, map, startWith, Subject, switchMap } from "rxjs"; @@ -13,15 +14,25 @@ import { solutionApplicationRoute } from "@api/solution/solution-navigation"; import { AppRoutes } from "@api/paths/app-routes"; import { ProgramSolution, SolutionApiError } from "@domain/solution/solution-contract.model"; import { validSolutionId } from "@domain/solution/ports/solution.repository.port"; -import { ButtonDirective } from "@uilib"; +import { ButtonDirective, FieldDirective } from "@uilib"; import { DatePipe } from "@angular/common"; import { SolutionTeamComponent } from "./solution-team.component"; +import { SolutionDraftFacade } from "@api/solution/facades/solution-draft.facade"; +import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fields.component"; -/** FE01: read surface and route foundation. Editing/team commands follow in FE03–FE06. */ @Component({ selector: "app-program-solution", standalone: true, - imports: [RouterLink, ButtonDirective, DatePipe, SolutionTeamComponent], + imports: [ + RouterLink, + ButtonDirective, + FieldDirective, + ReactiveFormsModule, + DatePipe, + SolutionTeamComponent, + SolutionFieldsComponent, + ], + providers: [SolutionDraftFacade], templateUrl: "./solution.component.html", styleUrl: "./solution.component.scss", changeDetection: ChangeDetectionStrategy.OnPush, @@ -32,6 +43,7 @@ export class ProgramSolutionComponent { private readonly context = inject(GetSolutionContextUseCase); private readonly create = inject(CreateSolutionUseCase); private readonly get = inject(GetSolutionUseCase); + readonly draft = inject(SolutionDraftFacade); readonly solution = signal(null); readonly loading = signal(true); readonly error = signal(null); @@ -42,6 +54,61 @@ export class ProgramSolutionComponent { readonly routes = AppRoutes; private readonly retry = new Subject(); private readonly teamRefresh = new Subject<{ programId: number; solutionId: number } | null>(); + private reconcileDraft = false; + + canLeave(): boolean { + return ( + !(this.draft.dirty() || this.draft.busy()) || + confirm("В заявке есть несохраненные изменения. Покинуть страницу?") + ); + } + + @HostListener("window:beforeunload", ["$event"]) + onBeforeUnload(event: BeforeUnloadEvent): void { + if (this.draft.dirty() || this.draft.busy()) { + event.preventDefault(); + event.returnValue = ""; + } + } + + saveDraft(): void { + if (this.refreshingTeam() || this.teamRefreshError()) return; + this.draft.save().subscribe(result => { + if (result.ok) this.solution.set(result.value); + else { + void this.router.navigate([], { relativeTo: this.route, queryParams: {} }); + if ( + [ + "version_conflict", + "form_revision_conflict", + "not_solution_captain", + "team_frozen", + "solution_mode_unavailable", + ].includes(result.error.code) + ) { + this.reconcileDraft = true; + this.refreshTeam(); + } else if ( + [ + "not_found", + "forbidden", + "permission_denied", + "unauthorized", + "authentication_required", + ].includes(result.error.code) + ) { + this.draft.clear(); + this.solution.set(null); + this.showError(result.error); + } + } + }); + } + + discardDraft(): void { + if (!this.draft.busy() && confirm("Отменить несохраненные изменения решения?")) + this.draft.discard(); + } reload(): void { this.retry.next(); @@ -74,10 +141,23 @@ export class ProgramSolutionComponent { if (this.programId() !== context.programId || this.solution()?.id !== context.solutionId) return; if (result.ok) { - this.solution.set(result.value); + if (this.draft.hydrate(result.value)) this.solution.set(result.value); + if (this.reconcileDraft) { + this.reconcileDraft = false; + this.draft.reconciled(); + } return; } - if (["not_found", "forbidden", "authentication_required"].includes(result.error.code)) { + if ( + [ + "not_found", + "forbidden", + "permission_denied", + "unauthorized", + "authentication_required", + ].includes(result.error.code) + ) { + this.draft.clear(); this.solution.set(null); this.showError(result.error); } else this.teamRefreshError.set(result.error.detail); @@ -98,6 +178,8 @@ export class ProgramSolutionComponent { this.teamRefresh.next(null); this.refreshingTeam.set(false); this.teamRefreshError.set(""); + this.reconcileDraft = false; + this.draft.clear(); this.solution.set(null); this.error.set(null); this.loading.set(true); @@ -156,6 +238,7 @@ export class ProgramSolutionComponent { return; } this.solution.set(result.value); + this.draft.hydrate(result.value); if (!this.route.snapshot.paramMap.has("solutionId")) { void this.router.navigate( [AppRoutes.program.solution(this.programId(), result.value.id)], @@ -169,12 +252,4 @@ export class ProgramSolutionComponent { this.error.set(error); this.loading.set(false); } - - answer(fieldId: number): string { - const value = this.solution()?.answers.find(answer => answer.fieldId === fieldId)?.value; - if (value === null || value === undefined || value === "") return "Не заполнено"; - if (typeof value === "boolean") return value ? "Да" : "Нет"; - // File metadata is displayed as text until the validated download flow in FE03. - return typeof value === "object" ? "Файл прикреплен" : String(value); - } } diff --git a/projects/social_platform/src/app/ui/routes/program/detail.routes.ts b/projects/social_platform/src/app/ui/routes/program/detail.routes.ts index 2bd0a0e76..f3b8daab8 100644 --- a/projects/social_platform/src/app/ui/routes/program/detail.routes.ts +++ b/projects/social_platform/src/app/ui/routes/program/detail.routes.ts @@ -11,21 +11,25 @@ import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/p import { ProgramAnalyticsComponent } from "@ui/pages/program/detail/analytics/analytics.component"; import { ProgramManagementComponent } from "@ui/pages/program/management/management.component"; import { canLeaveManagement } from "./management-leave.guard"; +import { canLeaveSolution } from "./solution-leave.guard"; /** Маршруты детальной страницы программы: информация, проекты и участники. */ export const PROGRAM_DETAIL_ROUTES: Routes = [ { path: "solutions/new", + canDeactivate: [canLeaveSolution], loadComponent: () => import("@ui/pages/program/solution/solution.component").then(m => m.ProgramSolutionComponent), }, { path: "solutions/:solutionId/edit", + canDeactivate: [canLeaveSolution], loadComponent: () => import("@ui/pages/program/solution/solution.component").then(m => m.ProgramSolutionComponent), }, { path: "solutions/:solutionId", + canDeactivate: [canLeaveSolution], loadComponent: () => import("@ui/pages/program/solution/solution.component").then(m => m.ProgramSolutionComponent), }, diff --git a/projects/social_platform/src/app/ui/routes/program/solution-leave.guard.ts b/projects/social_platform/src/app/ui/routes/program/solution-leave.guard.ts new file mode 100644 index 000000000..21227acec --- /dev/null +++ b/projects/social_platform/src/app/ui/routes/program/solution-leave.guard.ts @@ -0,0 +1,6 @@ +/** @format */ + +/** The router can cancel activation before creating the component. */ +export function canLeaveSolution(component: { canLeave(): boolean } | null) { + return component?.canLeave() ?? true; +} diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html index 53c55fca3..87bf9ae16 100644 --- a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html @@ -59,10 +59,12 @@ } @case ("file") { @if (fileLink(field); as link) { - Открыть файл +
+ Открыть файл +
} {{ field.helpText }}

@if (errors()[field.id]) { - } @else if (!readonly() && attempted() && field.required && !hasAnswer(value(field))) { + } @else if ( + !readonly() && attempted() && requireAnswers() && field.required && !hasAnswer(value(field)) + ) { } @else if ( !readonly() && control(field)?.invalid && (attempted() || control(field)?.touched) diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss index 75509c0bd..74eb5ec18 100644 --- a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss @@ -38,7 +38,8 @@ textarea.ui-field { border-radius: 12px; } -.file-upload { +.file-upload, +.file-current { display: flex; flex-wrap: wrap; gap: 12px; diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts index 064171be7..8b4008906 100644 --- a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts @@ -20,6 +20,7 @@ export class SolutionFieldsComponent { readonly preview = input(false); readonly busy = input(false); readonly attempted = input(false); + readonly requireAnswers = input(false); readonly errors = input>({}); readonly upload = output<{ fieldId: number; file: File }>(); readonly clearFile = output(); From 0555399a0e8d219a8fbd36189ba1f916b6ec7b52 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 22:22:15 +0300 Subject: [PATCH 04/47] feat(programs): confirm and submit solution snapshots --- docs/contracts/program-solutions-v1/README.md | 4 +- docs/product/program-solutions-fe06.md | 13 ++ .../facades/solution-draft.facade.spec.ts | 19 +- .../solution/facades/solution-draft.facade.ts | 40 +++- .../solution/solution-draft-contract.spec.ts | 17 ++ .../use-cases/solution-draft.use-cases.ts | 13 +- .../ports/solution.repository.port.ts | 6 + .../solution/solution-http.adapter.ts | 7 + .../solution/solution.repository.ts | 4 + .../solution/solution-team.component.html | 2 +- .../solution/solution-team.component.ts | 4 +- .../program/solution/solution.component.html | 91 +++++++- .../program/solution/solution.component.scss | 22 ++ .../solution/solution.component.spec.ts | 159 +++++++++++++- .../program/solution/solution.component.ts | 207 +++++++++++++++--- .../project-invite-dialog.component.html | 2 +- .../project-invite-dialog.component.ts | 1 + 17 files changed, 566 insertions(+), 45 deletions(-) create mode 100644 docs/product/program-solutions-fe06.md diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index ce8134869..ef201bbf0 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -2,7 +2,7 @@ # C00 Контракт решения в программе -Версия 1.2.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов. Сдача остается задачей BE06. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. +Версия 1.2.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. Изменение 1.2.0: общий POST `/files/` принимает необязательный query-параметр `preserve_original=true`. Он сохраняет исходные байты и формат изображения для материалов решения. Без параметра остается прежнее преобразование изображений в WebP. Multipart содержит только `file`, успешный ответ по-прежнему `{url}`. Сервер хранит фактический тип содержимого внутри UserFile и проверяет его при привязке к решению; новые публичные поля ответа не добавляются. Клиент с опцией оригинала внедряется после backend с ее поддержкой. @@ -224,7 +224,7 @@ PATCH и submit — две отдельные транзакции. Если sub | 409 | solution_mode_unavailable, form_not_configured, version_conflict, form_revision_conflict, already_in_program_team, ambiguous_application | | 409 | team_frozen, team_min_size, team_max_size, already_solution_member, duplicate_pending_invite, invite_already_processed | | 409 | captain_cannot_leave, captain_cannot_be_removed, captain_already_selected, not_solution_member | -| 409 | submission_closed, already_submitted, not_competitive, field_in_use, template_locked | +| 409 | submission_closed, already_submitted, not_competitive, field_in_use, template_locked, file_in_use | Это HTTP контракты новых endpoints. Существующие статусы старых /projects/, /invites/ и submit связи не меняются. Повторное действие с удаленным приглашением дает 404. Повтор accept/decline сохраненной обработанной строки — 409 invite_already_processed. diff --git a/docs/product/program-solutions-fe06.md b/docs/product/program-solutions-fe06.md new file mode 100644 index 000000000..2239b98e2 --- /dev/null +++ b/docs/product/program-solutions-fe06.md @@ -0,0 +1,13 @@ + + +# FE06: готовность и подтверждение сдачи + +Общее действие «Сдать решение» доступно из обеих вкладок по серверному permission и сроку. Сначала PATCH сохраняет ответы с обеими ревизиями. Readiness ответа дает список причин по полям и составу; ссылка открывает нужную вкладку и фокусирует поле/выбор файла или заголовок команды. Неполный черновик по-прежнему сохраняется отдельно. Неконкурсная программа показывает сценарий сохранения без искусственной сдачи. + +Готовая заявка открывает существующий тип диалога с подтверждением фиксации решения и состава. POST использует именно подтвержденную версию из PATCH; отмена оставляет сохраненный черновик и не отправляет submit. Повторные действия блокируются на запросе. Успех показывает snapshot и дополнительно выполняет GET. При потерянном сетевом ответе GET устанавливает фактическое состояние без автоматической повторной сдачи. Закрытие срока, чужие права, финальное состояние и конфликты сохраняют контролируемые ошибки и доступный локальный ввод. + +Проверено тестами: полный test:ci прошел — 2552 теста в 465 файлах и строгие TypeScript-примеры C00. Целевые сценарии включают сохранение перед подтверждением, свежую версию, отмену, неконкурсный сценарий, закрытие срока после PATCH, неизвестный результат POST с последующим GET и отсутствие авторетрая. Development build, ESLint (0 ошибок, 6 прежних предупреждений), Stylelint измененного SCSS и Prettier 3.8.3 по lockfile (1941 файл) прошли. + +Проверено в браузере: настоящие Angular + Django/PostgreSQL/JWT, КЧ и хакатон, оба вида ошибок с tab/focus, приглашение и принятие второго участника, отмена и подтверждение, версия POST из PATCH, accepted-only snapshot, pending revoke после сдачи, readonly капитана и участника, 0/false. Изменение организатором подписей, порядка и добавление required не меняет сданный просмотр. Проверены desktop и 390 px; ошибок JavaScript и горизонтального overflow нет. Подтверждение переиспользует существующий dialog/controls и Mont. + +Эксперт, управление, аналитика и QA первого выпуска остаются в плане. PR остается draft во время следующих интеграций. diff --git a/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts index 7fefb9197..3945461c7 100644 --- a/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts +++ b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts @@ -7,6 +7,7 @@ import { fail, ok, Result } from "@domain/shared/result.type"; import { UpdateSolutionUseCase, UploadSolutionFileUseCase, + SubmitSolutionUseCase, } from "../use-cases/solution-draft.use-cases"; import { SolutionDraftFacade } from "./solution-draft.facade"; const original = solutionContractScenarios.find(s => s.name === "created_solo")! @@ -21,16 +22,19 @@ const conflict: SolutionApiError = { describe("Solution draft editing", () => { const update = { execute: vi.fn() }, - files = { execute: vi.fn() }; + files = { execute: vi.fn() }, + submission = { execute: vi.fn() }; let draft: SolutionDraftFacade; beforeEach(() => { update.execute.mockReset(); files.execute.mockReset(); + submission.execute.mockReset(); TestBed.configureTestingModule({ providers: [ SolutionDraftFacade, { provide: UpdateSolutionUseCase, useValue: update }, { provide: UploadSolutionFileUseCase, useValue: files }, + { provide: SubmitSolutionUseCase, useValue: submission }, ], }); draft = TestBed.inject(SolutionDraftFacade); @@ -153,4 +157,17 @@ describe("Solution draft editing", () => { expect(update.execute.mock.lastCall?.[2].answers).toEqual([{ fieldId: 1003, value: null }]); expect(files.execute).toHaveBeenCalledTimes(1); }); + it("submits the explicitly confirmed checkpoint and never silently substitutes a newer version", async () => { + const confirmed = source(); + confirmed.version = 2; + draft.hydrate({ ...source(), version: 3 }); + submission.execute.mockReturnValue(of(fail(conflict))); + await firstValueFrom(draft.submit(confirmed)); + expect(submission.execute).toHaveBeenCalledWith(12, 301, { + expectedVersion: 2, + expectedFormRevision: confirmed.form.revision, + }); + expect(draft.source()?.version).toBe(3); + expect(draft.busy()).toBe(false); + }); }); diff --git a/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts index a267ec2cd..41078985f 100644 --- a/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts +++ b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts @@ -8,6 +8,7 @@ import { ProgramSolution, SolutionApiError, UpdateSolutionCommand, + SolutionIssue, } from "@domain/solution/solution-contract.model"; import { fail } from "@domain/shared/result.type"; import { hasSolutionAnswer } from "@domain/solution/solution-values"; @@ -15,6 +16,7 @@ import { solutionFieldsGroup, SolutionControlValue } from "@utils/solution-field import { UpdateSolutionUseCase, UploadSolutionFileUseCase, + SubmitSolutionUseCase, } from "../use-cases/solution-draft.use-cases"; export type LocalSolutionAnswer = { fieldId: number; label: string; value: SolutionControlValue }; @@ -24,6 +26,7 @@ const equal = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b export class SolutionDraftFacade { private readonly update = inject(UpdateSolutionUseCase); private readonly files = inject(UploadSolutionFileUseCase); + private readonly submission = inject(SubmitSolutionUseCase); private readonly destroyRef = inject(DestroyRef); private readonly contextChanged = new Subject(); private readonly controlsChanged = new Subject(); @@ -36,8 +39,9 @@ export class SolutionDraftFacade { validators: [Validators.required, Validators.maxLength(256)], }); readonly saving = signal(false); + readonly submitting = signal(false); readonly uploading = signal(null); - readonly busy = computed(() => this.saving() || this.uploading() !== null); + readonly busy = computed(() => this.saving() || this.submitting() || this.uploading() !== null); readonly attempted = signal(false); readonly fieldErrors = signal>({}); readonly nameError = signal(""); @@ -159,7 +163,9 @@ export class SolutionDraftFacade { this.fieldErrors.set({}); this.nameError.set(""); this.attempted.set(false); - this.notice.set("Черновик сохранен."); + this.notice.set( + incoming.submission.status === "submitted" ? "Решение сдано." : "Черновик сохранен.", + ); } this.tick.update(value => value + 1); return true; @@ -193,6 +199,36 @@ export class SolutionDraftFacade { } this.fieldErrors.set(fields); } + showIssues(issues: SolutionIssue[]) { + this.attempted.set(true); + this.report({ + code: "required_program_fields", + detail: "Проверьте решение и состав команды перед сдачей.", + errors: issues, + meta: {}, + }); + } + submit(checkpoint: ProgramSolution) { + if (this.busy() || this.key !== checkpoint.programId + ":" + checkpoint.id || !this.canEdit()) + return of(fail(this.localError("permission_denied", "Сдача сейчас недоступна."))); + this.submitting.set(true); + this.error.set(null); + this.notice.set(""); + return this.submission + .execute(checkpoint.programId, checkpoint.id, { + expectedVersion: checkpoint.version, + expectedFormRevision: checkpoint.form.revision, + }) + .pipe( + takeUntil(this.contextChanged), + takeUntilDestroyed(this.destroyRef), + tap(result => { + if (result.ok) this.hydrate(result.value, true); + else this.report(result.error); + }), + finalize(() => this.submitting.set(false)), + ); + } save() { const source = this.source(); this.attempted.set(true); diff --git a/projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts index e398dec5d..05fad1464 100644 --- a/projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts +++ b/projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts @@ -14,6 +14,7 @@ import { ProgramSolution } from "@domain/solution/solution-contract.model"; import { UpdateSolutionUseCase, UploadSolutionFileUseCase, + SubmitSolutionUseCase, } from "./use-cases/solution-draft.use-cases"; describe("C00 draft and original upload wire", () => { @@ -30,6 +31,22 @@ describe("C00 draft and original upload wire", () => { }), ); afterEach(() => TestBed.inject(HttpTestingController).verify()); + it("submits only the saved expected versions through the exact solution-scoped route", async () => { + const command = { expectedVersion: 9, expectedFormRevision: "a".repeat(64) }; + const result = firstValueFrom(TestBed.inject(SubmitSolutionUseCase).execute(12, 301, command)); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/301/submit/", + ); + expect(request.request.method).toBe("POST"); + expect(request.request.body).toEqual({ + expected_version: 9, + expected_form_revision: command.expectedFormRevision, + }); + const expected = solutionContractScenarios.find(s => s.name === "submit_success")! + .response as ProgramSolution; + request.flush(snakecaseKeys(expected, { deep: true })); + expect(await result).toEqual({ ok: true, value: expected }); + }); it("PATCH uses both expected versions and preserves null, zero, false and exact option strings", async () => { const command = { expectedVersion: 2, diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts index 7b46b9301..ec7fecf1e 100644 --- a/projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts @@ -2,9 +2,20 @@ import { inject, Injectable } from "@angular/core"; import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; -import { UpdateSolutionCommand } from "@domain/solution/solution-contract.model"; +import { + UpdateSolutionCommand, + SubmitSolutionCommand, +} from "@domain/solution/solution-contract.model"; import { solutionRequestResult } from "./solution.use-cases"; +@Injectable({ providedIn: "root" }) +export class SubmitSolutionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: SubmitSolutionCommand) { + return solutionRequestResult(() => this.repository.submit(programId, solutionId, command)); + } +} + @Injectable({ providedIn: "root" }) export class UpdateSolutionUseCase { private readonly repository = inject(SolutionRepositoryPort); diff --git a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts index ce140557a..c84dced8b 100644 --- a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts +++ b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts @@ -21,9 +21,15 @@ import { SolutionTemplates, UpdateSolutionCommand, SolutionUploadResponse, + SubmitSolutionCommand, } from "../solution-contract.model"; export abstract class SolutionRepositoryPort { + abstract submit( + programId: number, + solutionId: number, + command: SubmitSolutionCommand, + ): Observable; abstract update( programId: number, solutionId: number, diff --git a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts index 5e7612c24..a9416bd0d 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts @@ -23,6 +23,7 @@ import { SolutionTemplates, UpdateSolutionCommand, SolutionUploadResponse, + SubmitSolutionCommand, } from "@domain/solution/solution-contract.model"; import { SolutionListQuery, @@ -32,6 +33,12 @@ import { @Injectable({ providedIn: "root" }) export class SolutionHttpAdapter { + submit(programId: number, solutionId: number, command: SubmitSolutionCommand) { + return this.api.post( + `${this.solutionPath(programId, solutionId)}submit/`, + command, + ); + } private readonly api = inject(ApiService); update(programId: number, solutionId: number, command: UpdateSolutionCommand) { return this.api.patch(this.solutionPath(programId, solutionId), command); diff --git a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts index 858af3fd4..59ca46ae6 100644 --- a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts @@ -14,11 +14,15 @@ import { SolutionUpdateInvitation, SolutionFormWrite, UpdateSolutionCommand, + SubmitSolutionCommand, } from "@domain/solution/solution-contract.model"; import { SolutionHttpAdapter } from "../../adapters/solution/solution-http.adapter"; @Injectable({ providedIn: "root" }) export class SolutionRepository implements SolutionRepositoryPort { + submit(programId: number, solutionId: number, command: SubmitSolutionCommand) { + return this.http.submit(programId, solutionId, command); + } private readonly http = inject(SolutionHttpAdapter); update(programId: number, solutionId: number, command: UpdateSolutionCommand) { return this.http.update(programId, solutionId, command); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html index 3f76766c5..0ebba3b96 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html @@ -1,6 +1,6 @@
- @if (busy()) { + @if (updating()) {

Обновляем команду…

}
diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts index ab6671dda..a51d545e6 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts @@ -58,6 +58,7 @@ import { SolutionMemberInviteModalComponent } from "@ui/widgets/project-invite/s export class SolutionTeamComponent { readonly solution = input.required(); readonly refreshing = input(false); + readonly blocked = input(false); readonly changed = output(); readonly left = output(); readonly currentUser = inject(ProfileInfoService).profile; @@ -66,7 +67,8 @@ export class SolutionTeamComponent { readonly listError = signal(""); readonly error = signal(""); private readonly pendingCommand = signal(false); - readonly busy = computed(() => this.pendingCommand() || this.refreshing()); + readonly updating = computed(() => this.pendingCommand() || this.refreshing()); + readonly busy = computed(() => this.updating() || this.blocked()); readonly inviting = signal(false); readonly editing = signal(null); readonly role = new FormControl("", { nonNullable: true, validators: [inviteRoleValidator] }); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html index 8748d723a..4c66416e7 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html @@ -18,6 +18,16 @@

{{ item.name }}

{{ item.submission.status === "submitted" ? "Решение сдано" : "Черновик заявки" }}

+ @if (item.submission.status !== "submitted") { + @if (!item.submission.required) { +

Программа не требует сдачи. Сохраняйте работу как черновик.

+ } @else if (item.submission.deadline) { +

+ {{ item.submission.submissionOpen ? "Сдать до" : "Прием решений закрыт" }} + {{ item.submission.deadline | date: "dd.MM.yyyy, HH:mm" }} +

+ } + }
@if (item.submission.submittedAt) {

Сдано {{ item.submission.submittedAt | date: "dd.MM.yyyy, HH:mm" }}

@@ -41,12 +51,17 @@

{{ item.name }}

} } @else { -
+

Данные решения

@if (draft.canEdit()) {

@@ -59,7 +74,7 @@

Данные решения

id="solution-name" [formControl]="draft.name" maxlength="256" - [readOnly]="draft.busy() || refreshingTeam()" + [readOnly]="commandBusy() || refreshingTeam()" /> @if (draft.nameError()) { @@ -78,8 +93,9 @@

Данные решения

[fields]="item.form.fields" [group]="draft.displayGroup()" [readonly]="!draft.canEdit()" - [busy]="draft.busy() || refreshingTeam()" + [busy]="commandBusy() || refreshingTeam()" [attempted]="draft.attempted()" + [requireAnswers]="submissionAttempted()" [errors]="draft.fieldErrors()" (upload)="draft.upload($event.fieldId, $event.file)" (clearFile)="draft.clearFile($event)" @@ -115,7 +131,7 @@

Несохраненные ответы измен + + } + + } @if (draft.notice()) {

{{ draft.notice() }}

} @@ -146,11 +173,26 @@

Несохраненные ответы измен

}
+ @if (item.submission.required && item.submission.status === "draft" && draft.canEdit()) { + + } @if (draft.canEdit()) { } @if (draft.dirty()) { - } @@ -166,3 +213,33 @@

Несохраненные ответы измен } +@if (confirmation()) { + +
+ + +
+
+} diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss index 3fd2cdffc..500f2d674 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss @@ -120,6 +120,28 @@ nav a:focus-visible { align-items: center; } +.submission-confirmation-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.submission-issues { + padding-left: 20px; +} + +.submission-issues .ui-button { + height: auto; + padding: 4px 0; + text-align: left; + white-space: normal; +} + +h2:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 4px; +} + .ui-button { @include ui-foundation.button; } diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts index c1aaa339d..985a22fe5 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts @@ -16,7 +16,10 @@ import { ProgramSolution, SolutionContext } from "@domain/solution/solution-cont import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; import { fail, ok } from "@domain/shared/result.type"; import { ProgramSolutionComponent } from "./solution.component"; -import { UpdateSolutionUseCase } from "@api/solution/use-cases/solution-draft.use-cases"; +import { + UpdateSolutionUseCase, + SubmitSolutionUseCase, +} from "@api/solution/use-cases/solution-draft.use-cases"; import { canLeaveSolution } from "@ui/routes/program/solution-leave.guard"; const solution = solutionContractScenarios.find(s => s.name === "created_solo")! @@ -38,11 +41,13 @@ describe("Solution route foundation", () => { const create = { execute: vi.fn() }; const getContext = { execute: vi.fn() }; const update = { execute: vi.fn() }; + const submit = { execute: vi.fn() }; beforeEach(() => { get.execute.mockReset().mockReturnValue(of(ok(solution))); create.execute.mockReset().mockReturnValue(of(ok(solution))); getContext.execute.mockReset().mockReturnValue(of(ok(context))); update.execute.mockReset(); + submit.execute.mockReset(); TestBed.configureTestingModule({ providers: [ provideRouter([ @@ -67,6 +72,7 @@ describe("Solution route foundation", () => { { provide: CreateSolutionUseCase, useValue: create }, { provide: GetSolutionContextUseCase, useValue: getContext }, { provide: UpdateSolutionUseCase, useValue: update }, + { provide: SubmitSolutionUseCase, useValue: submit }, { provide: ProfileInfoService, useValue: { profile: signal({ id: 7 }) } }, { provide: SolutionRepositoryPort, @@ -230,4 +236,155 @@ describe("Solution route foundation", () => { expect(component.draft.dirty()).toBe(false); confirmation.mockRestore(); }); + + it("saves first, opens the team issue tab and does not confirm or submit an incomplete team", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + update.execute.mockReturnValue(of(ok({ ...solution, version: 2 }))); + component.prepareSubmission({ + currentTarget: document.createElement("button"), + } as unknown as Event); + await harness.fixture.whenStable(); + harness.detectChanges(); + expect(TestBed.inject(Router).url).toContain("tab=team"); + expect(component.confirmation()).toBeNull(); + expect(submit.execute).not.toHaveBeenCalled(); + expect(component.submissionIssues()[0].path).toBe("team"); + expect(component.draft.dirty()).toBe(false); + }); + + it("requires confirmation after PATCH, posts its fresh versions exactly once and rereads the submitted state", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88?tab=team", + ProgramSolutionComponent, + ); + const saved = { ...solution, version: 9, readiness: { isReady: true, issues: [] } }; + update.execute.mockReturnValue(of(ok(saved))); + component.prepareSubmission({ + currentTarget: document.createElement("button"), + } as unknown as Event); + expect(component.confirmation()).toEqual(saved); + expect(submit.execute).not.toHaveBeenCalled(); + const submitted: ProgramSolution = { + ...saved, + version: 10, + submission: { ...saved.submission, status: "submitted", submittedAt: "2026-10-24T12:00:00Z" }, + permissions: { + ...saved.permissions, + canEdit: false, + canSubmit: false, + canManageTeam: false, + canInvite: false, + }, + form: { ...saved.form, isSnapshot: true }, + }; + submit.execute.mockReturnValue(of(ok(submitted))); + get.execute.mockReturnValue(of(ok(submitted))); + component.confirmSubmission(); + harness.detectChanges(); + expect(submit.execute).toHaveBeenCalledWith(12, 301, { + expectedVersion: 9, + expectedFormRevision: saved.form.revision, + }); + expect(get.execute).toHaveBeenCalledTimes(2); + expect(component.solution()?.submission.status).toBe("submitted"); + expect(component.draft.canEdit()).toBe(false); + expect(component.confirmation()).toBeNull(); + }); + + it("canceling confirmation retains the saved draft and sends no submit", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + update.execute.mockReturnValue( + of(ok({ ...solution, version: 2, readiness: { isReady: true, issues: [] } })), + ); + component.prepareSubmission({ currentTarget: null } as unknown as Event); + component.confirmation.set(null); + expect(component.solution()?.version).toBe(2); + expect(component.draft.dirty()).toBe(false); + expect(submit.execute).not.toHaveBeenCalled(); + }); + + it("resolves an interrupted POST with GET and never automatically submits again", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + const saved = { ...solution, version: 2, readiness: { isReady: true, issues: [] } }; + update.execute.mockReturnValue(of(ok(saved))); + component.prepareSubmission({ currentTarget: null } as unknown as Event); + submit.execute.mockReturnValue( + of(fail({ code: "network", detail: "Соединение прервано", errors: [], meta: {} })), + ); + get.execute.mockReturnValue( + of( + ok({ + ...saved, + version: 3, + submission: { + ...saved.submission, + status: "submitted", + submittedAt: "2026-10-24T12:00:00Z", + }, + permissions: { ...saved.permissions, canEdit: false, canSubmit: false }, + }), + ), + ); + component.confirmSubmission(); + expect(submit.execute).toHaveBeenCalledTimes(1); + expect(component.solution()?.submission.status).toBe("submitted"); + expect(component.draft.canEdit()).toBe(false); + }); + + it("a closed deadline after PATCH keeps the saved draft and does not open confirmation", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + update.execute.mockReturnValue( + of( + ok({ + ...solution, + version: 2, + permissions: { ...solution.permissions, canSubmit: false }, + submission: { ...solution.submission, submissionOpen: false }, + }), + ), + ); + component.prepareSubmission({ currentTarget: null } as unknown as Event); + expect(component.confirmation()).toBeNull(); + expect(component.draft.dirty()).toBe(false); + expect(component.draft.error()?.code).toBe("submission_closed"); + expect(submit.execute).not.toHaveBeenCalled(); + }); + it("a noncompetitive program offers draft saving without a submit action", async () => { + get.execute.mockReturnValue( + of( + ok({ + ...solution, + submission: { ...solution.submission, required: false }, + readiness: null, + permissions: { ...solution.permissions, canSubmit: false }, + }), + ), + ); + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl("/office/program/12/solutions/88", ProgramSolutionComponent); + expect(harness.routeNativeElement?.textContent).toContain("Программа не требует сдачи"); + expect( + [...harness.routeNativeElement!.querySelectorAll("button")].some( + button => button.textContent?.trim() === "Сдать решение", + ), + ).toBe(false); + expect(harness.routeNativeElement?.textContent).toContain("Сохранить черновик"); + }); }); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts index 09da03236..2bf95e86d 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts @@ -1,6 +1,16 @@ /** @format */ -import { ChangeDetectionStrategy, Component, HostListener, inject, signal } from "@angular/core"; +import { + afterNextRender, + ChangeDetectionStrategy, + Component, + computed, + ElementRef, + HostListener, + inject, + Injector, + signal, +} from "@angular/core"; import { ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -12,13 +22,18 @@ import { } from "@api/solution/use-cases/solution.use-cases"; import { solutionApplicationRoute } from "@api/solution/solution-navigation"; import { AppRoutes } from "@api/paths/app-routes"; -import { ProgramSolution, SolutionApiError } from "@domain/solution/solution-contract.model"; +import { + ProgramSolution, + SolutionApiError, + SolutionIssue, +} from "@domain/solution/solution-contract.model"; import { validSolutionId } from "@domain/solution/ports/solution.repository.port"; import { ButtonDirective, FieldDirective } from "@uilib"; import { DatePipe } from "@angular/common"; import { SolutionTeamComponent } from "./solution-team.component"; import { SolutionDraftFacade } from "@api/solution/facades/solution-draft.facade"; import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fields.component"; +import { ProjectInviteDialogComponent } from "@ui/widgets/project-invite/project-invite-dialog.component"; @Component({ selector: "app-program-solution", @@ -31,6 +46,7 @@ import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fi DatePipe, SolutionTeamComponent, SolutionFieldsComponent, + ProjectInviteDialogComponent, ], providers: [SolutionDraftFacade], templateUrl: "./solution.component.html", @@ -43,6 +59,8 @@ export class ProgramSolutionComponent { private readonly context = inject(GetSolutionContextUseCase); private readonly create = inject(CreateSolutionUseCase); private readonly get = inject(GetSolutionUseCase); + private readonly element = inject>(ElementRef); + private readonly injector = inject(Injector); readonly draft = inject(SolutionDraftFacade); readonly solution = signal(null); readonly loading = signal(true); @@ -52,61 +70,191 @@ export class ProgramSolutionComponent { readonly refreshingTeam = signal(false); readonly teamRefreshError = signal(""); readonly routes = AppRoutes; + readonly submissionAttempted = signal(false); + readonly submissionIssues = signal([]); + readonly confirmation = signal(null); + readonly commandBusy = computed(() => this.draft.busy() || this.confirmation() !== null); + submissionTrigger: HTMLElement | null = null; private readonly retry = new Subject(); private readonly teamRefresh = new Subject<{ programId: number; solutionId: number } | null>(); private reconcileDraft = false; canLeave(): boolean { return ( - !(this.draft.dirty() || this.draft.busy()) || + !(this.draft.dirty() || this.commandBusy()) || confirm("В заявке есть несохраненные изменения. Покинуть страницу?") ); } @HostListener("window:beforeunload", ["$event"]) onBeforeUnload(event: BeforeUnloadEvent): void { - if (this.draft.dirty() || this.draft.busy()) { + if (this.draft.dirty() || this.commandBusy()) { event.preventDefault(); event.returnValue = ""; } } saveDraft(): void { - if (this.refreshingTeam() || this.teamRefreshError()) return; + if (this.commandBusy() || this.refreshingTeam() || this.teamRefreshError()) return; this.draft.save().subscribe(result => { if (result.ok) this.solution.set(result.value); - else { - void this.router.navigate([], { relativeTo: this.route, queryParams: {} }); + else this.handleCommandError(result.error); + }); + } + + prepareSubmission(event: Event): void { + if ( + this.commandBusy() || + this.refreshingTeam() || + this.teamRefreshError() || + !this.solution()?.permissions.canSubmit + ) + return; + this.submissionTrigger = event.currentTarget as HTMLElement; + this.submissionAttempted.set(true); + this.submissionIssues.set([]); + this.draft.save().subscribe(result => { + if (!result.ok) { + this.handleCommandError(result.error); + return; + } + const saved = result.value; + this.solution.set(saved); + if (!saved.permissions.canSubmit) { + this.draft.error.set({ + code: "submission_closed", + detail: "Сдача сейчас недоступна. Черновик сохранен.", + errors: [], + meta: {}, + }); + return; + } + const issues = saved.readiness?.issues ?? []; + if (issues.length) { + this.submissionIssues.set(issues); + this.draft.showIssues(issues); + void this.focusIssue(issues[0]); + } else this.confirmation.set(saved); + }); + } + + confirmSubmission(): void { + const checkpoint = this.confirmation(); + if (!checkpoint || this.draft.busy()) return; + this.draft.submit(checkpoint).subscribe(result => { + this.confirmation.set(null); + if (result.ok) { + this.solution.set(result.value); + this.submissionIssues.set([]); + this.submissionAttempted.set(false); + this.refreshTeam(); + } else { + this.handleCommandError(result.error); + // A network interruption has an unknown outcome: GET determines the actual state. if ( [ - "version_conflict", - "form_revision_conflict", - "not_solution_captain", - "team_frozen", - "solution_mode_unavailable", + "network", + "server_error", + "unknown", + "already_submitted", + "submission_closed", + "not_competitive", ].includes(result.error.code) - ) { - this.reconcileDraft = true; + ) this.refreshTeam(); - } else if ( - [ - "not_found", - "forbidden", - "permission_denied", - "unauthorized", - "authentication_required", - ].includes(result.error.code) - ) { - this.draft.clear(); - this.solution.set(null); - this.showError(result.error); - } } }); } + private handleCommandError(error: SolutionApiError): void { + this.submissionIssues.set(error.errors); + if (error.errors.length) void this.focusIssue(error.errors[0]); + else if (this.draft.name.invalid) + void this.focusIssue({ + path: "name", + code: "invalid", + detail: "Проверьте название", + fieldId: null, + }); + else { + const field = this.solution()?.form.fields.find( + field => this.draft.group().get(String(field.id))?.invalid, + ); + if (field) + void this.focusIssue({ + path: "answers", + code: "invalid", + detail: "Проверьте значение", + fieldId: field.id, + }); + } + if ( + [ + "version_conflict", + "form_revision_conflict", + "not_solution_captain", + "team_frozen", + "solution_mode_unavailable", + ].includes(error.code) + ) { + this.reconcileDraft = true; + this.refreshTeam(); + } else if ( + [ + "not_found", + "forbidden", + "permission_denied", + "unauthorized", + "authentication_required", + ].includes(error.code) + ) { + this.draft.clear(); + this.solution.set(null); + this.showError(error); + } + } + + async focusIssue(issue: SolutionIssue): Promise { + const key = this.solution()?.programId + ":" + this.solution()?.id; + await this.router.navigate([], { + relativeTo: this.route, + queryParams: issue.path.startsWith("team") ? { tab: "team" } : {}, + }); + afterNextRender( + () => { + if (this.solution()?.programId + ":" + this.solution()?.id !== key) return; + const selector = issue.path.startsWith("team") + ? "#team-title" + : issue.fieldId !== null + ? `#solution-field-${issue.fieldId}` + : issue.path === "name" + ? "#solution-name" + : "#data-title"; + let target = this.element.nativeElement.querySelector(selector); + if (target?.matches('input[type="file"]')) + target = target.closest("fieldset")?.querySelector("button") ?? null; + if (target) { + if (!target.matches("input,select,textarea,button,a")) target.tabIndex = -1; + target.focus(); + target.scrollIntoView?.({ block: "center", behavior: "smooth" }); + } + }, + { injector: this.injector }, + ); + } + + issueLabel(issue: SolutionIssue): string { + const label = + issue.fieldId === null + ? issue.path.startsWith("team") + ? "Команда" + : "Заявка" + : this.solution()?.form.fields.find(field => field.id === issue.fieldId)?.label; + return label ? label + ": " + issue.detail : issue.detail; + } + discardDraft(): void { - if (!this.draft.busy() && confirm("Отменить несохраненные изменения решения?")) + if (!this.commandBusy() && confirm("Отменить несохраненные изменения решения?")) this.draft.discard(); } @@ -179,6 +327,9 @@ export class ProgramSolutionComponent { this.refreshingTeam.set(false); this.teamRefreshError.set(""); this.reconcileDraft = false; + this.confirmation.set(null); + this.submissionIssues.set([]); + this.submissionAttempted.set(false); this.draft.clear(); this.solution.set(null); this.error.set(null); diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.html b/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.html index 8c2550b44..3b3a84af6 100644 --- a/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.html +++ b/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.html @@ -31,7 +31,7 @@

{{ title() }}

appButton="icon" type="button" class="project-invite__close" - aria-label="Закрыть приглашение" + [attr.aria-label]="closeLabel()" (click)="close()" > @if (compact()) { diff --git a/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.ts b/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.ts index 259ea9b22..720d1709f 100644 --- a/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.ts +++ b/projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.ts @@ -42,6 +42,7 @@ export class ProjectInviteDialogComponent implements AfterViewInit { readonly description = input.required(); readonly trigger = input(null); readonly compact = input(false); + readonly closeLabel = input("Закрыть приглашение"); readonly closed = output(); protected readonly open = signal(true); protected readonly attached = signal(false); From abc03dfa06a07f44294f1fbf593ed81c5cc728d3 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 23:27:14 +0300 Subject: [PATCH 05/47] feat(programs): connect solution evaluation API contracts --- docs/contracts/program-solutions-v1/README.md | 4 +- .../program-solutions-v1/examples.json | 845 +++- .../program-solutions-v1/openapi.json | 4201 ++++++++++++----- docs/product/program-solutions-fe07-api.md | 15 + .../src/app/api/solution/solution-error.ts | 4 + .../solution-evaluation-contract.spec.ts | 234 + .../solution-evaluation.use-cases.ts | 93 + .../ports/solution.repository.port.ts | 56 + .../solution/solution-contract.fixture.ts | 827 ++++ .../solution/solution-contract.model.ts | 208 +- .../solution/solution-http.adapter.ts | 92 + .../solution/solution.repository.ts | 36 + tools/generate-solution-contract.py | 4 +- tools/verify-solution-contract-types.ts | 32 + 14 files changed, 5546 insertions(+), 1105 deletions(-) create mode 100644 docs/product/program-solutions-fe07-api.md create mode 100644 projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts create mode 100644 projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index ef201bbf0..77b055fb3 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -2,7 +2,9 @@ # C00 Контракт решения в программе -Версия 1.2.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. +Рабочая версия 1.3.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. + +Изменение 1.3.0: совместимое расширение BE07/FE07 добавляет десять операций оценки, назначений, распределения, числа экспертов и результатов решений. Перед реализацией зафиксированы 72 схемы и 55 независимых примеров. В errors появился optional criterion_id; field_id остается ID поля решения. На проводе оценки используют string/null, назначения — настоящие solution_id и Expert.id. Распределение требует ревизию и подписанный предпросмотр; результаты доступны организатору. Старые URL и обязательность полей сохранены. API и frontend port/adapter/use cases реализованы в рабочей ветке; экспертный UI требует согласования D01 и еще не подключен. Rollout: migration project_rates.0008 → backend → frontend; default legacy_project сохранен. Изменение 1.2.0: общий POST `/files/` принимает необязательный query-параметр `preserve_original=true`. Он сохраняет исходные байты и формат изображения для материалов решения. Без параметра остается прежнее преобразование изображений в WebP. Multipart содержит только `file`, успешный ответ по-прежнему `{url}`. Сервер хранит фактический тип содержимого внутри UserFile и проверяет его при привязке к решению; новые публичные поля ответа не добавляются. Клиент с опцией оригинала внедряется после backend с ее поддержкой. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json index d88b4b22c..fea1f25d7 100644 --- a/docs/contracts/program-solutions-v1/examples.json +++ b/docs/contracts/program-solutions-v1/examples.json @@ -1,5 +1,5 @@ { - "contract_version": "1.2.0", + "contract_version": "1.3.0", "description": "Независимые примеры, не последовательность операций; фиктивные данные.", "scenarios": [ { @@ -4044,6 +4044,849 @@ "blocked_reason": null, "current_application": null } + }, + { + "name": "evaluation_list", + "operation_id": "getSolutionRatings", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "case_name": "Цифровой сервис", + "assigned": true, + "is_completed": false, + "can_rate": true, + "blocked_reason": null + } + ] + } + }, + { + "name": "evaluation_frame", + "operation_id": "getSolutionRating", + "status": 200, + "response": { + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "criteria_revision": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "criteria": [ + { + "id": 21, + "name": "Практическая ценность", + "description": "Польза аудитории", + "type": "int", + "min_value": 0, + "max_value": 10, + "value": null + }, + { + "id": 22, + "name": "Проработанность", + "description": null, + "type": "float", + "min_value": 0, + "max_value": 5, + "value": null + }, + { + "id": 23, + "name": "Показан прототип", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 24, + "name": "Рекомендация", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": null + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": false, + "rated_count": 0, + "max_rates": 3, + "required_evaluators": 2, + "deadline": "2026-10-30T18:00:00Z" + } + }, + { + "name": "evaluation_saved", + "operation_id": "rateSolution", + "status": 200, + "request": { + "expected_criteria_revision": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "scores": [ + { + "criterion_id": 21, + "value": "0" + }, + { + "criterion_id": 22, + "value": "4.5" + }, + { + "criterion_id": 23, + "value": "False" + }, + { + "criterion_id": 24, + "value": "" + } + ] + }, + "response": { + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "criteria_revision": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "criteria": [ + { + "id": 21, + "name": "Практическая ценность", + "description": "Польза аудитории", + "type": "int", + "min_value": 0, + "max_value": 10, + "value": "0" + }, + { + "id": 22, + "name": "Проработанность", + "description": null, + "type": "float", + "min_value": 0, + "max_value": 5, + "value": "4.5" + }, + { + "id": 23, + "name": "Показан прототип", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + }, + { + "id": 24, + "name": "Рекомендация", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": true, + "rated_count": 1, + "max_rates": 3, + "required_evaluators": 2, + "deadline": "2026-10-30T18:00:00Z" + } + }, + { + "name": "evaluation_criteria_changed", + "operation_id": "rateSolution", + "status": 409, + "request": { + "expected_criteria_revision": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "scores": [ + { + "criterion_id": 21, + "value": "0" + }, + { + "criterion_id": 22, + "value": "4.5" + }, + { + "criterion_id": 23, + "value": "False" + }, + { + "criterion_id": 24, + "value": "" + } + ] + }, + "response": { + "code": "criteria_changed", + "detail": "Критерии изменились. Обновите форму оценки.", + "errors": [], + "meta": { + "current_criteria_revision": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + } + }, + { + "name": "solution_assignment_options", + "operation_id": "getSolutionExpertAssignments", + "status": 200, + "response": { + "program_id": 12, + "expert_id": 31, + "revision": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "solutions": [ + { + "solution_id": 301, + "name": "Сервис для студентов", + "case_name": "Цифровой сервис", + "submitted": true, + "assigned": false, + "protected": false, + "is_completed": false, + "own_team": false, + "eligible": true + } + ] + } + }, + { + "name": "solution_assignments_saved", + "operation_id": "saveSolutionExpertAssignments", + "status": 200, + "request": { + "revision": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "solution_ids": [ + 301 + ] + }, + "response": { + "program_id": 12, + "expert_id": 31, + "revision": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "solutions": [ + { + "solution_id": 301, + "name": "Сервис для студентов", + "case_name": "Цифровой сервис", + "submitted": true, + "assigned": true, + "protected": false, + "is_completed": false, + "own_team": false, + "eligible": true + } + ] + } + }, + { + "name": "evaluation_invalid_value", + "operation_id": "rateSolution", + "status": 400, + "request": { + "expected_criteria_revision": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "scores": [ + { + "criterion_id": 21, + "value": "NaN" + } + ] + }, + "response": { + "code": "invalid_scores", + "detail": "Проверьте критерии оценки.", + "errors": [ + { + "path": "scores.21.value", + "code": "invalid_value", + "field_id": null, + "criterion_id": 21, + "detail": "Укажите конечное число." + } + ], + "meta": {} + } + }, + { + "name": "solution_allocation_preview", + "operation_id": "previewSolutionAllocation", + "status": 200, + "request": { + "revision": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "expert_ids": [ + 31 + ] + }, + "response": { + "revision": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "plan_token": "signed-local-example", + "assignments": [ + { + "solution_id": 301, + "solution_name": "Решение команды", + "expert_id": 31, + "expert_name": "Эксперт программы", + "case_name": "Цифровой сервис" + } + ], + "shortages": [], + "can_reroll": false, + "load": [ + { + "expert_id": 31, + "name": "Эксперт программы", + "before": 0, + "after": 1, + "assigned_before": 0, + "assigned_after": 1 + } + ] + } + }, + { + "name": "solution_allocation_applied", + "operation_id": "applySolutionAllocation", + "status": 200, + "request": { + "plan_token": "signed-local-example" + }, + "response": { + "program_id": 12, + "revision": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "created": 1 + } + }, + { + "name": "solution_quota_preview", + "operation_id": "previewSolutionQuota", + "status": 200, + "response": { + "revision": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "required_evaluators": 2, + "submitted_solutions": 1, + "solutions_missing_experts": 1, + "ready_before": 0, + "ready_after": 0, + "missing_assignments": 1, + "surplus_assignments": 0 + } + }, + { + "name": "solution_quota_saved", + "operation_id": "saveSolutionQuota", + "status": 200, + "request": { + "revision": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "required_evaluators": 2 + }, + "response": { + "revision": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "required_evaluators": 2, + "submitted_solutions": 1, + "solutions_missing_experts": 1, + "ready_before": 0, + "ready_after": 0, + "missing_assignments": 1, + "surplus_assignments": 0 + } + }, + { + "name": "solution_results", + "operation_id": "getSolutionResults", + "status": 200, + "response": { + "program_id": 12, + "program_name": "Кейс-чемпионат PROCOLLAB", + "generated_at": "2026-10-24T20:00:00Z", + "required_evaluators": 2, + "maximum": 15, + "has_numeric_criteria": true, + "case_options": [ + "Цифровой сервис" + ], + "groups": [ + { + "case_name": "Цифровой сервис", + "solutions": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "solution_id": 301, + "name": "Сервис для студентов", + "case_name": "Цифровой сервис", + "completed": 1, + "required": 2, + "evaluation_complete": false, + "ready": false, + "score": 4.5, + "place": null, + "averages": { + "21": 0, + "22": 4.5 + }, + "experts": [ + { + "user_id": 91, + "name": "Эксперт программы", + "email": "expert@local.example", + "avatar": null, + "complete": true, + "total": 4.5, + "values": [ + { + "criterion_id": 21, + "value": "0" + }, + { + "criterion_id": 22, + "value": "4.5" + }, + { + "criterion_id": 23, + "value": "False" + }, + { + "criterion_id": 24, + "value": "" + } + ] + } + ] + } + ] + } + ], + "criteria": [ + { + "id": 21, + "name": "Практическая ценность", + "description": "Польза аудитории", + "type": "int", + "min_value": 0, + "max_value": 10 + }, + { + "id": 22, + "name": "Проработанность", + "description": null, + "type": "float", + "min_value": 0, + "max_value": 5 + }, + { + "id": 23, + "name": "Показан прототип", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null + }, + { + "id": 24, + "name": "Рекомендация", + "description": null, + "type": "str", + "min_value": null, + "max_value": null + } + ] + } } ] } diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json index 2241f2664..676f61bb9 100644 --- a/docs/contracts/program-solutions-v1/openapi.json +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -3,8 +3,8 @@ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "PROCOLLAB Program Solutions C00", - "version": "1.2.0", - "description": "Контракт решений в программах для реализации и моков. Endpoints подключаются поэтапно по BE01–BE06. API base URL задается окружением. snake_case на проводе. Старые API не меняются." + "version": "1.3.0", + "description": "Контракт решений в программах для реализации и моков BE01–BE07. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ { @@ -5809,18 +5809,1972 @@ } ] } - } - }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" + }, + "/programs/{program_id}/solution-evaluation/": { + "get": { + "operationId": "getSolutionRatings", + "summary": "Работы для оценки экспертом", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string", + "maxLength": 200 + } + }, + { + "name": "case_scope", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "selected", + "without_case" + ], + "default": "all" + } + }, + { + "name": "case_name", + "in": "query", + "required": false, + "schema": { + "type": "string", + "maxLength": 500 + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "pending", + "completed" + ], + "default": "all" + } + }, + { + "name": "assigned_only", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RatingPage" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } } }, - "schemas": { - "User": { + "/programs/{program_id}/solutions/{solution_id}/evaluation/": { + "get": { + "operationId": "getSolutionRating", + "summary": "Сданное решение и собственная оценка", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RatingFrame" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "rateSolution", + "summary": "Сохранить собственную оценку решения", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RatingFrame" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateSolution" + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-evaluation/experts/{expert_id}/assignments/": { + "get": { + "operationId": "getSolutionExpertAssignments", + "summary": "Варианты назначений решений", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "expert_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignmentOptions" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "operationId": "saveSolutionExpertAssignments", + "summary": "Сохранить назначения решений эксперту", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "expert_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignmentOptions" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionAssignmentsWrite" + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-evaluation/allocation/": { + "post": { + "operationId": "previewSolutionAllocation", + "summary": "Предпросмотр распределения решений", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionAllocationPlan" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionAllocationWrite" + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-evaluation/allocation/apply/": { + "post": { + "operationId": "applySolutionAllocation", + "summary": "Применить распределение решений", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionAssignmentReceipt" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionAllocationApply" + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-evaluation/quota/": { + "get": { + "operationId": "previewSolutionQuota", + "summary": "Последствия изменения числа экспертов", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "required_evaluators", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionQuota" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "operationId": "saveSolutionQuota", + "summary": "Сохранить число экспертов без удаления оценок", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionQuota" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionQuotaWrite" + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-results/": { + "get": { + "operationId": "getSolutionResults", + "summary": "Результаты решений для организатора", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string", + "maxLength": 200 + } + }, + { + "name": "case_scope", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "selected", + "without_case" + ] + } + }, + { + "name": "case_name", + "in": "query", + "required": false, + "schema": { + "type": "string", + "maxLength": 500 + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "complete", + "pending" + ] + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionResults" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "avatar_url": { + "anyOf": [ + { + "type": "string", + "format": "uri", + "pattern": "^https?://" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "first_name", + "last_name", + "avatar_url" + ], + "additionalProperties": false + }, + "ProgramRef": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "SolutionRef": { + "type": "object", + "properties": { + "kind": { + "const": "solution" + }, + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + } + }, + "required": [ + "kind", + "program_id", + "solution_id", + "name" + ], + "additionalProperties": false + }, + "LegacyRef": { + "type": "object", + "properties": { + "kind": { + "const": "legacy_project" + }, + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "project_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "program_link_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + } + }, + "required": [ + "kind", + "program_id", + "project_id", + "program_link_id", + "name" + ], + "additionalProperties": false + }, + "ApplicationRef": { + "oneOf": [ + { + "$ref": "#/components/schemas/SolutionRef" + }, + { + "$ref": "#/components/schemas/LegacyRef" + } + ] + }, + "Issue": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "code": { + "type": "string" + }, + "field_id": { + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "detail": { + "type": "string" + }, + "criterion_id": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 9007199254740991, + "description": "ID критерия оценки; не подменяет field_id поля решения. Необязательное расширение 1.3.0." + } + }, + "required": [ + "path", + "code", + "field_id", + "detail" + ], + "additionalProperties": false + }, + "Error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } + }, + "meta": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "code", + "detail", + "errors", + "meta" + ], + "additionalProperties": false + }, + "Context": { + "type": "object", + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "storage_mode": { + "enum": [ + "legacy_project", + "solution" + ] + }, + "is_registered": { + "type": "boolean" + }, + "is_competitive": { + "type": "boolean" + }, + "submission_open": { + "type": "boolean" + }, + "submission_deadline": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "can_create": { + "type": "boolean" + }, + "blocked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "current_application": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApplicationRef" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "program_id", + "storage_mode", + "is_registered", + "is_competitive", + "submission_open", + "submission_deadline", + "can_create", + "blocked_reason", + "current_application" + ], + "additionalProperties": false + }, + "Validation": { + "type": "object", + "properties": { + "min_length": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "max_length": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "minimum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "allowed_extensions": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]+$" + } + }, + "max_file_size_bytes": { + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 104857600 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "min_length", + "max_length", + "minimum", + "maximum", + "allowed_extensions", + "max_file_size_bytes" + ], + "additionalProperties": false + }, + "FieldType": { + "enum": [ + "text", + "textarea", + "url", + "number", + "select", + "checkbox", + "file", + "radio" + ] + }, + "Purpose": { + "enum": [ + "other", + "case", + "presentation", + "repository", + "demo", + "video", + "run_instructions" + ] + }, + "TemplateName": { + "enum": [ + "case_championship", + "hackathon" + ] + }, + "Field": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "type": { + "$ref": "#/components/schemas/FieldType" + }, + "purpose": { + "$ref": "#/components/schemas/Purpose" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "help_text": { + "type": "string", + "maxLength": 4000 + }, + "required": { + "type": "boolean" + }, + "position": { + "type": "integer", + "minimum": 0 + }, + "options": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + }, + "validation": { + "$ref": "#/components/schemas/Validation" + } + }, + "required": [ + "id", + "type", + "purpose", + "label", + "help_text", + "required", + "position", + "options", + "validation" + ], + "additionalProperties": false + }, + "FieldWrite": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "type": { + "$ref": "#/components/schemas/FieldType" + }, + "purpose": { + "$ref": "#/components/schemas/Purpose" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "help_text": { + "type": "string", + "maxLength": 4000 + }, + "required": { + "type": "boolean" + }, + "position": { + "type": "integer", + "minimum": 0 + }, + "options": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + }, + "validation": { + "$ref": "#/components/schemas/Validation" + } + }, + "required": [ + "id", + "type", + "purpose", + "label", + "help_text", + "required", + "position", + "options", + "validation" + ], + "additionalProperties": false + }, + "Form": { + "type": "object", + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "template": { + "anyOf": [ + { + "$ref": "#/components/schemas/TemplateName" + }, + { + "type": "null" + } + ] + }, + "is_configured": { + "type": "boolean" + }, + "is_snapshot": { + "type": "boolean" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + } + } + }, + "required": [ + "program_id", + "revision", + "template", + "is_configured", + "is_snapshot", + "fields" + ], + "additionalProperties": false + }, + "FormWrite": { + "type": "object", + "properties": { + "expected_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "template": { + "$ref": "#/components/schemas/TemplateName" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldWrite" + } + } + }, + "required": [ + "expected_revision", + "template", + "fields" + ], + "additionalProperties": false + }, + "FormManagement": { + "type": "object", + "properties": { + "form": { + "$ref": "#/components/schemas/Form" + }, + "has_solutions": { + "type": "boolean" + }, + "has_answers": { + "type": "boolean" + }, + "affected_draft_count": { + "type": "integer", + "minimum": 0 + }, + "fields_usage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "answers_count": { + "type": "integer", + "minimum": 0 + }, + "can_delete": { + "type": "boolean" + }, + "can_change_type": { + "type": "boolean" + } + }, + "required": [ + "field_id", + "answers_count", + "can_delete", + "can_change_type" + ], + "additionalProperties": false + } + } + }, + "required": [ + "form", + "has_solutions", + "has_answers", + "affected_draft_count", + "fields_usage" + ], + "additionalProperties": false + }, + "Template": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/TemplateName" + }, + "label": { + "type": "string" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldWrite" + } + } + }, + "required": [ + "key", + "label", + "fields" + ], + "additionalProperties": false + }, + "Templates": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Template" + } + }, + "max_file_size_bytes": { + "const": 104857600 + }, + "files_per_field": { + "const": 1 + } + }, + "required": [ + "results", + "max_file_size_bytes", + "files_per_field" + ], + "additionalProperties": false + }, + "FileRef": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https?://" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "AnswerValue": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/FileRef" + } + ] + }, + "Answer": { + "type": "object", + "properties": { + "field_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "value": { + "$ref": "#/components/schemas/AnswerValue" + } + }, + "required": [ + "field_id", + "value" + ], + "additionalProperties": false + }, + "AnswerPatch": { + "type": "object", + "properties": { + "field_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "value": { + "anyOf": [ + { + "$ref": "#/components/schemas/AnswerValue" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "field_id", + "value" + ], + "additionalProperties": false + }, + "Member": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/User" + }, + "role": { + "anyOf": [ + { + "type": "string", + "maxLength": 128 + }, + { + "type": "null" + } + ] + }, + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] + }, + "joined_at": { + "type": "string", + "format": "date-time" + }, + "is_captain": { + "type": "boolean" + } + }, + "required": [ + "user", + "role", + "specialization", + "joined_at", + "is_captain" + ], + "additionalProperties": false + }, + "Team": { + "type": "object", + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Member" + } + }, + "accepted_count": { + "type": "integer", + "minimum": 0 + }, + "pending_count": { + "type": "integer", + "minimum": 0 + }, + "reserved_count": { + "type": "integer", + "minimum": 0 + }, + "min_size": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ] + }, + "max_size": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ] + }, + "is_frozen": { + "type": "boolean" + } + }, + "required": [ + "members", + "accepted_count", + "pending_count", + "reserved_count", + "min_size", + "max_size", + "is_frozen" + ], + "additionalProperties": false + }, + "Permissions": { + "type": "object", + "properties": { + "can_edit": { + "type": "boolean" + }, + "can_manage_team": { + "type": "boolean" + }, + "can_invite": { + "type": "boolean" + }, + "can_submit": { + "type": "boolean" + }, + "can_leave": { + "type": "boolean", + "description": "Собственный выход принятого участника: false для капитана, сданного решения и отключенного режима. Необязательное совместимое поле; при отсутствии клиент считает false." + } + }, + "required": [ + "can_edit", + "can_manage_team", + "can_invite", + "can_submit" + ], + "additionalProperties": false + }, + "Submission": { + "type": "object", + "properties": { + "required": { + "type": "boolean" + }, + "status": { + "enum": [ + "draft", + "submitted" + ] + }, + "submission_open": { + "type": "boolean" + }, + "deadline": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "submitted_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "required", + "status", + "submission_open", + "deadline", + "submitted_at" + ], + "additionalProperties": false + }, + "Readiness": { + "type": "object", + "properties": { + "is_ready": { + "type": "boolean" + }, + "issues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } + } + }, + "required": [ + "is_ready", + "issues" + ], + "additionalProperties": false + }, + "Solution": { "type": "object", "properties": { "id": { @@ -5828,18 +7782,122 @@ "minimum": 1, "maximum": 9007199254740991 }, - "first_name": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "version": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "maxLength": 256 + }, + "captain_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "team": { + "$ref": "#/components/schemas/Team" + }, + "form": { + "$ref": "#/components/schemas/Form" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Answer" + } + }, + "submission": { + "$ref": "#/components/schemas/Submission" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "readiness": { + "anyOf": [ + { + "$ref": "#/components/schemas/Readiness" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "program_id", + "version", + "name", + "captain_id", + "created_at", + "updated_at", + "team", + "form", + "answers", + "submission", + "permissions", + "readiness" + ], + "additionalProperties": false + }, + "SolutionSummary": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { "type": "string" }, - "last_name": { - "type": "string" + "captain": { + "$ref": "#/components/schemas/User" + }, + "accepted_count": { + "type": "integer", + "minimum": 0 + }, + "case_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": [ + "draft", + "submitted" + ] }, - "avatar_url": { + "submitted_at": { "anyOf": [ { "type": "string", - "format": "uri", - "pattern": "^https?://" + "format": "date-time" }, { "type": "null" @@ -5849,985 +7907,836 @@ }, "required": [ "id", - "first_name", - "last_name", - "avatar_url" + "program_id", + "name", + "captain", + "accepted_count", + "case_value", + "status", + "submitted_at" ], "additionalProperties": false }, - "ProgramRef": { + "CreateSolution": { "type": "object", "properties": { - "id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, "name": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 256 } }, - "required": [ - "id", - "name" - ], + "required": [], "additionalProperties": false }, - "SolutionRef": { + "UpdateSolution": { "type": "object", "properties": { - "kind": { - "const": "solution" - }, - "program_id": { + "expected_version": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 1 }, - "solution_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "expected_form_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" }, "name": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AnswerPatch" + } } }, "required": [ - "kind", - "program_id", - "solution_id", - "name" + "expected_version", + "expected_form_revision" ], - "additionalProperties": false + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "answers" + ] + } + ] }, - "LegacyRef": { + "Submit": { "type": "object", "properties": { - "kind": { - "const": "legacy_project" - }, - "program_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "project_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "program_link_id": { + "expected_version": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 1 }, - "name": { - "type": "string" + "expected_form_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" } }, "required": [ - "kind", - "program_id", - "project_id", - "program_link_id", - "name" + "expected_version", + "expected_form_revision" ], "additionalProperties": false }, - "ApplicationRef": { - "oneOf": [ - { - "$ref": "#/components/schemas/SolutionRef" - }, - { - "$ref": "#/components/schemas/LegacyRef" - } - ] - }, - "Issue": { + "CaptainCommand": { "type": "object", "properties": { - "path": { - "type": "string" - }, - "code": { - "type": "string" - }, - "field_id": { - "anyOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - { - "type": "null" - } - ] + "expected_solution_version": { + "type": "integer", + "minimum": 1 }, - "detail": { - "type": "string" + "new_captain_user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 } }, "required": [ - "path", - "code", - "field_id", - "detail" + "expected_solution_version", + "new_captain_user_id" ], "additionalProperties": false }, - "Error": { + "TeamCommand": { "type": "object", "properties": { - "code": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "errors": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Issue" - } - }, - "meta": { - "type": "object", - "additionalProperties": true + "expected_solution_version": { + "type": "integer", + "minimum": 1 } }, "required": [ - "code", - "detail", - "errors", - "meta" + "expected_solution_version" ], "additionalProperties": false }, - "Context": { + "InvitationPermissions": { "type": "object", "properties": { - "program_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "storage_mode": { - "enum": [ - "legacy_project", - "solution" - ] - }, - "is_registered": { + "can_edit": { "type": "boolean" }, - "is_competitive": { + "can_revoke": { "type": "boolean" }, - "submission_open": { + "can_accept": { "type": "boolean" }, - "submission_deadline": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ] - }, - "can_create": { + "can_decline": { "type": "boolean" - }, - "blocked_reason": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "current_application": { - "anyOf": [ - { - "$ref": "#/components/schemas/ApplicationRef" - }, - { - "type": "null" - } - ] } }, - "required": [ - "program_id", - "storage_mode", - "is_registered", - "is_competitive", - "submission_open", - "submission_deadline", - "can_create", - "blocked_reason", - "current_application" + "required": [ + "can_edit", + "can_revoke", + "can_accept", + "can_decline" ], "additionalProperties": false }, - "Validation": { + "Invitation": { "type": "object", "properties": { - "min_length": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 }, - "max_length": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] + "version": { + "type": "integer", + "minimum": 1 }, - "minimum": { + "program": { + "$ref": "#/components/schemas/ProgramRef" + }, + "solution": { + "$ref": "#/components/schemas/SolutionRef" + }, + "captain": { + "$ref": "#/components/schemas/User" + }, + "recipient": { + "$ref": "#/components/schemas/User" + }, + "role": { "anyOf": [ { - "type": "number" + "type": "string", + "maxLength": 128 }, { "type": "null" } ] }, - "maximum": { + "specialization": { "anyOf": [ { - "type": "number" + "type": "string", + "maxLength": 100 }, { "type": "null" } ] }, - "allowed_extensions": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9]+$" - } - }, - "max_file_size_bytes": { + "motivational_letter": { "anyOf": [ { - "type": "integer", - "minimum": 1, - "maximum": 104857600 + "type": "string", + "maxLength": 4096 }, { "type": "null" } ] - } - }, - "required": [ - "min_length", - "max_length", - "minimum", - "maximum", - "allowed_extensions", - "max_file_size_bytes" - ], - "additionalProperties": false - }, - "FieldType": { - "enum": [ - "text", - "textarea", - "url", - "number", - "select", - "checkbox", - "file", - "radio" - ] - }, - "Purpose": { - "enum": [ - "other", - "case", - "presentation", - "repository", - "demo", - "video", - "run_instructions" - ] - }, - "TemplateName": { - "enum": [ - "case_championship", - "hackathon" - ] - }, - "Field": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "type": { - "$ref": "#/components/schemas/FieldType" }, - "purpose": { - "$ref": "#/components/schemas/Purpose" + "status": { + "enum": [ + "pending", + "accepted", + "declined" + ] }, - "label": { + "created_at": { "type": "string", - "minLength": 1, - "maxLength": 256 + "format": "date-time" }, - "help_text": { + "updated_at": { "type": "string", - "maxLength": 4000 - }, - "required": { - "type": "boolean" - }, - "position": { - "type": "integer", - "minimum": 0 - }, - "options": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 1000 - } + "format": "date-time" }, - "validation": { - "$ref": "#/components/schemas/Validation" + "permissions": { + "$ref": "#/components/schemas/InvitationPermissions" } }, "required": [ "id", - "type", - "purpose", - "label", - "help_text", - "required", - "position", - "options", - "validation" + "version", + "program", + "solution", + "captain", + "recipient", + "role", + "specialization", + "motivational_letter", + "status", + "created_at", + "updated_at", + "permissions" ], "additionalProperties": false }, - "FieldWrite": { + "CreateInvitation": { "type": "object", "properties": { - "id": { + "expected_solution_version": { + "type": "integer", + "minimum": 1 + }, + "recipient_user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { "anyOf": [ { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "type": "string", + "maxLength": 128 }, { "type": "null" } ] }, - "type": { - "$ref": "#/components/schemas/FieldType" - }, - "purpose": { - "$ref": "#/components/schemas/Purpose" - }, - "label": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "help_text": { - "type": "string", - "maxLength": 4000 - }, - "required": { - "type": "boolean" - }, - "position": { - "type": "integer", - "minimum": 0 - }, - "options": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 1000 - } + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] }, - "validation": { - "$ref": "#/components/schemas/Validation" + "motivational_letter": { + "anyOf": [ + { + "type": "string", + "maxLength": 4096 + }, + { + "type": "null" + } + ] } }, - "required": [ - "id", - "type", - "purpose", - "label", - "help_text", - "required", - "position", - "options", - "validation" + "required": [ + "expected_solution_version", + "recipient_user_id" ], "additionalProperties": false }, - "Form": { + "UpdateInvitation": { "type": "object", "properties": { - "program_id": { + "expected_version": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 1 }, - "revision": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" + "expected_solution_version": { + "type": "integer", + "minimum": 1 }, - "template": { + "role": { "anyOf": [ { - "$ref": "#/components/schemas/TemplateName" + "type": "string", + "maxLength": 128 }, { "type": "null" } ] }, - "is_configured": { - "type": "boolean" - }, - "is_snapshot": { - "type": "boolean" + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] }, - "fields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Field" - } + "motivational_letter": { + "anyOf": [ + { + "type": "string", + "maxLength": 4096 + }, + { + "type": "null" + } + ] } }, "required": [ - "program_id", - "revision", - "template", - "is_configured", - "is_snapshot", - "fields" + "expected_version", + "expected_solution_version" ], - "additionalProperties": false - }, - "FormWrite": { - "type": "object", - "properties": { - "expected_revision": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "role" + ] }, - "template": { - "$ref": "#/components/schemas/TemplateName" + { + "required": [ + "specialization" + ] }, - "fields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FieldWrite" - } + { + "required": [ + "motivational_letter" + ] } - }, - "required": [ - "expected_revision", - "template", - "fields" - ], - "additionalProperties": false + ] }, - "FormManagement": { + "InvitationAction": { "type": "object", "properties": { - "form": { - "$ref": "#/components/schemas/Form" - }, - "has_solutions": { - "type": "boolean" - }, - "has_answers": { - "type": "boolean" - }, - "affected_draft_count": { + "expected_version": { "type": "integer", - "minimum": 0 - }, - "fields_usage": { - "type": "array", - "items": { - "type": "object", - "properties": { - "field_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "answers_count": { - "type": "integer", - "minimum": 0 - }, - "can_delete": { - "type": "boolean" - }, - "can_change_type": { - "type": "boolean" - } - }, - "required": [ - "field_id", - "answers_count", - "can_delete", - "can_change_type" - ], - "additionalProperties": false - } + "minimum": 1 } }, "required": [ - "form", - "has_solutions", - "has_answers", - "affected_draft_count", - "fields_usage" + "expected_version" ], "additionalProperties": false }, - "Template": { + "InvitationCreated": { "type": "object", "properties": { - "key": { - "$ref": "#/components/schemas/TemplateName" - }, - "label": { - "type": "string" + "invitation": { + "$ref": "#/components/schemas/Invitation" }, - "fields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FieldWrite" - } + "solution_version": { + "type": "integer", + "minimum": 1 } }, "required": [ - "key", - "label", - "fields" + "invitation", + "solution_version" ], "additionalProperties": false }, - "Templates": { + "InvitationActionResult": { "type": "object", "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Template" - } + "invitation": { + "$ref": "#/components/schemas/Invitation" }, - "max_file_size_bytes": { - "const": 104857600 + "application": { + "anyOf": [ + { + "$ref": "#/components/schemas/SolutionRef" + }, + { + "type": "null" + } + ] }, - "files_per_field": { - "const": 1 - } - }, - "required": [ - "results", - "max_file_size_bytes", - "files_per_field" - ], - "additionalProperties": false - }, - "FileRef": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri", - "pattern": "^https?://" + "solution_version": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ] } }, "required": [ - "url" + "invitation", + "application", + "solution_version" ], "additionalProperties": false }, - "AnswerValue": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "$ref": "#/components/schemas/FileRef" - } - ] - }, - "Answer": { + "MutationReceipt": { "type": "object", "properties": { - "field_id": { + "solution_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "value": { - "$ref": "#/components/schemas/AnswerValue" + "solution_version": { + "type": "integer", + "minimum": 1 + }, + "updated_at": { + "type": "string", + "format": "date-time" } }, "required": [ - "field_id", - "value" + "solution_id", + "solution_version", + "updated_at" ], "additionalProperties": false }, - "AnswerPatch": { + "SolutionPage": { "type": "object", "properties": { - "field_id": { + "count": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 0 }, - "value": { + "next": { "anyOf": [ { - "$ref": "#/components/schemas/AnswerValue" + "type": "string" + }, + { + "type": "null" + } + ] + }, + "previous": { + "anyOf": [ + { + "type": "string" }, { "type": "null" } ] + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionSummary" + } } }, "required": [ - "field_id", - "value" + "count", + "next", + "previous", + "results" ], "additionalProperties": false }, - "Member": { + "InvitationPage": { "type": "object", "properties": { - "user": { - "$ref": "#/components/schemas/User" + "count": { + "type": "integer", + "minimum": 0 }, - "role": { + "next": { "anyOf": [ { - "type": "string", - "maxLength": 128 + "type": "string" }, { "type": "null" } ] }, - "specialization": { + "previous": { "anyOf": [ { - "type": "string", - "maxLength": 100 + "type": "string" }, { "type": "null" } ] }, - "joined_at": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "required": [ + "count", + "next", + "previous", + "results" + ], + "additionalProperties": false + }, + "UploadResponse": { + "type": "object", + "properties": { + "url": { "type": "string", - "format": "date-time" - }, - "is_captain": { - "type": "boolean" + "format": "uri", + "pattern": "^https?://" } }, "required": [ - "user", - "role", - "specialization", - "joined_at", - "is_captain" + "url" ], "additionalProperties": false }, - "Team": { + "SubmissionSnapshot": { "type": "object", + "additionalProperties": false, + "required": [ + "name", + "captain_id", + "members", + "form", + "answers", + "submitted_at" + ], "properties": { + "name": { + "type": "string", + "maxLength": 256 + }, + "captain_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, "members": { "type": "array", "items": { "$ref": "#/components/schemas/Member" } }, - "accepted_count": { - "type": "integer", - "minimum": 0 + "form": { + "$ref": "#/components/schemas/Form" }, - "pending_count": { - "type": "integer", - "minimum": 0 + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Answer" + } }, - "reserved_count": { + "submitted_at": { + "type": "string", + "format": "date-time" + } + } + }, + "RatingCriterion": { + "type": "object", + "properties": { + "id": { "type": "integer", - "minimum": 0 + "minimum": 1, + "maximum": 9007199254740991 }, - "min_size": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, - { - "type": "null" - } + "name": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" ] }, - "max_size": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, - { - "type": "null" - } + "type": { + "type": "string", + "enum": [ + "int", + "float", + "bool", + "str" ] }, - "is_frozen": { - "type": "boolean" + "min_value": { + "type": [ + "number", + "null" + ] + }, + "max_value": { + "type": [ + "number", + "null" + ] + }, + "value": { + "type": [ + "string", + "null" + ], + "maxLength": 50 } }, "required": [ - "members", - "accepted_count", - "pending_count", - "reserved_count", - "min_size", - "max_size", - "is_frozen" + "id", + "name", + "description", + "type", + "min_value", + "max_value", + "value" ], "additionalProperties": false }, - "Permissions": { + "RatingValue": { "type": "object", "properties": { - "can_edit": { - "type": "boolean" - }, - "can_manage_team": { - "type": "boolean" - }, - "can_invite": { - "type": "boolean" + "criterion_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 }, - "can_submit": { - "type": "boolean" + "value": { + "type": [ + "string", + "null" + ], + "maxLength": 50 + } + }, + "required": [ + "criterion_id", + "value" + ], + "additionalProperties": false + }, + "RateSolution": { + "type": "object", + "properties": { + "expected_criteria_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" }, - "can_leave": { - "type": "boolean", - "description": "Собственный выход принятого участника: false для капитана, сданного решения и отключенного режима. Необязательное совместимое поле; при отсутствии клиент считает false." + "scores": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RatingValue" + }, + "minItems": 1 } }, "required": [ - "can_edit", - "can_manage_team", - "can_invite", - "can_submit" + "expected_criteria_revision", + "scores" ], "additionalProperties": false }, - "Submission": { + "RatingSummary": { "type": "object", "properties": { - "required": { - "type": "boolean" + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "status": { - "enum": [ - "draft", - "submitted" + "case_name": { + "type": [ + "string", + "null" ] }, - "submission_open": { + "assigned": { "type": "boolean" }, - "deadline": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ] + "is_completed": { + "type": "boolean" }, - "submitted_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } + "can_rate": { + "type": "boolean" + }, + "blocked_reason": { + "type": [ + "string", + "null" ] } }, "required": [ - "required", - "status", - "submission_open", - "deadline", - "submitted_at" + "application", + "case_name", + "assigned", + "is_completed", + "can_rate", + "blocked_reason" ], "additionalProperties": false }, - "Readiness": { + "RatingPage": { "type": "object", "properties": { - "is_ready": { - "type": "boolean" + "count": { + "type": "integer", + "minimum": 0 }, - "issues": { + "next": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "previous": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "results": { "type": "array", "items": { - "$ref": "#/components/schemas/Issue" + "$ref": "#/components/schemas/RatingSummary" } } }, "required": [ - "is_ready", - "issues" + "count", + "next", + "previous", + "results" ], "additionalProperties": false }, - "Solution": { + "RatingFrame": { "type": "object", "properties": { - "id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "program_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "program": { + "$ref": "#/components/schemas/ProgramRef" }, - "version": { - "type": "integer", - "minimum": 1 + "solution": { + "$ref": "#/components/schemas/Solution" }, - "name": { + "criteria_revision": { "type": "string", - "maxLength": 256 + "pattern": "^[a-f0-9]{64}$" }, - "captain_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "criteria": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RatingCriterion" + } }, - "created_at": { - "type": "string", - "format": "date-time" + "managed_evaluation": { + "type": "boolean" }, - "updated_at": { - "type": "string", - "format": "date-time" + "can_rate": { + "type": "boolean" }, - "team": { - "$ref": "#/components/schemas/Team" + "blocked_reason": { + "type": [ + "string", + "null" + ] }, - "form": { - "$ref": "#/components/schemas/Form" + "is_completed": { + "type": "boolean" }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Answer" - } + "rated_count": { + "type": "integer", + "minimum": 0 }, - "submission": { - "$ref": "#/components/schemas/Submission" + "max_rates": { + "type": [ + "integer", + "null" + ], + "minimum": 0 }, - "permissions": { - "$ref": "#/components/schemas/Permissions" + "required_evaluators": { + "type": "integer", + "minimum": 1 }, - "readiness": { - "anyOf": [ - { - "$ref": "#/components/schemas/Readiness" - }, - { - "type": "null" - } - ] + "deadline": { + "type": [ + "string", + "null" + ], + "format": "date-time" } }, "required": [ - "id", - "program_id", - "version", - "name", - "captain_id", - "created_at", - "updated_at", - "team", - "form", - "answers", - "submission", - "permissions", - "readiness" + "program", + "solution", + "criteria_revision", + "criteria", + "managed_evaluation", + "can_rate", + "blocked_reason", + "is_completed", + "rated_count", + "max_rates", + "required_evaluators", + "deadline" ], "additionalProperties": false }, - "SolutionSummary": { + "AssignmentOption": { "type": "object", "properties": { - "id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "program_id": { + "solution_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -6835,621 +8744,711 @@ "name": { "type": "string" }, - "captain": { - "$ref": "#/components/schemas/User" + "case_name": { + "type": [ + "string", + "null" + ] }, - "accepted_count": { - "type": "integer", - "minimum": 0 + "submitted": { + "type": "boolean" }, - "case_value": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "assigned": { + "type": "boolean" }, - "status": { - "enum": [ - "draft", - "submitted" - ] + "protected": { + "type": "boolean" }, - "submitted_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ] + "is_completed": { + "type": "boolean" + }, + "own_team": { + "type": "boolean" + }, + "eligible": { + "type": "boolean" } }, "required": [ - "id", - "program_id", + "solution_id", "name", - "captain", - "accepted_count", - "case_value", - "status", - "submitted_at" + "case_name", + "submitted", + "assigned", + "protected", + "is_completed", + "own_team", + "eligible" ], "additionalProperties": false }, - "CreateSolution": { + "AssignmentOptions": { "type": "object", "properties": { - "name": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "expert_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "revision": { "type": "string", - "minLength": 1, - "maxLength": 256 + "pattern": "^[a-f0-9]{64}$" + }, + "solutions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssignmentOption" + } } }, - "required": [], + "required": [ + "program_id", + "expert_id", + "revision", + "solutions" + ], "additionalProperties": false }, - "UpdateSolution": { + "SolutionAssignmentsWrite": { "type": "object", "properties": { - "expected_version": { - "type": "integer", - "minimum": 1 - }, - "expected_form_revision": { + "revision": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "answers": { + "solution_ids": { "type": "array", "items": { - "$ref": "#/components/schemas/AnswerPatch" - } + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "uniqueItems": true } }, "required": [ - "expected_version", - "expected_form_revision" + "revision", + "solution_ids" ], - "additionalProperties": false, - "anyOf": [ - { - "required": [ - "name" + "additionalProperties": false + }, + "SolutionCasePool": { + "type": "object", + "properties": { + "case_name": { + "type": [ + "string", + "null" ] }, - { - "required": [ - "answers" - ] + "expert_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "uniqueItems": true, + "maxItems": 1000 } - ] + }, + "required": [ + "case_name", + "expert_ids" + ], + "additionalProperties": false }, - "Submit": { + "SolutionAssignmentOverride": { "type": "object", "properties": { - "expected_version": { + "solution_id": { "type": "integer", - "minimum": 1 + "minimum": 1, + "maximum": 9007199254740991 }, - "expected_form_revision": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" + "expert_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "uniqueItems": true, + "maxItems": 1000 } }, "required": [ - "expected_version", - "expected_form_revision" + "solution_id", + "expert_ids" ], "additionalProperties": false }, - "CaptainCommand": { + "SolutionAllocationWrite": { "type": "object", "properties": { - "expected_solution_version": { - "type": "integer", - "minimum": 1 + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "expert_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "uniqueItems": true, + "maxItems": 1000 + }, + "solution_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "uniqueItems": true, + "maxItems": 10000 + }, + "case_pools": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionCasePool" + }, + "maxItems": 1000 }, - "new_captain_user_id": { + "seed": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 0, + "maximum": 2147483647 + }, + "previous_plan_token": { + "type": "string", + "maxLength": 500000 + }, + "assignment_overrides": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionAssignmentOverride" + }, + "maxItems": 10000 } }, "required": [ - "expected_solution_version", - "new_captain_user_id" + "revision", + "expert_ids" ], "additionalProperties": false }, - "TeamCommand": { + "SolutionPlannedAssignment": { "type": "object", "properties": { - "expected_solution_version": { + "solution_id": { "type": "integer", - "minimum": 1 - } - }, - "required": [ - "expected_solution_version" - ], - "additionalProperties": false - }, - "InvitationPermissions": { - "type": "object", - "properties": { - "can_edit": { - "type": "boolean" + "minimum": 1, + "maximum": 9007199254740991 }, - "can_revoke": { - "type": "boolean" + "solution_name": { + "type": "string" }, - "can_accept": { - "type": "boolean" + "expert_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 }, - "can_decline": { - "type": "boolean" + "expert_name": { + "type": "string" + }, + "case_name": { + "type": [ + "string", + "null" + ] } }, "required": [ - "can_edit", - "can_revoke", - "can_accept", - "can_decline" + "solution_id", + "solution_name", + "expert_id", + "expert_name", + "case_name" ], "additionalProperties": false }, - "Invitation": { + "SolutionAssignmentShortage": { "type": "object", "properties": { - "id": { + "solution_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "version": { + "solution_name": { + "type": "string" + }, + "missing": { "type": "integer", "minimum": 1 }, - "program": { - "$ref": "#/components/schemas/ProgramRef" - }, - "solution": { - "$ref": "#/components/schemas/SolutionRef" - }, - "captain": { - "$ref": "#/components/schemas/User" - }, - "recipient": { - "$ref": "#/components/schemas/User" - }, - "role": { - "anyOf": [ - { - "type": "string", - "maxLength": 128 - }, - { - "type": "null" - } - ] - }, - "specialization": { - "anyOf": [ - { - "type": "string", - "maxLength": 100 - }, - { - "type": "null" - } - ] - }, - "motivational_letter": { - "anyOf": [ - { - "type": "string", - "maxLength": 4096 - }, - { - "type": "null" - } - ] - }, - "status": { - "enum": [ - "pending", - "accepted", - "declined" + "case_name": { + "type": [ + "string", + "null" ] - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "permissions": { - "$ref": "#/components/schemas/InvitationPermissions" } }, "required": [ - "id", - "version", - "program", - "solution", - "captain", - "recipient", - "role", - "specialization", - "motivational_letter", - "status", - "created_at", - "updated_at", - "permissions" + "solution_id", + "solution_name", + "missing", + "case_name" ], "additionalProperties": false }, - "CreateInvitation": { + "SolutionExpertLoad": { "type": "object", "properties": { - "expected_solution_version": { - "type": "integer", - "minimum": 1 - }, - "recipient_user_id": { + "expert_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "role": { - "anyOf": [ - { - "type": "string", - "maxLength": 128 - }, - { - "type": "null" - } - ] + "name": { + "type": "string" }, - "specialization": { - "anyOf": [ - { - "type": "string", - "maxLength": 100 - }, - { - "type": "null" - } - ] + "before": { + "type": "integer", + "minimum": 0 }, - "motivational_letter": { - "anyOf": [ - { - "type": "string", - "maxLength": 4096 - }, - { - "type": "null" - } - ] + "after": { + "type": "integer", + "minimum": 0 + }, + "assigned_before": { + "type": "integer", + "minimum": 0 + }, + "assigned_after": { + "type": "integer", + "minimum": 0 } }, "required": [ - "expected_solution_version", - "recipient_user_id" + "expert_id", + "name", + "before", + "after", + "assigned_before", + "assigned_after" ], "additionalProperties": false }, - "UpdateInvitation": { + "SolutionAllocationPlan": { "type": "object", "properties": { - "expected_version": { - "type": "integer", - "minimum": 1 + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" }, - "expected_solution_version": { - "type": "integer", - "minimum": 1 + "plan_token": { + "type": "string", + "maxLength": 500000 }, - "role": { - "anyOf": [ - { - "type": "string", - "maxLength": 128 - }, - { - "type": "null" - } - ] + "assignments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionPlannedAssignment" + } }, - "specialization": { - "anyOf": [ - { - "type": "string", - "maxLength": 100 - }, - { - "type": "null" - } - ] + "shortages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionAssignmentShortage" + } }, - "motivational_letter": { - "anyOf": [ - { - "type": "string", - "maxLength": 4096 - }, - { - "type": "null" - } - ] + "can_reroll": { + "type": "boolean" + }, + "load": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionExpertLoad" + } } }, "required": [ - "expected_version", - "expected_solution_version" + "revision", + "plan_token", + "assignments", + "shortages", + "can_reroll", + "load" ], - "additionalProperties": false, - "anyOf": [ - { - "required": [ - "role" - ] - }, - { - "required": [ - "specialization" - ] - }, - { - "required": [ - "motivational_letter" - ] + "additionalProperties": false + }, + "SolutionAllocationApply": { + "type": "object", + "properties": { + "plan_token": { + "type": "string", + "maxLength": 500000 } - ] + }, + "required": [ + "plan_token" + ], + "additionalProperties": false }, - "InvitationAction": { + "SolutionAssignmentReceipt": { "type": "object", "properties": { - "expected_version": { + "program_id": { "type": "integer", - "minimum": 1 + "minimum": 1, + "maximum": 9007199254740991 + }, + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "created": { + "type": "integer", + "minimum": 0 } }, "required": [ - "expected_version" + "program_id", + "revision", + "created" ], "additionalProperties": false }, - "InvitationCreated": { + "SolutionQuotaWrite": { "type": "object", "properties": { - "invitation": { - "$ref": "#/components/schemas/Invitation" + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" }, - "solution_version": { + "required_evaluators": { "type": "integer", - "minimum": 1 + "minimum": 1, + "maximum": 1000 } }, "required": [ - "invitation", - "solution_version" + "revision", + "required_evaluators" ], "additionalProperties": false }, - "InvitationActionResult": { + "SolutionQuota": { "type": "object", "properties": { - "invitation": { - "$ref": "#/components/schemas/Invitation" + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" }, - "application": { - "anyOf": [ - { - "$ref": "#/components/schemas/SolutionRef" - }, - { - "type": "null" - } - ] + "required_evaluators": { + "type": "integer", + "minimum": 1, + "maximum": 1000 }, - "solution_version": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, - { - "type": "null" - } - ] + "submitted_solutions": { + "type": "integer", + "minimum": 0 + }, + "solutions_missing_experts": { + "type": "integer", + "minimum": 0 + }, + "ready_before": { + "type": "integer", + "minimum": 0 + }, + "ready_after": { + "type": "integer", + "minimum": 0 + }, + "missing_assignments": { + "type": "integer", + "minimum": 0 + }, + "surplus_assignments": { + "type": "integer", + "minimum": 0 } }, "required": [ - "invitation", - "application", - "solution_version" + "revision", + "required_evaluators", + "submitted_solutions", + "solutions_missing_experts", + "ready_before", + "ready_after", + "missing_assignments", + "surplus_assignments" ], "additionalProperties": false }, - "MutationReceipt": { + "ResultCriterion": { "type": "object", "properties": { - "solution_id": { + "id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "solution_version": { - "type": "integer", - "minimum": 1 + "name": { + "type": "string" }, - "updated_at": { + "description": { + "type": [ + "string", + "null" + ] + }, + "type": { "type": "string", - "format": "date-time" + "enum": [ + "int", + "float", + "bool", + "str" + ] + }, + "min_value": { + "type": [ + "number", + "null" + ] + }, + "max_value": { + "type": [ + "number", + "null" + ] } }, "required": [ - "solution_id", - "solution_version", - "updated_at" + "id", + "name", + "description", + "type", + "min_value", + "max_value" ], "additionalProperties": false }, - "SolutionPage": { + "ResultExpert": { "type": "object", "properties": { - "count": { + "user_id": { "type": "integer", - "minimum": 0 + "minimum": 1, + "maximum": 9007199254740991 }, - "next": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "avatar": { + "type": [ + "string", + "null" ] }, - "previous": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } + "complete": { + "type": "boolean" + }, + "total": { + "type": [ + "number", + "null" ] }, - "results": { + "values": { "type": "array", "items": { - "$ref": "#/components/schemas/SolutionSummary" + "$ref": "#/components/schemas/RatingValue" } } }, "required": [ - "count", - "next", - "previous", - "results" + "user_id", + "name", + "email", + "avatar", + "complete", + "total", + "values" ], "additionalProperties": false }, - "InvitationPage": { + "SolutionResult": { "type": "object", "properties": { - "count": { + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "solution_id": { "type": "integer", - "minimum": 0 + "minimum": 1, + "maximum": 9007199254740991 }, - "next": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } + "name": { + "type": "string" + }, + "case_name": { + "type": [ + "string", + "null" ] }, - "previous": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } + "completed": { + "type": "integer", + "minimum": 0 + }, + "required": { + "type": "integer", + "minimum": 1 + }, + "evaluation_complete": { + "type": "boolean" + }, + "ready": { + "type": "boolean" + }, + "score": { + "type": [ + "number", + "null" ] }, - "results": { + "place": { + "type": [ + "integer", + "null" + ], + "minimum": 1 + }, + "averages": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "experts": { "type": "array", "items": { - "$ref": "#/components/schemas/Invitation" + "$ref": "#/components/schemas/ResultExpert" } } }, "required": [ - "count", - "next", - "previous", - "results" + "application", + "solution_id", + "name", + "case_name", + "completed", + "required", + "evaluation_complete", + "ready", + "score", + "place", + "averages", + "experts" ], "additionalProperties": false }, - "UploadResponse": { + "SolutionResultGroup": { "type": "object", "properties": { - "url": { - "type": "string", - "format": "uri", - "pattern": "^https?://" + "case_name": { + "type": [ + "string", + "null" + ] + }, + "solutions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionResult" + } } }, "required": [ - "url" + "case_name", + "solutions" ], "additionalProperties": false }, - "SubmissionSnapshot": { + "SolutionResults": { "type": "object", - "additionalProperties": false, - "required": [ - "name", - "captain_id", - "members", - "form", - "answers", - "submitted_at" - ], "properties": { - "name": { - "type": "string", - "maxLength": 256 - }, - "captain_id": { + "program_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "members": { + "program_name": { + "type": "string" + }, + "generated_at": { + "type": "string", + "format": "date-time" + }, + "required_evaluators": { + "type": "integer", + "minimum": 1 + }, + "maximum": { + "type": [ + "number", + "null" + ] + }, + "has_numeric_criteria": { + "type": "boolean" + }, + "case_options": { "type": "array", "items": { - "$ref": "#/components/schemas/Member" + "type": "string" } }, - "form": { - "$ref": "#/components/schemas/Form" - }, - "answers": { + "groups": { "type": "array", "items": { - "$ref": "#/components/schemas/Answer" + "$ref": "#/components/schemas/SolutionResultGroup" } }, - "submitted_at": { - "type": "string", - "format": "date-time" + "criteria": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResultCriterion" + } } - } + }, + "required": [ + "program_id", + "program_name", + "generated_at", + "required_evaluators", + "maximum", + "has_numeric_criteria", + "case_options", + "groups", + "criteria" + ], + "additionalProperties": false } } } diff --git a/docs/product/program-solutions-fe07-api.md b/docs/product/program-solutions-fe07-api.md new file mode 100644 index 000000000..a08ca2878 --- /dev/null +++ b/docs/product/program-solutions-fe07-api.md @@ -0,0 +1,15 @@ + + +# FE07: подключение контрактов экспертной оценки + +Статус: API-слой подготовлен, экспертный UI еще не реализован. D01 расположен по адресу http://127.0.0.1:8769/expert-v1/ и ожидает явного согласования пользователя. Согласованный D00 v3 покрывает редактор, команду, форму и сдачу; он не является согласованием экспертных экранов. + +Контракт C00 1.3.0 синхронизирован с backend: 72 схемы, 32 операции всего, 55 независимых сценариев. Добавлены десять операций: список работ эксперта, frame, сохранение оценки, чтение/запись назначений, предпросмотр/применение распределения, предпросмотр/сохранение N и результаты организатора. + +Изменения: generated model/fixtures; SolutionRepositoryPort → SolutionRepository → SolutionHttpAdapter; десять use cases с Result и существующим error converter. Все ID проверяются как положительные safe integers. Query-параметры получают явный snake_case; точные значения кейса, 0 и false сохраняются. Оценки используют string/null, критерий ошибки хранится в criterionId отдельно от fieldId. Подтвержденные criteria revision и подписанный plan token передаются без подмены. Ошибки 409/400 возвращаются вызывающему коду; автоматического повторного POST/PUT нет. Project DTO/ID и старые маршруты не используются вместо решения. + +Checked by tests: полный Vitest — 2562 PASS в 466 файлах; десять новых HTTP wire tests с реальным CamelcaseInterceptor проверяют обе стороны десяти методов, роли ID, точные пути, query, string 0/False/null, ошибки критериев, conflict metadata, пустое снятие назначений, seed=0, case pools, подписанный план и квоту. Strict type checks PASS, включая негативные примеры без revision, boolean вместо wire value и projectIds вместо контракта распределения. Checked by build: Angular development build PASS. ESLint: 0 errors, 6 прежних warnings. Полная проверка форматирования выполняется locked Prettier 3.8.3 с LF, как в Linux CI; обычный Windows checkout содержит CRLF и дает посторонние style предупреждения без изменения содержания. + +Checked in browser: D01 prototype отдельно проверен на 320/390/768/1440, состояния и keyboard/focus сохранены в design-handoff. Product UI нового экспертного flow в браузере еще не проверялся, поскольку его реализация ожидает согласования. Backend полный HTTP cycle проверен на локальных КЧ и хакатоне; это отдельный уровень evidence, не frontend browser acceptance. + +Осталось: экспертный кабинет/просмотр/форма/результаты и административные экраны после design approval, общие входы BE08/FE08, аналитика/экспорт, QA01/REL01. FE07 и общая новая сущность не отмечаются завершенными. diff --git a/projects/social_platform/src/app/api/solution/solution-error.ts b/projects/social_platform/src/app/api/solution/solution-error.ts index eb177d392..8304c8ef7 100644 --- a/projects/social_platform/src/app/api/solution/solution-error.ts +++ b/projects/social_platform/src/app/api/solution/solution-error.ts @@ -31,6 +31,10 @@ export function solutionError(error: unknown): SolutionApiError { code: row["code"], detail: row["detail"], fieldId: typeof row["field_id"] === "number" ? row["field_id"] : null, + ...(typeof row["criterion_id"] === "number" && + Number.isSafeInteger(row["criterion_id"]) + ? { criterionId: row["criterion_id"] } + : {}), }, ]; }) diff --git a/projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts new file mode 100644 index 000000000..312903bb8 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts @@ -0,0 +1,234 @@ +/** @format */ +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom } from "rxjs"; +import snakecaseKeys from "snakecase-keys"; +import { CamelcaseInterceptor } from "@core/lib/interceptors/camelcase.interceptor"; +import { API_URL } from "@core/lib/providers"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { SolutionRepository } from "@infrastructure/repository/solution/solution.repository"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { + GetSolutionRatingsUseCase, + GetSolutionRatingUseCase, + RateSolutionUseCase, + GetSolutionExpertAssignmentsUseCase, + SaveSolutionExpertAssignmentsUseCase, + PreviewSolutionAllocationUseCase, + ApplySolutionAllocationUseCase, + PreviewSolutionQuotaUseCase, + SaveSolutionQuotaUseCase, + GetSolutionResultsUseCase, +} from "./use-cases/solution-evaluation.use-cases"; + +const example = (name: string) => + solutionContractScenarios.find(row => row.name === name)!.response; +const root = "https://local.test/programs/12/"; + +describe("C00 solution evaluation wire contract", () => { + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: API_URL, useValue: "https://local.test" }, + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: LoggerService, useValue: { warn: vi.fn() } }, + { provide: SolutionRepositoryPort, useExisting: SolutionRepository }, + ], + }), + ); + afterEach(() => TestBed.inject(HttpTestingController).verify()); + + it("encodes filters once in snake case and preserves false, zero and exact case names", async () => { + const pending = firstValueFrom( + TestBed.inject(GetSolutionRatingsUseCase).execute(12, { + search: "API_v2", + caseScope: "selected", + caseName: "API_v2 & ML", + status: "pending", + assignedOnly: false, + limit: 20, + offset: 0, + }), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + request => request.url === root + "solution-evaluation/", + ); + expect(req.request.method).toBe("GET"); + expect(req.request.params.keys()).toEqual([ + "search", + "case_scope", + "case_name", + "status", + "assigned_only", + "limit", + "offset", + ]); + expect(req.request.params.get("case_name")).toBe("API_v2 & ML"); + expect(req.request.params.get("assigned_only")).toBe("false"); + expect(req.request.params.get("offset")).toBe("0"); + const expected = example("evaluation_list"); + req.flush(snakecaseKeys(expected, { deep: true })); + expect(await pending).toEqual({ ok: true, value: expected }); + }); + + it("reads a real solution frame without projecting it into a Project DTO", async () => { + const pending = firstValueFrom(TestBed.inject(GetSolutionRatingUseCase).execute(12, 301)); + const req = TestBed.inject(HttpTestingController).expectOne(root + "solutions/301/evaluation/"); + const expected = example("evaluation_frame"); + req.flush(snakecaseKeys(expected, { deep: true })); + expect(await pending).toEqual({ ok: true, value: expected }); + }); + + it("saves exactly the confirmed criteria revision and string zero/False/null", async () => { + const command = { + expectedCriteriaRevision: "a".repeat(64), + scores: [ + { criterionId: 21, value: "0" }, + { criterionId: 22, value: "False" }, + { criterionId: 23, value: null }, + ], + }; + const pending = firstValueFrom(TestBed.inject(RateSolutionUseCase).execute(12, 301, command)); + const req = TestBed.inject(HttpTestingController).expectOne(root + "solutions/301/evaluation/"); + expect(req.request.method).toBe("POST"); + expect(req.request.body).toEqual(snakecaseKeys(command, { deep: true })); + const expected = example("evaluation_saved"); + req.flush(snakecaseKeys(expected, { deep: true })); + expect(await pending).toEqual({ ok: true, value: expected }); + }); + + it("keeps criterion errors separate from form field errors and never retries a write", async () => { + const pending = firstValueFrom( + TestBed.inject(RateSolutionUseCase).execute(12, 301, { + expectedCriteriaRevision: "a".repeat(64), + scores: [{ criterionId: 21, value: "NaN" }], + }), + ); + const req = TestBed.inject(HttpTestingController).expectOne(root + "solutions/301/evaluation/"); + const expected = example("evaluation_invalid_value"); + req.flush(snakecaseKeys(expected, { deep: true }), { status: 400, statusText: "Bad Request" }); + expect(await pending).toEqual({ ok: false, error: expected }); + TestBed.inject(HttpTestingController).expectNone(root + "solutions/301/evaluation/"); + }); + + it("returns conflict metadata for caller-driven refresh without a second POST", async () => { + const pending = firstValueFrom( + TestBed.inject(RateSolutionUseCase).execute(12, 301, { + expectedCriteriaRevision: "a".repeat(64), + scores: [{ criterionId: 21, value: "1" }], + }), + ); + const req = TestBed.inject(HttpTestingController).expectOne(root + "solutions/301/evaluation/"); + const expected = example("evaluation_criteria_changed"); + req.flush(snakecaseKeys(expected, { deep: true }), { status: 409, statusText: "Conflict" }); + expect(await pending).toEqual({ ok: false, error: expected }); + }); + + it("uses actual Expert IDs and solution IDs in read and full assignment replacement", async () => { + const url = root + "management/solution-evaluation/experts/91/assignments/"; + const read = firstValueFrom( + TestBed.inject(GetSolutionExpertAssignmentsUseCase).execute(12, 91), + ); + const expectedRead = example("solution_assignment_options"); + TestBed.inject(HttpTestingController) + .expectOne(url) + .flush(snakecaseKeys(expectedRead, { deep: true })); + expect(await read).toEqual({ ok: true, value: expectedRead }); + const command = { revision: "b".repeat(64), solutionIds: [] }; + const saved = firstValueFrom( + TestBed.inject(SaveSolutionExpertAssignmentsUseCase).execute(12, 91, command), + ); + const req = TestBed.inject(HttpTestingController).expectOne(url); + expect(req.request.method).toBe("PUT"); + expect(req.request.body).toEqual({ revision: command.revision, solution_ids: [] }); + const expected = example("solution_assignments_saved"); + req.flush(snakecaseKeys(expected, { deep: true })); + expect(await saved).toEqual({ ok: true, value: expected }); + }); + + it("rejects malformed route IDs before any HTTP request", async () => { + const read = TestBed.inject(GetSolutionRatingUseCase); + for (const id of [0, -1, NaN, Infinity, 1.5, Number.MAX_SAFE_INTEGER + 1]) { + const result = await firstValueFrom(read.execute(12, id)); + expect(result).toMatchObject({ ok: false, error: { code: "invalid_id" } }); + } + expect( + await firstValueFrom(TestBed.inject(GetSolutionRatingsUseCase).execute(0)), + ).toMatchObject({ ok: false, error: { code: "invalid_id" } }); + expect( + await firstValueFrom(TestBed.inject(GetSolutionExpertAssignmentsUseCase).execute(12, 0)), + ).toMatchObject({ ok: false, error: { code: "invalid_id" } }); + TestBed.inject(HttpTestingController).expectNone(() => true); + }); + + it("previews with exact case pools and applies only the returned signed plan token", async () => { + const command = { + revision: "d".repeat(64), + expertIds: [91], + solutionIds: [301], + casePools: [{ caseName: "API_v2", expertIds: [91] }], + seed: 0, + assignmentOverrides: [{ solutionId: 301, expertIds: [91] }], + }; + const pending = firstValueFrom( + TestBed.inject(PreviewSolutionAllocationUseCase).execute(12, command), + ); + const url = root + "management/solution-evaluation/allocation/"; + const req = TestBed.inject(HttpTestingController).expectOne(url); + expect(req.request.method).toBe("POST"); + expect(req.request.body).toEqual(snakecaseKeys(command, { deep: true })); + const expected = example("solution_allocation_preview"); + req.flush(snakecaseKeys(expected, { deep: true })); + expect(await pending).toEqual({ ok: true, value: expected }); + const applied = firstValueFrom( + TestBed.inject(ApplySolutionAllocationUseCase).execute(12, "signed-plan"), + ); + const apply = TestBed.inject(HttpTestingController).expectOne(url + "apply/"); + expect(apply.request.body).toEqual({ plan_token: "signed-plan" }); + const receipt = example("solution_allocation_applied"); + apply.flush(snakecaseKeys(receipt, { deep: true })); + expect(await applied).toEqual({ ok: true, value: receipt }); + }); + + it("previews quota via query and saves its revision without resending the preview body", async () => { + const url = root + "management/solution-evaluation/quota/"; + const pending = firstValueFrom(TestBed.inject(PreviewSolutionQuotaUseCase).execute(12, 2)); + const preview = TestBed.inject(HttpTestingController).expectOne(url + "?required_evaluators=2"); + expect(preview.request.method).toBe("GET"); + const expected = example("solution_quota_preview"); + preview.flush(snakecaseKeys(expected, { deep: true })); + expect(await pending).toEqual({ ok: true, value: expected }); + const command = { revision: "d".repeat(64), requiredEvaluators: 2 }; + const saved = firstValueFrom(TestBed.inject(SaveSolutionQuotaUseCase).execute(12, command)); + const req = TestBed.inject(HttpTestingController).expectOne(url); + expect(req.request.method).toBe("PUT"); + expect(req.request.body).toEqual(snakecaseKeys(command, { deep: true })); + req.flush(snakecaseKeys(expected, { deep: true })); + expect(await saved).toEqual({ ok: true, value: expected }); + }); + + it("reads organizer results with complete/pending semantics and real solution references", async () => { + const pending = firstValueFrom( + TestBed.inject(GetSolutionResultsUseCase).execute(12, { + search: "Решение", + caseScope: "selected", + caseName: "API_v2", + order: "asc", + status: "complete", + }), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + request => request.url === root + "management/solution-results/", + ); + expect(req.request.params.get("case_scope")).toBe("selected"); + expect(req.request.params.get("case_name")).toBe("API_v2"); + expect(req.request.params.get("status")).toBe("complete"); + const expected = example("solution_results"); + req.flush(snakecaseKeys(expected, { deep: true })); + expect(await pending).toEqual({ ok: true, value: expected }); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts new file mode 100644 index 000000000..a9ba397d5 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts @@ -0,0 +1,93 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { + SolutionRepositoryPort, + SolutionRatingQuery, + SolutionResultsQuery, +} from "@domain/solution/ports/solution.repository.port"; +import { + RateSolutionCommand, + SolutionAssignmentsWrite, + SolutionAllocationWrite, + SolutionQuotaWrite, +} from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class PreviewSolutionAllocationUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionAllocationWrite) { + return solutionRequestResult(() => this.repository.previewAllocation(programId, command)); + } +} +@Injectable({ providedIn: "root" }) +export class ApplySolutionAllocationUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, planToken: string) { + return solutionRequestResult(() => this.repository.applyAllocation(programId, planToken)); + } +} +@Injectable({ providedIn: "root" }) +export class PreviewSolutionQuotaUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, requiredEvaluators: number) { + return solutionRequestResult(() => this.repository.previewQuota(programId, requiredEvaluators)); + } +} +@Injectable({ providedIn: "root" }) +export class SaveSolutionQuotaUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionQuotaWrite) { + return solutionRequestResult(() => this.repository.saveQuota(programId, command)); + } +} +@Injectable({ providedIn: "root" }) +export class GetSolutionResultsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionResultsQuery) { + return solutionRequestResult(() => this.repository.results(programId, query)); + } +} + +@Injectable({ providedIn: "root" }) +export class GetSolutionRatingsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionRatingQuery) { + return solutionRequestResult(() => this.repository.ratings(programId, query)); + } +} + +@Injectable({ providedIn: "root" }) +export class GetSolutionRatingUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number) { + return solutionRequestResult(() => this.repository.rating(programId, solutionId)); + } +} + +@Injectable({ providedIn: "root" }) +export class RateSolutionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: RateSolutionCommand) { + return solutionRequestResult(() => this.repository.rate(programId, solutionId, command)); + } +} + +@Injectable({ providedIn: "root" }) +export class GetSolutionExpertAssignmentsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, expertId: number) { + return solutionRequestResult(() => this.repository.expertAssignments(programId, expertId)); + } +} + +@Injectable({ providedIn: "root" }) +export class SaveSolutionExpertAssignmentsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, expertId: number, command: SolutionAssignmentsWrite) { + return solutionRequestResult(() => + this.repository.saveExpertAssignments(programId, expertId, command), + ); + } +} diff --git a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts index c84dced8b..44ffed9f7 100644 --- a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts +++ b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts @@ -22,9 +22,47 @@ import { UpdateSolutionCommand, SolutionUploadResponse, SubmitSolutionCommand, + RateSolutionCommand, + SolutionRatingPage, + SolutionRatingFrame, + SolutionAssignmentOptions, + SolutionAssignmentsWrite, + SolutionAllocationWrite, + SolutionAllocationPlan, + SolutionAssignmentReceipt, + SolutionQuota, + SolutionQuotaWrite, + SolutionResults, } from "../solution-contract.model"; export abstract class SolutionRepositoryPort { + abstract previewAllocation( + programId: number, + command: SolutionAllocationWrite, + ): Observable; + abstract applyAllocation( + programId: number, + planToken: string, + ): Observable; + abstract previewQuota(programId: number, requiredEvaluators: number): Observable; + abstract saveQuota(programId: number, command: SolutionQuotaWrite): Observable; + abstract results(programId: number, query?: SolutionResultsQuery): Observable; + abstract ratings(programId: number, query?: SolutionRatingQuery): Observable; + abstract rating(programId: number, solutionId: number): Observable; + abstract rate( + programId: number, + solutionId: number, + command: RateSolutionCommand, + ): Observable; + abstract expertAssignments( + programId: number, + expertId: number, + ): Observable; + abstract saveExpertAssignments( + programId: number, + expertId: number, + command: SolutionAssignmentsWrite, + ): Observable; abstract submit( programId: number, solutionId: number, @@ -104,6 +142,24 @@ export type SolutionListQuery = { offset?: number; }; +export type SolutionRatingQuery = { + search?: string; + caseScope?: "all" | "selected" | "without_case"; + caseName?: string; + status?: "all" | "pending" | "completed"; + assignedOnly?: boolean; + limit?: number; + offset?: number; +}; + +export type SolutionResultsQuery = { + search?: string; + caseScope?: "all" | "selected" | "without_case"; + caseName?: string; + order?: "asc" | "desc"; + status?: "all" | "complete" | "pending"; +}; + export function validSolutionId(value: unknown): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value > 0; } diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts index 01e3ba3bd..90a318b93 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts @@ -3955,4 +3955,831 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ currentApplication: null, }, }, + { + name: "evaluation_list", + operationId: "getSolutionRatings", + status: 200, + response: { + count: 1, + next: null, + previous: null, + results: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + caseName: "Цифровой сервис", + assigned: true, + isCompleted: false, + canRate: true, + blockedReason: null, + }, + ], + }, + }, + { + name: "evaluation_frame", + operationId: "getSolutionRating", + status: 200, + response: { + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + id: 301, + programId: 12, + version: 10, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:05:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: true, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: true, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + ], + submission: { + required: true, + status: "submitted", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: "2026-10-23T10:05:00Z", + }, + permissions: { + canEdit: false, + canManageTeam: false, + canInvite: false, + canSubmit: false, + canLeave: false, + }, + readiness: { + isReady: true, + issues: [], + }, + }, + criteriaRevision: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + criteria: [ + { + id: 21, + name: "Практическая ценность", + description: "Польза аудитории", + type: "int", + minValue: 0, + maxValue: 10, + value: null, + }, + { + id: 22, + name: "Проработанность", + description: null, + type: "float", + minValue: 0, + maxValue: 5, + value: null, + }, + { + id: 23, + name: "Показан прототип", + description: null, + type: "bool", + minValue: null, + maxValue: null, + value: null, + }, + { + id: 24, + name: "Рекомендация", + description: null, + type: "str", + minValue: null, + maxValue: null, + value: null, + }, + ], + managedEvaluation: true, + canRate: true, + blockedReason: null, + isCompleted: false, + ratedCount: 0, + maxRates: 3, + requiredEvaluators: 2, + deadline: "2026-10-30T18:00:00Z", + }, + }, + { + name: "evaluation_saved", + operationId: "rateSolution", + status: 200, + request: { + expectedCriteriaRevision: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + scores: [ + { + criterionId: 21, + value: "0", + }, + { + criterionId: 22, + value: "4.5", + }, + { + criterionId: 23, + value: "False", + }, + { + criterionId: 24, + value: "", + }, + ], + }, + response: { + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + id: 301, + programId: 12, + version: 10, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:05:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: true, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: true, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + ], + submission: { + required: true, + status: "submitted", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: "2026-10-23T10:05:00Z", + }, + permissions: { + canEdit: false, + canManageTeam: false, + canInvite: false, + canSubmit: false, + canLeave: false, + }, + readiness: { + isReady: true, + issues: [], + }, + }, + criteriaRevision: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + criteria: [ + { + id: 21, + name: "Практическая ценность", + description: "Польза аудитории", + type: "int", + minValue: 0, + maxValue: 10, + value: "0", + }, + { + id: 22, + name: "Проработанность", + description: null, + type: "float", + minValue: 0, + maxValue: 5, + value: "4.5", + }, + { + id: 23, + name: "Показан прототип", + description: null, + type: "bool", + minValue: null, + maxValue: null, + value: "False", + }, + { + id: 24, + name: "Рекомендация", + description: null, + type: "str", + minValue: null, + maxValue: null, + value: "", + }, + ], + managedEvaluation: true, + canRate: true, + blockedReason: null, + isCompleted: true, + ratedCount: 1, + maxRates: 3, + requiredEvaluators: 2, + deadline: "2026-10-30T18:00:00Z", + }, + }, + { + name: "evaluation_criteria_changed", + operationId: "rateSolution", + status: 409, + request: { + expectedCriteriaRevision: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + scores: [ + { + criterionId: 21, + value: "0", + }, + { + criterionId: 22, + value: "4.5", + }, + { + criterionId: 23, + value: "False", + }, + { + criterionId: 24, + value: "", + }, + ], + }, + response: { + code: "criteria_changed", + detail: "Критерии изменились. Обновите форму оценки.", + errors: [], + meta: { + currentCriteriaRevision: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }, + }, + { + name: "solution_assignment_options", + operationId: "getSolutionExpertAssignments", + status: 200, + response: { + programId: 12, + expertId: 31, + revision: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + solutions: [ + { + solutionId: 301, + name: "Сервис для студентов", + caseName: "Цифровой сервис", + submitted: true, + assigned: false, + protected: false, + isCompleted: false, + ownTeam: false, + eligible: true, + }, + ], + }, + }, + { + name: "solution_assignments_saved", + operationId: "saveSolutionExpertAssignments", + status: 200, + request: { + revision: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + solutionIds: [301], + }, + response: { + programId: 12, + expertId: 31, + revision: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + solutions: [ + { + solutionId: 301, + name: "Сервис для студентов", + caseName: "Цифровой сервис", + submitted: true, + assigned: true, + protected: false, + isCompleted: false, + ownTeam: false, + eligible: true, + }, + ], + }, + }, + { + name: "evaluation_invalid_value", + operationId: "rateSolution", + status: 400, + request: { + expectedCriteriaRevision: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + scores: [ + { + criterionId: 21, + value: "NaN", + }, + ], + }, + response: { + code: "invalid_scores", + detail: "Проверьте критерии оценки.", + errors: [ + { + path: "scores.21.value", + code: "invalid_value", + fieldId: null, + criterionId: 21, + detail: "Укажите конечное число.", + }, + ], + meta: {}, + }, + }, + { + name: "solution_allocation_preview", + operationId: "previewSolutionAllocation", + status: 200, + request: { + revision: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + expertIds: [31], + }, + response: { + revision: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + planToken: "signed-local-example", + assignments: [ + { + solutionId: 301, + solutionName: "Решение команды", + expertId: 31, + expertName: "Эксперт программы", + caseName: "Цифровой сервис", + }, + ], + shortages: [], + canReroll: false, + load: [ + { + expertId: 31, + name: "Эксперт программы", + before: 0, + after: 1, + assignedBefore: 0, + assignedAfter: 1, + }, + ], + }, + }, + { + name: "solution_allocation_applied", + operationId: "applySolutionAllocation", + status: 200, + request: { + planToken: "signed-local-example", + }, + response: { + programId: 12, + revision: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + created: 1, + }, + }, + { + name: "solution_quota_preview", + operationId: "previewSolutionQuota", + status: 200, + response: { + revision: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + requiredEvaluators: 2, + submittedSolutions: 1, + solutionsMissingExperts: 1, + readyBefore: 0, + readyAfter: 0, + missingAssignments: 1, + surplusAssignments: 0, + }, + }, + { + name: "solution_quota_saved", + operationId: "saveSolutionQuota", + status: 200, + request: { + revision: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + requiredEvaluators: 2, + }, + response: { + revision: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + requiredEvaluators: 2, + submittedSolutions: 1, + solutionsMissingExperts: 1, + readyBefore: 0, + readyAfter: 0, + missingAssignments: 1, + surplusAssignments: 0, + }, + }, + { + name: "solution_results", + operationId: "getSolutionResults", + status: 200, + response: { + programId: 12, + programName: "Кейс-чемпионат PROCOLLAB", + generatedAt: "2026-10-24T20:00:00Z", + requiredEvaluators: 2, + maximum: 15, + hasNumericCriteria: true, + caseOptions: ["Цифровой сервис"], + groups: [ + { + caseName: "Цифровой сервис", + solutions: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + solutionId: 301, + name: "Сервис для студентов", + caseName: "Цифровой сервис", + completed: 1, + required: 2, + evaluationComplete: false, + ready: false, + score: 4.5, + place: null, + averages: { + "21": 0, + "22": 4.5, + }, + experts: [ + { + userId: 91, + name: "Эксперт программы", + email: "expert@local.example", + avatar: null, + complete: true, + total: 4.5, + values: [ + { + criterionId: 21, + value: "0", + }, + { + criterionId: 22, + value: "4.5", + }, + { + criterionId: 23, + value: "False", + }, + { + criterionId: 24, + value: "", + }, + ], + }, + ], + }, + ], + }, + ], + criteria: [ + { + id: 21, + name: "Практическая ценность", + description: "Польза аудитории", + type: "int", + minValue: 0, + maxValue: 10, + }, + { + id: 22, + name: "Проработанность", + description: null, + type: "float", + minValue: 0, + maxValue: 5, + }, + { + id: 23, + name: "Показан прототип", + description: null, + type: "bool", + minValue: null, + maxValue: null, + }, + { + id: 24, + name: "Рекомендация", + description: null, + type: "str", + minValue: null, + maxValue: null, + }, + ], + }, + }, ]; diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts index 6fb32a41a..d183e6b53 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts @@ -1,6 +1,6 @@ /** @format */ -// Сгенерировано tools/generate-solution-contract.py из C00 1.2.0. +// Сгенерировано tools/generate-solution-contract.py из C00 1.3.0. // camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются. export type SolutionUser = { @@ -37,6 +37,7 @@ export type SolutionIssue = { code: string; fieldId: number | null; detail: string; + criterionId?: number | null; }; export type SolutionApiError = { @@ -367,3 +368,208 @@ export type SolutionSubmissionSnapshot = { answers: Array; submittedAt: string; }; + +export type SolutionRatingCriterion = { + id: number; + name: string; + description: string | null; + type: "int" | "float" | "bool" | "str"; + minValue: number | null; + maxValue: number | null; + value: string | null; +}; + +export type SolutionRatingValue = { + criterionId: number; + value: string | null; +}; + +export type RateSolutionCommand = { + expectedCriteriaRevision: string; + scores: Array; +}; + +export type SolutionRatingSummary = { + application: SolutionRef; + caseName: string | null; + assigned: boolean; + isCompleted: boolean; + canRate: boolean; + blockedReason: string | null; +}; + +export type SolutionRatingPage = { + count: number; + next: string | null; + previous: string | null; + results: Array; +}; + +export type SolutionRatingFrame = { + program: SolutionProgramRef; + solution: ProgramSolution; + criteriaRevision: string; + criteria: Array; + managedEvaluation: boolean; + canRate: boolean; + blockedReason: string | null; + isCompleted: boolean; + ratedCount: number; + maxRates: number | null; + requiredEvaluators: number; + deadline: string | null; +}; + +export type SolutionAssignmentOption = { + solutionId: number; + name: string; + caseName: string | null; + submitted: boolean; + assigned: boolean; + protected: boolean; + isCompleted: boolean; + ownTeam: boolean; + eligible: boolean; +}; + +export type SolutionAssignmentOptions = { + programId: number; + expertId: number; + revision: string; + solutions: Array; +}; + +export type SolutionAssignmentsWrite = { + revision: string; + solutionIds: Array; +}; + +export type SolutionCasePool = { + caseName: string | null; + expertIds: Array; +}; + +export type SolutionAssignmentOverride = { + solutionId: number; + expertIds: Array; +}; + +export type SolutionAllocationWrite = { + revision: string; + expertIds: Array; + solutionIds?: Array; + casePools?: Array; + seed?: number; + previousPlanToken?: string; + assignmentOverrides?: Array; +}; + +export type SolutionPlannedAssignment = { + solutionId: number; + solutionName: string; + expertId: number; + expertName: string; + caseName: string | null; +}; + +export type SolutionAssignmentShortage = { + solutionId: number; + solutionName: string; + missing: number; + caseName: string | null; +}; + +export type SolutionExpertLoad = { + expertId: number; + name: string; + before: number; + after: number; + assignedBefore: number; + assignedAfter: number; +}; + +export type SolutionAllocationPlan = { + revision: string; + planToken: string; + assignments: Array; + shortages: Array; + canReroll: boolean; + load: Array; +}; + +export type SolutionAllocationApply = { + planToken: string; +}; + +export type SolutionAssignmentReceipt = { + programId: number; + revision: string; + created: number; +}; + +export type SolutionQuotaWrite = { + revision: string; + requiredEvaluators: number; +}; + +export type SolutionQuota = { + revision: string; + requiredEvaluators: number; + submittedSolutions: number; + solutionsMissingExperts: number; + readyBefore: number; + readyAfter: number; + missingAssignments: number; + surplusAssignments: number; +}; + +export type SolutionResultCriterion = { + id: number; + name: string; + description: string | null; + type: "int" | "float" | "bool" | "str"; + minValue: number | null; + maxValue: number | null; +}; + +export type SolutionResultExpert = { + userId: number; + name: string; + email: string; + avatar: string | null; + complete: boolean; + total: number | null; + values: Array; +}; + +export type SolutionResult = { + application: SolutionRef; + solutionId: number; + name: string; + caseName: string | null; + completed: number; + required: number; + evaluationComplete: boolean; + ready: boolean; + score: number | null; + place: number | null; + averages: Record; + experts: Array; +}; + +export type SolutionResultGroup = { + caseName: string | null; + solutions: Array; +}; + +export type SolutionResults = { + programId: number; + programName: string; + generatedAt: string; + requiredEvaluators: number; + maximum: number | null; + hasNumericCriteria: boolean; + caseOptions: Array; + groups: Array; + criteria: Array; +}; diff --git a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts index a9416bd0d..f0e289d19 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts @@ -24,15 +24,107 @@ import { UpdateSolutionCommand, SolutionUploadResponse, SubmitSolutionCommand, + RateSolutionCommand, + SolutionRatingPage, + SolutionRatingFrame, + SolutionAssignmentOptions, + SolutionAssignmentsWrite, + SolutionAllocationWrite, + SolutionAllocationPlan, + SolutionAssignmentReceipt, + SolutionQuota, + SolutionQuotaWrite, + SolutionResults, } from "@domain/solution/solution-contract.model"; import { SolutionListQuery, SolutionInvitationQuery, + SolutionRatingQuery, + SolutionResultsQuery, validSolutionId, } from "@domain/solution/ports/solution.repository.port"; @Injectable({ providedIn: "root" }) export class SolutionHttpAdapter { + private evaluationManagementPath(programId: number) { + this.path(programId); + return `/programs/${programId}/management/solution-evaluation/`; + } + previewAllocation(programId: number, command: SolutionAllocationWrite) { + return this.api.post( + `${this.evaluationManagementPath(programId)}allocation/`, + command, + ); + } + applyAllocation(programId: number, planToken: string) { + return this.api.post( + `${this.evaluationManagementPath(programId)}allocation/apply/`, + { planToken }, + ); + } + previewQuota(programId: number, requiredEvaluators: number) { + return this.api.get( + `${this.evaluationManagementPath(programId)}quota/`, + new HttpParams().set("required_evaluators", requiredEvaluators), + ); + } + saveQuota(programId: number, command: SolutionQuotaWrite) { + return this.api.put( + `${this.evaluationManagementPath(programId)}quota/`, + command, + ); + } + results(programId: number, query: SolutionResultsQuery = {}) { + this.path(programId); + return this.api.get( + `/programs/${programId}/management/solution-results/`, + this.evaluationQuery(query), + ); + } + private evaluationQuery(query: SolutionRatingQuery | SolutionResultsQuery) { + let params = new HttpParams(); + const wireKeys: Record = { + caseScope: "case_scope", + caseName: "case_name", + assignedOnly: "assigned_only", + }; + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) params = params.set(wireKeys[key] ?? key, value); + } + return params; + } + ratings(programId: number, query: SolutionRatingQuery = {}) { + this.path(programId); + return this.api.get( + `/programs/${programId}/solution-evaluation/`, + this.evaluationQuery(query), + ); + } + rating(programId: number, solutionId: number) { + return this.api.get( + `${this.solutionPath(programId, solutionId)}evaluation/`, + ); + } + rate(programId: number, solutionId: number, command: RateSolutionCommand) { + return this.api.post( + `${this.solutionPath(programId, solutionId)}evaluation/`, + command, + ); + } + private expertAssignmentsPath(programId: number, expertId: number) { + this.path(programId); + if (!validSolutionId(expertId)) throw new RangeError("Invalid expert ID"); + return `/programs/${programId}/management/solution-evaluation/experts/${expertId}/assignments/`; + } + expertAssignments(programId: number, expertId: number) { + return this.api.get(this.expertAssignmentsPath(programId, expertId)); + } + saveExpertAssignments(programId: number, expertId: number, command: SolutionAssignmentsWrite) { + return this.api.put( + this.expertAssignmentsPath(programId, expertId), + command, + ); + } submit(programId: number, solutionId: number, command: SubmitSolutionCommand) { return this.api.post( `${this.solutionPath(programId, solutionId)}submit/`, diff --git a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts index 59ca46ae6..a5707f187 100644 --- a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts @@ -5,6 +5,8 @@ import { SolutionRepositoryPort, SolutionListQuery, SolutionInvitationQuery, + SolutionRatingQuery, + SolutionResultsQuery, } from "@domain/solution/ports/solution.repository.port"; import { CreateSolutionCommand, @@ -15,11 +17,45 @@ import { SolutionFormWrite, UpdateSolutionCommand, SubmitSolutionCommand, + RateSolutionCommand, + SolutionAssignmentsWrite, + SolutionAllocationWrite, + SolutionQuotaWrite, } from "@domain/solution/solution-contract.model"; import { SolutionHttpAdapter } from "../../adapters/solution/solution-http.adapter"; @Injectable({ providedIn: "root" }) export class SolutionRepository implements SolutionRepositoryPort { + previewAllocation(programId: number, command: SolutionAllocationWrite) { + return this.http.previewAllocation(programId, command); + } + applyAllocation(programId: number, planToken: string) { + return this.http.applyAllocation(programId, planToken); + } + previewQuota(programId: number, requiredEvaluators: number) { + return this.http.previewQuota(programId, requiredEvaluators); + } + saveQuota(programId: number, command: SolutionQuotaWrite) { + return this.http.saveQuota(programId, command); + } + results(programId: number, query?: SolutionResultsQuery) { + return this.http.results(programId, query); + } + ratings(programId: number, query?: SolutionRatingQuery) { + return this.http.ratings(programId, query); + } + rating(programId: number, solutionId: number) { + return this.http.rating(programId, solutionId); + } + rate(programId: number, solutionId: number, command: RateSolutionCommand) { + return this.http.rate(programId, solutionId, command); + } + expertAssignments(programId: number, expertId: number) { + return this.http.expertAssignments(programId, expertId); + } + saveExpertAssignments(programId: number, expertId: number, command: SolutionAssignmentsWrite) { + return this.http.saveExpertAssignments(programId, expertId, command); + } submit(programId: number, solutionId: number, command: SubmitSolutionCommand) { return this.http.submit(programId, solutionId, command); } diff --git a/tools/generate-solution-contract.py b/tools/generate-solution-contract.py index 87ab73f80..63db44d51 100644 --- a/tools/generate-solution-contract.py +++ b/tools/generate-solution-contract.py @@ -17,7 +17,7 @@ def camel(value): def name(value): special = {"Solution": "ProgramSolution", "Context": "SolutionContext", "Error": "SolutionApiError", "CreateSolution": "CreateSolutionCommand", "UpdateSolution": "UpdateSolutionCommand", - "Submit": "SubmitSolutionCommand"} + "Submit": "SubmitSolutionCommand", "RateSolution": "RateSolutionCommand"} return special.get(value, value if value.startswith("Solution") else "Solution" + value) @@ -54,6 +54,8 @@ def ts(schema): if kind == "object": props = schema.get("properties", {}) if not props: + if isinstance(schema.get("additionalProperties"), dict): + return "Record" return "Record" required = schema.get("required", []) return "{\n" + "\n".join(" " + camel(k) + ("" if k in required else "?") + ": " + ts(v) + ";" for k, v in props.items()) + "\n}" diff --git a/tools/verify-solution-contract-types.ts b/tools/verify-solution-contract-types.ts index 0726d13fe..73dfb6849 100644 --- a/tools/verify-solution-contract-types.ts +++ b/tools/verify-solution-contract-types.ts @@ -3,6 +3,9 @@ import type { SolutionUpdateInvitation, UpdateSolutionCommand, + RateSolutionCommand, + SolutionAllocationWrite, + SolutionQuotaWrite, } from "../projects/social_platform/src/app/domain/solution/solution-contract.model"; // Независимые примеры C00: у обеих команд обязательны ревизии и хотя бы одно изменение. @@ -50,6 +53,29 @@ const emptySolution: UpdateSolutionCommand = { // @ts-expect-error Отсутствует обязательная ревизия формы. const missingRevision: UpdateSolutionCommand = { expectedVersion: 1, name: "Решение" }; +const validRating = { + expectedCriteriaRevision: "revision", + scores: [{ criterionId: 21, value: "False" }], +} satisfies RateSolutionCommand; +// @ts-expect-error Оценка требует подтвержденную ревизию критериев. +const missingCriteriaRevision: RateSolutionCommand = { scores: [] }; +const wrongRatingValue: RateSolutionCommand = { + expectedCriteriaRevision: "revision", + scores: [ + // @ts-expect-error На проводе оценки используют string/null, а не boolean. + { criterionId: 21, value: false }, + ], +}; +const validPlan = { + revision: "revision", + expertIds: [91], + solutionIds: [301], +} satisfies SolutionAllocationWrite; +// @ts-expect-error Распределение не принимает projectIds вместо обязательных expertIds. +const wrongPlan: SolutionAllocationWrite = { revision: "revision", projectIds: [301] }; +// @ts-expect-error Изменение числа экспертов требует ревизию назначений. +const missingQuotaRevision: SolutionQuotaWrite = { requiredEvaluators: 2 }; + void [ role, cleared, @@ -61,4 +87,10 @@ void [ invalidRole, emptySolution, missingRevision, + validRating, + missingCriteriaRevision, + wrongRatingValue, + validPlan, + wrongPlan, + missingQuotaRevision, ]; From 2432ef9ecd66f392f634d3c067ed02e6c8eea2ab Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sun, 25 Oct 2026 00:29:40 +0300 Subject: [PATCH 06/47] feat(programs): add unified evaluation cabinet API --- docs/contracts/program-solutions-v1/README.md | 4 +- .../program-solutions-v1/examples.json | 54 ++++- .../program-solutions-v1/openapi.json | 224 +++++++++++++++++- docs/product/program-solutions-fe08-api.md | 13 + .../solution-evaluation-contract.spec.ts | 35 +++ .../solution-evaluation.use-cases.ts | 9 + .../ports/solution.repository.port.ts | 12 + .../solution/solution-contract.fixture.ts | 52 ++++ .../solution/solution-contract.model.ts | 23 +- .../solution/solution-http.adapter.ts | 13 +- .../solution/solution.repository.ts | 4 + 11 files changed, 438 insertions(+), 5 deletions(-) create mode 100644 docs/product/program-solutions-fe08-api.md diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index 77b055fb3..90128a3a1 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -2,7 +2,9 @@ # C00 Контракт решения в программе -Рабочая версия 1.3.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. +Рабочая версия 1.4.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE08 и FE01–FE08. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. + +Изменение 1.4.0: GET /evaluation/applications/ возвращает одну страницу реальных назначений обоих форматов. ApplicationRef явно различает legacy_project и solution; project_id/program_link_id и solution_id не подменяются. Общие count/limit/offset применяются после объединения и фильтрации, а pending/completed/unavailable сохраняют семантику вкладок текущего кабинета. Старые /evaluation/programs/ и include=projects остаются совместимыми; счетчики нового формата читают SolutionExpertAssignment. Контракт содержит 74 схемы, 33 операции и 56 независимых примеров. Это рабочий checkpoint BE08; публичные материалы и остальные административные команды дополняются отдельно до реализации. Изменение 1.3.0: совместимое расширение BE07/FE07 добавляет десять операций оценки, назначений, распределения, числа экспертов и результатов решений. Перед реализацией зафиксированы 72 схемы и 55 независимых примеров. В errors появился optional criterion_id; field_id остается ID поля решения. На проводе оценки используют string/null, назначения — настоящие solution_id и Expert.id. Распределение требует ревизию и подписанный предпросмотр; результаты доступны организатору. Старые URL и обязательность полей сохранены. API и frontend port/adapter/use cases реализованы в рабочей ветке; экспертный UI требует согласования D01 и еще не подключен. Rollout: migration project_rates.0008 → backend → frontend; default legacy_project сохранен. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json index fea1f25d7..61969ca4f 100644 --- a/docs/contracts/program-solutions-v1/examples.json +++ b/docs/contracts/program-solutions-v1/examples.json @@ -1,5 +1,5 @@ { - "contract_version": "1.3.0", + "contract_version": "1.4.0", "description": "Независимые примеры, не последовательность операций; фиктивные данные.", "scenarios": [ { @@ -4887,6 +4887,58 @@ } ] } + }, + { + "name": "mixed_cabinet_applications", + "operation_id": "getCabinetApplications", + "status": 200, + "response": { + "count": 2, + "limit": 20, + "offset": 0, + "pending": 2, + "completed": 0, + "unavailable": 0, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "name": "Работа команды", + "solution_id": 301 + }, + "assignment_id": 7, + "program": { + "id": 12, + "name": "Программа" + }, + "deadline": null, + "case_name": null, + "is_completed": false, + "can_rate": true, + "blocked_reason": null + }, + { + "application": { + "kind": "legacy_project", + "program_id": 13, + "name": "Работа команды", + "project_id": 301, + "program_link_id": 44 + }, + "assignment_id": 7, + "program": { + "id": 13, + "name": "Программа" + }, + "deadline": null, + "case_name": null, + "is_completed": false, + "can_rate": true, + "blocked_reason": null + } + ] + } } ] } diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json index 676f61bb9..e50c4e584 100644 --- a/docs/contracts/program-solutions-v1/openapi.json +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -3,7 +3,7 @@ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "PROCOLLAB Program Solutions C00", - "version": "1.3.0", + "version": "1.4.0", "description": "Контракт решений в программах для реализации и моков BE01–BE07. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ @@ -6835,6 +6835,130 @@ } } } + }, + "/evaluation/applications/": { + "get": { + "operationId": "getCabinetApplications", + "summary": "Единая страница реальных назначений проектов и решений", + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "pending", + "completed" + ], + "default": "pending" + } + }, + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "type": "string", + "maxLength": 200 + } + }, + { + "name": "program_id", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CabinetApplications" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "description": "Одна общая пагинация после фильтрации. pending/completed/unavailable считаются до q и program_id. Сортировка: deadline (null в конце), program_id, название без учета регистра, assignment_id, kind." + } } }, "components": { @@ -9449,6 +9573,104 @@ "criteria" ], "additionalProperties": false + }, + "CabinetApplication": { + "type": "object", + "properties": { + "application": { + "$ref": "#/components/schemas/ApplicationRef" + }, + "assignment_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "program": { + "$ref": "#/components/schemas/ProgramRef" + }, + "deadline": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "case_name": { + "type": [ + "string", + "null" + ] + }, + "is_completed": { + "type": "boolean" + }, + "can_rate": { + "type": "boolean" + }, + "blocked_reason": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "application", + "assignment_id", + "program", + "deadline", + "case_name", + "is_completed", + "can_rate", + "blocked_reason" + ], + "additionalProperties": false + }, + "CabinetApplications": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "offset": { + "type": "integer", + "minimum": 0 + }, + "pending": { + "type": "integer", + "minimum": 0 + }, + "completed": { + "type": "integer", + "minimum": 0 + }, + "unavailable": { + "type": "integer", + "minimum": 0 + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CabinetApplication" + } + } + }, + "required": [ + "count", + "limit", + "offset", + "pending", + "completed", + "unavailable", + "results" + ], + "additionalProperties": false } } } diff --git a/docs/product/program-solutions-fe08-api.md b/docs/product/program-solutions-fe08-api.md new file mode 100644 index 000000000..904f587ef --- /dev/null +++ b/docs/product/program-solutions-fe08-api.md @@ -0,0 +1,13 @@ + + +# FE08: единая страница назначений + +Статус: API-слой одного общего входа подготовлен; FE07/FE08 UI по-прежнему ожидает согласования дизайна. Первый выпуск не завершен. + +C00 1.4.0 добавляет GET /evaluation/applications/ и ApplicationRef обоих видов. Старая проекция /evaluation/programs/?include=projects остается прежней. Новая страница объединяет реальные назначения на сервере и только затем применяет общий limit/offset; фронт не объединяет два частично загруженных списка. Query использует status/q/program_id/limit/offset. Вкладочные счетчики pending/completed/unavailable возвращает сервер. + +Реализованы generated types/fixtures, SolutionRepositoryPort, repository/HTTP adapter и GetCabinetApplicationsUseCase. Сохраняются programId, projectId/programLinkId и solutionId в своих ветках; discriminator legacy_project/solution не преобразуется. Wire test специально использует совпадающие числовые ID двух видов заявки и проверяет, что различие сохраняется после CamelcaseInterceptor. Всего у экспертного API 11 wire tests. + +Checked by tests: полный Vitest — 2563 PASS в 466 файлах; после review повторно 11 wire tests и strict C00 types PASS. Проверены общая смешанная страница и отдельный запрос program_id с согласованным ответом сервера. Checked by build: Angular development build PASS. ESLint: 0 errors / 6 прежних warnings. Locked Prettier 3.8.3: 1945 файлов с LF, нарушений нет. C00 1.3 operations/schemas/examples семантически сохранены, 1.4 синхронизирован byte-for-byte в обоих репозиториях. Проверка product UI нового кабинета в браузере еще неприменима: реализация UI не начата до согласования D01. Запущенный backend обеих локальных форм проверен через HTTP/JWT, включая общую пагинацию и совместимую старую проекцию. + +Осталось: административные команды с реальными SolutionRef, публикация материалов после уточнения неконкурсного сценария, BE09/FE08 аналитика и выгрузки; сам экспертный UI/результаты и QA01/REL01. Новые заглушки или автоматически включенный режим не добавлялись. diff --git a/projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts index 312903bb8..ca01b1f51 100644 --- a/projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts +++ b/projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts @@ -10,6 +10,7 @@ import { LoggerService } from "@core/lib/services/logger/logger.service"; import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; import { SolutionRepository } from "@infrastructure/repository/solution/solution.repository"; import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { SolutionCabinetApplications } from "@domain/solution/solution-contract.model"; import { GetSolutionRatingsUseCase, GetSolutionRatingUseCase, @@ -21,6 +22,7 @@ import { PreviewSolutionQuotaUseCase, SaveSolutionQuotaUseCase, GetSolutionResultsUseCase, + GetCabinetApplicationsUseCase, } from "./use-cases/solution-evaluation.use-cases"; const example = (name: string) => @@ -42,6 +44,39 @@ describe("C00 solution evaluation wire contract", () => { ); afterEach(() => TestBed.inject(HttpTestingController).verify()); + it("preserves kind and equal IDs across projects and solutions in one cabinet page", async () => { + const pending = firstValueFrom( + TestBed.inject(GetCabinetApplicationsUseCase).execute({ + status: "pending", + q: "Работа", + limit: 20, + offset: 0, + }), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + request => request.url === "https://local.test/evaluation/applications/", + ); + expect(req.request.params.has("program_id")).toBe(false); + expect(req.request.params.get("q")).toBe("Работа"); + const expected = example("mixed_cabinet_applications") as SolutionCabinetApplications; + req.flush(snakecaseKeys(expected, { deep: true })); + expect(await pending).toEqual({ ok: true, value: expected }); + const filtered = firstValueFrom( + TestBed.inject(GetCabinetApplicationsUseCase).execute({ programId: 12 }), + ); + const selected = TestBed.inject(HttpTestingController).expectOne( + request => request.url === "https://local.test/evaluation/applications/", + ); + expect(selected.request.params.get("program_id")).toBe("12"); + const page = { + ...expected, + count: 1, + results: expected.results.filter(row => row.program.id === 12), + }; + selected.flush(snakecaseKeys(page, { deep: true })); + expect(await filtered).toEqual({ ok: true, value: page }); + }); + it("encodes filters once in snake case and preserves false, zero and exact case names", async () => { const pending = firstValueFrom( TestBed.inject(GetSolutionRatingsUseCase).execute(12, { diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts index a9ba397d5..ab6f8ea77 100644 --- a/projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts @@ -5,6 +5,7 @@ import { SolutionRepositoryPort, SolutionRatingQuery, SolutionResultsQuery, + SolutionCabinetQuery, } from "@domain/solution/ports/solution.repository.port"; import { RateSolutionCommand, @@ -14,6 +15,14 @@ import { } from "@domain/solution/solution-contract.model"; import { solutionRequestResult } from "./solution.use-cases"; +@Injectable({ providedIn: "root" }) +export class GetCabinetApplicationsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(query?: SolutionCabinetQuery) { + return solutionRequestResult(() => this.repository.cabinetApplications(query)); + } +} + @Injectable({ providedIn: "root" }) export class PreviewSolutionAllocationUseCase { private readonly repository = inject(SolutionRepositoryPort); diff --git a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts index 44ffed9f7..211adc3e4 100644 --- a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts +++ b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts @@ -33,9 +33,13 @@ import { SolutionQuota, SolutionQuotaWrite, SolutionResults, + SolutionCabinetApplications, } from "../solution-contract.model"; export abstract class SolutionRepositoryPort { + abstract cabinetApplications( + query?: SolutionCabinetQuery, + ): Observable; abstract previewAllocation( programId: number, command: SolutionAllocationWrite, @@ -160,6 +164,14 @@ export type SolutionResultsQuery = { status?: "all" | "complete" | "pending"; }; +export type SolutionCabinetQuery = { + status?: "pending" | "completed"; + q?: string; + programId?: number; + limit?: number; + offset?: number; +}; + export function validSolutionId(value: unknown): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value > 0; } diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts index 90a318b93..7de453807 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts @@ -4782,4 +4782,56 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ ], }, }, + { + name: "mixed_cabinet_applications", + operationId: "getCabinetApplications", + status: 200, + response: { + count: 2, + limit: 20, + offset: 0, + pending: 2, + completed: 0, + unavailable: 0, + results: [ + { + application: { + kind: "solution", + programId: 12, + name: "Работа команды", + solutionId: 301, + }, + assignmentId: 7, + program: { + id: 12, + name: "Программа", + }, + deadline: null, + caseName: null, + isCompleted: false, + canRate: true, + blockedReason: null, + }, + { + application: { + kind: "legacy_project", + programId: 13, + name: "Работа команды", + projectId: 301, + programLinkId: 44, + }, + assignmentId: 7, + program: { + id: 13, + name: "Программа", + }, + deadline: null, + caseName: null, + isCompleted: false, + canRate: true, + blockedReason: null, + }, + ], + }, + }, ]; diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts index d183e6b53..37fe3939e 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts @@ -1,6 +1,6 @@ /** @format */ -// Сгенерировано tools/generate-solution-contract.py из C00 1.3.0. +// Сгенерировано tools/generate-solution-contract.py из C00 1.4.0. // camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются. export type SolutionUser = { @@ -573,3 +573,24 @@ export type SolutionResults = { groups: Array; criteria: Array; }; + +export type SolutionCabinetApplication = { + application: SolutionApplicationRef; + assignmentId: number; + program: SolutionProgramRef; + deadline: string | null; + caseName: string | null; + isCompleted: boolean; + canRate: boolean; + blockedReason: string | null; +}; + +export type SolutionCabinetApplications = { + count: number; + limit: number; + offset: number; + pending: number; + completed: number; + unavailable: number; + results: Array; +}; diff --git a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts index f0e289d19..e9f88d804 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts @@ -35,17 +35,25 @@ import { SolutionQuota, SolutionQuotaWrite, SolutionResults, + SolutionCabinetApplications, } from "@domain/solution/solution-contract.model"; import { SolutionListQuery, SolutionInvitationQuery, SolutionRatingQuery, SolutionResultsQuery, + SolutionCabinetQuery, validSolutionId, } from "@domain/solution/ports/solution.repository.port"; @Injectable({ providedIn: "root" }) export class SolutionHttpAdapter { + cabinetApplications(query: SolutionCabinetQuery = {}) { + return this.api.get( + "/evaluation/applications/", + this.evaluationQuery(query), + ); + } private evaluationManagementPath(programId: number) { this.path(programId); return `/programs/${programId}/management/solution-evaluation/`; @@ -81,12 +89,15 @@ export class SolutionHttpAdapter { this.evaluationQuery(query), ); } - private evaluationQuery(query: SolutionRatingQuery | SolutionResultsQuery) { + private evaluationQuery( + query: SolutionRatingQuery | SolutionResultsQuery | SolutionCabinetQuery, + ) { let params = new HttpParams(); const wireKeys: Record = { caseScope: "case_scope", caseName: "case_name", assignedOnly: "assigned_only", + programId: "program_id", }; for (const [key, value] of Object.entries(query)) { if (value !== undefined) params = params.set(wireKeys[key] ?? key, value); diff --git a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts index a5707f187..36657a771 100644 --- a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts @@ -7,6 +7,7 @@ import { SolutionInvitationQuery, SolutionRatingQuery, SolutionResultsQuery, + SolutionCabinetQuery, } from "@domain/solution/ports/solution.repository.port"; import { CreateSolutionCommand, @@ -26,6 +27,9 @@ import { SolutionHttpAdapter } from "../../adapters/solution/solution-http.adapt @Injectable({ providedIn: "root" }) export class SolutionRepository implements SolutionRepositoryPort { + cabinetApplications(query?: SolutionCabinetQuery) { + return this.http.cabinetApplications(query); + } previewAllocation(programId: number, command: SolutionAllocationWrite) { return this.http.previewAllocation(programId, command); } From a7567b2437edaefc7a71b52bd843512b68260396 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sun, 25 Oct 2026 00:47:49 +0300 Subject: [PATCH 07/47] feat(programs): add solution results export API --- docs/contracts/program-solutions-v1/README.md | 4 +- .../program-solutions-v1/examples.json | 20 +- .../program-solutions-v1/openapi.json | 175 +++++++++++++++++- docs/product/program-solutions-fe08-api.md | 8 +- .../solution-results-export-contract.spec.ts | 147 +++++++++++++++ .../solution-results-export.use-case.ts | 46 +++++ .../ports/solution.repository.port.ts | 5 + .../solution/solution-contract.fixture.ts | 18 ++ .../solution/solution-contract.model.ts | 2 +- .../solution/solution-http.adapter.ts | 8 + .../solution/solution.repository.ts | 4 + tools/verify-solution-contract-types.ts | 11 ++ 12 files changed, 441 insertions(+), 7 deletions(-) create mode 100644 projects/social_platform/src/app/api/solution/solution-results-export-contract.spec.ts create mode 100644 projects/social_platform/src/app/api/solution/use-cases/solution-results-export.use-case.ts diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index 90128a3a1..8a3f58e07 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -2,7 +2,9 @@ # C00 Контракт решения в программе -Рабочая версия 1.4.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE08 и FE01–FE08. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. +Рабочая версия 1.5.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE09 и FE01–FE08. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. + +Изменение 1.5.0: GET /programs/{program_id}/management/solution-results/export/ возвращает бинарный XLSX с тем же расчетом, что и результаты решения. Фильтры сохранены; scope=current/all повторяет действующую выгрузку проектов. Два листа содержат ID решения, числовые ячейки и прежние формулы; пользовательские строки защищены от интерпретации как формулы. Доступ только организатору программы; ошибки остаются JSON C00. Старые операции и все схемы не меняются: 74 схемы, 34 операции, 57 независимых примеров. Успешный XLSX проверяется чтением настоящей книги в API tests, JSON-пример нового endpoint фиксирует invalid scope. Это первый блок BE09; остальная аналитика, ролевой виджет и выгрузки еще в работе. Изменение 1.4.0: GET /evaluation/applications/ возвращает одну страницу реальных назначений обоих форматов. ApplicationRef явно различает legacy_project и solution; project_id/program_link_id и solution_id не подменяются. Общие count/limit/offset применяются после объединения и фильтрации, а pending/completed/unavailable сохраняют семантику вкладок текущего кабинета. Старые /evaluation/programs/ и include=projects остаются совместимыми; счетчики нового формата читают SolutionExpertAssignment. Контракт содержит 74 схемы, 33 операции и 56 независимых примеров. Это рабочий checkpoint BE08; публичные материалы и остальные административные команды дополняются отдельно до реализации. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json index 61969ca4f..88044d3b3 100644 --- a/docs/contracts/program-solutions-v1/examples.json +++ b/docs/contracts/program-solutions-v1/examples.json @@ -1,5 +1,5 @@ { - "contract_version": "1.4.0", + "contract_version": "1.5.0", "description": "Независимые примеры, не последовательность операций; фиктивные данные.", "scenarios": [ { @@ -4939,6 +4939,24 @@ } ] } + }, + { + "name": "solution_results_export_invalid_scope", + "operation_id": "exportSolutionResults", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [ + { + "path": "scope", + "code": "invalid_choice", + "detail": "Выберите current или all.", + "field_id": null + } + ], + "meta": {} + } } ] } diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json index e50c4e584..3f619a67e 100644 --- a/docs/contracts/program-solutions-v1/openapi.json +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -3,8 +3,8 @@ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "PROCOLLAB Program Solutions C00", - "version": "1.4.0", - "description": "Контракт решений в программах для реализации и моков BE01–BE07. API base URL задается окружением. snake_case на проводе. Старые API не меняются." + "version": "1.5.0", + "description": "Контракт решений в программах для реализации и моков BE01–BE09. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ { @@ -6959,6 +6959,177 @@ }, "description": "Одна общая пагинация после фильтрации. pending/completed/unavailable считаются до q и program_id. Сортировка: deadline (null в конце), program_id, название без учета регистра, assignment_id, kind." } + }, + "/programs/{program_id}/management/solution-results/export/": { + "get": { + "operationId": "exportSolutionResults", + "summary": "Выгрузка результатов решений в XLSX", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string", + "maxLength": 200 + } + }, + { + "name": "case_scope", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "selected", + "without_case" + ] + } + }, + { + "name": "case_name", + "in": "query", + "required": false, + "schema": { + "type": "string", + "maxLength": 500 + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "complete", + "pending" + ] + } + }, + { + "name": "scope", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "current", + "all" + ], + "default": "current" + } + } + ], + "responses": { + "200": { + "description": "Два листа с результатами одной программы", + "headers": { + "Content-Disposition": { + "schema": { + "type": "string" + }, + "example": "attachment; filename=\"program-12-solution-results.xlsx\"" + }, + "Cache-Control": { + "schema": { + "type": "string", + "const": "private, no-store" + } + }, + "Referrer-Policy": { + "schema": { + "type": "string", + "const": "no-referrer" + } + } + }, + "content": { + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "description": "Только организатор этой программы в режиме solution. Тот же расчет и порядок, что GET solution-results: места вычисляются до фильтрации, средние только по полным оценкам. scope=current (по умолчанию) применяет search, case_scope/case_name и status; scope=all выгружает все сданные решения, сохраняя order. Некорректный selected case_name остается ошибкой при обоих scope, как в действующей выгрузке проектов. Два листа: Рейтинг и Оценки экспертов. Столбец ID решения содержит ProgramSolution.id; Project и program_link_id не используются. Числа и формулы имеют прежнюю семантику; пользовательские строки остаются текстом, включая строки, начинающиеся с =. Черновики и чужие программы исключены. Оценки не публикуются участникам. Успешный ответ является бинарным XLSX; JSON возвращается только для ошибок C00." + } } }, "components": { diff --git a/docs/product/program-solutions-fe08-api.md b/docs/product/program-solutions-fe08-api.md index 904f587ef..209838739 100644 --- a/docs/product/program-solutions-fe08-api.md +++ b/docs/product/program-solutions-fe08-api.md @@ -1,6 +1,6 @@ -# FE08: единая страница назначений +# FE08: API общего кабинета и выгрузки результатов Статус: API-слой одного общего входа подготовлен; FE07/FE08 UI по-прежнему ожидает согласования дизайна. Первый выпуск не завершен. @@ -10,4 +10,8 @@ C00 1.4.0 добавляет GET /evaluation/applications/ и ApplicationRef о Checked by tests: полный Vitest — 2563 PASS в 466 файлах; после review повторно 11 wire tests и strict C00 types PASS. Проверены общая смешанная страница и отдельный запрос program_id с согласованным ответом сервера. Checked by build: Angular development build PASS. ESLint: 0 errors / 6 прежних warnings. Locked Prettier 3.8.3: 1945 файлов с LF, нарушений нет. C00 1.3 operations/schemas/examples семантически сохранены, 1.4 синхронизирован byte-for-byte в обоих репозиториях. Проверка product UI нового кабинета в браузере еще неприменима: реализация UI не начата до согласования D01. Запущенный backend обеих локальных форм проверен через HTTP/JWT, включая общую пагинацию и совместимую старую проекцию. -Осталось: административные команды с реальными SolutionRef, публикация материалов после уточнения неконкурсного сценария, BE09/FE08 аналитика и выгрузки; сам экспертный UI/результаты и QA01/REL01. Новые заглушки или автоматически включенный режим не добавлялись. +Следующий блок C00 1.5.0: ExportSolutionResultsUseCase → SolutionRepositoryPort/repository → HTTP adapter → GET /programs/{program_id}/management/solution-results/export/. Query использует прежние search/case_scope/case_name/status/order и scope=current/all; получает исходный Blob через ApiService.getFile. JSON ошибки при responseType=blob декодируются и проходят общий SolutionApiError. Ни solution_id, ни project_id не подменяются; файл относится к явной программе решения. + +Checked by tests нового блока: 5 wire/error/ID tests PASS; полный Vitest — 2568 tests / 467 файлов PASS, strict C00 types PASS. Angular development build PASS; ESLint измененных TS — 0 errors. C00 1.5.0: 74 схемы, 34 операции, 57 независимых примеров; старые схемы/операции/примеры сохранены и обе копии контракта byte identical. Запущенный backend двух форм проверен через HTTP/JWT и чтение настоящей XLSX-книги. UI кнопки/результатов в браузере еще не проверяется: он ожидает согласования дизайна, API-слой интерфейс не добавляет. + +Осталось: административные команды с реальными SolutionRef, публикация материалов после уточнения неконкурсного сценария, остальная BE09/FE08 аналитика и выгрузки; сам экспертный UI/результаты и QA01/REL01. Новые заглушки или автоматически включенный режим не добавлялись. diff --git a/projects/social_platform/src/app/api/solution/solution-results-export-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-results-export-contract.spec.ts new file mode 100644 index 000000000..ce6ec2f0c --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-results-export-contract.spec.ts @@ -0,0 +1,147 @@ +/** @format */ + +import { + HTTP_INTERCEPTORS, + HttpErrorResponse, + provideHttpClient, + withInterceptorsFromDi, +} from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom, throwError } from "rxjs"; +import { CamelcaseInterceptor } from "@core/lib/interceptors/camelcase.interceptor"; +import { API_URL } from "@core/lib/providers"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { SolutionRepository } from "@infrastructure/repository/solution/solution.repository"; +import { ExportSolutionResultsUseCase } from "./use-cases/solution-results-export.use-case"; + +describe("C00 solution results XLSX wire contract", () => { + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: API_URL, useValue: "https://local.test" }, + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: LoggerService, useValue: { warn: vi.fn() } }, + { provide: SolutionRepositoryPort, useExisting: SolutionRepository }, + ], + }), + ); + afterEach(() => TestBed.inject(HttpTestingController).verify()); + + it("uses real solution route, snake_case filters and preserves the original Blob", async () => { + const pending = firstValueFrom( + TestBed.inject(ExportSolutionResultsUseCase).execute(12, { + search: "Кейс", + caseScope: "selected", + caseName: "Задача 1", + order: "asc", + status: "pending", + scope: "current", + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + r => r.url === "https://local.test/programs/12/management/solution-results/export/", + ); + expect(request.request.method).toBe("GET"); + expect(request.request.responseType).toBe("blob"); + expect(request.request.params.keys().sort()).toEqual([ + "case_name", + "case_scope", + "order", + "scope", + "search", + "status", + ]); + expect(request.request.params.get("case_scope")).toBe("selected"); + expect(request.request.params.get("case_name")).toBe("Задача 1"); + expect(request.request.params.get("search")).toBe("Кейс"); + const blob = new Blob(["PK XLSX fixture"], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + request.flush(blob); + expect(await pending).toEqual({ ok: true, value: blob }); + }); + + it("passes scope=all without inventing project IDs or default filters", async () => { + const pending = firstValueFrom( + TestBed.inject(ExportSolutionResultsUseCase).execute(12, { scope: "all" }), + ); + const request = TestBed.inject(HttpTestingController).expectOne(r => + r.url.endsWith("/solution-results/export/"), + ); + expect(request.request.params.keys()).toEqual(["scope"]); + expect(request.request.params.get("scope")).toBe("all"); + const blob = new Blob(); + request.flush(blob); + expect(await pending).toEqual({ ok: true, value: blob }); + }); + + it("decodes C00 JSON errors returned as Blob", async () => { + const error = new HttpErrorResponse({ + status: 400, + error: new Blob( + [ + JSON.stringify({ + code: "invalid_filter", + detail: "Выберите существующий кейс.", + errors: [ + { + path: "case_name", + code: "invalid", + detail: "Недоступный кейс.", + field_id: null, + }, + ], + meta: { current_form_revision: "revision" }, + }), + ], + { type: "application/json" }, + ), + }); + const pending = firstValueFrom(TestBed.inject(ExportSolutionResultsUseCase).execute(12)); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/management/solution-results/export/", + ); + request.flush(error.error, { status: 400, statusText: "Bad Request" }); + expect(await pending).toEqual({ + ok: false, + error: { + code: "invalid_filter", + detail: "Выберите существующий кейс.", + errors: [ + { + path: "case_name", + code: "invalid", + detail: "Недоступный кейс.", + fieldId: null, + }, + ], + meta: { currentFormRevision: "revision" }, + }, + }); + }); + + it("handles a non-JSON error Blob without exposing its contents", async () => { + vi.spyOn(TestBed.inject(SolutionRepository), "exportResults").mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 403, error: new Blob(["private traceback"]) }), + ), + ); + const result = await firstValueFrom(TestBed.inject(ExportSolutionResultsUseCase).execute(12)); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("forbidden"); + expect(result.error.detail).not.toContain("traceback"); + } + }); + + it("rejects invalid program IDs before requesting a file", async () => { + const result = await firstValueFrom(TestBed.inject(ExportSolutionResultsUseCase).execute(0)); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error.code).toBe("invalid_id"); + TestBed.inject(HttpTestingController).expectNone(() => true); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-results-export.use-case.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-results-export.use-case.ts new file mode 100644 index 000000000..db0ce463e --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-results-export.use-case.ts @@ -0,0 +1,46 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { HttpErrorResponse } from "@angular/common/http"; +import { catchError, defer, map, of } from "rxjs"; +import { + SolutionRepositoryPort, + SolutionResultsExportQuery, +} from "@domain/solution/ports/solution.repository.port"; +import { fail, ok } from "@domain/shared/result.type"; +import { solutionError } from "../solution-error"; + +@Injectable({ providedIn: "root" }) +export class ExportSolutionResultsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + + execute(programId: number, query?: SolutionResultsExportQuery) { + return defer(() => this.repository.exportResults(programId, query)).pipe( + map(value => ok(value)), + catchError((error: unknown) => { + const fallback = () => fail(solutionError(error)); + if (!(error instanceof HttpErrorResponse) || !(error.error instanceof Blob)) { + return of(fallback()); + } + // responseType=blob относится и к JSON-ошибке API. + const body: Blob = error.error; + return defer(() => body.text()).pipe( + map(text => + fail( + solutionError( + new HttpErrorResponse({ + error: JSON.parse(text), + status: error.status, + statusText: error.statusText, + headers: error.headers, + url: error.url ?? undefined, + }), + ), + ), + ), + catchError(() => of(fallback())), + ); + }), + ); + } +} diff --git a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts index 211adc3e4..a97fb5638 100644 --- a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts +++ b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts @@ -51,6 +51,7 @@ export abstract class SolutionRepositoryPort { abstract previewQuota(programId: number, requiredEvaluators: number): Observable; abstract saveQuota(programId: number, command: SolutionQuotaWrite): Observable; abstract results(programId: number, query?: SolutionResultsQuery): Observable; + abstract exportResults(programId: number, query?: SolutionResultsExportQuery): Observable; abstract ratings(programId: number, query?: SolutionRatingQuery): Observable; abstract rating(programId: number, solutionId: number): Observable; abstract rate( @@ -164,6 +165,10 @@ export type SolutionResultsQuery = { status?: "all" | "complete" | "pending"; }; +export type SolutionResultsExportQuery = SolutionResultsQuery & { + scope?: "current" | "all"; +}; + export type SolutionCabinetQuery = { status?: "pending" | "completed"; q?: string; diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts index 7de453807..6790eb608 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts @@ -4834,4 +4834,22 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ ], }, }, + { + name: "solution_results_export_invalid_scope", + operationId: "exportSolutionResults", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные запроса.", + errors: [ + { + path: "scope", + code: "invalid_choice", + detail: "Выберите current или all.", + fieldId: null, + }, + ], + meta: {}, + }, + }, ]; diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts index 37fe3939e..6e48acf36 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts @@ -1,6 +1,6 @@ /** @format */ -// Сгенерировано tools/generate-solution-contract.py из C00 1.4.0. +// Сгенерировано tools/generate-solution-contract.py из C00 1.5.0. // camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются. export type SolutionUser = { diff --git a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts index e9f88d804..68e527398 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts @@ -42,6 +42,7 @@ import { SolutionInvitationQuery, SolutionRatingQuery, SolutionResultsQuery, + SolutionResultsExportQuery, SolutionCabinetQuery, validSolutionId, } from "@domain/solution/ports/solution.repository.port"; @@ -89,6 +90,13 @@ export class SolutionHttpAdapter { this.evaluationQuery(query), ); } + exportResults(programId: number, query: SolutionResultsExportQuery = {}) { + this.path(programId); + return this.api.getFile( + `/programs/${programId}/management/solution-results/export/`, + this.evaluationQuery(query), + ); + } private evaluationQuery( query: SolutionRatingQuery | SolutionResultsQuery | SolutionCabinetQuery, ) { diff --git a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts index 36657a771..b7751556b 100644 --- a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts @@ -7,6 +7,7 @@ import { SolutionInvitationQuery, SolutionRatingQuery, SolutionResultsQuery, + SolutionResultsExportQuery, SolutionCabinetQuery, } from "@domain/solution/ports/solution.repository.port"; import { @@ -45,6 +46,9 @@ export class SolutionRepository implements SolutionRepositoryPort { results(programId: number, query?: SolutionResultsQuery) { return this.http.results(programId, query); } + exportResults(programId: number, query?: SolutionResultsExportQuery) { + return this.http.exportResults(programId, query); + } ratings(programId: number, query?: SolutionRatingQuery) { return this.http.ratings(programId, query); } diff --git a/tools/verify-solution-contract-types.ts b/tools/verify-solution-contract-types.ts index 73dfb6849..966061358 100644 --- a/tools/verify-solution-contract-types.ts +++ b/tools/verify-solution-contract-types.ts @@ -7,6 +7,15 @@ import type { SolutionAllocationWrite, SolutionQuotaWrite, } from "../projects/social_platform/src/app/domain/solution/solution-contract.model"; +import type { SolutionResultsExportQuery } from "../projects/social_platform/src/app/domain/solution/ports/solution.repository.port"; + +const validResultsExport = { + scope: "all", + caseScope: "selected", + caseName: "Задача 1", +} satisfies SolutionResultsExportQuery; +// @ts-expect-error Выгрузка принимает только current/all. +const invalidResultsExport: SolutionResultsExportQuery = { scope: "everything" }; // Независимые примеры C00: у обеих команд обязательны ревизии и хотя бы одно изменение. const role = { @@ -77,6 +86,8 @@ const wrongPlan: SolutionAllocationWrite = { revision: "revision", projectIds: [ const missingQuotaRevision: SolutionQuotaWrite = { requiredEvaluators: 2 }; void [ + validResultsExport, + invalidResultsExport, role, cleared, text, From 3ea8bcdf75157ab32d7a657ed0b58cc8afd58b73 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sun, 25 Oct 2026 01:45:58 +0300 Subject: [PATCH 08/47] feat(programs): add shared expert solution evaluation screens --- docs/contracts/program-solutions-v1/README.md | 4 +- .../program-solutions-v1/examples.json | 302 +++++++++++++++- .../program-solutions-v1/openapi.json | 37 +- docs/product/program-solutions-fe07-api.md | 2 +- docs/product/program-solutions-fe07-ui.md | 61 ++++ .../solution/solution-contract.fixture.ts | 295 ++++++++++++++++ .../solution/solution-contract.model.ts | 7 +- .../project-rating.component.ts | 14 +- .../solution-evaluation-list.component.html | 168 +++++++++ ...solution-evaluation-list.component.spec.ts | 113 ++++++ .../solution-evaluation-list.component.ts | 141 ++++++++ .../solution-evaluation.component.html | 186 ++++++++++ .../solution-evaluation.component.scss | 42 +++ .../solution-evaluation.component.spec.ts | 232 +++++++++++++ .../solution-evaluation.component.ts | 326 ++++++++++++++++++ .../app/ui/routes/program/detail.routes.ts | 18 + .../solution-fields.component.html | 4 +- .../solution-fields.component.spec.ts | 79 +++++ .../solution-fields.component.ts | 11 +- .../app/utils/evaluation-criterion-control.ts | 5 +- .../button/button.directive.spec.ts | 43 +++ .../primitives/button/button.directive.ts | 13 +- 22 files changed, 2082 insertions(+), 21 deletions(-) create mode 100644 docs/product/program-solutions-fe07-ui.md create mode 100644 projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.html create mode 100644 projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html create mode 100644 projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.scss create mode 100644 projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts create mode 100644 projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.spec.ts create mode 100644 projects/ui/src/lib/components/primitives/button/button.directive.spec.ts diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index 8a3f58e07..9de928fef 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -2,7 +2,9 @@ # C00 Контракт решения в программе -Рабочая версия 1.5.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE09 и FE01–FE08. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. +Рабочая версия 1.6.0 от 24 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE09 и FE01–FE08. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. + +Изменение 1.6.0: перед подключением FE07 синхронизированы последние согласованные правки оценивания из go to UI / PR #506 и API #823. RatingValue.value и RatingCriterion.value расширены с 50 до 1000 символов; текстовые комментарии повторяют текущий ProjectScore. Типы, диапазоны, обязательность, ID и все операции сохранены. Новый независимый пример фиксирует комментарий 1000 символов: 74 схемы, 34 операции, 59 сценариев. Optional RatingPage.evaluation_counts передает собственные счетчики после фильтров и до статуса/пагинации, как новый список проектов из PR #506; UI не считает их из одной страницы. Optional RatingPage.case_options дает эксперту только варианты кейса для текущего фильтра; доступ к закрытой живой форме не расширяется. Backend требует merge migrations для двух веток project_rates.0008 и расширение SolutionScore.value; затем backend → frontend с общей формой критериев, без преобразования ProgramSolution в Project. Изменение 1.5.0: GET /programs/{program_id}/management/solution-results/export/ возвращает бинарный XLSX с тем же расчетом, что и результаты решения. Фильтры сохранены; scope=current/all повторяет действующую выгрузку проектов. Два листа содержат ID решения, числовые ячейки и прежние формулы; пользовательские строки защищены от интерпретации как формулы. Доступ только организатору программы; ошибки остаются JSON C00. Старые операции и все схемы не меняются: 74 схемы, 34 операции, 57 независимых примеров. Успешный XLSX проверяется чтением настоящей книги в API tests, JSON-пример нового endpoint фиксирует invalid scope. Это первый блок BE09; остальная аналитика, ролевой виджет и выгрузки еще в работе. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json index 88044d3b3..5c2d5a591 100644 --- a/docs/contracts/program-solutions-v1/examples.json +++ b/docs/contracts/program-solutions-v1/examples.json @@ -1,5 +1,5 @@ { - "contract_version": "1.5.0", + "contract_version": "1.6.0", "description": "Независимые примеры, не последовательность операций; фиктивные данные.", "scenarios": [ { @@ -4957,6 +4957,306 @@ ], "meta": {} } + }, + { + "name": "solution_rating_long_comment", + "operation_id": "rateSolution", + "status": 200, + "request": { + "expected_criteria_revision": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "scores": [ + { + "criterion_id": 21, + "value": "0" + }, + { + "criterion_id": 22, + "value": "4.5" + }, + { + "criterion_id": 23, + "value": "False" + }, + { + "criterion_id": 24, + "value": "АААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААА" + } + ] + }, + "response": { + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "criteria_revision": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "criteria": [ + { + "id": 21, + "name": "Практическая ценность", + "description": "Польза аудитории", + "type": "int", + "min_value": 0, + "max_value": 10, + "value": "0" + }, + { + "id": 22, + "name": "Проработанность", + "description": null, + "type": "float", + "min_value": 0, + "max_value": 5, + "value": "4.5" + }, + { + "id": 23, + "name": "Показан прототип", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + }, + { + "id": 24, + "name": "Рекомендация", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "АААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААА" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": true, + "rated_count": 1, + "max_rates": 3, + "required_evaluators": 2, + "deadline": "2026-10-30T18:00:00Z" + } + }, + { + "name": "solution_rating_counts_before_status_and_page", + "operation_id": "getSolutionRatings", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "case_name": "Цифровой сервис", + "assigned": true, + "is_completed": false, + "can_rate": true, + "blocked_reason": null + } + ], + "evaluation_counts": { + "pending": 1, + "completed": 0 + }, + "case_options": [ + "Цифровой сервис" + ] + } } ] } diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json index 3f619a67e..de042732a 100644 --- a/docs/contracts/program-solutions-v1/openapi.json +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -3,7 +3,7 @@ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "PROCOLLAB Program Solutions C00", - "version": "1.5.0", + "version": "1.6.0", "description": "Контракт решений в программах для реализации и моков BE01–BE09. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ @@ -5963,7 +5963,8 @@ } } } - } + }, + "description": "Сданные решения в текущей видимости эксперта. evaluation_counts содержит собственные pending/completed после search/case_scope/assigned_only, до status и limit/offset, как действующий список оценивания проектов. Счетчики не вычисляются из загруженной страницы." } }, "/programs/{program_id}/solutions/{solution_id}/evaluation/": { @@ -8824,7 +8825,8 @@ "string", "null" ], - "maxLength": 50 + "maxLength": 1000, + "description": "Строка/null по прежнему типу критерия. Текстовый комментарий допускает до 1000 символов, как текущий ProjectScore; числовой диапазон и True/False продолжают проверяться отдельно." } }, "required": [ @@ -8851,7 +8853,8 @@ "string", "null" ], - "maxLength": 50 + "maxLength": 1000, + "description": "Строка/null по прежнему типу критерия. Текстовый комментарий допускает до 1000 символов, как текущий ProjectScore; числовой диапазон и True/False продолжают проверяться отдельно." } }, "required": [ @@ -8945,6 +8948,32 @@ "items": { "$ref": "#/components/schemas/RatingSummary" } + }, + "evaluation_counts": { + "type": "object", + "additionalProperties": false, + "properties": { + "pending": { + "type": "integer", + "minimum": 0 + }, + "completed": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "pending", + "completed" + ], + "description": "Собственные оценки текущего эксперта после search/case/assigned_only, до status и пагинации. Значение возвращает актуальный backend; optional для совместимости старых моков и клиентов." + }, + "case_options": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Текущие точные варианты кейса для фильтра эксперта этой программы. Без полей/ответов закрытой формы; optional для совместимости старых клиентов." } }, "required": [ diff --git a/docs/product/program-solutions-fe07-api.md b/docs/product/program-solutions-fe07-api.md index a08ca2878..cbdffa2ef 100644 --- a/docs/product/program-solutions-fe07-api.md +++ b/docs/product/program-solutions-fe07-api.md @@ -2,7 +2,7 @@ # FE07: подключение контрактов экспертной оценки -Статус: API-слой подготовлен, экспертный UI еще не реализован. D01 расположен по адресу http://127.0.0.1:8769/expert-v1/ и ожидает явного согласования пользователя. Согласованный D00 v3 покрывает редактор, команду, форму и сдачу; он не является согласованием экспертных экранов. +Исторический отчет API-блока C00 1.3.0. Текущий статус на 25.10.2026: пользователь согласовал D01; первый экспертный UI реализован на основе свежего общего оценивания из frontend #506 и API #823. Контракт расширен до 1.6.0. Актуальные поведение, проверки и оставшаяся работа описаны в [program-solutions-fe07-ui.md](program-solutions-fe07-ui.md). Полная FE07 еще не завершена. Контракт C00 1.3.0 синхронизирован с backend: 72 схемы, 32 операции всего, 55 независимых сценариев. Добавлены десять операций: список работ эксперта, frame, сохранение оценки, чтение/запись назначений, предпросмотр/применение распределения, предпросмотр/сохранение N и результаты организатора. diff --git a/docs/product/program-solutions-fe07-ui.md b/docs/product/program-solutions-fe07-ui.md new file mode 100644 index 000000000..61a1732b3 --- /dev/null +++ b/docs/product/program-solutions-fe07-ui.md @@ -0,0 +1,61 @@ + + +# FE07: экспертная оценка решения + +Первый UI-блок FE07 реализован и проверен в локальном браузере; общий выпуск еще не завершен. + +## Источник и согласование + +25.10.2026 пользователь явно согласовал D01 и уточнил ожидание последних правок из чата «go to UI». Проверка показала: ранний локальный D01 опирался на прежний Angular-код, поэтому актуальным источником выбраны уже согласованные и слитые frontend #506 (15905184) и API #823 (c68a004e). Их origin/dev интегрирован в рабочие ветки. Подробности: `design-artifacts/program-solutions-20261024/expert-v1/design-handoff.md` и `design-artifacts/project-scoring-form-20261024/implementation/review.md` в workspace. + +Используется существующий `ProjectRatingComponent` как чистый renderer числовых/булевых/текстовых критериев. Его вход сужен до данных поля без обязательного expertId: никакого вымышленного Project или ID эксперта для рендера решения нет. Структурная совместимость прежнего ProjectRatingCriterion сохранена. Общие native Button/Field/Modal, Mont и стили текущего сценария оценивания переиспользованы. + +## Поведение и контракт + +Список `/office/program/:programId/solutions-rating` использует реальные SolutionRef; собственные pending/completed приходят с сервера после поиска/кейса и до статуса/пагинации. Query сохраняет q/case/status/page; возврат из оценки восстанавливает URL и положение. Пагинация использует явные limit/offset C00 и query page, как общий кабинет. Фильтр кейса открывается в существующей модалке; отмена не применяет выбор. Эксперту не нужен доступ к закрытой живой форме: только case_options из scoped списка. + +Форма `/office/program/:programId/solutions/:solutionId/evaluation` читает сданный snapshot и собственные критерии. Вкладки «Решение» / «Команда» сохраняют ввод; ожидающие приглашения не становятся составом. Сервер управляет canRate и blockedReason. Критерии валидируются общей формой; 0 и false не пустые. До POST открывается подтверждение всех значений; pending блокирует повтор/закрытие/выход. После успеха показывается сохраненная оценка, доступное изменение определяется сервером. Ошибка оставляет ввод; смена критериев требует GET новой ревизии и сохраняет совместимые значения по ID/типу. Не совпавшие поля доступны для копирования. Потеря доступа очищает закрытое содержимое. + +C00 1.6.0 перед реализацией расширен до комментария 1000 символов, как текущий ProjectScore, и optional RatingPage.evaluation_counts/case_options. Старые операции/ID/ошибки не меняются; числовой диапазон, True/False и обязательность сохраняются. Backend добавляет merge migration для двух веток project_rates.0008 и расширяет SolutionScore.value. + +## Критерии приемки этого блока + +| ID | Проверяемое поведение | +| ---- | ------------------------------------------------------------------------------------------------------------------------------ | +| UI01 | Список использует настоящие solutionId, серверные собственные счетчики и query-фильтры/пагинацию | +| UI02 | Сданные материалы и принятый состав видны по конкретной заявке; переход на вторую вкладку не теряет оценку | +| UI03 | Общие controls требуют числовые и булевы ответы, принимают 0/false и необязательный комментарий до 1000 | +| UI04 | Подтверждение показывает все ответы; pending дает один POST и не позволяет закрыть/уйти | +| UI05 | Saved/edit/closed состояния соответствуют серверу, показываются только собственные значения | +| UI06 | Network/revision errors сохраняют ввод; обновление критериев сохраняет совместимые значения, устаревшие ответы не отправляются | +| UI07 | Чужая программа/заявка и потеря доступа не раскрывают данные; поздний ответ прошлого контекста игнорируется | +| UI08 | Возврат разрешен только в собственный список или общий кабинет; несохраненный ввод защищен stay/leave и beforeunload | +| UI09 | Desktop/mobile, Mont, keyboard/focus, error/empty/loading/no access, отсутствие overflow и ошибок JS проверены в браузере | +| UI10 | Существующие Project scoring, native buttons/links и маршруты сохраняют поведение | + +## Проверка + +Backend: 181 PostgreSQL tests оценивания/результатов PASS, миграции применены локально; graph и migration consistency PASS. Angular development build, ESLint измененных файлов и Stylelint нового SCSS PASS. Полный Vitest: 2594 tests в 472 файлах PASS; strict C00 types PASS. После финального review guard отдельно 8 tests PASS; повторный build PASS. Locked Prettier 3.8.3: 1960 файлов при LF без нарушений; финально измененные файлы проверены отдельно. Source fix Button сохраняет native disabled только у button и aria-disabled/click guard у ссылок, устраняя наблюдавшийся NG0303. Отдельный тест проверяет disabled/loading и запрет перехода ссылки. + +Браузер: настоящие Angular/Django/PostgreSQL/JWT, вымышленные КЧ и хакатон, сохраненная оценка → изменение → 0/False/1000 символов → вторая вкладка «Команда» → отмена/подтверждение → один POST → сохранение → список/фильтры/счетчики PASS. Pending блокирует действия и Escape. Участнику запрещен экспертный frame; на 320/390/768/1440 нет горизонтального overflow и ошибок JavaScript. Дополнительные controlled HTTP сценарии UI: пустые критерии, закрытая оценка, network error с сохранением ввода, stay/leave, required/focus, Escape, criteria_changed → обновление ревизии с сохранением совместимых значений PASS. Эти подмененные HTTP состояния проверяют UI, а права/сроки проверены отдельными PostgreSQL tests. Нативное системное окно beforeunload не проверялось; HostListener и маршрутный guard проверены отдельно. Полный аудит контраста не заявляется. + +Evidence: `design-artifacts/program-solutions-20261024/expert-v1/implementation/` в workspace, browser-evidence.json, ui-states-evidence.json и screenshots desktop/mobile/confirmation/list. Состав и критерии на скриншотах вымышленные. В общих элементах оболочки на fixture отсутствуют изображения; новая форма использует Mont и существующие tokens. Timing/selector ошибки первых QA-скриптов исправлены; итоговые прогоны PASS. Ошибка нового readonly URL unit test была в обновлении прежнего FormGroup без изменения OnPush input; тест исправлен на замену frame/group, как в фактическом readonly сценарии. + +Requirement audit после review: + +| AC | Evidence | Verdict | +| ---- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| UI01 | list unit (query/cancel/counts), реальные HTTP/browser поиск/кейс/переход/возврат | PASS в новом списке | +| UI02 | реальный snapshot/Team обеих форм, unit сохраняет renderer и значения | PASS | +| UI03 | общий renderer tests, backend границы 1000/1001, browser 0/False/1000 | PASS | +| UI04 | unit один POST/blocked leave, browser подтверждение/отмена/pending/Escape | PASS | +| UI05 | source server canRate, unit saved/closed, browser saved/edit и controlled closed | PASS | +| UI06 | unit criteria refresh, browser controlled network и criteria_changed с сохранением | PASS | +| UI07 | backend permissions tests, actual HTTP участника, unit late POST/access loss | PASS в проверенном scope | +| UI08 | unit return URL/context/beforeunload, browser stay/leave/return | PASS; native beforeunload dialog не проверен | +| UI09 | реальные 320/390/768/1440, controlled states, focus/Escape, screenshots | PASS в указанном browser scope; полного a11y аудита нет | +| UI10 | полный регрессионный suite, shared renderer/Button tests, существующие Project routes | PASS по tests/source; старый Project browser flow здесь повторно не запускался | + +Блокирующих замечаний текущего scope после исправлений не осталось. На review сравнение по отображаемой подписи заменено сравнением исходных значений: буквальный комментарий «Не указано» теперь запускает защиту ухода и остается доступен для копирования при удалении критерия. Regression test проверяет guard и beforeunload. Это не приемка полного смешанного кабинета или административных экранов. + +Еще осталось в FE07/FE08: подключить смешанный кабинет к общему ApplicationRef API, оба входа программы к нужному формату, административные интерфейсы и результаты/выгрузки, остальную аналитику и публикацию. Не объявлять всю FE07 или feature готовой по этому блоку. diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts index 6790eb608..70f5f0341 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts @@ -4852,4 +4852,299 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ meta: {}, }, }, + { + name: "solution_rating_long_comment", + operationId: "rateSolution", + status: 200, + request: { + expectedCriteriaRevision: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + scores: [ + { + criterionId: 21, + value: "0", + }, + { + criterionId: 22, + value: "4.5", + }, + { + criterionId: 23, + value: "False", + }, + { + criterionId: 24, + value: + "АААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААА", + }, + ], + }, + response: { + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + id: 301, + programId: 12, + version: 10, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:05:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: true, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: true, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + ], + submission: { + required: true, + status: "submitted", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: "2026-10-23T10:05:00Z", + }, + permissions: { + canEdit: false, + canManageTeam: false, + canInvite: false, + canSubmit: false, + canLeave: false, + }, + readiness: { + isReady: true, + issues: [], + }, + }, + criteriaRevision: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + criteria: [ + { + id: 21, + name: "Практическая ценность", + description: "Польза аудитории", + type: "int", + minValue: 0, + maxValue: 10, + value: "0", + }, + { + id: 22, + name: "Проработанность", + description: null, + type: "float", + minValue: 0, + maxValue: 5, + value: "4.5", + }, + { + id: 23, + name: "Показан прототип", + description: null, + type: "bool", + minValue: null, + maxValue: null, + value: "False", + }, + { + id: 24, + name: "Рекомендация", + description: null, + type: "str", + minValue: null, + maxValue: null, + value: + "АААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААААА", + }, + ], + managedEvaluation: true, + canRate: true, + blockedReason: null, + isCompleted: true, + ratedCount: 1, + maxRates: 3, + requiredEvaluators: 2, + deadline: "2026-10-30T18:00:00Z", + }, + }, + { + name: "solution_rating_counts_before_status_and_page", + operationId: "getSolutionRatings", + status: 200, + response: { + count: 1, + next: null, + previous: null, + results: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + caseName: "Цифровой сервис", + assigned: true, + isCompleted: false, + canRate: true, + blockedReason: null, + }, + ], + evaluationCounts: { + pending: 1, + completed: 0, + }, + caseOptions: ["Цифровой сервис"], + }, + }, ]; diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts index 6e48acf36..857a234d1 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts @@ -1,6 +1,6 @@ /** @format */ -// Сгенерировано tools/generate-solution-contract.py из C00 1.5.0. +// Сгенерировано tools/generate-solution-contract.py из C00 1.6.0. // camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются. export type SolutionUser = { @@ -403,6 +403,11 @@ export type SolutionRatingPage = { next: string | null; previous: string | null; results: Array; + evaluationCounts?: { + pending: number; + completed: number; + }; + caseOptions?: Array; }; export type SolutionRatingFrame = { diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.ts b/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.ts index 370acf7fc..b2209289f 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.ts @@ -25,9 +25,11 @@ import { } from "@angular/forms"; import { noop, Subscription } from "rxjs"; import { RangeCriterionInputComponent } from "./components/range-criterion-input/range-criterion-input.component"; -import { ProjectRatingCriterion } from "@domain/project/project-rating-criterion"; import { ErrorMessage } from "@core/lib/models/error/error-message"; -import { evaluationCriterionControl } from "@utils/evaluation-criterion-control"; +import { + evaluationCriterionControl, + EvaluationCriterion, +} from "@utils/evaluation-criterion-control"; import { ButtonDirective, FieldDirective } from "@uilib"; /** Оценка проекта по числовым, булевым и текстовым критериям через ControlValueAccessor. */ @@ -58,7 +60,7 @@ import { ButtonDirective, FieldDirective } from "@uilib"; }) export class ProjectRatingComponent implements OnDestroy, ControlValueAccessor, Validator { @Input({ required: true }) - set criteria(val: ProjectRatingCriterion[]) { + set criteria(val: EvaluationCriterion[]) { if (!val) return; const ordered = [...val.filter(c => c.type !== "str"), ...val.filter(c => c.type === "str")]; this._criteria.set(ordered); @@ -70,7 +72,7 @@ export class ProjectRatingComponent implements OnDestroy, ControlValueAccessor, }); } - get criteria(): ProjectRatingCriterion[] { + get criteria(): EvaluationCriterion[] { return this._criteria(); } @@ -116,7 +118,7 @@ export class ProjectRatingComponent implements OnDestroy, ControlValueAccessor, readonly layout = input<"compact" | "full">("compact"); /** Сигнал для хранения критериев оценки */ - _criteria = signal([]); + _criteria = signal([]); /** Форма для управления всеми критериями оценки */ form!: FormGroup; @@ -177,7 +179,7 @@ export class ProjectRatingComponent implements OnDestroy, ControlValueAccessor, this.subscriptions$().forEach(subscription => subscription.unsubscribe()); } - private createFormControls(criteria: ProjectRatingCriterion[]): void { + private createFormControls(criteria: EvaluationCriterion[]): void { this.subscriptions$().forEach(subscription => subscription.unsubscribe()); this.subscriptions$.set([]); const formGroupControls: Record = {}; diff --git a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.html b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.html new file mode 100644 index 000000000..2c748cf90 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.html @@ -0,0 +1,168 @@ + + +
+ На страницу программы +
+
+

Оценка решений

+

{{ program()?.name }}

+
+
+
+ +
+ +
+ @if (loading()) { +

Загружаем решения…

+ } @else if (error(); as problem) { +
+

{{ problem.detail }}

+ + @if (filtered()) { + + } +
+ } @else if (data(); as pageData) { +
    + @for (row of pageData.results; track row.application.solutionId) { +
  • +
    +

    {{ row.application.name }}

    + @if (row.caseName) { +

    {{ row.caseName }}

    + } + @if (!row.canRate && !row.isCompleted) { +

    {{ row.blockedReason }}

    + } +
    + {{ + row.isCompleted ? "Оценено" : "Не оценено" + }} + @if (row.canRate || row.isCompleted) { + {{ row.isCompleted ? "Посмотреть" : "Оценить" }} + } @else { + + } +
  • + } @empty { +
  • +

    + {{ + filtered() + ? "Решения не найдены" + : status() === "completed" + ? "Сохраненных оценок пока нет" + : counts().completed + ? "Все решения оценены" + : "Пока нет решений для оценки" + }} +

    +

    + {{ + filtered() ? "Измените поиск или фильтры." : "Здесь появятся доступные вам решения." + }} +

    + @if (filtered()) { + + } +
  • + } +
+ @if (count()) { + + } + } +
+
+
+
+

Фильтры

+ +
+
+ +
+
+ +
+
diff --git a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.spec.ts new file mode 100644 index 000000000..404714cef --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.spec.ts @@ -0,0 +1,113 @@ +/** @format */ + +import { signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { ActivatedRoute, convertToParamMap, provideRouter, Router } from "@angular/router"; +import { BehaviorSubject, of, Subject } from "rxjs"; +import { GetSolutionRatingsUseCase } from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/program-detail-main-ui-info.service"; +import { SolutionRatingPage } from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { ok, fail } from "@domain/shared/result.type"; +import { SolutionEvaluationListComponent } from "./solution-evaluation-list.component"; + +async function setup(initial: Record = {}) { + const response = structuredClone( + solutionContractScenarios.find(s => s.operationId === "getSolutionRatings" && s.status === 200)! + .response, + ) as SolutionRatingPage; + response.evaluationCounts = { pending: 7, completed: 12 }; + response.caseOptions = ["Задача 1", "Задача 2"]; + const params = new BehaviorSubject(convertToParamMap({ programId: "12" })); + const query = new BehaviorSubject(convertToParamMap(initial)); + const route = { paramMap: params, queryParamMap: query, pathFromRoot: [] as unknown[] }; + route.pathFromRoot = [route]; + const get = { execute: vi.fn(() => of(ok(response))) }; + TestBed.configureTestingModule({ + imports: [SolutionEvaluationListComponent], + providers: [ + provideRouter([]), + { provide: ActivatedRoute, useValue: route }, + { provide: GetSolutionRatingsUseCase, useValue: get }, + { + provide: ProgramDetailMainUIInfoService, + useValue: { program: signal({ id: 12, name: "Кейс-чемпионат" }) }, + }, + ], + }); + const fixture = TestBed.createComponent(SolutionEvaluationListComponent); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + return { response, fixture, page: fixture.componentInstance, params, query, get }; +} + +describe("Solution expert list", () => { + it("uses server counts before pagination and real solution links", async () => { + const { page, fixture, response } = await setup(); + expect(page.counts()).toEqual({ pending: 7, completed: 12 }); + expect(fixture.nativeElement.textContent).toContain("Осталось оценить · 7"); + const row = response.results[0]; + if (row) + expect( + fixture.nativeElement.querySelector(".rating-list__rows a")?.getAttribute("href"), + ).toContain(`/solutions/${row.application.solutionId}/evaluation`); + expect(page.caseOptions()).toEqual(["Задача 1", "Задача 2"]); + }); + it("restores query filters/status/page and resets pagination on changing status", async () => { + const { page, get } = await setup({ + status: "completed", + q: "Кейс", + case: "Задача 2", + page: "2", + }); + expect(get.execute).toHaveBeenCalledWith(12, { + status: "completed", + search: "Кейс", + caseScope: "selected", + caseName: "Задача 2", + limit: 20, + offset: 20, + }); + const navigate = vi.spyOn(TestBed.inject(Router), "navigate").mockResolvedValue(true); + page.setStatus("pending"); + expect(navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: { status: null, page: null }, + queryParamsHandling: "merge", + }), + ); + page.openFilters(); + page.caseValue.setValue("Задача 1"); + page.filtersOpen.set(false); + page.openFilters(); + expect(page.caseValue.value).toBe("Задача 2"); + }); + it("requires server counts and shows actual errors instead of inventing numbers", async () => { + const { page, get, response, fixture } = await setup(); + delete response.evaluationCounts; + page.reload(); + fixture.detectChanges(); + expect(page.data()).toBeNull(); + expect(page.error()?.code).toBe("missing_counts"); + get.execute.mockReturnValue( + of( + fail({ code: "not_program_expert", detail: "Нет доступа.", errors: [], meta: {} }), + ) as never, + ); + page.reload(); + expect(page.error()?.code).toBe("not_program_expert"); + }); + it("cancels previous-program reads and keeps the current scope when late data arrives", async () => { + const { page, get, params, response } = await setup(); + const pending = new Subject>>(); + get.execute.mockReturnValue(pending); + page.reload(); + get.execute.mockReturnValue(of(ok({ ...response, results: [], count: 0 }))); + params.next(convertToParamMap({ programId: "99" })); + pending.next(ok(response)); + expect(get.execute).toHaveBeenLastCalledWith(99, expect.anything()); + expect(page.data()?.results).toEqual([]); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.ts b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.ts new file mode 100644 index 000000000..703081c63 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.ts @@ -0,0 +1,141 @@ +/** @format */ + +import { ChangeDetectionStrategy, Component, computed, inject, signal } from "@angular/core"; +import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { + combineLatest, + debounceTime, + distinctUntilChanged, + map, + startWith, + Subject, + switchMap, +} from "rxjs"; +import { GetSolutionRatingsUseCase } from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/program-detail-main-ui-info.service"; +import { EvaluationNavigationService } from "@api/program/evaluation-navigation.service"; +import { SolutionApiError, SolutionRatingPage } from "@domain/solution/solution-contract.model"; +import { ButtonDirective, FieldDirective, IconComponent } from "@uilib"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; + +@Component({ + selector: "app-solution-evaluation-list", + imports: [ + RouterLink, + ReactiveFormsModule, + ButtonDirective, + FieldDirective, + IconComponent, + ModalComponent, + ], + templateUrl: "./solution-evaluation-list.component.html", + styleUrl: "./solution-evaluation.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionEvaluationListComponent { + private readonly route = inject(ActivatedRoute); + readonly router = inject(Router); + private readonly get = inject(GetSolutionRatingsUseCase); + readonly navigation = inject(EvaluationNavigationService); + readonly program = inject(ProgramDetailMainUIInfoService).program; + private readonly retry = new Subject(); + private programId = 0; + private appliedCase = ""; + readonly data = signal(null); + readonly loading = signal(true); + readonly error = signal(null); + readonly search = new FormControl("", { nonNullable: true }); + readonly caseValue = new FormControl("", { nonNullable: true }); + readonly caseOptions = signal([]); + readonly filtersOpen = signal(false); + readonly page = signal(1); + readonly status = signal<"pending" | "completed">("pending"); + readonly filtered = signal(false); + readonly count = computed(() => this.data()?.count ?? 0); + readonly counts = computed(() => this.data()?.evaluationCounts ?? { pending: 0, completed: 0 }); + constructor() { + this.search.valueChanges + .pipe(debounceTime(250), distinctUntilChanged(), takeUntilDestroyed()) + .subscribe(q => this.navigate({ q: q.trim() || null, page: null })); + combineLatest([ + combineLatest(this.route.pathFromRoot.map(r => r.paramMap)), + this.route.queryParamMap, + this.retry.pipe(startWith(undefined)), + ]) + .pipe( + map(([pathParams, params]) => { + const programId = Number(pathParams.find(p => p.has("programId"))?.get("programId")); + if (this.programId !== programId) { + this.programId = programId; + this.caseOptions.set([]); + } + const page = Number(params.get("page")); + this.page.set(Number.isSafeInteger(page * 20) && page > 0 ? page : 1); + this.status.set(params.get("status") === "completed" ? "completed" : "pending"); + this.search.setValue(params.get("q") ?? "", { emitEvent: false }); + this.caseValue.setValue(params.get("case") ?? "", { emitEvent: false }); + this.appliedCase = this.caseValue.value; + this.filtered.set(!!this.search.value || !!this.caseValue.value); + return { + status: this.status(), + search: this.search.value.trim(), + caseScope: this.caseValue.value ? ("selected" as const) : ("all" as const), + ...(this.caseValue.value ? { caseName: this.caseValue.value } : {}), + limit: 20, + offset: (this.page() - 1) * 20, + }; + }), + switchMap(query => { + this.loading.set(true); + this.error.set(null); + this.data.set(null); + return this.get.execute(this.programId, query); + }), + takeUntilDestroyed(), + ) + .subscribe(result => { + this.loading.set(false); + if (result.ok && result.value.evaluationCounts) { + this.data.set(result.value); + this.caseOptions.set(result.value.caseOptions ?? []); + this.navigation.restore(this.router.url); + } else + this.error.set( + result.ok + ? { + code: "missing_counts", + detail: "Обновите данные оценивания. Не удалось загрузить счетчики.", + errors: [], + meta: {}, + } + : result.error, + ); + }); + } + openFilters(): void { + this.caseValue.setValue(this.appliedCase); + this.filtersOpen.set(true); + } + navigate(queryParams: Record): void { + void this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: "merge", + }); + } + reload(): void { + this.retry.next(); + } + reset(): void { + this.navigate({ q: null, case: null, page: null }); + } + setStatus(status: "pending" | "completed"): void { + this.navigate({ status: status === "completed" ? status : null, page: null }); + } + applyFilters(): void { + this.filtersOpen.set(false); + this.navigate({ case: this.caseValue.value || null, page: null }); + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html new file mode 100644 index 000000000..dda751b58 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html @@ -0,0 +1,186 @@ + + +
+ {{ + returnUrl().startsWith("/office/evaluation") ? "В кабинет эксперта" : "К оценке решений" + }} + @if (loading()) { +

Оценка решения

+

Загружаем решение…

+ } @else if (error(); as problem) { +

Решение недоступно

+

{{ problem.detail }}

+ @if (problem.code === "unauthorized" || problem.code === "authentication_required") { + Войти в аккаунт + } @else { + + } + } @else if (frame(); as data) { +
+
+

{{ data.solution.name }}

+

{{ data.program.name }}

+ @if (data.deadline) { +

Оценить до {{ data.deadline | date: "dd.MM.yyyy, HH:mm" }}

+ } +
+
+ +
+ +
+
+

Моя оценка

+ @if (data.isCompleted && !editing()) { + Оценка сохранена + } +
+ @if (!data.canRate) { +

+ {{ data.blockedReason || "Оценка доступна только для просмотра." }} +

+ } + @if (!data.criteria.length) { +

Организатор еще не настроил критерии оценки.

+ } @else if (!editing()) { +
+ @for (c of ordered(); track c.id) { +
+
{{ c.name }}
+
{{ text(form.getRawValue()["" + c.id]) }}
+
+ } +
+ @if (data.canRate) { + + } + } @else { + + @if (saveError(); as problem) { + + } +
+ +
+ } + @if (previous().length) { +
+ Предыдущие ответы + @for (item of previous(); track $index) { +

{{ item.name }}: {{ item.value }}

+ } +
+ } +
+
+
+ +
+
+
+

Проверьте оценку

+ +
+
+

{{ data.solution.name }}

+
+ @for (c of ordered(); track c.id) { +
+
{{ c.name }}
+
{{ text(form.getRawValue()["" + c.id]) }}
+
+ } +
+
+
+ +
+
+ } +
+
+

Выйти без сохранения?

+ +
+

Введенные ответы не сохранятся.

+
+ +
+
+
diff --git a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.scss b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.scss new file mode 100644 index 000000000..695386fa1 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.scss @@ -0,0 +1,42 @@ +@use "../rating/rating-workspace.component"; +@use "../detail/list/rating-card/rating-card.component"; + +[hidden] { + display: none !important; +} + +.solution-evaluation-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.solution-evaluation-sections { + display: flex; + gap: 4px; + width: min(100%, 432px); + padding: 4px; + background: var(--medium-grey-for-outline); + border-radius: 24px; +} + +.solution-evaluation-sections .ui-button { + flex: 1; + min-height: 40px; + color: var(--black); + background: transparent; + border-color: transparent; +} + +.solution-evaluation-sections [aria-current="page"] { + color: var(--accent); + background: var(--white, #fff); +} + +.scoring__materials { + overflow-wrap: anywhere; +} + +.rating-list__rows li.rating-list__empty { + grid-template-columns: minmax(0, 1fr); +} diff --git a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.spec.ts new file mode 100644 index 000000000..8e0735126 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.spec.ts @@ -0,0 +1,232 @@ +/** @format */ + +import { Component, input } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { ActivatedRoute, convertToParamMap, provideRouter } from "@angular/router"; +import { BehaviorSubject, of, Subject } from "rxjs"; +import { + GetSolutionRatingUseCase, + RateSolutionUseCase, +} from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { fail, ok } from "@domain/shared/result.type"; +import { SolutionRatingFrame } from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { SolutionTeamComponent } from "../solution/solution-team.component"; +import { SolutionEvaluationComponent } from "./solution-evaluation.component"; + +@Component({ selector: "app-solution-team", template: "

Принятый состав

" }) +class TeamStub { + readonly solution = input(); + readonly blocked = input(); +} +const frame = () => + structuredClone( + solutionContractScenarios.find(s => s.operationId === "getSolutionRating" && s.status === 200)! + .response, + ) as SolutionRatingFrame; +const problem = (code: string) => ({ code, detail: "Проверьте оценку.", errors: [], meta: {} }); + +async function setup(saved = false, initialReturn = "/office/evaluation?tab=completed&q=работа") { + const data = frame(); + data.isCompleted = saved; + data.canRate = true; + data.criteria.forEach( + c => + (c.value = saved + ? c.type === "bool" + ? "False" + : c.type === "str" + ? "Комментарий" + : "0" + : null), + ); + const params = new BehaviorSubject( + convertToParamMap({ programId: String(data.program.id), solutionId: String(data.solution.id) }), + ); + const query = new BehaviorSubject(convertToParamMap({ returnUrl: initialReturn })); + const route = { + paramMap: params, + queryParamMap: query, + snapshot: { queryParamMap: query.value }, + pathFromRoot: [] as unknown[], + }; + route.pathFromRoot = [route]; + const get = { execute: vi.fn(() => of(ok(data))) }; + const rate = { execute: vi.fn(() => of(ok({ ...data, isCompleted: true }))) }; + TestBed.configureTestingModule({ + imports: [SolutionEvaluationComponent], + providers: [ + provideRouter([]), + { provide: ActivatedRoute, useValue: route }, + { provide: GetSolutionRatingUseCase, useValue: get }, + { provide: RateSolutionUseCase, useValue: rate }, + ], + }); + TestBed.overrideComponent(SolutionEvaluationComponent, { + remove: { imports: [SolutionTeamComponent] }, + add: { imports: [TeamStub] }, + }); + const fixture = TestBed.createComponent(SolutionEvaluationComponent); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + const page = fixture.componentInstance; + const fill = async () => { + for (const c of data.criteria) + page + .renderer()! + .form.get(String(c.id))! + .setValue(c.type === "bool" ? false : c.type === "str" ? "Текст" : 0); + fixture.detectChanges(); + await fixture.whenStable(); + }; + return { data, page, fixture, params, get, rate, fill }; +} + +describe("Solution evaluation using the shared criterion form", () => { + it("keeps literal True/False comments as text while rendering actual booleans", async () => { + const { page } = await setup(); + expect(page.text("False")).toBe("False"); + expect(page.text("True")).toBe("True"); + expect(page.text(false)).toBe("Нет"); + expect(page.text(0)).toBe("0"); + }); + it("protects a literal empty-answer label and retains it when the criterion disappears", async () => { + const { page, data, get } = await setup(); + const comment = data.criteria.find(c => c.type === "str")!; + page.form.setValue({ ...page.form.getRawValue(), [comment.id]: "Не указано" }); + expect(page.hasUnsavedChanges()).toBe(true); + const beforeUnload = { preventDefault: vi.fn(), returnValue: undefined }; + page.beforeUnload(beforeUnload as unknown as BeforeUnloadEvent); + expect(beforeUnload.preventDefault).toHaveBeenCalledOnce(); + expect(beforeUnload.returnValue).toBe(""); + const newer = structuredClone(data); + newer.criteria = newer.criteria.filter(c => c.id !== comment.id); + get.execute.mockReturnValue(of(ok(newer))); + page.reload(true); + expect(page.previous()).toEqual([{ name: comment.name, value: "Не указано" }]); + }); + it("requires numeric/boolean answers, preserves zero/false and keeps inputs across the Team tab", async () => { + const { page, fixture, fill } = await setup(); + page.prepare(); + expect(page.confirmation()).toBe(false); + await fill(); + const renderer = page.renderer(); + page.teamTab.set(true); + fixture.detectChanges(); + page.teamTab.set(false); + fixture.detectChanges(); + expect(page.renderer()).toBe(renderer); + page.prepare(); + expect(page.confirmation()).toBe(true); + expect(fixture.nativeElement.textContent).toContain("Нет"); + page.closeConfirmation(); + expect(page.hasUnsavedChanges()).toBe(true); + }); + it("sends the real criterion IDs, confirmed revision and zero/False exactly once while pending", async () => { + const { page, fixture, data, rate, fill } = await setup(); + const pending = new Subject>>(); + rate.execute.mockReturnValue(pending); + await fill(); + page.prepare(); + page.confirm(); + page.confirm(); + page.closeConfirmation(); + expect(rate.execute).toHaveBeenCalledTimes(1); + expect(page.confirmation()).toBe(true); + expect(page.canLeave()).toBe(false); + expect(rate.execute.mock.calls[0]).toEqual([ + data.program.id, + data.solution.id, + { + expectedCriteriaRevision: data.criteriaRevision, + scores: data.criteria.map(c => ({ + criterionId: c.id, + value: c.type === "bool" ? "False" : c.type === "str" ? "Текст" : "0", + })), + }, + ]); + const saved = structuredClone(data); + saved.isCompleted = true; + saved.criteria.forEach( + c => (c.value = c.type === "bool" ? "False" : c.type === "str" ? "Текст" : "0"), + ); + pending.next(ok(saved)); + pending.complete(); + fixture.detectChanges(); + expect(page.editing()).toBe(false); + expect(page.hasUnsavedChanges()).toBe(false); + expect(fixture.nativeElement.textContent).toContain("Оценка сохранена"); + page.edit(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(page.hasUnsavedChanges()).toBe(false); + }); + it("keeps local answers after a failed save and refreshes changed criteria by ID/type", async () => { + const { page, fixture, data, rate, get, fill } = await setup(); + rate.execute.mockReturnValue(of(fail(problem("criteria_changed"))) as never); + await fill(); + page.prepare(); + page.confirm(); + expect(page.saveError()?.code).toBe("criteria_changed"); + expect(page.hasUnsavedChanges()).toBe(true); + const newer = structuredClone(data); + newer.criteriaRevision = "f".repeat(64); + newer.criteria[0].name = "Обновленный критерий"; + get.execute.mockReturnValue(of(ok(newer))); + page.reload(true); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + expect(page.frame()?.criteriaRevision).toBe(newer.criteriaRevision); + expect(page.form.getRawValue()[String(data.criteria[0].id)]).toBe(0); + expect(page.saveError()).toBeNull(); + page.prepare(); + expect(page.confirmation()).toBe(true); + }); + it("shows saved and closed results read-only and clears private data when access is lost", async () => { + const { page, fixture, rate, get, data } = await setup(true); + expect(page.editing()).toBe(false); + page.edit(); + fixture.detectChanges(); + await fixture.whenStable(); + rate.execute.mockReturnValue(of(fail(problem("not_found"))) as never); + page.prepare(); + page.confirm(); + fixture.detectChanges(); + expect(page.frame()).toBeNull(); + expect(page.form.value).toEqual({}); + const closed = structuredClone(data); + closed.canRate = false; + closed.blockedReason = "Срок оценивания завершен."; + get.execute.mockReturnValue(of(ok(closed))); + page.reload(); + fixture.detectChanges(); + expect(page.editing()).toBe(false); + page.edit(); + expect(page.editing()).toBe(false); + }); + it("protects local input on leave, cancel, and ignores a previous context's late save", async () => { + const { page, fixture, rate, params, data, fill } = await setup(); + await fill(); + const stay = page.canLeave(); + page.answerLeave(false); + expect(await stay).toBe(false); + const leave = page.canLeave(); + page.answerLeave(true); + expect(await leave).toBe(true); + const pending = new Subject>>(); + rate.execute.mockReturnValue(pending); + page.prepare(); + page.confirm(); + params.next(convertToParamMap({ programId: "999", solutionId: "998" })); + fixture.detectChanges(); + pending.next(ok({ ...data, isCompleted: true })); + expect(page.editing()).toBe(true); + expect(page.busy()).toBe(false); + }); + it("rejects external or other-program return destinations", async () => { + const { page, data } = await setup(false, "https://example.test/private"); + expect(page.returnUrl()).toBe(`/office/program/${data.program.id}/solutions-rating`); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts new file mode 100644 index 000000000..5009b0f84 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts @@ -0,0 +1,326 @@ +/** @format */ + +import { DatePipe } from "@angular/common"; +import { + ChangeDetectionStrategy, + Component, + HostListener, + inject, + signal, + viewChild, + DestroyRef, + computed, +} from "@angular/core"; +import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { combineLatest, EMPTY, map, startWith, Subject, switchMap, takeUntil } from "rxjs"; +import { + GetSolutionRatingUseCase, + RateSolutionUseCase, +} from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { SolutionApiError, SolutionRatingFrame } from "@domain/solution/solution-contract.model"; +import { validSolutionId } from "@domain/solution/ports/solution.repository.port"; +import { ButtonDirective } from "@uilib"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; +import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fields.component"; +import { SolutionTeamComponent } from "../solution/solution-team.component"; +import { ProjectRatingComponent } from "../detail/list/rating-card/project-rating/project-rating.component"; +import { EvaluationCriterion } from "@utils/evaluation-criterion-control"; + +type Values = Record; + +@Component({ + selector: "app-solution-evaluation", + imports: [ + DatePipe, + RouterLink, + ReactiveFormsModule, + ButtonDirective, + ModalComponent, + SolutionFieldsComponent, + SolutionTeamComponent, + ProjectRatingComponent, + ], + templateUrl: "./solution-evaluation.component.html", + styleUrl: "./solution-evaluation.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionEvaluationComponent { + private readonly route = inject(ActivatedRoute); + readonly router = inject(Router); + private readonly get = inject(GetSolutionRatingUseCase); + private readonly rate = inject(RateSolutionUseCase); + private readonly destroy = inject(DestroyRef); + private readonly retry = new Subject(); + private readonly contextChanged = new Subject(); + private context = { programId: 0, solutionId: 0 }; + private resolveLeave?: (value: boolean) => void; + readonly frame = signal(null); + readonly loading = signal(true); + readonly error = signal(null); + readonly saveError = signal(null); + readonly busy = signal(false); + readonly editing = signal(false); + readonly teamTab = signal(false); + readonly confirmation = signal(false); + readonly leaveOpen = signal(false); + readonly previous = signal<{ name: string; value: string }[]>([]); + readonly form = new FormControl({}, { nonNullable: true }); + readonly materials = signal(new FormGroup({})); + readonly renderer = viewChild(ProjectRatingComponent); + readonly criteria = computed(() => + (this.frame()?.criteria ?? []).map(c => ({ + ...c, + description: c.description ?? "", + value: c.value ?? "", + })), + ); + readonly ordered = computed(() => [ + ...this.criteria().filter(c => c.type !== "str"), + ...this.criteria().filter(c => c.type === "str"), + ]); + readonly returnUrl = signal("/office/evaluation"); + readonly returnTree = computed(() => this.router.parseUrl(this.returnUrl())); + + constructor() { + this.destroy.onDestroy(() => { + this.resolveLeave?.(false); + this.contextChanged.next(); + }); + combineLatest([ + combineLatest(this.route.pathFromRoot.map(r => r.paramMap)), + this.retry.pipe(startWith(false)), + ]) + .pipe( + map(([params, preserve]) => ({ + programId: Number(params.find(p => p.has("programId"))?.get("programId")), + solutionId: Number(params.find(p => p.has("solutionId"))?.get("solutionId")), + preserve, + })), + switchMap(({ programId, solutionId, preserve }) => { + preserve = + preserve && + this.context.programId === programId && + this.context.solutionId === solutionId; + if (this.context.programId !== programId || this.context.solutionId !== solutionId) + this.teamTab.set(false); + this.contextChanged.next(); + this.context = { programId, solutionId }; + this.loading.set(true); + this.error.set(null); + this.confirmation.set(false); + this.busy.set(false); + if (!preserve) { + this.frame.set(null); + this.form.reset({}); + this.previous.set([]); + } + const destination = this.route.snapshot.queryParamMap.get("returnUrl"); + const list = `/office/program/${programId}/solutions-rating`; + this.returnUrl.set( + destination && + (/^\/office\/evaluation(?:\?[^#]*)?$/.test(destination) || + destination.split("?")[0] === list) + ? destination + : list, + ); + if (!validSolutionId(programId) || !validSolutionId(solutionId)) { + this.loading.set(false); + this.error.set({ + code: "not_found", + detail: "Решение не найдено.", + errors: [], + meta: {}, + }); + return EMPTY; + } + return this.get + .execute(programId, solutionId) + .pipe(map(result => ({ result, preserve }))); + }), + takeUntilDestroyed(), + ) + .subscribe(({ result, preserve }) => { + this.loading.set(false); + if (result.ok) this.hydrate(result.value, preserve); + else { + this.frame.set(null); + this.form.reset({}); + this.previous.set([]); + this.error.set(result.error); + } + }); + } + + reload(preserve = false): void { + if (!this.busy()) this.retry.next(preserve); + } + private valuesFor(frame: SolutionRatingFrame): Values { + return Object.fromEntries( + frame.criteria.map(c => [ + String(c.id), + c.type === "bool" + ? c.value === null + ? null + : c.value === "True" + : c.type === "str" + ? (c.value ?? "") + : c.value === null || c.value === "" + ? null + : Number(c.value), + ]), + ); + } + private hydrate(frame: SolutionRatingFrame, preserve: boolean): void { + const old = this.frame(); + const values = this.valuesFor(frame); + const local = this.form.getRawValue(); + const previous: { name: string; value: string }[] = []; + if (preserve && old) { + for (const criterion of old.criteria) { + const current = frame.criteria.find( + c => c.id === criterion.id && c.type === criterion.type, + ); + if (current && frame.canRate) values[String(criterion.id)] = local[String(criterion.id)]; + else if ( + !this.sameValue(local[String(criterion.id)], this.valuesFor(old)[String(criterion.id)]) + ) + previous.push({ name: criterion.name, value: this.text(local[String(criterion.id)]) }); + } + } + this.previous.set(previous); + this.frame.set(frame); + this.materials.set( + new FormGroup( + Object.fromEntries( + frame.solution.answers.map(a => [String(a.fieldId), new FormControl(a.value)]), + ), + ), + ); + this.form.reset(values); + if (frame.canRate) this.form.enable({ emitEvent: false }); + else this.form.disable({ emitEvent: false }); + this.editing.set(frame.canRate && (!frame.isCompleted || preserve)); + this.saveError.set(null); + } + text(value: unknown): string { + return value === false + ? "Нет" + : value === true + ? "Да" + : value === null || value === undefined || value === "" + ? "Не указано" + : String(value); + } + private sameValue(left: unknown, right: unknown): boolean { + return (left ?? "") === (right ?? ""); + } + hasUnsavedChanges(): boolean { + const frame = this.frame(); + if (!frame || !this.editing()) return false; + const original = this.valuesFor(frame), + current = this.form.getRawValue(); + return frame.criteria.some(c => !this.sameValue(original[String(c.id)], current[String(c.id)])); + } + prepare(): void { + if (this.busy() || !this.frame()?.canRate || this.saveError()?.code === "criteria_changed") + return; + if (!this.renderer()?.validateAndFocus()) return; + this.confirmation.set(true); + } + closeConfirmation(): void { + if (!this.busy()) this.confirmation.set(false); + } + confirm(): void { + const frame = this.frame(); + if (!frame || !frame.canRate || !this.confirmation() || this.busy()) return; + this.busy.set(true); + this.form.disable({ emitEvent: false }); + this.saveError.set(null); + const values = this.form.getRawValue(); + const scores = frame.criteria.map(c => ({ + criterionId: c.id, + value: + c.type === "str" + ? String(values[String(c.id)] ?? "") + : values[String(c.id)] === null || + values[String(c.id)] === undefined || + values[String(c.id)] === "" + ? null + : c.type === "bool" + ? values[String(c.id)] + ? "True" + : "False" + : String(Number(values[String(c.id)])), + })); + this.rate + .execute(this.context.programId, this.context.solutionId, { + expectedCriteriaRevision: frame.criteriaRevision, + scores, + }) + .pipe(takeUntil(this.contextChanged), takeUntilDestroyed(this.destroy)) + .subscribe(result => { + this.busy.set(false); + if (this.frame()?.canRate) this.form.enable({ emitEvent: false }); + this.confirmation.set(false); + if (result.ok) { + this.hydrate(result.value, false); + this.editing.set(false); + } else { + this.saveError.set( + ["network", "server_error"].includes(result.error.code) + ? { + ...result.error, + detail: + "Не удалось сохранить оценку. Ответы остались в форме. Попробуйте еще раз.", + } + : result.error, + ); + if ( + [ + "not_found", + "forbidden", + "not_assigned", + "permission_denied", + "unauthorized", + "authentication_required", + ].includes(result.error.code) + ) { + this.error.set(result.error); + this.frame.set(null); + this.form.reset({}); + this.previous.set([]); + } else if ( + ["evaluation_closed", "max_rates_reached", "own_team"].includes(result.error.code) + ) + this.reload(true); + } + }); + } + edit(): void { + if (this.frame()?.canRate && !this.busy()) this.editing.set(true); + } + canLeave(): boolean | Promise { + if (this.busy()) return false; + if (!this.hasUnsavedChanges()) return true; + if (this.resolveLeave) return false; + this.confirmation.set(false); + this.leaveOpen.set(true); + return new Promise(resolve => { + this.resolveLeave = resolve; + }); + } + answerLeave(value: boolean): void { + this.leaveOpen.set(false); + this.resolveLeave?.(value); + this.resolveLeave = undefined; + } + @HostListener("window:beforeunload", ["$event"]) + beforeUnload(event: BeforeUnloadEvent): void { + if (this.busy() || this.hasUnsavedChanges()) { + event.preventDefault(); + event.returnValue = ""; + } + } +} diff --git a/projects/social_platform/src/app/ui/routes/program/detail.routes.ts b/projects/social_platform/src/app/ui/routes/program/detail.routes.ts index 80d44b1d6..03c9ae30c 100644 --- a/projects/social_platform/src/app/ui/routes/program/detail.routes.ts +++ b/projects/social_platform/src/app/ui/routes/program/detail.routes.ts @@ -16,6 +16,24 @@ import { RatingWorkspaceComponent } from "@ui/pages/program/rating/rating-worksp /** Маршруты детальной страницы программы: информация, проекты и участники. */ export const PROGRAM_DETAIL_ROUTES: Routes = [ + { + path: "solutions-rating", + providers: [ProgramDetailMainUIInfoService], + resolve: { data: ProgramDetailResolver }, + runGuardsAndResolvers: "pathParamsChange", + loadComponent: () => + import("@ui/pages/program/solution-evaluation/solution-evaluation-list.component").then( + m => m.SolutionEvaluationListComponent, + ), + }, + { + path: "solutions/:solutionId/evaluation", + canDeactivate: [canLeaveManagement], + loadComponent: () => + import("@ui/pages/program/solution-evaluation/solution-evaluation.component").then( + m => m.SolutionEvaluationComponent, + ), + }, ...["projects-rating", "projects-rating/:projectId"].map(path => ({ path, component: RatingWorkspaceComponent, diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html index 87bf9ae16..804c03394 100644 --- a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html @@ -8,7 +8,9 @@ > @if (readonly()) { @if (fileLink(field); as link) { - Открыть файл + {{ + field.type === "url" ? "Открыть ссылку" : "Открыть файл" + }} } @else {

{{ labelValue(field) }}

} diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.spec.ts b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.spec.ts new file mode 100644 index 000000000..0e3ce6017 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.spec.ts @@ -0,0 +1,79 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { FormControl, FormGroup } from "@angular/forms"; +import { SolutionField } from "@domain/solution/solution-contract.model"; +import { SolutionFieldsComponent } from "./solution-fields.component"; + +describe("Readonly solution materials", () => { + it("opens safe URL answers while preserving zero and false as actual answers", () => { + const validation = { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }; + const fields: SolutionField[] = [ + { + id: 1, + type: "url", + purpose: "repository", + label: "Репозиторий", + helpText: "", + required: false, + position: 0, + options: [], + validation, + }, + { + id: 2, + type: "number", + purpose: "other", + label: "Число", + helpText: "", + required: false, + position: 1, + options: [], + validation, + }, + { + id: 3, + type: "checkbox", + purpose: "other", + label: "Прототип", + helpText: "", + required: false, + position: 2, + options: [], + validation, + }, + ]; + const group = new FormGroup({ + "1": new FormControl("https://example.test/repository"), + "2": new FormControl(0), + "3": new FormControl(false), + }); + const fixture = TestBed.createComponent(SolutionFieldsComponent); + fixture.componentRef.setInput("fields", fields); + fixture.componentRef.setInput("group", group); + fixture.componentRef.setInput("readonly", true); + fixture.detectChanges(); + const link = fixture.nativeElement.querySelector("a") as HTMLAnchorElement; + expect(link.href).toBe("https://example.test/repository"); + expect(link.rel).toContain("noopener"); + expect(fixture.nativeElement.textContent).toContain("Нет"); + expect(fixture.nativeElement.textContent).toContain("0"); + fixture.componentRef.setInput( + "group", + new FormGroup({ + "1": new FormControl("javascript:alert(1)"), + "2": new FormControl(0), + "3": new FormControl(false), + }), + ); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector("a")).toBeNull(); + }); +}); diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts index 8b4008906..fa733b7dd 100644 --- a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts @@ -40,10 +40,15 @@ export class SolutionFieldsComponent { } fileLink(field: SolutionField): string | null { const value = this.value(field); - if (!value || typeof value !== "object" || !("url" in value) || typeof value.url !== "string") - return null; + const link = + field.type === "url" && typeof value === "string" + ? value + : value && typeof value === "object" && "url" in value && typeof value.url === "string" + ? value.url + : null; + if (!link) return null; try { - const url = new URL(value.url); + const url = new URL(link); return ["http:", "https:"].includes(url.protocol) ? url.href : null; } catch { return null; diff --git a/projects/social_platform/src/app/utils/evaluation-criterion-control.ts b/projects/social_platform/src/app/utils/evaluation-criterion-control.ts index 99dc3632f..e80ed2ecd 100644 --- a/projects/social_platform/src/app/utils/evaluation-criterion-control.ts +++ b/projects/social_platform/src/app/utils/evaluation-criterion-control.ts @@ -3,8 +3,11 @@ import { AbstractControl, FormControl, Validators } from "@angular/forms"; import { ProjectRatingCriterion } from "@domain/project/project-rating-criterion"; +/** Только данные поля; renderer не требует Project или вымышленного ID эксперта. */ +export type EvaluationCriterion = Omit; + /** One validation contract for real expert forms and organizer preview. */ -export function evaluationCriterionControl(c: ProjectRatingCriterion): FormControl { +export function evaluationCriterionControl(c: EvaluationCriterion): FormControl { if (c.type === "bool") { const value = c.value == null || c.value === "" ? null : String(c.value).toLowerCase() === "true"; diff --git a/projects/ui/src/lib/components/primitives/button/button.directive.spec.ts b/projects/ui/src/lib/components/primitives/button/button.directive.spec.ts new file mode 100644 index 000000000..e05d6606b --- /dev/null +++ b/projects/ui/src/lib/components/primitives/button/button.directive.spec.ts @@ -0,0 +1,43 @@ +/** @format */ + +import { Component, signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { ButtonDirective } from "./button.directive"; + +@Component({ + imports: [ButtonDirective], + template: + 'Открыть', +}) +class NativeButtonHost { + readonly disabled = signal(false); + readonly loading = signal(false); +} + +describe("Button native semantics", () => { + it("keeps native button disabling and prevents disabled link navigation without unsupported property bindings", () => { + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + const fixture = TestBed.createComponent(NativeButtonHost); + fixture.detectChanges(); + const button = fixture.nativeElement.querySelector("button") as HTMLButtonElement; + const link = fixture.nativeElement.querySelector("a") as HTMLAnchorElement; + expect(button.disabled).toBe(false); + fixture.componentInstance.disabled.set(true); + fixture.detectChanges(); + expect(button.disabled).toBe(true); + expect(link.hasAttribute("disabled")).toBe(false); + expect(link.getAttribute("aria-disabled")).toBe("true"); + const click = new MouseEvent("click", { bubbles: true, cancelable: true }); + link.dispatchEvent(click); + expect(click.defaultPrevented).toBe(true); + fixture.componentInstance.disabled.set(false); + fixture.componentInstance.loading.set(true); + fixture.detectChanges(); + expect(button.disabled).toBe(true); + fixture.componentInstance.loading.set(false); + fixture.detectChanges(); + expect(button.disabled).toBe(false); + expect(errors.mock.calls.flat().join(" ")).not.toContain("NG0303"); + errors.mockRestore(); + }); +}); diff --git a/projects/ui/src/lib/components/primitives/button/button.directive.ts b/projects/ui/src/lib/components/primitives/button/button.directive.ts index 7bbc0c8d3..acf588685 100644 --- a/projects/ui/src/lib/components/primitives/button/button.directive.ts +++ b/projects/ui/src/lib/components/primitives/button/button.directive.ts @@ -1,18 +1,27 @@ /** @format */ -import { booleanAttribute, Directive, HostListener, input } from "@angular/core"; +import { + booleanAttribute, + Directive, + ElementRef, + HostListener, + inject, + input, +} from "@angular/core"; /** Native adapter for the same Button foundation; keeps form semantics and HTMLElement refs. */ @Directive({ selector: "button[appButton], a[appButton]", host: { "[class]": "'ui-button ui-button--' + appButton()", - "[disabled]": "disabled() || appButtonLoading()", + "[attr.disabled]": "nativeButton && (disabled() || appButtonLoading()) ? '' : null", "[attr.aria-busy]": "appButtonLoading() || null", "[attr.aria-disabled]": "disabled() || appButtonLoading() || null", }, }) export class ButtonDirective { + readonly nativeButton = + inject>(ElementRef).nativeElement.tagName === "BUTTON"; @HostListener("click", ["$event"]) onClick(event: MouseEvent): void { if (this.disabled() || this.appButtonLoading()) { event.preventDefault(); From 24f87503739c4e0c84b95e2cba5bbc1eab6ad26d Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sun, 25 Oct 2026 01:56:23 +0300 Subject: [PATCH 09/47] feat(evaluation): route mixed cabinet applications by actual kind --- docs/product/program-solutions-fe07-ui.md | 23 ++- .../evaluation-programs.component.html | 61 +++--- .../evaluation-programs.component.spec.ts | 178 ++++++++++++++---- .../programs/evaluation-programs.component.ts | 67 +++++-- 4 files changed, 241 insertions(+), 88 deletions(-) diff --git a/docs/product/program-solutions-fe07-ui.md b/docs/product/program-solutions-fe07-ui.md index 61a1732b3..76b7f243d 100644 --- a/docs/product/program-solutions-fe07-ui.md +++ b/docs/product/program-solutions-fe07-ui.md @@ -58,4 +58,25 @@ Requirement audit после review: Блокирующих замечаний текущего scope после исправлений не осталось. На review сравнение по отображаемой подписи заменено сравнением исходных значений: буквальный комментарий «Не указано» теперь запускает защиту ухода и остается доступен для копирования при удалении критерия. Regression test проверяет guard и beforeunload. Это не приемка полного смешанного кабинета или административных экранов. -Еще осталось в FE07/FE08: подключить смешанный кабинет к общему ApplicationRef API, оба входа программы к нужному формату, административные интерфейсы и результаты/выгрузки, остальную аналитику и публикацию. Не объявлять всю FE07 или feature готовой по этому блоку. +Еще осталось в FE07/FE08: подключить программные входы к нужному формату, административные интерфейсы и результаты/выгрузки, остальную аналитику и публикацию. Не объявлять всю FE07 или feature готовой по этому блоку. + +## Следующий блок: общий кабинет + +Current: свежий согласованный кабинет из #506 запрашивает include=projects и строит маршруты только по projectId. Target: сохранить его расположение, вкладки, приглашения, debounce, фильтры, пагинацию, own counters, возврат и account scope; данные работ брать из уже зафиксированного GET /evaluation/applications/, карточки программ/приглашения из прежнего GET /evaluation/programs/. Новый API не вводится. Роль остается прежней: авторизованный эксперт видит только свои реальные назначения. + +CAB01: объединение двух ответов атомарно для текущего аккаунта/запроса, ошибка любого не выглядит пустым успехом. CAB02: kind=solution открывает /solutions/:solutionId/evaluation, kind=legacy_project сохраняет /projects-rating/:projectId; одинаковые assignment/entity IDs не конфликтуют. CAB03: прежние query, debounce, pagination и returnUrl сохранены. CAB04: счетчики только серверные, глобальные по прежнему контракту кабинета; ошибки/недоступные работы не дают ложного «все оценено». CAB05: существующий invitation deep link/accept/profile refresh сохраняется, смена аккаунта очищает приватные данные и отменяет оба запроса. CAB06: actual browser mixed cabinet → решение/Project → возврат, mobile и negative/error states; только вымышленные данные. После реализации evidence фиксируется отдельно. + +Реализовано: forkJoin прежних программ/приглашений и нового Result списка работ; snapshot применяется только целиком. Ошибка C00 показывает detail/вход при 401, сбой прежнего endpoint тоже очищает snapshot. Account/query cleanup отменяет оба запроса. Group и route используют настоящий ApplicationRef, track включает kind/program/assignment. Тексты общего кабинета говорят «работы», layout/controls остаются согласованными из #506. На пустой странице за пределами count диапазон 0–0 вместо 21–3. + +| AC | Evidence | Verdict | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| CAB01 | unit двух ответов/error/unauthorized/account cancellation; browser controlled network/program API failure + retry | PASS | +| CAB02 | unit коллизии ID; реальный browser Project.id=2 и ProgramSolution.id=2 в разных программах, обе правильные формы | PASS | +| CAB03 | прежние unit query/таб/программа/страница/cancellation, новый empty range; реальный browser фильтр → форма → точный возврат, поиск/reset и page=2 | PASS | +| CAB04 | unit серверных counts/no access/unavailable, реальный HTTP/browser 3 completed и поиск без результатов с теми же глобальными counts | PASS | +| CAB05 | прежние invitation deep link/accept once/profile/error tests и новый cancel обоих источников | PASS по tests/source; actual invitation accept browser повторно не запускался | +| CAB06 | actual mixed cabinet → readonly решение/Team/Project → return, 320/390/768/1440 без overflow/JS errors; controlled errors | PASS в указанном scope | + +Browser evidence: cabinet-browser-evidence.json, cabinet-desktop.png, cabinet-mobile.png в том же implementation каталоге. Backend данные вымышленные, внешние запросы заблокированы. Первая неуспешная попытка была ошибкой seed: программе автоматически добавляется штатный комментарий, а fixture сначала не создал его пустую оценку. Исправлен только локальный seed; итоговый browser PASS. Layout сравнен с актуальным кабинетом #506; новых компонентов и визуального redesign нет. Блокирующих замечаний текущего CAB scope после review не осталось. + +Финальные checks CAB: npm run test:ci — 2600 tests в 472 файлах и strict C00 types PASS; Angular development build с актуальным кодом для локального QA PASS; environment.ts восстановлен byte-for-byte к обычному dev. ESLint измененных TS — 0 errors; выбранные TS/HTML/MD проходят locked Prettier 3.8.3, git diff --check PASS. Это дополнение не меняет C00, backend permissions или схему БД. diff --git a/projects/social_platform/src/app/ui/pages/evaluation/programs/evaluation-programs.component.html b/projects/social_platform/src/app/ui/pages/evaluation/programs/evaluation-programs.component.html index 0b109b006..9f4bcec3e 100644 --- a/projects/social_platform/src/app/ui/pages/evaluation/programs/evaluation-programs.component.html +++ b/projects/social_platform/src/app/ui/pages/evaluation/programs/evaluation-programs.component.html @@ -2,16 +2,16 @@

Кабинет эксперта

-

Все проекты, назначенные вам для оценки

+

Все работы, назначенные вам для оценки

-

[attr.aria-current]="tab() === 'completed' ? 'page' : null" (click)="setTab('completed')" > - Оцененные · {{ data()?.projects?.completed ?? "—" }} + Оцененные · {{ data()?.applications?.completed ?? "—" }}

Поиск ${esc(person.name)}${work ? btn("solution", esc(workName(work)), `data-id="${work.id}" data-focus="person-${person.id}"`, "link") : "Нет решения"}${work?.archived ? "Решение в архиве" : work?.pending.some(item => item.id === person.id) ? "Ожидает приглашение" : person.captain ? "Капитан" : work ? "В команде" : "Без команды"}`; + }) + .join( + "", + )}${rows.length ? "" : '

Регистраций нет.

'}`, + }; + } + function removalEffects(ids) { + const picked = registrations.filter(person => ids.includes(person.id)); + const changes = works + .filter(work => !work.archived) + .map(work => { + const captain = picked.some(person => person.id === work.members[0]?.id); + const members = captain ? [] : work.members.filter(person => ids.includes(person.id)); + const pending = work.pending.filter(person => captain || ids.includes(person.id)); + return { work, captain, members, pending }; + }) + .filter(item => item.captain || item.members.length || item.pending.length); + return { picked, changes }; + } + function previewBody() { + const work = workBy(modal.id); + let rows = [], + explanation = "", + label = "", + title = ""; + if (modal.operation === "submit") { + title = "Сдать решение за команду"; + label = "Подтвердить сдачу"; + const missing = submissionIssues(work); + rows = [ + ["Готовность решения", missing.length ? "Есть незаполненные поля" : "Готово к сдаче"], + ["Принятых участников", work.members.length], + ["Ожидающих приглашений, не входят в состав", work.pending.length], + ]; + return { + title, + label, + blocked: !!missing.length, + body: `

${esc(workName(work))}

${effectRows(rows)}${missing.length ? `` : '

После подтверждения материалы и принятый состав будут зафиксированы. Повторная сдача не изменит уже сданное решение.

'}`, + }; + } else if (modal.operation === "case") { + title = "Сменить кейс"; + label = "Подтвердить смену кейса"; + rows = [ + ["Текущий кейс", work.caseName || "Не выбран"], + ["Новый кейс", modal.caseName], + ["Назначений будет удалено", caseRemovedAssignments(work, modal.caseName).length], + ["Значений по критериям сохраняется", scoreCount(work)], + ]; + explanation = + "Материалы и команда сохраняются. Изменение кейса и снимка сдачи попадет в историю. Удаляются только назначения без оценок, не подходящие для нового кейса."; + } else if (modal.operation === "member") { + title = "Исключить участника из команды"; + label = "Исключить участника"; + const person = work.members.find(item => item.id === modal.person); + rows = [ + ["Участник", person?.name], + ["Останется в команде", work.members.length - 1], + ["Значений по критериям сохраняется", scoreCount(work)], + ["Назначений будет удалено", 0], + ]; + explanation = + "Состав и снимок сдачи будут обновлены. Прежний состав останется в истории. Регистрация в программе, материалы и оценки сохраняются."; + } else if (modal.operation === "archive") { + title = competitive() ? "Снять решение с участия" : "Переместить решение в архив"; + label = competitive() ? "Снять с участия" : "Переместить в архив"; + rows = [ + ["Решение", workName(work)], + ["Назначений будет удалено", assigned(work).length], + ["Ожидающих приглашений будет отозвано", work.pending.length], + ["Значений по критериям сохраняется", scoreCount(work)], + ]; + explanation = + "Материалы, состав и оценки сохраняются в архиве. Решение исчезнет из активных списков, оценивания и публичной выдачи. Возврат из архива не предусмотрен."; + } else { + title = "Удалить регистрации из программы"; + label = "Удалить регистрации"; + const { picked, changes } = removalEffects(modal.ids); + rows = [ + ["Регистраций будет удалено", picked.length], + ["Решений перейдет в архив", changes.filter(item => item.captain).length], + [ + "Участников будет исключено из состава", + changes.reduce((n, item) => n + item.members.length, 0), + ], + ["Приглашений будет отозвано", changes.reduce((n, item) => n + item.pending.length, 0)], + [ + "Назначений будет удалено", + changes.reduce((n, item) => n + (item.captain ? assigned(item.work).length : 0), 0), + ], + ]; + explanation = + "Учетные записи и участие в других программах сохраняются. Если выбран капитан, его решение перейдет в архив. Уже архивные материалы, состав и история остаются без изменений."; + return { + title, + label, + body: `

${picked.map(person => esc(person.name)).join(", ")}

${effectRows(rows)}

${explanation}

Последствия для решений

${changes.length ? changes.map(item => `

${esc(workName(item.work))}${item.captain ? "Архивирование с сохранением материалов, состава и оценок" : item.members.length ? "Исключение: " + item.members.map(person => esc(person.name)).join(", ") + ". Материалы, оценки и назначения сохраняются." : "Отзыв ожидающего приглашения. Принятый состав сохраняется."}

`).join("") : '

Активные решения не затронуты. Операция будет записана в общую историю регистраций.

'}
`, + }; + } + return { + title, + label, + body: `${work ? `

${esc(workName(work))}${competitive() && work.submitted ? " · Сданное решение" : ""}

` : ""}${effectRows(rows)}

${explanation}

`, + }; + } + function previewView() { + const data = previewBody(), + mode = modal.forceReady ? "ready" : response(); + const loading = mode === "loading"; + const legacy = mode === "legacy" && modal.operation === "registrations"; + let error = ""; + if (legacy) + error = + ''; + if (modal.failure) { + const copy = { + stale: [ + "Данные изменились", + "Последствия могли измениться. Загрузите новый предпросмотр и подтвердите действие еще раз.", + ], + error: [ + "Не удалось подтвердить действие", + "Результат операции пока неизвестен. Обновите данные перед повторным подтверждением.", + ], + "no-access": [ + "Нет доступа к действию", + "Ваши права изменились. Обновите данные программы.", + ], + }[modal.failure]; + error = ``; + } + return { + title: data.title, + subtitle: "Проверьте последствия перед подтверждением.", + body: loading + ? '

Загружаем последствия действия…

' + : `${data.body}${error}
${btn("admin-apply", modal.busy ? "Подтверждаем…" : data.label, `${data.blocked || legacy || modal.failure || modal.busy ? "disabled" : ""} aria-busy="${!!modal.busy}"`, "primary")}

В макете меняются только вымышленные данные текущей вкладки.

`, + }; + } + function historyView() { + const work = workBy(modal.id), + rows = events.filter(event => event.workId === work.id).toReversed(); + return { + title: "История изменений", + subtitle: workName(work), + body: rows.length + ? `
    ${rows.map(event => `
  1. ${esc(event.title)}

    ${event.time} · Организатор программы

    ${esc(event.description)}

    ${btn("admin-event", "Посмотреть до и после", `data-event="${event.id}" data-focus="event-${event.id}"`, "link")}
  2. `).join("")}
` + : '

Административных изменений еще нет

Здесь появятся смена кейса, исключение участников и архивирование.

', + }; + } + function snapshotHtml(data) { + return `

Кейс

${esc(data.caseName || "Не выбран")}

Состав · ${data.members.length}

${data.members.map(person => esc(person.name)).join(", ")}

Состояние

${data.archived ? "Архив" : data.submitted ? "Сдано" : "Сохранено"}

${data.answers.map(answer => `

${esc(answer.label)}

${answer.value === false ? "Нет" : answer.value === true ? "Да" : esc(answer.value ?? "Не заполнено")}

`).join("")}
`; + } + function eventView() { + const event = events.find(item => item.id === modal.event), + tab = modal.version || "before"; + return { + title: event.title, + subtitle: "Материалы и состав в момент административного изменения.", + body: `
${snapshotHtml(event[tab])}
${event.audit ? btn("admin-audit", "Открыть удаление регистраций", `data-audit="${event.audit}"`, "link") : ""}`, + }; + } + function auditView() { + if (modal.view === "admin-audits") + return { + title: "История удаления регистраций", + subtitle: "Общая история программы включает участников без решения.", + body: audits.length + ? `
    ${audits + .toReversed() + .map( + audit => + `
  1. Удалено регистраций: ${audit.people.length}

    ${audit.time} · Организатор программы

    ${audit.people.map(person => esc(person.name)).join(", ")}

    ${btn("admin-audit", "Посмотреть последствия", `data-audit="${audit.id}" data-focus="audit-${audit.id}"`, "link")}
  2. `, + ) + .join("")}
` + : '

Удалений регистраций еще нет

История появится после подтвержденного действия.

', + }; + const audit = audits.find(item => item.id === modal.audit); + return { + title: "Удаление регистраций", + subtitle: `${audit.time} · Организатор программы`, + body: `

${audit.people.map(person => esc(person.name)).join(", ")}

Учетные записи участников сохранены. Персональные ответы анкеты в этой истории не дублируются.

${ + audit.events.length + ? audit.events + .map(id => { + const event = events.find(item => item.id === id); + return `

${esc(event.before.name)}

${esc(event.description)}

${btn("admin-event", "Посмотреть до и после", `data-event="${event.id}"`, "link")}
`; + }) + .join("") + : "

Активные решения не затронуты. Архивные решения сохраняют прежний состав и историю.

" + }`, + }; + } + function renderModal() { + init(); + if (modal.view === "solution") return solution(workBy(modal.id)); + if (modal.view === "people" || modal.view === "admin-people") return peopleView(); + if (modal.view === "admin-preview") return previewView(); + if (modal.view === "admin-history") return historyView(); + if (modal.view === "admin-event") return eventView(); + if (["admin-audits", "admin-audit"].includes(modal.view)) return auditView(); + if (modal.view === "admin-case") { + const work = workBy(modal.id); + return { + title: "Сменить кейс", + subtitle: workName(work), + body: `

Выберите актуальный кейс программы. Затем проверьте последствия изменения.

${btn("back", "Отмена")}${btn("admin-case-preview", "Посмотреть последствия", modal.caseName ? "" : "disabled", "primary")}
`, + }; + } + return null; + } + function submissionIssues(work) { + return [ + ...(work.caseName ? [] : ["Кейс"]), + ...fields() + .filter( + field => field.required && applicable(work, field) && !filled(answer(work, field.id)), + ) + .map(field => field.label), + ]; + } + function caseRemovedAssignments(work, caseName) { + // Вымышленное ограничение эксперта Марии: только кейс «Образование». + return assigned(work).filter( + row => + !ratings.some(score => score.workId === row.workId && score.expert === row.expert) && + row.expert === 1 && + caseName !== "Образование", + ); + } + function addEvent(work, before, title, description, audit = null) { + const event = { + id: ++sequence, + workId: work.id, + time: new Date().toLocaleString("ru-RU"), + title, + description, + before, + after: snapshot(work), + audit, + }; + events.push(event); + return event; + } + function archiveWork(work, audit = null) { + const before = snapshot(work); + work.archived = true; + work.pending = []; + assignments = assignments.filter(row => row.workId !== work.id); + return addEvent( + work, + before, + "Решение перемещено в архив", + "Материалы, состав и оценки сохранены. Назначения удалены, ожидающие приглашения отозваны.", + audit, + ); + } + function applyChanges() { + const work = workBy(modal.id); + if (modal.operation === "submit") { + work.submitted = true; + work.submittedAt = new Date().toLocaleString("ru-RU"); + } else if (modal.operation === "case") { + const before = snapshot(work), + removed = new Set(caseRemovedAssignments(work, modal.caseName).map(row => row.id)); + work.caseName = modal.caseName; + assignments = assignments.filter(row => !removed.has(row.id)); + addEvent( + work, + before, + "Кейс изменен", + `${before.caseName || "Кейс не выбран"} → ${work.caseName}. Материалы, состав и оценки сохранены.`, + ); + } else if (modal.operation === "member") { + const before = snapshot(work), + person = work.members.find(item => item.id === modal.person); + work.members = work.members.filter(item => item.id !== modal.person); + work.teamSize = work.members.length; + work.pending = work.pending.filter(item => item.id !== modal.person); + addEvent( + work, + before, + "Участник исключен из команды", + `${person.name}. Регистрация, материалы, оценки и назначения сохранены.`, + ); + } else if (modal.operation === "archive") archiveWork(work); + else { + const { picked, changes } = removalEffects(modal.ids), + audit = { + id: audits.length + 1, + time: new Date().toLocaleString("ru-RU"), + people: structuredClone(picked.map(({ id, name }) => ({ id, name }))), + events: [], + }; + for (const item of changes) { + if (item.captain) audit.events.push(archiveWork(item.work, audit.id).id); + else { + const before = snapshot(item.work); + item.work.members = item.work.members.filter(person => !modal.ids.includes(person.id)); + item.work.teamSize = item.work.members.length; + item.work.pending = item.work.pending.filter(person => !modal.ids.includes(person.id)); + audit.events.push( + addEvent( + item.work, + before, + "Регистрации удалены", + item.members.length + ? `Исключены из состава: ${item.members.map(person => person.name).join(", ")}. Материалы, оценки и назначения сохранены.` + : "Ожидающие приглашения отозваны. Принятый состав сохранен.", + audit.id, + ).id, + ); + } + } + registrations = registrations.filter(person => !modal.ids.includes(person.id)); + selected.clear(); + audits.push(audit); + return { view: "admin-audit", audit: audit.id }; + } + return { + view: "solution", + id: work.id, + tab: modal.operation === "member" ? "team" : "solution", + }; + } + function navigate(next) { + modal ? openInner(next) : openModal(next); + } + function handleClick(target) { + const action = target.dataset.action; + if (action === "people") selected.clear(); + if (!action?.startsWith("admin-")) return false; + init(); + const id = Number(target.dataset.id); + if (action === "admin-scope") { + listScope = target.dataset.scope; + renderPage(); + document.querySelector(`[data-action="admin-scope"][data-scope="${listScope}"]`)?.focus(); + } + if (action === "admin-people") { + selected.clear(); + navigate({ view: "admin-people", kind: "all" }); + } + if (action === "admin-case") navigate({ view: "admin-case", id }); + if (action === "admin-case-preview") + navigate({ + view: "admin-preview", + operation: "case", + id: modal.id, + caseName: modal.caseName, + }); + if (action === "admin-remove-member") + navigate({ + view: "admin-preview", + operation: "member", + id, + person: Number(target.dataset.person), + }); + if (action === "admin-submit") navigate({ view: "admin-preview", operation: "submit", id }); + if (action === "admin-archive") navigate({ view: "admin-preview", operation: "archive", id }); + if (action === "admin-registration-preview" && selected.size) + navigate({ view: "admin-preview", operation: "registrations", ids: [...selected] }); + if (action === "admin-history") navigate({ view: "admin-history", id }); + if (action === "admin-event") + navigate({ view: "admin-event", event: Number(target.dataset.event) }); + if (action === "admin-version") { + modal.version = target.dataset.version; + renderModalOuter(); + dialog.querySelector(`[data-version="${modal.version}"]`).focus(); + } + if (action === "admin-audits") navigate({ view: "admin-audits" }); + if (action === "admin-audit") + navigate({ view: "admin-audit", audit: Number(target.dataset.audit) }); + if (action === "admin-refresh-preview") { + if (modal.failure === "no-access") { + closeModal(); + renderPage(); + showNotice("Данные программы обновлены в макете."); + } else { + modal.failure = null; + modal.forceReady = true; + renderModalOuter(); + $("#dialog-title").focus(); + } + } + if (action === "admin-apply" && !modal.busy && !modal.failure) { + if (previewBody().blocked) return true; + const mode = modal.forceReady ? "ready" : response(); + if (["stale", "error", "no-access"].includes(mode)) { + modal.failure = mode; + renderModalOuter(); + $("#dialog-title").focus(); + return true; + } + if ((mode === "legacy" && modal.operation === "registrations") || mode === "loading") + return true; + const current = modal; + modal.busy = true; + renderModalOuter(); + $("#dialog-title").focus(); + setTimeout(() => { + if (modal !== current) return; + const next = applyChanges(); + modal = next; + renderPage(); + renderModalOuter(); + $("#dialog-title").focus(); + showNotice( + current.operation === "submit" + ? "Решение сдано в макете. Материалы и принятый состав зафиксированы." + : "Действие выполнено в макете. Изменения доступны в истории.", + ); + }, 250); + } + return true; + } + // Имя внешнего renderer не совпадает с локальным renderModal этого дополнения. + const renderModalOuter = () => window.renderModal(); + document.addEventListener("change", event => { + if (event.target.name === "registration") { + event.target.checked + ? selected.add(Number(event.target.value)) + : selected.delete(Number(event.target.value)); + renderModalOuter(); + dialog.querySelector(`[name="registration"][value="${event.target.value}"]`)?.focus(); + } + if (event.target.name === "admin-case") { + modal.caseName = event.target.value; + renderModalOuter(); + dialog.querySelector('[name="admin-case"]')?.focus(); + } + }); + const teamCount = () => { + init(); + return scenario === "empty" + ? 0 + : works.filter(work => !work.archived).reduce((sum, work) => sum + work.members.length, 0); + }; + const withoutVisit = () => { + init(); + return scenario === "empty" ? 0 : registrations.filter(person => person.id >= 9).length; + }; + return { + init, + isArchived, + teamCount, + withoutVisit, + regionCount: index => { + init(); + return scenario === "empty" + ? 0 + : registrations.filter(person => + index === 0 + ? person.id <= 6 + : index === 1 + ? person.id > 6 && person.id <= 10 + : person.id > 10, + ).length; + }, + listWorks, + listToolbar, + isArchiveScope: () => listScope === "archived", + listFilter: () => + listScope === "archived" ? (archiveFilter ??= newFilter()) : filterState.solutions, + archiveFilters: filter => + `
`, + emptyArchive: () => listScope === "archived" && !listWorks().length, + registrationCount, + renderModal, + handleClick, + savedRating: id => { + init(); + return ratings.find(row => row.id === id); + }, + reset: () => { + initialized = false; + listScope = "active"; + archiveFilter = null; + selected.clear(); + events = []; + audits = []; + sequence = 0; + init(); + }, + }; +})(); diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/analytics-reference.css b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/analytics-reference.css new file mode 100644 index 000000000..4d3362f52 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/analytics-reference.css @@ -0,0 +1,845 @@ +/** @format */ +/** @format */ +.prototype-analytics { + /** @format */ +} +.prototype-analytics + .analytics + .ui-button.ui-button:not(.attention__action, .evaluation__assignment-action) { + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; +} +.prototype-analytics + .analytics + .ui-button.ui-button:not(.attention__action, .evaluation__assignment-action).ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-analytics + .analytics + .ui-button.ui-button:not(.attention__action, .evaluation__assignment-action) { + padding-inline: 16px; + } + .prototype-analytics + .analytics + .ui-button.ui-button:not(.attention__action, .evaluation__assignment-action)::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-analytics + .analytics + .ui-button.ui-button:not( + .attention__action, + .evaluation__assignment-action + ).ui-button--icon::after { + inset: -6px; + } +} +.prototype-analytics .analytics ::ng-deep app-button > button.button.button { + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; + min-width: 0; + max-height: none; +} +.prototype-analytics .analytics ::ng-deep app-button > button.button.button.ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-analytics .analytics ::ng-deep app-button > button.button.button { + padding-inline: 16px; + } + .prototype-analytics .analytics ::ng-deep app-button > button.button.button::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-analytics + .analytics + ::ng-deep + app-button + > button.button.button.ui-button--icon::after { + inset: -6px; + } +} +.prototype-analytics .analytics { + display: flex; + flex-direction: column; + gap: 16px; + padding: 4px 0 40px; + color: var(--black); +} +.prototype-analytics .analytics__header { + display: flex; + flex-direction: column; + gap: 16px; + justify-content: space-between; + padding: 20px; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; +} +.prototype-analytics .analytics__eyebrow { + margin-bottom: 4px; + color: var(--accent); +} +.prototype-analytics .analytics__title { + margin: 0; +} +.prototype-analytics .analytics__title-row { + display: flex; + gap: 16px; + align-items: center; + justify-content: space-between; + width: 100%; +} +.prototype-analytics .analytics__subtitle { + max-width: 600px; + margin-top: 8px; + color: var(--grey-for-text); +} +.prototype-analytics .analytics__heading-row { + position: relative; + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; +} +.prototype-analytics .analytics__heading-row h2, +.prototype-analytics .analytics__heading-row h3 { + font-weight: 600; + color: var(--black); +} +.prototype-analytics .analytics__heading-row h2 { + font-size: var(--ui-font-h3); + line-height: 22px; +} +.prototype-analytics .analytics__loading { + display: flex; + flex-direction: column; + gap: 16px; + color: var(--grey-for-text); +} +.prototype-analytics .exports { + width: min(100%, 540px); + min-width: 0; +} +.prototype-analytics .exports__actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} +.prototype-analytics .exports__actions app-button { + flex: 1 1 160px; + min-width: 0; +} +@media screen and (max-width: 520px) { + .prototype-analytics .exports__actions app-button { + flex-basis: 100%; + } +} +.prototype-analytics .exports__error { + margin-top: 8px; + color: var(--red-dark); +} +.prototype-analytics .summary { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} +@media screen and (min-width: 600px) { + .prototype-analytics .summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .summary { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} +.prototype-analytics .summary--loading { + margin-top: 4px; +} +.prototype-analytics .summary-card, +.prototype-analytics .analytics-card, +.prototype-analytics .regions, +.prototype-analytics .attention, +.prototype-analytics .activity { + min-width: 0; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; + box-shadow: var(--ui-chart-shadow); +} +.prototype-analytics .regions-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 12px; + align-items: start; +} +@media screen and (min-width: 1000px) { + .prototype-analytics .regions-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +.prototype-analytics .regions { + display: flex; + flex-direction: column; + padding: 16px 18px; +} +.prototype-analytics .regions .analytics__heading-row app-tooltip { + flex-shrink: 0; +} +.prototype-analytics .summary-card { + display: flex; + gap: 12px; + align-items: center; + min-height: 92px; + padding: 16px; +} +.prototype-analytics .summary-card__icon { + display: grid; + flex: 0 0 44px; + place-items: center; + width: 44px; + height: 44px; + color: var(--accent); + background: var(--accent-light); + border-radius: 12px; +} +.prototype-analytics + .summary-card[data-metric="submitted-projects"] + .prototype-analytics + .summary-card__icon { + color: var(--green-dark); + background: var(--green-light); +} +.prototype-analytics .summary-card__content { + min-width: 0; +} +.prototype-analytics .summary-card__label { + min-width: 0; + font-weight: 600; + color: var(--black); + overflow-wrap: anywhere; +} +.prototype-analytics .summary-card__value { + display: block; + margin-top: 6px; + line-height: 1; + color: var(--black); +} +@media screen and (max-width: 999px) { + .prototype-analytics .summary-card__value { + font-size: var(--ui-font-h1); + font-weight: 700; + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .summary-card__value { + font-size: 24px; + font-weight: 700; + } +} +.prototype-analytics .summary-card--skeleton { + min-height: 92px; + background: linear-gradient( + 90deg, + var(--light-gray) 20%, + var(--medium-grey-for-outline) 50%, + var(--light-gray) 80% + ); + background-size: 200% 100%; + animation: analytics-skeleton 1.3s ease-in-out infinite; +} +.prototype-analytics .analytics-grid { + display: grid; + gap: 12px; + align-items: start; +} +@media screen and (min-width: 1000px) { + .prototype-analytics .analytics-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .analytics-grid--priority > .attention, + .prototype-analytics .analytics-grid--priority > .program-deadlines { + align-self: stretch; + } +} +.prototype-analytics .analytics-card { + display: flex; + flex-direction: column; + min-height: 250px; + padding: 18px; +} +.prototype-analytics .analytics-card__title { + font-size: var(--ui-font-h3); + font-weight: 600; + line-height: 22px; + color: var(--black); +} +.prototype-analytics .analytics-card__subtitle { + font-size: var(--ui-font-body); + font-weight: 500; + line-height: 20px; + color: var(--dark-grey); +} +.prototype-analytics .analytics-card__section-label { + line-height: 18px; + color: var(--black); +} +@media screen and (max-width: 999px) { + .prototype-analytics .analytics-card__section-label { + font-size: var(--ui-font-body); + font-weight: 600; + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .analytics-card__section-label { + font-size: 13px; + font-weight: 600; + } +} +.prototype-analytics .analytics-empty-state { + display: grid; + flex: 1 1 auto; + place-items: center; + min-height: 96px; + margin: 16px 0 0; + color: var(--grey-for-text); + text-align: center; +} +.prototype-analytics .program-deadlines { + display: flex; + flex-direction: column; + align-self: start; + min-height: 0; +} +.prototype-analytics .program-deadlines__list { + display: grid; + gap: 8px; + align-content: start; + margin-top: 12px; +} +.prototype-analytics .program-deadlines__row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + padding: 10px 12px; + background: var(--light-gray); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-analytics .program-deadlines__content { + min-width: 0; +} +.prototype-analytics .program-deadlines__label { + font-size: 13px; + font-weight: 600; + line-height: 18px; + color: var(--black); +} +.prototype-analytics .program-deadlines__date, +.prototype-analytics .program-deadlines__hint { + display: block; + margin-top: 2px; + font-size: 12px; + line-height: 18px; + color: var(--dark-grey); +} +.prototype-analytics .program-deadlines__hint { + font-size: 11px; + line-height: 16px; +} +.prototype-analytics .program-deadlines__status { + font-size: 11px; + font-weight: 600; + line-height: 16px; + color: var(--dark-grey); + text-align: right; +} +.prototype-analytics .program-deadlines__row--active { + background: var(--accent-light); + border-color: var(--accent); +} +.prototype-analytics .program-deadlines__row--active .program-deadlines__status { + color: var(--accent-dark); +} +.prototype-analytics .program-deadlines__row--ended .program-deadlines__label, +.prototype-analytics .program-deadlines__row--ended .program-deadlines__date, +.prototype-analytics .program-deadlines__row--ended .program-deadlines__status { + color: var(--grey-for-text); +} +@media screen and (max-width: 420px) { + .prototype-analytics .program-deadlines__row { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + } + .prototype-analytics .program-deadlines__status { + text-align: left; + } +} +.prototype-analytics .analytics-grid--priority .attention__list, +.prototype-analytics .analytics-grid--priority .program-deadlines__list { + flex: 1; + grid-auto-rows: minmax(68px, 1fr); + gap: 8px; + align-content: stretch; + margin-top: 16px; +} +.prototype-analytics .attention__list .attention__action { + height: 100%; + padding-block: 10px; +} +.prototype-analytics .analytics-export-menu { + display: grid; + min-width: 220px; + padding: 6px; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 10px; + box-shadow: 0 8px 24px rgba(51, 51, 51, 0.12); +} +.prototype-analytics .analytics-export-menu button { + padding: 10px 12px; + font: inherit; + font-size: 12px; + color: var(--black); + text-align: left; + cursor: pointer; + background: transparent; + border: 0; + border-radius: 6px; +} +.prototype-analytics .analytics-export-menu button:hover, +.prototype-analytics .analytics-export-menu button:focus-visible { + background: var(--accent-light); + outline: 2px solid var(--accent); + outline-offset: -2px; +} +.prototype-analytics .cases-card__title { + font-size: var(--ui-font-h3); + font-weight: 600; + line-height: 22px; +} +.prototype-analytics .cases-card__list { + display: grid; + gap: 8px; + margin-top: 12px; +} +.prototype-analytics .cases-card__columns { + display: none; +} +.prototype-analytics .cases-card__expand { + align-self: flex-start; + margin-top: 16px; +} +.prototype-analytics .case-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + align-items: center; + width: 100%; + min-width: 0; + padding: 16px; + font: inherit; + text-align: left; + cursor: pointer; + background: var(--light-gray); + border: 1px solid transparent; + border-radius: 8px; +} +.prototype-analytics .case-row:hover { + border-color: var(--accent); +} +.prototype-analytics .case-row:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-analytics .case-row--without-case { + border: 1px dashed var(--medium-grey-for-outline); +} +.prototype-analytics .case-row--without-submission { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.prototype-analytics .case-row__name { + grid-column: 1/-1; + min-width: 0; + font-size: 14px; + font-weight: 600; + line-height: 21px; + color: var(--black); + overflow-wrap: anywhere; +} +.prototype-analytics .case-row__projects, +.prototype-analytics .case-row__metric { + display: grid; + gap: 6px; + min-width: 0; +} +.prototype-analytics .case-row__projects strong, +.prototype-analytics .case-row__metric strong { + font-size: 18px; + font-weight: 600; + line-height: 24px; + color: var(--black); +} +.prototype-analytics .case-row__metric strong { + order: -1; +} +.prototype-analytics .case-row__mobile-label { + font-size: 12px; + line-height: 18px; + color: var(--dark-grey); +} +.prototype-analytics .case-row__bar { + display: flex; + height: 6px; + overflow: hidden; + background: var(--grey-button); + border-radius: 3px; +} +.prototype-analytics .case-row__volume { + background: var(--accent); +} +@media screen and (min-width: 700px) { + .prototype-analytics .cases-card__columns, + .prototype-analytics .case-row { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(100px, 1fr) 80px 100px; + gap: 24px; + } + .prototype-analytics .cases-card__columns { + padding: 16px 16px 0; + font-size: 12px; + line-height: 18px; + color: var(--dark-grey); + } + .prototype-analytics .case-row__name { + grid-column: auto; + } + .prototype-analytics .case-row__mobile-label { + display: none; + } + .prototype-analytics .cases-card__columns--without-submission, + .prototype-analytics .case-row--without-submission { + grid-template-columns: minmax(0, 2fr) minmax(100px, 1fr) 100px; + } +} +.prototype-analytics .status-list { + display: grid; + gap: 10px; + margin-top: 16px; +} +.prototype-analytics .status-list li { + display: flex; + gap: 16px; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + background: var(--light-gray); + border-radius: 8px; +} +.prototype-analytics .status-list li span { + font-weight: 500; + line-height: 18px; + color: var(--black); +} +.prototype-analytics .status-list li strong { + line-height: 20px; + color: var(--black); +} +@media screen and (max-width: 999px) { + .prototype-analytics .status-list li strong { + font-size: var(--ui-font-h2); + font-weight: 700; + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .status-list li strong { + font-size: 17px; + font-weight: 700; + } +} +.prototype-analytics .status-list--compact { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; + margin-top: 8px; +} +.prototype-analytics .status-list--compact li { + flex-direction: column; + gap: 4px; + align-items: flex-start; +} +@media screen and (max-width: 520px) { + .prototype-analytics .status-list--compact { + grid-template-columns: 1fr; + } + .prototype-analytics .status-list--compact li { + flex-direction: row; + justify-content: space-between; + } +} +.prototype-analytics .evaluation { + grid-column: 1/-1; +} +.prototype-analytics .evaluation__mode { + margin-top: 2px; + font-weight: 500; + color: var(--dark-grey); +} +.prototype-analytics .analytics-bottom { + display: grid; + gap: 12px; + align-items: start; +} +.prototype-analytics .attention, +.prototype-analytics .activity { + padding: 18px; +} +.prototype-analytics .attention { + display: flex; + flex-direction: column; + align-self: start; + width: 100%; + padding-left: 15px; + background: var(--light-white); + border-color: var(--medium-grey-for-outline); + border-left: 4px solid var(--red); +} +.prototype-analytics .attention__action { + display: grid; + grid-template-columns: 32px minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + min-width: 0; + padding: 14px 12px; + font: inherit; + text-align: left; + cursor: pointer; + background: transparent; + border: 0; + border-radius: 8px; +} +.prototype-analytics .attention__action:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-analytics .attention__action:disabled { + cursor: default; +} +.prototype-analytics .attention__action span { + min-width: 0; + overflow-wrap: anywhere; +} +.prototype-analytics .attention--empty { + border-left-color: var(--green); +} +.prototype-analytics .attention__list { + display: grid; + gap: 10px; + margin-top: 16px; +} +.prototype-analytics .attention__list li { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px; + align-items: center; + padding: 0 12px 0 0; + background: var(--light-gray); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-analytics .attention__list li:not(.attention__row--disabled):hover, +.prototype-analytics .attention__list li:not(.attention__row--disabled):active { + background: var(--accent-light); + border-color: var(--accent); +} +.prototype-analytics .attention__list li app-tooltip:focus-visible { + border-radius: 50%; + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-analytics .attention__list li span { + font-weight: 500; + color: var(--black); +} +.prototype-analytics .attention__list strong, +.prototype-analytics .attention__list i { + color: var(--red-dark); +} +.prototype-analytics .attention__list strong { + font-weight: 700; +} +.prototype-analytics .attention__empty { + font-weight: 500; + color: var(--grey-for-text); +} +.prototype-analytics .activity { + display: flex; + flex-direction: column; +} +.prototype-analytics .activity__note { + margin-top: 2px; + color: var(--dark-grey); +} +.prototype-analytics .regions__export { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + margin-left: auto; + color: var(--accent); + cursor: pointer; + background: transparent; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-analytics .regions__export:disabled { + color: var(--dark-grey); + cursor: not-allowed; +} +.prototype-analytics .regions__export:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-analytics .regions__export-message { + margin-top: 8px; + font-size: var(--ui-font-small); + line-height: 18px; +} +.prototype-analytics .analytics-state { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + justify-content: center; + min-height: 260px; + padding: 24px; + color: var(--grey-for-text); + text-align: center; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; +} +.prototype-analytics .analytics-state i { + color: var(--accent); +} +.prototype-analytics .analytics-state--compact { + flex-direction: row; + justify-content: flex-start; + min-height: 0; + color: var(--black); + text-align: left; +} +.prototype-analytics .analytics-state--compact p { + margin-top: 4px; + color: var(--grey-for-text); +} +@keyframes analytics-skeleton { + from { + background-position: 100% 0; + } + to { + background-position: -100% 0; + } +} +@media screen and (max-width: 999px) { + .prototype-analytics .attention .attention__list .attention__action.ui-button { + width: 100%; + padding: 14px 12px; + font: inherit; + color: var(--black); + text-align: left; + background: transparent; + border: 0; + border-radius: 8px; + } + .prototype-analytics + .attention + .attention__list + .attention__action.ui-button:hover:not(:disabled), + .prototype-analytics + .attention + .attention__list + .attention__action.ui-button:active:not(:disabled) { + background: transparent; + border-color: transparent; + } +} +.prototype-analytics .attention__list li[data-attention-kind] { + --attention-tone: var(--accent); + --attention-fill: var(--accent-light); + min-height: 64px; + background: color-mix(in srgb, var(--attention-fill) 48%, var(--light-white)); + border-color: color-mix(in srgb, var(--attention-tone) 28%, var(--medium-grey-for-outline)); + box-shadow: 0 2px 0 color-mix(in srgb, var(--attention-tone) 10%, transparent); + transition: + background 150ms ease, + box-shadow 150ms ease; +} +.prototype-analytics .attention__list li[data-attention-kind][data-attention-kind="without-visit"] { + --attention-tone: var(--gold-dark); + --attention-fill: var(--gold-light); +} +.prototype-analytics + .attention__list + li[data-attention-kind][data-attention-kind="awaiting-evaluation"] { + --attention-tone: var(--green-dark); + --attention-fill: var(--green-light); +} +.prototype-analytics .attention__list li[data-attention-kind]:not(.attention__row--disabled):hover { + background: var(--attention-fill); + border-color: var(--attention-tone); + box-shadow: 0 3px 8px color-mix(in srgb, var(--attention-tone) 12%, transparent); +} +.prototype-analytics .attention__list li[data-attention-kind] .attention__icon { + display: grid; + place-items: center; + width: 32px; + height: 32px; + background: var(--attention-fill); + border-radius: 8px; +} +.prototype-analytics .attention__list li[data-attention-kind] .attention__icon i, +.prototype-analytics .attention__list li[data-attention-kind] strong { + color: var(--attention-tone); +} +.prototype-analytics .attention__list li[data-attention-kind] .attention__action span.text-body-10 { + font-size: 12px; + line-height: 18px; +} +.prototype-analytics .attention__list li[data-attention-kind] strong { + min-width: 26px; + padding: 3px 6px; + font-size: 14px; + text-align: center; + background: var(--attention-fill); + border-radius: 6px; +} +.prototype-analytics .attention__list li[data-attention-kind].attention__row--disabled { + --attention-tone: var(--grey-for-text); + --attention-fill: var(--light-gray); + box-shadow: none; +} diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/app.js b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/app.js new file mode 100644 index 000000000..0eab67dd9 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/app.js @@ -0,0 +1,707 @@ +/** + * D02 review prototype. Synthetic data only; no authentication or API requests. + * + * @format + */ + +const $ = selector => document.querySelector(selector); +const esc = value => + String(value ?? "").replace( + /[&<>"']/g, + c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c], + ); +const icon = name => + ``; +const params = new URLSearchParams(location.search); +let format = params.get("format") || "kch", + screen = params.get("screen") || "analytics", + scenario = params.get("state") || "ready"; +const names = [ + "Сервис поддержки студентов", + "Город без барьеров", + "Навигатор наставников", + "Лаборатория практики", +]; +const people = [ + ["Иван", "Токарев"], + ["Мария", "Климова"], + ["Анна", "Петрова"], +]; +const works = names.map((name, i) => ({ + id: 301 + i, + name, + caseName: ["Образование", "Городская среда", "Образование", null][i], + captain: ["Иван Токарев", "Дарья Соколова", "Александр Иванов", "Елена Морозова"][i], + teamSize: [3, 2, 2, 1][i], + submitted: i < 3, +})); +const originalAssignments = [ + { id: 801, workId: 301, expert: 0, completed: true, value: 0 }, + { id: 802, workId: 301, expert: 1, completed: false, value: null }, + { id: 803, workId: 302, expert: 0, completed: true, value: 8 }, + { id: 804, workId: 302, expert: 2, completed: true, value: 7 }, +]; +let assignments = originalAssignments.map(row => ({ ...row })); +const filterState = { solutions: newFilter(), results: newFilter() }; +let modal = null, + lastTrigger = null, + noticeTimer = null; +const dialog = $("#manager-dialog"); +function newFilter() { + return { draft: "", search: "", submission: "all", fieldId: "", answer: "all", expanded: false }; +} +const competitive = () => format !== "noncompetitive"; +const effectiveAssignments = () => (scenario === "empty" ? [] : assignments); +const distributed = () => competitive() && scenario !== "open-mode"; +const activeWorks = () => + scenario === "empty" ? [] : works.filter(work => !managerAdmin.isArchived(work.id)); +function workName(work) { + return scenario === "long" && work.id === 301 + ? "Сервис поддержки студентов и молодых специалистов при подготовке к профессиональным конкурсам и первому трудоустройству" + : work.name; +} +function fields() { + return [ + { id: 2, label: "Краткое описание", type: "textarea", required: true }, + ...(format === "kch" + ? [{ id: 10, label: "Презентация PDF", type: "file", required: true }] + : [{ id: 11, label: "Репозиторий", type: "url", required: true }]), + { + id: 12, + label: format === "kch" ? "Видео решения" : "Демонстрация", + type: "url", + required: false, + }, + { id: 30, label: "Размер пилота", type: "number", required: false }, + { id: 31, label: "Разрешение на демонстрацию", type: "checkbox", required: false }, + { + id: 32, + label: format === "kch" ? "План доработки" : "Инструкция запуска", + type: "textarea", + required: true, + }, + ]; +} +function applicable(work, field) { + return !competitive() || field.id !== 32 || work.id >= 303; +} +function answer(work, id) { + if (scenario === "ready-draft" && work.id === 304) + return { + 2: "Готовые материалы решения.", + 10: "Презентация.pdf", + 11: "example.com/repository", + 12: null, + 30: 0, + 31: false, + 32: "Порядок проверки решения.", + }[id]; + const i = work.id - 301; + return { + 2: "Сервис помогает участникам найти материалы, экспертов и команду для решения задачи.", + 10: i < 3 ? "Презентация.pdf" : null, + 11: i < 3 ? "example.com/repository" : null, + 12: i === 1 ? "example.com/demo" : null, + 30: i === 0 ? 0 : i === 3 ? null : 120, + 31: i === 0 ? false : i === 3 ? null : true, + 32: i === 2 ? "Описаны шаги проверки и развития решения." : null, + }[id]; +} +const filled = value => + value !== null && value !== undefined && !(typeof value === "string" && !value.trim()); +function evaluation(work) { + if (!competitive()) return "not_applicable"; + if (!work.submitted) return "not_submitted"; + const own = effectiveAssignments().filter(row => row.workId === work.id), + done = own.filter(row => row.completed).length; + if (!distributed()) return done ? "evaluated" : "awaiting_evaluation"; + return done >= 2 ? "evaluated" : done ? "partially_evaluated" : "awaiting_evaluation"; +} +function status(work) { + if (managerAdmin.isArchived(work.id)) return ["Архив", "muted"]; + const state = evaluation(work); + return { + not_applicable: ["Сохранено", "muted"], + not_submitted: ["Не сдано", "muted"], + awaiting_evaluation: ["Ожидает оценки", "muted"], + partially_evaluated: ["Частично оценено", "warning"], + evaluated: ["Оценено", "good"], + }[state]; +} +function statusHtml(work) { + const [label, tone] = status(work); + return `${label}`; +} +function persist() { + history.replaceState(null, "", `?screen=${screen}&format=${format}&state=${scenario}`); +} +function updateControls() { + if (!["kch", "hack", "noncompetitive"].includes(format)) format = "kch"; + if (!["analytics", "solutions", "evaluation", "results"].includes(screen)) screen = "analytics"; + if (!$("#scenario").querySelector(`option[value="${scenario}"]`)) scenario = "ready"; + $("#program-format").value = format; + $("#scenario").value = scenario; + $("#program-name").textContent = { + kch: "Кейс-чемпионат PROCOLLAB", + hack: "Хакатон PROCOLLAB", + noncompetitive: "Практикум PROCOLLAB", + }[format]; + document + .querySelectorAll("[data-screen]") + .forEach(button => + button.setAttribute("aria-current", button.dataset.screen === screen ? "page" : "false"), + ); +} +function showNotice(text) { + clearTimeout(noticeTimer); + $("#notice").textContent = text; + $("#notice").hidden = false; + noticeTimer = setTimeout(() => ($("#notice").hidden = true), 2400); +} +function exportMenu() { + return `
${icon("download")}
${[ + ["program", "Вся программа"], + ["solutions", "Все решения"], + ...(competitive() + ? [ + ["submitted", "Сданные решения"], + ["scores", "Оценки решений"], + ] + : []), + ] + .map(([kind, label]) => ``) + .join("")}
`; +} +function statePanel() { + if (scenario === "loading") + return `

Загружаем данные программы…

`; + if (scenario === "error") + return ``; + if (scenario === "no-access") + return `

Нет доступа к управлению программой

Этот раздел доступен организаторам программы.

`; + return null; +} +function metric(key, label, value, tip, ico) { + return `
${icon(ico)}

${key === "participants" ? `` : label}

${value === null ? "—" : value}
`; +} +function funnelLine(label, value, max) { + return `
${label}${value}
`; +} +function coverageRows() { + return fields() + .map(field => { + const relevant = activeWorks().filter(work => applicable(work, field)), + answered = relevant.filter(work => filled(answer(work, field.id))).length; + return `${esc(field.label)}${field.required ? "Обязательное поле" : "Необязательное поле"}${relevant.length}${answered}${field.required ? relevant.length - answered : "—"}`; + }) + .join(""); +} +function analyticsView() { + const rows = activeWorks(), + count = rows.length, + submitted = rows.filter(row => row.submitted).length, + done = rows.filter(row => evaluation(row) === "evaluated").length, + total = effectiveAssignments().length, + completed = effectiveAssignments().filter(row => row.completed).length; + return `
+

Аналитика

${exportMenu()}
+
${metric("participants", "Участники", managerAdmin.registrationCount(), "Уникальные зарегистрированные участники программы.", "person")}${metric("solutions", "Решения", count, "Заявки команд этой программы.", "folder")}${metric("submitted", "Сдано", competitive() ? submitted : null, competitive() ? "Решения с подтвержденной сдачей." : "В этой программе сдача не предусмотрена.", "analytics-submitted")}${metric("team", "Участников / решение", count ? Math.round((managerAdmin.registrationCount() / count) * 100) / 100 : 0, "Число зарегистрированных участников, деленное на число решений программы.", "analytics-team")}
+

Требует внимания

${count ? `` : '

Ничего не требует внимания

'}
+

Сроки программы

  • Регистрация
  • ${competitive() ? "
  • Прием решений
  • Оценивание
  • " : "
  • Завершение программы
  • "}
+

Воронка участников

${funnelLine("Регистрация", managerAdmin.registrationCount(), 12)}${funnelLine("Заходили на платформу", managerAdmin.registrationCount() - managerAdmin.withoutVisit(), 12)}${funnelLine("Вступили в команду", managerAdmin.teamCount(), 12)}

${competitive() ? "Воронка решений" : "Сохраненные решения"}

${funnelLine("Создано", count, 4)}${competitive() ? funnelLine("Сдано", submitted, 4) + funnelLine("Оценено", done, 4) : '

Участники сохраняют материалы без отдельной сдачи.

'}
+ ${competitive() ? `

Ход оценивания

${distributed() ? "Распределенное оценивание" : "Открытое оценивание"}

${distributed() ? '' : ""}
${distributed() && count ? `` : `

${count ? "Назначения в открытом режиме не применяются. Эксперты выбирают сданные решения программы." : "Назначений пока нет."}

`}
${distributed() ? '

Задержки экспертов

Задержек пока нет. Контроль незавершенных назначений включается за пять дней до окончания оценивания.

' : ""}
` : ""} +

Кейсы

КейсРешения${competitive() ? "Сдано" : ""}Участники
${[ + "Образование", + "Городская среда", + null, + ] + .map(name => { + const group = rows.filter(row => row.caseName === name); + return ``; + }) + .join( + "", + )}

Активность

За последние 30 дней

26 сентября25 октября

Регистрации${competitive() ? " и сдачи решений" : ""}

+

Заполнение полей решения

${competitive() ? "Для сданной работы учитывается форма на момент сдачи. Новое поле не становится пропуском в ранее сданном решении." : "Показано заполнение текущей формы. Ответы можно сохранять без отдельной сдачи."}

${coverageRows()}
ПолеПрименимоЕсть ответНет обязательного ответа
+

Регионы участников

${[ + ["Москва", 6], + ["Татарстан", 4], + ["Свердловская область", 2], + ] + .map(([name], index) => { + const value = managerAdmin.regionCount(index); + return `
${name}${value}
`; + }) + .join("")}
+
`; +} +function filterHtml(filter, { results = false } = {}) { + const filters = fields(); + return `
+
+ ${competitive() ? `` : ""} + ${ + results + ? "" + : `

«Неприменимо» означает, что поля не было в форме при сдаче. Ответы 0 и «Нет» считаются заполненными.

` + } +
`; +} +function filteredRows(filter, base = activeWorks()) { + return base.filter(work => { + if (filter.search && !workName(work).toLowerCase().includes(filter.search.toLowerCase())) + return false; + if ( + competitive() && + filter.submission !== "all" && + (filter.submission === "submitted") !== work.submitted + ) + return false; + if (filter.fieldId && filter.answer !== "all") { + const field = fields().find(row => row.id === Number(filter.fieldId)); + if (field) { + const applies = applicable(work, field), + has = applies && filled(answer(work, field.id)); + if (filter.answer === "not_applicable") return !applies; + if (!applies) return false; + if (filter.answer === "present") return has; + if (filter.answer === "missing") return !has; + } + } + return true; + }); +} +function workTable(rows, { result = false } = {}) { + return rows.length + ? `
${result ? "" : ""}${rows + .map(work => { + const count = effectiveAssignments().filter( + row => row.workId === work.id && row.completed, + ).length; + return `${result ? `` : ""}`; + }) + .join("")}
${result ? "Место" : "Решение"}РешениеКейс${result ? "Балл" : "Капитан"}${result ? "Оценки" : "Состояние"}
${work.id === 302 ? "1" : "—"}${!result ? `${work.teamSize} ${work.teamSize === 1 ? "участник" : work.teamSize < 5 ? "участника" : "участников"}` : ""}${esc(work.caseName ?? "Не выбран")}${result ? (work.id === 302 ? "7,5" : work.id === 301 ? "0" : "—") : esc(work.captain)}${result ? `${count} из 2` : statusHtml(work)}
` + : `

${scenario === "empty" ? "Решений пока нет" : "Решения не найдены"}

${scenario === "empty" ? "Здесь появятся заявки команд этой программы." : "Измените запрос или сбросьте фильтры."}

${scenario === "empty" ? "" : ''}
`; +} +function solutionList() { + const filter = managerAdmin.listFilter(), + rows = filteredRows(filter, managerAdmin.listWorks()); + return `

Решения программы

В списке: ${managerAdmin.listWorks().length}${competitive() ? "" : ". Сохраненные материалы без отдельной сдачи."}

${managerAdmin.isArchiveScope() ? "" : exportMenu()}
${managerAdmin.listToolbar()}${managerAdmin.isArchiveScope() ? managerAdmin.archiveFilters(filter) : filterHtml(filter)}${managerAdmin.emptyArchive() ? '

Архив пока пуст

Здесь появятся решения, снятые с участия или перемещенные в архив организатором.

' : workTable(rows)}
`; +} +function resultsView() { + if (!competitive()) + return `

Сдача и конкурсное оценивание не предусмотрены

Сохраненные материалы участников доступны в разделе «Решения».

`; + const filter = filterState.results, + rows = filteredRows( + filter, + activeWorks().filter(work => work.submitted), + ); + return `

Рейтинг решений

${exportMenu()}

Место появляется после необходимого числа завершенных оценок. Фильтры не меняют места в общем рейтинге.

${filterHtml(filter, { results: true })}${workTable(rows, { result: true })}
`; +} +function evaluationView() { + if (!competitive()) return resultsView(); + return `

Оценивание решений

${distributed() ? "Распределенное оценивание" : "Открытое оценивание"}

${distributed() ? `` : ""}

${distributed() ? "На решение необходимо 2 завершенные оценки. Сохраненные оценки и соответствующие назначения защищены." : "Эксперты выбирают сданные решения программы. Персональные назначения не требуются."}

${people.map((person, i) => ``).join("")}
ЭкспертНазначеноВыполнено
${person.join(" ")}${distributed() ? effectiveAssignments().filter(row => row.expert === i).length : "—"}${distributed() ? effectiveAssignments().filter(row => row.expert === i && row.completed).length : "—"}
`; +} +function renderPage() { + updateControls(); + const state = statePanel(); + $("#manager-main").innerHTML = + state || + (screen === "analytics" + ? analyticsView() + : screen === "solutions" + ? solutionList() + : screen === "results" + ? resultsView() + : evaluationView()); + persist(); +} +function currentFilter() { + return modal?.filters ?? (screen === "results" ? filterState.results : managerAdmin.listFilter()); +} +function openModal(next) { + lastTrigger = document.activeElement; + modal = next; + renderModal(); + dialog.showModal(); + $("#dialog-title").focus(); +} +function openInner(next) { + next.back = modal; + next.backFocus = document.activeElement?.dataset.focus; + modal = next; + renderModal(); + $("#dialog-title").focus(); +} +function closeModal() { + if (modal?.busy) return; + dialog.close(); + modal = null; + if (lastTrigger?.isConnected) lastTrigger.focus(); + else document.querySelector('[data-screen][aria-current="page"]')?.focus(); +} +function modalHeader(title, subtitle = "") { + return `${modal?.back ? '' : ""}`; +} +function renderModal() { + if (!modal) return; + const adminView = managerAdmin.renderModal(); + if (adminView) { + dialog.innerHTML = + modalHeader(adminView.title, adminView.subtitle) + + ``; + if (modal.busy) + dialog + .querySelectorAll(".close-button,.modal-back") + .forEach(button => (button.disabled = true)); + return; + } + let title = "", + subtitle = "", + body = ""; + if (["case", "waiting", "list"].includes(modal.view)) { + title = + modal.view === "case" + ? modal.caseName + ? "Кейс: " + modal.caseName + : "Без выбранного кейса" + : modal.view === "waiting" + ? "Решения ожидают оценивания" + : "Решения программы"; + const base = activeWorks().filter(work => + modal.view === "case" + ? work.caseName === modal.caseName + : modal.view === "waiting" + ? ["awaiting_evaluation", "partially_evaluated"].includes(evaluation(work)) + : true, + ), + rows = filteredRows(modal.filters, base); + body = `
Решения${base.length}Участники${base.reduce((n, w) => n + w.teamSize, 0)}${competitive() ? `Сдано${base.filter(w => w.submitted).length}` : ""}
${filterHtml(modal.filters)}${workTable(rows)}

${rows.length ? `1–${rows.length} из ${rows.length}` : "0 из 0"}

`; + } else if (["assignments", "work-scores"].includes(modal.view)) { + title = modal.view === "work-scores" ? "Оценки решения" : "Назначения экспертов"; + let rows = effectiveAssignments().filter(row => + modal.workId + ? row.workId === modal.workId + : modal.expert !== undefined + ? row.expert === modal.expert + : true, + ); + if (modal.scope === "completed") rows = rows.filter(row => row.completed); + if (modal.scope === "pending") rows = rows.filter(row => !row.completed); + subtitle = modal.workId ? workName(works.find(row => row.id === modal.workId)) : ""; + body = `${rows.map(row => ``).join("")}
РешениеЭкспертСтатус
${esc(workName(works.find(work => work.id === row.workId)))}${people[row.expert].join(" ")}${row.completed ? "Выполнено" : "Ожидает оценки"}
${rows.length ? "" : '

Назначения не найдены

При выбранном статусе назначений нет.

'}`; + } else if (modal.view === "scores") { + const row = + assignments.find(item => item.id === modal.id) ?? managerAdmin.savedRating(modal.id); + title = "Оценка решения"; + subtitle = + workName(works.find(work => work.id === row.workId)) + " / " + people[row.expert].join(" "); + body = `

${row.completed ? "Эксперт заполнил все критерии." : "Оценка еще не сохранена."}

Качество решения, от 0 до 10

${row.completed ? row.value : "—"}

Работоспособность подтверждена

${row.completed ? (row.value === 0 ? "Нет" : "Да") : "—"}

Комментарий эксперта

${row.completed ? (row.value === 0 ? "Прототип требует доработки перед демонстрацией." : "Решение соответствует задаче.") : "Комментарий не добавлен."}

`; + } else if (modal.view === "allocation") { + title = "Предпросмотр распределения"; + body = `

Будут распределены сданные решения. Назначения с сохраненными оценками останутся на месте.

Сданных решений
3
Назначений с оценками, сохраняются
3
Новых назначений
${assignments.some(row => row.workId === 303) ? 0 : 2}

Необходимо 2 завершенные оценки на решение.

`; + } else if (modal.view === "export") { + title = "Выгрузить данные"; + body = `

${esc(modal.label ?? "Будут выгружены найденные решения с учетом поиска и фильтров.")}

Данные будут доступны в файле Excel.

`; + } else if (modal.view === "formula") { + title = "Как считается балл"; + body = + '

Итоговый балл использует завершенные оценки экспертов и действующие критерии программы.

Незавершенная оценка не участвует в среднем. Нулевой балл и ответ «Нет» сохраняются. Место появляется после необходимого числа оценок; фильтры не пересчитывают общий рейтинг.

'; + } else if (modal.view === "reminder") { + title = "Отправить напоминание"; + body = `

Получателей: ${modal.back?.kind === "without-visit" ? managerAdmin.withoutVisit() : managerAdmin.registrationCount() - managerAdmin.teamCount()}

${modal.back?.kind === "without-team" ? "Вы участвуете в программе PROCOLLAB, но пока не состоите в команде. Создайте решение или присоединитесь к команде." : "Вы зарегистрированы в программе PROCOLLAB. Зайдите на платформу, чтобы продолжить участие."}

`; + } + dialog.innerHTML = modalHeader(title, subtitle) + ``; +} +function refreshCurrent(focusName) { + if (modal) renderModal(); + else renderPage(); + if (focusName) + (modal ? dialog : $("#manager-main")).querySelector(`[name="${focusName}"]`)?.focus(); +} +document.addEventListener("click", event => { + const target = event.target.closest("button,[data-action]"); + if (!target) return; + if (target.dataset.screen) { + if (dialog.open) closeModal(); + screen = target.dataset.screen; + renderPage(); + return; + } + const action = target.dataset.action; + if (!action) return; + if (managerAdmin.handleClick(target)) return; + if (action === "close") { + closeModal(); + return; + } + if (action === "back") { + const previous = modal.back, + focus = modal.backFocus; + if (previous) { + modal = previous; + renderModal(); + if (focus) dialog.querySelector(`[data-focus="${focus}"]`)?.focus(); + else $("#dialog-title").focus(); + } else closeModal(); + return; + } + if (action === "retry") { + $("#scenario").value = scenario = "ready"; + renderPage(); + return; + } + if (action === "solutions") { + screen = "solutions"; + if (dialog.open) closeModal(); + renderPage(); + return; + } + if (action === "case") { + openModal({ + view: "case", + caseName: target.dataset.case === "without-case" ? null : target.dataset.case, + filters: newFilter(), + }); + return; + } + if (action === "waiting") { + openModal({ view: "waiting", filters: newFilter() }); + return; + } + if (action === "people") { + openModal({ view: "people", kind: target.dataset.kind }); + return; + } + if (action === "solution") { + const next = { view: "solution", id: Number(target.dataset.id), tab: "solution" }; + modal ? openInner(next) : openModal(next); + return; + } + if (action === "solution-tab") { + modal.tab = target.dataset.tab; + renderModal(); + dialog.querySelector(`[data-tab="${modal.tab}"]`)?.focus(); + return; + } + if (action === "assignments") { + openModal({ + view: "assignments", + scope: target.dataset.scope ?? "all", + ...(target.dataset.expert !== undefined ? { expert: Number(target.dataset.expert) } : {}), + }); + return; + } + if (action === "work-scores") { + const next = { view: "work-scores", workId: Number(target.dataset.id), scope: "all" }; + modal ? openInner(next) : openModal(next); + return; + } + if (action === "assignment-scope") { + modal.scope = target.dataset.scope; + renderModal(); + dialog.querySelector(`[data-scope="${modal.scope}"]`)?.focus(); + return; + } + if (action === "score-detail") { + openInner({ view: "scores", id: Number(target.dataset.id) }); + return; + } + if (action === "allocation") { + openModal({ view: "allocation" }); + return; + } + if (action === "apply-allocation") { + if (!assignments.some(row => row.workId === 303)) + assignments.push( + { id: 805, workId: 303, expert: 1, completed: false, value: null }, + { id: 806, workId: 303, expert: 2, completed: false, value: null }, + ); + closeModal(); + renderPage(); + showNotice("Распределение применено в макете. Добавлено 2 назначения."); + return; + } + if (action === "formula") { + openModal({ view: "formula" }); + return; + } + if (action === "material") { + showNotice("Пример материала в макете. В рабочем интерфейсе откроется файл или ссылка."); + return; + } + if (action === "filters") { + const filter = currentFilter(); + filter.expanded = !filter.expanded; + refreshCurrent(); + (modal ? dialog : $("#manager-main")).querySelector('[data-action="filters"]')?.focus(); + return; + } + if (action === "reset-filters") { + const filter = currentFilter(); + Object.assign(filter, newFilter()); + refreshCurrent("search"); + return; + } + if (action === "export" || action === "export-found") { + const next = { + view: "export", + label: + action === "export-found" + ? "Будут выгружены все найденные решения с учетом поиска и фильтров." + : { + program: "Вся программа: решения, команды, поля, ответы, назначения и оценки.", + solutions: "Все решения программы с материалами и принятым составом.", + submitted: "Сданные решения с зафиксированными материалами.", + scores: "Сохраненные оценки решений.", + }[target.dataset.kind], + }; + modal ? openInner(next) : openModal(next); + return; + } + if (action === "prepare-export") { + closeModal(); + showNotice("Пример состояния: файл Excel подготовлен. Макет не выгружает реальные данные."); + return; + } + if (action === "reminder-preview") { + openInner({ view: "reminder" }); + return; + } + if (action === "send-reminder") { + closeModal(); + showNotice("Пример состояния: напоминание отправлено. Реальные письма не отправляются."); + return; + } +}); +document.addEventListener("input", event => { + if (event.target.name === "search") currentFilter().draft = event.target.value; +}); +document.addEventListener("submit", event => { + if (event.target.dataset.form === "search") { + event.preventDefault(); + currentFilter().search = currentFilter().draft.trim(); + refreshCurrent("search"); + } +}); +document.addEventListener("change", event => { + const name = event.target.name; + if (["submission", "fieldId", "answer"].includes(name)) { + const filter = currentFilter(); + filter[name] = event.target.value; + if (name === "fieldId" && !filter.fieldId) filter.answer = "all"; + refreshCurrent(name); + } +}); +$("#program-format").addEventListener("change", event => { + format = event.target.value; + assignments = originalAssignments.map(row => ({ ...row })); + managerAdmin.reset(); + filterState.solutions = newFilter(); + filterState.results = newFilter(); + if (dialog.open) closeModal(); + renderPage(); +}); +$("#scenario").addEventListener("change", event => { + scenario = event.target.value; + assignments = originalAssignments.map(row => ({ ...row })); + managerAdmin.reset(); + if (dialog.open) closeModal(); + if (scenario === "no-results") { + screen = "solutions"; + filterState.solutions = { + ...newFilter(), + draft: "Несуществующее решение", + search: "Несуществующее решение", + }; + } else { + filterState.solutions = newFilter(); + filterState.results = newFilter(); + } + renderPage(); +}); +dialog.addEventListener("cancel", event => { + event.preventDefault(); + closeModal(); +}); +dialog.addEventListener("keydown", event => { + if (event.key !== "Tab") return; + const focusable = [ + ...dialog.querySelectorAll( + "button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]", + ), + ].filter(el => el.tabIndex >= 0 && el.getClientRects().length); + if (!focusable.length) { + event.preventDefault(); + $("#dialog-title").focus(); + return; + } + const first = focusable[0], + last = focusable[focusable.length - 1], + active = document.activeElement; + if (event.shiftKey && (active === first || !focusable.includes(active))) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && (active === last || !focusable.includes(active))) { + event.preventDefault(); + first.focus(); + } +}); +dialog.addEventListener("click", event => { + if (event.target === dialog) { + const box = dialog.getBoundingClientRect(); + if ( + event.clientX < box.left || + event.clientX > box.right || + event.clientY < box.top || + event.clientY > box.bottom + ) + closeModal(); + } +}); +renderPage(); + +document.addEventListener("keydown", event => { + if (!event.target.matches('[role="tab"]') || !dialog.open) return; + const tabs = [...dialog.querySelectorAll('[role="tab"]')], + index = tabs.indexOf(event.target); + let next = index; + if (event.key === "ArrowRight") next = (index + 1) % tabs.length; + else if (event.key === "ArrowLeft") next = (index + tabs.length - 1) % tabs.length; + else if (event.key === "Home") next = 0; + else if (event.key === "End") next = tabs.length - 1; + else return; + event.preventDefault(); + tabs[next].click(); +}); diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/design-handoff.md b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/design-handoff.md new file mode 100644 index 000000000..2914c7643 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/design-handoff.md @@ -0,0 +1,179 @@ + + +# D02: интерфейс организатора, дополнение административных действий + +Обновление 25.10.2026. Статус: **DESIGN APPROVAL REQUIRED**. Существующий запрос согласования D02 остается открытым; новый вопрос не создавался. D00v3 и D01 согласованы, D03 согласуется отдельно. Angular implementation этого дополнения не начиналась. + +Открыть [интерактивный макет](http://127.0.0.1:8769/manager-v1/?screen=solutions). Четыре раздела сохранены. «Команда» остается второй вкладкой внутри заявки. Все имена и записи в прототипе вымышлены; действия меняют только память этой вкладки. Перезагрузка, смена формата или состояния сбрасывает демонстрационные действия. Прототип не вызывает API, не отправляет письма, не создает Excel и не проверяет серверные подписи. Панель «Ответ на действие» относится только к просмотру макета. + +## Что добавлено + +- «Активные» / «Архив» в существующем разделе решений. Архив дает чтение материалов, принятого состава и истории; оценки сохраняются, но отдельного просмотра архивных значений текущий reader не предоставляет. Восстановление не предлагается: соответствующего API нет. +- Смена кейса черновика или сданной работы: выбор актуального кейса → последствия → подтверждение. Сохраняются материалы, команда и оценки; снимаются только подходящие под правило API назначения без оценок. +- Исключение принятого участника: прежний и новый состав доступны в истории. Капитан не исключается этой командой; удаление его регистрации архивирует решение. Регистрация самого исключенного участника остается. +- Снятие с участия / архив неконкурсных материалов: сохраняются материалы, состав и оценки; снимаются назначения и ожидающие приглашения. После успеха работа исчезает из активного списка. +- Участники программы → выбор регистраций → конкретные последствия → подтверждение → независимая история удаления. Выбор капитана, принятого участника, ожидающего приглашенного и человека без решения различается. Учетная запись и другие программы сохраняются. Удаление регистрации уже архивного участника не переписывает архивный состав или прежние события. +- Конфликт связи с Project блокирует всю новую команду; частичного удаления нет. Старые Project-программы продолжают прежний путь. Прототип показывает это состояние, а не доказывает сохранность production данных. +- Первая сдача организатором: неполный черновик блокируется с названиями полей, готовый — подтверждается. Повторная сдача и искусственная сдача неконкурсной работы не предлагаются. Состояние «Готовый черновик» показывает успешный путь на работе «Лаборатория практики». +- История решения открывает «До изменения» / «После изменения», сохраняя материалы, 0, «Нет» и состав. Общая история регистраций доступна и без связанного решения. Это отдельные просмотры, не третья вкладка команды. + +## Источники и API mapping + +Актуальные product refs: BE `394bf1666825ea87e7985b88c401a4e3c9f8d316`, FE `b4191a8b082e41b318c67c7605fa2764b647c46c`; до дополнения макета отчеты находятся на BE `999a3cf7b9166430f5e16a188e928826ca64cc22` / FE `5be54769627aa4c2fdd85f73e759e2aba2972f52`. C00 **1.20.1 / 156 schemas / 59 operations / 299 scenarios**. Код API и Angular продукта этим макетом не меняется. + +Все пути имеют префикс `/programs/{program_id}/`. Настоящие ID, capabilities, nullable counts, readiness/errors, token, подпись, срок действия и повторная проверка принадлежат серверу. В Angular нельзя выводить право из присутствия кнопки, повторять apply автоматически или подменять Solution.id на Project.id. + +| Действие | Контракт | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Список, материалы, команда, архив | GET `solutions/?participation=archived` / detail для архива, `management/solution-analytics/solutions/` для активного списка; accepted/archive snapshot roster | +| Первая сдача | POST `management/solutions/actions/preview/` / `apply/`, action=`submit`, ids; последняя проверка readiness на сервере | +| Смена кейса | POST `management/solutions/{solution_id}/case/preview/` / `apply/`, case_name | +| Исключение участника | POST `management/solutions/{solution_id}/members/remove/preview/` / `apply/`, user_id | +| Архив | POST `management/solutions/archive/preview/` / `apply/`, ids | +| История решения | GET `management/solutions/{solution_id}/history/` и `history/{change_id}/` | +| Удаление регистрации | POST `management/registrations/remove/preview/` / `apply/`, **registration_ids**, не User.id | +| Общая история | GET `management/registrations/removals/` и `removals/{audit_id}/` | + +Архив использует отдельный поиск и pagination общего solutions reader. Фильтр по полю/кейсу и XLSX найденных относятся к активному manager reader и не предлагаются в архиве. Значения архивных оценок остаются в базе, но отдельный архивный rating reader не введен: макет не обещает его через снятые назначения. + +В preview `score_count` означает число значений критериев, не количество экспертов. В синтетической полной оценке три значения; 0 и False сохраняются. В реальном UI все числа и эффекты берутся из ответа сервера, не вычисляются из демонстрационных назначений. Поисковые/field/answer фильтры и XLSX уже доступны по C00 1.12; нижний первоначальный handoff сохранен как исторический источник. + +## Дополнительные AC D02 + +Исходные D02-01–13 ниже сохранены. Эти проверки дополняют макет и не заменяют исходные 26 AC полного выпуска. + +| AC | Наблюдаемый результат | +| ------ | -------------------------------------------------------------------------------------------------------------------------------------------- | +| D02-14 | Первая сдача организатором имеет preview; неполная работа заблокирована с названиями полей; у сданной/неконкурсной работы кнопки нет | +| D02-15 | После смены кейса видны новый кейс и история до/после; 0/False, материалы и команда сохраняются | +| D02-16 | Исключение участника показывает оставшийся состав и последствия; капитан защищен от этого действия | +| D02-17 | Архив имеет явное подтверждение с числами; после успеха нет редактирования, работа доступна в архивном списке | +| D02-18 | При удалении регистраций перечислены выбранные люди и эффекты для каждого затронутого решения; отмена сохраняет выбор | +| D02-19 | Участник без решения и pending-only имеют независимый аудит; архивный состав/события не переписываются | +| D02-20 | Конфликт Project блокирует всю выборку и объясняет дальнейший шаг, apply недоступен | +| D02-21 | Устаревший preview и неопределенный результат требуют обновления данных и нового подтверждения; повторный apply автоматически не выполняется | +| D02-22 | Loading, apply busy, ошибка и потеря прав различимы; повтор/закрытие во время подтверждения блокируются; preview можно закрыть | +| D02-23 | Мобильный список/preview/history читаемы; фокус удерживается и возвращается; стрелки/Home/End переключают вкладки | + +## States, continuity и quality pass + +Использованы существующие ProgramManagement, ParticipantList, Detail/TeamManagement и Modal/confirmation patterns. Components: текущие Button, Search/Filters, DataTable, Modal, Tabs, Status, Empty/Error, readonly answers. Tokens/Mont/логотип/иконки взяты из прежнего D02 и соседних Angular references; **New DS additions: нет**. [Figma DS reference](https://www.figma.com/design/tvog0fpEgWgEtr7KUTjbyE/PROCOLLAB-Design-System-v1); новый Figma screen не создавался, результат согласуется в HTML. + +Operate: сначала объект и последствия, затем одна кнопка подтверждения; никакой отдельной команды вне заявки. Layout: прежние четыре раздела, сетка и плотность; административные действия вынесены в footer и последовательные просмотры. На мобильном списки перестраиваются с подписями, definition list последствий идет одной колонкой, dialog прокручивается; история не расширяет набор вкладок заявки. + +Bounded critique (не полный `$impeccable critique`): + +1. Отсутствовали административные действия, уже существующие в API: добавлены конкретные entry points, previews и конечные состояния. +2. Снятие можно было принять за удаление данных: прямо указаны сохранение материалов/состава/оценок, отзыв pending и снятие assignments; архив остается читаемым. +3. Удаление регистрации без решения теряло место в истории: добавлена отдельная история программы, без дублирования анкеты и email. +4. После изменения данных или неопределенного результата опасен повтор старого подтверждения: apply блокируется, требуется новый просмотр. Busy предотвращает повторный клик и закрытие до результата. +5. Pending и принятый состав легко смешать: отдельный блок приглашений, счетчики состава и отображение отозванной связи уточнены; смена формата сбрасывает демонстрационные данные. + +Polish сохраняет Mont, цвета и существующие кнопки/модальные окна. Полный accessibility audit и WCAG AA не заявляются. Клавиатура/видимый фокус/области действий, длинные тексты и 320/390/768/1440 проверяются в browser evidence. Контраст inherited controls не модернизировался глобально. + +## Проверка дополнения и границы + +Завершенный Chrome 156 headless run: **130 проверок PASS** — 48 комбинаций формата/раздела/ширины, 32 page states и 50 проверок сценариев. Матрица 320/390/768/1440; административные переходы на 390/1440 во всех трех форматах. 0 page errors, 0 failed localhost requests, 0 прикладных non-GET requests. Внедренные внешние запросы антивируса заблокированы отдельно (только hostname/method/count в evidence). Node syntax check PASS, locked Prettier 3.8.3, layout detector `[]` / exit 0. Detector смотрит static shell; динамический DOM проверен браузером. Дополнительно сохранены четыре baseline/current continuity screenshots, два мобильных и три action/history screenshots просмотрены визуально. Это проверки прототипа, не Angular/API тесты. + +Старый `qa-evidence.json` и старые screenshots относятся к первоначальному D02. Новое дополнение имеет отдельный `admin-qa-evidence.json` и screenshots `admin-*`. До изменения сохранены исходные файлы и их hashes в соседнем `manager-v1-before-admin-20261025`; неудачные попытки проверки не удалены. В первой проверке helper ожидал одну кнопку решения, хотя строка имеет название и «Открыть»; во второй helper искал ссылку на странице вместо drilldown. Эти ошибки исправлены без ослабления проверяемого поведения. Внешние запросы внедренного антивирусного расширения отделены от приложения и заблокированы в завершенном проверочном браузере. + +Not checked: реальный Angular manager UI, API из этого макета, серверные подписи и права в browser run, отправка, XLSX, production и миграция существующих программ. Изменения не закрывают AC18/19, QA01/REL01 или первый выпуск; AC21/BE10 остается отдельно. Согласование D02/D03 и вопрос о несданных конкурсных черновиках уже ожидают ответа, это дополнение их не разрешает. + +Перед Angular требуется явное согласование по [procollab-design-screen/SKILL.md](Z:/Документы/PROCOLLAB-dev/procollab-design-system-v1/.codex/skills/procollab-design-screen/SKILL.md): «После Design QA, visual continuity и Impeccable предъяви конкретный дизайн и остановись со статусом **DESIGN APPROVAL REQUIRED** … Дождись явного approval пользователя». Это проектный design gate, новый approval flow не вводится. + +--- + +## Первоначальный D02 handoff (исторический источник) + +# D02 Интерфейс организатора для решений программы + +Обновление API от 25.10.2026: первоначальный контрактный gap фильтра поля/ответа и выгрузки найденных закрыт C00 1.12.0. Query field_id/answer_state использует applied snapshot/current форму и 0/false; XLSX solutions/submitted принимает тот же непагинационный scope. Backend FLT01–FLT08: 15 новых / 244 общего PostgreSQL прогона, 143 HTTP checks; frontend API layer: 20 новых wire / 2754 полного прогона. См. docs/product/program-solutions-be09.md и program-solutions-fe08-api.md в рабочих feature checkout. Это не согласование D02 и не подключение Angular controls: design approval по-прежнему ожидается. Нижние ссылки C00 1.9.0 описывают источник при первоначальной подготовке макета. + +Статус: **DESIGN APPROVAL REQUIRED**. D00 v3 и D01 остаются согласованными. Этот макет расширяет организаторский путь; его Angular-реализация еще не начиналась. + +Открыть: http://127.0.0.1:8769/manager-v1/ . Это интерактивный HTML-прототип с вымышленными данными. Смена формата и состояния находится в отдельной панели просмотра. Реальные данные, письма и оценки не изменяются; кнопки выгрузки показывают состояние, файл Excel не создают. + +## Источники и границы + +- Frontend checkout: `procollab-dev-program-solutions-fe01-20261024`, head `90fdffcbd2e7565a3b0462d09d778668ef3e2574`, включает dev `1590518422749a19f375c4de0d781748beaf2f3a` с согласованными свежими изменениями формы оценивания из #506. Backend head: `411268589bd50be50c60e5f2ef3f2527d8f811b4`. +- Соседние существующие экраны: `projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.*`, `projects/social_platform/src/app/ui/pages/program/detail/analytics/drilldown/analytics-drilldown.component.*`, `projects/social_platform/src/app/ui/pages/program/management/results/results.component.*`. Из их фактического SCSS собраны `analytics-reference.css`, `drilldown-reference.css`, `results-reference.css` с локальной областью действия. Глобальный CSS, Mont и иконки взяты из того же Angular checkout. Deployed production SHA в этом проходе не проверялся. +- Поведение API: C00 1.9.0, BE09 ANA01–ANA12 и frontend `docs/product/program-solutions-fe08-api.md`. Новый фильтр по полю/наличию ответа и выгрузка найденных с этим фильтром — контрактный gap, который нужно закрыть до подключения соответствующих действий. Макет не доказывает готовность этих API. +- Figma link: [PROCOLLAB Design System v1](https://www.figma.com/design/tvog0fpEgWgEtr7KUTjbyE/PROCOLLAB-Design-System-v1). Новый экран в Figma не создан: согласуется существующий локальный HTML-прототип. Локальные DS contracts и actual Angular source использованы как references; записи в Figma не выполнялись. + +## Current и target behavior + +Сейчас организаторский путь использует Project DTO, списки проектов, проектные назначения и жесткий фильтр наличия презентации. При выборе ProgramSolution нужны настоящие решения, их принятый состав и снимок сдачи. Поля формы зависят от программы, поэтому презентация не может быть универсальным фильтром. + +Сохраняются четыре текущих раздела: «Решения», «Оценивание», «Результаты», «Аналитика». Список или кейс открывает материалы в модальном окне; «Команда» — вторая вкладка той же заявки. Из назначения можно открыть оценку и вернуться в исходный список с прежними фильтрами. Никакого выбора самостоятельной команды и подмены Solution.id на Project.id нет. + +Роль этого пути — организатор/администратор с действующим server-side доступом. Кнопки не являются проверкой прав. Экспертный путь D01, участник и публикация для внешних посетителей здесь не перепроектируются. + +## Проверяемые требования + +| AC | Предусловие | Наблюдаемый результат | +| ------ | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| D02-01 | Организатор открыл solution-программу | Четыре раздела используют названия решений; основное меню и визуальный характер текущего кабинета сохранены | +| D02-02 | Открыт список или кейс | Поиск применяется по кнопке/Enter, выбранные фильтры видны; есть сброс и счетчик найденных | +| D02-03 | Раскрыты фильтры | Можно выбрать поле формы и «Есть ответ», «Нет ответа», «Неприменимо»; для хакатона доступны его поля, презентация не навязывается | +| D02-04 | Сданная работа не содержала добавленное позже поле | Она относится к «Неприменимо», а не к пропущенному обязательному ответу; 0 и «Нет» считаются ответами | +| D02-05 | Открыто решение | Первая вкладка показывает материалы, вторая принятый состав; у сданной работы показан факт фиксации материалов и состава | +| D02-06 | Решение открыто из отфильтрованного кейса | Возврат сохраняет поле/состояние ответа и поиск; Escape закрывает окно, фокус возвращается к исходной кнопке | +| D02-07 | Открыто назначение с полной оценкой 0/False | Видны 0, «Нет» и текстовый комментарий; отсутствие ответа не заменяется нулем | +| D02-08 | Неконкурсная программа | Показываются сохраненные материалы; сдача и назначения неприменимы, их счетчики не подменяются нулями, распределение недоступно | +| D02-09 | Открытое оценивание конкурсной программы | Агрегаты назначений обозначены неприменимыми; распределение не предлагается | +| D02-10 | Программа пуста / поиск ничего не дал / API вернул ошибку или запрет | Различимые состояния, безопасный повтор/сброс; нет ложной сводки при запрете | +| D02-11 | Узкий экран или длинные русские названия | Нет горизонтального переполнения; строки таблицы перестраиваются с подписями колонок, модальное окно прокручивается | +| D02-12 | Навигация клавиатурой | Фокус видим, Tab/Shift+Tab остаются внутри окна; стрелки/Home/End переключают вкладки «Решение»/«Команда» | +| D02-13 | Запрошено распределение/напоминание | Перед действием есть конкретный просмотр; текст участнику без команды предлагает создать решение или присоединиться к команде | + +«Участников / решение» сохраняет текущую формулу registered participants / solutions. В демонстрации 12 / 4 = 3; это не среднее число принятых участников команды. Итоговые места не пересчитываются при локальном поиске/фильтрации. Регионы относятся к участникам: у решения нет выдуманного Project.region. + +## Pattern used и Components used + +Patterns: существующие ProgramManagement, Analytics/Dashboard, ListPage, readonly DetailPage/TeamManagement. DS patterns Analytics и ProgramManagement имеют статус PROPOSED; макет не превращает их в разрешение мигрировать текущий UI. + +Components: текущие PageHeader, Button primary/outline, Search, Filters/Select, DataTable/Pagination, Card/Metric, Empty/Error state, Modal, вкладки Solution/Team и readonly критерии. HTML-контроли здесь только прототип; в Angular нужно переиспользовать существующие компоненты и фасады, сохраняя lifecycle/cancel при смене программы. Новых DS additions: **нет**. + +Tokens: Mont из локальных assets; существующие `--accent`, `--black`, `--grey-for-text`, `--light-gray`, `--medium-grey-for-outline`, `--light-white`; размеры/радиусы наследуют scoped source CSS. Акцент и плотность не меняются ради новой сущности. Текст интерфейса использует Е/е. + +## States responsive и accessibility + +| Состояние | Проверка прототипа | Реализация после approval | +| -------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Ready, КЧ/хакатон/неконкурсная | 4 раздела × 320/390/768/1440 px, Mont, actual rows | Реальные manager DTO, настоящий ID и capability/null | +| Loading, empty, no results, error/retry, no access, long names | Нет переполнения на всех четырех ширинах, переход retry работает | Сопоставить фактическим server responses и отмене устаревших запросов | +| Open evaluation | Назначения неприменимы; нет распределения | Не создавать искусственные aggregate values | +| Field presence | 0/False, present/not_applicable, разные формы | Дополнить C00 и backend; одна семантика списка и filtered XLSX | +| Solution/team/back | Вкладки, состав, сохранение фильтров | Использовать accepted/snapshot команды, не pending invitations | +| Keyboard | Tab/Shift+Tab, стрелки вкладок, Escape и return focus | Использовать текущий shared Modal и проверить реальный Angular DOM | +| Allocation/reminder preview | Синтетический preview и локальный feedback, без API mutations | Действующие permissions, preview/revalidation, серверная отправка/idempotency | + +Полный аудит WCAG/контраста не заявляется. Видимый focus и чтение подписей проверены; основная мобильная кнопка закрытия имеет область 44 px. Размеры других унаследованных controls не модернизировались глобально. + +## Design QA и Impeccable + +Operate constraints: сканируемость существующих списков, знакомые действия, одна модальная поверхность с возвратом, отсутствие декоративного редизайна. Layout review: прежняя сетка, порядок чтения и плотность; мобильные таблицы перестраиваются в подписанные строки, длинные названия переносятся. + +Bounded critique и polish: + +1. Жесткий фильтр «Презентация» не охватывает хакатон: заменен выбором настроенного поля и наличия ответа. +2. 0/False можно ошибочно прочитать как отсутствие: демонстрационные данные и проверки явно сохраняют заполненность и значения. +3. Подпись «Команда» скрывала смысл среднего: переименована в «Участников / решение», текущая формула сохранена. +4. Плотная таблица на мобильном теряла контекст колонок: сохранено преобразование в строки с подписями, проверены длинные названия. +5. Добавленное обязательное поле создавало неоднозначность старых сдач: показаны «Неприменимо» и объяснение снимка формы. + +Механический Impeccable layout scan вернул `[]` с exit 0. Он анализировал только статический HTML shell; динамические элементы проверялись браузером. Полный `$impeccable critique` не запускался. + +## Evidence + +- **Checked by source:** текущие Angular SCSS/templates/facades, C00 1.9.0, BE09 и FE08 API specs. Визуальное сравнение с унаследованными стилями выполнено; deployed production browser не проверялся. +- **Checked by tests/browser:** `qa-evidence.json`, Playwright + headless Edge, localhost only, 3 формата × 4 раздела × 4 ширины, 7 дополнительных состояний. Field filters/0/False/not_applicable, решение → команда → возврат, клавиатура, оценка, retry, allocation и reminder preview PASS. Ошибок JS и загрузки assets нет. +- **Checked by syntax:** `node --check app.js` PASS. Framework build для статического макета неприменим. +- **Screenshots:** `analytics-desktop.png`, `analytics-mobile.png`, `solutions-desktop.png`, `solutions-mobile.png`, `evaluation-desktop.png`, `evaluation-mobile.png`, `results-desktop.png`, `results-mobile.png`, `case-field-filter-desktop.png`, `case-field-filter-mobile.png`, `solution-team-mobile.png`, `assignment-score-desktop.png`. +- **Not checked:** runtime manager Angular UI, API-операции из макета, реальная доставка/публикация, production, полный accessibility audit. Backend API/XLSX evidence находится в предыдущем отдельном implementation checkpoint и не заменяет проверку нового UI. + +## Unresolved questions и следующий шаг + +Нужно явное согласование D02 перед Angular. Контракт field/answer filters и filtered export уже реализован в C00 1.12. После approval: подключить организаторские экраны и административные preview/apply/history по актуальному C00 1.20.1, сохранить legacy branch и проверить реальный Angular UI. Напоминания и другие независимые backend-блоки можно продолжать, пока дизайн ожидает ответа. + +Публикация отдельно подтверждена внутри раздела «Решения» самой программы: данные остаются в ProgramSolution/связанных моделях и существующем файловом хранилище, Project не создается. Для неконкурсной программы отдельная сдача не нужна. Вопрос о несданных конкурсных черновиках задан ранее и еще не разрешен; макет D02 этого правила не вводит. diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/drilldown-reference.css b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/drilldown-reference.css new file mode 100644 index 000000000..9d8cb0f0b --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/drilldown-reference.css @@ -0,0 +1,968 @@ +/** @format */ +/** @format */ +/** @format */ +.prototype-drilldown { + /** @format */ +} +.prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--case { + box-sizing: border-box; + width: min(880px, 100% - 24px); + padding: 20px; + overflow-y: auto; +} +@media screen and (max-width: 999px) { + .prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--case { + max-height: calc(var(--visual-viewport-height, 100dvh) - 32px); + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--case { + max-height: calc(100dvh - 32px); + } +} +.prototype-drilldown .analytics-case__metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + padding: 10px 12px; + margin: 0 0 16px; + background: var(--light-gray); + border-radius: 8px; +} +.prototype-drilldown .analytics-case__metrics div { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: baseline; +} +.prototype-drilldown .analytics-case__metrics dt { + font-size: var(--ui-font-small); + color: var(--dark-grey); +} +.prototype-drilldown .analytics-case__metrics dd { + margin: 0; + font-weight: 600; +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-case__metrics { + display: flex; + } + .prototype-drilldown .analytics-case__metrics div + div { + padding-left: 12px; + border-left: 1px solid var(--medium-grey-for-outline); + } +} +.prototype-drilldown .analytics-case__export { + display: grid; + gap: 6px; + margin-bottom: 16px; + font-size: var(--ui-font-small); +} +.prototype-drilldown .analytics-case__export app-button { + width: 100%; +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-case__export { + justify-items: start; + } + .prototype-drilldown .analytics-case__export app-button { + width: 220px; + } +} +.prototype-drilldown .analytics-case__filters { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 12px; + margin: 12px 0 20px; +} +.prototype-drilldown .analytics-case__filters label { + display: grid; + gap: 6px; + font-size: var(--ui-font-small); + color: var(--dark-grey); +} +.prototype-drilldown .analytics-case__filters select { + box-sizing: border-box; + width: 100%; + min-height: 44px; + padding: 10px 12px; + font: inherit; + font-size: 14px; + color: var(--black); + background: var(--white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-drilldown .analytics-case__filters select:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +@media screen and (min-width: 600px) { + .prototype-drilldown .analytics-case__filters { + grid-template-columns: repeat(2, minmax(0, 220px)); + } +} +.prototype-drilldown .analytics-case__loading { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + padding: 48px 12px; + color: var(--dark-grey); +} +.prototype-drilldown .analytics-case .analytics-drilldown__search-controls { + max-width: 100%; +} +.prototype-drilldown .analytics-case .analytics-drilldown__search-controls input { + flex-basis: 100%; + border-radius: var(--rounded-xxl); +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-case .analytics-drilldown__search-controls input { + flex-basis: 220px; + } +} +.prototype-drilldown .analytics-case .analytics-case__table small { + display: block; + margin-top: 4px; + font-size: var(--ui-font-small); +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-case .analytics-case__table th, + .prototype-drilldown .analytics-case .analytics-case__table td { + padding: 10px 8px; + } + .prototype-drilldown .analytics-case .analytics-case__table th { + font-size: 13px; + } + .prototype-drilldown .analytics-case .analytics-case__table th:first-child, + .prototype-drilldown .analytics-case .analytics-case__table td:first-child { + width: 32%; + } + .prototype-drilldown .analytics-case .analytics-case__table th:nth-child(2), + .prototype-drilldown .analytics-case .analytics-case__table td:nth-child(2) { + width: 28%; + } + .prototype-drilldown .analytics-case .analytics-case__table th:nth-child(3), + .prototype-drilldown .analytics-case .analytics-case__table td:nth-child(3) { + width: 20%; + } + .prototype-drilldown .analytics-case .analytics-case__table th:last-child, + .prototype-drilldown .analytics-case .analytics-case__table td:last-child { + width: 20%; + } +} +.prototype-drilldown .modal__body.analytics-drilldown-body { + box-sizing: border-box; + width: min(880px, 100vw - 24px); + padding: 20px; + overflow-y: auto; +} +@media screen and (max-width: 999px) { + .prototype-drilldown .modal__body.analytics-drilldown-body { + max-height: calc(var(--visual-viewport-height, 100dvh) - 32px); + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .modal__body.analytics-drilldown-body { + max-height: calc(100dvh - 32px); + } +} +.prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments { + width: min(980px, 100vw - 32px); + padding: 16px; +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments { + padding: 28px; + } +} +.prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__header { + align-items: center; + margin-bottom: 24px; +} +.prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__header + h2 { + line-height: 30px; +} +@media screen and (max-width: 999px) { + .prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__header + h2 { + font-size: var(--ui-font-h1); + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__header + h2 { + font-size: 24px; + } +} +.prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__close { + display: grid; + place-items: center; + padding: 0; + line-height: 1; +} +.prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__close:hover { + background: var(--light-gray); +} +.prototype-drilldown .analytics-drilldown { + width: 100%; + min-width: 0; + font-size: var(--ui-font-body); + line-height: 1.5; + color: var(--black); + overflow-wrap: anywhere; +} +.prototype-drilldown .analytics-drilldown h2 { + font-size: 20px; + font-weight: 600; +} +.prototype-drilldown .analytics-drilldown h3 { + font-size: var(--ui-font-h3); + font-weight: 600; +} +.prototype-drilldown .analytics-drilldown ul { + padding: 0; + list-style: none; +} +.prototype-drilldown .analytics-drilldown button { + font: inherit; + cursor: pointer; +} +.prototype-drilldown .analytics-drilldown button:focus-visible, +.prototype-drilldown .analytics-drilldown a:focus-visible, +.prototype-drilldown .analytics-drilldown input:focus-visible, +.prototype-drilldown .analytics-drilldown h2:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} +.prototype-drilldown .analytics-drilldown__header { + display: flex; + gap: 12px; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 20px; +} +.prototype-drilldown .analytics-drilldown__close { + flex: 0 0 32px; + width: 32px; + height: 32px; + font-size: 26px !important; + color: var(--dark-grey); + background: transparent; + border: 0; + border-radius: 8px; +} +.prototype-drilldown .analytics-drilldown__action { + padding: 6px 0; + color: var(--accent-dark); + text-align: left; + background: transparent; + border: 0; +} +.prototype-drilldown .analytics-drilldown__action:hover { + color: var(--accent-dark); +} +.prototype-drilldown .analytics-drilldown__action:disabled { + color: var(--dark-grey); + text-decoration: none; + cursor: default; + opacity: 0.6; +} +.prototype-drilldown .analytics-drilldown__search { + display: grid; + gap: 6px; + margin-bottom: 16px; +} +.prototype-drilldown .analytics-drilldown__search-controls { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} +.prototype-drilldown .analytics-drilldown__search-controls input { + box-sizing: border-box; + min-width: 0; + max-width: 100%; + min-height: var(--ui-control-height); + padding: var(--ui-control-padding); + font: inherit; + font-size: var(--ui-font-body); + line-height: 1.4; + color: var(--ui-text); + background: var(--ui-surface-subtle); + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-control); + outline: none; + transition: + border-color 0.2s, + box-shadow 0.2s; + flex: 1 1 220px; + width: 100%; + font-size: 12px; +} +.prototype-drilldown .analytics-drilldown__search-controls input:is(select) { + padding-right: 44px; + appearance: none; + background-image: url("/assets/icons/svg/chevron-down.svg"); + background-repeat: no-repeat; + background-position: right 16px center; + background-size: 14px; +} +.prototype-drilldown .analytics-drilldown__search-controls input::placeholder { + color: var(--dark-grey); +} +.prototype-drilldown .analytics-drilldown__search-controls input:hover:not(:disabled) { + border-color: var(--dark-grey); +} +.prototype-drilldown .analytics-drilldown__search-controls input:focus-visible, +.prototype-drilldown .analytics-drilldown__search-controls input:focus-within, +.prototype-drilldown .analytics-drilldown__search-controls input--open { + border-color: var(--ui-accent); + box-shadow: var(--ui-focus-ring); +} +.prototype-drilldown .analytics-drilldown__search-controls input--error, +.prototype-drilldown .analytics-drilldown__search-controls input--overflow, +.prototype-drilldown .analytics-drilldown__search-controls input[aria-invalid="true"] { + border: 1px solid var(--ui-danger); +} +.prototype-drilldown .analytics-drilldown__search-controls input--error:focus, +.prototype-drilldown .analytics-drilldown__search-controls input--error:focus-within, +.prototype-drilldown .analytics-drilldown__search-controls input--overflow:focus, +.prototype-drilldown .analytics-drilldown__search-controls input--overflow:focus-within, +.prototype-drilldown .analytics-drilldown__search-controls input[aria-invalid="true"]:focus, +.prototype-drilldown .analytics-drilldown__search-controls input[aria-invalid="true"]:focus-within { + box-shadow: var(--ui-error-ring); +} +.prototype-drilldown .analytics-drilldown__search-controls input:disabled, +.prototype-drilldown .analytics-drilldown__search-controls input--disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); +} +@media (max-width: 749px) { + .prototype-drilldown .analytics-drilldown__search-controls input { + font-size: 16px; + } +} +.prototype-drilldown .analytics-drilldown__count { + margin-bottom: 12px; +} +.prototype-drilldown .analytics-drilldown__pagination { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + margin-top: 16px; +} +.prototype-drilldown .analytics-drilldown__identity { + display: flex; + gap: 8px; + align-items: center; + min-width: 0; +} +.prototype-drilldown .analytics-drilldown app-avatar { + flex-shrink: 0; +} +.prototype-drilldown .analytics-drilldown__secondary { + color: var(--dark-grey); +} +.prototype-drilldown .analytics-drilldown__metadata { + margin-top: 8px; + font-weight: 600; +} +.prototype-drilldown .analytics-drilldown__empty { + padding: 32px 12px; + text-align: center; +} +.prototype-drilldown .analytics-drilldown__error { + padding: 16px; + margin-block: 12px; + border: 1px solid var(--red); + border-radius: 12px; +} +.prototype-drilldown .analytics-drilldown__section { + margin-block: 20px 12px; +} +.prototype-drilldown .analytics-drilldown__criteria, +.prototype-drilldown .analytics-drilldown__experts { + display: grid; + gap: 12px; +} +.prototype-drilldown .analytics-drilldown__criteria li { + display: grid; + gap: 6px; + padding: 14px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 12px; +} +.prototype-drilldown .analytics-drilldown__criteria strong { + white-space: pre-wrap; +} +.prototype-drilldown .analytics-drilldown__expert { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + width: 100%; + padding: 14px; + color: var(--black); + text-align: left; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 12px; +} +.prototype-drilldown .analytics-drilldown__expert:hover { + border-color: var(--accent); +} +.prototype-drilldown .analytics-drilldown__expert > .analytics-drilldown__identity { + flex-basis: 100%; +} +.prototype-drilldown .analytics-drilldown__badge { + display: inline-block; + padding: 3px 7px; + font-size: var(--ui-font-small); + font-weight: 600; + line-height: 18px; + color: var(--dark-grey); + background: var(--light-gray); + border-radius: 6px; +} +.prototype-drilldown .analytics-drilldown__badge--pending { + color: var(--gold-dark); + background: var(--gold-light); +} +.prototype-drilldown .analytics-drilldown__badge--in_progress { + color: var(--blue-dark); + background: var(--accent-light); +} +.prototype-drilldown .analytics-drilldown__badge--completed { + color: var(--green-dark); + background: var(--green-light); +} +.prototype-drilldown .analytics-drilldown__badge--critical { + color: var(--red-dark); + background: var(--red-light); +} +.prototype-drilldown .analytics-drilldown__table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; +} +.prototype-drilldown .analytics-drilldown__table thead { + display: none; +} +.prototype-drilldown .analytics-drilldown__table tbody { + display: grid; + gap: 12px; +} +.prototype-drilldown .analytics-drilldown__table tr { + display: block; + padding: 12px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 12px; +} +.prototype-drilldown .analytics-drilldown__table td { + display: block; + padding-block: 4px; +} +.prototype-drilldown .analytics-drilldown__table td::before { + display: block; + font-size: var(--ui-font-small); + font-weight: 600; + color: var(--dark-grey); + content: attr(data-label); +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table thead { + display: table-header-group; + } + .prototype-drilldown .analytics-drilldown__table tbody { + display: table-row-group; + } + .prototype-drilldown .analytics-drilldown__table tr { + display: table-row; + padding: 0; + border: 0; + } + .prototype-drilldown .analytics-drilldown__table th, + .prototype-drilldown .analytics-drilldown__table td { + display: table-cell; + padding: 12px 8px; + text-align: left; + vertical-align: top; + border-bottom: 1px solid var(--medium-grey-for-outline); + } + .prototype-drilldown .analytics-drilldown__table th { + font-size: 13px; + font-weight: 600; + } + .prototype-drilldown .analytics-drilldown__table td::before { + display: none; + } + .prototype-drilldown .analytics-drilldown__table th:first-child { + width: 25%; + } + .prototype-drilldown .analytics-drilldown__table th:last-child { + width: 15%; + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table--projects th:first-child { + width: 18%; + } +} +.prototype-drilldown .analytics-drilldown__table--assignments tr { + display: grid; + gap: 12px; + padding: 14px; +} +.prototype-drilldown .analytics-drilldown__table--assignments td { + padding: 0; + line-height: 22px; +} +.prototype-drilldown .analytics-drilldown__table--assignments td::before { + margin-bottom: 4px; +} +.prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__identity { + gap: 12px; + font-weight: 500; +} +.prototype-drilldown + .analytics-drilldown__table--assignments + .analytics-drilldown__assignment-status { + display: flex; + flex-flow: column wrap; + gap: 12px 14px; + align-items: flex-start; +} +.prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__badge { + display: inline-flex; + gap: 6px; + align-items: center; + min-height: 28px; + padding: 4px 9px; + border-radius: 8px; +} +.prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__badge::before { + flex: 0 0 6px; + width: 6px; + height: 6px; + content: ""; + background: currentcolor; + border-radius: 50%; +} +.prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__action { + padding: 4px 0; + line-height: 20px; +} +@media screen and (max-width: 999px) { + .prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__action { + font-size: var(--ui-font-body); + font-weight: 500; + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__action { + font-size: 13px; + font-weight: 500; + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table--assignments { + border-spacing: 0; + border-collapse: separate; + } + .prototype-drilldown + .analytics-drilldown__table--assignments + .analytics-drilldown__assignment-status { + flex-direction: row; + align-items: center; + } + .prototype-drilldown .analytics-drilldown__table--assignments tr { + display: table-row; + height: 72px; + padding: 0; + } + .prototype-drilldown .analytics-drilldown__table--assignments th, + .prototype-drilldown .analytics-drilldown__table--assignments td { + padding: 16px 12px; + vertical-align: middle; + } + .prototype-drilldown .analytics-drilldown__table--assignments thead tr { + height: auto; + } + .prototype-drilldown .analytics-drilldown__table--assignments th { + padding-block: 13px; + color: var(--dark-grey); + background: var(--light-gray); + border-bottom: 0; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:first-child { + width: 27%; + border-radius: 8px 0 0 8px; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(2) { + width: 31%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(3) { + width: 29%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:last-child { + width: 13%; + border-radius: 0 8px 8px 0; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(3), + .prototype-drilldown .analytics-drilldown__table--assignments td:nth-child(3) { + padding-inline: 8px; + } +} +.prototype-drilldown .analytics-drilldown .ui-button.ui-button { + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; +} +.prototype-drilldown .analytics-drilldown .ui-button.ui-button.ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-drilldown .analytics-drilldown .ui-button.ui-button { + padding-inline: 16px; + } + .prototype-drilldown .analytics-drilldown .ui-button.ui-button::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-drilldown .analytics-drilldown .ui-button.ui-button.ui-button--icon::after { + inset: -6px; + } +} +.prototype-drilldown .analytics-drilldown app-button > button.button.button { + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; + min-width: 0; + max-height: none; +} +.prototype-drilldown .analytics-drilldown app-button > button.button.button.ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-drilldown .analytics-drilldown app-button > button.button.button { + padding-inline: 16px; + } + .prototype-drilldown .analytics-drilldown app-button > button.button.button::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-drilldown + .analytics-drilldown + app-button + > button.button.button.ui-button--icon::after { + inset: -6px; + } +} +.prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--attention { + box-sizing: border-box; + width: min(1100px, 100vw - 24px); + max-height: calc(100dvh - 32px); + padding: 20px; + overflow-y: auto; +} +.prototype-drilldown .attention-selection { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + align-items: center; + margin: 16px 0; +} +.prototype-drilldown .attention-sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} +.prototype-drilldown .analytics-drilldown__table th.attention-check-cell, +.prototype-drilldown .analytics-drilldown__table td.attention-check-cell { + width: 44px; + padding: 0; +} +.prototype-drilldown .analytics-drilldown__table th.attention-check-cell::before, +.prototype-drilldown .analytics-drilldown__table td.attention-check-cell::before { + display: none; +} +.prototype-drilldown .attention-check { + display: grid; + place-items: center; + width: 44px; + height: 44px; + cursor: pointer; +} +.prototype-drilldown .attention-check input { + width: 18px; + height: 18px; + margin: 0; + cursor: inherit; + accent-color: var(--accent); +} +.prototype-drilldown .attention-check input:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} +.prototype-drilldown .attention-check__label { + display: none; +} +@media (max-width: 999px) { + .prototype-drilldown .analytics-drilldown__table:has(th.attention-check-cell) thead { + display: block; + } + .prototype-drilldown .analytics-drilldown__table:has(th.attention-check-cell) thead tr { + padding: 0; + border: 0; + } + .prototype-drilldown + .analytics-drilldown__table:has(th.attention-check-cell) + thead + th:not(.attention-check-cell) { + display: none; + } + .prototype-drilldown + .analytics-drilldown__table:has(th.attention-check-cell) + th.attention-check-cell { + width: auto; + } + .prototype-drilldown + .analytics-drilldown__table:has(th.attention-check-cell) + thead + .attention-check { + display: flex; + gap: 12px; + width: auto; + font-size: 12px; + } + .prototype-drilldown + .analytics-drilldown__table:has(th.attention-check-cell) + .attention-check__label { + display: inline; + } +} +.prototype-drilldown .analytics-drilldown__table tr.attention-selected-row { + background: color-mix(in srgb, var(--accent-light) 35%, var(--light-white)); +} +.prototype-drilldown .attention-reminder { + margin: 20px 0; +} +.prototype-drilldown .attention-reminder h3 { + margin-bottom: 16px; + font-size: 18px; + font-weight: 600; +} +.prototype-drilldown .attention-reminder h4 { + margin: 24px 0 8px; + font-size: 13px; + font-weight: 600; +} +.prototype-drilldown .attention-reminder__summary { + display: flex; + gap: 24px; + margin: 0 0 12px; +} +.prototype-drilldown .attention-reminder__summary dt { + font-size: 12px; + color: var(--grey-for-text); +} +.prototype-drilldown .attention-reminder__summary dd { + margin: 4px 0 0; + font-size: 24px; + font-weight: 600; +} +.prototype-drilldown .attention-reminder__channels { + color: var(--green-dark); +} +.prototype-drilldown .attention-reminder__recipients { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 0; + margin: 16px 0; + list-style: none; +} +.prototype-drilldown .attention-reminder__recipients li { + padding: 5px 8px; + font-size: 12px; + background: var(--accent-light); + border-radius: 6px; +} +.prototype-drilldown .attention-reminder__recipient-details { + margin: 16px 0; +} +.prototype-drilldown .attention-reminder__recipient-details summary { + min-height: 32px; + padding-block: 6px; + font-size: 12px; + color: var(--accent); + cursor: pointer; +} +.prototype-drilldown .attention-reminder__message { + padding: 16px; + margin: 0 0 16px; + font-size: 14px; + line-height: 1.6; + white-space: pre-line; + background: var(--light-gray); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 10px; +} +.prototype-drilldown .attention-reminder__recipient-count { + margin: 0 0 20px; + font-size: 14px; +} +.prototype-drilldown .attention-reminder__email { + display: grid; + gap: 16px; + padding: 20px; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 10px; +} +.prototype-drilldown .attention-reminder__email p { + margin: 0; + font-size: 14px; + line-height: 1.6; + white-space: pre-line; +} +.prototype-drilldown .attention-reminder__email small { + color: var(--grey-for-text); +} +.prototype-drilldown .attention-reminder__email-button { + justify-self: start; + padding: 12px 18px; + color: var(--light-white); + background: var(--accent); + border-radius: 8px; +} +.prototype-drilldown .attention-list-tools { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} +.prototype-drilldown .attention-list-tools .analytics-drilldown__count { + margin: 0; +} +.prototype-drilldown .analytics-drilldown .ui-button.ui-button.attention-list-tools__export { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + min-height: 32px; + padding: 0; + color: var(--accent); + background: transparent; + border: 0; +} +@media (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table--assignments th:first-child { + width: 24%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(2) { + width: 27%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(3) { + width: 18%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(4) { + width: 19%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:last-child { + width: 12%; + } +} +.prototype-drilldown .attention-actions { + position: sticky; + bottom: -20px; + z-index: 2; + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + padding: 16px 0 20px; + margin-top: 16px; + background: var(--light-white); + border-top: 1px solid var(--medium-grey-for-outline); +} +.prototype-drilldown .attention-actions__buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} +.prototype-drilldown .attention-actions__count { + font-size: 12px; + font-weight: 600; +} +.prototype-drilldown .attention-actions__error, +.prototype-drilldown .attention-actions__success { + flex-basis: 100%; + margin: 0; + font-size: 12px; +} +.prototype-drilldown .attention-actions__error { + color: var(--red-dark); +} +.prototype-drilldown .attention-actions__success { + color: var(--green-dark); +} +.prototype-drilldown .attention-actions .ui-button.ui-button.attention-actions__send { + min-height: 40px; + padding: 9px 16px; + color: var(--light-white); + background: var(--accent); + border: 1px solid var(--accent); + border-radius: 8px; +} +.prototype-drilldown .attention-actions .ui-button.ui-button.attention-actions__send:disabled { + color: var(--grey-for-text); + background: var(--light-gray); + border-color: var(--medium-grey-for-outline); +} +@media (max-width: 599px) { + .prototype-drilldown .attention-actions__buttons { + flex-basis: 100%; + width: 100%; + } + .prototype-drilldown .attention-actions__buttons .ui-button.ui-button { + flex: 1 1 100%; + min-height: 44px; + } +} diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/global-reference.css b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/global-reference.css new file mode 100644 index 000000000..52ee381d9 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/global-reference.css @@ -0,0 +1,4685 @@ +/** + * projects/social_platform/src/styles.scss + * + * @format + */ + +:root { + --gradient: linear-gradient(90deg, #242424 0%, #8a63e6 50%); + --gradient-mild: linear-gradient(90deg, #501d53 0.3%, #3b1f72 22%, #8a63e6 100%); + --accent: #8a63e6; + --accent-dark: rgb(122.8167938931, 99, 179); + --accent-mild: rgba(108, 39, 255, 0.6); + --accent-light: #e8e0fa; + --accent-medium: #9764ba; + --gold: #e5b25d; + --gold-light: #fdf8f0; + --gold-dark: #c69849; + --light-white: #fff; + --white: #fafafa; + --black: #333; + --dark-grey: #a59fb9; + --gray: #d3d3d3; + --light-gray: #f9f9f9; + --grey-button: #e5e5e5e5; + --medium-grey-for-outline: #eee; + --grey-for-text: #827e80; + --green: #88c9a1; + --green-light: #e3f0e8; + --green-dark: #297373; + --lime: #d6ff54; + --red: #d48a9e; + --red-light: #f4e2e7; + --red-dark: rgb(186.5, 138, 151.1081081081); + --blue-dark: #2f36aa; + --purple: #9a80e6; + --cyan: #4cd9f1; +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 900; + src: + local(""), + url("./media/Mont-Black-CKKAF2O3.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Black-ZXUYXEJ6.woff2") format("woff2"), + url("./media/Mont-Black-UEG4C46Y.woff") format("woff"), + url("./media/Mont-Black-O4OJXHSC.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 800; + src: url("./media/Mont-Heavy-CZVFDIWI.eot"); + src: + local(""), + url("./media/Mont-Heavy-CZVFDIWI.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Heavy-SAJ5B6FS.woff2") format("woff2"), + url("./media/Mont-Heavy-WMPUKQNL.woff") format("woff"), + url("./media/Mont-Heavy-JVDMIVZJ.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: bold; + src: url("./media/Mont-Bold-JCFB53E2.eot"); + src: + local(""), + url("./media/Mont-Bold-JCFB53E2.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Bold-7L6HWBWW.woff2") format("woff2"), + url("./media/Mont-Bold-A3OSQYTY.woff") format("woff"), + url("./media/Mont-Bold-VNDQNQDW.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 600; + src: url("./media/Mont-SemiBold-BZSF3DKB.eot"); + src: + local(""), + url("./media/Mont-SemiBold-BZSF3DKB.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-SemiBold-7XOUXTAI.woff2") format("woff2"), + url("./media/Mont-SemiBold-WNUGA77E.woff") format("woff"), + url("./media/Mont-SemiBold-M6WLAC2K.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: normal; + src: url("./media/Mont-Regular-RSRKQCMK.eot"); + src: + local(""), + url("./media/Mont-Regular-RSRKQCMK.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Regular-UA5RCVYG.woff2") format("woff2"), + url("./media/Mont-Regular-T6L2NNOO.woff") format("woff"), + url("./media/Mont-Regular-5LPDNI7X.ttf") format("truetype"); +} +.text-heading-1 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-1-bold { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-2 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-2-bold { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-3 { + font-family: Mont, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-heading-4 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-body-bold-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-body-12 { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.text-body-12-bold { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 130%; +} +@media screen and (max-width: 999px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--ui-text-muted); + } +} +@media screen and (min-width: 1000px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--grey-button); + } +} +@media screen and (max-width: 999px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +@media screen and (min-width: 1000px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: 6px; + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} +ul, +li, +ol { + list-style-type: none; +} +fieldset { + border: none; + outline: none; +} +img { + display: block; + max-width: 100%; +} +a, +.link { + color: var(--accent); + text-decoration: none; + transition: color 0.2s; +} +a:hover, +.link:hover { + color: var(--accent-hover); +} +.edit-link { + position: relative; +} +.edit-link__remove { + position: absolute; + top: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + width: 50px; + color: var(--white); + cursor: pointer; + background-color: var(--gray); + border-radius: 0 8px 8px 0; +} +button { + background-color: transparent; + border: none; + outline: none; +} +label.field-label { + display: block; + margin-bottom: 6px; + margin-left: 12px; + color: var(--black); + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.error { + display: block; + gap: 10px; + margin-top: 6px; + color: var(--red) !important; +} +.error i { + color: var(--red) !important; +} +.error p:not(:last-child) { + margin-bottom: 10px; +} +:root { + --rounded-sm: 3px; + --rounded-md: 5px; + --rounded-lg: 8px; + --rounded-xl: 15px; + --rounded-xxl: 45px; +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 900; + src: + local(""), + url("./media/Mont-Black-CKKAF2O3.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Black-ZXUYXEJ6.woff2") format("woff2"), + url("./media/Mont-Black-UEG4C46Y.woff") format("woff"), + url("./media/Mont-Black-O4OJXHSC.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 800; + src: url("./media/Mont-Heavy-CZVFDIWI.eot"); + src: + local(""), + url("./media/Mont-Heavy-CZVFDIWI.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Heavy-SAJ5B6FS.woff2") format("woff2"), + url("./media/Mont-Heavy-WMPUKQNL.woff") format("woff"), + url("./media/Mont-Heavy-JVDMIVZJ.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: bold; + src: url("./media/Mont-Bold-JCFB53E2.eot"); + src: + local(""), + url("./media/Mont-Bold-JCFB53E2.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Bold-7L6HWBWW.woff2") format("woff2"), + url("./media/Mont-Bold-A3OSQYTY.woff") format("woff"), + url("./media/Mont-Bold-VNDQNQDW.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 600; + src: url("./media/Mont-SemiBold-BZSF3DKB.eot"); + src: + local(""), + url("./media/Mont-SemiBold-BZSF3DKB.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-SemiBold-7XOUXTAI.woff2") format("woff2"), + url("./media/Mont-SemiBold-WNUGA77E.woff") format("woff"), + url("./media/Mont-SemiBold-M6WLAC2K.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: normal; + src: url("./media/Mont-Regular-RSRKQCMK.eot"); + src: + local(""), + url("./media/Mont-Regular-RSRKQCMK.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Regular-UA5RCVYG.woff2") format("woff2"), + url("./media/Mont-Regular-T6L2NNOO.woff") format("woff"), + url("./media/Mont-Regular-5LPDNI7X.ttf") format("truetype"); +} +.text-heading-1 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-1-bold { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-2 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-2-bold { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-3 { + font-family: Mont, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-heading-4 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-body-bold-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-body-12 { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.text-body-12-bold { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 130%; +} +@media screen and (max-width: 999px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--ui-text-muted); + } +} +@media screen and (min-width: 1000px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--grey-button); + } +} +@media screen and (max-width: 999px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +@media screen and (min-width: 1000px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: 6px; + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +.tag-list { + display: flex; + flex-wrap: wrap; +} +.tag-list__item { + display: block; + margin-bottom: 12px; +} +.tag-list__item:not(:last-child) { + margin-right: 10px; +} +.tag-list__remove-icon { + color: var(--red); + cursor: pointer; +} +.tag-list-form { + display: flex; +} +.tag-list-form__input { + flex-grow: 1; + margin-right: 6px; +} +.tag-list-form__button { + width: 52px; +} +.nav--vertical .nav__list { + flex-direction: column; + align-items: flex-start; +} +.nav--vertical .nav__link:not(:last-child) { + margin-right: 0; + margin-bottom: 30px; +} +.nav__list { + display: flex; + align-items: center; +} +.nav__link { + color: var(--dark-grey); + transition: color 0.2s; +} +.nav__link:not(:last-child) { + margin-right: 38px; +} +.nav__link:hover { + color: var(--black); +} +.nav__link--active { + color: var(--black); +} +.nav__item { + display: flex; + align-items: center; + color: inherit; + cursor: pointer; +} +.nav__item i { + margin-right: 12px; +} +.contact-link { + color: var(--accent); + cursor: pointer; + transition: color 0.2s; +} +.contact-link:hover { + color: var(--accent-dark); +} +.contact-link__link { + display: flex; + align-items: center; + cursor: pointer; +} +.contact-link__icon { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + margin-right: 6px; + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-xxl); +} +.image-preview-dialog { + position: fixed; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100vw; + max-width: 100vw; + height: 100vh; + max-height: 100vh; + padding: 40px; + margin: 0; + background: transparent; + border: none; +} +.image-preview-dialog::backdrop { + background: var(--ui-preview-scrim); + animation: fade-in 0.25s ease-out; +} +.image-preview-dialog__img { + max-width: 90vw; + max-height: 85vh; + object-fit: contain; + border-radius: var(--rounded-lg); + animation: zoom-in 0.25s ease-out; +} +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes zoom-in { + from { + opacity: 0; + transform: scale(0.85); + } + to { + opacity: 1; + transform: scale(1); + } +} +.members { + padding-bottom: 100px; +} +.members__bar { + padding: 20px; + margin-bottom: 12px; + background-color: var(--white); + border-radius: var(--rounded-xl); +} +.members__list { + display: grid; + grid-template-columns: 1fr; + grid-gap: 16px; + align-items: start; +} +@media screen and (min-width: 1000px) { + .members__list { + grid-template-columns: repeat(2, 1fr); + } +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 900; + src: + local(""), + url("./media/Mont-Black-CKKAF2O3.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Black-ZXUYXEJ6.woff2") format("woff2"), + url("./media/Mont-Black-UEG4C46Y.woff") format("woff"), + url("./media/Mont-Black-O4OJXHSC.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 800; + src: url("./media/Mont-Heavy-CZVFDIWI.eot"); + src: + local(""), + url("./media/Mont-Heavy-CZVFDIWI.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Heavy-SAJ5B6FS.woff2") format("woff2"), + url("./media/Mont-Heavy-WMPUKQNL.woff") format("woff"), + url("./media/Mont-Heavy-JVDMIVZJ.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: bold; + src: url("./media/Mont-Bold-JCFB53E2.eot"); + src: + local(""), + url("./media/Mont-Bold-JCFB53E2.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Bold-7L6HWBWW.woff2") format("woff2"), + url("./media/Mont-Bold-A3OSQYTY.woff") format("woff"), + url("./media/Mont-Bold-VNDQNQDW.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 600; + src: url("./media/Mont-SemiBold-BZSF3DKB.eot"); + src: + local(""), + url("./media/Mont-SemiBold-BZSF3DKB.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-SemiBold-7XOUXTAI.woff2") format("woff2"), + url("./media/Mont-SemiBold-WNUGA77E.woff") format("woff"), + url("./media/Mont-SemiBold-M6WLAC2K.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: normal; + src: url("./media/Mont-Regular-RSRKQCMK.eot"); + src: + local(""), + url("./media/Mont-Regular-RSRKQCMK.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Regular-UA5RCVYG.woff2") format("woff2"), + url("./media/Mont-Regular-T6L2NNOO.woff") format("woff"), + url("./media/Mont-Regular-5LPDNI7X.ttf") format("truetype"); +} +.text-heading-1 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-1-bold { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-2 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-2-bold { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-3 { + font-family: Mont, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-heading-4 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-body-bold-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-body-12 { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.text-body-12-bold { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 130%; +} +@media screen and (max-width: 999px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--ui-text-muted); + } +} +@media screen and (min-width: 1000px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--grey-button); + } +} +@media screen and (max-width: 999px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +@media screen and (min-width: 1000px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: 6px; + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +.auth { + padding: 100px 0; +} +.auth__logo { + position: fixed; + top: 45px; + left: 18px; + display: block; + width: 95px; +} +@media screen and (min-width: 1000px) { + .auth__logo { + top: 120px; + left: 200px; + width: 157px; + height: 30px; + } +} +.auth__greeting { + max-width: 510px; + margin-bottom: 20px; +} +@media screen and (min-width: 1000px) { + .auth__greeting { + margin-bottom: 25px; + } +} +@media screen and (min-width: 1000px) { + .auth__wrapper { + max-width: 333px; + } +} +.auth__title { + margin-bottom: 6px; + color: var(--black); + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.auth__title--register { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +@media screen and (min-width: 1000px) { + .auth__title--register { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; + } +} +.auth__info { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.auth__field { + margin-bottom: 12px; +} +.auth__field app-input, +.auth__field app-select { + display: block; +} +.auth__field app-input.auth__password-input { + --auth-password-eye-inset: 8px; + --auth-password-eye-size: 25px; + --auth-password-error-size: 20px; + --auth-password-error-gap: 4px; +} +.auth__field app-input.auth__password-input .field { + display: flex; + align-items: center; + background: var(--white); + border: 0.5px solid var(--gray); + border-radius: var(--rounded-xxl); +} +.auth__field app-input.auth__password-input .field:focus-within { + border-color: var(--accent); + box-shadow: 0 0 6px rgba(109, 40, 255, 0.3); +} +.auth__field app-input.auth__password-input .field--error { + border-color: var(--red); +} +.auth__field app-input.auth__password-input .field--error:focus-within { + box-shadow: 0 0 6px rgba(255, 40, 40, 0.308); +} +.auth__field app-input.auth__password-input .field__input { + box-sizing: border-box; + flex: 1 1 0; + width: 0; + min-width: 0; + padding-right: 4px; + border: 0; + border-radius: var(--rounded-xxl) 0 0 var(--rounded-xxl); + box-shadow: none; +} +.auth__field app-input.auth__password-input .field__right-icon { + position: relative; + top: auto; + right: auto !important; + flex: 0 0 var(--auth-password-eye-size); + width: var(--auth-password-eye-size); + height: var(--auth-password-eye-size); + padding: 0; + margin-right: var(--auth-password-eye-inset); + pointer-events: none; + background-color: transparent; + transform: none; +} +.auth__field app-input.auth__password-input .field__right-icon button { + pointer-events: auto; + cursor: pointer; +} +.auth__field app-input.auth__password-input .field__right-icon::before { + position: absolute; + top: 4px; + bottom: 4px; + left: 0; + width: 1px; + content: ""; + background: var(--medium-grey-for-outline); +} +.auth__field app-input.auth__password-input .field__error-icon { + position: static; + flex: 0 0 var(--auth-password-error-size); + width: var(--auth-password-error-size); + height: var(--auth-password-error-size); + margin: 0 var(--auth-password-error-gap); + transform: none; +} +.auth__password-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + padding: 0; + color: var(--dark-grey); + background: transparent; + border: 0; + border-radius: 50%; +} +.auth__password-toggle:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} +.auth__password-toggle--muted { + color: var(--dark-grey); +} +.auth__row { + display: flex; +} +.auth__row fieldset { + width: 50%; +} +.auth__row fieldset:not(:last-child) { + margin-right: 15px; +} +.auth__agreement { + display: flex; + align-items: center; + margin-bottom: 30px; + cursor: pointer; +} +.auth__checkbox { + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; +} +.auth__agreement-text { + color: var(--gray-100); +} +.auth__button { + display: block; + width: 100%; + margin-top: 35px; +} +.auth__button-typography { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +@media screen and (min-width: 1000px) { + .auth__button-typography { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; + } +} +@media screen and (min-width: 1000px) { + .auth__button { + margin-top: 30px; + } +} +.auth__button-icon { + display: none !important; + margin-left: 10px; + transform: rotate(90deg); +} +@media screen and (min-width: 1000px) { + .auth__button-icon { + display: block !important; + } +} +.auth__toggle { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; + margin-top: 4vh; +} +.auth a { + color: var(--accent); + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 3px; + transition: text-decoration-color 0.2s; +} +.auth a:hover { + text-decoration-color: var(--accent); +} +@media screen and (max-width: 749px) { + .auth { + padding: 24px 0; + } + .auth__logo { + position: static; + margin-bottom: 24px; + } + .auth__row { + flex-direction: column; + gap: 12px; + } + .auth__row fieldset { + width: 100%; + margin-right: 0; + } + .auth__field app-input.auth__password-input { + --auth-password-eye-size: 44px; + } + .auth__password-toggle { + width: 44px; + height: 44px; + } + .auth__agreement, + .auth__checkbox { + min-height: 44px; + } +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 900; + src: + local(""), + url("./media/Mont-Black-CKKAF2O3.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Black-ZXUYXEJ6.woff2") format("woff2"), + url("./media/Mont-Black-UEG4C46Y.woff") format("woff"), + url("./media/Mont-Black-O4OJXHSC.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 800; + src: url("./media/Mont-Heavy-CZVFDIWI.eot"); + src: + local(""), + url("./media/Mont-Heavy-CZVFDIWI.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Heavy-SAJ5B6FS.woff2") format("woff2"), + url("./media/Mont-Heavy-WMPUKQNL.woff") format("woff"), + url("./media/Mont-Heavy-JVDMIVZJ.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: bold; + src: url("./media/Mont-Bold-JCFB53E2.eot"); + src: + local(""), + url("./media/Mont-Bold-JCFB53E2.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Bold-7L6HWBWW.woff2") format("woff2"), + url("./media/Mont-Bold-A3OSQYTY.woff") format("woff"), + url("./media/Mont-Bold-VNDQNQDW.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 600; + src: url("./media/Mont-SemiBold-BZSF3DKB.eot"); + src: + local(""), + url("./media/Mont-SemiBold-BZSF3DKB.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-SemiBold-7XOUXTAI.woff2") format("woff2"), + url("./media/Mont-SemiBold-WNUGA77E.woff") format("woff"), + url("./media/Mont-SemiBold-M6WLAC2K.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: normal; + src: url("./media/Mont-Regular-RSRKQCMK.eot"); + src: + local(""), + url("./media/Mont-Regular-RSRKQCMK.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Regular-UA5RCVYG.woff2") format("woff2"), + url("./media/Mont-Regular-T6L2NNOO.woff") format("woff"), + url("./media/Mont-Regular-5LPDNI7X.ttf") format("truetype"); +} +.text-heading-1 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-1-bold { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-2 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-2-bold { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-3 { + font-family: Mont, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-heading-4 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-body-bold-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-body-12 { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.text-body-12-bold { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 130%; +} +@media screen and (max-width: 999px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--ui-text-muted); + } +} +@media screen and (min-width: 1000px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--grey-button); + } +} +@media screen and (max-width: 999px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +@media screen and (min-width: 1000px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: 6px; + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +.unsubscribe-modal { + display: flex; + flex-direction: column; + align-items: center; + margin: 4px 20px; +} +.unsubscribe-modal h3 { + margin-bottom: 40px; + text-align: center; +} +.unsubscribe-modal app-avatar { + margin-bottom: 30px; +} +@media screen and (min-width: 1000px) { + .unsubscribe-modal app-button ::ng-deep .button--inline { + min-height: 52px; + } +} +.unsubscribe-modal__buttons { + display: flex; + flex-direction: column; + gap: 15px; + justify-content: space-between; + width: 100%; +} +@media screen and (min-width: 1000px) { + .unsubscribe-modal__buttons { + flex-direction: row; + } +} +.unsubscribe-modal-btn__typography { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +@media screen and (min-width: 1000px) { + .unsubscribe-modal-btn__typography { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; + } +} +:root { + --app-height: 100vh; +} +@media screen and (max-width: 999px) { + :root { + --page-gutter: 16px; + } + @supports (height: 100dvh) { + :root { + --app-height: 100dvh; + } + } +} +html { + scrollbar-gutter: stable; +} +html, +body { + min-width: 280px; + height: var(--app-height, 100%); +} +body { + margin: 0; + overflow-x: hidden; + font-family: Mont, sans-serif; +} +button, +input, +textarea, +select { + font-family: inherit; +} +.mat-elevation-z0, +.mat-mdc-elevation-specific.mat-elevation-z0 { + box-shadow: var( + --mat-app-elevation-shadow-level-0, + 0px 0px 0px 0px --mat-sys-shadow, + 0px 0px 0px 0px --mat-sys-shadow, + 0px 0px 0px 0px --mat-sys-shadow + ); +} +.mat-elevation-z1, +.mat-mdc-elevation-specific.mat-elevation-z1 { + box-shadow: var( + --mat-app-elevation-shadow-level-1, + 0px 2px 1px -1px --mat-sys-shadow, + 0px 1px 1px 0px --mat-sys-shadow, + 0px 1px 3px 0px --mat-sys-shadow + ); +} +.mat-elevation-z2, +.mat-mdc-elevation-specific.mat-elevation-z2 { + box-shadow: var( + --mat-app-elevation-shadow-level-2, + 0px 3px 1px -2px --mat-sys-shadow, + 0px 2px 2px 0px --mat-sys-shadow, + 0px 1px 5px 0px --mat-sys-shadow + ); +} +.mat-elevation-z3, +.mat-mdc-elevation-specific.mat-elevation-z3 { + box-shadow: var( + --mat-app-elevation-shadow-level-3, + 0px 3px 3px -2px --mat-sys-shadow, + 0px 3px 4px 0px --mat-sys-shadow, + 0px 1px 8px 0px --mat-sys-shadow + ); +} +.mat-elevation-z4, +.mat-mdc-elevation-specific.mat-elevation-z4 { + box-shadow: var( + --mat-app-elevation-shadow-level-4, + 0px 2px 4px -1px --mat-sys-shadow, + 0px 4px 5px 0px --mat-sys-shadow, + 0px 1px 10px 0px --mat-sys-shadow + ); +} +.mat-elevation-z5, +.mat-mdc-elevation-specific.mat-elevation-z5 { + box-shadow: var( + --mat-app-elevation-shadow-level-5, + 0px 3px 5px -1px --mat-sys-shadow, + 0px 5px 8px 0px --mat-sys-shadow, + 0px 1px 14px 0px --mat-sys-shadow + ); +} +.mat-elevation-z6, +.mat-mdc-elevation-specific.mat-elevation-z6 { + box-shadow: var( + --mat-app-elevation-shadow-level-6, + 0px 3px 5px -1px --mat-sys-shadow, + 0px 6px 10px 0px --mat-sys-shadow, + 0px 1px 18px 0px --mat-sys-shadow + ); +} +.mat-elevation-z7, +.mat-mdc-elevation-specific.mat-elevation-z7 { + box-shadow: var( + --mat-app-elevation-shadow-level-7, + 0px 4px 5px -2px --mat-sys-shadow, + 0px 7px 10px 1px --mat-sys-shadow, + 0px 2px 16px 1px --mat-sys-shadow + ); +} +.mat-elevation-z8, +.mat-mdc-elevation-specific.mat-elevation-z8 { + box-shadow: var( + --mat-app-elevation-shadow-level-8, + 0px 5px 5px -3px --mat-sys-shadow, + 0px 8px 10px 1px --mat-sys-shadow, + 0px 3px 14px 2px --mat-sys-shadow + ); +} +.mat-elevation-z9, +.mat-mdc-elevation-specific.mat-elevation-z9 { + box-shadow: var( + --mat-app-elevation-shadow-level-9, + 0px 5px 6px -3px --mat-sys-shadow, + 0px 9px 12px 1px --mat-sys-shadow, + 0px 3px 16px 2px --mat-sys-shadow + ); +} +.mat-elevation-z10, +.mat-mdc-elevation-specific.mat-elevation-z10 { + box-shadow: var( + --mat-app-elevation-shadow-level-10, + 0px 6px 6px -3px --mat-sys-shadow, + 0px 10px 14px 1px --mat-sys-shadow, + 0px 4px 18px 3px --mat-sys-shadow + ); +} +.mat-elevation-z11, +.mat-mdc-elevation-specific.mat-elevation-z11 { + box-shadow: var( + --mat-app-elevation-shadow-level-11, + 0px 6px 7px -4px --mat-sys-shadow, + 0px 11px 15px 1px --mat-sys-shadow, + 0px 4px 20px 3px --mat-sys-shadow + ); +} +.mat-elevation-z12, +.mat-mdc-elevation-specific.mat-elevation-z12 { + box-shadow: var( + --mat-app-elevation-shadow-level-12, + 0px 7px 8px -4px --mat-sys-shadow, + 0px 12px 17px 2px --mat-sys-shadow, + 0px 5px 22px 4px --mat-sys-shadow + ); +} +.mat-elevation-z13, +.mat-mdc-elevation-specific.mat-elevation-z13 { + box-shadow: var( + --mat-app-elevation-shadow-level-13, + 0px 7px 8px -4px --mat-sys-shadow, + 0px 13px 19px 2px --mat-sys-shadow, + 0px 5px 24px 4px --mat-sys-shadow + ); +} +.mat-elevation-z14, +.mat-mdc-elevation-specific.mat-elevation-z14 { + box-shadow: var( + --mat-app-elevation-shadow-level-14, + 0px 7px 9px -4px --mat-sys-shadow, + 0px 14px 21px 2px --mat-sys-shadow, + 0px 5px 26px 4px --mat-sys-shadow + ); +} +.mat-elevation-z15, +.mat-mdc-elevation-specific.mat-elevation-z15 { + box-shadow: var( + --mat-app-elevation-shadow-level-15, + 0px 8px 9px -5px --mat-sys-shadow, + 0px 15px 22px 2px --mat-sys-shadow, + 0px 6px 28px 5px --mat-sys-shadow + ); +} +.mat-elevation-z16, +.mat-mdc-elevation-specific.mat-elevation-z16 { + box-shadow: var( + --mat-app-elevation-shadow-level-16, + 0px 8px 10px -5px --mat-sys-shadow, + 0px 16px 24px 2px --mat-sys-shadow, + 0px 6px 30px 5px --mat-sys-shadow + ); +} +.mat-elevation-z17, +.mat-mdc-elevation-specific.mat-elevation-z17 { + box-shadow: var( + --mat-app-elevation-shadow-level-17, + 0px 8px 11px -5px --mat-sys-shadow, + 0px 17px 26px 2px --mat-sys-shadow, + 0px 6px 32px 5px --mat-sys-shadow + ); +} +.mat-elevation-z18, +.mat-mdc-elevation-specific.mat-elevation-z18 { + box-shadow: var( + --mat-app-elevation-shadow-level-18, + 0px 9px 11px -5px --mat-sys-shadow, + 0px 18px 28px 2px --mat-sys-shadow, + 0px 7px 34px 6px --mat-sys-shadow + ); +} +.mat-elevation-z19, +.mat-mdc-elevation-specific.mat-elevation-z19 { + box-shadow: var( + --mat-app-elevation-shadow-level-19, + 0px 9px 12px -6px --mat-sys-shadow, + 0px 19px 29px 2px --mat-sys-shadow, + 0px 7px 36px 6px --mat-sys-shadow + ); +} +.mat-elevation-z20, +.mat-mdc-elevation-specific.mat-elevation-z20 { + box-shadow: var( + --mat-app-elevation-shadow-level-20, + 0px 10px 13px -6px --mat-sys-shadow, + 0px 20px 31px 3px --mat-sys-shadow, + 0px 8px 38px 7px --mat-sys-shadow + ); +} +.mat-elevation-z21, +.mat-mdc-elevation-specific.mat-elevation-z21 { + box-shadow: var( + --mat-app-elevation-shadow-level-21, + 0px 10px 13px -6px --mat-sys-shadow, + 0px 21px 33px 3px --mat-sys-shadow, + 0px 8px 40px 7px --mat-sys-shadow + ); +} +.mat-elevation-z22, +.mat-mdc-elevation-specific.mat-elevation-z22 { + box-shadow: var( + --mat-app-elevation-shadow-level-22, + 0px 10px 14px -6px --mat-sys-shadow, + 0px 22px 35px 3px --mat-sys-shadow, + 0px 8px 42px 7px --mat-sys-shadow + ); +} +.mat-elevation-z23, +.mat-mdc-elevation-specific.mat-elevation-z23 { + box-shadow: var( + --mat-app-elevation-shadow-level-23, + 0px 11px 14px -7px --mat-sys-shadow, + 0px 23px 36px 3px --mat-sys-shadow, + 0px 9px 44px 8px --mat-sys-shadow + ); +} +.mat-elevation-z24, +.mat-mdc-elevation-specific.mat-elevation-z24 { + box-shadow: var( + --mat-app-elevation-shadow-level-24, + 0px 11px 15px -7px --mat-sys-shadow, + 0px 24px 38px 3px --mat-sys-shadow, + 0px 9px 46px 8px --mat-sys-shadow + ); +} +html { + --mat-sys-on-surface: initial; +} +.mat-app-background { + background-color: var(--mat-app-background-color, var(--mat-sys-background, transparent)); + color: var(--mat-app-text-color, var(--mat-sys-on-background, inherit)); +} +.mat-progress-bar { + z-index: 200; +} +html { + --mat-app-background-color: #fafafa; + --mat-app-text-color: rgba(0, 0, 0, 0.87); + --mat-app-elevation-shadow-level-0: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-1: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-2: + 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-3: + 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), + 0px 1px 8px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-4: + 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), + 0px 1px 10px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-5: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14), + 0px 1px 14px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-6: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), + 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-7: + 0px 4px 5px -2px rgba(0, 0, 0, 0.2), 0px 7px 10px 1px rgba(0, 0, 0, 0.14), + 0px 2px 16px 1px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-8: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-9: + 0px 5px 6px -3px rgba(0, 0, 0, 0.2), 0px 9px 12px 1px rgba(0, 0, 0, 0.14), + 0px 3px 16px 2px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-10: + 0px 6px 6px -3px rgba(0, 0, 0, 0.2), 0px 10px 14px 1px rgba(0, 0, 0, 0.14), + 0px 4px 18px 3px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-11: + 0px 6px 7px -4px rgba(0, 0, 0, 0.2), 0px 11px 15px 1px rgba(0, 0, 0, 0.14), + 0px 4px 20px 3px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-12: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), + 0px 5px 22px 4px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-13: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 13px 19px 2px rgba(0, 0, 0, 0.14), + 0px 5px 24px 4px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-14: + 0px 7px 9px -4px rgba(0, 0, 0, 0.2), 0px 14px 21px 2px rgba(0, 0, 0, 0.14), + 0px 5px 26px 4px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-15: + 0px 8px 9px -5px rgba(0, 0, 0, 0.2), 0px 15px 22px 2px rgba(0, 0, 0, 0.14), + 0px 6px 28px 5px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-16: + 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), + 0px 6px 30px 5px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-17: + 0px 8px 11px -5px rgba(0, 0, 0, 0.2), 0px 17px 26px 2px rgba(0, 0, 0, 0.14), + 0px 6px 32px 5px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-18: + 0px 9px 11px -5px rgba(0, 0, 0, 0.2), 0px 18px 28px 2px rgba(0, 0, 0, 0.14), + 0px 7px 34px 6px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-19: + 0px 9px 12px -6px rgba(0, 0, 0, 0.2), 0px 19px 29px 2px rgba(0, 0, 0, 0.14), + 0px 7px 36px 6px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-20: + 0px 10px 13px -6px rgba(0, 0, 0, 0.2), 0px 20px 31px 3px rgba(0, 0, 0, 0.14), + 0px 8px 38px 7px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-21: + 0px 10px 13px -6px rgba(0, 0, 0, 0.2), 0px 21px 33px 3px rgba(0, 0, 0, 0.14), + 0px 8px 40px 7px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-22: + 0px 10px 14px -6px rgba(0, 0, 0, 0.2), 0px 22px 35px 3px rgba(0, 0, 0, 0.14), + 0px 8px 42px 7px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-23: + 0px 11px 14px -7px rgba(0, 0, 0, 0.2), 0px 23px 36px 3px rgba(0, 0, 0, 0.14), + 0px 9px 44px 8px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-24: + 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), + 0px 9px 46px 8px rgba(0, 0, 0, 0.12); +} +html { + --mat-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); +} +html { + --mat-option-selected-state-label-text-color: #3f51b5; + --mat-option-label-text-color: rgba(0, 0, 0, 0.87); + --mat-option-hover-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 4%, transparent); + --mat-option-focus-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-option-selected-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); +} +.mat-accent { + --mat-option-selected-state-label-text-color: #ff4081; + --mat-option-label-text-color: rgba(0, 0, 0, 0.87); + --mat-option-hover-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 4%, transparent); + --mat-option-focus-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-option-selected-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); +} +.mat-warn { + --mat-option-selected-state-label-text-color: #f44336; + --mat-option-label-text-color: rgba(0, 0, 0, 0.87); + --mat-option-hover-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 4%, transparent); + --mat-option-focus-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-option-selected-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); +} +html { + --mat-optgroup-label-text-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-pseudo-checkbox-full-selected-icon-color: #ff4081; + --mat-pseudo-checkbox-full-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-pseudo-checkbox-full-disabled-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-full-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: #ff4081; + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); +} +.mat-primary { + --mat-pseudo-checkbox-full-selected-icon-color: #3f51b5; + --mat-pseudo-checkbox-full-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-pseudo-checkbox-full-disabled-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-full-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: #3f51b5; + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); +} +.mat-accent { + --mat-pseudo-checkbox-full-selected-icon-color: #ff4081; + --mat-pseudo-checkbox-full-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-pseudo-checkbox-full-disabled-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-full-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: #ff4081; + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); +} +.mat-warn { + --mat-pseudo-checkbox-full-selected-icon-color: #f44336; + --mat-pseudo-checkbox-full-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-pseudo-checkbox-full-disabled-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-full-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: #f44336; + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); +} +html { + --mat-option-label-text-font: Mont, sans-serif; + --mat-option-label-text-line-height: 24px; + --mat-option-label-text-size: 16px; + --mat-option-label-text-tracking: 0.03125em; + --mat-option-label-text-weight: 400; +} +html { + --mat-optgroup-label-text-font: Mont, sans-serif; + --mat-optgroup-label-text-line-height: 24px; + --mat-optgroup-label-text-size: 16px; + --mat-optgroup-label-text-tracking: 0.03125em; + --mat-optgroup-label-text-weight: 400; +} +html { + --mat-card-elevated-container-shape: 4px; + --mat-card-outlined-container-shape: 4px; + --mat-card-filled-container-shape: 4px; + --mat-card-outlined-outline-width: 1px; +} +html { + --mat-card-elevated-container-color: white; + --mat-card-elevated-container-elevation: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + --mat-card-outlined-container-color: white; + --mat-card-outlined-container-elevation: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + --mat-card-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-card-subtitle-text-color: rgba(0, 0, 0, 0.54); + --mat-card-filled-container-color: white; + --mat-card-filled-container-elevation: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); +} +html { + --mat-card-title-text-font: Mont, sans-serif; + --mat-card-title-text-line-height: 32px; + --mat-card-title-text-size: 20px; + --mat-card-title-text-tracking: 0.0125em; + --mat-card-title-text-weight: 500; + --mat-card-subtitle-text-font: Mont, sans-serif; + --mat-card-subtitle-text-line-height: 22px; + --mat-card-subtitle-text-size: 14px; + --mat-card-subtitle-text-tracking: 0.0071428571em; + --mat-card-subtitle-text-weight: 500; +} +html { + --mat-progress-bar-active-indicator-height: 4px; + --mat-progress-bar-track-height: 4px; + --mat-progress-bar-track-shape: 0; +} +.mat-mdc-progress-bar { + --mat-progress-bar-active-indicator-color: #3f51b5; + --mat-progress-bar-track-color: rgba(63, 81, 181, 0.25); +} +.mat-mdc-progress-bar.mat-accent { + --mat-progress-bar-active-indicator-color: #ff4081; + --mat-progress-bar-track-color: rgba(255, 64, 129, 0.25); +} +.mat-mdc-progress-bar.mat-warn { + --mat-progress-bar-active-indicator-color: #f44336; + --mat-progress-bar-track-color: rgba(244, 67, 54, 0.25); +} +html { + --mat-tooltip-container-shape: 4px; + --mat-tooltip-supporting-text-line-height: 16px; +} +html { + --mat-tooltip-container-color: #424242; + --mat-tooltip-supporting-text-color: white; +} +html { + --mat-tooltip-supporting-text-font: Mont, sans-serif; + --mat-tooltip-supporting-text-size: 12px; + --mat-tooltip-supporting-text-weight: 400; + --mat-tooltip-supporting-text-tracking: 0.0333333333em; +} +html { + --mat-form-field-filled-active-indicator-height: 1px; + --mat-form-field-filled-focus-active-indicator-height: 2px; + --mat-form-field-filled-container-shape: 4px; + --mat-form-field-outlined-outline-width: 1px; + --mat-form-field-outlined-focus-outline-width: 2px; + --mat-form-field-outlined-container-shape: 4px; +} +html { + --mat-form-field-focus-select-arrow-color: color-mix(in srgb, #3f51b5 87%, transparent); + --mat-form-field-filled-caret-color: #3f51b5; + --mat-form-field-filled-focus-active-indicator-color: #3f51b5; + --mat-form-field-filled-focus-label-text-color: color-mix(in srgb, #3f51b5 87%, transparent); + --mat-form-field-outlined-caret-color: #3f51b5; + --mat-form-field-outlined-focus-outline-color: #3f51b5; + --mat-form-field-outlined-focus-label-text-color: color-mix(in srgb, #3f51b5 87%, transparent); + --mat-form-field-disabled-input-text-placeholder-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-form-field-error-text-color: #f44336; + --mat-form-field-select-option-text-color: inherit; + --mat-form-field-select-disabled-option-text-color: GrayText; + --mat-form-field-leading-icon-color: unset; + --mat-form-field-disabled-leading-icon-color: unset; + --mat-form-field-trailing-icon-color: unset; + --mat-form-field-disabled-trailing-icon-color: unset; + --mat-form-field-error-focus-trailing-icon-color: unset; + --mat-form-field-error-hover-trailing-icon-color: unset; + --mat-form-field-error-trailing-icon-color: unset; + --mat-form-field-enabled-select-arrow-color: rgba(0, 0, 0, 0.54); + --mat-form-field-disabled-select-arrow-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-hover-state-layer-opacity: 0.04; + --mat-form-field-focus-state-layer-opacity: 0.12; + --mat-form-field-filled-container-color: #f6f6f6; + --mat-form-field-filled-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 4%, + transparent + ); + --mat-form-field-filled-label-text-color: rgba(0, 0, 0, 0.54); + --mat-form-field-filled-hover-label-text-color: rgba(0, 0, 0, 0.54); + --mat-form-field-filled-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-filled-input-text-color: rgba(0, 0, 0, 0.87); + --mat-form-field-filled-disabled-input-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-filled-input-text-placeholder-color: rgba(0, 0, 0, 0.54); + --mat-form-field-filled-error-hover-label-text-color: #f44336; + --mat-form-field-filled-error-focus-label-text-color: #f44336; + --mat-form-field-filled-error-label-text-color: #f44336; + --mat-form-field-filled-error-caret-color: #f44336; + --mat-form-field-filled-active-indicator-color: rgba(0, 0, 0, 0.54); + --mat-form-field-filled-disabled-active-indicator-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-form-field-filled-hover-active-indicator-color: rgba(0, 0, 0, 0.87); + --mat-form-field-filled-error-active-indicator-color: #f44336; + --mat-form-field-filled-error-focus-active-indicator-color: #f44336; + --mat-form-field-filled-error-hover-active-indicator-color: #f44336; + --mat-form-field-outlined-label-text-color: rgba(0, 0, 0, 0.54); + --mat-form-field-outlined-hover-label-text-color: rgba(0, 0, 0, 0.87); + --mat-form-field-outlined-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-outlined-input-text-color: rgba(0, 0, 0, 0.87); + --mat-form-field-outlined-disabled-input-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-outlined-input-text-placeholder-color: rgba(0, 0, 0, 0.54); + --mat-form-field-outlined-error-caret-color: #f44336; + --mat-form-field-outlined-error-focus-label-text-color: #f44336; + --mat-form-field-outlined-error-label-text-color: #f44336; + --mat-form-field-outlined-error-hover-label-text-color: #f44336; + --mat-form-field-outlined-outline-color: rgba(0, 0, 0, 0.38); + --mat-form-field-outlined-disabled-outline-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-form-field-outlined-hover-outline-color: rgba(0, 0, 0, 0.87); + --mat-form-field-outlined-error-focus-outline-color: #f44336; + --mat-form-field-outlined-error-hover-outline-color: #f44336; + --mat-form-field-outlined-error-outline-color: #f44336; +} +.mat-mdc-form-field.mat-accent { + --mat-form-field-focus-select-arrow-color: color-mix(in srgb, #ff4081 87%, transparent); + --mat-form-field-filled-caret-color: #ff4081; + --mat-form-field-filled-focus-active-indicator-color: #ff4081; + --mat-form-field-filled-focus-label-text-color: color-mix(in srgb, #ff4081 87%, transparent); + --mat-form-field-outlined-caret-color: #ff4081; + --mat-form-field-outlined-focus-outline-color: #ff4081; + --mat-form-field-outlined-focus-label-text-color: color-mix(in srgb, #ff4081 87%, transparent); +} +.mat-mdc-form-field.mat-warn { + --mat-form-field-focus-select-arrow-color: color-mix(in srgb, #f44336 87%, transparent); + --mat-form-field-filled-caret-color: #f44336; + --mat-form-field-filled-focus-active-indicator-color: #f44336; + --mat-form-field-filled-focus-label-text-color: color-mix(in srgb, #f44336 87%, transparent); + --mat-form-field-outlined-caret-color: #f44336; + --mat-form-field-outlined-focus-outline-color: #f44336; + --mat-form-field-outlined-focus-label-text-color: color-mix(in srgb, #f44336 87%, transparent); +} +html { + --mat-form-field-container-height: 56px; + --mat-form-field-filled-label-display: block; + --mat-form-field-container-vertical-padding: 16px; + --mat-form-field-filled-with-label-container-padding-top: 24px; + --mat-form-field-filled-with-label-container-padding-bottom: 8px; +} +html { + --mat-form-field-container-text-font: Mont, sans-serif; + --mat-form-field-container-text-line-height: 24px; + --mat-form-field-container-text-size: 16px; + --mat-form-field-container-text-tracking: 0.03125em; + --mat-form-field-container-text-weight: 400; + --mat-form-field-outlined-label-text-populated-size: 16px; + --mat-form-field-subscript-text-font: Mont, sans-serif; + --mat-form-field-subscript-text-line-height: 20px; + --mat-form-field-subscript-text-size: 12px; + --mat-form-field-subscript-text-tracking: 0.0333333333em; + --mat-form-field-subscript-text-weight: 400; + --mat-form-field-filled-label-text-font: Mont, sans-serif; + --mat-form-field-filled-label-text-size: 16px; + --mat-form-field-filled-label-text-tracking: 0.03125em; + --mat-form-field-filled-label-text-weight: 400; + --mat-form-field-outlined-label-text-font: Mont, sans-serif; + --mat-form-field-outlined-label-text-size: 16px; + --mat-form-field-outlined-label-text-tracking: 0.03125em; + --mat-form-field-outlined-label-text-weight: 400; +} +html { + --mat-select-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} +html { + --mat-select-panel-background-color: white; + --mat-select-enabled-trigger-text-color: rgba(0, 0, 0, 0.87); + --mat-select-disabled-trigger-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-select-placeholder-text-color: rgba(0, 0, 0, 0.54); + --mat-select-enabled-arrow-color: rgba(0, 0, 0, 0.54); + --mat-select-disabled-arrow-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-select-focused-arrow-color: #3f51b5; + --mat-select-invalid-arrow-color: #f44336; +} +.mat-mdc-form-field.mat-accent { + --mat-select-panel-background-color: white; + --mat-select-enabled-trigger-text-color: rgba(0, 0, 0, 0.87); + --mat-select-disabled-trigger-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-select-placeholder-text-color: rgba(0, 0, 0, 0.54); + --mat-select-enabled-arrow-color: rgba(0, 0, 0, 0.54); + --mat-select-disabled-arrow-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-select-focused-arrow-color: #ff4081; + --mat-select-invalid-arrow-color: #f44336; +} +.mat-mdc-form-field.mat-warn { + --mat-select-panel-background-color: white; + --mat-select-enabled-trigger-text-color: rgba(0, 0, 0, 0.87); + --mat-select-disabled-trigger-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-select-placeholder-text-color: rgba(0, 0, 0, 0.54); + --mat-select-enabled-arrow-color: rgba(0, 0, 0, 0.54); + --mat-select-disabled-arrow-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-select-focused-arrow-color: #f44336; + --mat-select-invalid-arrow-color: #f44336; +} +html { + --mat-select-arrow-transform: translateY(-8px); +} +html { + --mat-select-trigger-text-font: Mont, sans-serif; + --mat-select-trigger-text-line-height: 24px; + --mat-select-trigger-text-size: 16px; + --mat-select-trigger-text-tracking: 0.03125em; + --mat-select-trigger-text-weight: 400; +} +html { + --mat-autocomplete-container-shape: 4px; + --mat-autocomplete-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} +html { + --mat-autocomplete-background-color: white; +} +html { + --mat-dialog-container-shape: 4px; + --mat-dialog-container-elevation-shadow: + 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), + 0px 9px 46px 8px rgba(0, 0, 0, 0.12); + --mat-dialog-container-max-width: 80vw; + --mat-dialog-container-small-max-width: 80vw; + --mat-dialog-container-min-width: 0; + --mat-dialog-actions-alignment: start; + --mat-dialog-actions-padding: 8px; + --mat-dialog-content-padding: 20px 24px; + --mat-dialog-with-actions-content-padding: 20px 24px; + --mat-dialog-headline-padding: 0 24px 9px; +} +html { + --mat-dialog-container-color: white; + --mat-dialog-subhead-color: rgba(0, 0, 0, 0.87); + --mat-dialog-supporting-text-color: rgba(0, 0, 0, 0.54); +} +html { + --mat-dialog-subhead-font: Mont, sans-serif; + --mat-dialog-subhead-line-height: 32px; + --mat-dialog-subhead-size: 20px; + --mat-dialog-subhead-weight: 500; + --mat-dialog-subhead-tracking: 0.0125em; + --mat-dialog-supporting-text-font: Mont, sans-serif; + --mat-dialog-supporting-text-line-height: 24px; + --mat-dialog-supporting-text-size: 16px; + --mat-dialog-supporting-text-weight: 400; + --mat-dialog-supporting-text-tracking: 0.03125em; +} +.mat-mdc-standard-chip { + --mat-chip-container-shape-radius: 16px; + --mat-chip-disabled-container-opacity: 0.4; + --mat-chip-disabled-outline-color: transparent; + --mat-chip-flat-selected-outline-width: 0; + --mat-chip-focus-outline-color: transparent; + --mat-chip-hover-state-layer-opacity: 0.04; + --mat-chip-outline-color: transparent; + --mat-chip-outline-width: 0; + --mat-chip-selected-hover-state-layer-opacity: 0.04; + --mat-chip-selected-trailing-action-state-layer-color: transparent; + --mat-chip-trailing-action-focus-opacity: 1; + --mat-chip-trailing-action-focus-state-layer-opacity: 0; + --mat-chip-trailing-action-hover-state-layer-opacity: 0; + --mat-chip-trailing-action-opacity: 0.54; + --mat-chip-trailing-action-state-layer-color: transparent; + --mat-chip-with-avatar-avatar-shape-radius: 14px; + --mat-chip-with-avatar-avatar-size: 28px; + --mat-chip-with-avatar-disabled-avatar-opacity: 1; + --mat-chip-with-icon-disabled-icon-opacity: 1; + --mat-chip-with-icon-icon-size: 18px; + --mat-chip-with-trailing-icon-disabled-trailing-icon-opacity: 1; +} +.mat-mdc-standard-chip { + --mat-chip-disabled-label-text-color: rgba(0, 0, 0, 0.87); + --mat-chip-elevated-container-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-chip-elevated-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-chip-elevated-selected-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-chip-flat-disabled-selected-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-chip-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-focus-state-layer-opacity: 0.12; + --mat-chip-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-label-text-color: rgba(0, 0, 0, 0.87); + --mat-chip-selected-disabled-trailing-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-selected-focus-state-layer-color: 0.12; + --mat-chip-selected-focus-state-layer-opacity: 0.12; + --mat-chip-selected-hover-state-layer-color: 0.04; + --mat-chip-selected-label-text-color: rgba(0, 0, 0, 0.87); + --mat-chip-selected-trailing-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-icon-disabled-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-icon-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-icon-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-trailing-icon-disabled-trailing-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-trailing-icon-trailing-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-standard-chip.mat-mdc-chip-selected.mat-primary, +.mat-mdc-standard-chip.mat-mdc-chip-highlighted.mat-primary { + --mat-chip-disabled-label-text-color: white; + --mat-chip-elevated-container-color: #3f51b5; + --mat-chip-elevated-disabled-container-color: #3f51b5; + --mat-chip-elevated-selected-container-color: #3f51b5; + --mat-chip-flat-disabled-selected-container-color: #3f51b5; + --mat-chip-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-focus-state-layer-opacity: 0.12; + --mat-chip-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-label-text-color: white; + --mat-chip-selected-disabled-trailing-icon-color: white; + --mat-chip-selected-focus-state-layer-color: 0.12; + --mat-chip-selected-focus-state-layer-opacity: 0.12; + --mat-chip-selected-hover-state-layer-color: 0.04; + --mat-chip-selected-label-text-color: white; + --mat-chip-selected-trailing-icon-color: white; + --mat-chip-with-icon-disabled-icon-color: white; + --mat-chip-with-icon-icon-color: white; + --mat-chip-with-icon-selected-icon-color: white; + --mat-chip-with-trailing-icon-disabled-trailing-icon-color: white; + --mat-chip-with-trailing-icon-trailing-icon-color: white; +} +.mat-mdc-standard-chip.mat-mdc-chip-selected.mat-accent, +.mat-mdc-standard-chip.mat-mdc-chip-highlighted.mat-accent { + --mat-chip-disabled-label-text-color: white; + --mat-chip-elevated-container-color: #ff4081; + --mat-chip-elevated-disabled-container-color: #ff4081; + --mat-chip-elevated-selected-container-color: #ff4081; + --mat-chip-flat-disabled-selected-container-color: #ff4081; + --mat-chip-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-focus-state-layer-opacity: 0.12; + --mat-chip-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-label-text-color: white; + --mat-chip-selected-disabled-trailing-icon-color: white; + --mat-chip-selected-focus-state-layer-color: 0.12; + --mat-chip-selected-focus-state-layer-opacity: 0.12; + --mat-chip-selected-hover-state-layer-color: 0.04; + --mat-chip-selected-label-text-color: white; + --mat-chip-selected-trailing-icon-color: white; + --mat-chip-with-icon-disabled-icon-color: white; + --mat-chip-with-icon-icon-color: white; + --mat-chip-with-icon-selected-icon-color: white; + --mat-chip-with-trailing-icon-disabled-trailing-icon-color: white; + --mat-chip-with-trailing-icon-trailing-icon-color: white; +} +.mat-mdc-standard-chip.mat-mdc-chip-selected.mat-warn, +.mat-mdc-standard-chip.mat-mdc-chip-highlighted.mat-warn { + --mat-chip-disabled-label-text-color: white; + --mat-chip-elevated-container-color: #f44336; + --mat-chip-elevated-disabled-container-color: #f44336; + --mat-chip-elevated-selected-container-color: #f44336; + --mat-chip-flat-disabled-selected-container-color: #f44336; + --mat-chip-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-focus-state-layer-opacity: 0.12; + --mat-chip-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-label-text-color: white; + --mat-chip-selected-disabled-trailing-icon-color: white; + --mat-chip-selected-focus-state-layer-color: 0.12; + --mat-chip-selected-focus-state-layer-opacity: 0.12; + --mat-chip-selected-hover-state-layer-color: 0.04; + --mat-chip-selected-label-text-color: white; + --mat-chip-selected-trailing-icon-color: white; + --mat-chip-with-icon-disabled-icon-color: white; + --mat-chip-with-icon-icon-color: white; + --mat-chip-with-icon-selected-icon-color: white; + --mat-chip-with-trailing-icon-disabled-trailing-icon-color: white; + --mat-chip-with-trailing-icon-trailing-icon-color: white; +} +.mat-mdc-chip.mat-mdc-standard-chip { + --mat-chip-container-height: 32px; +} +.mat-mdc-standard-chip { + --mat-chip-label-text-font: Mont, sans-serif; + --mat-chip-label-text-line-height: 20px; + --mat-chip-label-text-size: 14px; + --mat-chip-label-text-tracking: 0.0178571429em; + --mat-chip-label-text-weight: 400; +} +html { + --mat-slide-toggle-disabled-handle-opacity: 0.38; + --mat-slide-toggle-disabled-selected-handle-opacity: 0.38; + --mat-slide-toggle-disabled-selected-icon-opacity: 0.38; + --mat-slide-toggle-disabled-track-opacity: 0.12; + --mat-slide-toggle-disabled-unselected-handle-opacity: 0.38; + --mat-slide-toggle-disabled-unselected-icon-opacity: 0.38; + --mat-slide-toggle-disabled-unselected-track-outline-color: transparent; + --mat-slide-toggle-disabled-unselected-track-outline-width: 1px; + --mat-slide-toggle-handle-height: 20px; + --mat-slide-toggle-handle-shape: 10px; + --mat-slide-toggle-handle-width: 20px; + --mat-slide-toggle-hidden-track-opacity: 1; + --mat-slide-toggle-hidden-track-transition: transform 75ms 0ms cubic-bezier(0.4, 0, 0.6, 1); + --mat-slide-toggle-pressed-handle-size: 20px; + --mat-slide-toggle-selected-focus-state-layer-opacity: 0.12; + --mat-slide-toggle-selected-handle-horizontal-margin: 0; + --mat-slide-toggle-selected-handle-size: 20px; + --mat-slide-toggle-selected-hover-state-layer-opacity: 0.04; + --mat-slide-toggle-selected-icon-size: 18px; + --mat-slide-toggle-selected-pressed-handle-horizontal-margin: 0; + --mat-slide-toggle-selected-pressed-state-layer-opacity: 0.12; + --mat-slide-toggle-selected-track-outline-color: transparent; + --mat-slide-toggle-selected-track-outline-width: 1px; + --mat-slide-toggle-selected-with-icon-handle-horizontal-margin: 0; + --mat-slide-toggle-track-height: 14px; + --mat-slide-toggle-track-outline-color: transparent; + --mat-slide-toggle-track-outline-width: 1px; + --mat-slide-toggle-track-shape: 7px; + --mat-slide-toggle-track-width: 36px; + --mat-slide-toggle-unselected-focus-state-layer-opacity: 0.12; + --mat-slide-toggle-unselected-handle-horizontal-margin: 0; + --mat-slide-toggle-unselected-handle-size: 20px; + --mat-slide-toggle-unselected-hover-state-layer-opacity: 0.12; + --mat-slide-toggle-unselected-icon-size: 18px; + --mat-slide-toggle-unselected-pressed-handle-horizontal-margin: 0; + --mat-slide-toggle-unselected-pressed-state-layer-opacity: 0.1; + --mat-slide-toggle-unselected-with-icon-handle-horizontal-margin: 0; + --mat-slide-toggle-visible-track-opacity: 1; + --mat-slide-toggle-visible-track-transition: transform 75ms 0ms cubic-bezier(0, 0, 0.2, 1); + --mat-slide-toggle-with-icon-handle-size: 20px; + --mat-slide-toggle-touch-target-size: 48px; +} +html { + --mat-slide-toggle-selected-icon-color: white; + --mat-slide-toggle-disabled-selected-icon-color: white; + --mat-slide-toggle-selected-focus-state-layer-color: #3f51b5; + --mat-slide-toggle-selected-handle-color: #3f51b5; + --mat-slide-toggle-selected-hover-state-layer-color: #3f51b5; + --mat-slide-toggle-selected-pressed-state-layer-color: #3f51b5; + --mat-slide-toggle-selected-focus-handle-color: #3f51b5; + --mat-slide-toggle-selected-hover-handle-color: #3f51b5; + --mat-slide-toggle-selected-pressed-handle-color: #3f51b5; + --mat-slide-toggle-selected-focus-track-color: #7986cb; + --mat-slide-toggle-selected-hover-track-color: #7986cb; + --mat-slide-toggle-selected-pressed-track-color: #7986cb; + --mat-slide-toggle-selected-track-color: #7986cb; + --mat-slide-toggle-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-slide-toggle-disabled-handle-elevation-shadow: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + --mat-slide-toggle-disabled-selected-handle-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-disabled-selected-track-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-disabled-unselected-handle-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-disabled-unselected-icon-color: #f6f6f6; + --mat-slide-toggle-disabled-unselected-track-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-handle-elevation-shadow: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + --mat-slide-toggle-handle-surface-color: white; + --mat-slide-toggle-label-text-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-unselected-hover-handle-color: #424242; + --mat-slide-toggle-unselected-focus-handle-color: #424242; + --mat-slide-toggle-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-unselected-focus-track-color: rgba(0, 0, 0, 0.12); + --mat-slide-toggle-unselected-icon-color: #f6f6f6; + --mat-slide-toggle-unselected-handle-color: rgba(0, 0, 0, 0.54); + --mat-slide-toggle-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-unselected-hover-track-color: rgba(0, 0, 0, 0.12); + --mat-slide-toggle-unselected-pressed-handle-color: #424242; + --mat-slide-toggle-unselected-pressed-track-color: rgba(0, 0, 0, 0.12); + --mat-slide-toggle-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-unselected-track-color: rgba(0, 0, 0, 0.12); +} +.mat-mdc-slide-toggle.mat-accent { + --mat-slide-toggle-selected-icon-color: white; + --mat-slide-toggle-disabled-selected-icon-color: white; + --mat-slide-toggle-selected-focus-state-layer-color: #ff4081; + --mat-slide-toggle-selected-handle-color: #ff4081; + --mat-slide-toggle-selected-hover-state-layer-color: #ff4081; + --mat-slide-toggle-selected-pressed-state-layer-color: #ff4081; + --mat-slide-toggle-selected-focus-handle-color: #ff4081; + --mat-slide-toggle-selected-hover-handle-color: #ff4081; + --mat-slide-toggle-selected-pressed-handle-color: #ff4081; + --mat-slide-toggle-selected-focus-track-color: #f06292; + --mat-slide-toggle-selected-hover-track-color: #f06292; + --mat-slide-toggle-selected-pressed-track-color: #f06292; + --mat-slide-toggle-selected-track-color: #f06292; +} +.mat-mdc-slide-toggle.mat-warn { + --mat-slide-toggle-selected-icon-color: white; + --mat-slide-toggle-disabled-selected-icon-color: white; + --mat-slide-toggle-selected-focus-state-layer-color: #f44336; + --mat-slide-toggle-selected-handle-color: #f44336; + --mat-slide-toggle-selected-hover-state-layer-color: #f44336; + --mat-slide-toggle-selected-pressed-state-layer-color: #f44336; + --mat-slide-toggle-selected-focus-handle-color: #f44336; + --mat-slide-toggle-selected-hover-handle-color: #f44336; + --mat-slide-toggle-selected-pressed-handle-color: #f44336; + --mat-slide-toggle-selected-focus-track-color: #e57373; + --mat-slide-toggle-selected-hover-track-color: #e57373; + --mat-slide-toggle-selected-pressed-track-color: #e57373; + --mat-slide-toggle-selected-track-color: #e57373; +} +html { + --mat-slide-toggle-state-layer-size: 40px; + --mat-slide-toggle-touch-target-display: block; +} +html { + --mat-slide-toggle-label-text-font: Mont, sans-serif; + --mat-slide-toggle-label-text-line-height: 20px; + --mat-slide-toggle-label-text-size: 14px; + --mat-slide-toggle-label-text-tracking: 0.0178571429em; + --mat-slide-toggle-label-text-weight: 400; +} +html .mat-mdc-slide-toggle { + --mat-slide-toggle-label-text-font: Mont, sans-serif; + --mat-slide-toggle-label-text-line-height: 20px; + --mat-slide-toggle-label-text-size: 14px; + --mat-slide-toggle-label-text-tracking: 0.0178571429em; + --mat-slide-toggle-label-text-weight: 400; +} +html { + --mat-radio-disabled-selected-icon-opacity: 0.38; + --mat-radio-disabled-unselected-icon-opacity: 0.38; + --mat-radio-state-layer-size: 40px; + --mat-radio-touch-target-size: 48px; +} +.mat-mdc-radio-button.mat-primary { + --mat-radio-checked-ripple-color: #3f51b5; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #3f51b5; + --mat-radio-selected-hover-icon-color: #3f51b5; + --mat-radio-selected-icon-color: #3f51b5; + --mat-radio-selected-pressed-icon-color: #3f51b5; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-radio-button.mat-accent { + --mat-radio-checked-ripple-color: #ff4081; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #ff4081; + --mat-radio-selected-hover-icon-color: #ff4081; + --mat-radio-selected-icon-color: #ff4081; + --mat-radio-selected-pressed-icon-color: #ff4081; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-radio-button.mat-warn { + --mat-radio-checked-ripple-color: #f44336; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #f44336; + --mat-radio-selected-hover-icon-color: #f44336; + --mat-radio-selected-icon-color: #f44336; + --mat-radio-selected-pressed-icon-color: #f44336; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-radio-state-layer-size: 40px; + --mat-radio-touch-target-display: block; +} +html { + --mat-radio-label-text-font: Mont, sans-serif; + --mat-radio-label-text-line-height: 20px; + --mat-radio-label-text-size: 14px; + --mat-radio-label-text-tracking: 0.0178571429em; + --mat-radio-label-text-weight: 400; +} +html { + --mat-slider-active-track-height: 6px; + --mat-slider-active-track-shape: 9999px; + --mat-slider-handle-elevation: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + --mat-slider-handle-height: 20px; + --mat-slider-handle-shape: 50%; + --mat-slider-handle-width: 20px; + --mat-slider-inactive-track-height: 4px; + --mat-slider-inactive-track-shape: 9999px; + --mat-slider-value-indicator-border-radius: 4px; + --mat-slider-value-indicator-caret-display: block; + --mat-slider-value-indicator-container-transform: translateX(-50%); + --mat-slider-value-indicator-height: 32px; + --mat-slider-value-indicator-padding: 0 12px; + --mat-slider-value-indicator-text-transform: none; + --mat-slider-value-indicator-width: auto; + --mat-slider-with-overlap-handle-outline-width: 1px; + --mat-slider-with-tick-marks-active-container-opacity: 0.6; + --mat-slider-with-tick-marks-container-shape: 50%; + --mat-slider-with-tick-marks-container-size: 2px; + --mat-slider-with-tick-marks-inactive-container-opacity: 0.6; + --mat-slider-value-indicator-transform-origin: bottom; +} +html { + --mat-slider-active-track-color: #3f51b5; + --mat-slider-focus-handle-color: #3f51b5; + --mat-slider-handle-color: #3f51b5; + --mat-slider-hover-handle-color: #3f51b5; + --mat-slider-focus-state-layer-color: color-mix(in srgb, #3f51b5 12%, transparent); + --mat-slider-hover-state-layer-color: color-mix(in srgb, #3f51b5 4%, transparent); + --mat-slider-inactive-track-color: #3f51b5; + --mat-slider-ripple-color: #3f51b5; + --mat-slider-with-tick-marks-active-container-color: white; + --mat-slider-with-tick-marks-inactive-container-color: #3f51b5; + --mat-slider-disabled-active-track-color: rgba(0, 0, 0, 0.87); + --mat-slider-disabled-handle-color: rgba(0, 0, 0, 0.87); + --mat-slider-disabled-inactive-track-color: rgba(0, 0, 0, 0.87); + --mat-slider-label-container-color: #424242; + --mat-slider-label-label-text-color: white; + --mat-slider-value-indicator-opacity: 1; + --mat-slider-with-overlap-handle-outline-color: rgba(0, 0, 0, 0.87); + --mat-slider-with-tick-marks-disabled-container-color: rgba(0, 0, 0, 0.87); +} +.mat-accent { + --mat-slider-active-track-color: #ff4081; + --mat-slider-focus-handle-color: #ff4081; + --mat-slider-handle-color: #ff4081; + --mat-slider-hover-handle-color: #ff4081; + --mat-slider-focus-state-layer-color: color-mix(in srgb, #ff4081 12%, transparent); + --mat-slider-hover-state-layer-color: color-mix(in srgb, #ff4081 4%, transparent); + --mat-slider-inactive-track-color: #ff4081; + --mat-slider-ripple-color: #ff4081; + --mat-slider-with-tick-marks-active-container-color: white; + --mat-slider-with-tick-marks-inactive-container-color: #ff4081; +} +.mat-warn { + --mat-slider-active-track-color: #f44336; + --mat-slider-focus-handle-color: #f44336; + --mat-slider-handle-color: #f44336; + --mat-slider-hover-handle-color: #f44336; + --mat-slider-focus-state-layer-color: color-mix(in srgb, #f44336 12%, transparent); + --mat-slider-hover-state-layer-color: color-mix(in srgb, #f44336 4%, transparent); + --mat-slider-inactive-track-color: #f44336; + --mat-slider-ripple-color: #f44336; + --mat-slider-with-tick-marks-active-container-color: white; + --mat-slider-with-tick-marks-inactive-container-color: #f44336; +} +html { + --mat-slider-label-label-text-font: Mont, sans-serif; + --mat-slider-label-label-text-size: 14px; + --mat-slider-label-label-text-line-height: 22px; + --mat-slider-label-label-text-tracking: 0.0071428571em; + --mat-slider-label-label-text-weight: 500; +} +html { + --mat-menu-container-shape: 4px; + --mat-menu-divider-bottom-spacing: 0; + --mat-menu-divider-top-spacing: 0; + --mat-menu-item-spacing: 16px; + --mat-menu-item-icon-size: 24px; + --mat-menu-item-leading-spacing: 16px; + --mat-menu-item-trailing-spacing: 16px; + --mat-menu-item-with-icon-leading-spacing: 16px; + --mat-menu-item-with-icon-trailing-spacing: 16px; + --mat-menu-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} +html { + --mat-menu-item-label-text-color: rgba(0, 0, 0, 0.87); + --mat-menu-item-icon-color: rgba(0, 0, 0, 0.87); + --mat-menu-item-hover-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 4%, transparent); + --mat-menu-item-focus-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-menu-container-color: white; + --mat-menu-divider-color: rgba(0, 0, 0, 0.12); +} +html { + --mat-menu-item-label-text-font: Mont, sans-serif; + --mat-menu-item-label-text-size: 16px; + --mat-menu-item-label-text-tracking: 0.03125em; + --mat-menu-item-label-text-line-height: 24px; + --mat-menu-item-label-text-weight: 400; +} +html { + --mat-list-active-indicator-color: transparent; + --mat-list-active-indicator-shape: 4px; + --mat-list-list-item-container-shape: 0; + --mat-list-list-item-leading-avatar-shape: 50%; + --mat-list-list-item-container-color: transparent; + --mat-list-list-item-selected-container-color: transparent; + --mat-list-list-item-leading-avatar-color: transparent; + --mat-list-list-item-leading-icon-size: 24px; + --mat-list-list-item-leading-avatar-size: 40px; + --mat-list-list-item-trailing-icon-size: 24px; + --mat-list-list-item-disabled-state-layer-color: transparent; + --mat-list-list-item-disabled-state-layer-opacity: 0; + --mat-list-list-item-disabled-label-text-opacity: 0.38; + --mat-list-list-item-disabled-leading-icon-opacity: 0.38; + --mat-list-list-item-disabled-trailing-icon-opacity: 0.38; +} +html { + --mat-list-list-item-label-text-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-supporting-text-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-leading-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-trailing-supporting-text-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-trailing-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-selected-trailing-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-disabled-label-text-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-disabled-leading-icon-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-disabled-trailing-icon-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-hover-label-text-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-hover-leading-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-hover-state-layer-opacity: 0.04; + --mat-list-list-item-hover-trailing-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-focus-label-text-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-focus-state-layer-opacity: 0.12; +} +.mdc-list-item__start, +.mdc-list-item__end { + --mat-radio-checked-ripple-color: #3f51b5; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #3f51b5; + --mat-radio-selected-hover-icon-color: #3f51b5; + --mat-radio-selected-icon-color: #3f51b5; + --mat-radio-selected-pressed-icon-color: #3f51b5; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-accent .mdc-list-item__start, +.mat-accent .mdc-list-item__end { + --mat-radio-checked-ripple-color: #ff4081; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #ff4081; + --mat-radio-selected-hover-icon-color: #ff4081; + --mat-radio-selected-icon-color: #ff4081; + --mat-radio-selected-pressed-icon-color: #ff4081; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-warn .mdc-list-item__start, +.mat-warn .mdc-list-item__end { + --mat-radio-checked-ripple-color: #f44336; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #f44336; + --mat-radio-selected-hover-icon-color: #f44336; + --mat-radio-selected-icon-color: #f44336; + --mat-radio-selected-pressed-icon-color: #f44336; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-list-option { + --mat-checkbox-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-checkbox-label-text-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #3f51b5; + --mat-checkbox-selected-hover-icon-color: #3f51b5; + --mat-checkbox-selected-icon-color: #3f51b5; + --mat-checkbox-selected-pressed-icon-color: #3f51b5; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #3f51b5; + --mat-checkbox-selected-hover-state-layer-color: #3f51b5; + --mat-checkbox-selected-pressed-state-layer-color: #3f51b5; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-list-option.mat-accent { + --mat-checkbox-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-checkbox-label-text-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #ff4081; + --mat-checkbox-selected-hover-icon-color: #ff4081; + --mat-checkbox-selected-icon-color: #ff4081; + --mat-checkbox-selected-pressed-icon-color: #ff4081; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #ff4081; + --mat-checkbox-selected-hover-state-layer-color: #ff4081; + --mat-checkbox-selected-pressed-state-layer-color: #ff4081; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-list-option.mat-warn { + --mat-checkbox-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-checkbox-label-text-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #f44336; + --mat-checkbox-selected-hover-icon-color: #f44336; + --mat-checkbox-selected-icon-color: #f44336; + --mat-checkbox-selected-pressed-icon-color: #f44336; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #f44336; + --mat-checkbox-selected-hover-state-layer-color: #f44336; + --mat-checkbox-selected-pressed-state-layer-color: #f44336; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-list-base.mat-mdc-list-base .mdc-list-item--selected .mdc-list-item__primary-text, +.mat-mdc-list-base.mat-mdc-list-base .mdc-list-item--selected .mdc-list-item__start, +.mat-mdc-list-base.mat-mdc-list-base .mdc-list-item--activated .mdc-list-item__primary-text, +.mat-mdc-list-base.mat-mdc-list-base .mdc-list-item--activated .mdc-list-item__start { + color: #3f51b5; +} +.mat-mdc-list-base .mdc-list-item--disabled .mdc-list-item__start, +.mat-mdc-list-base .mdc-list-item--disabled .mdc-list-item__content, +.mat-mdc-list-base .mdc-list-item--disabled .mdc-list-item__end { + opacity: 1; +} +html { + --mat-list-list-item-leading-icon-start-space: 16px; + --mat-list-list-item-leading-icon-end-space: 32px; + --mat-list-list-item-one-line-container-height: 48px; + --mat-list-list-item-two-line-container-height: 64px; + --mat-list-list-item-three-line-container-height: 88px; +} +.mdc-list-item__start, +.mdc-list-item__end { + --mat-radio-state-layer-size: 40px; + --mat-radio-touch-target-display: block; +} +.mat-mdc-list-item.mdc-list-item--with-leading-avatar.mdc-list-item--with-one-line, +.mat-mdc-list-item.mdc-list-item--with-leading-checkbox.mdc-list-item--with-one-line, +.mat-mdc-list-item.mdc-list-item--with-leading-icon.mdc-list-item--with-one-line { + height: 56px; +} +.mat-mdc-list-item.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines, +.mat-mdc-list-item.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines, +.mat-mdc-list-item.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines { + height: 72px; +} +html { + --mat-list-list-item-label-text-font: Mont, sans-serif; + --mat-list-list-item-label-text-line-height: 24px; + --mat-list-list-item-label-text-size: 16px; + --mat-list-list-item-label-text-tracking: 0.03125em; + --mat-list-list-item-label-text-weight: 400; + --mat-list-list-item-supporting-text-font: Mont, sans-serif; + --mat-list-list-item-supporting-text-line-height: 20px; + --mat-list-list-item-supporting-text-size: 14px; + --mat-list-list-item-supporting-text-tracking: 0.0178571429em; + --mat-list-list-item-supporting-text-weight: 400; + --mat-list-list-item-trailing-supporting-text-font: Mont, sans-serif; + --mat-list-list-item-trailing-supporting-text-line-height: 20px; + --mat-list-list-item-trailing-supporting-text-size: 12px; + --mat-list-list-item-trailing-supporting-text-tracking: 0.0333333333em; + --mat-list-list-item-trailing-supporting-text-weight: 400; +} +.mdc-list-group__subheader { + font: + 400 16px / 28px Mont, + sans-serif; + letter-spacing: 0.009375em; +} +html { + --mat-paginator-page-size-select-width: 84px; + --mat-paginator-page-size-select-touch-target-height: 48px; +} +html { + --mat-paginator-container-text-color: rgba(0, 0, 0, 0.87); + --mat-paginator-container-background-color: white; + --mat-paginator-enabled-icon-color: rgba(0, 0, 0, 0.54); + --mat-paginator-disabled-icon-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); +} +html { + --mat-paginator-container-size: 56px; + --mat-paginator-form-field-container-height: 40px; + --mat-paginator-form-field-container-vertical-padding: 8px; + --mat-paginator-touch-target-display: block; +} +html { + --mat-paginator-container-text-font: Mont, sans-serif; + --mat-paginator-container-text-line-height: 20px; + --mat-paginator-container-text-size: 12px; + --mat-paginator-container-text-tracking: 0.0333333333em; + --mat-paginator-container-text-weight: 400; + --mat-paginator-select-trigger-text-size: 12px; +} +html { + --mat-tab-container-height: 48px; + --mat-tab-divider-color: transparent; + --mat-tab-divider-height: 0; + --mat-tab-active-indicator-height: 2px; + --mat-tab-active-indicator-shape: 0; +} +.mat-mdc-tab-group, +.mat-mdc-tab-nav-bar { + --mat-tab-disabled-ripple-color: rgba(0, 0, 0, 0.54); + --mat-tab-pagination-icon-color: rgba(0, 0, 0, 0.87); + --mat-tab-inactive-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-label-text-color: #3f51b5; + --mat-tab-active-ripple-color: #3f51b5; + --mat-tab-inactive-ripple-color: #3f51b5; + --mat-tab-inactive-focus-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-inactive-hover-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-focus-label-text-color: #3f51b5; + --mat-tab-active-hover-label-text-color: #3f51b5; + --mat-tab-active-focus-indicator-color: #3f51b5; + --mat-tab-active-hover-indicator-color: #3f51b5; + --mat-tab-active-indicator-color: #3f51b5; +} +.mat-mdc-tab-group.mat-accent, +.mat-mdc-tab-nav-bar.mat-accent { + --mat-tab-disabled-ripple-color: rgba(0, 0, 0, 0.54); + --mat-tab-pagination-icon-color: rgba(0, 0, 0, 0.87); + --mat-tab-inactive-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-label-text-color: #ff4081; + --mat-tab-active-ripple-color: #ff4081; + --mat-tab-inactive-ripple-color: #ff4081; + --mat-tab-inactive-focus-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-inactive-hover-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-focus-label-text-color: #ff4081; + --mat-tab-active-hover-label-text-color: #ff4081; + --mat-tab-active-focus-indicator-color: #ff4081; + --mat-tab-active-hover-indicator-color: #ff4081; + --mat-tab-active-indicator-color: #ff4081; +} +.mat-mdc-tab-group.mat-warn, +.mat-mdc-tab-nav-bar.mat-warn { + --mat-tab-disabled-ripple-color: rgba(0, 0, 0, 0.54); + --mat-tab-pagination-icon-color: rgba(0, 0, 0, 0.87); + --mat-tab-inactive-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-label-text-color: #f44336; + --mat-tab-active-ripple-color: #f44336; + --mat-tab-inactive-ripple-color: #f44336; + --mat-tab-inactive-focus-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-inactive-hover-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-focus-label-text-color: #f44336; + --mat-tab-active-hover-label-text-color: #f44336; + --mat-tab-active-focus-indicator-color: #f44336; + --mat-tab-active-hover-indicator-color: #f44336; + --mat-tab-active-indicator-color: #f44336; +} +.mat-mdc-tab-group.mat-background-primary, +.mat-mdc-tab-nav-bar.mat-background-primary { + --mat-tab-background-color: #3f51b5; + --mat-tab-foreground-color: white; +} +.mat-mdc-tab-group.mat-background-accent, +.mat-mdc-tab-nav-bar.mat-background-accent { + --mat-tab-background-color: #ff4081; + --mat-tab-foreground-color: white; +} +.mat-mdc-tab-group.mat-background-warn, +.mat-mdc-tab-nav-bar.mat-background-warn { + --mat-tab-background-color: #f44336; + --mat-tab-foreground-color: white; +} +.mat-mdc-tab-header { + --mat-tab-container-height: 48px; +} +.mat-mdc-tab-header { + --mat-tab-label-text-font: Mont, sans-serif; + --mat-tab-label-text-size: 14px; + --mat-tab-label-text-tracking: 0.0892857143em; + --mat-tab-label-text-line-height: 36px; + --mat-tab-label-text-weight: 500; +} +html { + --mat-checkbox-disabled-selected-checkmark-color: white; + --mat-checkbox-selected-focus-state-layer-opacity: 0.12; + --mat-checkbox-selected-hover-state-layer-opacity: 0.04; + --mat-checkbox-selected-pressed-state-layer-opacity: 0.12; + --mat-checkbox-unselected-focus-state-layer-opacity: 0.12; + --mat-checkbox-unselected-hover-state-layer-opacity: 0.04; + --mat-checkbox-unselected-pressed-state-layer-opacity: 0.12; + --mat-checkbox-touch-target-size: 48px; +} +html { + --mat-checkbox-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-checkbox-label-text-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #ff4081; + --mat-checkbox-selected-hover-icon-color: #ff4081; + --mat-checkbox-selected-icon-color: #ff4081; + --mat-checkbox-selected-pressed-icon-color: #ff4081; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #ff4081; + --mat-checkbox-selected-hover-state-layer-color: #ff4081; + --mat-checkbox-selected-pressed-state-layer-color: #ff4081; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-checkbox.mat-primary { + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #3f51b5; + --mat-checkbox-selected-hover-icon-color: #3f51b5; + --mat-checkbox-selected-icon-color: #3f51b5; + --mat-checkbox-selected-pressed-icon-color: #3f51b5; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #3f51b5; + --mat-checkbox-selected-hover-state-layer-color: #3f51b5; + --mat-checkbox-selected-pressed-state-layer-color: #3f51b5; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-checkbox.mat-warn { + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #f44336; + --mat-checkbox-selected-hover-icon-color: #f44336; + --mat-checkbox-selected-icon-color: #f44336; + --mat-checkbox-selected-pressed-icon-color: #f44336; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #f44336; + --mat-checkbox-selected-hover-state-layer-color: #f44336; + --mat-checkbox-selected-pressed-state-layer-color: #f44336; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-checkbox-touch-target-display: block; + --mat-checkbox-state-layer-size: 40px; +} +html { + --mat-checkbox-label-text-font: Mont, sans-serif; + --mat-checkbox-label-text-line-height: 20px; + --mat-checkbox-label-text-size: 14px; + --mat-checkbox-label-text-tracking: 0.0178571429em; + --mat-checkbox-label-text-weight: 400; +} +html { + --mat-button-filled-container-shape: 4px; + --mat-button-filled-horizontal-padding: 16px; + --mat-button-filled-icon-offset: -4px; + --mat-button-filled-icon-spacing: 8px; + --mat-button-filled-touch-target-size: 48px; + --mat-button-outlined-container-shape: 4px; + --mat-button-outlined-horizontal-padding: 15px; + --mat-button-outlined-icon-offset: -4px; + --mat-button-outlined-icon-spacing: 8px; + --mat-button-outlined-keep-touch-target: false; + --mat-button-outlined-outline-width: 1px; + --mat-button-outlined-touch-target-size: 48px; + --mat-button-protected-container-elevation-shadow: + 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + --mat-button-protected-container-shape: 4px; + --mat-button-protected-disabled-container-elevation-shadow: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + --mat-button-protected-focus-container-elevation-shadow: + 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), + 0px 1px 10px 0px rgba(0, 0, 0, 0.12); + --mat-button-protected-horizontal-padding: 16px; + --mat-button-protected-hover-container-elevation-shadow: + 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), + 0px 1px 10px 0px rgba(0, 0, 0, 0.12); + --mat-button-protected-icon-offset: -4px; + --mat-button-protected-icon-spacing: 8px; + --mat-button-protected-pressed-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-button-protected-touch-target-size: 48px; + --mat-button-text-container-shape: 4px; + --mat-button-text-horizontal-padding: 8px; + --mat-button-text-icon-offset: 0; + --mat-button-text-icon-spacing: 8px; + --mat-button-text-with-icon-horizontal-padding: 8px; + --mat-button-text-touch-target-size: 48px; + --mat-button-tonal-container-shape: 4px; + --mat-button-tonal-horizontal-padding: 16px; + --mat-button-tonal-icon-offset: -4px; + --mat-button-tonal-icon-spacing: 8px; + --mat-button-tonal-touch-target-size: 48px; +} +html { + --mat-button-filled-container-color: white; + --mat-button-filled-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-filled-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-filled-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-filled-focus-state-layer-opacity: 0.12; + --mat-button-filled-hover-state-layer-opacity: 0.04; + --mat-button-filled-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-filled-pressed-state-layer-opacity: 0.12; + --mat-button-filled-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-filled-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-outlined-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-outlined-disabled-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-outlined-focus-state-layer-opacity: 0.12; + --mat-button-outlined-hover-state-layer-opacity: 0.04; + --mat-button-outlined-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-pressed-state-layer-opacity: 0.12; + --mat-button-outlined-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-outlined-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-protected-container-color: white; + --mat-button-protected-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-protected-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-protected-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-protected-focus-state-layer-opacity: 0.12; + --mat-button-protected-hover-state-layer-opacity: 0.04; + --mat-button-protected-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-protected-pressed-state-layer-opacity: 0.12; + --mat-button-protected-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-protected-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-text-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-text-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-text-focus-state-layer-opacity: 0.12; + --mat-button-text-hover-state-layer-opacity: 0.04; + --mat-button-text-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-text-pressed-state-layer-opacity: 0.12; + --mat-button-text-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-text-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-tonal-container-color: white; + --mat-button-tonal-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-tonal-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-tonal-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-tonal-focus-state-layer-opacity: 0.12; + --mat-button-tonal-hover-state-layer-opacity: 0.04; + --mat-button-tonal-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-tonal-pressed-state-layer-opacity: 0.12; + --mat-button-tonal-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-tonal-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-button.mat-primary, +.mat-mdc-unelevated-button.mat-primary, +.mat-mdc-raised-button.mat-primary, +.mat-mdc-outlined-button.mat-primary, +.mat-tonal-button.mat-primary { + --mat-button-filled-container-color: #3f51b5; + --mat-button-filled-label-text-color: white; + --mat-button-filled-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-filled-state-layer-color: white; + --mat-button-outlined-label-text-color: #3f51b5; + --mat-button-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-ripple-color: color-mix(in srgb, #3f51b5 12%, transparent); + --mat-button-outlined-state-layer-color: #3f51b5; + --mat-button-protected-container-color: #3f51b5; + --mat-button-protected-label-text-color: white; + --mat-button-protected-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-protected-state-layer-color: white; + --mat-button-text-label-text-color: #3f51b5; + --mat-button-text-ripple-color: color-mix(in srgb, #3f51b5 12%, transparent); + --mat-button-text-state-layer-color: #3f51b5; + --mat-button-tonal-container-color: #3f51b5; + --mat-button-tonal-label-text-color: white; + --mat-button-tonal-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-tonal-state-layer-color: white; +} +.mat-mdc-button.mat-accent, +.mat-mdc-unelevated-button.mat-accent, +.mat-mdc-raised-button.mat-accent, +.mat-mdc-outlined-button.mat-accent, +.mat-tonal-button.mat-accent { + --mat-button-filled-container-color: #ff4081; + --mat-button-filled-label-text-color: white; + --mat-button-filled-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-filled-state-layer-color: white; + --mat-button-outlined-label-text-color: #ff4081; + --mat-button-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-ripple-color: color-mix(in srgb, #ff4081 12%, transparent); + --mat-button-outlined-state-layer-color: #ff4081; + --mat-button-protected-container-color: #ff4081; + --mat-button-protected-label-text-color: white; + --mat-button-protected-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-protected-state-layer-color: white; + --mat-button-text-label-text-color: #ff4081; + --mat-button-text-ripple-color: color-mix(in srgb, #ff4081 12%, transparent); + --mat-button-text-state-layer-color: #ff4081; + --mat-button-tonal-container-color: #ff4081; + --mat-button-tonal-label-text-color: white; + --mat-button-tonal-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-tonal-state-layer-color: white; +} +.mat-mdc-button.mat-warn, +.mat-mdc-unelevated-button.mat-warn, +.mat-mdc-raised-button.mat-warn, +.mat-mdc-outlined-button.mat-warn, +.mat-tonal-button.mat-warn { + --mat-button-filled-container-color: #f44336; + --mat-button-filled-label-text-color: white; + --mat-button-filled-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-filled-state-layer-color: white; + --mat-button-outlined-label-text-color: #f44336; + --mat-button-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-ripple-color: color-mix(in srgb, #f44336 12%, transparent); + --mat-button-outlined-state-layer-color: #f44336; + --mat-button-protected-container-color: #f44336; + --mat-button-protected-label-text-color: white; + --mat-button-protected-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-protected-state-layer-color: white; + --mat-button-text-label-text-color: #f44336; + --mat-button-text-ripple-color: color-mix(in srgb, #f44336 12%, transparent); + --mat-button-text-state-layer-color: #f44336; + --mat-button-tonal-container-color: #f44336; + --mat-button-tonal-label-text-color: white; + --mat-button-tonal-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-tonal-state-layer-color: white; +} +html { + --mat-button-filled-container-height: 36px; + --mat-button-filled-touch-target-display: block; + --mat-button-outlined-container-height: 36px; + --mat-button-outlined-touch-target-display: block; + --mat-button-protected-container-height: 36px; + --mat-button-protected-touch-target-display: block; + --mat-button-text-container-height: 36px; + --mat-button-text-touch-target-display: block; + --mat-button-tonal-container-height: 36px; + --mat-button-tonal-touch-target-display: block; +} +html { + --mat-button-filled-label-text-font: Mont, sans-serif; + --mat-button-filled-label-text-size: 14px; + --mat-button-filled-label-text-tracking: 0.0892857143em; + --mat-button-filled-label-text-transform: none; + --mat-button-filled-label-text-weight: 500; + --mat-button-outlined-label-text-font: Mont, sans-serif; + --mat-button-outlined-label-text-size: 14px; + --mat-button-outlined-label-text-tracking: 0.0892857143em; + --mat-button-outlined-label-text-transform: none; + --mat-button-outlined-label-text-weight: 500; + --mat-button-protected-label-text-font: Mont, sans-serif; + --mat-button-protected-label-text-size: 14px; + --mat-button-protected-label-text-tracking: 0.0892857143em; + --mat-button-protected-label-text-transform: none; + --mat-button-protected-label-text-weight: 500; + --mat-button-text-label-text-font: Mont, sans-serif; + --mat-button-text-label-text-size: 14px; + --mat-button-text-label-text-tracking: 0.0892857143em; + --mat-button-text-label-text-transform: none; + --mat-button-text-label-text-weight: 500; + --mat-button-tonal-label-text-font: Mont, sans-serif; + --mat-button-tonal-label-text-size: 14px; + --mat-button-tonal-label-text-tracking: 0.0892857143em; + --mat-button-tonal-label-text-transform: none; + --mat-button-tonal-label-text-weight: 500; +} +html { + --mat-icon-button-icon-size: 24px; + --mat-icon-button-container-shape: 50%; + --mat-icon-button-touch-target-size: 48px; +} +html { + --mat-icon-button-disabled-icon-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-icon-button-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-icon-button-focus-state-layer-opacity: 0.12; + --mat-icon-button-hover-state-layer-opacity: 0.04; + --mat-icon-button-icon-color: inherit; + --mat-icon-button-pressed-state-layer-opacity: 0.12; + --mat-icon-button-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-icon-button-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-icon-button.mat-primary { + --mat-icon-button-icon-color: #3f51b5; + --mat-icon-button-state-layer-color: #3f51b5; + --mat-icon-button-ripple-color: color-mix(in srgb, #3f51b5 12%, transparent); +} +.mat-mdc-icon-button.mat-accent { + --mat-icon-button-icon-color: #ff4081; + --mat-icon-button-state-layer-color: #ff4081; + --mat-icon-button-ripple-color: color-mix(in srgb, #ff4081 12%, transparent); +} +.mat-mdc-icon-button.mat-warn { + --mat-icon-button-icon-color: #f44336; + --mat-icon-button-state-layer-color: #f44336; + --mat-icon-button-ripple-color: color-mix(in srgb, #f44336 12%, transparent); +} +html { + --mat-icon-button-touch-target-display: block; +} +.mat-mdc-icon-button.mat-mdc-button-base { + --mdc-icon-button-state-layer-size: 48px; + --mat-icon-button-state-layer-size: 48px; + width: var(--mat-icon-button-state-layer-size); + height: var(--mat-icon-button-state-layer-size); + padding: 12px; +} +html { + --mat-fab-container-elevation-shadow: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), + 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + --mat-fab-container-shape: 50%; + --mat-fab-touch-target-size: 48px; + --mat-fab-extended-container-elevation-shadow: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), + 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + --mat-fab-extended-container-height: 48px; + --mat-fab-extended-container-shape: 24px; + --mat-fab-extended-focus-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-extended-hover-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-extended-pressed-container-elevation-shadow: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), + 0px 5px 22px 4px rgba(0, 0, 0, 0.12); + --mat-fab-focus-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-hover-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-pressed-container-elevation-shadow: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), + 0px 5px 22px 4px rgba(0, 0, 0, 0.12); + --mat-fab-small-container-elevation-shadow: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), + 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + --mat-fab-small-container-shape: 50%; + --mat-fab-small-touch-target-size: 48px; + --mat-fab-small-focus-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-small-hover-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-small-pressed-container-elevation-shadow: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), + 0px 5px 22px 4px rgba(0, 0, 0, 0.12); +} +html { + --mat-fab-container-color: white; + --mat-fab-disabled-state-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-fab-disabled-state-foreground-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-fab-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-fab-focus-state-layer-opacity: 0.12; + --mat-fab-foreground-color: rgba(0, 0, 0, 0.87); + --mat-fab-hover-state-layer-opacity: 0.04; + --mat-fab-pressed-state-layer-opacity: 0.12; + --mat-fab-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-fab-small-container-color: white; + --mat-fab-small-disabled-state-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-fab-small-disabled-state-foreground-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-fab-small-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-fab-small-focus-state-layer-opacity: 0.12; + --mat-fab-small-foreground-color: rgba(0, 0, 0, 0.87); + --mat-fab-small-hover-state-layer-opacity: 0.04; + --mat-fab-small-pressed-state-layer-opacity: 0.12; + --mat-fab-small-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-fab-small-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-fab-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-fab.mat-primary, +.mat-mdc-mini-fab.mat-primary { + --mat-fab-container-color: #3f51b5; + --mat-fab-foreground-color: white; + --mat-fab-ripple-color: color-mix(in srgb, #3f51b5 12%, transparent); + --mat-fab-small-container-color: #3f51b5; + --mat-fab-small-foreground-color: white; + --mat-fab-small-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-fab-small-state-layer-color: white; + --mat-fab-state-layer-color: white; +} +.mat-mdc-fab.mat-accent, +.mat-mdc-mini-fab.mat-accent { + --mat-fab-container-color: #ff4081; + --mat-fab-foreground-color: white; + --mat-fab-ripple-color: color-mix(in srgb, #ff4081 12%, transparent); + --mat-fab-small-container-color: #ff4081; + --mat-fab-small-foreground-color: white; + --mat-fab-small-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-fab-small-state-layer-color: white; + --mat-fab-state-layer-color: white; +} +.mat-mdc-fab.mat-warn, +.mat-mdc-mini-fab.mat-warn { + --mat-fab-container-color: #f44336; + --mat-fab-foreground-color: white; + --mat-fab-ripple-color: color-mix(in srgb, #f44336 12%, transparent); + --mat-fab-small-container-color: #f44336; + --mat-fab-small-foreground-color: white; + --mat-fab-small-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-fab-small-state-layer-color: white; + --mat-fab-state-layer-color: white; +} +html { + --mat-fab-small-touch-target-display: block; + --mat-fab-touch-target-display: block; +} +html { + --mat-fab-extended-label-text-font: Mont, sans-serif; + --mat-fab-extended-label-text-size: 14px; + --mat-fab-extended-label-text-tracking: 0.0892857143em; + --mat-fab-extended-label-text-weight: 500; +} +html { + --mat-snack-bar-container-shape: 4px; +} +html { + --mat-snack-bar-container-color: #424242; + --mat-snack-bar-supporting-text-color: white; + --mat-snack-bar-button-color: #7986cb; +} +html { + --mat-snack-bar-supporting-text-font: Mont, sans-serif; + --mat-snack-bar-supporting-text-line-height: 20px; + --mat-snack-bar-supporting-text-size: 14px; + --mat-snack-bar-supporting-text-weight: 400; +} +html { + --mat-table-row-item-outline-width: 1px; +} +html { + --mat-table-background-color: white; + --mat-table-header-headline-color: rgba(0, 0, 0, 0.87); + --mat-table-row-item-label-text-color: rgba(0, 0, 0, 0.87); + --mat-table-row-item-outline-color: rgba(0, 0, 0, 0.12); +} +html { + --mat-table-header-container-height: 56px; + --mat-table-footer-container-height: 52px; + --mat-table-row-item-container-height: 52px; +} +html { + --mat-table-header-headline-font: Mont, sans-serif; + --mat-table-header-headline-line-height: 22px; + --mat-table-header-headline-size: 14px; + --mat-table-header-headline-weight: 500; + --mat-table-header-headline-tracking: 0.0071428571em; + --mat-table-row-item-label-text-font: Mont, sans-serif; + --mat-table-row-item-label-text-line-height: 20px; + --mat-table-row-item-label-text-size: 14px; + --mat-table-row-item-label-text-weight: 400; + --mat-table-row-item-label-text-tracking: 0.0178571429em; + --mat-table-footer-supporting-text-font: Mont, sans-serif; + --mat-table-footer-supporting-text-line-height: 20px; + --mat-table-footer-supporting-text-size: 14px; + --mat-table-footer-supporting-text-weight: 400; + --mat-table-footer-supporting-text-tracking: 0.0178571429em; +} +html { + --mat-progress-spinner-active-indicator-width: 4px; + --mat-progress-spinner-size: 48px; +} +html { + --mat-progress-spinner-active-indicator-color: #3f51b5; +} +.mat-accent { + --mat-progress-spinner-active-indicator-color: #ff4081; +} +.mat-warn { + --mat-progress-spinner-active-indicator-color: #f44336; +} +html { + --mat-badge-container-shape: 50%; + --mat-badge-container-size: unset; + --mat-badge-small-size-container-size: unset; + --mat-badge-large-size-container-size: unset; + --mat-badge-legacy-container-size: 22px; + --mat-badge-legacy-small-size-container-size: 16px; + --mat-badge-legacy-large-size-container-size: 28px; + --mat-badge-container-offset: -11px 0; + --mat-badge-small-size-container-offset: -8px 0; + --mat-badge-large-size-container-offset: -14px 0; + --mat-badge-container-overlap-offset: -11px; + --mat-badge-small-size-container-overlap-offset: -8px; + --mat-badge-large-size-container-overlap-offset: -14px; + --mat-badge-container-padding: 0; + --mat-badge-small-size-container-padding: 0; + --mat-badge-large-size-container-padding: 0; +} +html { + --mat-badge-background-color: #3f51b5; + --mat-badge-text-color: white; + --mat-badge-disabled-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-badge-disabled-state-text-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); +} +.mat-badge-accent { + --mat-badge-background-color: #ff4081; + --mat-badge-text-color: white; +} +.mat-badge-warn { + --mat-badge-background-color: #f44336; + --mat-badge-text-color: white; +} +html { + --mat-badge-text-font: Mont, sans-serif; + --mat-badge-line-height: 22px; + --mat-badge-text-size: 12px; + --mat-badge-text-weight: 600; + --mat-badge-small-size-text-size: 9px; + --mat-badge-small-size-line-height: 16px; + --mat-badge-large-size-text-size: 24px; + --mat-badge-large-size-line-height: 28px; +} +html { + --mat-bottom-sheet-container-shape: 4px; +} +html { + --mat-bottom-sheet-container-text-color: rgba(0, 0, 0, 0.87); + --mat-bottom-sheet-container-background-color: white; +} +html { + --mat-bottom-sheet-container-text-font: Mont, sans-serif; + --mat-bottom-sheet-container-text-line-height: 20px; + --mat-bottom-sheet-container-text-size: 14px; + --mat-bottom-sheet-container-text-tracking: 0.0178571429em; + --mat-bottom-sheet-container-text-weight: 400; +} +html { + --mat-button-toggle-focus-state-layer-opacity: 0.12; + --mat-button-toggle-hover-state-layer-opacity: 0.04; + --mat-button-toggle-legacy-focus-state-layer-opacity: 1; + --mat-button-toggle-legacy-height: 36px; + --mat-button-toggle-legacy-shape: 2px; + --mat-button-toggle-shape: 4px; +} +html { + --mat-button-toggle-background-color: white; + --mat-button-toggle-disabled-selected-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-disabled-selected-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-toggle-disabled-state-background-color: white; + --mat-button-toggle-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-toggle-divider-color: rgba(0, 0, 0, 0.12); + --mat-button-toggle-legacy-disabled-selected-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-legacy-disabled-state-background-color: white; + --mat-button-toggle-legacy-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-toggle-legacy-selected-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-legacy-selected-state-text-color: rgba(0, 0, 0, 0.87); + --mat-button-toggle-legacy-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-legacy-text-color: rgba(0, 0, 0, 0.87); + --mat-button-toggle-selected-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-selected-state-text-color: rgba(0, 0, 0, 0.87); + --mat-button-toggle-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-toggle-text-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-button-toggle-height: 48px; +} +html { + --mat-button-toggle-label-text-font: Mont, sans-serif; + --mat-button-toggle-label-text-line-height: 24px; + --mat-button-toggle-label-text-size: 16px; + --mat-button-toggle-label-text-tracking: 0.03125em; + --mat-button-toggle-label-text-weight: 400; + --mat-button-toggle-legacy-label-text-font: Mont, sans-serif; + --mat-button-toggle-legacy-label-text-line-height: 24px; + --mat-button-toggle-legacy-label-text-size: 16px; + --mat-button-toggle-legacy-label-text-tracking: 0.03125em; + --mat-button-toggle-legacy-label-text-weight: 400; +} +html { + --mat-datepicker-calendar-container-shape: 4px; + --mat-datepicker-calendar-container-touch-shape: 4px; + --mat-datepicker-calendar-container-elevation-shadow: + 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), + 0px 1px 10px 0px rgba(0, 0, 0, 0.12); + --mat-datepicker-calendar-container-touch-elevation-shadow: + 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), + 0px 9px 46px 8px rgba(0, 0, 0, 0.12); +} +html { + --mat-datepicker-calendar-date-in-range-state-background-color: color-mix( + in srgb, + #3f51b5 20%, + transparent + ); + --mat-datepicker-calendar-date-in-comparison-range-state-background-color: color-mix( + in srgb, + #ff4081 20%, + transparent + ); + --mat-datepicker-calendar-date-in-overlap-range-state-background-color: #a8dab5; + --mat-datepicker-calendar-date-in-overlap-range-selected-state-background-color: rgb( + 69.5241935484, + 163.4758064516, + 93.9516129032 + ); + --mat-datepicker-calendar-date-selected-state-text-color: white; + --mat-datepicker-calendar-date-selected-state-background-color: #3f51b5; + --mat-datepicker-calendar-date-selected-disabled-state-background-color: color-mix( + in srgb, + #3f51b5 38%, + transparent + ); + --mat-datepicker-calendar-date-today-selected-state-outline-color: white; + --mat-datepicker-calendar-date-focus-state-background-color: color-mix( + in srgb, + #3f51b5 12%, + transparent + ); + --mat-datepicker-calendar-date-hover-state-background-color: color-mix( + in srgb, + #3f51b5 4%, + transparent + ); + --mat-datepicker-toggle-active-state-icon-color: #3f51b5; + --mat-datepicker-toggle-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-body-label-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-period-button-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-period-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-navigation-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-header-divider-color: rgba(0, 0, 0, 0.12); + --mat-datepicker-calendar-header-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-disabled-state-outline-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-date-outline-color: transparent; + --mat-datepicker-calendar-date-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-preview-state-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-range-input-separator-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-range-input-disabled-state-separator-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-range-input-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-container-background-color: white; + --mat-datepicker-calendar-container-text-color: rgba(0, 0, 0, 0.87); +} +.mat-datepicker-content.mat-accent, +.mat-datepicker-toggle-active.mat-accent { + --mat-datepicker-calendar-date-in-range-state-background-color: color-mix( + in srgb, + #ff4081 20%, + transparent + ); + --mat-datepicker-calendar-date-in-comparison-range-state-background-color: color-mix( + in srgb, + #ff4081 20%, + transparent + ); + --mat-datepicker-calendar-date-in-overlap-range-state-background-color: #a8dab5; + --mat-datepicker-calendar-date-in-overlap-range-selected-state-background-color: rgb( + 69.5241935484, + 163.4758064516, + 93.9516129032 + ); + --mat-datepicker-calendar-date-selected-state-text-color: white; + --mat-datepicker-calendar-date-selected-state-background-color: #ff4081; + --mat-datepicker-calendar-date-selected-disabled-state-background-color: color-mix( + in srgb, + #ff4081 38%, + transparent + ); + --mat-datepicker-calendar-date-today-selected-state-outline-color: white; + --mat-datepicker-calendar-date-focus-state-background-color: color-mix( + in srgb, + #ff4081 12%, + transparent + ); + --mat-datepicker-calendar-date-hover-state-background-color: color-mix( + in srgb, + #ff4081 4%, + transparent + ); + --mat-datepicker-toggle-active-state-icon-color: #ff4081; + --mat-datepicker-toggle-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-body-label-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-period-button-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-period-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-navigation-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-header-divider-color: rgba(0, 0, 0, 0.12); + --mat-datepicker-calendar-header-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-disabled-state-outline-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-date-outline-color: transparent; + --mat-datepicker-calendar-date-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-preview-state-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-range-input-separator-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-range-input-disabled-state-separator-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-range-input-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-container-background-color: white; + --mat-datepicker-calendar-container-text-color: rgba(0, 0, 0, 0.87); +} +.mat-datepicker-content.mat-warn, +.mat-datepicker-toggle-active.mat-warn { + --mat-datepicker-calendar-date-in-range-state-background-color: color-mix( + in srgb, + #f44336 20%, + transparent + ); + --mat-datepicker-calendar-date-in-comparison-range-state-background-color: color-mix( + in srgb, + #ff4081 20%, + transparent + ); + --mat-datepicker-calendar-date-in-overlap-range-state-background-color: #a8dab5; + --mat-datepicker-calendar-date-in-overlap-range-selected-state-background-color: rgb( + 69.5241935484, + 163.4758064516, + 93.9516129032 + ); + --mat-datepicker-calendar-date-selected-state-text-color: white; + --mat-datepicker-calendar-date-selected-state-background-color: #f44336; + --mat-datepicker-calendar-date-selected-disabled-state-background-color: color-mix( + in srgb, + #f44336 38%, + transparent + ); + --mat-datepicker-calendar-date-today-selected-state-outline-color: white; + --mat-datepicker-calendar-date-focus-state-background-color: color-mix( + in srgb, + #f44336 12%, + transparent + ); + --mat-datepicker-calendar-date-hover-state-background-color: color-mix( + in srgb, + #f44336 4%, + transparent + ); + --mat-datepicker-toggle-active-state-icon-color: #f44336; + --mat-datepicker-toggle-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-body-label-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-period-button-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-period-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-navigation-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-header-divider-color: rgba(0, 0, 0, 0.12); + --mat-datepicker-calendar-header-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-disabled-state-outline-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-date-outline-color: transparent; + --mat-datepicker-calendar-date-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-preview-state-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-range-input-separator-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-range-input-disabled-state-separator-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-range-input-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-container-background-color: white; + --mat-datepicker-calendar-container-text-color: rgba(0, 0, 0, 0.87); +} +.mat-calendar-controls { + --mat-icon-button-touch-target-display: none; +} +.mat-calendar-controls .mat-mdc-icon-button.mat-mdc-button-base { + --mdc-icon-button-state-layer-size: 40px; + --mat-icon-button-state-layer-size: 40px; + width: var(--mat-icon-button-state-layer-size); + height: var(--mat-icon-button-state-layer-size); + padding: 8px; +} +html { + --mat-datepicker-calendar-text-font: Mont, sans-serif; + --mat-datepicker-calendar-text-size: 13px; + --mat-datepicker-calendar-body-label-text-size: 14px; + --mat-datepicker-calendar-body-label-text-weight: 500; + --mat-datepicker-calendar-period-button-text-size: 14px; + --mat-datepicker-calendar-period-button-text-weight: 500; + --mat-datepicker-calendar-header-text-size: 11px; + --mat-datepicker-calendar-header-text-weight: 400; +} +html { + --mat-divider-width: 1px; +} +html { + --mat-divider-color: rgba(0, 0, 0, 0.12); +} +html { + --mat-expansion-container-shape: 4px; + --mat-expansion-container-elevation-shadow: + 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + --mat-expansion-legacy-header-indicator-display: inline-block; + --mat-expansion-header-indicator-display: none; +} +html { + --mat-expansion-container-background-color: white; + --mat-expansion-container-text-color: rgba(0, 0, 0, 0.87); + --mat-expansion-actions-divider-color: rgba(0, 0, 0, 0.12); + --mat-expansion-header-hover-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 4%, + transparent + ); + --mat-expansion-header-focus-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-expansion-header-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-expansion-header-text-color: rgba(0, 0, 0, 0.87); + --mat-expansion-header-description-color: rgba(0, 0, 0, 0.54); + --mat-expansion-header-indicator-color: rgba(0, 0, 0, 0.54); +} +html { + --mat-expansion-header-collapsed-state-height: 48px; + --mat-expansion-header-expanded-state-height: 64px; +} +html { + --mat-expansion-header-text-font: Mont, sans-serif; + --mat-expansion-header-text-size: 14px; + --mat-expansion-header-text-weight: 500; + --mat-expansion-header-text-line-height: inherit; + --mat-expansion-header-text-tracking: inherit; + --mat-expansion-container-text-font: Mont, sans-serif; + --mat-expansion-container-text-line-height: 20px; + --mat-expansion-container-text-size: 14px; + --mat-expansion-container-text-tracking: 0.0178571429em; + --mat-expansion-container-text-weight: 400; +} +html { + --mat-grid-list-tile-header-primary-text-size: 14px; + --mat-grid-list-tile-header-secondary-text-size: 12px; + --mat-grid-list-tile-footer-primary-text-size: 14px; + --mat-grid-list-tile-footer-secondary-text-size: 12px; +} +html { + --mat-icon-color: inherit; +} +.mat-icon.mat-primary { + --mat-icon-color: #3f51b5; +} +.mat-icon.mat-accent { + --mat-icon-color: #ff4081; +} +.mat-icon.mat-warn { + --mat-icon-color: #f44336; +} +html { + --mat-sidenav-container-shape: 0; + --mat-sidenav-container-elevation-shadow: + 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), + 0px 6px 30px 5px rgba(0, 0, 0, 0.12); + --mat-sidenav-container-width: auto; +} +html { + --mat-sidenav-container-divider-color: rgba(0, 0, 0, 0.12); + --mat-sidenav-container-background-color: white; + --mat-sidenav-container-text-color: rgba(0, 0, 0, 0.87); + --mat-sidenav-content-background-color: #fafafa; + --mat-sidenav-content-text-color: rgba(0, 0, 0, 0.87); + --mat-sidenav-scrim-color: rgba(0, 0, 0, 0.6); +} +html { + --mat-stepper-header-focus-state-layer-shape: 0; + --mat-stepper-header-hover-state-layer-shape: 0; +} +html { + --mat-stepper-header-icon-foreground-color: white; + --mat-stepper-header-selected-state-icon-background-color: #3f51b5; + --mat-stepper-header-selected-state-icon-foreground-color: white; + --mat-stepper-header-done-state-icon-background-color: #3f51b5; + --mat-stepper-header-done-state-icon-foreground-color: white; + --mat-stepper-header-edit-state-icon-background-color: #3f51b5; + --mat-stepper-header-edit-state-icon-foreground-color: white; + --mat-stepper-container-color: white; + --mat-stepper-line-color: rgba(0, 0, 0, 0.12); + --mat-stepper-header-hover-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 4%, + transparent + ); + --mat-stepper-header-focus-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-stepper-header-label-text-color: rgba(0, 0, 0, 0.54); + --mat-stepper-header-optional-label-text-color: rgba(0, 0, 0, 0.54); + --mat-stepper-header-selected-state-label-text-color: rgba(0, 0, 0, 0.87); + --mat-stepper-header-error-state-label-text-color: #f44336; + --mat-stepper-header-icon-background-color: rgba(0, 0, 0, 0.54); + --mat-stepper-header-error-state-icon-foreground-color: #f44336; + --mat-stepper-header-error-state-icon-background-color: transparent; +} +.mat-step-header.mat-accent { + --mat-stepper-header-icon-foreground-color: white; + --mat-stepper-header-selected-state-icon-background-color: #ff4081; + --mat-stepper-header-selected-state-icon-foreground-color: white; + --mat-stepper-header-done-state-icon-background-color: #ff4081; + --mat-stepper-header-done-state-icon-foreground-color: white; + --mat-stepper-header-edit-state-icon-background-color: #ff4081; + --mat-stepper-header-edit-state-icon-foreground-color: white; +} +.mat-step-header.mat-warn { + --mat-stepper-header-icon-foreground-color: white; + --mat-stepper-header-selected-state-icon-background-color: #f44336; + --mat-stepper-header-selected-state-icon-foreground-color: white; + --mat-stepper-header-done-state-icon-background-color: #f44336; + --mat-stepper-header-done-state-icon-foreground-color: white; + --mat-stepper-header-edit-state-icon-background-color: #f44336; + --mat-stepper-header-edit-state-icon-foreground-color: white; +} +html { + --mat-stepper-header-height: 72px; +} +html { + --mat-stepper-container-text-font: Mont, sans-serif; + --mat-stepper-header-label-text-font: Mont, sans-serif; + --mat-stepper-header-label-text-size: 14px; + --mat-stepper-header-label-text-weight: 400; + --mat-stepper-header-error-state-label-text-size: 16px; + --mat-stepper-header-selected-state-label-text-size: 16px; + --mat-stepper-header-selected-state-label-text-weight: 400; +} +html { + --mat-sort-arrow-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-toolbar-container-background-color: white; + --mat-toolbar-container-text-color: rgba(0, 0, 0, 0.87); +} +.mat-toolbar.mat-primary { + --mat-toolbar-container-background-color: #3f51b5; + --mat-toolbar-container-text-color: white; +} +.mat-toolbar.mat-accent { + --mat-toolbar-container-background-color: #ff4081; + --mat-toolbar-container-text-color: white; +} +.mat-toolbar.mat-warn { + --mat-toolbar-container-background-color: #f44336; + --mat-toolbar-container-text-color: white; +} +html { + --mat-toolbar-standard-height: 64px; + --mat-toolbar-mobile-height: 56px; +} +html { + --mat-toolbar-title-text-font: Mont, sans-serif; + --mat-toolbar-title-text-line-height: 32px; + --mat-toolbar-title-text-size: 20px; + --mat-toolbar-title-text-tracking: 0.0125em; + --mat-toolbar-title-text-weight: 500; +} +html { + --mat-tree-container-background-color: white; + --mat-tree-node-text-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-tree-node-min-height: 48px; +} +html { + --mat-tree-node-text-font: Mont, sans-serif; + --mat-tree-node-text-size: 14px; + --mat-tree-node-text-weight: 400; +} +html { + --mat-timepicker-container-shape: 4px; + --mat-timepicker-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} +html { + --mat-timepicker-container-background-color: white; +} +@media screen and (max-width: 999px) { + app-root, + app-input, + app-textarea, + app-select, + app-autocomplete-input, + app-search, + app-upload-file, + app-file-item, + app-file-upload-item, + app-button, + ui-button { + min-width: 0; + max-width: 100%; + } +} +@media screen and (max-width: 999px) { + h1, + h2, + h3, + h4, + p, + label, + .error { + overflow-wrap: anywhere; + } +} +@media screen and (max-width: 999px) { + video, + iframe, + canvas { + max-width: 100%; + } +} +@media screen and (max-width: 999px) { + .cdk-overlay-container { + z-index: 10010; + } +} +@media screen and (max-width: 999px) { + .responsive-header, + .responsive-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + min-width: 0; + } +} +@media screen and (max-width: 999px) { + .responsive-table { + min-width: 0; + max-width: 100%; + overflow-x: auto; + overscroll-behavior-inline: contain; + } +} +@media screen and (max-width: 999px) { + .responsive-dialog-header, + .responsive-dialog-actions { + position: sticky; + z-index: 2; + background: inherit; + } +} +@media screen and (max-width: 999px) { + .responsive-dialog-header { + top: 0; + } +} +@media screen and (max-width: 999px) { + .responsive-dialog-actions { + bottom: 0; + padding-bottom: env(safe-area-inset-bottom, 0); + } +} +@media screen and (max-width: 999px) and (max-width: 749px) { + html { + scrollbar-gutter: auto; + } + .text-body-10 { + font-size: 12px; + line-height: 18px; + } + .text-body-12 { + font-size: 14px; + line-height: 20px; + } + input:not([type="checkbox"], [type="radio"]), + textarea, + select { + min-width: 0; + max-width: 100%; + font-size: 16px; + } + .responsive-header, + .responsive-actions { + flex-direction: column; + align-items: stretch; + } + .cdk-overlay-pane { + max-width: calc(100vw - 24px); + } + .mat-datepicker-content { + max-width: 100%; + max-height: calc(100dvh - 24px); + overflow: auto; + } +} +@media screen and (max-width: 999px) and (max-width: 359px) { + :root { + --page-gutter: 12px; + } +} +:root { + --ui-accent: var(--accent); + --accent-hover: var(--accent-medium); + --ui-text: var(--black); + --ui-text-muted: var(--grey-for-text); + --ui-surface: var(--light-white); + --ui-surface-subtle: var(--white); + --ui-border: var(--gray); + --ui-border-subtle: var(--medium-grey-for-outline); + --ui-danger: var(--red); + --ui-success: var(--green); + --ui-warning: var(--gold); + --ui-radius-control: var(--rounded-xxl); + --ui-radius-card: var(--rounded-xl); + --ui-radius-dialog: var(--rounded-xl); + --ui-control-height: 44px; + --ui-compact-sidebar-width: 157px; + --ui-mobile-entity-card-width: 260px; + --ui-control-padding: 12px 16px; + --ui-card-padding: 16px; + --ui-dialog-padding: 24px; + --ui-dialog-sm: 400px; + --ui-dialog-md: 560px; + --ui-dialog-lg: 800px; + --ui-space-xs: 4px; + --ui-space-sm: 8px; + --ui-space-md: 16px; + --ui-space-lg: 24px; + --ui-focus-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); + --ui-error-ring: 0 0 0 3px color-mix(in srgb, var(--red) 25%, transparent); + --ui-card-shadow: none; + --ui-card-hover-shadow: 0 4px 12px color-mix(in srgb, var(--black) 8%, transparent); + --ui-backdrop: color-mix(in srgb, var(--black) 50%, transparent); + --ui-font-h1: 24px; + --ui-font-h2: 18px; + --ui-font-h3: 16px; + --ui-font-body: 14px; + --ui-font-small: 12px; + --ui-font-caption: 12px; + --ui-disabled-opacity: 0.5; + --ui-nav-gradient-end: #4d3780; + --ui-board-surface: #d9d9d9; + --ui-error-text: #b42318; + --ui-crop-surface: #f8f9fa; + --ui-message-surface: #f7f3ff; + --ui-media-button: #81818180; + --ui-media-button-active: #37373780; + --ui-media-backdrop: #333b; + --ui-overlay-shadow-color: #0002; + --ui-scrim: rgb(51 46 45 / 50%); + --ui-preview-scrim: rgb(0 0 0 / 60%); + --ui-crop-scrim: rgb(0 0 0 / 50%); + --ui-crop-border: rgb(255 255 255 / 50%); + --ui-success-glow: rgb(0 200 147 / 51%); + --ui-success-glow-end: rgb(0 200 147 / 0%); + --ui-accent-tint-subtle: rgb(109 40 255 / 6%); + --ui-accent-tint-hover: rgb(109 40 255 / 12%); + --ui-accent-tint-chart: rgb(109 40 255 / 8%); + --ui-accent-gradient-end: rgb(138 99 230 / 28%); + --ui-panel-shadow: 0 2px 8px rgb(51 51 51 / 8%); + --ui-chart-shadow: 0 6px 22px rgb(51 51 51 / 4%); + --ui-tooltip-shadow: 0 4px 12px rgb(0 0 0 / 15%); + --ui-crop-shadow: 0 4px 12px rgb(0 0 0 / 10%); + --ui-crop-handle-shadow: 0 2px 4px rgb(0 0 0 / 20%); + --ui-message-shadow: 5px 5px 25px rgb(159 159 159 / 15%); + --ui-header-shadow: 5px 5px 50px rgb(196 196 196 / 15%); + --ui-composer-shadow: 4px -4px 4px rgb(51 51 51 / 20%); +} +@media (max-width: 749px) { + :root { + --ui-dialog-padding: 16px; + --ui-font-h1: 18px; + } +} +@media screen and (min-width: 1000px) { + :where(button.ui-button) { + padding: 0; + font: inherit; + color: inherit; + text-align: inherit; + cursor: pointer; + background: transparent; + border: 0; + } +} +@media screen and (max-width: 999px) { + button.ui-button.ui-button { + --button-color: var(--ui-accent); + --button-hover: var(--accent-medium); + --button-text: var(--light-white); + min-width: 0; + max-width: 100%; + min-height: var(--ui-control-height); + padding: 10px 24px; + font: inherit; + font-size: var(--ui-font-small); + font-weight: 600; + line-height: 1.4; + color: var(--button-text); + text-align: center; + text-decoration: none; + overflow-wrap: anywhere; + white-space: normal; + cursor: pointer; + background: var(--button-color); + border: 1px solid var(--button-color); + border-radius: var(--ui-radius-control); + transition: + background-color 0.2s, + border-color 0.2s, + box-shadow 0.2s; + } + button.ui-button.ui-button:hover:not(:disabled) { + text-decoration: none; + background: var(--button-hover); + border-color: var(--button-hover); + } + button.ui-button.ui-button:active:not(:disabled) { + background: var(--accent-dark); + } + button.ui-button.ui-button:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; + } + button.ui-button.ui-button:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); + } + button.ui-button.ui-button--secondary, + button.ui-button.ui-button--outline { + color: var(--button-color); + background: transparent; + } + button.ui-button.ui-button--secondary:hover:not(:disabled), + button.ui-button.ui-button--secondary:active:not(:disabled), + button.ui-button.ui-button--outline:hover:not(:disabled), + button.ui-button.ui-button--outline:active:not(:disabled) { + color: var(--button-color); + background: var(--accent-light); + } + button.ui-button.ui-button--danger, + button.ui-button.ui-button--red { + --button-color: var(--ui-danger); + --button-hover: var(--red-dark); + } + button.ui-button.ui-button--danger:active:not(:disabled), + button.ui-button.ui-button--red:active:not(:disabled) { + background: var(--red-dark); + } + button.ui-button.ui-button--danger.button--outline:hover:not(:disabled), + button.ui-button.ui-button--red.button--outline:hover:not(:disabled) { + background: var(--red-light); + } + button.ui-button.ui-button--grey { + --button-color: var(--grey-button); + --button-hover: var(--gray); + --button-text: var(--black); + } + button.ui-button.ui-button--green { + --button-color: var(--green); + --button-hover: var(--green-dark); + } + button.ui-button.ui-button--gold { + --button-color: var(--gold); + --button-hover: var(--gold-dark); + } + button.ui-button.ui-button--white { + --button-color: var(--light-white); + --button-hover: var(--white); + --button-text: var(--black); + } + button.ui-button.ui-button--gradient { + background: var(--gradient); + } + button.ui-button.ui-button--no-border { + border-color: transparent; + } + button.ui-button.ui-button--medium { + min-width: min(157px, 100%); + } + button.ui-button.ui-button--big { + width: 100%; + } + button.ui-button.ui-button--extra-small { + padding-inline: 12px; + } + button.ui-button.ui-button--icon, + button.ui-button.ui-button--text { + color: var(--ui-accent); + background: transparent; + border-color: transparent; + } + button.ui-button.ui-button--icon:hover:not(:disabled), + button.ui-button.ui-button--icon:active:not(:disabled), + button.ui-button.ui-button--text:hover:not(:disabled), + button.ui-button.ui-button--text:active:not(:disabled) { + color: var(--accent-dark); + background: var(--accent-light); + border-color: transparent; + } + button.ui-button.ui-button--icon { + min-width: 44px; + padding: 10px; + } + button.ui-button.ui-button--text { + padding-inline: 12px; + } +} +@media screen and (max-width: 999px) and (max-width: 749px) { + button.ui-button.ui-button { + padding-inline: 16px; + } + button.ui-button.ui-button--icon { + padding: 9px; + } + button.ui-button.ui-button--text { + padding-inline: 12px; + } +} +@media screen and (max-width: 999px) { + .ui-button { + display: inline-flex; + gap: var(--ui-space-sm); + align-items: center; + justify-content: center; + } +} +@media screen and (max-width: 999px) { + button.ui-button[role="radio"] { + justify-content: space-between; + width: 100%; + padding: var(--ui-control-padding); + color: var(--ui-text); + text-align: left; + background: var(--ui-surface-subtle); + border-color: var(--ui-border-subtle); + border-radius: var(--ui-radius-card); + } + button.ui-button[role="radio"][aria-checked="true"] { + background: var(--accent-light); + border-color: var(--ui-accent); + } +} +@media screen and (max-width: 999px) { + .ui-field { + box-sizing: border-box; + min-width: 0; + max-width: 100%; + min-height: var(--ui-control-height); + padding: var(--ui-control-padding); + font: inherit; + font-size: var(--ui-font-body); + line-height: 1.4; + color: var(--ui-text); + background: var(--ui-surface-subtle); + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-control); + outline: none; + transition: + border-color 0.2s, + box-shadow 0.2s; + } + .ui-field:is(select) { + padding-right: 44px; + appearance: none; + background-image: url(/assets/icons/svg/chevron-down.svg); + background-repeat: no-repeat; + background-position: right 16px center; + background-size: 14px; + } + .ui-field::placeholder { + color: var(--dark-grey); + } + .ui-field:hover:not(:disabled) { + border-color: var(--dark-grey); + } + .ui-field:focus-visible, + .ui-field:focus-within, + .ui-field--open { + border-color: var(--ui-accent); + box-shadow: var(--ui-focus-ring); + } + .ui-field--error, + .ui-field--overflow, + .ui-field[aria-invalid="true"] { + border: 1px solid var(--ui-danger); + } + .ui-field--error:focus, + .ui-field--error:focus-within, + .ui-field--overflow:focus, + .ui-field--overflow:focus-within, + .ui-field[aria-invalid="true"]:focus, + .ui-field[aria-invalid="true"]:focus-within { + box-shadow: var(--ui-error-ring); + } + .ui-field:disabled, + .ui-field--disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); + } +} +@media screen and (max-width: 999px) and (max-width: 749px) { + .ui-field { + font-size: 16px; + } +} +@media screen and (max-width: 999px) { + input.ui-choice.ui-choice { + min-height: 18px; + padding: 0; + background: transparent; + flex-shrink: 0; + width: 18px; + height: 18px; + margin: 0; + accent-color: var(--ui-accent); + cursor: pointer; + appearance: auto; + } + input.ui-choice.ui-choice:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; + } + input.ui-choice.ui-choice:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); + } +} +@media screen and (max-width: 999px) { + .ui-card { + min-width: 0; + max-width: 100%; + padding: var(--ui-card-padding); + color: var(--ui-text); + background: var(--ui-surface); + border: 1px solid var(--ui-border-subtle); + border-radius: var(--ui-radius-card); + box-shadow: var(--ui-card-shadow); + } + .ui-card--interactive { + transition: + box-shadow 0.2s, + border-color 0.2s; + } + .ui-card--interactive:hover { + border-color: var(--ui-accent); + box-shadow: var(--ui-card-hover-shadow); + } + .ui-card:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; + } +} +@media screen and (max-width: 999px) { + .ui-form { + min-width: 0; + } +} +@media screen and (max-width: 999px) { + .ui-form label.field-label, + label.field-label { + margin-bottom: var(--ui-space-sm); + font-size: var(--ui-font-small); + line-height: 1.5; + color: var(--ui-text); + } +} +@media screen and (max-width: 999px) { + .ui-form .error, + .error { + font-size: var(--ui-font-small); + line-height: 1.5; + color: var(--ui-danger); + } +} +@media screen and (max-width: 999px) { + .ui-filters { + display: flex; + flex-direction: column; + gap: var(--ui-space-md); + min-width: 0; + } +} +@media screen and (max-width: 999px) { + .ui-filters__header { + display: flex; + flex-wrap: wrap; + gap: var(--ui-space-xs); + align-items: center; + justify-content: space-between; + } + .ui-filters__header > p, + .ui-filters__header > .ui-button { + flex-shrink: 0; + } +} +@media screen and (max-width: 999px) { + .ui-dialog-body { + min-width: 0; + font-size: var(--ui-font-body); + line-height: 1.5; + overflow-wrap: anywhere; + } +} +@media screen and (max-width: 999px) { + .ui-dialog-footer { + display: flex; + flex-wrap: wrap; + gap: var(--ui-space-sm); + align-items: center; + justify-content: flex-end; + padding-top: var(--ui-space-md); + margin-top: var(--ui-space-md); + } +} +@media screen and (max-width: 999px) { + .ui-alert { + padding: var(--ui-space-md); + color: var(--ui-text); + background: var(--accent-light); + border-radius: var(--ui-radius-card); + } +} +@media screen and (max-width: 999px) { + .ui-alert--error { + color: var(--red-dark); + background: var(--red-light); + } +} +@media screen and (max-width: 999px) { + .ui-alert--success { + color: var(--green-dark); + background: var(--green-light); + } +} +@media screen and (max-width: 999px) { + .ui-alert--warning { + color: var(--gold-dark); + background: var(--gold-light); + } +} +@media screen and (max-width: 999px) { + .ui-drawer { + min-width: 0; + max-width: 100vw; + overscroll-behavior: contain; + } +} +@media screen and (max-width: 999px) { + table.ui-table { + width: 100%; + font-size: var(--ui-font-small); + line-height: 1.5; + color: var(--ui-text); + border-collapse: collapse; + } + table.ui-table th { + padding: 12px 16px; + font-weight: 600; + text-align: left; + background: var(--ui-surface-subtle); + border-bottom: 1px solid var(--ui-border); + } + table.ui-table td { + padding: 12px 16px; + border-bottom: 1px solid var(--ui-border-subtle); + } + table.ui-table tbody tr:hover { + background: var(--accent-light); + } +} +@media screen and (max-width: 999px) { + .ui-h1 { + font-size: var(--ui-font-h1); + font-weight: 600; + line-height: 1.3; + } +} +@media screen and (max-width: 999px) { + .ui-h2 { + font-size: var(--ui-font-h2); + font-weight: 600; + line-height: 1.4; + } +} +@media screen and (max-width: 999px) { + .ui-h3 { + font-size: var(--ui-font-h3); + font-weight: 600; + line-height: 1.4; + } +} +@media screen and (max-width: 999px) { + .ui-body { + font-size: var(--ui-font-body); + line-height: 1.5; + } +} +@media screen and (max-width: 999px) { + .ui-small { + font-size: var(--ui-font-small); + line-height: 1.5; + } +} +@media screen and (max-width: 999px) { + .ui-caption { + font-size: var(--ui-font-caption); + line-height: 1.5; + color: var(--ui-text-muted); + } +} + +/* angular:styles/global:styles */ +/*# sourceMappingURL=styles-AOKJA37L.css.map */ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/icons.js b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/icons.js new file mode 100644 index 000000000..4ebb11781 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/icons.js @@ -0,0 +1,22 @@ +/** @format */ + +window.prototypeIcons = { + person: + '\r\n\r\n\r\n', + folder: + '\r\n \r\n\r\n', + download: + '\r\n \r\n\r\n', + filter: + '\r\n \r\n\r\n', + search: + '\r\n \r\n\r\n', + reload: + '\r\n \r\n \r\n\r\n', + hint: '\r\n\r\n\r\n', + "analytics-team": + '\r\n \r\n \r\n \r\n \r\n \r\n\r\n', + "analytics-submitted": + '\r\n \r\n \r\n\r\n', + pen: '\r\n\r\n\r\n', +}; diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/index.html b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/index.html new file mode 100644 index 000000000..794773b4e --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/index.html @@ -0,0 +1,89 @@ + + + + + + + + Решения программы — интерфейс организатора + + + + + + + +
+ Макет организатора / вымышленные данные +
+ + + + +
+
+
+ +
+
+ PROCOLLABУправление программой +
+
+

Управление программой

+

Кейс-чемпионат PROCOLLAB

+
+ +
+
+
+
+ +
+ + + + + + diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/results-reference.css b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/results-reference.css new file mode 100644 index 000000000..b788591df --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/results-reference.css @@ -0,0 +1,681 @@ +/** @format */ +/** @format */ +.prototype-results { + /** @format */ +} +.prototype-results :host { + display: block; + min-width: 0; +} +.prototype-results .ui-button.ui-button { + --button-color: var(--ui-accent); + --button-hover: var(--accent-medium); + --button-text: var(--light-white); + display: inline-flex; + gap: var(--ui-space-sm); + align-items: center; + justify-content: center; + min-width: 0; + max-width: 100%; + min-height: var(--ui-control-height); + padding: 10px 24px; + font: inherit; + font-size: var(--ui-font-small); + font-weight: 600; + line-height: 1.4; + color: var(--button-text); + text-align: center; + text-decoration: none; + overflow-wrap: anywhere; + white-space: normal; + cursor: pointer; + background: var(--button-color); + border: 1px solid var(--button-color); + border-radius: var(--ui-radius-control); + transition: + background-color 0.2s, + border-color 0.2s, + box-shadow 0.2s; + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; + white-space: nowrap; +} +.prototype-results .ui-button.ui-button:hover:not(:disabled) { + text-decoration: none; + background: var(--button-hover); + border-color: var(--button-hover); +} +.prototype-results .ui-button.ui-button:active:not(:disabled) { + background: var(--accent-dark); +} +.prototype-results .ui-button.ui-button:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; +} +.prototype-results .ui-button.ui-button:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); +} +.prototype-results .ui-button.ui-button--secondary, +.prototype-results .ui-button.ui-button--outline { + color: var(--button-color); + background: transparent; +} +.prototype-results .ui-button.ui-button--secondary:hover:not(:disabled), +.prototype-results .ui-button.ui-button--secondary:active:not(:disabled), +.prototype-results .ui-button.ui-button--outline:hover:not(:disabled), +.prototype-results .ui-button.ui-button--outline:active:not(:disabled) { + color: var(--button-color); + background: var(--accent-light); +} +.prototype-results .ui-button.ui-button--danger, +.prototype-results .ui-button.ui-button--red { + --button-color: var(--ui-danger); + --button-hover: var(--red-dark); +} +.prototype-results .ui-button.ui-button--danger:active:not(:disabled), +.prototype-results .ui-button.ui-button--red:active:not(:disabled) { + background: var(--red-dark); +} +.prototype-results .ui-button.ui-button--danger.button--outline:hover:not(:disabled), +.prototype-results .ui-button.ui-button--red.button--outline:hover:not(:disabled) { + background: var(--red-light); +} +.prototype-results .ui-button.ui-button--grey { + --button-color: var(--grey-button); + --button-hover: var(--gray); + --button-text: var(--black); +} +.prototype-results .ui-button.ui-button--green { + --button-color: var(--green); + --button-hover: var(--green-dark); +} +.prototype-results .ui-button.ui-button--gold { + --button-color: var(--gold); + --button-hover: var(--gold-dark); +} +.prototype-results .ui-button.ui-button--white { + --button-color: var(--light-white); + --button-hover: var(--white); + --button-text: var(--black); +} +.prototype-results .ui-button.ui-button--gradient { + background: var(--gradient); +} +.prototype-results .ui-button.ui-button--no-border { + border-color: transparent; +} +.prototype-results .ui-button.ui-button--medium { + min-width: min(157px, 100%); +} +.prototype-results .ui-button.ui-button--big { + width: 100%; +} +.prototype-results .ui-button.ui-button--extra-small { + padding-inline: 12px; +} +.prototype-results .ui-button.ui-button--icon, +.prototype-results .ui-button.ui-button--text { + color: var(--ui-accent); + background: transparent; + border-color: transparent; +} +.prototype-results .ui-button.ui-button--icon:hover:not(:disabled), +.prototype-results .ui-button.ui-button--icon:active:not(:disabled), +.prototype-results .ui-button.ui-button--text:hover:not(:disabled), +.prototype-results .ui-button.ui-button--text:active:not(:disabled) { + color: var(--accent-dark); + background: var(--accent-light); + border-color: transparent; +} +.prototype-results .ui-button.ui-button--icon { + min-width: 44px; + padding: 10px; +} +.prototype-results .ui-button.ui-button--text { + padding-inline: 12px; +} +@media (max-width: 749px) { + .prototype-results .ui-button.ui-button { + padding-inline: 16px; + } + .prototype-results .ui-button.ui-button--icon { + padding: 9px; + } + .prototype-results .ui-button.ui-button--text { + padding-inline: 12px; + } +} +.prototype-results .ui-button.ui-button.ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-results .ui-button.ui-button { + padding-inline: 16px; + } + .prototype-results .ui-button.ui-button::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-results .ui-button.ui-button.ui-button--icon::after { + inset: -6px; + } +} +.prototype-results .ui-field { + box-sizing: border-box; + min-width: 0; + max-width: 100%; + min-height: var(--ui-control-height); + padding: var(--ui-control-padding); + font: inherit; + font-size: var(--ui-font-body); + line-height: 1.4; + color: var(--ui-text); + background: var(--ui-surface-subtle); + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-control); + outline: none; + transition: + border-color 0.2s, + box-shadow 0.2s; + width: 100%; + font-size: 12px; +} +.prototype-results .ui-field:is(select) { + padding-right: 44px; + appearance: none; + background-image: url("/assets/icons/svg/chevron-down.svg"); + background-repeat: no-repeat; + background-position: right 16px center; + background-size: 14px; +} +.prototype-results .ui-field::placeholder { + color: var(--dark-grey); +} +.prototype-results .ui-field:hover:not(:disabled) { + border-color: var(--dark-grey); +} +.prototype-results .ui-field:focus-visible, +.prototype-results .ui-field:focus-within, +.prototype-results .ui-field--open { + border-color: var(--ui-accent); + box-shadow: var(--ui-focus-ring); +} +.prototype-results .ui-field--error, +.prototype-results .ui-field--overflow, +.prototype-results .ui-field[aria-invalid="true"] { + border: 1px solid var(--ui-danger); +} +.prototype-results .ui-field--error:focus, +.prototype-results .ui-field--error:focus-within, +.prototype-results .ui-field--overflow:focus, +.prototype-results .ui-field--overflow:focus-within, +.prototype-results .ui-field[aria-invalid="true"]:focus, +.prototype-results .ui-field[aria-invalid="true"]:focus-within { + box-shadow: var(--ui-error-ring); +} +.prototype-results .ui-field:disabled, +.prototype-results .ui-field--disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); +} +@media (max-width: 749px) { + .prototype-results .ui-field { + font-size: 16px; + } +} +.prototype-results .ui-choice { + flex-shrink: 0; + width: 18px; + height: 18px; + margin: 0; + accent-color: var(--ui-accent); + cursor: pointer; + flex-shrink: 0; +} +.prototype-results .ui-choice:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; +} +.prototype-results .ui-choice:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); +} +.prototype-results .results, +.prototype-results .results__dialog { + display: grid; + gap: 16px; + min-width: 0; + font-size: 12px; + line-height: 1.5; + color: var(--black); +} +.prototype-results .results { + padding: 24px; + background: var(--light-white); + border-radius: 15px; +} +.prototype-results h2, +.prototype-results h3, +.prototype-results p { + margin: 0; +} +.prototype-results h2 { + font-size: 16px; + font-weight: 600; + line-height: 24px; +} +.prototype-results h3 { + font-size: 12px; + font-weight: 600; + line-height: 20px; +} +.prototype-results .results__muted, +.prototype-results small { + color: var(--grey-for-text); +} +.prototype-results .results__heading, +.prototype-results .results__heading-actions, +.prototype-results .results__toolbar, +.prototype-results .results__toolbar-actions, +.prototype-results .results__summary, +.prototype-results .results__filter-actions, +.prototype-results .results__pagination { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + min-width: 0; +} +.prototype-results .results__heading-actions { + flex-shrink: 0; + gap: 8px; +} +.prototype-results .results__icon.ui-button.ui-button, +.prototype-results .results__close.ui-button.ui-button { + width: 32px; + min-width: 32px; + height: 32px; + padding: 5px; + color: var(--accent); + background: var(--light-white); +} +.prototype-results .results__export.ui-button.ui-button { + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-results .results__formula-mobile.ui-button { + display: none; +} +.prototype-results .results__search { + display: block; + flex: 1; + min-width: 0; +} +.prototype-results .results__toolbar-actions { + flex-shrink: 0; +} +.prototype-results .results__toolbar-actions .ui-button { + gap: 8px; +} +.prototype-results .results__summary { + flex-wrap: wrap; + gap: 4px 12px; + color: var(--grey-for-text); +} +.prototype-results .results__summary time { + font-size: 11px; +} +.prototype-results .results__attention { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + font: inherit; + color: var(--black); + text-align: left; + cursor: pointer; + background: var(--gold-light); + border: 1px solid transparent; + border-radius: 8px; +} +.prototype-results .results__attention > span:last-child { + color: var(--accent); +} +.prototype-results .results__attention--active { + border-color: var(--gold); +} +.prototype-results .results__attention:focus-visible, +.prototype-results .results__row:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-results .results__group { + position: relative; + min-width: 0; +} +.prototype-results .results__group > h3 { + margin-bottom: 12px; +} +.prototype-results .results__columns, +.prototype-results .results__row { + display: grid; + grid-template-columns: 40px minmax(0, 1fr) 180px 128px 24px; + gap: 16px; + align-items: center; +} +.prototype-results .results__columns { + padding-block: 8px; + color: var(--grey-for-text); +} +.prototype-results .results__row { + width: 100%; + min-height: 72px; + padding: 12px 0; + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + background: transparent; + border: 0; + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.prototype-results .results__row:hover, +.prototype-results .results__row--selected { + background: var(--light-gray); +} +.prototype-results .results__name { + min-width: 0; + overflow-wrap: anywhere; +} +.prototype-results .results__count { + display: grid; + gap: 4px; + justify-items: start; + font-size: 11px; +} +.prototype-results .results__count .results__badge { + max-width: 100%; +} +.prototype-results .results__score { + display: grid; + gap: 4px; + font-variant-numeric: tabular-nums; + text-align: right; +} +.prototype-results .results__score small { + font-size: 11px; +} +.prototype-results .results__score strong { + font-size: 14px; + font-weight: 600; +} +.prototype-results .results__score-mobile-label { + display: none; +} +.prototype-results .results__number { + text-align: right; +} +.prototype-results .results__arrow { + justify-self: center; + transition: transform 120ms; +} +.prototype-results .results__arrow--open { + transform: rotate(180deg); +} +.prototype-results .results__formula.ui-button.ui-button { + position: absolute; + top: 32px; + right: 0; + width: 24px; + min-width: 24px; + height: 24px; + min-height: 24px; + padding: 4px; +} +.prototype-results .results--unscored .results__columns, +.prototype-results .results--unscored .results__row { + grid-template-columns: minmax(0, 1fr) 180px 24px; +} +.prototype-results .results__detail { + min-width: 0; + padding: 16px; + background: var(--light-gray); + border-radius: 10px; +} +.prototype-results .results__pagination { + gap: 20px; + justify-content: flex-end; + color: var(--grey-for-text); +} +.prototype-results .results__empty, +.prototype-results .results__sheet-info { + display: grid; + gap: 12px; + padding-block: 16px; +} +.prototype-results .results__empty button { + justify-self: start; +} +.prototype-results .results__sheet-info { + padding: 16px; + background: var(--light-gray); + border-radius: 10px; +} +.prototype-results .results__scope { + display: grid; + gap: 4px; + padding: 0; + margin: 0; + border: 0; +} +.prototype-results .results__scope label { + display: flex; + gap: 12px; + align-items: center; + min-height: 44px; + cursor: pointer; +} +.prototype-results .results__scope label > span { + display: flex; + flex: 1; + gap: 12px; + justify-content: space-between; +} +.prototype-results .results__download { + justify-self: end; + min-width: 180px; +} +.prototype-results .results__filter-label { + display: grid; + gap: 8px; +} +.prototype-results .results__sr { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); +} +.prototype-results .results__sort-menu { + box-sizing: border-box; + display: grid; + gap: 4px; + width: 260px; + max-width: calc(100vw - 32px); + padding: 8px; + font-size: 12px; + color: var(--black); + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); +} +.prototype-results .results__sort-option { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + min-height: 44px; + padding: 8px; + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + background: transparent; + border: 0; + border-radius: 8px; +} +.prototype-results .results__sort-option:hover, +.prototype-results .results__sort-option:focus-visible { + background: var(--light-gray); + outline: none; +} +.prototype-results ::ng-deep .modal .modal__body.results-modal { + width: 480px; + max-width: calc(100vw - 40px); + max-height: calc(var(--visual-viewport-height, 100dvh) - 40px); + padding: 24px; + overflow-y: auto; +} +.prototype-results .ui-alert.ui-alert.ui-alert { + box-sizing: border-box; + padding: 12px 16px; + margin: 0; + font-size: 12px; + line-height: 1.5; + color: var(--black); + background: var(--accent-light); + border-radius: 10px; +} +.prototype-results .ui-alert.ui-alert.ui-alert.ui-alert--success { + background: var(--green-light); +} +.prototype-results .ui-alert.ui-alert.ui-alert.ui-alert--warning { + background: color-mix(in srgb, var(--gold) 28%, var(--light-white)); +} +.prototype-results .ui-alert.ui-alert.ui-alert.ui-alert--error { + background: var(--red-light); +} +.prototype-results .ui-alert.ui-alert.ui-alert p, +.prototype-results .ui-alert.ui-alert.ui-alert strong, +.prototype-results .ui-alert.ui-alert.ui-alert summary { + color: inherit; +} +.prototype-results .ui-alert.ui-alert.ui-alert p { + margin: 4px 0 0; +} +.prototype-results .results__badge { + display: inline-block; + padding: 3px 8px; + font-size: 11px; + line-height: 16px; + color: var(--black); + background: color-mix(in srgb, var(--gold) 28%, var(--light-white)); + border-radius: 6px; +} +.prototype-results .results__badge--ready { + background: var(--green-light); +} +.prototype-results .results__badge--error { + background: var(--red-light); +} +@media (max-width: 999px) { + .prototype-results .results__score-mobile-label { + display: inline; + } + .prototype-results .results { + padding: 16px; + } + .prototype-results .results__toolbar { + flex-wrap: wrap; + } + .prototype-results .results__search { + flex-basis: 100%; + } + .prototype-results .results__toolbar-actions { + flex-wrap: wrap; + width: 100%; + } + .prototype-results .results__toolbar-actions .ui-button { + flex: 1 0 auto; + } + .prototype-results .results__columns, + .prototype-results .results__formula.ui-button { + display: none; + } + .prototype-results .results__formula-mobile.ui-button { + display: inline-flex; + } + .prototype-results .results__row { + grid-template-columns: 24px minmax(0, 1fr) auto 16px; + gap: 8px; + min-height: 100px; + } + .prototype-results .results__place { + grid-row: 1; + grid-column: 1; + font-weight: 600; + } + .prototype-results .results__name { + grid-row: 1; + grid-column: 2/4; + } + .prototype-results .results__arrow { + grid-row: 1; + grid-column: 4; + } + .prototype-results .results__count { + grid-row: 2; + grid-column: 2/4; + } + .prototype-results .results__score { + display: flex; + grid-row: 3; + grid-column: 2/4; + gap: 12px; + align-items: center; + justify-content: space-between; + } + .prototype-results .results--unscored .results__row { + grid-template-columns: minmax(0, 1fr) 16px; + min-height: 80px; + } + .prototype-results .results--unscored .results__name { + grid-column: 1; + } + .prototype-results .results--unscored .results__count { + grid-column: 1; + } + .prototype-results .results--unscored .results__arrow { + grid-column: 2; + } + .prototype-results .results__detail { + padding: 12px 0; + background: transparent; + } + .prototype-results .results__pagination { + gap: 12px; + } + .prototype-results .results__dialog .results__heading { + align-items: start; + } + .prototype-results .results__download { + width: 100%; + } +} +@media (prefers-reduced-motion: reduce) { + .prototype-results .results__arrow { + transition: none; + } +} diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/styles.css b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/styles.css new file mode 100644 index 000000000..79dbcbf69 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/styles.css @@ -0,0 +1,1178 @@ +/** @format */ + +@font-face { + font-family: Mont; + src: url("../assets/Mont-Regular.woff2"); + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: Mont; + src: url("../assets/Mont-SemiBold.woff2"); + font-weight: 600; + font-display: swap; +} +@font-face { + font-family: Mont; + src: url("../assets/Mont-Bold.woff2"); + font-weight: 700; + font-display: swap; +} +* { + box-sizing: border-box; +} +[hidden] { + display: none !important; +} +body { + margin: 0; + font: + 14px/1.5 Mont, + sans-serif; + background: var(--light-gray, #f8f8f8); + color: var(--black, #171717); +} +button, +input, +select, +textarea { + font: inherit; + color: inherit; +} +button, +a, +select { + touch-action: manipulation; +} +button { + cursor: pointer; +} +button:disabled { + cursor: default; + opacity: 0.5; +} +button:focus-visible, +a:focus-visible, +input:focus-visible, +select:focus-visible { + outline: 2px solid var(--accent, #8862ec); + outline-offset: 3px; +} +button, +input, +select { + min-width: 0; +} +a, +.link { + color: var(--accent, #8862ec); + text-decoration: none; +} +.link { + padding: 0; + border: 0; + background: none; + text-align: left; + line-height: 1.6; + overflow-wrap: anywhere; +} +h1, +h2, +h3, +p { + margin: 0; +} +h1 { + font-size: 22px; + font-weight: 600; + line-height: 1.4; + overflow-wrap: anywhere; +} +h2 { + font-size: 16px; + font-weight: 600; + line-height: 1.5; +} +h3 { + font-size: 14px; + font-weight: 600; +} +.muted { + color: var(--grey-for-text, #858585); + font-size: 12px; + line-height: 1.6; +} +.prototype-bar { + padding: 12px 24px; + display: flex; + gap: 16px; + justify-content: space-between; + align-items: center; + background: #fff; + border-bottom: 1px solid var(--medium-grey-for-outline, #ddd); + font-size: 12px; +} +.prototype-controls { + display: flex; + gap: 16px; + flex-wrap: wrap; +} +.prototype-controls label { + display: flex; + gap: 8px; + align-items: center; +} +.prototype-controls select { + padding: 6px 8px; + background: #fff; + border: 1px solid var(--medium-grey-for-outline, #ddd); + border-radius: 8px; + max-width: 240px; +} +.office { + display: grid; + grid-template-columns: 176px minmax(0, 1fr); + gap: 20px; + max-width: 1200px; + margin: auto; +} +.sidebar { + padding: 32px 12px; +} +.logo { + width: 112px; + margin-bottom: 28px; +} +.sidebar nav { + display: grid; + gap: 20px; + font-size: 12px; +} +.sidebar .active { + color: var(--accent); +} +.workspace { + min-width: 0; + padding: 24px 20px 48px; +} +.program-header { + margin-bottom: 20px; +} +.program-header p { + margin-bottom: 4px; +} +.mobile-header { + display: none; +} +.section-tabs { + display: flex; + flex-wrap: wrap; + gap: 6px 20px; + margin: 0 0 20px; + padding-bottom: 10px; + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.section-tabs button { + background: none; + border: 0; + padding: 7px 0; + color: var(--grey-for-text); + font-size: 13px; +} +.section-tabs button[aria-current="page"] { + color: var(--accent); + font-weight: 600; +} +.section-tabs button[aria-current="page"]::after { + content: ""; + display: block; + height: 2px; + margin-top: 6px; + background: var(--accent); +} +.icon { + display: inline-flex; + width: 18px; + height: 18px; + flex: none; + align-items: center; + justify-content: center; +} +.icon svg { + width: 100%; + height: 100%; +} +.summary-card__icon .icon { + width: 24px; + height: 24px; +} +.primary, +.secondary, +.quiet { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 32px; + padding: 6px 16px; + font-size: 12px; + line-height: 1.4; + border-radius: 20px; + white-space: normal; + text-align: center; +} +.primary { + border: 1px solid var(--accent); + background: var(--accent); + color: #fff; +} +.secondary { + border: 1px solid var(--accent); + background: transparent; + color: var(--accent); +} +.quiet { + border: 1px solid var(--medium-grey-for-outline); + background: transparent; +} +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 0; + background: none; + border-radius: 8px; + flex: none; +} +.icon-button:hover, +.quiet:hover { + background: var(--light-gray); +} +.heading-row, +.toolbar, +.actions, +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.actions { + justify-content: flex-start; + flex-wrap: wrap; +} +.page-panel { + padding: 20px; + background: var(--light-white, #fff); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; + display: grid; + gap: 16px; + min-width: 0; +} +.toolbar { + flex-wrap: wrap; +} +.search-form { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 180px; +} +.search-form input { + width: 100%; + min-height: 36px; + padding: 8px 12px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 18px; + background: transparent; + font-size: 12px; +} +.toolbar-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.filters { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + padding: 16px 0; + border-top: 1px solid var(--medium-grey-for-outline); + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.field { + display: grid; + gap: 6px; + font-size: 12px; + align-content: start; +} +.field select, +.field input { + width: 100%; + padding: 9px 12px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 18px; + min-height: 36px; + background: transparent; +} +.filters-note { + grid-column: 1/-1; +} +.filter-reset { + justify-self: start; +} +.table-wrap { + width: 100%; + min-width: 0; +} +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + line-height: 1.6; +} +.data-table th { + padding: 10px 8px; + font-weight: 400; + color: var(--grey-for-text); + text-align: left; + white-space: normal; + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.data-table td { + padding: 16px 8px; + vertical-align: top; + border-bottom: 1px solid var(--light-gray, #eee); + text-align: left; + overflow-wrap: anywhere; +} +.data-table th:first-child, +.data-table td:first-child { + padding-left: 0; +} +.data-table .name-cell { + width: 35%; +} +.subtext { + display: block; + color: var(--grey-for-text); + font-size: 11px; + margin-top: 3px; +} +.status { + display: inline-block; + font-size: 12px; + line-height: 1.5; +} +.status--good { + color: var(--green-dark, #407b53); +} +.status--warning { + color: var(--orange-dark, #936e32); +} +.status--muted { + color: var(--grey-for-text); +} +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + font-size: 12px; + color: var(--grey-for-text); + margin-top: 8px; +} +.pagination .actions { + gap: 8px; +} +.empty-state { + padding: 36px 16px; + text-align: center; + display: grid; + gap: 10px; + justify-items: center; +} +.empty-state p { + max-width: 480px; +} +.inline-note { + font-size: 12px; + color: var(--grey-for-text); + line-height: 1.6; +} +.state-error { + color: var(--red-dark, #aa4343); +} +.prototype-analytics .analytics { + padding: 0 0 32px; +} +.prototype-analytics .summary-card__value { + font-variant-numeric: tabular-nums; +} +.prototype-analytics .summary-card { + min-width: 0; +} +.prototype-analytics .summary-card__label { + overflow-wrap: anywhere; +} +.prototype-analytics .analytics__header { + padding: 18px 20px; +} +.prototype-analytics .analytics-card, +.prototype-analytics .attention { + min-width: 0; +} +.prototype-analytics .analytics-grid--priority { + align-items: stretch; +} +.attention-links { + list-style: none; + padding: 0; + margin: 12px 0 0; + display: grid; + gap: 8px; +} +.attention-links button { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 0; + border: 0; + background: none; + text-align: left; + font-size: 12px; + line-height: 1.6; +} +.attention-links button strong { + font-size: 16px; + font-weight: 600; + color: var(--accent); +} +.dates-list { + margin: 12px 0 0; + padding: 0; + list-style: none; + font-size: 12px; + display: grid; + gap: 8px; +} +.dates-list li { + display: flex; + justify-content: space-between; + gap: 10px; +} +.dates-list time { + color: var(--grey-for-text); +} +.funnel-bars { + display: grid; + gap: 14px; + margin-top: 18px; +} +.funnel-line { + display: grid; + grid-template-columns: 140px minmax(0, 1fr) 30px; + gap: 12px; + align-items: center; + font-size: 12px; +} +.bar-track { + height: 9px; + border-radius: 5px; + background: var(--light-gray); +} +.bar-value { + height: 100%; + border-radius: 5px; + background: var(--accent); + min-width: 0; +} +.progress-track { + height: 10px; + background: var(--light-gray); + border-radius: 8px; + margin: 14px 0; +} +.progress-track span { + display: block; + height: 100%; + background: var(--accent); + border-radius: 8px; +} +.metric-links { + display: flex; + gap: 16px; + flex-wrap: wrap; + font-size: 12px; +} +.metric-links button { + font-size: 12px; +} +.metric-links strong { + margin-right: 4px; +} +.cases-list { + display: grid; + margin-top: 14px; + gap: 0; +} +.case-heading, +.case-data { + display: grid; + grid-template-columns: minmax(0, 1.8fr) repeat(3, minmax(0, 0.7fr)); + gap: 12px; + padding: 10px 0; + font-size: 12px; + align-items: center; +} +.case-heading { + color: var(--grey-for-text); + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.case-data { + border: 0; + border-bottom: 1px solid var(--light-gray); + background: none; + text-align: left; + width: 100%; +} +.case-data span:first-child { + color: var(--accent); + overflow-wrap: anywhere; +} +.case-data--saved { + grid-template-columns: minmax(0, 1.8fr) repeat(2, minmax(0, 0.7fr)); +} +.region-row { + display: grid; + grid-template-columns: 90px minmax(0, 1fr) 26px; + gap: 12px; + align-items: center; + margin: 12px 0; + font-size: 12px; +} +.activity-bars { + display: flex; + height: 72px; + align-items: flex-end; + gap: 8px; + margin-top: 20px; +} +.activity-bars span { + flex: 1; + background: var(--accent); + border-radius: 3px 3px 0 0; +} +.activity-labels { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--grey-for-text); + margin-top: 8px; +} +.coverage-table td, +.coverage-table th { + padding: 10px 8px; +} +.exports-menu { + position: relative; +} +.exports-menu summary { + list-style: none; + cursor: pointer; +} +.exports-menu summary::-webkit-details-marker { + display: none; +} +.exports-popup { + position: absolute; + top: 100%; + right: 0; + width: 216px; + z-index: 5; + display: grid; + padding: 6px; + background: #fff; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 10px; + box-shadow: 0 4px 12px #0000000d; +} +.exports-popup button { + background: none; + border: 0; + padding: 10px 12px; + text-align: left; + font-size: 12px; +} +.exports-popup button:hover { + background: var(--light-gray); +} +dialog { + margin: auto; + padding: 20px; + border: 0; + border-radius: 15px; + background: var(--light-white, #fff); + color: var(--black); + width: min(880px, calc(100vw - 24px)); + max-width: calc(100vw - 24px); + max-height: calc(100dvh - 32px); + overflow-y: auto; + overscroll-behavior: contain; + font: + 14px/1.5 Mont, + sans-serif; +} +dialog::backdrop { + background: #0006; +} +.prototype-drilldown dialog.modal__body { + width: min(880px, calc(100vw - 24px)); + padding: 20px; +} +.modal-header { + align-items: flex-start; + margin-bottom: 18px; +} +.modal-header h2 { + font-size: 18px; + font-weight: 400; + overflow-wrap: anywhere; +} +.modal-header .muted { + margin-top: 6px; +} +.close-button { + font-size: 23px; + line-height: 1; + min-width: 32px; +} +.modal-body { + display: grid; + gap: 16px; + min-width: 0; +} +.modal-back { + margin-bottom: 14px; +} +.metrics-row { + display: flex; + gap: 24px; + flex-wrap: wrap; + padding: 10px 12px; + background: var(--light-gray); + border-radius: 8px; + font-size: 12px; +} +.metrics-row strong { + margin-left: 6px; + font-weight: 600; +} +.modal-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + border-bottom: 1px solid var(--medium-grey-for-outline); + padding-bottom: 10px; +} +.modal-tabs button { + border: 0; + background: none; + padding: 6px 0; + font-size: 12px; +} +.modal-tabs button[aria-selected="true"] { + color: var(--accent); + font-weight: 600; +} +.answer-list { + display: grid; + gap: 20px; +} +.answer h3 { + font-size: 12px; + margin-bottom: 6px; + color: var(--grey-for-text); + font-weight: 400; +} +.answer p { + font-size: 14px; + line-height: 1.6; +} +.team-row { + display: grid; + grid-template-columns: 40px minmax(0, 1fr) minmax(90px, 0.7fr); + gap: 12px; + align-items: center; + padding: 14px 0; + border-bottom: 1px solid var(--light-gray); + font-size: 12px; +} +.avatar { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: var(--light-gray); + border-radius: 50%; + color: var(--accent); + font-size: 12px; +} +.score-list { + display: grid; + gap: 0; +} +.score-row { + padding: 16px 0; + border-bottom: 1px solid var(--light-gray); +} +.score-row h3 { + font-size: 13px; + font-weight: 400; + margin-bottom: 8px; +} +.score-row strong { + font-size: 18px; +} +.score-row p { + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; +} +.allocation-summary { + display: grid; + gap: 12px; +} +.allocation-summary dl { + margin: 0; + display: grid; + grid-template-columns: 1fr auto; + gap: 12px; + font-size: 13px; +} +.allocation-summary dd { + margin: 0; + font-weight: 600; +} +.notice { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 30; + padding: 12px 20px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 12px; + background: #fff; + font-size: 12px; + max-width: calc(100vw - 32px); +} +.skeleton { + height: 20px; + width: 70%; + border-radius: 6px; + background: var(--light-gray); + margin: 8px 0; +} +.skeleton--large { + height: 100px; + width: 100%; +} +.loading-panels { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} +@media (min-width: 1000px) { + .prototype-analytics .summary { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} +@media (max-width: 999px) { + .office { + display: block; + } + .sidebar { + display: none; + } + .workspace { + padding: 16px; + } + .mobile-header { + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + font-size: 12px; + } + .mobile-header img { + width: 100px; + } + .prototype-bar { + padding: 12px 16px; + align-items: flex-start; + flex-wrap: wrap; + } + .program-header { + margin-top: 8px; + } + .filters { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .prototype-analytics .summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .prototype-analytics .analytics-grid { + grid-template-columns: 1fr; + } +} +@media (max-width: 600px) { + .prototype-controls { + width: 100%; + gap: 8px; + display: grid; + grid-template-columns: 1fr; + } + .prototype-controls label { + display: grid; + grid-template-columns: 75px minmax(0, 1fr); + } + .prototype-controls select { + width: 100%; + max-width: none; + } + .workspace { + padding: 16px; + } + h1 { + font-size: 20px; + } + .section-tabs { + gap: 4px 16px; + } + .section-tabs button { + font-size: 12px; + min-height: 40px; + } + .page-panel { + padding: 16px; + } + .toolbar-actions { + width: 100%; + } + .search-form { + min-width: 0; + flex-basis: 100%; + } + .primary, + .secondary, + .quiet { + min-height: 40px; + padding: 8px 14px; + } + .filters { + grid-template-columns: 1fr; + } + .prototype-analytics .summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .prototype-analytics .summary-card { + padding: 12px; + gap: 8px; + } + .prototype-analytics .summary-card__icon { + display: none; + } + .prototype-analytics .summary-card__value { + font-size: 24px; + } + .funnel-line { + grid-template-columns: 115px minmax(0, 1fr) 25px; + gap: 8px; + } + .case-heading, + .case-data { + gap: 8px; + grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 0.65fr)); + font-size: 11px; + } + .case-data--saved { + grid-template-columns: minmax(0, 1.6fr) repeat(2, minmax(0, 0.65fr)); + } + .data-table thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + } + .data-table, + .data-table tbody, + .data-table tr { + display: block; + width: 100%; + } + .data-table tr { + padding: 14px 0; + border-bottom: 1px solid var(--medium-grey-for-outline); + } + .data-table td { + display: grid; + grid-template-columns: 90px minmax(0, 1fr); + gap: 10px; + padding: 5px 0; + border: 0; + min-width: 0; + width: 100%; + } + .data-table td::before { + content: attr(data-label); + color: var(--grey-for-text); + font-size: 11px; + } + .data-table .name-cell { + width: 100%; + } + .data-table td.name-cell { + display: block; + font-size: 14px; + padding-top: 0; + margin-bottom: 6px; + } + .data-table td.name-cell::before { + content: none; + } + .pagination { + align-items: flex-start; + flex-wrap: wrap; + } + .coverage-table td.name-cell { + font-size: 13px; + } + .dates-list li { + display: grid; + gap: 2px; + } + .prototype-drilldown dialog.modal__body { + padding: 16px; + max-height: calc(100dvh - 24px); + } + .modal-header h2 { + font-size: 17px; + } + .team-row { + grid-template-columns: 36px minmax(0, 1fr); + gap: 10px; + } + .team-row > span:last-child { + grid-column: 2; + } + .loading-panels { + grid-template-columns: 1fr; + } + .metrics-row { + gap: 12px; + } + .metric-links { + gap: 12px; + } + .heading-row { + align-items: flex-start; + } + .exports-popup { + right: -2px; + } + .region-row { + grid-template-columns: 75px minmax(0, 1fr) 25px; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation: none !important; + transition: none !important; + } +} +@media (max-width: 600px) { + .icon-button { + min-width: 44px; + min-height: 44px; + } + .modal-header .close-button { + margin-top: -4px; + margin-right: -4px; + } +} +.modal-tabs [aria-pressed="true"] { + color: var(--accent); + font-weight: 600; +} +.admin-list-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} +.admin-list-toolbar .modal-tabs { + border: 0; + padding: 0; +} +.admin-list-toolbar .actions { + gap: 16px; +} +.admin-footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; + border-top: 1px solid var(--medium-grey-for-outline); + padding-top: 16px; +} +.admin-effects { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + margin: 0; + font-size: 13px; +} +.admin-effects dd { + margin: 0; + font-weight: 600; + overflow-wrap: anywhere; +} +.admin-effects dt { + overflow-wrap: anywhere; +} +.admin-section { + display: grid; + gap: 8px; + padding-top: 16px; + border-top: 1px solid var(--medium-grey-for-outline); +} +.admin-effect-item { + display: grid; + gap: 4px; + margin-top: 8px; + font-size: 13px; +} +.admin-effect-item strong { + font-weight: 600; +} +.admin-selection { + display: flex; + align-items: flex-start; + gap: 12px; + cursor: pointer; + min-height: 32px; +} +.admin-selection input { + flex: none; + accent-color: var(--accent); + margin-top: 4px; + width: 16px; + height: 16px; +} +.admin-history { + list-style: none; + margin: 0; + padding: 0; + display: grid; +} +.admin-history li { + display: grid; + gap: 8px; + padding: 16px 0; + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.admin-history li:first-child { + padding-top: 0; +} +.admin-history li > button { + justify-self: start; +} +.admin-error { + display: grid; + gap: 8px; + border-left: 2px solid var(--red-dark, #aa4343); + padding: 12px 16px; + background: var(--light-gray); + font-size: 13px; +} +.admin-error > button { + justify-self: start; +} +.admin-preservation { + padding: 12px 16px; + background: var(--light-gray); + border-radius: 8px; +} +.admin-team-row { + grid-template-columns: 36px minmax(0, 1fr) minmax(90px, 0.7fr); +} +.admin-team-row > .link { + justify-self: start; +} +.prototype-controls .quiet { + min-height: 32px; +} +.prototype-bar { + align-items: flex-start; +} +.prototype-controls { + justify-content: flex-end; +} +@media (max-width: 600px) { + .admin-list-toolbar, + .admin-footer { + align-items: flex-start; + flex-direction: column; + } + .admin-list-toolbar .actions { + gap: 12px; + } + .admin-effects { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + } + .admin-effects dd { + margin-bottom: 8px; + } + .admin-team-row { + grid-template-columns: 36px minmax(0, 1fr); + } + .admin-team-row > .link, + .admin-team-row > .muted { + grid-column: 2; + } + .admin-selection { + align-items: center; + min-height: 44px; + } + .admin-selection input { + margin: 0; + } + .admin-footer .link, + .admin-list-toolbar .link { + min-height: 44px; + display: inline-flex; + align-items: center; + } + .prototype-controls { + justify-content: stretch; + } + .prototype-controls label { + grid-template-columns: 100px minmax(0, 1fr); + } +} diff --git a/docs/design/program-solutions-manager-d02-20261025/source-manifest.json b/docs/design/program-solutions-manager-d02-20261025/source-manifest.json new file mode 100644 index 000000000..7e8fedf59 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/source-manifest.json @@ -0,0 +1,50 @@ +{ + "kind": "D02 synthetic prototype review bundle; no runtime product changes", + "beProduct": "394bf1666825ea87e7985b88c401a4e3c9f8d316", + "feProduct": "b4191a8b082e41b318c67c7605fa2764b647c46c", + "contract": "1.20.1", + "designApproval": "PENDING; existing D02 question", + "runtimeSources": { + "index.html": "444cf18af00a8e7448e46abc76380147ed7c96437a172beca4b6d8a8a5269420", + "app.js": "73eb3cdd678174e7151afbfb7869ba62a7c0a37f4f652b2287d3c9aca1a07343", + "admin.js": "73ab1ab7846cde895b1336c8dd35be114aa707bd0de56789c61e49bf358c4bbc", + "styles.css": "1d32f0bb7e80462359a864cd27f747684726ab9dd09b117f07d760cd5ce37418", + "icons.js": "7bb1132b94391e3ab52123e6e528d38057d222722d4ec675711ba398dc12662f", + "global-reference.css": "ac2b519c5e7d05310332298736f9e5fd19b634d21d2766b00bb8361c74e76138", + "analytics-reference.css": "5c22d9d89a5f7998e4db3331834906ead8b35e5c23d9a992e235ca7c91c0320e", + "drilldown-reference.css": "6fdaf18d7cf3a7d0f95f61ed5498361634d86605e668547ffd8d2ab5a19cb868", + "results-reference.css": "a92e429596fc8ddd9c382f2c38ea2d486ad2c90aa84e8f9ce294a8214419e4f2" + }, + "baseline": { + "app.js": "e6951de710596c6edf80f4a2349bd25875db280dfe31e911db81a6aa33a84f84", + "styles.css": "1bbd65322ab8fff199f3160035a51e4b6631ec97d58cf9c6e8f6c9e302853d74", + "index.html": "4d9f412ba6f882c31c3f89fc3869f02b3ecc1993ac1bdfa1f15850ff0acf9f11", + "design-handoff.md": "46ca528ee4e93527d3b7616d0b246dfb71429865d33ad66ec49b6542cfac6ab0" + }, + "originalAC26": "unchanged", + "fullRelease": "incomplete; AC18/19 OPEN, AC21 BE10 separate", + "unverified": [ + "Angular manager UI", + "server API in prototype browser run", + "delivery", + "XLSX", + "production", + "real-program migration" + ], + "attempts": [ + { + "name": "attempt1", + "result": "FAIL after78 checks", + "cause": "QA helper expected1 locator; row has2 solution links. Original report retained outside Git." + }, + { + "name": "attempt2", + "result": "FAIL after111 checks", + "cause": "QA helper searched main instead of dialog. Original report retained outside Git." + }, + { + "name": "intermediate", + "result": "115/128/129 PASS, later superseded by source changes and final130 run" + } + ] +} diff --git a/docs/design/program-solutions-manager-d02-20261025/verify-prototype.cjs b/docs/design/program-solutions-manager-d02-20261025/verify-prototype.cjs new file mode 100644 index 000000000..6880609f7 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/verify-prototype.cjs @@ -0,0 +1,420 @@ +/** @format */ + +const fs = require("fs"); +const assert = require("assert/strict"); +const { chromium } = require(process.env.PLAYWRIGHT_MODULE || "playwright"); +const root = + process.env.D02_OUTPUT_DIR || require("path").join(__dirname, "prototype", "manager-v1"); +const baseUrl = process.env.D02_BASE_URL || "http://127.0.0.1:8769/manager-v1/"; +const allowedOrigin = new URL(baseUrl).origin + "/"; +const evidence = { + kind: "Synthetic HTML prototype only; no backend or Angular verification", + started: new Date().toISOString(), + checks: [], + screenshots: [], + pageErrors: [], + failedRequests: [], + nonStaticRequests: [], + externalBlocked: {}, +}; +let browser, page; +const check = (name, details = {}) => evidence.checks.push({ name, status: "PASS", ...details }); +const click = action => page.locator(`[data-action="${action}"]`).first().click(); +const text = () => page.locator(".modal-body").innerText(); +const title = () => page.locator("#dialog-title").innerText(); +async function go(format = "kch", width = 1440, screen = "solutions", state = "ready") { + await page.setViewportSize({ width, height: 1000 }); + await page.goto(`${baseUrl}?format=${format}&screen=${screen}&state=${state}`); + await page.evaluate(() => document.fonts.ready); +} +async function bounds(name) { + const result = await page.evaluate(() => ({ + page: document.documentElement.scrollWidth <= innerWidth, + dialog: + !document.querySelector("dialog").open || + document.querySelector("dialog").scrollWidth <= + document.querySelector("dialog").clientWidth + 1, + font: getComputedStyle(document.body).fontFamily, + })); + assert(result.page && result.dialog, `${name}: overflow ${JSON.stringify(result)}`); + assert(result.font.includes("Mont")); +} +async function shot(name) { + await page.screenshot({ path: `${root}/${name}.png`, fullPage: true }); + evidence.screenshots.push(`${name}.png`); +} +async function work(id = 301) { + const scope = (await page.locator("dialog").evaluate(el => el.open)) ? "dialog" : "#manager-main"; + await page.locator(`${scope} [data-action="solution"][data-id="${id}"]`).first().click(); +} +async function apply() { + await click("admin-apply"); + await page.waitForFunction(() => document.querySelector('[data-action="admin-apply"]') === null); +} +async function pick(ids) { + for (const id of ids) await page.locator(`[name="registration"][value="${id}"]`).check(); +} +async function previewArchive() { + await work(); + await click("admin-archive"); +} +async function casePreview() { + await work(); + await click("admin-case"); + await page.locator('[name="admin-case"]').selectOption({ label: "Городская среда" }); + await click("admin-case-preview"); +} +async function main() { + browser = await chromium.launch({ headless: true, channel: "chrome" }); + page = await browser.newPage(); + await page.route("**/*", route => { + const url = route.request().url(); + if (url.startsWith(allowedOrigin)) return route.continue(); + const key = new URL(url).hostname + ":" + route.request().method(); + evidence.externalBlocked[key] = (evidence.externalBlocked[key] || 0) + 1; + return route.abort(); + }); + page.on("pageerror", e => evidence.pageErrors.push(e.message)); + page.on("requestfailed", r => { + if (r.url().startsWith(allowedOrigin)) + evidence.failedRequests.push({ url: r.url(), error: r.failure()?.errorText }); + }); + page.on("request", r => { + if (r.url().startsWith(allowedOrigin) && r.method() !== "GET") + evidence.nonStaticRequests.push({ url: r.url(), method: r.method() }); + }); + for (const format of ["kch", "hack", "noncompetitive"]) + for (const screen of ["solutions", "evaluation", "results", "analytics"]) + for (const width of [320, 390, 768, 1440]) { + await go(format, width, screen); + await bounds(`${format}/${screen}/${width}`); + assert.equal(await page.locator("[data-screen]").count(), 4); + check("Existing four-section layout", { format, screen, width }); + } + for (const state of [ + "ready-draft", + "loading", + "empty", + "no-results", + "error", + "no-access", + "long", + "open-mode", + ]) + for (const width of [320, 390, 768, 1440]) { + await go("kch", width, "analytics", state); + await bounds(`${state}/${width}`); + check("Existing page state", { state, width }); + } + console.log("Matrix PASS: 48 layouts + 32 page states"); + for (const format of ["kch", "hack", "noncompetitive"]) + for (const width of [390, 1440]) { + await go(format, width); + await casePreview(); + assert((await text()).includes("Городская среда")); + await bounds("case preview"); + if (format === "kch") await shot(`admin-case-preview-${width}`); + await apply(); + assert((await text()).includes("Городская среда")); + await click("admin-history"); + await click("admin-event"); + assert((await text()).includes("Образование")); + assert((await text()).includes("Нет")); + await page.getByRole("tab", { name: "После изменения" }).click(); + assert((await text()).includes("Городская среда")); + assert((await text()).includes("Нет")); + if (format === "kch" && width === 1440) await shot("admin-case-history-desktop"); + check("Case change preserves materials/zero/false; before and after", { format, width }); + await go(format, width); + await work(); + await page.getByRole("tab", { name: "Команда", exact: true }).click(); + assert.equal(await page.getByRole("tab").count(), 2); + assert((await text()).includes("Ожидают подтверждения · 1")); + await page.locator('[data-action="admin-remove-member"][data-person="2"]').click(); + await bounds("member preview"); + await apply(); + assert((await text()).includes("Принятые участники: 2")); + assert(!(await page.locator('[role="tabpanel"]').innerText()).includes("Мария Климова")); + await click("admin-history"); + await click("admin-event"); + assert((await text()).includes("Мария Климова")); + await page.getByRole("tab", { name: "После изменения" }).click(); + assert(!(await text()).includes("Мария Климова")); + check("Accepted member removal; captain protected; original roster retained", { + format, + width, + }); + await go(format, width); + await previewArchive(); + await bounds("archive preview"); + if (format === "kch") await shot(`admin-archive-preview-${width}`); + await apply(); + assert((await title()).includes("Сервис")); + assert((await page.locator(".modal-header").innerText()).includes("Архив")); + assert.equal( + await page + .locator( + '[data-action="admin-case"],[data-action="admin-archive"],[data-action="admin-remove-member"]', + ) + .count(), + 0, + ); + await page.getByRole("tab", { name: "Команда", exact: true }).click(); + assert((await text()).includes("Принятые участники: 3")); + assert(!(await text()).includes("Приглашение отправлено")); + if (format === "kch" && width === 390) await shot("admin-archived-team-mobile"); + assert.equal(await page.locator('[data-action="work-scores"]').count(), 0); + assert((await text()).includes("Оценки сохранены")); + await click("close"); + assert.equal( + await page.locator('#manager-main [data-action="solution"][data-id="301"]').count(), + 0, + ); + await page.locator('[data-action="admin-scope"][data-scope="archived"]').click(); + assert.equal( + await page.locator('#manager-main [data-action="solution"][data-id="301"]').count(), + 2, + ); + await bounds("archive list"); + check( + "Archive readonly; materials/team retained, ratings preservation explained; active list excludes it", + { format, width }, + ); + await go(format, width); + await click("admin-people"); + await pick([1, 5, 9, 10]); + await click("admin-registration-preview"); + assert((await text()).includes("Виктория Орлова")); + assert((await text()).includes("Исключение: Андрей Сергеев")); + await bounds("registration preview"); + if (format === "kch") await shot(`admin-registration-preview-${width}`); + await apply(); + assert.equal(await title(), "Удаление регистраций"); + assert((await text()).includes("Учетные записи участников сохранены")); + await click("close"); + await click("admin-people"); + assert.equal(await page.locator('[name="registration"]').count(), 8); + assert.equal( + await page + .locator( + '[name="registration"][value="1"],[name="registration"][value="5"],[name="registration"][value="9"],[name="registration"][value="10"]', + ) + .count(), + 0, + ); + check("Bulk registration removal: captain/member/pending/unaffiliated, independent audit", { + format, + width, + }); + } + console.log("Six format/viewport administrative journeys PASS"); + for (const ids of [[9], [10]]) { + await go(); + await click("admin-people"); + await pick(ids); + await click("admin-registration-preview"); + await apply(); + assert.equal(await title(), "Удаление регистраций"); + if (ids[0] === 10) assert((await text()).includes("Активные решения не затронуты")); + await click("close"); + await work(); + await page.getByRole("tab", { name: "Команда", exact: true }).click(); + assert((await text()).includes("Принятые участники: 3")); + check("Pending-only/no-solution independent audit", { ids }); + } + await go(); + await previewArchive(); + await apply(); + await click("close"); + await click("admin-people"); + await pick([1]); + await click("admin-registration-preview"); + await apply(); + assert((await text()).includes("Активные решения не затронуты")); + await click("close"); + await page.locator('[data-action="admin-scope"][data-scope="archived"]').click(); + await work(); + await click("admin-history"); + assert.equal(await page.locator(".admin-history li").count(), 1); + check("Removing registration after archive keeps its roster and earlier history"); + for (const mode of ["stale", "error", "no-access"]) { + await go("kch", 390); + await page.locator("#action-response").selectOption(mode); + await previewArchive(); + await click("admin-apply"); + assert.equal(await page.locator('[role="alert"]').count(), 1); + assert(await page.locator('[data-action="admin-apply"]').isDisabled()); + await bounds(mode); + if (mode === "stale") await shot("admin-stale-mobile"); + await click("admin-refresh-preview"); + if (mode !== "no-access") await apply(); + else assert(!(await page.locator("dialog").evaluate(el => el.open))); + check("Action failure is recoverable and requires fresh confirmation", { mode }); + } + await go("kch", 390); + await page.locator("#action-response").selectOption("legacy"); + await click("admin-people"); + await pick([1, 5]); + await click("admin-registration-preview"); + assert((await text()).includes("Вся операция остановлена")); + assert(await page.locator('[data-action="admin-apply"]').isDisabled()); + await bounds("legacy"); + await shot("admin-legacy-conflict-mobile"); + await click("back"); + assert.equal(await page.locator('[name="registration"]').count(), 12); + assert.equal(await page.locator('[name="registration"]:checked').count(), 2); + check("Mixed Project conflict blocks entire command, selection retained"); + await go("kch", 390); + await page.locator("#action-response").selectOption("loading"); + await previewArchive(); + assert.equal( + await page.locator('[aria-label="Загрузка предпросмотра"][aria-busy="true"]').count(), + 1, + ); + await bounds("preview loading"); + await click("close"); + check("Preview loading can be dismissed without mutation"); + await go("kch", 390); + await work(); + await page.getByRole("tab", { name: "Решение", exact: true }).focus(); + await page.keyboard.press("ArrowRight"); + assert.equal( + await page.getByRole("tab", { name: "Команда", exact: true }).getAttribute("aria-selected"), + "true", + ); + await page.keyboard.press("Home"); + assert.equal( + await page.getByRole("tab", { name: "Решение", exact: true }).getAttribute("aria-selected"), + "true", + ); + await page.locator('[data-action="close"]').focus(); + await page.keyboard.press("Tab"); + assert(await page.locator("dialog").evaluate(el => el.contains(document.activeElement))); + await page.keyboard.press("Escape"); + assert(!(await page.locator("dialog").evaluate(el => el.open))); + assert( + await page + .locator('[data-action="solution"][data-id="301"]') + .first() + .evaluate(el => el === document.activeElement), + ); + check("Keyboard tabs, focus containment, Escape, return focus"); + await go(); + await previewArchive(); + await click("admin-apply"); + await page.keyboard.press("Escape"); + await page.waitForTimeout(350); + assert((await page.locator(".modal-header").innerText()).includes("Архив")); + check("Apply blocks repeat/close while busy; completed state stays visible"); + await go("kch", 390); + await click("filters"); + await page.locator('[name="fieldId"]').selectOption("30"); + await page.locator('[name="answer"]').selectOption("present"); + assert((await page.locator("#manager-main").innerText()).includes("Сервис поддержки студентов")); + await page.locator('[name="fieldId"]').selectOption("31"); + assert((await page.locator("#manager-main").innerText()).includes("Сервис поддержки студентов")); + await page.locator('[name="fieldId"]').selectOption("32"); + await page.locator('[name="answer"]').selectOption("not_applicable"); + assert((await page.locator("#manager-main").innerText()).includes("Сервис поддержки студентов")); + check("Existing field filters preserve zero, false, not-applicable"); + await go("kch", 1440, "analytics"); + await click("case"); + await work(); + await page.getByRole("tab", { name: "Команда", exact: true }).click(); + await click("back"); + assert((await title()).includes("Кейс")); + await click("close"); + check("Existing analytics drilldown / team / back"); + await go("kch", 1440, "evaluation"); + await click("allocation"); + assert((await text()).includes("Назначений с оценками, сохраняются")); + await click("apply-allocation"); + check("Existing allocation preview and apply"); + await go("kch", 1440, "analytics"); + await page.locator('[data-action="people"][data-kind="without-team"]').click(); + await click("reminder-preview"); + assert((await text()).includes("Создайте решение или присоединитесь к команде")); + await click("send-reminder"); + check("Existing reminder preview"); + await go("kch", 1440, "results"); + await click("work-scores"); + await click("score-detail"); + assert((await text()).includes("Нет")); + check("Existing completed zero/false score"); + for (const format of ["kch", "hack"]) + for (const width of [390, 1440]) { + await go(format, width, "solutions", "ready-draft"); + await work(304); + await click("admin-submit"); + assert(!(await page.locator('[data-action="admin-apply"]').isDisabled())); + await bounds("first-submit"); + if (format === "kch" && width === 390) await shot("admin-submit-preview-mobile"); + await apply(); + assert.equal(await page.locator('[data-action="admin-submit"]').count(), 0); + assert((await page.locator(".modal-header").innerText()).includes("Сдано")); + check("Manager first submission: ready draft, no second-submit control", { format, width }); + await go(format, width); + await work(304); + await click("admin-submit"); + assert(await page.locator('[data-action="admin-apply"]').isDisabled()); + assert((await text()).includes("Сдача пока недоступна")); + check("Incomplete first submission is blocked with field names", { format, width }); + } + await go("noncompetitive", 390); + await work(304); + assert.equal(await page.locator('[data-action="admin-submit"]').count(), 0); + check("Noncompetitive format has no artificial submit action"); + await go(); + await work(); + await page.getByRole("tab", { name: "Команда", exact: true }).click(); + await page.locator('[data-action="admin-remove-member"][data-person="2"]').click(); + await apply(); + await click("close"); + await page.locator("#program-format").selectOption("hack"); + await work(); + await page.getByRole("tab", { name: "Команда", exact: true }).click(); + assert((await text()).includes("Принятые участники: 3")); + await click("close"); + await page.locator('[data-screen="analytics"]').click(); + assert((await page.locator('[data-metric="participants"]').innerText()).includes("12")); + check("Changing review format resets local mutations and team counters"); + await go(); + await previewArchive(); + await apply(); + await click("close"); + await page.locator('[data-action="admin-scope"][data-scope="archived"]').click(); + assert.equal( + await page.locator('[data-action="filters"],[data-action="export-found"]').count(), + 0, + ); + await page.locator('[name="search"]').fill("поддержки"); + await page.locator(".search-form button").click(); + assert((await page.locator("#manager-main").innerText()).includes("Сервис поддержки")); + check("Archive uses separate supported search without active-only fields/export"); + assert.deepEqual(evidence.pageErrors, []); + assert.deepEqual(evidence.failedRequests, []); + assert.deepEqual(evidence.nonStaticRequests, []); + evidence.finished = new Date().toISOString(); + evidence.status = "PASS"; + evidence.count = evidence.checks.length; +} +main() + .catch(error => { + evidence.status = "FAIL"; + evidence.error = error.stack; + console.error(error); + process.exitCode = 1; + }) + .finally(async () => { + fs.writeFileSync(`${root}/admin-qa-evidence.json`, JSON.stringify(evidence, null, 2) + "\n"); + if (browser) await browser.close(); + console.log( + JSON.stringify({ + status: evidence.status, + checks: evidence.checks.length, + pageErrors: evidence.pageErrors, + nonStaticRequestCount: evidence.nonStaticRequests.length, + externalBlocked: evidence.externalBlocked, + }), + ); + }); diff --git a/docs/product/program-solutions-fe08-api.md b/docs/product/program-solutions-fe08-api.md index d114d7ec0..2a3d85f6f 100644 --- a/docs/product/program-solutions-fe08-api.md +++ b/docs/product/program-solutions-fe08-api.md @@ -83,3 +83,9 @@ C00 125 schemas/47 operations/185 scenarios; прежние 120/45/161 стру 128 schemas/49 operations/209 scenarios, прежние125/47/185 неизменны и три файла byte-identical.31 новых real-interceptor wire tests охватывают24 независимых сценария, errors/null/token/exact spaces/pipe/ID bounds; вместе с прежними submit61 PASS. Final test:ci2827 tests/480 files+strict types, development build/scoped lint PASS. Backend:19 новых tests, три observed PostgreSQL races, broad356 до final token guard и final52 после; actual115 response/88 valid requests+15 rejected invalid types трех форматов, без изменения прежних15 domain моделей. [Spec/evidence](https://github.com/PROCOLLAB-github/api/blob/d4709651e45b7d3411e19001ee4eada516afea48/docs/product/program-solutions-be08-case-actions.md). Это API слой до согласования D02; новых UI/layout/SCSS/routes нет. Submitted сохраняет freeze до ответа на ранее заданный вопрос, удаление/отвязка и конкурсная публикация не реализованы этим case command. Полные BE08/FE08/QA01/REL01 и первый выпуск остаются открытыми. + +## D02: административные сценарии, макет от 25.10.2026 + +Существующий pending D02 дополнен до C00 1.20.1: первая сдача организатором, case/member preview/apply, архив с чтением материалов/состава, удаление registration_ids и независимая история. Команда остается второй вкладкой. Project conflict блокирует всю операцию; архивные фильтры/оценки не обещают неподдержанные readers. [Review bundle и handoff](../design/program-solutions-manager-d02-20261025/README.md) содержит source, tokens/patterns, AC D02-01–23, states, 130 Chrome prototype checks и screenshots. + +Это docs/design checkpoint; новые Angular routes/components/SCSS/фасады и API контракты не менялись. Runtime product BE394bf166/FEb4191a8b и C00 1.20.1 сохраняются. 130 проверок статического макета не заменяют 420 native/2949 FE предыдущего implementation checkpoint и не закрывают исходные AC18/19, QA01/REL01. D02/D03 approval и публикация несданных конкурсных черновиков уже запрошены, ответа здесь не предполагается. Без merge/deploy/реальных переключений. From 609253594c88fc8255e4841c918fc375a72e47e5 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sun, 25 Oct 2026 21:47:54 +0300 Subject: [PATCH 42/47] feat(programs): extend public solution contract to submitted competitions --- docs/contracts/program-solutions-v1/README.md | 2 + .../program-solutions-v1/examples.json | 206 +++++++++++++++++- .../program-solutions-v1/openapi.json | 6 +- docs/product/program-solutions-fe08-api.md | 10 + .../solution/solution-contract.fixture.ts | 204 ++++++++++++++++- .../solution/solution-contract.model.ts | 2 +- 6 files changed, 415 insertions(+), 15 deletions(-) diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index c6fd14550..1d9a0d6ff 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -2,6 +2,8 @@ # C00 Контракт решения в программе +Изменение 1.21.0: публичные GET охватывают уже сданные конкурсные решения КЧ/хакатона после окончания программы и установки marker существующим publisher. Неконкурсные сохраненные материалы остаются без сдачи. DTO/156 schemas/59 operations сохраняются;6 новых независимых сценариев, итого305. Временный сценарий published_solution_list_competitive_pending_rule намеренно меняется404→200 с пустой страницей: весь конкурсный список больше не закрыт из-за отдельного вопроса о draft. Остальные298 старых сценариев структурно неизменны. Несданные конкурсные работы и архивы не выдаются, окончательная eligibility draft все еще ожидает ответа. Нового Angular UI, схемы БД или Project нет. + Рабочая версия 1.20.1 от 25 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE09 и FE01–FE08. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. Изменение 1.20.1: исправлен код отказа оценивания архивного решения для пользователя с совмещенными ролями организатора/эксперта: solution_archived/409 проверяется до criteria/quota/assignment. Обычный эксперт без сохраненного доступа по-прежнему получает404. Существующая форма D01 закрывает материалы и ввод при таком отказе. Payload/59 операций/156 схем и297 прежних примеров не меняются; добавлены2 независимых примера. Миграций и изменений макета нет. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json index 3af765a8d..e0d4745c0 100644 --- a/docs/contracts/program-solutions-v1/examples.json +++ b/docs/contracts/program-solutions-v1/examples.json @@ -1,5 +1,5 @@ { - "contract_version": "1.20.1", + "contract_version": "1.21.0", "description": "Независимые примеры, не последовательность операций; фиктивные данные.", "scenarios": [ { @@ -7033,12 +7033,13 @@ { "name": "published_solution_list_competitive_pending_rule", "operation_id": "getPublishedSolutions", - "status": 404, + "status": 200, "response": { - "code": "not_found", - "detail": "Контролируемая ошибка.", - "errors": [], - "meta": {} + "program_id": 14, + "count": 0, + "next": null, + "previous": null, + "results": [] } }, { @@ -12451,6 +12452,199 @@ "errors": [], "meta": {} } + }, + { + "name": "published_competitive_submitted_list", + "operation_id": "getPublishedSolutions", + "status": 200, + "response": { + "program_id": 14, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 14, + "solution_id": 501, + "name": "Сданное конкурсное решение" + }, + "published_at": "2026-10-25T06:00:00Z", + "captain": { + "id": 11, + "first_name": "Иван", + "last_name": "Токарев", + "avatar_url": null + }, + "accepted_count": 1 + } + ] + } + }, + { + "name": "published_competitive_submitted_snapshot", + "operation_id": "getPublishedSolution", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 14, + "solution_id": 501, + "name": "Сданное конкурсное решение" + }, + "published_at": "2026-10-25T06:00:00Z", + "fields": [ + { + "id": 701, + "type": "number", + "purpose": "other", + "label": "Размер пилота", + "help_text": "", + "required": false, + "position": 0, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 702, + "type": "checkbox", + "purpose": "other", + "label": "Разрешение на демонстрацию", + "help_text": "", + "required": false, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 703, + "type": "file", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": false, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [ + { + "field_id": 701, + "value": 0 + }, + { + "field_id": 702, + "value": false + }, + { + "field_id": 703, + "value": { + "url": "https://cdn.example.org/solution/materials.zip" + } + } + ], + "team": { + "members": [ + { + "user": { + "id": 11, + "first_name": "Иван", + "last_name": "Токарев", + "avatar_url": null + }, + "role": "Капитан", + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + } + }, + { + "name": "published_competitive_submitted_page", + "operation_id": "getPublishedSolutions", + "status": 200, + "response": { + "program_id": 14, + "count": 25, + "next": "https://local.test/programs/14/published-solutions/?limit=1&offset=1", + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 14, + "solution_id": 501, + "name": "Сданное конкурсное решение" + }, + "published_at": "2026-10-25T06:00:00Z", + "captain": { + "id": 11, + "first_name": "Иван", + "last_name": "Токарев", + "avatar_url": null + }, + "accepted_count": 1 + } + ] + } + }, + { + "name": "published_competitive_draft_hidden", + "operation_id": "getPublishedSolution", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_competitive_archived_hidden", + "operation_id": "getPublishedSolution", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_competitive_unpublished_hidden", + "operation_id": "getPublishedSolution", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } } ] } diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json index 8872c30aa..c425736cd 100644 --- a/docs/contracts/program-solutions-v1/openapi.json +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -3,7 +3,7 @@ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "PROCOLLAB Program Solutions C00", - "version": "1.20.1", + "version": "1.21.0", "description": "Контракт решений в программах для реализации и моков BE01–BE09. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ @@ -8430,7 +8430,7 @@ } }, "security": [], - "description": "Анонимное read-only представление опубликованных неконкурсных решений. Данные остаются в ProgramSolution. Публикация определяется сохраненным published_at; GET не публикует. Конкурсная ветка еще не реализована до решения о черновиках. Cache-Control no-store." + "description": "Публичное read-only чтение опубликованных материалов solution программы. Неконкурсная программа: сохраненные материалы без искусственной сдачи; конкурсная: уже сданные решения и действующий снимок сдачи. Eligibility несданных конкурсных черновиков остается открытым вопросом, пока они не публикуются. Доступ определяет сохраненный published_at, архивы исключаются; GET не публикует. Пустой конкурсный список дает200, legacy mode404. Закрытые оценки, назначения, pending и личные данные регистрации не раскрываются. Cache-Control no-store." } }, "/programs/{program_id}/published-solutions/{solution_id}/": { @@ -8502,7 +8502,7 @@ } }, "security": [], - "description": "Анонимное read-only представление опубликованных неконкурсных решений. Данные остаются в ProgramSolution. Публикация определяется сохраненным published_at; GET не публикует. Конкурсная ветка еще не реализована до решения о черновиках. Cache-Control no-store." + "description": "Публичное read-only чтение опубликованных материалов solution программы. Неконкурсная программа: сохраненные материалы без искусственной сдачи; конкурсная: уже сданные решения и действующий снимок сдачи. Eligibility несданных конкурсных черновиков остается открытым вопросом, пока они не публикуются. Доступ определяет сохраненный published_at, архивы исключаются; GET не публикует. Пустой конкурсный список дает200, legacy mode404. Закрытые оценки, назначения, pending и личные данные регистрации не раскрываются. Cache-Control no-store." } }, "/programs/{program_id}/management/solutions/actions/preview/": { diff --git a/docs/product/program-solutions-fe08-api.md b/docs/product/program-solutions-fe08-api.md index 2a3d85f6f..30cd03863 100644 --- a/docs/product/program-solutions-fe08-api.md +++ b/docs/product/program-solutions-fe08-api.md @@ -89,3 +89,13 @@ C00 125 schemas/47 operations/185 scenarios; прежние 120/45/161 стру Существующий pending D02 дополнен до C00 1.20.1: первая сдача организатором, case/member preview/apply, архив с чтением материалов/состава, удаление registration_ids и независимая история. Команда остается второй вкладкой. Project conflict блокирует всю операцию; архивные фильтры/оценки не обещают неподдержанные readers. [Review bundle и handoff](../design/program-solutions-manager-d02-20261025/README.md) содержит source, tokens/patterns, AC D02-01–23, states, 130 Chrome prototype checks и screenshots. Это docs/design checkpoint; новые Angular routes/components/SCSS/фасады и API контракты не менялись. Runtime product BE394bf166/FEb4191a8b и C00 1.20.1 сохраняются. 130 проверок статического макета не заменяют 420 native/2949 FE предыдущего implementation checkpoint и не закрывают исходные AC18/19, QA01/REL01. D02/D03 approval и публикация несданных конкурсных черновиков уже запрошены, ответа здесь не предполагается. Без merge/deploy/реальных переключений. + +## C00 1.21: сданные конкурсные материалы + +Публичные GET поддерживают опубликованные submitted КЧ/хакатон работы; неконкурсные сохраненные материалы остаются без отдельной сдачи. DTO156/operations59 неизменны,6 новых scenarios, итого305. Временный пример404 всего competitive списка намеренно заменен200 пустого списка;298 остальных старых примеров сохранены. Публичные score/assignment/registration данные не добавлены. Shared contracts byte-identical; сгенерированы только новая версия types и fixtures. Существующий port/repository/adapter/use cases и CamelcaseInterceptor совместимы; новые route/layout/SCSS не добавлялись. + +Backend использует опубликованный действующий snapshot сдачи, сохраняет административную историю/оценки и исключает архивы. Запрос о несданных конкурсных черновиках остается открытым, D02/D03 approval не предполагается. Новый Angular public интерфейс и общие AC18/19/QA01/REL01 остаются в первом выпуске; BE10 отдельно. Миграций в этом checkpoint нет, backend1.21 → frontend; более старый competitive404 reader скроет уже опубликованные конкурсные материалы. Реальные программы не включались, merge/deploy не выполнялись. + +Проверки C00 1.21:31 public wire tests внутри2955 FE/484 files PASS, strict types/development build/scoped ESLint PASS. Native432+отдельные4 legacy/OK и97 actual HTTP/34 bodies подтверждают allowlist и совместимость. Предыдущий D02 docs/design checkpoint d59998e4 и его130 synthetic browser checks остаются отдельным evidence; этот API checkpoint не изменяет его HTML/Mont/вкладки или approval. [Backend CPUB spec и audit](https://github.com/PROCOLLAB-github/api/blob/feat/dev-program-solutions-be02-20261024/docs/product/program-solutions-competitive-publication.md). + +Последний target dev отдельно продвинулся: backend587a9155/frontend08414b79 содержат certificate changes. Read-only preview backend migration graph дает две leaf0031_certificate/0038_solution, хотя Git merge-tree без текстовых конфликтов. Интеграция с этим target и повторные проверки графа/данных остаются required_next;432/2955 не доказывают еще не созданную объединенную версию. Реальные ветки не сливались, histories не переписывались. diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts index 7a52934b4..f0fb808d1 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts @@ -6914,12 +6914,13 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ { name: "published_solution_list_competitive_pending_rule", operationId: "getPublishedSolutions", - status: 404, + status: 200, response: { - code: "not_found", - detail: "Контролируемая ошибка.", - errors: [], - meta: {}, + programId: 14, + count: 0, + next: null, + previous: null, + results: [], }, }, { @@ -12166,4 +12167,197 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ meta: {}, }, }, + { + name: "published_competitive_submitted_list", + operationId: "getPublishedSolutions", + status: 200, + response: { + programId: 14, + count: 1, + next: null, + previous: null, + results: [ + { + application: { + kind: "solution", + programId: 14, + solutionId: 501, + name: "Сданное конкурсное решение", + }, + publishedAt: "2026-10-25T06:00:00Z", + captain: { + id: 11, + firstName: "Иван", + lastName: "Токарев", + avatarUrl: null, + }, + acceptedCount: 1, + }, + ], + }, + }, + { + name: "published_competitive_submitted_snapshot", + operationId: "getPublishedSolution", + status: 200, + response: { + application: { + kind: "solution", + programId: 14, + solutionId: 501, + name: "Сданное конкурсное решение", + }, + publishedAt: "2026-10-25T06:00:00Z", + fields: [ + { + id: 701, + type: "number", + purpose: "other", + label: "Размер пилота", + helpText: "", + required: false, + position: 0, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 702, + type: "checkbox", + purpose: "other", + label: "Разрешение на демонстрацию", + helpText: "", + required: false, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 703, + type: "file", + purpose: "other", + label: "Материалы", + helpText: "", + required: false, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + answers: [ + { + fieldId: 701, + value: 0, + }, + { + fieldId: 702, + value: false, + }, + { + fieldId: 703, + value: { + url: "https://cdn.example.org/solution/materials.zip", + }, + }, + ], + team: { + members: [ + { + user: { + id: 11, + firstName: "Иван", + lastName: "Токарев", + avatarUrl: null, + }, + role: "Капитан", + specialization: null, + isCaptain: true, + }, + ], + acceptedCount: 1, + }, + }, + }, + { + name: "published_competitive_submitted_page", + operationId: "getPublishedSolutions", + status: 200, + response: { + programId: 14, + count: 25, + next: "https://local.test/programs/14/published-solutions/?limit=1&offset=1", + previous: null, + results: [ + { + application: { + kind: "solution", + programId: 14, + solutionId: 501, + name: "Сданное конкурсное решение", + }, + publishedAt: "2026-10-25T06:00:00Z", + captain: { + id: 11, + firstName: "Иван", + lastName: "Токарев", + avatarUrl: null, + }, + acceptedCount: 1, + }, + ], + }, + }, + { + name: "published_competitive_draft_hidden", + operationId: "getPublishedSolution", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_competitive_archived_hidden", + operationId: "getPublishedSolution", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_competitive_unpublished_hidden", + operationId: "getPublishedSolution", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, ]; diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts index a2a8d562c..4ec855694 100644 --- a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts +++ b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts @@ -1,6 +1,6 @@ /** @format */ -// Сгенерировано tools/generate-solution-contract.py из C00 1.20.1. +// Сгенерировано tools/generate-solution-contract.py из C00 1.21.0. // camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются. export type SolutionUser = { From ca1b203a34fddedd6f7d7d6a45752073697dfd7c Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sun, 25 Oct 2026 21:56:21 +0300 Subject: [PATCH 43/47] docs(programs): record public material contract compatibility --- .../README.md | 35 + .../contract-compatibility.json | 16 + .../domain-and-contract-evidence.json | 113 + .../evidence.json | 187 + .../http-wire.json | 7835 +++++++++++++++++ .../native-targets.txt | 31 + .../prospective-dev-integration.json | 22 + .../retention-final.json | 27 + 8 files changed, 8266 insertions(+) create mode 100644 docs/qa/program-solutions-competitive-publication-20261025/README.md create mode 100644 docs/qa/program-solutions-competitive-publication-20261025/contract-compatibility.json create mode 100644 docs/qa/program-solutions-competitive-publication-20261025/domain-and-contract-evidence.json create mode 100644 docs/qa/program-solutions-competitive-publication-20261025/evidence.json create mode 100644 docs/qa/program-solutions-competitive-publication-20261025/http-wire.json create mode 100644 docs/qa/program-solutions-competitive-publication-20261025/native-targets.txt create mode 100644 docs/qa/program-solutions-competitive-publication-20261025/prospective-dev-integration.json create mode 100644 docs/qa/program-solutions-competitive-publication-20261025/retention-final.json diff --git a/docs/qa/program-solutions-competitive-publication-20261025/README.md b/docs/qa/program-solutions-competitive-publication-20261025/README.md new file mode 100644 index 000000000..56128873a --- /dev/null +++ b/docs/qa/program-solutions-competitive-publication-20261025/README.md @@ -0,0 +1,35 @@ + + +# CPUB: опубликованные материалы сданных КЧ/хакатон решений + +Статус: **CPUB API PASS; первый выпуск не завершен, не READY TO MERGE**. Данные живут в ProgramSolution/answers/team/UserFile; Project не создается. После окончания solution-программы и при publication flag publisher отмечает активные submitted работы. Неконкурсные saved материалы по-прежнему без искусственной сдачи. Eligibility несданных конкурсных черновиков уже запрошена и остается открытой. + +Product refs: backend `8d089229617893745c5dfa749f20d891152b9caa`, frontend `609253594c88fc8255e4841c918fc375a72e47e5`. Новых DB migrations в CPUB нет. Публичные DTO/permissions на запись не расширялись: два прежних GET возвращают allowlist по marker, конкурсные draft/архив/неопубликованные скрыты. Уже установленный marker не снимается выключением флага или переносом даты; confirmed archive скрывает выдачу, сохраняя данные. + +## Контракт и потребители + +C00 **1.21.0 / 156 schemas / 59 operations / 305 scenarios**. Все156 схем и структура59 операций неизменны; изменились описания двух public GET. Добавлены6 сценариев. **Один временный пример** `published_solution_list_competitive_pending_rule` намеренно меняет404→200 пустого списка: вопрос о draft больше не закрывает весь конкурсный reader. Остальные298 старых сценариев сохранены. Три shared файла byte-identical. Generated TS types отличаются version comment, fixtures отражают новые/обновленный примеры; существующий adapter/repository/use cases принимают тот же DTO. + +## Проверки + +- Final **432 PostgreSQL tests/OK без skips**, включая12 новых CPUB tests и3 observed lock races. Native wrapper использует `test_procollab_ci_archived_rating_unicode` (ICU ru-RU), keepdb, обычный manage.py test mode; патч изолирует только файловый logger sink Windows, не домен/БД. [Точные targets](native-targets.txt). Отдельно **4 legacy Project publisher tests/OK**; focused30 — subset, не складывать с432. +- **2955 FE tests / 484 files**, strict types, development build, scoped ESLint PASS.31 public wire tests — subset. Black/Flake8/Django check/migration consistency/C00 PASS. Build имеет прежние Sass warnings. Locked Prettier3.8.3 до новых reports:2061 files PASS; итог с reports:2068 files PASS. +- **97 немокированных HTTP ответов / 34 валидных тела**, C00 validation до редактирования Git wire. Новые программы49/50/51, решения66–71. КЧ/хакатон: create/save/invite/accept/pending/submit/rate → controlled finish/flag → scoped publisher → anonymous list/search/detail → signed case/member revision → public applied snapshot → archive → public404. Noncompetitive: saved work +пустая saved work опубликованы без submit, pagination проверена. Before finish/flagFalse publisher0, затем1/1/2, repeat0; creationFalse сохраняет чтение/админ действия. Program flags изменялись только у собственных fixtures с проверенным prefix, global main publisher не запускался. +- **25 прежних моделей** (включая Auth User, Project и C2 audit/history) совпали до/после и после final native/FE/build checks: SHA `54631ba6896d1d18cadf7f855ca1fd1b83b0a2396a72b28e2b61b720f1783169`. **Project4→4**, все7 прежних registration audit headers сохранены. У49/50 marker/архивный snapshot и4 criterion values (0/False/комментарии) сохранены, у51 нет искусственной сдачи. Creation flags трех fixturesFalse. +- Owned API wrapper10408/child39748 остановлены,8877 освобожден. Старые8769/8871/8876/55439 не менялись. Старые8871/8876 не использовались как актуальная сборка. + +[Evidence/source hashes](evidence.json), [contract compatibility](contract-compatibility.json), [HTTP wire](http-wire.json), [domain proof](domain-and-contract-evidence.json), [final readonly retention](retention-final.json). + +## Неудачные и промежуточные попытки + +До реализации3 проверки дали3 product FAIL: publisher0 вместо1 и competitive404. Первый focused30 дал2 fixture errors: вариант нового кейса не был добавлен в конфигурацию, а secret text был передан в int criterion. Fixtures исправлены на валидный кейс/str criterion, валидаторы и guards не ослаблялись. После30/OK и431/OK тесты усилены новым обязательным полем и pagination/foreign scope; final432/OK. Старые logs/failed attempts сохранены с hashes в evidence. Первый verifier исходных AC включил header в счетчик; после точного выбора все26 строк byte-exact. Fixtures/критерии/оценки ради PASS не сбрасывались, завершенный HTTP helper запрещает replay. + +JWT/credentials не входят в report. **12 signed proof occurrences скрыты только в Git-копии wire** после проверки настоящих запросов/ответов. Raw private manifest/wire и helper logs находятся в TEMP/design-artifacts; не переносить их в Git. Повторять завершенный HTTP script нельзя:66/68 уже в архиве. Допустим отдельный readonly verifier. + +## Integration finding и границы готовности + +[Read-only prospective dev graph](prospective-dev-integration.json): target BE587a9155/FE08414b79 продвинулся certificate changes. Git merge-tree preview до CPUB не имеет текстовых конфликтов, но новый dev `0031_programcertificate_rendered_batch` и feature `0038_solution_registration_removal_audit` дают **две leaf nodes**. Текущий checkout graph чист. Overlay MigrationLoader(None) не пишет в БД; migration/sync/merge/rebase/force-push не выполнялись. **MAJOR required_next**: совместить certificate models/migrations и повторить граф/retention verification без перенумерации или удаления старых миграций.432/2955 не доказывают еще не собранную объединенную версию. + +Не проверены новый Angular public UI, CDN bytes (собственные metadata fixtures), production и окончательная draft eligibility. D02/D03 уже ожидают approval; новый UI не реализован этим checkpoint. Исходные26 AC сохранены:23 PASS, AC18/19 OPEN, AC21 отдельно BE10. Полный QA01/REL01, интеграция последнего dev и весь первый выпуск остаются в scope. Старые programs автоматически не переносятся; новых merge/deploy/реальных switches нет. После первого конкурсного marker сохраняется reader/publisher1.21: старый competitive404 reader скроет опубликованные материалы при откате. + +[CPUB backend spec и audit](https://github.com/PROCOLLAB-github/api/blob/8d089229617893745c5dfa749f20d891152b9caa/docs/product/program-solutions-competitive-publication.md). diff --git a/docs/qa/program-solutions-competitive-publication-20261025/contract-compatibility.json b/docs/qa/program-solutions-competitive-publication-20261025/contract-compatibility.json new file mode 100644 index 000000000..30622d63f --- /dev/null +++ b/docs/qa/program-solutions-competitive-publication-20261025/contract-compatibility.json @@ -0,0 +1,16 @@ +{ + "status": "PASS", + "version": "1.21.0", + "schemas": 156, + "all156SchemasUnchanged": true, + "operations": 59, + "operationDescriptionsOnly": ["getPublishedSolutions", "getPublishedSolution"], + "priorOperationsOtherwiseUnchanged": 59, + "unchangedPriorScenarios": 298, + "intentionallyRevisedScenario": "published_solution_list_competitive_pending_rule", + "priorStatus": 404, + "newStatus": 200, + "addedScenarios": 6, + "totalScenarios": 305, + "sharedFilesByteIdentical": 3 +} diff --git a/docs/qa/program-solutions-competitive-publication-20261025/domain-and-contract-evidence.json b/docs/qa/program-solutions-competitive-publication-20261025/domain-and-contract-evidence.json new file mode 100644 index 000000000..4b426b8ee --- /dev/null +++ b/docs/qa/program-solutions-competitive-publication-20261025/domain-and-contract-evidence.json @@ -0,0 +1,113 @@ +{ + "status": "PASS", + "contract": "1.21.0", + "responses": 97, + "validRequests": 34, + "programs": [ + { + "program": 49, + "solution": 66, + "draft": 67, + "template": "case_championship", + "competitive": true, + "marker": "2026-10-25T18:25:12.448804+00:00", + "status": "PASS" + }, + { + "program": 50, + "solution": 68, + "draft": 69, + "template": "hackathon", + "competitive": true, + "marker": "2026-10-25T18:25:15.456936+00:00", + "status": "PASS" + }, + { + "program": 51, + "solution": 70, + "draft": 71, + "template": "hackathon", + "competitive": false, + "marker": "2026-10-25T18:25:18.652630+00:00", + "status": "PASS" + } + ], + "publisherInvocations": [ + { + "program": 49, + "count": 0, + "at": "2026-10-25T18:25:12.429609+00:00" + }, + { + "program": 49, + "count": 0, + "at": "2026-10-25T18:25:12.439459+00:00" + }, + { + "program": 49, + "count": 1, + "at": "2026-10-25T18:25:12.451951+00:00" + }, + { + "program": 49, + "count": 0, + "at": "2026-10-25T18:25:12.462700+00:00" + }, + { + "program": 50, + "count": 0, + "at": "2026-10-25T18:25:15.422372+00:00" + }, + { + "program": 50, + "count": 0, + "at": "2026-10-25T18:25:15.441363+00:00" + }, + { + "program": 50, + "count": 1, + "at": "2026-10-25T18:25:15.459000+00:00" + }, + { + "program": 50, + "count": 0, + "at": "2026-10-25T18:25:15.477111+00:00" + }, + { + "program": 51, + "count": 0, + "at": "2026-10-25T18:25:18.606239+00:00" + }, + { + "program": 51, + "count": 0, + "at": "2026-10-25T18:25:18.628576+00:00" + }, + { + "program": 51, + "count": 2, + "at": "2026-10-25T18:25:18.654701+00:00" + }, + { + "program": 51, + "count": 0, + "at": "2026-10-25T18:25:18.679348+00:00" + } + ], + "oldModelsUnchanged": 25, + "hashBefore": "54631ba6896d1d18cadf7f855ca1fd1b83b0a2396a72b28e2b61b720f1783169", + "hashAfter": "54631ba6896d1d18cadf7f855ca1fd1b83b0a2396a72b28e2b61b720f1783169", + "projectCount": 4, + "noNewProject": true, + "creationFlagsFalse": true, + "closedScoresRetained": true, + "privateDataNotExposed": true, + "noncompetitiveHasNoArtificialSubmit": true, + "apiResponsesNotMocked": true, + "notChecked": [ + "new Angular public UI", + "CDN file bytes (owned metadata fixture only)", + "production", + "final competitive draft eligibility" + ] +} diff --git a/docs/qa/program-solutions-competitive-publication-20261025/evidence.json b/docs/qa/program-solutions-competitive-publication-20261025/evidence.json new file mode 100644 index 000000000..e8484dd63 --- /dev/null +++ b/docs/qa/program-solutions-competitive-publication-20261025/evidence.json @@ -0,0 +1,187 @@ +{ + "status": "CPUB_PASS_FEATURE_INCOMPLETE", + "backendProduct": "8d089229617893745c5dfa749f20d891152b9caa", + "frontendProduct": "609253594c88fc8255e4841c918fc375a72e47e5", + "contract": { + "status": "PASS", + "version": "1.21.0", + "schemas": 156, + "all156SchemasUnchanged": true, + "operations": 59, + "operationDescriptionsOnly": ["getPublishedSolutions", "getPublishedSolution"], + "priorOperationsOtherwiseUnchanged": 59, + "unchangedPriorScenarios": 298, + "intentionallyRevisedScenario": "published_solution_list_competitive_pending_rule", + "priorStatus": 404, + "newStatus": 200, + "addedScenarios": 6, + "totalScenarios": 305, + "sharedFilesByteIdentical": 3 + }, + "tests": { + "nativePostgreSQL": 432, + "newNative": 12, + "nativeSkipped": 0, + "legacyPublisher": 4, + "focusedBeforeFinalExtension": 30, + "frontend": 2955, + "frontendFiles": 484, + "publicWireSubset": 31, + "strictTypes": "PASS", + "developmentBuild": "PASS (pre-existing Sass warnings)", + "scopedESLint": "PASS", + "static": [ + { + "command": [ + "python.exe", + "-m", + "black", + "--check", + "partner_programs/services/publishing.py", + "partner_programs/services/solution_publication.py", + "partner_programs/solution_publication_api.py", + "partner_programs/tests/test_solution_publication.py", + "partner_programs/tests/test_solution_competitive_publication.py" + ], + "exitCode": 0, + "output": "All done! ✨ 🍰 ✨\n5 files would be left unchanged.\n" + }, + { + "command": [ + "python.exe", + "-m", + "flake8", + "partner_programs/services/publishing.py", + "partner_programs/services/solution_publication.py", + "partner_programs/solution_publication_api.py", + "partner_programs/tests/test_solution_publication.py", + "partner_programs/tests/test_solution_competitive_publication.py" + ], + "exitCode": 0, + "output": "" + }, + { + "command": ["python.exe", "manage.py", "check"], + "exitCode": 0, + "output": "System check identified no issues (0 silenced).\n" + }, + { + "command": ["python.exe", "manage.py", "makemigrations", "--check", "--dry-run"], + "exitCode": 0, + "output": "No changes detected\n" + }, + { + "command": ["python.exe", "docs/contracts/program-solutions-v1/validate_contract.py"], + "exitCode": 0, + "output": "PASS: OpenAPI; 156 schemas; 59 operations; 305 scenarios\n" + } + ], + "lockedPrettierBeforeReports": { + "version": "3.8.3", + "files": 2061, + "status": "PASS" + }, + "lockedPrettierWithReports": { + "version": "3.8.3", + "files": 2068, + "status": "PASS" + } + }, + "http": { + "responses": 97, + "validRequests": 34, + "APIResponsesNotMocked": true, + "publisherScopedOwnProgramsOnly": true, + "signedProofOccurrencesRedactedOnlyInGitCopy": 12 + }, + "retention": { + "status": "PASS", + "oldModelsUnchanged": 25, + "baseline": "54631ba6896d1d18cadf7f855ca1fd1b83b0a2396a72b28e2b61b720f1783169", + "afterAllNativeFrontendBuildChecks": true, + "globalRegistrationAuditsRetained": 7, + "fixtures": [ + { + "program": 49, + "solution": 66, + "scoreRows": 4, + "archived": true + }, + { + "program": 50, + "solution": 68, + "scoreRows": 4, + "archived": true + }, + { + "program": 51, + "solution": 70, + "scoreRows": 0, + "archived": false + } + ] + }, + "sourceHashes": { + "backend/partner_programs/services/publishing.py": "f81d1c1257f26081709567e554fdf523f2df0a7b3f1bdf507870ea1e7f06afc9", + "backend/partner_programs/services/solution_publication.py": "48f21b691c2d880d30bac0b18d08b15545edf9e07967ea717eeb7f2e2f4451bf", + "backend/partner_programs/solution_publication_api.py": "01e36a9dc0b37d62fb2921a26d9d59e60097c3dcfddcc909b6a99cd59c677f87", + "backend/partner_programs/tests/test_solution_publication.py": "a26e733eb5156ded66f03bf87f7cd6befa9677b4f88277803d510feb0874e085", + "backend/partner_programs/tests/test_solution_competitive_publication.py": "9f0d9c0f5d0da44660d6bb8a08cd36c4131224fe5b66f2c8402173506db31709", + "frontend/projects/social_platform/src/app/domain/solution/solution-contract.model.ts": "2f0e764f5bb7d0f871e7377f2127d127e77f049e6e021aa985cd2181a3412cd1", + "frontend/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts": "1763b9122cd0429f64b8a6a42fc9b4a1226c09205418a26114b33dce2b5fc82f" + }, + "logs": { + "procollab-competitive-publication-red.log": "30e3e0a5311cef424d181447fba74ff95c83dfbf68f1b272fe0a1e9a8ec8d5e9", + "procollab-competitive-publication-focused.log": "7c1aaaef685c845f6568d4b242fab5eac8962fc790d53f349baf216ab85f9f76", + "procollab-competitive-publication-focused-final.log": "b8236336be3061806f79393436c07ef0f462abf87871bd89ebf5e73db7d2f592", + "procollab-competitive-publication-native.log": "b0bd91e3ee79e68a730637dc712883d8f2e84f2828441c4abb5517fd7df5bcad", + "procollab-competitive-publication-native-final.log": "de9e614d81492c33836b272e95a54cd9eba7abc4f6e349eb2d6556ee5077cddc", + "procollab-competitive-publication-legacy.log": "0bd9a87af5abafd59ab1a4a13363d81db267e334da2a224e5d093e950922e2bc", + "procollab-competitive-publication-frontend.log": "51fe37f4c40dc9ee0dcd27ec22e626baa96c283cd5ec49ee10bc0ce9ebae9366", + "procollab-competitive-publication-build.log": "5ad22324570492434867d6a897a024d2092abfb77172785d36516bac7bd41b14" + }, + "retainedAttempts": [ + "red3 failed before implementation", + "first focused30 had2 fixture errors: missing target case option and text score in int criterion; corrected fixture, no validator weakened", + "focused30 then431 passed; final432 includes added required-field and submitted-only pagination/foreign-scope assertions", + "first AC row verifier counted table header; exact26 rows verified unchanged afterward" + ], + "review": { + "CPUB01to08": "PASS in bounded API scope", + "fullFeature": "NEEDS_CHANGES", + "majorIntegrationFinding": "Observed dev certificate0031 and solution0038 form two leaves in read-only overlay; reconcile models/migrations without renumbering or deleting historical data" + }, + "qa": { + "original26RowsUnchanged": true, + "passed": "23: AC01–17/20/22–26", + "open": ["AC18", "AC19"], + "separate": "AC21/BE10" + }, + "pending": [ + "existing D02 approval", + "existing D03 approval", + "competitive unsent draft eligibility", + "new Angular organizer/public screens and full QA01/REL01", + "observed dev migration/model integration" + ], + "noNewMigrationInCPUB": true, + "noMergeDeployRealProgramSwitch": true, + "ownedHelpers": { + "stopped": true, + "wrapper": 10408, + "child": 39748, + "releasedPort": 8877, + "oldListenersUnchanged": { + "8769": 20920, + "8871": 19092, + "8876": 33332, + "55439": 8688 + } + }, + "notChecked": [ + "new Angular public UI", + "CDN bytes: owned metadata fixtures only", + "production", + "latest dev combined implementation (prospective graph conflict observed)" + ] +} diff --git a/docs/qa/program-solutions-competitive-publication-20261025/http-wire.json b/docs/qa/program-solutions-competitive-publication-20261025/http-wire.json new file mode 100644 index 000000000..ab4f752c6 --- /dev/null +++ b/docs/qa/program-solutions-competitive-publication-20261025/http-wire.json @@ -0,0 +1,7835 @@ +[ + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 49, + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": "no-store" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/49/solutions/", + "status": 201, + "request": { + "name": "Сданное решение case_championship-competitive" + }, + "response": { + "id": 66, + "program_id": 49, + "version": 1, + "name": "Сданное решение case_championship-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:11.338682Z", + "updated_at": "2026-10-25T18:25:11.338682Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:11.344320Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 49, + "revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 315, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 315 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 316 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 317 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 318 + } + ] + } + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/49/solutions/66/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 34, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 70, + "version": 1, + "program": { + "id": 49, + "name": "Публикация решений case_championship-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T18:25:11.441070Z", + "updated_at": "2026-10-25T18:25:11.441070Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/70/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 70, + "version": 2, + "program": { + "id": 49, + "name": "Публикация решений case_championship-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T18:25:11.441070Z", + "updated_at": "2026-10-25T18:25:11.528437Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/49/solutions/66/", + "status": 200, + "request": null, + "response": { + "id": 66, + "program_id": 49, + "version": 3, + "name": "Сданное решение case_championship-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:11.338682Z", + "updated_at": "2026-10-25T18:25:11.531510Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:11.344320Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:11.523754Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 49, + "revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 315, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 315 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 316 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 317 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 318 + } + ] + } + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/49/solutions/66/invitations/", + "status": 201, + "request": { + "expected_solution_version": 3, + "recipient_user_id": 35, + "role": "Дизайнер" + }, + "response": { + "invitation": { + "id": 71, + "version": 1, + "program": { + "id": 49, + "name": "Публикация решений case_championship-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 35, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Дизайнер", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T18:25:11.691826Z", + "updated_at": "2026-10-25T18:25:11.691826Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 4 + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/49/solutions/66/", + "status": 200, + "request": null, + "response": { + "id": 66, + "program_id": 49, + "version": 4, + "name": "Сданное решение case_championship-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:11.338682Z", + "updated_at": "2026-10-25T18:25:11.695946Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:11.344320Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:11.523754Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 49, + "revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 315, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 315 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 316 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 317 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 318 + } + ] + } + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/49/solutions/66/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "answers": [ + { + "field_id": 315, + "value": "Город" + }, + { + "field_id": 316, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 317, + "value": 0 + }, + { + "field_id": 318, + "value": false + }, + { + "field_id": 319, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-49.pdf" + } + } + ] + }, + "response": { + "id": 66, + "program_id": 49, + "version": 5, + "name": "Сданное решение case_championship-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:11.338682Z", + "updated_at": "2026-10-25T18:25:11.872497Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:11.344320Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:11.523754Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 49, + "revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 315, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 315, + "value": "Город" + }, + { + "field_id": 316, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 317, + "value": 0 + }, + { + "field_id": 318, + "value": false + }, + { + "field_id": 319, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-49.pdf" + } + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/49/solutions/66/submit/", + "status": 200, + "request": { + "expected_version": 5, + "expected_form_revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734" + }, + "response": { + "id": 66, + "program_id": 49, + "version": 6, + "name": "Сданное решение case_championship-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:11.338682Z", + "updated_at": "2026-10-25T18:25:11.989790Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:11.344320Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:11.523754Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 49, + "revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 315, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 315, + "value": "Город" + }, + { + "field_id": 316, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 317, + "value": 0 + }, + { + "field_id": 318, + "value": false + }, + { + "field_id": 319, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-49.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": "2026-10-25T18:25:11.987209Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "role": "other", + "method": "POST", + "path": "/programs/49/solutions/", + "status": 201, + "request": { + "name": "Черновик case_championship-competitive" + }, + "response": { + "id": 67, + "program_id": 49, + "version": 1, + "name": "Черновик case_championship-competitive", + "captain_id": 36, + "created_at": "2026-10-25T18:25:12.071824Z", + "updated_at": "2026-10-25T18:25:12.071824Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:12.075949Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 49, + "revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 315, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 315 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 316 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 317 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 318 + } + ] + } + }, + "cacheControl": null + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/49/solutions/66/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 49, + "name": "Публикация решений case_championship-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "id": 66, + "program_id": 49, + "version": 6, + "name": "Сданное решение case_championship-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:11.338682Z", + "updated_at": "2026-10-25T18:25:11.989790Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 33, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:11.344320Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 34, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:11.523754Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 315, + "type": "select", + "label": "Кейс", + "options": ["Город", "Образование"], + "purpose": "case", + "position": 0, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "label": "Материалы", + "options": [], + "purpose": "other", + "position": 1, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "label": "Число", + "options": [], + "purpose": "other", + "position": 2, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "label": "Флаг", + "options": [], + "purpose": "other", + "position": 3, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "label": "Презентация", + "options": [], + "purpose": "presentation", + "position": 4, + "required": false, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "template": "case_championship", + "program_id": 49, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 315 + }, + { + "value": "Материалы qa01cp-20261025-55fd4d0c", + "field_id": 316 + }, + { + "value": 0, + "field_id": 317 + }, + { + "value": false, + "field_id": 318 + }, + { + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-49.pdf" + }, + "field_id": 319 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": "2026-10-25T18:25:11.987209Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "c8b2bce3ec5ff6eaa666695cf98d5afde4d02e64565cea02e990908e6153d3bd", + "criteria": [ + { + "id": 93, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 94, + "name": "Качество 2", + "description": null, + "type": "int", + "min_value": 0.0, + "max_value": 10.0, + "value": null + }, + { + "id": 95, + "name": "Проверено 3", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 96, + "name": "Комментарий 4", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": null + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": false, + "rated_count": 0, + "max_rates": 3, + "required_evaluators": 1, + "deadline": "2026-10-27T18:25:10.954965Z" + }, + "cacheControl": "private, no-store" + }, + { + "role": "expert", + "method": "POST", + "path": "/programs/49/solutions/66/evaluation/", + "status": 200, + "request": { + "expected_criteria_revision": "c8b2bce3ec5ff6eaa666695cf98d5afde4d02e64565cea02e990908e6153d3bd", + "scores": [ + { + "criterion_id": 94, + "value": "0" + }, + { + "criterion_id": 95, + "value": "False" + }, + { + "criterion_id": 96, + "value": "private-score-qa01cp-20261025-55fd4d0c" + } + ] + }, + "response": { + "program": { + "id": 49, + "name": "Публикация решений case_championship-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "id": 66, + "program_id": 49, + "version": 6, + "name": "Сданное решение case_championship-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:11.338682Z", + "updated_at": "2026-10-25T18:25:11.989790Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 33, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:11.344320Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 34, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:11.523754Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 315, + "type": "select", + "label": "Кейс", + "options": ["Город", "Образование"], + "purpose": "case", + "position": 0, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "label": "Материалы", + "options": [], + "purpose": "other", + "position": 1, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "label": "Число", + "options": [], + "purpose": "other", + "position": 2, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "label": "Флаг", + "options": [], + "purpose": "other", + "position": 3, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "label": "Презентация", + "options": [], + "purpose": "presentation", + "position": 4, + "required": false, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "ef1e0b0dff5b0f1546a6a2c523cbac372599c4d5608154102514674bfb902734", + "template": "case_championship", + "program_id": 49, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 315 + }, + { + "value": "Материалы qa01cp-20261025-55fd4d0c", + "field_id": 316 + }, + { + "value": 0, + "field_id": 317 + }, + { + "value": false, + "field_id": 318 + }, + { + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-49.pdf" + }, + "field_id": 319 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": "2026-10-25T18:25:11.987209Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "c8b2bce3ec5ff6eaa666695cf98d5afde4d02e64565cea02e990908e6153d3bd", + "criteria": [ + { + "id": 93, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": "" + }, + { + "id": 94, + "name": "Качество 2", + "description": null, + "type": "int", + "min_value": 0.0, + "max_value": 10.0, + "value": "0" + }, + { + "id": 95, + "name": "Проверено 3", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + }, + { + "id": 96, + "name": "Комментарий 4", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "private-score-qa01cp-20261025-55fd4d0c" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": true, + "rated_count": 1, + "max_rates": 3, + "required_evaluators": 1, + "deadline": "2026-10-27T18:25:10.954965Z" + }, + "cacheControl": "private, no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/?limit=1", + "status": 200, + "request": null, + "response": { + "program_id": 49, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "published_at": "2026-10-25T18:25:12.448804Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/66/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "published_at": "2026-10-25T18:25:12.448804Z", + "fields": [ + { + "id": 315, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [ + { + "field_id": 315, + "value": "Город" + }, + { + "field_id": 316, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 317, + "value": 0 + }, + { + "field_id": 318, + "value": false + }, + { + "field_id": 319, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-49.pdf" + } + } + ], + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "is_captain": false + } + ], + "accepted_count": 2 + } + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/?search=%D0%A1%D0%B4%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5+%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5+case_championship-competitive", + "status": 200, + "request": null, + "response": { + "program_id": 49, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "published_at": "2026-10-25T18:25:12.448804Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/67/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/49/published-solutions/67/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/published-solutions/67/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/49/published-solutions/67/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "other", + "method": "GET", + "path": "/programs/49/published-solutions/67/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/solutions/66/", + "status": 401, + "request": null, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "role": "pending", + "method": "GET", + "path": "/programs/49/solutions/66/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/?submission=all", + "status": 400, + "request": null, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [ + { + "path": "submission", + "code": "invalid", + "detail": "Неизвестное поле.", + "field_id": null + } + ], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/66/?unknown=1", + "status": 400, + "request": null, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [ + { + "path": "unknown", + "code": "invalid", + "detail": "Неизвестное поле.", + "field_id": null + } + ], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/9007199254740992/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/", + "status": 401, + "request": null, + "response": { + "code": "authentication_required", + "detail": "{'detail': ErrorDetail(string='Given token not valid for any token type', code='token_not_valid'), 'code': ErrorDetail(string='token_not_valid', code='token_not_valid'), 'messages': [{'token_class': ErrorDetail(string='AccessToken', code='token_not_valid'), 'token_type': ErrorDetail(string='access', code='token_not_valid'), 'message': ErrorDetail(string='Token is invalid or expired', code='token_not_valid')}]}", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 49, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "published_at": "2026-10-25T18:25:12.448804Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/49/management/solutions/66/case/preview/", + "status": 200, + "request": { + "case_name": "Образование" + }, + "response": { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "case_field_id": 315, + "current_case": "Город", + "case_name": "Образование", + "solution_version": 6, + "errors": [], + "can_apply": true, + "is_submitted": true, + "removed_assignments_count": 0, + "score_count": 4, + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/49/management/solutions/66/case/apply/", + "status": 200, + "request": { + "case_name": "Образование", + "preview_token": "" + }, + "response": { + "solution_id": 66, + "solution_version": 7, + "updated_at": "2026-10-25T18:25:13.620522Z" + }, + "cacheControl": "private, no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/49/management/solutions/66/members/remove/preview/", + "status": 200, + "request": { + "user_id": 34 + }, + "response": { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "member": { + "role": "Аналитик", + "user": { + "id": 34, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:11.523754Z", + "is_captain": false, + "specialization": null + }, + "solution_version": 7, + "is_submitted": true, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 4, + "errors": [], + "can_apply": true, + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/49/management/solutions/66/members/remove/apply/", + "status": 200, + "request": { + "user_id": 34, + "preview_token": "" + }, + "response": { + "solution_id": 66, + "solution_version": 8, + "updated_at": "2026-10-25T18:25:13.777803Z" + }, + "cacheControl": "private, no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/66/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "published_at": "2026-10-25T18:25:12.448804Z", + "fields": [ + { + "id": 315, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 316, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 317, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 318, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 319, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [ + { + "field_id": 315, + "value": "Образование" + }, + { + "field_id": 316, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 317, + "value": 0 + }, + { + "field_id": 318, + "value": false + }, + { + "field_id": 319, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-49.pdf" + } + } + ], + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + }, + "cacheControl": "no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/49/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [66] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "solution_version": 8, + "is_submitted": true, + "is_archived": false, + "score_count": 4, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "errors": [] + } + ], + "score_count": 4, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "can_apply": true, + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/49/management/solutions/archive/apply/", + "status": 200, + "request": { + "ids": [66], + "preview_token": "" + }, + "response": { + "changed_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 49, + "solution_id": 66, + "name": "Сданное решение case_championship-competitive" + }, + "solution_version": 9, + "archived_at": "2026-10-25T18:25:14.031890Z" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 49, + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/49/published-solutions/66/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 50, + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": "no-store" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/50/solutions/", + "status": 201, + "request": { + "name": "Сданное решение hackathon-competitive" + }, + "response": { + "id": 68, + "program_id": 50, + "version": 1, + "name": "Сданное решение hackathon-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:14.307696Z", + "updated_at": "2026-10-25T18:25:14.307696Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:14.311111Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 50, + "revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 320, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 320 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 321 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 322 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 323 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 325 + } + ] + } + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/50/solutions/68/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 34, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 72, + "version": 1, + "program": { + "id": 50, + "name": "Публикация решений hackathon-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T18:25:14.423092Z", + "updated_at": "2026-10-25T18:25:14.423092Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/72/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 72, + "version": 2, + "program": { + "id": 50, + "name": "Публикация решений hackathon-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T18:25:14.423092Z", + "updated_at": "2026-10-25T18:25:14.514171Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/50/solutions/68/", + "status": 200, + "request": null, + "response": { + "id": 68, + "program_id": 50, + "version": 3, + "name": "Сданное решение hackathon-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:14.307696Z", + "updated_at": "2026-10-25T18:25:14.517198Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:14.311111Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:14.508117Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 50, + "revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 320, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 320 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 321 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 322 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 323 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 325 + } + ] + } + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/50/solutions/68/invitations/", + "status": 201, + "request": { + "expected_solution_version": 3, + "recipient_user_id": 35, + "role": "Дизайнер" + }, + "response": { + "invitation": { + "id": 73, + "version": 1, + "program": { + "id": 50, + "name": "Публикация решений hackathon-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 35, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Дизайнер", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T18:25:14.683499Z", + "updated_at": "2026-10-25T18:25:14.683499Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 4 + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/50/solutions/68/", + "status": 200, + "request": null, + "response": { + "id": 68, + "program_id": 50, + "version": 4, + "name": "Сданное решение hackathon-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:14.307696Z", + "updated_at": "2026-10-25T18:25:14.687149Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:14.311111Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:14.508117Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 50, + "revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 320, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 320 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 321 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 322 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 323 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 325 + } + ] + } + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/50/solutions/68/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "answers": [ + { + "field_id": 320, + "value": "Город" + }, + { + "field_id": 321, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 322, + "value": 0 + }, + { + "field_id": 323, + "value": false + }, + { + "field_id": 324, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-50.pdf" + } + }, + { + "field_id": 325, + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c" + } + ] + }, + "response": { + "id": 68, + "program_id": 50, + "version": 5, + "name": "Сданное решение hackathon-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:14.307696Z", + "updated_at": "2026-10-25T18:25:14.880164Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:14.311111Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:14.508117Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 50, + "revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 320, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 320, + "value": "Город" + }, + { + "field_id": 321, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 322, + "value": 0 + }, + { + "field_id": 323, + "value": false + }, + { + "field_id": 324, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-50.pdf" + } + }, + { + "field_id": 325, + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c" + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/50/solutions/68/submit/", + "status": 200, + "request": { + "expected_version": 5, + "expected_form_revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808" + }, + "response": { + "id": 68, + "program_id": 50, + "version": 6, + "name": "Сданное решение hackathon-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:14.307696Z", + "updated_at": "2026-10-25T18:25:15.020120Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:14.311111Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:14.508117Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 50, + "revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "template": "hackathon", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 320, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 320, + "value": "Город" + }, + { + "field_id": 321, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 322, + "value": 0 + }, + { + "field_id": 323, + "value": false + }, + { + "field_id": 324, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-50.pdf" + } + }, + { + "field_id": 325, + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c" + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": "2026-10-25T18:25:15.016593Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "role": "other", + "method": "POST", + "path": "/programs/50/solutions/", + "status": 201, + "request": { + "name": "Черновик hackathon-competitive" + }, + "response": { + "id": 69, + "program_id": 50, + "version": 1, + "name": "Черновик hackathon-competitive", + "captain_id": 36, + "created_at": "2026-10-25T18:25:15.133099Z", + "updated_at": "2026-10-25T18:25:15.133099Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:15.137137Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 50, + "revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 320, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 320 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 321 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 322 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 323 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 325 + } + ] + } + }, + "cacheControl": null + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/50/solutions/68/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 50, + "name": "Публикация решений hackathon-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "id": 68, + "program_id": 50, + "version": 6, + "name": "Сданное решение hackathon-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:14.307696Z", + "updated_at": "2026-10-25T18:25:15.020120Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 33, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:14.311111Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 34, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:14.508117Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 320, + "type": "select", + "label": "Кейс", + "options": ["Город", "Образование"], + "purpose": "case", + "position": 0, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "label": "Материалы", + "options": [], + "purpose": "other", + "position": 1, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "label": "Число", + "options": [], + "purpose": "other", + "position": 2, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "label": "Флаг", + "options": [], + "purpose": "other", + "position": 3, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "label": "Презентация", + "options": [], + "purpose": "presentation", + "position": 4, + "required": false, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 5, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "template": "hackathon", + "program_id": 50, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 320 + }, + { + "value": "Материалы qa01cp-20261025-55fd4d0c", + "field_id": 321 + }, + { + "value": 0, + "field_id": 322 + }, + { + "value": false, + "field_id": 323 + }, + { + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-50.pdf" + }, + "field_id": 324 + }, + { + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c", + "field_id": 325 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": "2026-10-25T18:25:15.016593Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "b07e0c39115e6bd9dd87a97292e0d652f7ddd055b3b80738099a21ec61b3543e", + "criteria": [ + { + "id": 97, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 98, + "name": "Качество 5", + "description": null, + "type": "int", + "min_value": 0.0, + "max_value": 10.0, + "value": null + }, + { + "id": 99, + "name": "Проверено 6", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 100, + "name": "Комментарий 7", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": null + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": false, + "rated_count": 0, + "max_rates": 3, + "required_evaluators": 1, + "deadline": "2026-10-27T18:25:10.954965Z" + }, + "cacheControl": "private, no-store" + }, + { + "role": "expert", + "method": "POST", + "path": "/programs/50/solutions/68/evaluation/", + "status": 200, + "request": { + "expected_criteria_revision": "b07e0c39115e6bd9dd87a97292e0d652f7ddd055b3b80738099a21ec61b3543e", + "scores": [ + { + "criterion_id": 98, + "value": "0" + }, + { + "criterion_id": 99, + "value": "False" + }, + { + "criterion_id": 100, + "value": "private-score-qa01cp-20261025-55fd4d0c" + } + ] + }, + "response": { + "program": { + "id": 50, + "name": "Публикация решений hackathon-competitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "id": 68, + "program_id": 50, + "version": 6, + "name": "Сданное решение hackathon-competitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:14.307696Z", + "updated_at": "2026-10-25T18:25:15.020120Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 33, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:14.311111Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 34, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:14.508117Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 320, + "type": "select", + "label": "Кейс", + "options": ["Город", "Образование"], + "purpose": "case", + "position": 0, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "label": "Материалы", + "options": [], + "purpose": "other", + "position": 1, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "label": "Число", + "options": [], + "purpose": "other", + "position": 2, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "label": "Флаг", + "options": [], + "purpose": "other", + "position": 3, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "label": "Презентация", + "options": [], + "purpose": "presentation", + "position": 4, + "required": false, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 5, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "1b67ffc516194714476107fc88cf254a4aeac359a5699fd32f399e03e8b13808", + "template": "hackathon", + "program_id": 50, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 320 + }, + { + "value": "Материалы qa01cp-20261025-55fd4d0c", + "field_id": 321 + }, + { + "value": 0, + "field_id": 322 + }, + { + "value": false, + "field_id": 323 + }, + { + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-50.pdf" + }, + "field_id": 324 + }, + { + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c", + "field_id": 325 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": "2026-10-25T18:25:15.016593Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "b07e0c39115e6bd9dd87a97292e0d652f7ddd055b3b80738099a21ec61b3543e", + "criteria": [ + { + "id": 97, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": "" + }, + { + "id": 98, + "name": "Качество 5", + "description": null, + "type": "int", + "min_value": 0.0, + "max_value": 10.0, + "value": "0" + }, + { + "id": 99, + "name": "Проверено 6", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + }, + { + "id": 100, + "name": "Комментарий 7", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "private-score-qa01cp-20261025-55fd4d0c" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": true, + "rated_count": 1, + "max_rates": 3, + "required_evaluators": 1, + "deadline": "2026-10-27T18:25:10.954965Z" + }, + "cacheControl": "private, no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/?limit=1", + "status": 200, + "request": null, + "response": { + "program_id": 50, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "published_at": "2026-10-25T18:25:15.456936Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/68/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "published_at": "2026-10-25T18:25:15.456936Z", + "fields": [ + { + "id": 320, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [ + { + "field_id": 320, + "value": "Город" + }, + { + "field_id": 321, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 322, + "value": 0 + }, + { + "field_id": 323, + "value": false + }, + { + "field_id": 324, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-50.pdf" + } + }, + { + "field_id": 325, + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c" + } + ], + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "is_captain": false + } + ], + "accepted_count": 2 + } + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/?search=%D0%A1%D0%B4%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5+%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5+hackathon-competitive", + "status": 200, + "request": null, + "response": { + "program_id": 50, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "published_at": "2026-10-25T18:25:15.456936Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/69/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/50/published-solutions/69/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/50/published-solutions/69/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/50/published-solutions/69/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "other", + "method": "GET", + "path": "/programs/50/published-solutions/69/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/solutions/68/", + "status": 401, + "request": null, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "role": "pending", + "method": "GET", + "path": "/programs/50/solutions/68/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/?submission=all", + "status": 400, + "request": null, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [ + { + "path": "submission", + "code": "invalid", + "detail": "Неизвестное поле.", + "field_id": null + } + ], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/68/?unknown=1", + "status": 400, + "request": null, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [ + { + "path": "unknown", + "code": "invalid", + "detail": "Неизвестное поле.", + "field_id": null + } + ], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/9007199254740992/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/", + "status": 401, + "request": null, + "response": { + "code": "authentication_required", + "detail": "{'detail': ErrorDetail(string='Given token not valid for any token type', code='token_not_valid'), 'code': ErrorDetail(string='token_not_valid', code='token_not_valid'), 'messages': [{'token_class': ErrorDetail(string='AccessToken', code='token_not_valid'), 'token_type': ErrorDetail(string='access', code='token_not_valid'), 'message': ErrorDetail(string='Token is invalid or expired', code='token_not_valid')}]}", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 50, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "published_at": "2026-10-25T18:25:15.456936Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/50/management/solutions/68/case/preview/", + "status": 200, + "request": { + "case_name": "Образование" + }, + "response": { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "case_field_id": 320, + "current_case": "Город", + "case_name": "Образование", + "solution_version": 6, + "errors": [], + "can_apply": true, + "is_submitted": true, + "removed_assignments_count": 0, + "score_count": 4, + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/50/management/solutions/68/case/apply/", + "status": 200, + "request": { + "case_name": "Образование", + "preview_token": "" + }, + "response": { + "solution_id": 68, + "solution_version": 7, + "updated_at": "2026-10-25T18:25:16.739417Z" + }, + "cacheControl": "private, no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/50/management/solutions/68/members/remove/preview/", + "status": 200, + "request": { + "user_id": 34 + }, + "response": { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "member": { + "role": "Аналитик", + "user": { + "id": 34, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T18:25:14.508117Z", + "is_captain": false, + "specialization": null + }, + "solution_version": 7, + "is_submitted": true, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 4, + "errors": [], + "can_apply": true, + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/50/management/solutions/68/members/remove/apply/", + "status": 200, + "request": { + "user_id": 34, + "preview_token": "" + }, + "response": { + "solution_id": 68, + "solution_version": 8, + "updated_at": "2026-10-25T18:25:17.023912Z" + }, + "cacheControl": "private, no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/68/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "published_at": "2026-10-25T18:25:15.456936Z", + "fields": [ + { + "id": 320, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 321, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 322, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 323, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 324, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 325, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [ + { + "field_id": 320, + "value": "Образование" + }, + { + "field_id": 321, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 322, + "value": 0 + }, + { + "field_id": 323, + "value": false + }, + { + "field_id": 324, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-50.pdf" + } + }, + { + "field_id": 325, + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c" + } + ], + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + }, + "cacheControl": "no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/50/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [68] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "solution_version": 8, + "is_submitted": true, + "is_archived": false, + "score_count": 4, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "errors": [] + } + ], + "score_count": 4, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "can_apply": true, + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/50/management/solutions/archive/apply/", + "status": 200, + "request": { + "ids": [68], + "preview_token": "" + }, + "response": { + "changed_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 50, + "solution_id": 68, + "name": "Сданное решение hackathon-competitive" + }, + "solution_version": 9, + "archived_at": "2026-10-25T18:25:17.291952Z" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 50, + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/50/published-solutions/68/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 51, + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": "no-store" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/51/solutions/", + "status": 201, + "request": { + "name": "Сданное решение hackathon-noncompetitive" + }, + "response": { + "id": 70, + "program_id": 51, + "version": 1, + "name": "Сданное решение hackathon-noncompetitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:17.719046Z", + "updated_at": "2026-10-25T18:25:17.719046Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:17.723325Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 51, + "revision": "9739c26eb756e7196e9c489de23e5ee86d2cb1f160421d257838f1ae1a9a0eba", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/51/solutions/70/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 34, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 74, + "version": 1, + "program": { + "id": 51, + "name": "Публикация решений hackathon-noncompetitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 51, + "solution_id": 70, + "name": "Сданное решение hackathon-noncompetitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T18:25:17.838143Z", + "updated_at": "2026-10-25T18:25:17.838143Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/74/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 74, + "version": 2, + "program": { + "id": 51, + "name": "Публикация решений hackathon-noncompetitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 51, + "solution_id": 70, + "name": "Сданное решение hackathon-noncompetitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T18:25:17.838143Z", + "updated_at": "2026-10-25T18:25:17.930729Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 70, + "name": "Сданное решение hackathon-noncompetitive" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/51/solutions/70/", + "status": 200, + "request": null, + "response": { + "id": 70, + "program_id": 51, + "version": 3, + "name": "Сданное решение hackathon-noncompetitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:17.719046Z", + "updated_at": "2026-10-25T18:25:17.934320Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:17.723325Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:17.926001Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 51, + "revision": "9739c26eb756e7196e9c489de23e5ee86d2cb1f160421d257838f1ae1a9a0eba", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/51/solutions/70/invitations/", + "status": 201, + "request": { + "expected_solution_version": 3, + "recipient_user_id": 35, + "role": "Дизайнер" + }, + "response": { + "invitation": { + "id": 75, + "version": 1, + "program": { + "id": 51, + "name": "Публикация решений hackathon-noncompetitive qa01cp-20261025-55fd4d0c" + }, + "solution": { + "kind": "solution", + "program_id": 51, + "solution_id": 70, + "name": "Сданное решение hackathon-noncompetitive" + }, + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 35, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Дизайнер", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T18:25:18.200861Z", + "updated_at": "2026-10-25T18:25:18.201866Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 4 + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/51/solutions/70/", + "status": 200, + "request": null, + "response": { + "id": 70, + "program_id": 51, + "version": 4, + "name": "Сданное решение hackathon-noncompetitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:17.719046Z", + "updated_at": "2026-10-25T18:25:18.204930Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:17.723325Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:17.926001Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 51, + "revision": "9739c26eb756e7196e9c489de23e5ee86d2cb1f160421d257838f1ae1a9a0eba", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/51/solutions/70/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "9739c26eb756e7196e9c489de23e5ee86d2cb1f160421d257838f1ae1a9a0eba", + "answers": [ + { + "field_id": 326, + "value": "Город" + }, + { + "field_id": 327, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 328, + "value": 0 + }, + { + "field_id": 329, + "value": false + }, + { + "field_id": 330, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-51.pdf" + } + }, + { + "field_id": 331, + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c" + } + ] + }, + "response": { + "id": 70, + "program_id": 51, + "version": 5, + "name": "Сданное решение hackathon-noncompetitive", + "captain_id": 33, + "created_at": "2026-10-25T18:25:17.719046Z", + "updated_at": "2026-10-25T18:25:18.428362Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:17.723325Z", + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T18:25:17.926001Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 51, + "revision": "9739c26eb756e7196e9c489de23e5ee86d2cb1f160421d257838f1ae1a9a0eba", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 326, + "value": "Город" + }, + { + "field_id": 327, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 328, + "value": 0 + }, + { + "field_id": 329, + "value": false + }, + { + "field_id": 330, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-51.pdf" + } + }, + { + "field_id": 331, + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c" + } + ], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "role": "other", + "method": "POST", + "path": "/programs/51/solutions/", + "status": 201, + "request": { + "name": "Черновик hackathon-noncompetitive" + }, + "response": { + "id": 71, + "program_id": 51, + "version": 1, + "name": "Черновик hackathon-noncompetitive", + "captain_id": 36, + "created_at": "2026-10-25T18:25:18.542531Z", + "updated_at": "2026-10-25T18:25:18.542531Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T18:25:18.545996Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 51, + "revision": "9739c26eb756e7196e9c489de23e5ee86d2cb1f160421d257838f1ae1a9a0eba", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T18:25:10.954965Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/?limit=1", + "status": 200, + "request": null, + "response": { + "program_id": 51, + "count": 2, + "next": "http://127.0.0.1:8877/programs/51/published-solutions/?limit=1&offset=1", + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 70, + "name": "Сданное решение hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/70/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 70, + "name": "Сданное решение hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [ + { + "field_id": 326, + "value": "Город" + }, + { + "field_id": 327, + "value": "Материалы qa01cp-20261025-55fd4d0c" + }, + { + "field_id": 328, + "value": 0 + }, + { + "field_id": 329, + "value": false + }, + { + "field_id": 330, + "value": { + "url": "https://cdn.test/uploads/qa01cp-20261025-55fd4d0c-51.pdf" + } + }, + { + "field_id": 331, + "value": "https://example.org/repository/qa01cp-20261025-55fd4d0c" + } + ], + "team": { + "members": [ + { + "user": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + }, + { + "user": { + "id": 34, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "is_captain": false + } + ], + "accepted_count": 2 + } + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/?search=%D0%A1%D0%B4%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5+%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5+hackathon-noncompetitive", + "status": 200, + "request": null, + "response": { + "program_id": 51, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 70, + "name": "Сданное решение hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/?limit=1&offset=1", + "status": 200, + "request": null, + "response": { + "program_id": 51, + "count": 2, + "next": null, + "previous": "http://127.0.0.1:8877/programs/51/published-solutions/?limit=1&offset=0", + "results": [ + { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 71, + "name": "Черновик hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "captain": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 1 + } + ] + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/71/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 71, + "name": "Черновик hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [], + "team": { + "members": [ + { + "user": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + }, + "cacheControl": "no-store" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/51/published-solutions/71/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 71, + "name": "Черновик hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [], + "team": { + "members": [ + { + "user": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + }, + "cacheControl": "no-store" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/51/published-solutions/71/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 71, + "name": "Черновик hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [], + "team": { + "members": [ + { + "user": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + }, + "cacheControl": "no-store" + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/51/published-solutions/71/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 71, + "name": "Черновик hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [], + "team": { + "members": [ + { + "user": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + }, + "cacheControl": "no-store" + }, + { + "role": "other", + "method": "GET", + "path": "/programs/51/published-solutions/71/", + "status": 200, + "request": null, + "response": { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 71, + "name": "Черновик hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "fields": [ + { + "id": 326, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": ["Город", "Образование"], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 327, + "type": "textarea", + "purpose": "other", + "label": "Материалы", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 328, + "type": "number", + "purpose": "other", + "label": "Число", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 329, + "type": "checkbox", + "purpose": "other", + "label": "Флаг", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 330, + "type": "file", + "purpose": "presentation", + "label": "Презентация", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 331, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "answers": [], + "team": { + "members": [ + { + "user": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/solutions/70/", + "status": 401, + "request": null, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "role": "pending", + "method": "GET", + "path": "/programs/51/solutions/70/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/?submission=all", + "status": 400, + "request": null, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [ + { + "path": "submission", + "code": "invalid", + "detail": "Неизвестное поле.", + "field_id": null + } + ], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/70/?unknown=1", + "status": 400, + "request": null, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [ + { + "path": "unknown", + "code": "invalid", + "detail": "Неизвестное поле.", + "field_id": null + } + ], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/9007199254740992/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/", + "status": 401, + "request": null, + "response": { + "code": "authentication_required", + "detail": "{'detail': ErrorDetail(string='Given token not valid for any token type', code='token_not_valid'), 'code': ErrorDetail(string='token_not_valid', code='token_not_valid'), 'messages': [{'token_class': ErrorDetail(string='AccessToken', code='token_not_valid'), 'token_type': ErrorDetail(string='access', code='token_not_valid'), 'message': ErrorDetail(string='Token is invalid or expired', code='token_not_valid')}]}", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 51, + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 70, + "name": "Сданное решение hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "captain": { + "id": 33, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 2 + }, + { + "application": { + "kind": "solution", + "program_id": 51, + "solution_id": 71, + "name": "Черновик hackathon-noncompetitive" + }, + "published_at": "2026-10-25T18:25:18.652630Z", + "captain": { + "id": 36, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "accepted_count": 1 + } + ] + }, + "cacheControl": "no-store" + } +] diff --git a/docs/qa/program-solutions-competitive-publication-20261025/native-targets.txt b/docs/qa/program-solutions-competitive-publication-20261025/native-targets.txt new file mode 100644 index 000000000..36a80ad07 --- /dev/null +++ b/docs/qa/program-solutions-competitive-publication-20261025/native-targets.txt @@ -0,0 +1,31 @@ +partner_programs.tests.test_solution_access_boundaries +partner_programs.tests.test_solution_administrative_history +partner_programs.tests.test_solution_answers +partner_programs.tests.test_solution_archive_actions +partner_programs.tests.test_solution_case_actions +partner_programs.tests.test_solution_case_analytics +partner_programs.tests.test_solution_competitive_publication +partner_programs.tests.test_solution_creation_switch +partner_programs.tests.test_solution_entrypoints +partner_programs.tests.test_solution_expert_reminders +partner_programs.tests.test_solution_field_filters +partner_programs.tests.test_solution_forms +partner_programs.tests.test_solution_foundation +partner_programs.tests.test_solution_invitation_roles +partner_programs.tests.test_solution_invitations +partner_programs.tests.test_solution_manager_analytics +partner_programs.tests.test_solution_member_actions +partner_programs.tests.test_solution_participant_reminders +partner_programs.tests.test_solution_program_routing +partner_programs.tests.test_solution_publication +partner_programs.tests.test_solution_registration_actions +partner_programs.tests.test_solution_role_widget +partner_programs.tests.test_solution_submission +partner_programs.tests.test_solution_submission_actions +partner_programs.tests.test_solution_team +project_rates.tests.test_solution_admin +project_rates.tests.test_solution_allocation +project_rates.tests.test_solution_cabinet +project_rates.tests.test_solution_evaluation +project_rates.tests.test_solution_results +project_rates.tests.test_solution_results_export diff --git a/docs/qa/program-solutions-competitive-publication-20261025/prospective-dev-integration.json b/docs/qa/program-solutions-competitive-publication-20261025/prospective-dev-integration.json new file mode 100644 index 000000000..9659e0479 --- /dev/null +++ b/docs/qa/program-solutions-competitive-publication-20261025/prospective-dev-integration.json @@ -0,0 +1,22 @@ +{ + "status": "PROSPECTIVE_INTEGRATION_CONFLICT", + "currentCheckoutGraphConflicts": {}, + "overlayWithObservedDevMigration": { + "partner_programs": [ + "0031_programcertificate_rendered_batch", + "0038_solution_registration_removal_audit" + ] + }, + "readOnlyPreview": true, + "noMigrationsApplied": true, + "upstreamMigrationSHA256": "2fe9364b189b5f7db866d67a77eb3d171a131f250215a621ac7bb280ec005e66", + "backendDev": "587a9155b9f53d9e50981a9939a7007c19fd6ecc", + "frontendDev": "08414b794dd5b34de5471dddaa19cd0af7f4a167", + "backendSourceBase": "c68a004e858fd181dd513d01641dc841746178fe", + "frontendSourceBase": "1590518422749a19f375c4de0d781748beaf2f3a", + "gitMergeTreePreview": { + "backend": "dc7cd12561372bdac6e42f7e7f7426932568ed5a", + "frontend": "39b8c405f1408eb7e160aa03f2069dd6f10be49c" + }, + "requiredNext": "Reconcile observed dev certificate migration and model compatibility; preserve all existing migration names/data; no branch merge/rebase/force-push performed" +} diff --git a/docs/qa/program-solutions-competitive-publication-20261025/retention-final.json b/docs/qa/program-solutions-competitive-publication-20261025/retention-final.json new file mode 100644 index 000000000..999360476 --- /dev/null +++ b/docs/qa/program-solutions-competitive-publication-20261025/retention-final.json @@ -0,0 +1,27 @@ +{ + "status": "PASS", + "oldModelsUnchanged": 25, + "baseline": "54631ba6896d1d18cadf7f855ca1fd1b83b0a2396a72b28e2b61b720f1783169", + "afterAllNativeFrontendBuildChecks": true, + "globalRegistrationAuditsRetained": 7, + "fixtures": [ + { + "program": 49, + "solution": 66, + "scoreRows": 4, + "archived": true + }, + { + "program": 50, + "solution": 68, + "scoreRows": 4, + "archived": true + }, + { + "program": 51, + "solution": 70, + "scoreRows": 0, + "archived": false + } + ] +} From 2dc10ce11c207053ba1ab998f51bad07ba27d15e Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sun, 25 Oct 2026 23:09:43 +0300 Subject: [PATCH 44/47] docs(qa): verify frontend against current dev --- .../README.md | 17 +++ .../eslint-targets.txt | 103 ++++++++++++++++ .../evidence.json | 112 ++++++++++++++++++ .../prospective-source.json | 18 +++ .../source-retention.json | 10 ++ 5 files changed, 260 insertions(+) create mode 100644 docs/qa/program-solutions-dev-integration-20261025/README.md create mode 100644 docs/qa/program-solutions-dev-integration-20261025/eslint-targets.txt create mode 100644 docs/qa/program-solutions-dev-integration-20261025/evidence.json create mode 100644 docs/qa/program-solutions-dev-integration-20261025/prospective-source.json create mode 100644 docs/qa/program-solutions-dev-integration-20261025/source-retention.json diff --git a/docs/qa/program-solutions-dev-integration-20261025/README.md b/docs/qa/program-solutions-dev-integration-20261025/README.md new file mode 100644 index 000000000..7bdfad574 --- /dev/null +++ b/docs/qa/program-solutions-dev-integration-20261025/README.md @@ -0,0 +1,17 @@ + + +# Совместимость фронтенда решений с актуальным dev + +Статус: **prospective FE tests/build/types/lint/format PASS; первый выпуск не завершен, не READY TO MERGE**. Исходники feature `ca1b203a34fddedd6f7d7d6a45752073697dfd7c` и observed dev `08414b794dd5b34de5471dddaa19cd0af7f4a167` проверены вместе в tree `2f4f4f23b39b198247c3da01685685debf534faa`. Git merge-tree/archive использован только для отдельного TEMP snapshot; branch merge/rebase/force-push/deploy не выполнялись. Certificate changes из dev не переносились вручную в рабочую ветку. Здесь только QA documentation. + +- **2987 tests /484 files/402.27s/PASS** в объединенном снимке: прежние решения/команда/приглашения/сдача/экспертный UI, Project program flow и обновленные certificate tests. Это unit/component Vitest coverage, не новый реальный browser/HTTP journey. Прежние2955 сохраняют свой source ref; counts не складываются. +- **Development build /57.127s/PASS**, strict C00 types PASS, **ESLint103 TS paths/PASS**. [Targets](eslint-targets.txt) включают все ACMR TypeScript files общего base15905184..prospective tree. Сборка показывает прежние Sass deprecation warnings. Build и тесты выполнялись последовательно; локальные PostgreSQL native suites к тому моменту завершены. +- Locked **Prettier3.8.3/2073 tracked supported files/PASS** в prospective snapshot; проверка читает список git ls-tree, не притворяется рабочей Git-веткой. Новые QA files форматируются отдельно перед commit. +- [Source provenance](prospective-source.json):3681 tracked files, manifest SHA1199f2ca…, dependency package/lock byte-exact относительно feature. Использован junction к тем же node_modules без install. [После тестов/сборки](source-retention.json) все3681 исходных файла byte-exact с Git archive; tracked mutations отсутствуют. Generated outputs/cache остаются в собственном TEMP snapshot; исходная рабочая ветка не менялась. +- C00 **1.21.0/156 schemas/59 operations/305 scenarios** не менялся; shared contract content совпадает с Git после учета CRLF Windows/archive и LF Git. Byte differences не выдаются за domain change. API/права/дизайн этой проверкой не менялись. + +[Backend compatibility evidence](https://github.com/PROCOLLAB-github/api/blob/a787f0ee7d1cb01f9e777fbe2e0fa60999cc0805/docs/qa/program-solutions-dev-compatibility-20261025/README.md): исходная dev0031 и пустая0039 исправляют две leaf nodes без изменения старой истории,439 PostgreSQL/OK без skips, final focused15 PG/OK без skips,15 SQLite/OK с3 PG-only skips. Guarded main forward/recheck сохранил27 прежних моделей SHA74555320…, Project4→4 и CPUB/C2. Actual synthetic BE020cc78c содержит родителей dev587a9155+a787f0ee, runtime совпадает с проверенным prospective source tree. [PG40227938298](https://github.com/PROCOLLAB-github/api/actions/runs/40227938298) уже прошел graph/models/migrate/locking steps, full suite in_progress при упаковке; [general40227933396](https://github.com/PROCOLLAB-github/api/actions/runs/40227933396) также без полного PASS. Native Windows459 PDF attempt не завершился в FontConfiguration; renderer/assertions не менялись, требуется поддерживаемый Linux suite. + +[Evidence и log hashes](evidence.json) сохраняют две harness ошибки: ранний запуск Vitest до окончания extraction/junction дал MODULE_NOT_FOUND; две первые source assertions не учли CRLF/LF. До реального suite готовность snapshot проверена; source verifier исправлен по read-only diagnostics, код/контракты не менялись. Полные logs/raw source reports остаются в TEMP/design-artifacts, credentials не публикуются. + +Оригинальные26 AC не менялись:23 PASS, AC18/19 OPEN, AC21 отдельно BE10. D02/D03 approval и competitive unsent draft eligibility уже запрошены; новый Angular manager/public UI, полный QA01/REL01 и весь первый выпуск остаются в scope. Согласованные D00v3/D01 не перерисовывались. Старые Project-программы автоматически не переключаются, данные не удаляются. diff --git a/docs/qa/program-solutions-dev-integration-20261025/eslint-targets.txt b/docs/qa/program-solutions-dev-integration-20261025/eslint-targets.txt new file mode 100644 index 000000000..4eba1a3b9 --- /dev/null +++ b/docs/qa/program-solutions-dev-integration-20261025/eslint-targets.txt @@ -0,0 +1,103 @@ +projects/social_platform/src/app/api/paths/app-routes.ts +projects/social_platform/src/app/api/program/program-widget-presentation.ts +projects/social_platform/src/app/api/program/use-cases/program-certificates.use-case.ts +projects/social_platform/src/app/api/program/use-cases/program-role-widget-contract.spec.ts +projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts +projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts +projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts +projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.ts +projects/social_platform/src/app/api/solution/solution-archive-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-case-actions-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-error.ts +projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-expert-reminder-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-field-filter-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-form-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-history-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-manager-analytics-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-member-actions-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-navigation.ts +projects/social_platform/src/app/api/solution/solution-publication-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-registration-removal-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-results-export-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-submission-actions-contract.spec.ts +projects/social_platform/src/app/api/solution/solution-team-contract.spec.ts +projects/social_platform/src/app/api/solution/use-cases/solution-analytics.use-cases.spec.ts +projects/social_platform/src/app/api/solution/use-cases/solution-analytics.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-archive.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-case-actions.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-form.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-history.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-manager-analytics.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-member-actions.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-publication.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-registration-removal.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-results-export.use-case.ts +projects/social_platform/src/app/api/solution/use-cases/solution-submission-actions.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution-team.use-cases.ts +projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts +projects/social_platform/src/app/domain/program/ports/program-certificates.repository.port.ts +projects/social_platform/src/app/domain/program/program-certificates.model.ts +projects/social_platform/src/app/domain/program/program-management.model.ts +projects/social_platform/src/app/domain/program/program-role-widget.model.ts +projects/social_platform/src/app/domain/program/program.model.ts +projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts +projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts +projects/social_platform/src/app/domain/solution/solution-contract.model.ts +projects/social_platform/src/app/domain/solution/solution-form-rebase.spec.ts +projects/social_platform/src/app/domain/solution/solution-form-rebase.ts +projects/social_platform/src/app/domain/solution/solution-values.ts +projects/social_platform/src/app/infrastructure/adapters/program/program-certificates-http.adapter.ts +projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts +projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts +projects/social_platform/src/app/ui/pages/evaluation/programs/evaluation-programs.component.spec.ts +projects/social_platform/src/app/ui/pages/evaluation/programs/evaluation-programs.component.ts +projects/social_platform/src/app/ui/pages/office/nav/nav.component.ts +projects/social_platform/src/app/ui/pages/office/office.component.spec.ts +projects/social_platform/src/app/ui/pages/office/office.component.ts +projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.ts +projects/social_platform/src/app/ui/pages/program/detail/main/role-widget/program-role-widget.component.spec.ts +projects/social_platform/src/app/ui/pages/program/detail/main/role-widget/program-role-widget.component.ts +projects/social_platform/src/app/ui/pages/program/management/certificates/certificate-canvas.component.ts +projects/social_platform/src/app/ui/pages/program/management/certificates/certificates.component.spec.ts +projects/social_platform/src/app/ui/pages/program/management/certificates/certificates.component.ts +projects/social_platform/src/app/ui/pages/program/management/management.component.ts +projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.spec.ts +projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts +projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.spec.ts +projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.ts +projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.spec.ts +projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts +projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.spec.ts +projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts +projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts +projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts +projects/social_platform/src/app/ui/pages/projects/edit/edit.component.spec.ts +projects/social_platform/src/app/ui/routes/program/detail.routes.ts +projects/social_platform/src/app/ui/routes/program/solution-leave.guard.ts +projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts +projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts +projects/social_platform/src/app/ui/widgets/project-invite/member-invite-form.component.ts +projects/social_platform/src/app/ui/widgets/project-invite/participant-picker.component.ts +projects/social_platform/src/app/ui/widgets/project-invite/project-invite-dialog.component.ts +projects/social_platform/src/app/ui/widgets/project-invite/project-invite-role-input.component.ts +projects/social_platform/src/app/ui/widgets/project-invite/project-member-invite-modal.component.ts +projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.spec.ts +projects/social_platform/src/app/ui/widgets/project-invite/solution-member-invite-modal.component.ts +projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.spec.ts +projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts +projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.spec.ts +projects/social_platform/src/app/ui/widgets/solution-invitation-inbox/solution-invitation-inbox.component.ts +projects/social_platform/src/app/utils/certificate-layout.spec.ts +projects/social_platform/src/app/utils/certificate-layout.ts +projects/social_platform/src/app/utils/evaluation-criterion-control.ts +projects/social_platform/src/app/utils/solution-field-controls.spec.ts +projects/social_platform/src/app/utils/solution-field-controls.ts +projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.ts +projects/ui/src/lib/components/primitives/button/button.directive.spec.ts +projects/ui/src/lib/components/primitives/button/button.directive.ts +tools/verify-solution-contract-types.ts diff --git a/docs/qa/program-solutions-dev-integration-20261025/evidence.json b/docs/qa/program-solutions-dev-integration-20261025/evidence.json new file mode 100644 index 000000000..08f90efee --- /dev/null +++ b/docs/qa/program-solutions-dev-integration-20261025/evidence.json @@ -0,0 +1,112 @@ +{ + "status": "PROSPECTIVE FRONTEND CHECKS PASS; full release incomplete", + "feature": "ca1b203a34fddedd6f7d7d6a45752073697dfd7c", + "observedDev": "08414b794dd5b34de5471dddaa19cd0af7f4a167", + "prospectiveTree": "2f4f4f23b39b198247c3da01685685debf534faa", + "trackedSourceManifestSha256": "1199f2ca996a9fc9cb430687e4b9bdb7bb169e03452bbc17da7a2f93d991ddcc", + "tests": { + "files": 484, + "tests": 2987, + "result": "PASS", + "seconds": 402.27, + "command": "node node_modules/vitest/vitest.mjs run --maxWorkers=2 --minWorkers=1", + "mockScope": "Existing unit/component Vitest tests; not a real HTTP/browser journey" + }, + "build": { + "command": "npm run build:pr", + "result": "PASS", + "seconds": 57.127, + "warnings": "Existing Sass deprecation warnings" + }, + "strictTypes": { + "command": "npm run test:solution-contract", + "result": "PASS" + }, + "eslint": { + "result": "PASS", + "targets": 103, + "selection": "All ACMR TS paths common base15905184..prospective tree2f4f4f23" + }, + "format": { + "prettier": "3.8.3", + "locked": "3.8.3", + "checked": 2073, + "failures": [] + }, + "sourceRetention": { + "status": "PASS", + "snapshotFiles": 3681, + "archiveAndInitialManifestMatch": true, + "initialManifestSha256": "1199f2ca996a9fc9cb430687e4b9bdb7bb169e03452bbc17da7a2f93d991ddcc", + "trackedFilesChangedByBuild": [], + "realWorkspaceRuntimeChanged": false, + "noBranchMerge": true, + "noRealProgramSwitch": true + }, + "contract": { + "version": "1.21.0", + "schemas": 156, + "operations": 59, + "scenarios": 305, + "changed": false, + "gitBytesComparedAfterCRLFNormalization": true + }, + "backend": { + "head": "a787f0ee7d1cb01f9e777fbe2e0fa60999cc0805", + "product": "7cfe01be1c2caba38ad04fef0ab6d68147df1d02", + "synthetic": "020cc78ce6e45421cb1222c62a98611d8018562c", + "prospectiveTree": "2b93d6a99be4a1ac250912b59bea7b220ebf195f", + "native439": "OK no skips", + "focusedPostgreSQL15": "OK no skips", + "focusedSQLite15": "OK 3 PostgreSQL-only skips", + "mainModelsPreserved": 27, + "mainHash": "74555320f955379af8b51b609d5f27e6e6663fd9a2a8756ad871a9a383103ba6", + "linuxPostgreSQLRun": 40227938298, + "linuxGeneralRun": 40227933396, + "linuxFullSuiteStatus": "in_progress at packaging; not PASS", + "graphMigrateAndLockingSteps": "PASS on actual synthetic merged source" + }, + "logs": { + "tests": { + "name": "procollab-solution-dev-integration-fe-tests.log", + "bytes": 81450, + "sha256": "8b6cc3fc6b14292fe64a5b6281c0eb4551813be586830626c653e11973db2d3c" + }, + "build": { + "name": "procollab-solution-dev-integration-fe-build.log", + "bytes": 135488, + "sha256": "5b958dfe4b3b7dd075fbf32acfe7306af8f599c0a8e067b892de84947bad2b5c" + }, + "types": { + "name": "procollab-solution-dev-integration-fe-types.log", + "bytes": 198, + "sha256": "9c49338a6bd87668f4525097ed9dbc34a680477f0387588bce160c2d95c749d8" + }, + "eslint": { + "name": "procollab-solution-dev-integration-fe-eslint.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "format": { + "name": "procollab-solution-dev-integration-fe-format.log", + "bytes": 90, + "sha256": "a243ef1f35e07668d780781fe0a1b6c964edaea690e4e19ab4649359d3701bcb" + }, + "tests-before-snapshot": { + "name": "procollab-solution-dev-integration-fe-tests-before-snapshot.log", + "bytes": 841, + "sha256": "6f658f1b7e71820f7564e5cd828ad6e9729a5f89669549430bd8627d9ce83e73" + } + }, + "retainedHarnessFailures": [ + "Early Vitest launch before archive/junction completion: MODULE_NOT_FOUND; retained log; not product failure", + "First two source equality assertions compared CRLF Windows/archive bytes against Git LF bytes; diagnostic proved normalized equality, corrected verifier without changing source/contracts" + ], + "limits": [ + "No new Angular manager/public screens, design approval, browser/HTTP/CDN/production proof", + "D02/D03 approvals and competitive unsent draft publication eligibility already pending", + "Original26 AC unchanged:23 PASS, AC18/19 OPEN, AC21 separate BE10", + "Complete BE01-BE09/FE01-FE08/C00/D00/QA01/REL01 first release remains in scope", + "No branch merge/rebase/force-push/deploy/real program switches" + ] +} diff --git a/docs/qa/program-solutions-dev-integration-20261025/prospective-source.json b/docs/qa/program-solutions-dev-integration-20261025/prospective-source.json new file mode 100644 index 000000000..5064fcdfa --- /dev/null +++ b/docs/qa/program-solutions-dev-integration-20261025/prospective-source.json @@ -0,0 +1,18 @@ +{ + "status": "PASS source provenance; runtime results in evidence.json", + "feature": "ca1b203a34fddedd6f7d7d6a45752073697dfd7c", + "observedDev": "08414b794dd5b34de5471dddaa19cd0af7f4a167", + "prospectiveTree": "2f4f4f23b39b198247c3da01685685debf534faa", + "trackedFiles": 3681, + "trackedSourcesManifestSha256": "1199f2ca996a9fc9cb430687e4b9bdb7bb169e03452bbc17da7a2f93d991ddcc", + "packageInputsUnchanged": true, + "noDependencyInstall": true, + "noBranchMerge": true, + "buildOutputsWillStayInOwnedSnapshot": true, + "sharedContracts": { + "examples.json": "1c7f220fc5f956dd9aa4043f68c52a1918ed18b048d56bffa31a1afe3499edcc", + "openapi.json": "49b6e5741e64aac9169e4d5e945a9e054020a1f4d807ac0b015e025c32860af2", + "README.md": "6dc80bae4062f523a1e88fcd3edbc849c3b1fe423e20ebb0b83ece836d5500f2" + }, + "dependencyReuse": "Owned TEMP junction to existing unchanged node_modules; no install" +} diff --git a/docs/qa/program-solutions-dev-integration-20261025/source-retention.json b/docs/qa/program-solutions-dev-integration-20261025/source-retention.json new file mode 100644 index 000000000..4e4a1bd84 --- /dev/null +++ b/docs/qa/program-solutions-dev-integration-20261025/source-retention.json @@ -0,0 +1,10 @@ +{ + "status": "PASS", + "snapshotFiles": 3681, + "archiveAndInitialManifestMatch": true, + "initialManifestSha256": "1199f2ca996a9fc9cb430687e4b9bdb7bb169e03452bbc17da7a2f93d991ddcc", + "trackedFilesChangedByBuild": [], + "realWorkspaceRuntimeChanged": false, + "noBranchMerge": true, + "noRealProgramSwitch": true +} From 8bf663c7db666ee447cd26742871e66d1b52cb2f Mon Sep 17 00:00:00 2001 From: DarKylX Date: Mon, 26 Oct 2026 00:04:30 +0300 Subject: [PATCH 45/47] docs(program-solutions): preserve existing navigation in review prototypes --- .prettierignore | 3 + .../.gitattributes | 3 + .../README.md | 27 + .../bundle-qa.json | 123 + .../d03-annotation-qa.json | 25 + .../navigation-qa-evidence.json | 116 + .../packaging-attempts.json | 10 + .../prototype/assets/Mont-Bold.woff2 | Bin 0 -> 45380 bytes .../prototype/assets/Mont-Regular.woff2 | Bin 0 -> 45924 bytes .../prototype/assets/Mont-SemiBold.woff2 | Bin 0 -> 46044 bytes .../prototype/assets/logo.svg | 28 + .../prototype/assets/search.svg | 3 + .../prototype/manager-v1/admin.js | 694 +++ .../manager-v1/analytics-reference.css | 845 +++ .../prototype/manager-v1/app.js | 807 +++ .../manager-v1/drilldown-reference.css | 968 ++++ .../prototype/manager-v1/global-reference.css | 4685 +++++++++++++++++ .../prototype/manager-v1/icons.js | 22 + .../prototype/manager-v1/index.html | 116 + .../media/Mont-Regular-5LPDNI7X.ttf | Bin 0 -> 144348 bytes .../media/Mont-Regular-T6L2NNOO.woff | Bin 0 -> 65164 bytes .../media/Mont-Regular-UA5RCVYG.woff2 | Bin 0 -> 45924 bytes .../media/Mont-SemiBold-7XOUXTAI.woff2 | Bin 0 -> 46044 bytes .../media/Mont-SemiBold-M6WLAC2K.ttf | Bin 0 -> 143908 bytes .../media/Mont-SemiBold-WNUGA77E.woff | Bin 0 -> 65064 bytes .../manager-v1/results-reference.css | 681 +++ .../prototype/manager-v1/styles.css | 1178 +++++ .../prototype/public-v1/app.js | 103 + .../prototype/public-v1/index.html | 30 + .../prototype/public-v1/source-reference.css | 1205 +++++ .../prototype/public-v1/styles.css | 20 + .../screenshots-clean/analytics-1440.png | Bin 0 -> 203584 bytes .../screenshots-clean/analytics-390.png | Bin 0 -> 209632 bytes .../screenshots-clean/solutions-1440.png | Bin 0 -> 100619 bytes .../screenshots-clean/solutions-390.png | Bin 0 -> 110550 bytes .../analytics-1440.png | Bin 0 -> 210494 bytes .../analytics-390.png | Bin 0 -> 213235 bytes .../solutions-1440.png | Bin 0 -> 107210 bytes .../solutions-390.png | Bin 0 -> 115275 bytes .../source-manifest.json | 45 + .../verify-bundle.cjs | 142 + 41 files changed, 11879 insertions(+) create mode 100644 docs/design/program-solutions-scope-20261025/.gitattributes create mode 100644 docs/design/program-solutions-scope-20261025/README.md create mode 100644 docs/design/program-solutions-scope-20261025/bundle-qa.json create mode 100644 docs/design/program-solutions-scope-20261025/d03-annotation-qa.json create mode 100644 docs/design/program-solutions-scope-20261025/navigation-qa-evidence.json create mode 100644 docs/design/program-solutions-scope-20261025/packaging-attempts.json create mode 100644 docs/design/program-solutions-scope-20261025/prototype/assets/Mont-Bold.woff2 create mode 100644 docs/design/program-solutions-scope-20261025/prototype/assets/Mont-Regular.woff2 create mode 100644 docs/design/program-solutions-scope-20261025/prototype/assets/Mont-SemiBold.woff2 create mode 100644 docs/design/program-solutions-scope-20261025/prototype/assets/logo.svg create mode 100644 docs/design/program-solutions-scope-20261025/prototype/assets/search.svg create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/admin.js create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/analytics-reference.css create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/app.js create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/drilldown-reference.css create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/global-reference.css create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/icons.js create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/index.html create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-5LPDNI7X.ttf create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-T6L2NNOO.woff create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-UA5RCVYG.woff2 create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-7XOUXTAI.woff2 create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-M6WLAC2K.ttf create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-WNUGA77E.woff create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/results-reference.css create mode 100644 docs/design/program-solutions-scope-20261025/prototype/manager-v1/styles.css create mode 100644 docs/design/program-solutions-scope-20261025/prototype/public-v1/app.js create mode 100644 docs/design/program-solutions-scope-20261025/prototype/public-v1/index.html create mode 100644 docs/design/program-solutions-scope-20261025/prototype/public-v1/source-reference.css create mode 100644 docs/design/program-solutions-scope-20261025/prototype/public-v1/styles.css create mode 100644 docs/design/program-solutions-scope-20261025/screenshots-clean/analytics-1440.png create mode 100644 docs/design/program-solutions-scope-20261025/screenshots-clean/analytics-390.png create mode 100644 docs/design/program-solutions-scope-20261025/screenshots-clean/solutions-1440.png create mode 100644 docs/design/program-solutions-scope-20261025/screenshots-clean/solutions-390.png create mode 100644 docs/design/program-solutions-scope-20261025/screenshots-historical-run/analytics-1440.png create mode 100644 docs/design/program-solutions-scope-20261025/screenshots-historical-run/analytics-390.png create mode 100644 docs/design/program-solutions-scope-20261025/screenshots-historical-run/solutions-1440.png create mode 100644 docs/design/program-solutions-scope-20261025/screenshots-historical-run/solutions-390.png create mode 100644 docs/design/program-solutions-scope-20261025/source-manifest.json create mode 100644 docs/design/program-solutions-scope-20261025/verify-bundle.cjs diff --git a/.prettierignore b/.prettierignore index 195c990bb..e743364ba 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,6 @@ # Frozen C00 source documents are copied byte-for-byte from the backend contract package. /docs/contracts/program-solutions-v1/*.json + +# Frozen synthetic design sources preserve the exact bytes checked by browser QA. +/docs/design/program-solutions-scope-20261025/prototype/** diff --git a/docs/design/program-solutions-scope-20261025/.gitattributes b/docs/design/program-solutions-scope-20261025/.gitattributes new file mode 100644 index 000000000..ee9093e62 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/.gitattributes @@ -0,0 +1,3 @@ +# Keep browser-verified fixture bytes and manifest hashes stable across checkouts. +prototype/** -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol +source-manifest.json -text diff --git a/docs/design/program-solutions-scope-20261025/README.md b/docs/design/program-solutions-scope-20261025/README.md new file mode 100644 index 000000000..3b58bf9db --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/README.md @@ -0,0 +1,27 @@ + + +# D02/D03: граница изменения заявок + +Статус: **DESIGN APPROVAL REQUIRED**. Уже заданные вопросы D02/D03 остаются открытыми. Это исправление scope pending макета по прямому уточнению пользователя, а не новый согласованный дизайн и не Angular implementation. D00v3/D01 остаются согласованными. + +[Макет организатора](http://127.0.0.1:8769/manager-v1/?screen=analytics&format=kch&state=ready) сохраняет прежние семь вкладок и их порядок: **Участники → Заявки → Эксперты → Результаты → Аналитика → Настройки → Сертификаты**. Только у solution-программы «Проекты» называется «Заявки». Старые Project-программы сохраняют название, маршруты и данные. Никакие другие сведения, разделы и настройки не перерабатываются. Результаты/аналитика адаптируются только в части источника заявок, принятого состава, материалов и связанного текста. + +Оценивание остается внутри заявок; отдельная общая вкладка не добавляется. Выбор экрана в макете вынесен в служебный select «Экран макета». Четыре незатронутых раздела показаны контекстом: нажатие сообщает о сохранении существующего интерфейса, не меняет URL/основное содержимое. В Angular сохраняются их реальные компоненты и маршруты. «Команда» остается второй вкладкой заявки, приглашение использует прежний согласованный путь поиска участника и свободной роли. + +Предыдущий [D02 checkpoint](../program-solutions-manager-d02-20261025/README.md) и его130 checks/screenshots — исторические материалы до исправления навигации. Они не доказывают новую навигацию. Текущие runtime sources макета, общие Mont assets и [source hashes](source-manifest.json) находятся в `prototype/`. Это synthetic данные; макет не вызывает прикладной API, не отправляет сообщения и не создает XLSX. + +[Navigation QA](navigation-qa-evidence.json): **48 layout checks** (3формата ×4экрана ×320/390/768/1440), **12 полных сравнений основного DOM** со старым макетом и **12 кликов** на незатронутые разделы. Angular tabs array совпадает с observed dev08414b79 после нормализации LF. Оценивание подсвечивает «Заявки»;0 JS errors/0 domain requests/0 failed assets. Headless Edge использовал exact frozen/current HTML+app JS через route.fulfill, остальные local assets с8769; это не проверка production HTTP. Cua kernel оказался недоступен и после reset, поэтому использован существующий проектный Playwright pattern. Старые130 evidence не перезаписывались. + +Сохраненные `screenshots-historical-run/` относятся к этому48-check run и могут содержать transient notice после тестового нажатия. Desktop/mobile analytics просмотрены визуально; это не новая полная130-проверка административных действий. Первая попытка helper остановилась из-за whitespace в labels; следующая — после блокировки внешней инъекции антивируса в direct HTTP HTML. Внешний запрос не был выполнен; в Git не сохраняются его query/token. Итоговая frozen-source проверка завершилась PASS. + +[Публичный макет D03](http://127.0.0.1:8769/public-v1/) получил только актуальную служебную аннотацию C001.21: API поддерживает submitted конкурсные и saved неконкурсные решения. Публикация несданных конкурсных черновиков остается отдельным открытым вопросом. [81 jsdom comparison](d03-annotation-qa.json) (3формата ×9состояний ×3маршрута) подтверждают неизменность product DOM/title и расположение аннотации вне `.public-shell`;0 domain requests/errors. Это copy-only source check, не новое browser/visual QA D03. Новый public Angular UI остается впереди. + +Для автономного просмотра запустите статический сервер с корнем `prototype` на свободном порту, затем откройте `/manager-v1/` или `/public-v1/`. Существующий8769 и пользовательские tabs не менялись. Source files скопированы byte-exact с текущего макета; общий CSS/Mont не менялся. Только frozen `prototype/**` исключен из автоформатирования ради сохранения browser source hashes; остальные новые документы проходят locked formatter. + +`verify-bundle.cjs` принимает `PLAYWRIGHT_MODULE`, `BROWSER_CHANNEL` (по умолчанию msedge) и необязательный новый `SCOPE_QA_OUTPUT`. Он открывает собственный headless browser context, отдает все static bytes через route.fulfill из `prototype/` и запрещает внешние/прикладные запросы. Сервер, backend и БД не нужны. Это проверка упаковки, Mont, навигации и mobile overflow, а не повтор всех130 сценариев. Existing output directory не перезаписывается. + +Итоговая [проверка автономного bundle](bundle-qa.json): **17 layout checks PASS**,24 source hashes совпали, Mont действительно загружен, семь вкладок/вложенное оценивание/сохранение остальных разделов подтверждены;0 JS errors/0 denied requests. [Чистые desktop](screenshots-clean/analytics-1440.png) и [mobile](screenshots-clean/analytics-390.png) screenshots просмотрены визуально; уведомление после клика в них отсутствует. Первый packaging smoke нашел пропущенные зависимости CSS: существующие media fonts и search.svg. Они скопированы byte-exact из текущего artifact root, без замены шрифта или изменения HTML/JS/CSS; [попытка и исправление](packaging-attempts.json) сохранены отдельно. Этот smoke не заменяет48/81-check scopes и не подтверждает прикладной API. + +Исходные26 AC сохранены:23 PASS, AC18/19 OPEN, AC21 отдельно BE10. Весь BE01–BE09/FE01–FE08/C00/D00/QA01/REL01 остается в scope. **Не реализовано**: новый Angular organizer/reminder/actions/results/public UI и общая приемка. Никаких merge/deploy/переноса действующих программ. + +[procollab-design-screen](Z:/Документы/PROCOLLAB-dev/procollab-design-system-v1/.codex/skills/procollab-design-screen/SKILL.md) требует «Дождись явного approval пользователя». Поэтому зависимая Angular реализация следует после уже ожидаемого согласования D02/D03; повторный вопрос не отправлялся. diff --git a/docs/design/program-solutions-scope-20261025/bundle-qa.json b/docs/design/program-solutions-scope-20261025/bundle-qa.json new file mode 100644 index 000000000..20c022daf --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/bundle-qa.json @@ -0,0 +1,123 @@ +{ + "status": "PASS", + "sourceManifestSha256": "f419a5f072a26df4e66bf14ad32497425545fe68ac5000307fa2e72ec2be1ad3", + "sourceFiles": 24, + "browser": "headless msedge", + "scope": "Frozen synthetic bundle smoke; not HTTP/API/Angular or complete design QA", + "checks": [ + { + "screen": "solutions", + "width": 320, + "noOverflow": true + }, + { + "screen": "solutions", + "width": 390, + "noOverflow": true + }, + { + "screen": "solutions", + "width": 768, + "noOverflow": true + }, + { + "screen": "solutions", + "width": 1440, + "noOverflow": true + }, + { + "screen": "analytics", + "width": 320, + "noOverflow": true + }, + { + "screen": "analytics", + "width": 390, + "noOverflow": true + }, + { + "screen": "analytics", + "width": 768, + "noOverflow": true + }, + { + "screen": "analytics", + "width": 1440, + "noOverflow": true + }, + { + "screen": "public", + "format": "kch", + "width": 320, + "noOverflow": true + }, + { + "screen": "public", + "format": "kch", + "width": 390, + "noOverflow": true + }, + { + "screen": "public", + "format": "kch", + "width": 1440, + "noOverflow": true + }, + { + "screen": "public", + "format": "hack", + "width": 320, + "noOverflow": true + }, + { + "screen": "public", + "format": "hack", + "width": 390, + "noOverflow": true + }, + { + "screen": "public", + "format": "hack", + "width": 1440, + "noOverflow": true + }, + { + "screen": "public", + "format": "noncompetitive", + "width": 320, + "noOverflow": true + }, + { + "screen": "public", + "format": "noncompetitive", + "width": 390, + "noOverflow": true + }, + { + "screen": "public", + "format": "noncompetitive", + "width": 1440, + "noOverflow": true + } + ], + "errors": [], + "deniedRequests": [], + "screenshots": [ + { + "name": "solutions-390.png", + "sha256": "46f73ec3d473896e11e99532872f1e8faf34c75762a19f9c11e28b949482330b" + }, + { + "name": "solutions-1440.png", + "sha256": "94fd95713cb7556efa8f4f780274f0d9bea1c186ec691ce5aba9af72a6266142" + }, + { + "name": "analytics-390.png", + "sha256": "e5f6874b93477987546f4e98d30768a9f0e23ca537b753f7c7675b1e385953ef" + }, + { + "name": "analytics-1440.png", + "sha256": "45be658f93f5a53bd703cbe28868e39650daef78a708ca6d0becb9347ab9da0c" + } + ] +} diff --git a/docs/design/program-solutions-scope-20261025/d03-annotation-qa.json b/docs/design/program-solutions-scope-20261025/d03-annotation-qa.json new file mode 100644 index 000000000..1dd8b20c5 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/d03-annotation-qa.json @@ -0,0 +1,25 @@ +{ + "status": "PASS", + "scope": "Copy-only prototype API annotation, jsdom comparison, not browser/visual QA", + "cases": 81, + "formats": 3, + "states": 9, + "routes": 3, + "productDomAndTitleUnchanged": true, + "annotationOutsideProduct": true, + "domainRequests": 0, + "jsdomErrors": [], + "approval": "DESIGN APPROVAL REQUIRED; existing pending question unchanged", + "before": { + "index.html": "18cc66a613dd95ae1f968d0b2f93f62a4737c9659d9ce0c7724f0be479a51e6c", + "app.js": "07da85fdc5d5149c12ab5e8214b2ba1fcf2eea39276a89913d9f97a814a3f5ef", + "design-handoff.md": "78a97dd77eedec3c28edb36ca80f424339386028885bb5fef3070fb222e2e96b" + }, + "after": { + "index.html": "8817a0e6b0a8e958d7db4b70225ca0f567803bcce57b3298b561c7da22ab50c7", + "app.js": "bd5a92a4428ad22a24afe5fba3d3dc5006dceafea33793d1697b15bac9abeff7", + "design-handoff.md": "5538c5c8790d21f03285b729fd914c61d41bdaec9582924c139cb7749e053614" + }, + "oldQaAndScreenshotsPreserved": true, + "newVisualOrBrowserQa": false +} diff --git a/docs/design/program-solutions-scope-20261025/navigation-qa-evidence.json b/docs/design/program-solutions-scope-20261025/navigation-qa-evidence.json new file mode 100644 index 000000000..eeee9a972 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/navigation-qa-evidence.json @@ -0,0 +1,116 @@ +{ + "status": "PASS", + "scope": "D02 navigation restoration only; no Angular/runtime or backend changes", + "tabs": [ + "Участники", + "Заявки", + "Эксперты", + "Результаты", + "Аналитика", + "Настройки", + "Сертификаты" + ], + "currentAngularNavArrayUnchanged": true, + "checks": [ + { + "format": "kch", + "screen": "solutions", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "kch", + "screen": "evaluation", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "kch", + "screen": "results", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "kch", + "screen": "analytics", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "hack", + "screen": "solutions", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "hack", + "screen": "evaluation", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "hack", + "screen": "results", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "hack", + "screen": "analytics", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "noncompetitive", + "screen": "solutions", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "noncompetitive", + "screen": "evaluation", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "noncompetitive", + "screen": "results", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + }, + { + "format": "noncompetitive", + "screen": "analytics", + "widths": [320, 390, 768, 1440], + "productBodyIdentical": true + } + ], + "layoutChecks": 48, + "unchangedSectionClicks": 12, + "productBodiesCompared": 12, + "evaluationInsideApplicationTab": true, + "errors": [], + "domainRequests": [], + "failedAssets": [], + "browser": "headless Edge via bundled Playwright; cua kernel unavailable after reset", + "approval": "DESIGN APPROVAL REQUIRED remains pending", + "old130EvidencePreserved": true, + "sources": { + "index.html": { + "before": "444cf18af00a8e7448e46abc76380147ed7c96437a172beca4b6d8a8a5269420", + "after": "bb28ce87f3c982df53d593c9d7507d946bbc0d74dad2c47568af18dec9c81d20" + }, + "app.js": { + "before": "73eb3cdd678174e7151afbfb7869ba62a7c0a37f4f652b2287d3c9aca1a07343", + "after": "33d8d4b3c36ea2004a1b9dee2f4dde24f70c86cee06cee09a0b4636401cf7d8b" + }, + "styles.css": { + "before": "1d32f0bb7e80462359a864cd27f747684726ab9dd09b117f07d760cd5ce37418", + "after": "1d32f0bb7e80462359a864cd27f747684726ab9dd09b117f07d760cd5ce37418" + }, + "design-handoff.md": { + "before": "e2b61d799a2483da68aea0c79bb8023ffc591db1cf01f859f7ec3003005f1054", + "after": "a460d187f0a70b95775acd08f0ef550e463c1fef24a28894944e95f4536afb88" + } + } +} diff --git a/docs/design/program-solutions-scope-20261025/packaging-attempts.json b/docs/design/program-solutions-scope-20261025/packaging-attempts.json new file mode 100644 index 000000000..69101c890 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/packaging-attempts.json @@ -0,0 +1,10 @@ +{ + "attempt1": { + "status": "FAIL", + "cause": "Autonomous bundle omitted existing global-reference.css media dependencies and assets/search.svg; local-only route denied missing assets", + "externalOrDomainRequestsCompleted": 0, + "productSourceChanges": false, + "outputPreservedOutsideGit": "procollab-d02-scope-bundle-clean-20261025" + }, + "correction": "Copy exact existing assets from served artifact root; update source manifest; no font substitution or changes to live source" +} diff --git a/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-Bold.woff2 b/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1093fe0dabcd2db5554fb520e78d120b8dd0c3ef GIT binary patch literal 45380 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 literal 0 HcmV?d00001 diff --git a/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-Regular.woff2 b/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0b682c4d22575741a9e041a3b4743bf695a1851a GIT binary patch literal 45924 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%0Dbgle2vT3l+3G-UW8U@w~ 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 ZeviYmuzIf!O|7P{y+?%; + + + + + + + + + + + + + + + diff --git a/docs/design/program-solutions-scope-20261025/prototype/assets/search.svg b/docs/design/program-solutions-scope-20261025/prototype/assets/search.svg new file mode 100644 index 000000000..cff6dc991 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/assets/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/admin.js b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/admin.js new file mode 100644 index 000000000..d442663ec --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/admin.js @@ -0,0 +1,694 @@ +/** + * D02 administrative flows. All records below are synthetic and live only in this tab. + * + * @format + */ + +window.managerAdmin = (() => { + let initialized = false, + listScope = "active", + selected = new Set(), + registrations = [], + events = [], + audits = [], + ratings = []; + let sequence = 0; + let archiveFilter = null; + const response = () => document.querySelector("#action-response")?.value || "ready"; + const memberNames = [ + ["Мария Климова", "Анна Петрова"], + ["Андрей Сергеев"], + ["Ольга Смирнова"], + [], + ]; + function init() { + if (initialized) return; + initialized = true; + let id = 1; + registrations = works.flatMap((work, index) => { + work.archived = false; + work.caseName = [ + "Образование", + "Городская среда", + "Образование", + scenario === "ready-draft" ? "Образование" : null, + ][index]; + work.submitted = index < 3; + work.submittedAt = null; + work.members = [work.captain, ...memberNames[index]].map((name, n) => ({ + id: id++, + name, + captain: n === 0, + role: n === 0 ? "Капитан" : n === 1 ? "Разработчик" : "Дизайнер", + })); + work.pending = []; + work.teamSize = work.members.length; + return work.members.map(person => ({ ...person, workId: work.id, pending: false })); + }); + const pending = { id: id++, name: "Константин Шперлингш", captain: false, role: "Разработчик" }; + works[0].pending.push(pending); + registrations.push( + { ...pending, workId: 301, pending: true }, + ...["Виктория Орлова", "Наталья Волкова", "Артем Семенов"].map(name => ({ + id: id++, + name, + workId: null, + captain: false, + pending: false, + })), + ); + ratings = originalAssignments.filter(row => row.completed).map(row => ({ ...row })); + } + const scores = work => (competitive() ? ratings.filter(row => row.workId === work.id) : []); + const scoreCount = work => scores(work).length * 3; + const assigned = work => + competitive() && distributed() ? assignments.filter(row => row.workId === work.id) : []; + const registrationCount = () => { + init(); + return scenario === "empty" ? 0 : registrations.length; + }; + const snapshot = work => ({ + name: workName(work), + caseName: work.caseName, + members: structuredClone(work.members), + answers: fields() + .filter(field => applicable(work, field)) + .map(field => ({ label: field.label, value: answer(work, field.id) })), + archived: work.archived, + submitted: competitive() && work.submitted, + }); + const btn = (action, label, attrs = "", style = "secondary") => + ``; + const workBy = id => works.find(work => work.id === Number(id)); + function isArchived(id) { + init(); + return !!workBy(id)?.archived; + } + function listWorks() { + init(); + return scenario === "empty" + ? [] + : works.filter(work => work.archived === (listScope === "archived")); + } + function listToolbar() { + return `
${btn("admin-people", "Участники программы")}${btn("admin-audits", "История удаления регистраций", "", "link")}
`; + } + function effectRows(items) { + return `
${items.map(([label, value]) => `
${label}
${esc(value)}
`).join("")}
`; + } + function team(work) { + return `

Принятые участники: ${work.members.length}

${work.members + .map( + person => + `
${esc(person.name)}${person.role}${work.archived ? "" : person.captain ? 'Капитана нельзя исключить из состава' : btn("admin-remove-member", "Исключить", `data-id="${work.id}" data-person="${person.id}"`, "link")}
`, + ) + .join( + "", + )}${work.pending.length ? `

Ожидают подтверждения · ${work.pending.length}

Приглашенные появятся в составе после принятия приглашения.

${work.pending.map(person => `
${esc(person.name)}${person.role}Приглашение отправлено
`).join("")}
` : ""}`; + } + function solution(work) { + const archived = work.archived; + const title = workName(work); + const subtitle = archived + ? "Архив. Доступно только чтение." + : competitive() && work.submitted + ? `Сдано ${work.submittedAt || "24 октября 2026, 17:30"}. Административные исключения фиксируются в истории.` + : competitive() + ? "Черновик. Команда еще работает над решением." + : "Сохраненные материалы. Отдельная сдача не предусмотрена."; + const tabs = ``; + const content = + modal.tab === "team" + ? team(work) + : `

Кейс

${esc(work.caseName || "Не выбран")}

${archived ? "" : btn("admin-case", "Сменить кейс", `data-id="${work.id}"`, "link")}
${fields() + .filter(field => applicable(work, field)) + .map(field => { + const value = answer(work, field.id); + return `

${esc(field.label)}

${!filled(value) ? "Не заполнено" : ["file", "url"].includes(field.type) ? btn("material", esc(value), "", "link") : field.type === "checkbox" ? (value ? "Да" : "Нет") : esc(value)}

`; + }) + .join("")}
`; + const body = `${archived ? '

Материалы и состав на момент снятия доступны для чтения. Оценки сохранены; новые оценки недоступны. Решение исключено из активных списков и оценивания.

' : ""}${tabs}
${content}
${btn("admin-history", "История изменений", `data-id="${work.id}"`, "link")}${!archived && scores(work).length ? btn("work-scores", "Сохраненные оценки", `data-id="${work.id}"`, "link") : ""}
${!archived && competitive() && !work.submitted ? btn("admin-submit", "Сдать за команду", `data-id="${work.id}"`, "primary") : ""}${archived ? "" : btn("admin-archive", competitive() ? "Снять с участия" : "Переместить в архив", `data-id="${work.id}"`, "quiet")}
`; + return { title, subtitle, body }; + } + function peopleView() { + const rows = + scenario === "empty" + ? [] + : registrations.filter(person => + modal.kind === "without-visit" + ? person.id >= 9 + : modal.kind === "without-team" + ? !works.some( + work => !work.archived && work.members.some(member => member.id === person.id), + ) + : true, + ); + return { + title: + modal.kind === "without-team" + ? "Участники без команды" + : modal.kind === "without-visit" + ? "Участники еще не заходили на платформу" + : "Участники программы", + subtitle: "Регистрация относится к этой программе. Учетная запись участника сохраняется.", + body: `

Выберите регистрации для просмотра последствий удаления. Ожидающее приглашение не означает участие в команде.

${rows + .map(person => { + const linked = workBy(person.workId), + work = + linked && + (linked.members.some(member => member.id === person.id) || + linked.pending.some(member => member.id === person.id)) + ? linked + : null; + return ``; + }) + .join( + "", + )}
УчастникЗаявкаСтатус
${work ? btn("solution", esc(workName(work)), `data-id="${work.id}" data-focus="person-${person.id}"`, "link") : "Нет решения"}${work?.archived ? "Решение в архиве" : work?.pending.some(item => item.id === person.id) ? "Ожидает приглашение" : person.captain ? "Капитан" : work ? "В команде" : "Без команды"}
${rows.length ? "" : '

Регистраций нет.

'}`, + }; + } + function removalEffects(ids) { + const picked = registrations.filter(person => ids.includes(person.id)); + const changes = works + .filter(work => !work.archived) + .map(work => { + const captain = picked.some(person => person.id === work.members[0]?.id); + const members = captain ? [] : work.members.filter(person => ids.includes(person.id)); + const pending = work.pending.filter(person => captain || ids.includes(person.id)); + return { work, captain, members, pending }; + }) + .filter(item => item.captain || item.members.length || item.pending.length); + return { picked, changes }; + } + function previewBody() { + const work = workBy(modal.id); + let rows = [], + explanation = "", + label = "", + title = ""; + if (modal.operation === "submit") { + title = "Сдать решение за команду"; + label = "Подтвердить сдачу"; + const missing = submissionIssues(work); + rows = [ + ["Готовность решения", missing.length ? "Есть незаполненные поля" : "Готово к сдаче"], + ["Принятых участников", work.members.length], + ["Ожидающих приглашений, не входят в состав", work.pending.length], + ]; + return { + title, + label, + blocked: !!missing.length, + body: `

${esc(workName(work))}

${effectRows(rows)}${missing.length ? `` : '

После подтверждения материалы и принятый состав будут зафиксированы. Повторная сдача не изменит уже сданное решение.

'}`, + }; + } else if (modal.operation === "case") { + title = "Сменить кейс"; + label = "Подтвердить смену кейса"; + rows = [ + ["Текущий кейс", work.caseName || "Не выбран"], + ["Новый кейс", modal.caseName], + ["Назначений будет удалено", caseRemovedAssignments(work, modal.caseName).length], + ["Значений по критериям сохраняется", scoreCount(work)], + ]; + explanation = + "Материалы и команда сохраняются. Изменение кейса и снимка сдачи попадет в историю. Удаляются только назначения без оценок, не подходящие для нового кейса."; + } else if (modal.operation === "member") { + title = "Исключить участника из команды"; + label = "Исключить участника"; + const person = work.members.find(item => item.id === modal.person); + rows = [ + ["Участник", person?.name], + ["Останется в команде", work.members.length - 1], + ["Значений по критериям сохраняется", scoreCount(work)], + ["Назначений будет удалено", 0], + ]; + explanation = + "Состав и снимок сдачи будут обновлены. Прежний состав останется в истории. Регистрация в программе, материалы и оценки сохраняются."; + } else if (modal.operation === "archive") { + title = competitive() ? "Снять решение с участия" : "Переместить решение в архив"; + label = competitive() ? "Снять с участия" : "Переместить в архив"; + rows = [ + ["Решение", workName(work)], + ["Назначений будет удалено", assigned(work).length], + ["Ожидающих приглашений будет отозвано", work.pending.length], + ["Значений по критериям сохраняется", scoreCount(work)], + ]; + explanation = + "Материалы, состав и оценки сохраняются в архиве. Решение исчезнет из активных списков, оценивания и публичной выдачи. Возврат из архива не предусмотрен."; + } else { + title = "Удалить регистрации из программы"; + label = "Удалить регистрации"; + const { picked, changes } = removalEffects(modal.ids); + rows = [ + ["Регистраций будет удалено", picked.length], + ["Решений перейдет в архив", changes.filter(item => item.captain).length], + [ + "Участников будет исключено из состава", + changes.reduce((n, item) => n + item.members.length, 0), + ], + ["Приглашений будет отозвано", changes.reduce((n, item) => n + item.pending.length, 0)], + [ + "Назначений будет удалено", + changes.reduce((n, item) => n + (item.captain ? assigned(item.work).length : 0), 0), + ], + ]; + explanation = + "Учетные записи и участие в других программах сохраняются. Если выбран капитан, его решение перейдет в архив. Уже архивные материалы, состав и история остаются без изменений."; + return { + title, + label, + body: `

${picked.map(person => esc(person.name)).join(", ")}

${effectRows(rows)}

${explanation}

Последствия для решений

${changes.length ? changes.map(item => `

${esc(workName(item.work))}${item.captain ? "Архивирование с сохранением материалов, состава и оценок" : item.members.length ? "Исключение: " + item.members.map(person => esc(person.name)).join(", ") + ". Материалы, оценки и назначения сохраняются." : "Отзыв ожидающего приглашения. Принятый состав сохраняется."}

`).join("") : '

Активные решения не затронуты. Операция будет записана в общую историю регистраций.

'}
`, + }; + } + return { + title, + label, + body: `${work ? `

${esc(workName(work))}${competitive() && work.submitted ? " · Сданное решение" : ""}

` : ""}${effectRows(rows)}

${explanation}

`, + }; + } + function previewView() { + const data = previewBody(), + mode = modal.forceReady ? "ready" : response(); + const loading = mode === "loading"; + const legacy = mode === "legacy" && modal.operation === "registrations"; + let error = ""; + if (legacy) + error = + ''; + if (modal.failure) { + const copy = { + stale: [ + "Данные изменились", + "Последствия могли измениться. Загрузите новый предпросмотр и подтвердите действие еще раз.", + ], + error: [ + "Не удалось подтвердить действие", + "Результат операции пока неизвестен. Обновите данные перед повторным подтверждением.", + ], + "no-access": [ + "Нет доступа к действию", + "Ваши права изменились. Обновите данные программы.", + ], + }[modal.failure]; + error = ``; + } + return { + title: data.title, + subtitle: "Проверьте последствия перед подтверждением.", + body: loading + ? '

Загружаем последствия действия…

' + : `${data.body}${error}
${btn("admin-apply", modal.busy ? "Подтверждаем…" : data.label, `${data.blocked || legacy || modal.failure || modal.busy ? "disabled" : ""} aria-busy="${!!modal.busy}"`, "primary")}

В макете меняются только вымышленные данные текущей вкладки.

`, + }; + } + function historyView() { + const work = workBy(modal.id), + rows = events.filter(event => event.workId === work.id).toReversed(); + return { + title: "История изменений", + subtitle: workName(work), + body: rows.length + ? `
    ${rows.map(event => `
  1. ${esc(event.title)}

    ${event.time} · Организатор программы

    ${esc(event.description)}

    ${btn("admin-event", "Посмотреть до и после", `data-event="${event.id}" data-focus="event-${event.id}"`, "link")}
  2. `).join("")}
` + : '

Административных изменений еще нет

Здесь появятся смена кейса, исключение участников и архивирование.

', + }; + } + function snapshotHtml(data) { + return `

Кейс

${esc(data.caseName || "Не выбран")}

Состав · ${data.members.length}

${data.members.map(person => esc(person.name)).join(", ")}

Состояние

${data.archived ? "Архив" : data.submitted ? "Сдано" : "Сохранено"}

${data.answers.map(answer => `

${esc(answer.label)}

${answer.value === false ? "Нет" : answer.value === true ? "Да" : esc(answer.value ?? "Не заполнено")}

`).join("")}
`; + } + function eventView() { + const event = events.find(item => item.id === modal.event), + tab = modal.version || "before"; + return { + title: event.title, + subtitle: "Материалы и состав в момент административного изменения.", + body: `
${snapshotHtml(event[tab])}
${event.audit ? btn("admin-audit", "Открыть удаление регистраций", `data-audit="${event.audit}"`, "link") : ""}`, + }; + } + function auditView() { + if (modal.view === "admin-audits") + return { + title: "История удаления регистраций", + subtitle: "Общая история программы включает участников без решения.", + body: audits.length + ? `
    ${audits + .toReversed() + .map( + audit => + `
  1. Удалено регистраций: ${audit.people.length}

    ${audit.time} · Организатор программы

    ${audit.people.map(person => esc(person.name)).join(", ")}

    ${btn("admin-audit", "Посмотреть последствия", `data-audit="${audit.id}" data-focus="audit-${audit.id}"`, "link")}
  2. `, + ) + .join("")}
` + : '

Удалений регистраций еще нет

История появится после подтвержденного действия.

', + }; + const audit = audits.find(item => item.id === modal.audit); + return { + title: "Удаление регистраций", + subtitle: `${audit.time} · Организатор программы`, + body: `

${audit.people.map(person => esc(person.name)).join(", ")}

Учетные записи участников сохранены. Персональные ответы анкеты в этой истории не дублируются.

${ + audit.events.length + ? audit.events + .map(id => { + const event = events.find(item => item.id === id); + return `

${esc(event.before.name)}

${esc(event.description)}

${btn("admin-event", "Посмотреть до и после", `data-event="${event.id}"`, "link")}
`; + }) + .join("") + : "

Активные решения не затронуты. Архивные решения сохраняют прежний состав и историю.

" + }`, + }; + } + function renderModal() { + init(); + if (modal.view === "solution") return solution(workBy(modal.id)); + if (modal.view === "people" || modal.view === "admin-people") return peopleView(); + if (modal.view === "admin-preview") return previewView(); + if (modal.view === "admin-history") return historyView(); + if (modal.view === "admin-event") return eventView(); + if (["admin-audits", "admin-audit"].includes(modal.view)) return auditView(); + if (modal.view === "admin-case") { + const work = workBy(modal.id); + return { + title: "Сменить кейс", + subtitle: workName(work), + body: `

Выберите актуальный кейс программы. Затем проверьте последствия изменения.

${btn("back", "Отмена")}${btn("admin-case-preview", "Посмотреть последствия", modal.caseName ? "" : "disabled", "primary")}
`, + }; + } + return null; + } + function submissionIssues(work) { + return [ + ...(work.caseName ? [] : ["Кейс"]), + ...fields() + .filter( + field => field.required && applicable(work, field) && !filled(answer(work, field.id)), + ) + .map(field => field.label), + ]; + } + function caseRemovedAssignments(work, caseName) { + // Вымышленное ограничение эксперта Марии: только кейс «Образование». + return assigned(work).filter( + row => + !ratings.some(score => score.workId === row.workId && score.expert === row.expert) && + row.expert === 1 && + caseName !== "Образование", + ); + } + function addEvent(work, before, title, description, audit = null) { + const event = { + id: ++sequence, + workId: work.id, + time: new Date().toLocaleString("ru-RU"), + title, + description, + before, + after: snapshot(work), + audit, + }; + events.push(event); + return event; + } + function archiveWork(work, audit = null) { + const before = snapshot(work); + work.archived = true; + work.pending = []; + assignments = assignments.filter(row => row.workId !== work.id); + return addEvent( + work, + before, + "Решение перемещено в архив", + "Материалы, состав и оценки сохранены. Назначения удалены, ожидающие приглашения отозваны.", + audit, + ); + } + function applyChanges() { + const work = workBy(modal.id); + if (modal.operation === "submit") { + work.submitted = true; + work.submittedAt = new Date().toLocaleString("ru-RU"); + } else if (modal.operation === "case") { + const before = snapshot(work), + removed = new Set(caseRemovedAssignments(work, modal.caseName).map(row => row.id)); + work.caseName = modal.caseName; + assignments = assignments.filter(row => !removed.has(row.id)); + addEvent( + work, + before, + "Кейс изменен", + `${before.caseName || "Кейс не выбран"} → ${work.caseName}. Материалы, состав и оценки сохранены.`, + ); + } else if (modal.operation === "member") { + const before = snapshot(work), + person = work.members.find(item => item.id === modal.person); + work.members = work.members.filter(item => item.id !== modal.person); + work.teamSize = work.members.length; + work.pending = work.pending.filter(item => item.id !== modal.person); + addEvent( + work, + before, + "Участник исключен из команды", + `${person.name}. Регистрация, материалы, оценки и назначения сохранены.`, + ); + } else if (modal.operation === "archive") archiveWork(work); + else { + const { picked, changes } = removalEffects(modal.ids), + audit = { + id: audits.length + 1, + time: new Date().toLocaleString("ru-RU"), + people: structuredClone(picked.map(({ id, name }) => ({ id, name }))), + events: [], + }; + for (const item of changes) { + if (item.captain) audit.events.push(archiveWork(item.work, audit.id).id); + else { + const before = snapshot(item.work); + item.work.members = item.work.members.filter(person => !modal.ids.includes(person.id)); + item.work.teamSize = item.work.members.length; + item.work.pending = item.work.pending.filter(person => !modal.ids.includes(person.id)); + audit.events.push( + addEvent( + item.work, + before, + "Регистрации удалены", + item.members.length + ? `Исключены из состава: ${item.members.map(person => person.name).join(", ")}. Материалы, оценки и назначения сохранены.` + : "Ожидающие приглашения отозваны. Принятый состав сохранен.", + audit.id, + ).id, + ); + } + } + registrations = registrations.filter(person => !modal.ids.includes(person.id)); + selected.clear(); + audits.push(audit); + return { view: "admin-audit", audit: audit.id }; + } + return { + view: "solution", + id: work.id, + tab: modal.operation === "member" ? "team" : "solution", + }; + } + function navigate(next) { + modal ? openInner(next) : openModal(next); + } + function handleClick(target) { + const action = target.dataset.action; + if (action === "people") selected.clear(); + if (!action?.startsWith("admin-")) return false; + init(); + const id = Number(target.dataset.id); + if (action === "admin-scope") { + listScope = target.dataset.scope; + renderPage(); + document.querySelector(`[data-action="admin-scope"][data-scope="${listScope}"]`)?.focus(); + } + if (action === "admin-people") { + selected.clear(); + navigate({ view: "admin-people", kind: "all" }); + } + if (action === "admin-case") navigate({ view: "admin-case", id }); + if (action === "admin-case-preview") + navigate({ + view: "admin-preview", + operation: "case", + id: modal.id, + caseName: modal.caseName, + }); + if (action === "admin-remove-member") + navigate({ + view: "admin-preview", + operation: "member", + id, + person: Number(target.dataset.person), + }); + if (action === "admin-submit") navigate({ view: "admin-preview", operation: "submit", id }); + if (action === "admin-archive") navigate({ view: "admin-preview", operation: "archive", id }); + if (action === "admin-registration-preview" && selected.size) + navigate({ view: "admin-preview", operation: "registrations", ids: [...selected] }); + if (action === "admin-history") navigate({ view: "admin-history", id }); + if (action === "admin-event") + navigate({ view: "admin-event", event: Number(target.dataset.event) }); + if (action === "admin-version") { + modal.version = target.dataset.version; + renderModalOuter(); + dialog.querySelector(`[data-version="${modal.version}"]`).focus(); + } + if (action === "admin-audits") navigate({ view: "admin-audits" }); + if (action === "admin-audit") + navigate({ view: "admin-audit", audit: Number(target.dataset.audit) }); + if (action === "admin-refresh-preview") { + if (modal.failure === "no-access") { + closeModal(); + renderPage(); + showNotice("Данные программы обновлены в макете."); + } else { + modal.failure = null; + modal.forceReady = true; + renderModalOuter(); + $("#dialog-title").focus(); + } + } + if (action === "admin-apply" && !modal.busy && !modal.failure) { + if (previewBody().blocked) return true; + const mode = modal.forceReady ? "ready" : response(); + if (["stale", "error", "no-access"].includes(mode)) { + modal.failure = mode; + renderModalOuter(); + $("#dialog-title").focus(); + return true; + } + if ((mode === "legacy" && modal.operation === "registrations") || mode === "loading") + return true; + const current = modal; + modal.busy = true; + renderModalOuter(); + $("#dialog-title").focus(); + setTimeout(() => { + if (modal !== current) return; + const next = applyChanges(); + modal = next; + renderPage(); + renderModalOuter(); + $("#dialog-title").focus(); + showNotice( + current.operation === "submit" + ? "Решение сдано в макете. Материалы и принятый состав зафиксированы." + : "Действие выполнено в макете. Изменения доступны в истории.", + ); + }, 250); + } + return true; + } + // Имя внешнего renderer не совпадает с локальным renderModal этого дополнения. + const renderModalOuter = () => window.renderModal(); + document.addEventListener("change", event => { + if (event.target.name === "registration") { + event.target.checked + ? selected.add(Number(event.target.value)) + : selected.delete(Number(event.target.value)); + renderModalOuter(); + dialog.querySelector(`[name="registration"][value="${event.target.value}"]`)?.focus(); + } + if (event.target.name === "admin-case") { + modal.caseName = event.target.value; + renderModalOuter(); + dialog.querySelector('[name="admin-case"]')?.focus(); + } + }); + const teamCount = () => { + init(); + return scenario === "empty" + ? 0 + : works.filter(work => !work.archived).reduce((sum, work) => sum + work.members.length, 0); + }; + const withoutVisit = () => { + init(); + return scenario === "empty" ? 0 : registrations.filter(person => person.id >= 9).length; + }; + return { + init, + isArchived, + teamCount, + withoutVisit, + regionCount: index => { + init(); + return scenario === "empty" + ? 0 + : registrations.filter(person => + index === 0 + ? person.id <= 6 + : index === 1 + ? person.id > 6 && person.id <= 10 + : person.id > 10, + ).length; + }, + listWorks, + listToolbar, + isArchiveScope: () => listScope === "archived", + listFilter: () => + listScope === "archived" ? (archiveFilter ??= newFilter()) : filterState.solutions, + archiveFilters: filter => + `
`, + emptyArchive: () => listScope === "archived" && !listWorks().length, + registrationCount, + renderModal, + handleClick, + savedRating: id => { + init(); + return ratings.find(row => row.id === id); + }, + reset: () => { + initialized = false; + listScope = "active"; + archiveFilter = null; + selected.clear(); + events = []; + audits = []; + sequence = 0; + init(); + }, + }; +})(); diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/analytics-reference.css b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/analytics-reference.css new file mode 100644 index 000000000..4d3362f52 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/analytics-reference.css @@ -0,0 +1,845 @@ +/** @format */ +/** @format */ +.prototype-analytics { + /** @format */ +} +.prototype-analytics + .analytics + .ui-button.ui-button:not(.attention__action, .evaluation__assignment-action) { + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; +} +.prototype-analytics + .analytics + .ui-button.ui-button:not(.attention__action, .evaluation__assignment-action).ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-analytics + .analytics + .ui-button.ui-button:not(.attention__action, .evaluation__assignment-action) { + padding-inline: 16px; + } + .prototype-analytics + .analytics + .ui-button.ui-button:not(.attention__action, .evaluation__assignment-action)::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-analytics + .analytics + .ui-button.ui-button:not( + .attention__action, + .evaluation__assignment-action + ).ui-button--icon::after { + inset: -6px; + } +} +.prototype-analytics .analytics ::ng-deep app-button > button.button.button { + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; + min-width: 0; + max-height: none; +} +.prototype-analytics .analytics ::ng-deep app-button > button.button.button.ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-analytics .analytics ::ng-deep app-button > button.button.button { + padding-inline: 16px; + } + .prototype-analytics .analytics ::ng-deep app-button > button.button.button::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-analytics + .analytics + ::ng-deep + app-button + > button.button.button.ui-button--icon::after { + inset: -6px; + } +} +.prototype-analytics .analytics { + display: flex; + flex-direction: column; + gap: 16px; + padding: 4px 0 40px; + color: var(--black); +} +.prototype-analytics .analytics__header { + display: flex; + flex-direction: column; + gap: 16px; + justify-content: space-between; + padding: 20px; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; +} +.prototype-analytics .analytics__eyebrow { + margin-bottom: 4px; + color: var(--accent); +} +.prototype-analytics .analytics__title { + margin: 0; +} +.prototype-analytics .analytics__title-row { + display: flex; + gap: 16px; + align-items: center; + justify-content: space-between; + width: 100%; +} +.prototype-analytics .analytics__subtitle { + max-width: 600px; + margin-top: 8px; + color: var(--grey-for-text); +} +.prototype-analytics .analytics__heading-row { + position: relative; + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; +} +.prototype-analytics .analytics__heading-row h2, +.prototype-analytics .analytics__heading-row h3 { + font-weight: 600; + color: var(--black); +} +.prototype-analytics .analytics__heading-row h2 { + font-size: var(--ui-font-h3); + line-height: 22px; +} +.prototype-analytics .analytics__loading { + display: flex; + flex-direction: column; + gap: 16px; + color: var(--grey-for-text); +} +.prototype-analytics .exports { + width: min(100%, 540px); + min-width: 0; +} +.prototype-analytics .exports__actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} +.prototype-analytics .exports__actions app-button { + flex: 1 1 160px; + min-width: 0; +} +@media screen and (max-width: 520px) { + .prototype-analytics .exports__actions app-button { + flex-basis: 100%; + } +} +.prototype-analytics .exports__error { + margin-top: 8px; + color: var(--red-dark); +} +.prototype-analytics .summary { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} +@media screen and (min-width: 600px) { + .prototype-analytics .summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .summary { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} +.prototype-analytics .summary--loading { + margin-top: 4px; +} +.prototype-analytics .summary-card, +.prototype-analytics .analytics-card, +.prototype-analytics .regions, +.prototype-analytics .attention, +.prototype-analytics .activity { + min-width: 0; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; + box-shadow: var(--ui-chart-shadow); +} +.prototype-analytics .regions-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 12px; + align-items: start; +} +@media screen and (min-width: 1000px) { + .prototype-analytics .regions-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +.prototype-analytics .regions { + display: flex; + flex-direction: column; + padding: 16px 18px; +} +.prototype-analytics .regions .analytics__heading-row app-tooltip { + flex-shrink: 0; +} +.prototype-analytics .summary-card { + display: flex; + gap: 12px; + align-items: center; + min-height: 92px; + padding: 16px; +} +.prototype-analytics .summary-card__icon { + display: grid; + flex: 0 0 44px; + place-items: center; + width: 44px; + height: 44px; + color: var(--accent); + background: var(--accent-light); + border-radius: 12px; +} +.prototype-analytics + .summary-card[data-metric="submitted-projects"] + .prototype-analytics + .summary-card__icon { + color: var(--green-dark); + background: var(--green-light); +} +.prototype-analytics .summary-card__content { + min-width: 0; +} +.prototype-analytics .summary-card__label { + min-width: 0; + font-weight: 600; + color: var(--black); + overflow-wrap: anywhere; +} +.prototype-analytics .summary-card__value { + display: block; + margin-top: 6px; + line-height: 1; + color: var(--black); +} +@media screen and (max-width: 999px) { + .prototype-analytics .summary-card__value { + font-size: var(--ui-font-h1); + font-weight: 700; + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .summary-card__value { + font-size: 24px; + font-weight: 700; + } +} +.prototype-analytics .summary-card--skeleton { + min-height: 92px; + background: linear-gradient( + 90deg, + var(--light-gray) 20%, + var(--medium-grey-for-outline) 50%, + var(--light-gray) 80% + ); + background-size: 200% 100%; + animation: analytics-skeleton 1.3s ease-in-out infinite; +} +.prototype-analytics .analytics-grid { + display: grid; + gap: 12px; + align-items: start; +} +@media screen and (min-width: 1000px) { + .prototype-analytics .analytics-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .analytics-grid--priority > .attention, + .prototype-analytics .analytics-grid--priority > .program-deadlines { + align-self: stretch; + } +} +.prototype-analytics .analytics-card { + display: flex; + flex-direction: column; + min-height: 250px; + padding: 18px; +} +.prototype-analytics .analytics-card__title { + font-size: var(--ui-font-h3); + font-weight: 600; + line-height: 22px; + color: var(--black); +} +.prototype-analytics .analytics-card__subtitle { + font-size: var(--ui-font-body); + font-weight: 500; + line-height: 20px; + color: var(--dark-grey); +} +.prototype-analytics .analytics-card__section-label { + line-height: 18px; + color: var(--black); +} +@media screen and (max-width: 999px) { + .prototype-analytics .analytics-card__section-label { + font-size: var(--ui-font-body); + font-weight: 600; + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .analytics-card__section-label { + font-size: 13px; + font-weight: 600; + } +} +.prototype-analytics .analytics-empty-state { + display: grid; + flex: 1 1 auto; + place-items: center; + min-height: 96px; + margin: 16px 0 0; + color: var(--grey-for-text); + text-align: center; +} +.prototype-analytics .program-deadlines { + display: flex; + flex-direction: column; + align-self: start; + min-height: 0; +} +.prototype-analytics .program-deadlines__list { + display: grid; + gap: 8px; + align-content: start; + margin-top: 12px; +} +.prototype-analytics .program-deadlines__row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + padding: 10px 12px; + background: var(--light-gray); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-analytics .program-deadlines__content { + min-width: 0; +} +.prototype-analytics .program-deadlines__label { + font-size: 13px; + font-weight: 600; + line-height: 18px; + color: var(--black); +} +.prototype-analytics .program-deadlines__date, +.prototype-analytics .program-deadlines__hint { + display: block; + margin-top: 2px; + font-size: 12px; + line-height: 18px; + color: var(--dark-grey); +} +.prototype-analytics .program-deadlines__hint { + font-size: 11px; + line-height: 16px; +} +.prototype-analytics .program-deadlines__status { + font-size: 11px; + font-weight: 600; + line-height: 16px; + color: var(--dark-grey); + text-align: right; +} +.prototype-analytics .program-deadlines__row--active { + background: var(--accent-light); + border-color: var(--accent); +} +.prototype-analytics .program-deadlines__row--active .program-deadlines__status { + color: var(--accent-dark); +} +.prototype-analytics .program-deadlines__row--ended .program-deadlines__label, +.prototype-analytics .program-deadlines__row--ended .program-deadlines__date, +.prototype-analytics .program-deadlines__row--ended .program-deadlines__status { + color: var(--grey-for-text); +} +@media screen and (max-width: 420px) { + .prototype-analytics .program-deadlines__row { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + } + .prototype-analytics .program-deadlines__status { + text-align: left; + } +} +.prototype-analytics .analytics-grid--priority .attention__list, +.prototype-analytics .analytics-grid--priority .program-deadlines__list { + flex: 1; + grid-auto-rows: minmax(68px, 1fr); + gap: 8px; + align-content: stretch; + margin-top: 16px; +} +.prototype-analytics .attention__list .attention__action { + height: 100%; + padding-block: 10px; +} +.prototype-analytics .analytics-export-menu { + display: grid; + min-width: 220px; + padding: 6px; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 10px; + box-shadow: 0 8px 24px rgba(51, 51, 51, 0.12); +} +.prototype-analytics .analytics-export-menu button { + padding: 10px 12px; + font: inherit; + font-size: 12px; + color: var(--black); + text-align: left; + cursor: pointer; + background: transparent; + border: 0; + border-radius: 6px; +} +.prototype-analytics .analytics-export-menu button:hover, +.prototype-analytics .analytics-export-menu button:focus-visible { + background: var(--accent-light); + outline: 2px solid var(--accent); + outline-offset: -2px; +} +.prototype-analytics .cases-card__title { + font-size: var(--ui-font-h3); + font-weight: 600; + line-height: 22px; +} +.prototype-analytics .cases-card__list { + display: grid; + gap: 8px; + margin-top: 12px; +} +.prototype-analytics .cases-card__columns { + display: none; +} +.prototype-analytics .cases-card__expand { + align-self: flex-start; + margin-top: 16px; +} +.prototype-analytics .case-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + align-items: center; + width: 100%; + min-width: 0; + padding: 16px; + font: inherit; + text-align: left; + cursor: pointer; + background: var(--light-gray); + border: 1px solid transparent; + border-radius: 8px; +} +.prototype-analytics .case-row:hover { + border-color: var(--accent); +} +.prototype-analytics .case-row:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-analytics .case-row--without-case { + border: 1px dashed var(--medium-grey-for-outline); +} +.prototype-analytics .case-row--without-submission { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.prototype-analytics .case-row__name { + grid-column: 1/-1; + min-width: 0; + font-size: 14px; + font-weight: 600; + line-height: 21px; + color: var(--black); + overflow-wrap: anywhere; +} +.prototype-analytics .case-row__projects, +.prototype-analytics .case-row__metric { + display: grid; + gap: 6px; + min-width: 0; +} +.prototype-analytics .case-row__projects strong, +.prototype-analytics .case-row__metric strong { + font-size: 18px; + font-weight: 600; + line-height: 24px; + color: var(--black); +} +.prototype-analytics .case-row__metric strong { + order: -1; +} +.prototype-analytics .case-row__mobile-label { + font-size: 12px; + line-height: 18px; + color: var(--dark-grey); +} +.prototype-analytics .case-row__bar { + display: flex; + height: 6px; + overflow: hidden; + background: var(--grey-button); + border-radius: 3px; +} +.prototype-analytics .case-row__volume { + background: var(--accent); +} +@media screen and (min-width: 700px) { + .prototype-analytics .cases-card__columns, + .prototype-analytics .case-row { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(100px, 1fr) 80px 100px; + gap: 24px; + } + .prototype-analytics .cases-card__columns { + padding: 16px 16px 0; + font-size: 12px; + line-height: 18px; + color: var(--dark-grey); + } + .prototype-analytics .case-row__name { + grid-column: auto; + } + .prototype-analytics .case-row__mobile-label { + display: none; + } + .prototype-analytics .cases-card__columns--without-submission, + .prototype-analytics .case-row--without-submission { + grid-template-columns: minmax(0, 2fr) minmax(100px, 1fr) 100px; + } +} +.prototype-analytics .status-list { + display: grid; + gap: 10px; + margin-top: 16px; +} +.prototype-analytics .status-list li { + display: flex; + gap: 16px; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + background: var(--light-gray); + border-radius: 8px; +} +.prototype-analytics .status-list li span { + font-weight: 500; + line-height: 18px; + color: var(--black); +} +.prototype-analytics .status-list li strong { + line-height: 20px; + color: var(--black); +} +@media screen and (max-width: 999px) { + .prototype-analytics .status-list li strong { + font-size: var(--ui-font-h2); + font-weight: 700; + } +} +@media screen and (min-width: 1000px) { + .prototype-analytics .status-list li strong { + font-size: 17px; + font-weight: 700; + } +} +.prototype-analytics .status-list--compact { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; + margin-top: 8px; +} +.prototype-analytics .status-list--compact li { + flex-direction: column; + gap: 4px; + align-items: flex-start; +} +@media screen and (max-width: 520px) { + .prototype-analytics .status-list--compact { + grid-template-columns: 1fr; + } + .prototype-analytics .status-list--compact li { + flex-direction: row; + justify-content: space-between; + } +} +.prototype-analytics .evaluation { + grid-column: 1/-1; +} +.prototype-analytics .evaluation__mode { + margin-top: 2px; + font-weight: 500; + color: var(--dark-grey); +} +.prototype-analytics .analytics-bottom { + display: grid; + gap: 12px; + align-items: start; +} +.prototype-analytics .attention, +.prototype-analytics .activity { + padding: 18px; +} +.prototype-analytics .attention { + display: flex; + flex-direction: column; + align-self: start; + width: 100%; + padding-left: 15px; + background: var(--light-white); + border-color: var(--medium-grey-for-outline); + border-left: 4px solid var(--red); +} +.prototype-analytics .attention__action { + display: grid; + grid-template-columns: 32px minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + min-width: 0; + padding: 14px 12px; + font: inherit; + text-align: left; + cursor: pointer; + background: transparent; + border: 0; + border-radius: 8px; +} +.prototype-analytics .attention__action:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-analytics .attention__action:disabled { + cursor: default; +} +.prototype-analytics .attention__action span { + min-width: 0; + overflow-wrap: anywhere; +} +.prototype-analytics .attention--empty { + border-left-color: var(--green); +} +.prototype-analytics .attention__list { + display: grid; + gap: 10px; + margin-top: 16px; +} +.prototype-analytics .attention__list li { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px; + align-items: center; + padding: 0 12px 0 0; + background: var(--light-gray); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-analytics .attention__list li:not(.attention__row--disabled):hover, +.prototype-analytics .attention__list li:not(.attention__row--disabled):active { + background: var(--accent-light); + border-color: var(--accent); +} +.prototype-analytics .attention__list li app-tooltip:focus-visible { + border-radius: 50%; + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-analytics .attention__list li span { + font-weight: 500; + color: var(--black); +} +.prototype-analytics .attention__list strong, +.prototype-analytics .attention__list i { + color: var(--red-dark); +} +.prototype-analytics .attention__list strong { + font-weight: 700; +} +.prototype-analytics .attention__empty { + font-weight: 500; + color: var(--grey-for-text); +} +.prototype-analytics .activity { + display: flex; + flex-direction: column; +} +.prototype-analytics .activity__note { + margin-top: 2px; + color: var(--dark-grey); +} +.prototype-analytics .regions__export { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + margin-left: auto; + color: var(--accent); + cursor: pointer; + background: transparent; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-analytics .regions__export:disabled { + color: var(--dark-grey); + cursor: not-allowed; +} +.prototype-analytics .regions__export:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-analytics .regions__export-message { + margin-top: 8px; + font-size: var(--ui-font-small); + line-height: 18px; +} +.prototype-analytics .analytics-state { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + justify-content: center; + min-height: 260px; + padding: 24px; + color: var(--grey-for-text); + text-align: center; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; +} +.prototype-analytics .analytics-state i { + color: var(--accent); +} +.prototype-analytics .analytics-state--compact { + flex-direction: row; + justify-content: flex-start; + min-height: 0; + color: var(--black); + text-align: left; +} +.prototype-analytics .analytics-state--compact p { + margin-top: 4px; + color: var(--grey-for-text); +} +@keyframes analytics-skeleton { + from { + background-position: 100% 0; + } + to { + background-position: -100% 0; + } +} +@media screen and (max-width: 999px) { + .prototype-analytics .attention .attention__list .attention__action.ui-button { + width: 100%; + padding: 14px 12px; + font: inherit; + color: var(--black); + text-align: left; + background: transparent; + border: 0; + border-radius: 8px; + } + .prototype-analytics + .attention + .attention__list + .attention__action.ui-button:hover:not(:disabled), + .prototype-analytics + .attention + .attention__list + .attention__action.ui-button:active:not(:disabled) { + background: transparent; + border-color: transparent; + } +} +.prototype-analytics .attention__list li[data-attention-kind] { + --attention-tone: var(--accent); + --attention-fill: var(--accent-light); + min-height: 64px; + background: color-mix(in srgb, var(--attention-fill) 48%, var(--light-white)); + border-color: color-mix(in srgb, var(--attention-tone) 28%, var(--medium-grey-for-outline)); + box-shadow: 0 2px 0 color-mix(in srgb, var(--attention-tone) 10%, transparent); + transition: + background 150ms ease, + box-shadow 150ms ease; +} +.prototype-analytics .attention__list li[data-attention-kind][data-attention-kind="without-visit"] { + --attention-tone: var(--gold-dark); + --attention-fill: var(--gold-light); +} +.prototype-analytics + .attention__list + li[data-attention-kind][data-attention-kind="awaiting-evaluation"] { + --attention-tone: var(--green-dark); + --attention-fill: var(--green-light); +} +.prototype-analytics .attention__list li[data-attention-kind]:not(.attention__row--disabled):hover { + background: var(--attention-fill); + border-color: var(--attention-tone); + box-shadow: 0 3px 8px color-mix(in srgb, var(--attention-tone) 12%, transparent); +} +.prototype-analytics .attention__list li[data-attention-kind] .attention__icon { + display: grid; + place-items: center; + width: 32px; + height: 32px; + background: var(--attention-fill); + border-radius: 8px; +} +.prototype-analytics .attention__list li[data-attention-kind] .attention__icon i, +.prototype-analytics .attention__list li[data-attention-kind] strong { + color: var(--attention-tone); +} +.prototype-analytics .attention__list li[data-attention-kind] .attention__action span.text-body-10 { + font-size: 12px; + line-height: 18px; +} +.prototype-analytics .attention__list li[data-attention-kind] strong { + min-width: 26px; + padding: 3px 6px; + font-size: 14px; + text-align: center; + background: var(--attention-fill); + border-radius: 6px; +} +.prototype-analytics .attention__list li[data-attention-kind].attention__row--disabled { + --attention-tone: var(--grey-for-text); + --attention-fill: var(--light-gray); + box-shadow: none; +} diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/app.js b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/app.js new file mode 100644 index 000000000..44e20099e --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/app.js @@ -0,0 +1,807 @@ +/** + * D02 review prototype. Synthetic data only; no authentication or API requests. + * + * @format + */ + +const $ = (selector) => document.querySelector(selector); +const esc = (value) => + String(value ?? "").replace( + /[&<>"']/g, + (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ + c + ], + ); +const icon = (name) => + ``; +const params = new URLSearchParams(location.search); +let format = params.get("format") || "kch", + screen = params.get("screen") || "analytics", + scenario = params.get("state") || "ready"; +const names = [ + "Сервис поддержки студентов", + "Город без барьеров", + "Навигатор наставников", + "Лаборатория практики", +]; +const people = [ + ["Иван", "Токарев"], + ["Мария", "Климова"], + ["Анна", "Петрова"], +]; +const works = names.map((name, i) => ({ + id: 301 + i, + name, + caseName: ["Образование", "Городская среда", "Образование", null][i], + captain: [ + "Иван Токарев", + "Дарья Соколова", + "Александр Иванов", + "Елена Морозова", + ][i], + teamSize: [3, 2, 2, 1][i], + submitted: i < 3, +})); +const originalAssignments = [ + { id: 801, workId: 301, expert: 0, completed: true, value: 0 }, + { id: 802, workId: 301, expert: 1, completed: false, value: null }, + { id: 803, workId: 302, expert: 0, completed: true, value: 8 }, + { id: 804, workId: 302, expert: 2, completed: true, value: 7 }, +]; +let assignments = originalAssignments.map((row) => ({ ...row })); +const filterState = { solutions: newFilter(), results: newFilter() }; +let modal = null, + lastTrigger = null, + noticeTimer = null; +const dialog = $("#manager-dialog"); +function newFilter() { + return { + draft: "", + search: "", + submission: "all", + fieldId: "", + answer: "all", + expanded: false, + }; +} +const competitive = () => format !== "noncompetitive"; +const effectiveAssignments = () => (scenario === "empty" ? [] : assignments); +const distributed = () => competitive() && scenario !== "open-mode"; +const activeWorks = () => + scenario === "empty" + ? [] + : works.filter((work) => !managerAdmin.isArchived(work.id)); +function workName(work) { + return scenario === "long" && work.id === 301 + ? "Сервис поддержки студентов и молодых специалистов при подготовке к профессиональным конкурсам и первому трудоустройству" + : work.name; +} +function fields() { + return [ + { id: 2, label: "Краткое описание", type: "textarea", required: true }, + ...(format === "kch" + ? [{ id: 10, label: "Презентация PDF", type: "file", required: true }] + : [{ id: 11, label: "Репозиторий", type: "url", required: true }]), + { + id: 12, + label: format === "kch" ? "Видео решения" : "Демонстрация", + type: "url", + required: false, + }, + { id: 30, label: "Размер пилота", type: "number", required: false }, + { + id: 31, + label: "Разрешение на демонстрацию", + type: "checkbox", + required: false, + }, + { + id: 32, + label: format === "kch" ? "План доработки" : "Инструкция запуска", + type: "textarea", + required: true, + }, + ]; +} +function applicable(work, field) { + return !competitive() || field.id !== 32 || work.id >= 303; +} +function answer(work, id) { + if (scenario === "ready-draft" && work.id === 304) + return { + 2: "Готовые материалы решения.", + 10: "Презентация.pdf", + 11: "example.com/repository", + 12: null, + 30: 0, + 31: false, + 32: "Порядок проверки решения.", + }[id]; + const i = work.id - 301; + return { + 2: "Сервис помогает участникам найти материалы, экспертов и команду для решения задачи.", + 10: i < 3 ? "Презентация.pdf" : null, + 11: i < 3 ? "example.com/repository" : null, + 12: i === 1 ? "example.com/demo" : null, + 30: i === 0 ? 0 : i === 3 ? null : 120, + 31: i === 0 ? false : i === 3 ? null : true, + 32: i === 2 ? "Описаны шаги проверки и развития решения." : null, + }[id]; +} +const filled = (value) => + value !== null && + value !== undefined && + !(typeof value === "string" && !value.trim()); +function evaluation(work) { + if (!competitive()) return "not_applicable"; + if (!work.submitted) return "not_submitted"; + const own = effectiveAssignments().filter((row) => row.workId === work.id), + done = own.filter((row) => row.completed).length; + if (!distributed()) return done ? "evaluated" : "awaiting_evaluation"; + return done >= 2 + ? "evaluated" + : done + ? "partially_evaluated" + : "awaiting_evaluation"; +} +function status(work) { + if (managerAdmin.isArchived(work.id)) return ["Архив", "muted"]; + const state = evaluation(work); + return { + not_applicable: ["Сохранено", "muted"], + not_submitted: ["Не сдано", "muted"], + awaiting_evaluation: ["Ожидает оценки", "muted"], + partially_evaluated: ["Частично оценено", "warning"], + evaluated: ["Оценено", "good"], + }[state]; +} +function statusHtml(work) { + const [label, tone] = status(work); + return `${label}`; +} +function persist() { + history.replaceState( + null, + "", + `?screen=${screen}&format=${format}&state=${scenario}`, + ); +} +function updateControls() { + if (!["kch", "hack", "noncompetitive"].includes(format)) format = "kch"; + if (!["analytics", "solutions", "evaluation", "results"].includes(screen)) + screen = "analytics"; + if (!$("#scenario").querySelector(`option[value="${scenario}"]`)) + scenario = "ready"; + $("#program-format").value = format; + $("#prototype-screen").value = screen; + $("#scenario").value = scenario; + $("#program-name").textContent = { + kch: "Кейс-чемпионат PROCOLLAB", + hack: "Хакатон PROCOLLAB", + noncompetitive: "Практикум PROCOLLAB", + }[format]; + document + .querySelectorAll("[data-screen]") + .forEach((button) => + button.setAttribute( + "aria-current", + button.dataset.screen === screen || + (screen === "evaluation" && button.dataset.screen === "solutions") + ? "page" + : "false", + ), + ); +} +function showNotice(text) { + clearTimeout(noticeTimer); + $("#notice").textContent = text; + $("#notice").hidden = false; + noticeTimer = setTimeout(() => ($("#notice").hidden = true), 2400); +} +function exportMenu() { + return `
${icon("download")}
${[ + ["program", "Вся программа"], + ["solutions", "Все решения"], + ...(competitive() + ? [ + ["submitted", "Сданные решения"], + ["scores", "Оценки решений"], + ] + : []), + ] + .map( + ([kind, label]) => + ``, + ) + .join("")}
`; +} +function statePanel() { + if (scenario === "loading") + return `

Загружаем данные программы…

`; + if (scenario === "error") + return ``; + if (scenario === "no-access") + return `

Нет доступа к управлению программой

Этот раздел доступен организаторам программы.

`; + return null; +} +function metric(key, label, value, tip, ico) { + return `
${icon(ico)}

${key === "participants" ? `` : label}

${value === null ? "—" : value}
`; +} +function funnelLine(label, value, max) { + return `
${label}${value}
`; +} +function coverageRows() { + return fields() + .map((field) => { + const relevant = activeWorks().filter((work) => applicable(work, field)), + answered = relevant.filter((work) => + filled(answer(work, field.id)), + ).length; + return `${esc(field.label)}${field.required ? "Обязательное поле" : "Необязательное поле"}${relevant.length}${answered}${field.required ? relevant.length - answered : "—"}`; + }) + .join(""); +} +function analyticsView() { + const rows = activeWorks(), + count = rows.length, + submitted = rows.filter((row) => row.submitted).length, + done = rows.filter((row) => evaluation(row) === "evaluated").length, + total = effectiveAssignments().length, + completed = effectiveAssignments().filter((row) => row.completed).length; + return `
+

Аналитика

${exportMenu()}
+
${metric("participants", "Участники", managerAdmin.registrationCount(), "Уникальные зарегистрированные участники программы.", "person")}${metric("solutions", "Решения", count, "Заявки команд этой программы.", "folder")}${metric("submitted", "Сдано", competitive() ? submitted : null, competitive() ? "Решения с подтвержденной сдачей." : "В этой программе сдача не предусмотрена.", "analytics-submitted")}${metric("team", "Участников / решение", count ? Math.round((managerAdmin.registrationCount() / count) * 100) / 100 : 0, "Число зарегистрированных участников, деленное на число решений программы.", "analytics-team")}
+

Требует внимания

${count ? `` : '

Ничего не требует внимания

'}
+

Сроки программы

  • Регистрация
  • ${competitive() ? "
  • Прием решений
  • Оценивание
  • " : "
  • Завершение программы
  • "}
+

Воронка участников

${funnelLine("Регистрация", managerAdmin.registrationCount(), 12)}${funnelLine("Заходили на платформу", managerAdmin.registrationCount() - managerAdmin.withoutVisit(), 12)}${funnelLine("Вступили в команду", managerAdmin.teamCount(), 12)}

${competitive() ? "Воронка решений" : "Сохраненные решения"}

${funnelLine("Создано", count, 4)}${competitive() ? funnelLine("Сдано", submitted, 4) + funnelLine("Оценено", done, 4) : '

Участники сохраняют материалы без отдельной сдачи.

'}
+ ${competitive() ? `

Ход оценивания

${distributed() ? "Распределенное оценивание" : "Открытое оценивание"}

${distributed() ? '' : ""}
${distributed() && count ? `` : `

${count ? "Назначения в открытом режиме не применяются. Эксперты выбирают сданные решения программы." : "Назначений пока нет."}

`}
${distributed() ? '

Задержки экспертов

Задержек пока нет. Контроль незавершенных назначений включается за пять дней до окончания оценивания.

' : ""}
` : ""} +

Кейсы

КейсРешения${competitive() ? "Сдано" : ""}Участники
${[ + "Образование", + "Городская среда", + null, + ] + .map((name) => { + const group = rows.filter((row) => row.caseName === name); + return ``; + }) + .join( + "", + )}

Активность

За последние 30 дней

26 сентября25 октября

Регистрации${competitive() ? " и сдачи решений" : ""}

+

Заполнение полей решения

${competitive() ? "Для сданной работы учитывается форма на момент сдачи. Новое поле не становится пропуском в ранее сданном решении." : "Показано заполнение текущей формы. Ответы можно сохранять без отдельной сдачи."}

${coverageRows()}
ПолеПрименимоЕсть ответНет обязательного ответа
+

Регионы участников

${[ + ["Москва", 6], + ["Татарстан", 4], + ["Свердловская область", 2], + ] + .map(([name], index) => { + const value = managerAdmin.regionCount(index); + return `
${name}${value}
`; + }) + .join("")}
+
`; +} +function filterHtml(filter, { results = false } = {}) { + const filters = fields(); + return `
+
+ ${competitive() ? `` : ""} + ${ + results + ? "" + : `

«Неприменимо» означает, что поля не было в форме при сдаче. Ответы 0 и «Нет» считаются заполненными.

` + } +
`; +} +function filteredRows(filter, base = activeWorks()) { + return base.filter((work) => { + if ( + filter.search && + !workName(work).toLowerCase().includes(filter.search.toLowerCase()) + ) + return false; + if ( + competitive() && + filter.submission !== "all" && + (filter.submission === "submitted") !== work.submitted + ) + return false; + if (filter.fieldId && filter.answer !== "all") { + const field = fields().find((row) => row.id === Number(filter.fieldId)); + if (field) { + const applies = applicable(work, field), + has = applies && filled(answer(work, field.id)); + if (filter.answer === "not_applicable") return !applies; + if (!applies) return false; + if (filter.answer === "present") return has; + if (filter.answer === "missing") return !has; + } + } + return true; + }); +} +function workTable(rows, { result = false } = {}) { + return rows.length + ? `
${result ? "" : ""}${rows + .map((work) => { + const count = effectiveAssignments().filter( + (row) => row.workId === work.id && row.completed, + ).length; + return `${result ? `` : ""}`; + }) + .join("")}
${result ? "Место" : "Решение"}РешениеКейс${result ? "Балл" : "Капитан"}${result ? "Оценки" : "Состояние"}
${work.id === 302 ? "1" : "—"}${!result ? `${work.teamSize} ${work.teamSize === 1 ? "участник" : work.teamSize < 5 ? "участника" : "участников"}` : ""}${esc(work.caseName ?? "Не выбран")}${result ? (work.id === 302 ? "7,5" : work.id === 301 ? "0" : "—") : esc(work.captain)}${result ? `${count} из 2` : statusHtml(work)}
` + : `

${scenario === "empty" ? "Решений пока нет" : "Решения не найдены"}

${scenario === "empty" ? "Здесь появятся заявки команд этой программы." : "Измените запрос или сбросьте фильтры."}

${scenario === "empty" ? "" : ''}
`; +} +function solutionList() { + const filter = managerAdmin.listFilter(), + rows = filteredRows(filter, managerAdmin.listWorks()); + return `

Решения программы

В списке: ${managerAdmin.listWorks().length}${competitive() ? "" : ". Сохраненные материалы без отдельной сдачи."}

${managerAdmin.isArchiveScope() ? "" : exportMenu()}
${managerAdmin.listToolbar()}${managerAdmin.isArchiveScope() ? managerAdmin.archiveFilters(filter) : filterHtml(filter)}${managerAdmin.emptyArchive() ? '

Архив пока пуст

Здесь появятся решения, снятые с участия или перемещенные в архив организатором.

' : workTable(rows)}
`; +} +function resultsView() { + if (!competitive()) + return `

Сдача и конкурсное оценивание не предусмотрены

Сохраненные материалы участников доступны в разделе «Решения».

`; + const filter = filterState.results, + rows = filteredRows( + filter, + activeWorks().filter((work) => work.submitted), + ); + return `

Рейтинг решений

${exportMenu()}

Место появляется после необходимого числа завершенных оценок. Фильтры не меняют места в общем рейтинге.

${filterHtml(filter, { results: true })}${workTable(rows, { result: true })}
`; +} +function evaluationView() { + if (!competitive()) return resultsView(); + return `

Оценивание решений

${distributed() ? "Распределенное оценивание" : "Открытое оценивание"}

${distributed() ? `` : ""}

${distributed() ? "На решение необходимо 2 завершенные оценки. Сохраненные оценки и соответствующие назначения защищены." : "Эксперты выбирают сданные решения программы. Персональные назначения не требуются."}

${people.map((person, i) => ``).join("")}
ЭкспертНазначеноВыполнено
${person.join(" ")}${distributed() ? effectiveAssignments().filter((row) => row.expert === i).length : "—"}${distributed() ? effectiveAssignments().filter((row) => row.expert === i && row.completed).length : "—"}
`; +} +function renderPage() { + updateControls(); + const state = statePanel(); + $("#manager-main").innerHTML = + state || + (screen === "analytics" + ? analyticsView() + : screen === "solutions" + ? solutionList() + : screen === "results" + ? resultsView() + : evaluationView()); + persist(); +} +function currentFilter() { + return ( + modal?.filters ?? + (screen === "results" ? filterState.results : managerAdmin.listFilter()) + ); +} +function openModal(next) { + lastTrigger = document.activeElement; + modal = next; + renderModal(); + dialog.showModal(); + $("#dialog-title").focus(); +} +function openInner(next) { + next.back = modal; + next.backFocus = document.activeElement?.dataset.focus; + modal = next; + renderModal(); + $("#dialog-title").focus(); +} +function closeModal() { + if (modal?.busy) return; + dialog.close(); + modal = null; + if (lastTrigger?.isConnected) lastTrigger.focus(); + else document.querySelector('[data-screen][aria-current="page"]')?.focus(); +} +function modalHeader(title, subtitle = "") { + return `${modal?.back ? '' : ""}`; +} +function renderModal() { + if (!modal) return; + const adminView = managerAdmin.renderModal(); + if (adminView) { + dialog.innerHTML = + modalHeader(adminView.title, adminView.subtitle) + + ``; + if (modal.busy) + dialog + .querySelectorAll(".close-button,.modal-back") + .forEach((button) => (button.disabled = true)); + return; + } + let title = "", + subtitle = "", + body = ""; + if (["case", "waiting", "list"].includes(modal.view)) { + title = + modal.view === "case" + ? modal.caseName + ? "Кейс: " + modal.caseName + : "Без выбранного кейса" + : modal.view === "waiting" + ? "Решения ожидают оценивания" + : "Решения программы"; + const base = activeWorks().filter((work) => + modal.view === "case" + ? work.caseName === modal.caseName + : modal.view === "waiting" + ? ["awaiting_evaluation", "partially_evaluated"].includes( + evaluation(work), + ) + : true, + ), + rows = filteredRows(modal.filters, base); + body = `
Решения${base.length}Участники${base.reduce((n, w) => n + w.teamSize, 0)}${competitive() ? `Сдано${base.filter((w) => w.submitted).length}` : ""}
${filterHtml(modal.filters)}${workTable(rows)}

${rows.length ? `1–${rows.length} из ${rows.length}` : "0 из 0"}

`; + } else if (["assignments", "work-scores"].includes(modal.view)) { + title = + modal.view === "work-scores" ? "Оценки решения" : "Назначения экспертов"; + let rows = effectiveAssignments().filter((row) => + modal.workId + ? row.workId === modal.workId + : modal.expert !== undefined + ? row.expert === modal.expert + : true, + ); + if (modal.scope === "completed") rows = rows.filter((row) => row.completed); + if (modal.scope === "pending") rows = rows.filter((row) => !row.completed); + subtitle = modal.workId + ? workName(works.find((row) => row.id === modal.workId)) + : ""; + body = `${rows.map((row) => ``).join("")}
РешениеЭкспертСтатус
${esc(workName(works.find((work) => work.id === row.workId)))}${people[row.expert].join(" ")}${row.completed ? "Выполнено" : "Ожидает оценки"}
${rows.length ? "" : '

Назначения не найдены

При выбранном статусе назначений нет.

'}`; + } else if (modal.view === "scores") { + const row = + assignments.find((item) => item.id === modal.id) ?? + managerAdmin.savedRating(modal.id); + title = "Оценка решения"; + subtitle = + workName(works.find((work) => work.id === row.workId)) + + " / " + + people[row.expert].join(" "); + body = `

${row.completed ? "Эксперт заполнил все критерии." : "Оценка еще не сохранена."}

Качество решения, от 0 до 10

${row.completed ? row.value : "—"}

Работоспособность подтверждена

${row.completed ? (row.value === 0 ? "Нет" : "Да") : "—"}

Комментарий эксперта

${row.completed ? (row.value === 0 ? "Прототип требует доработки перед демонстрацией." : "Решение соответствует задаче.") : "Комментарий не добавлен."}

`; + } else if (modal.view === "allocation") { + title = "Предпросмотр распределения"; + body = `

Будут распределены сданные решения. Назначения с сохраненными оценками останутся на месте.

Сданных решений
3
Назначений с оценками, сохраняются
3
Новых назначений
${assignments.some((row) => row.workId === 303) ? 0 : 2}

Необходимо 2 завершенные оценки на решение.

`; + } else if (modal.view === "export") { + title = "Выгрузить данные"; + body = `

${esc(modal.label ?? "Будут выгружены найденные решения с учетом поиска и фильтров.")}

Данные будут доступны в файле Excel.

`; + } else if (modal.view === "formula") { + title = "Как считается балл"; + body = + '

Итоговый балл использует завершенные оценки экспертов и действующие критерии программы.

Незавершенная оценка не участвует в среднем. Нулевой балл и ответ «Нет» сохраняются. Место появляется после необходимого числа оценок; фильтры не пересчитывают общий рейтинг.

'; + } else if (modal.view === "reminder") { + title = "Отправить напоминание"; + body = `

Получателей: ${modal.back?.kind === "without-visit" ? managerAdmin.withoutVisit() : managerAdmin.registrationCount() - managerAdmin.teamCount()}

${modal.back?.kind === "without-team" ? "Вы участвуете в программе PROCOLLAB, но пока не состоите в команде. Создайте решение или присоединитесь к команде." : "Вы зарегистрированы в программе PROCOLLAB. Зайдите на платформу, чтобы продолжить участие."}

`; + } + dialog.innerHTML = + modalHeader(title, subtitle) + ``; +} +function refreshCurrent(focusName) { + if (modal) renderModal(); + else renderPage(); + if (focusName) + (modal ? dialog : $("#manager-main")) + .querySelector(`[name="${focusName}"]`) + ?.focus(); +} +document.addEventListener("click", (event) => { + const target = event.target.closest("button,[data-action]"); + if (!target) return; + if (target.dataset.preservedSection) { + showNotice( + `Раздел «${target.textContent.trim()}» сохраняет текущий вид и логику. В макете показан контур заявок.`, + ); + return; + } + if (target.dataset.screen) { + if (dialog.open) closeModal(); + screen = target.dataset.screen; + renderPage(); + return; + } + const action = target.dataset.action; + if (!action) return; + if (managerAdmin.handleClick(target)) return; + if (action === "close") { + closeModal(); + return; + } + if (action === "back") { + const previous = modal.back, + focus = modal.backFocus; + if (previous) { + modal = previous; + renderModal(); + if (focus) dialog.querySelector(`[data-focus="${focus}"]`)?.focus(); + else $("#dialog-title").focus(); + } else closeModal(); + return; + } + if (action === "retry") { + $("#scenario").value = scenario = "ready"; + renderPage(); + return; + } + if (action === "solutions") { + screen = "solutions"; + if (dialog.open) closeModal(); + renderPage(); + return; + } + if (action === "case") { + openModal({ + view: "case", + caseName: + target.dataset.case === "without-case" ? null : target.dataset.case, + filters: newFilter(), + }); + return; + } + if (action === "waiting") { + openModal({ view: "waiting", filters: newFilter() }); + return; + } + if (action === "people") { + openModal({ view: "people", kind: target.dataset.kind }); + return; + } + if (action === "solution") { + const next = { + view: "solution", + id: Number(target.dataset.id), + tab: "solution", + }; + modal ? openInner(next) : openModal(next); + return; + } + if (action === "solution-tab") { + modal.tab = target.dataset.tab; + renderModal(); + dialog.querySelector(`[data-tab="${modal.tab}"]`)?.focus(); + return; + } + if (action === "assignments") { + openModal({ + view: "assignments", + scope: target.dataset.scope ?? "all", + ...(target.dataset.expert !== undefined + ? { expert: Number(target.dataset.expert) } + : {}), + }); + return; + } + if (action === "work-scores") { + const next = { + view: "work-scores", + workId: Number(target.dataset.id), + scope: "all", + }; + modal ? openInner(next) : openModal(next); + return; + } + if (action === "assignment-scope") { + modal.scope = target.dataset.scope; + renderModal(); + dialog.querySelector(`[data-scope="${modal.scope}"]`)?.focus(); + return; + } + if (action === "score-detail") { + openInner({ view: "scores", id: Number(target.dataset.id) }); + return; + } + if (action === "allocation") { + openModal({ view: "allocation" }); + return; + } + if (action === "apply-allocation") { + if (!assignments.some((row) => row.workId === 303)) + assignments.push( + { id: 805, workId: 303, expert: 1, completed: false, value: null }, + { id: 806, workId: 303, expert: 2, completed: false, value: null }, + ); + closeModal(); + renderPage(); + showNotice("Распределение применено в макете. Добавлено 2 назначения."); + return; + } + if (action === "formula") { + openModal({ view: "formula" }); + return; + } + if (action === "material") { + showNotice( + "Пример материала в макете. В рабочем интерфейсе откроется файл или ссылка.", + ); + return; + } + if (action === "filters") { + const filter = currentFilter(); + filter.expanded = !filter.expanded; + refreshCurrent(); + (modal ? dialog : $("#manager-main")) + .querySelector('[data-action="filters"]') + ?.focus(); + return; + } + if (action === "reset-filters") { + const filter = currentFilter(); + Object.assign(filter, newFilter()); + refreshCurrent("search"); + return; + } + if (action === "export" || action === "export-found") { + const next = { + view: "export", + label: + action === "export-found" + ? "Будут выгружены все найденные решения с учетом поиска и фильтров." + : { + program: + "Вся программа: решения, команды, поля, ответы, назначения и оценки.", + solutions: + "Все решения программы с материалами и принятым составом.", + submitted: "Сданные решения с зафиксированными материалами.", + scores: "Сохраненные оценки решений.", + }[target.dataset.kind], + }; + modal ? openInner(next) : openModal(next); + return; + } + if (action === "prepare-export") { + closeModal(); + showNotice( + "Пример состояния: файл Excel подготовлен. Макет не выгружает реальные данные.", + ); + return; + } + if (action === "reminder-preview") { + openInner({ view: "reminder" }); + return; + } + if (action === "send-reminder") { + closeModal(); + showNotice( + "Пример состояния: напоминание отправлено. Реальные письма не отправляются.", + ); + return; + } +}); +document.addEventListener("input", (event) => { + if (event.target.name === "search") + currentFilter().draft = event.target.value; +}); +document.addEventListener("submit", (event) => { + if (event.target.dataset.form === "search") { + event.preventDefault(); + currentFilter().search = currentFilter().draft.trim(); + refreshCurrent("search"); + } +}); +document.addEventListener("change", (event) => { + const name = event.target.name; + if (["submission", "fieldId", "answer"].includes(name)) { + const filter = currentFilter(); + filter[name] = event.target.value; + if (name === "fieldId" && !filter.fieldId) filter.answer = "all"; + refreshCurrent(name); + } +}); +$("#program-format").addEventListener("change", (event) => { + format = event.target.value; + assignments = originalAssignments.map((row) => ({ ...row })); + managerAdmin.reset(); + filterState.solutions = newFilter(); + filterState.results = newFilter(); + if (dialog.open) closeModal(); + renderPage(); +}); +$("#scenario").addEventListener("change", (event) => { + scenario = event.target.value; + assignments = originalAssignments.map((row) => ({ ...row })); + managerAdmin.reset(); + if (dialog.open) closeModal(); + if (scenario === "no-results") { + screen = "solutions"; + filterState.solutions = { + ...newFilter(), + draft: "Несуществующее решение", + search: "Несуществующее решение", + }; + } else { + filterState.solutions = newFilter(); + filterState.results = newFilter(); + } + renderPage(); +}); +dialog.addEventListener("cancel", (event) => { + event.preventDefault(); + closeModal(); +}); +dialog.addEventListener("keydown", (event) => { + if (event.key !== "Tab") return; + const focusable = [ + ...dialog.querySelectorAll( + "button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]", + ), + ].filter((el) => el.tabIndex >= 0 && el.getClientRects().length); + if (!focusable.length) { + event.preventDefault(); + $("#dialog-title").focus(); + return; + } + const first = focusable[0], + last = focusable[focusable.length - 1], + active = document.activeElement; + if (event.shiftKey && (active === first || !focusable.includes(active))) { + event.preventDefault(); + last.focus(); + } else if ( + !event.shiftKey && + (active === last || !focusable.includes(active)) + ) { + event.preventDefault(); + first.focus(); + } +}); +dialog.addEventListener("click", (event) => { + if (event.target === dialog) { + const box = dialog.getBoundingClientRect(); + if ( + event.clientX < box.left || + event.clientX > box.right || + event.clientY < box.top || + event.clientY > box.bottom + ) + closeModal(); + } +}); +$("#prototype-screen").addEventListener("change", (event) => { + if (dialog.open) closeModal(); + screen = event.target.value; + renderPage(); +}); +renderPage(); + +document.addEventListener("keydown", (event) => { + if (!event.target.matches('[role="tab"]') || !dialog.open) return; + const tabs = [...dialog.querySelectorAll('[role="tab"]')], + index = tabs.indexOf(event.target); + let next = index; + if (event.key === "ArrowRight") next = (index + 1) % tabs.length; + else if (event.key === "ArrowLeft") + next = (index + tabs.length - 1) % tabs.length; + else if (event.key === "Home") next = 0; + else if (event.key === "End") next = tabs.length - 1; + else return; + event.preventDefault(); + tabs[next].click(); +}); diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/drilldown-reference.css b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/drilldown-reference.css new file mode 100644 index 000000000..9d8cb0f0b --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/drilldown-reference.css @@ -0,0 +1,968 @@ +/** @format */ +/** @format */ +/** @format */ +.prototype-drilldown { + /** @format */ +} +.prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--case { + box-sizing: border-box; + width: min(880px, 100% - 24px); + padding: 20px; + overflow-y: auto; +} +@media screen and (max-width: 999px) { + .prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--case { + max-height: calc(var(--visual-viewport-height, 100dvh) - 32px); + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--case { + max-height: calc(100dvh - 32px); + } +} +.prototype-drilldown .analytics-case__metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + padding: 10px 12px; + margin: 0 0 16px; + background: var(--light-gray); + border-radius: 8px; +} +.prototype-drilldown .analytics-case__metrics div { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: baseline; +} +.prototype-drilldown .analytics-case__metrics dt { + font-size: var(--ui-font-small); + color: var(--dark-grey); +} +.prototype-drilldown .analytics-case__metrics dd { + margin: 0; + font-weight: 600; +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-case__metrics { + display: flex; + } + .prototype-drilldown .analytics-case__metrics div + div { + padding-left: 12px; + border-left: 1px solid var(--medium-grey-for-outline); + } +} +.prototype-drilldown .analytics-case__export { + display: grid; + gap: 6px; + margin-bottom: 16px; + font-size: var(--ui-font-small); +} +.prototype-drilldown .analytics-case__export app-button { + width: 100%; +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-case__export { + justify-items: start; + } + .prototype-drilldown .analytics-case__export app-button { + width: 220px; + } +} +.prototype-drilldown .analytics-case__filters { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 12px; + margin: 12px 0 20px; +} +.prototype-drilldown .analytics-case__filters label { + display: grid; + gap: 6px; + font-size: var(--ui-font-small); + color: var(--dark-grey); +} +.prototype-drilldown .analytics-case__filters select { + box-sizing: border-box; + width: 100%; + min-height: 44px; + padding: 10px 12px; + font: inherit; + font-size: 14px; + color: var(--black); + background: var(--white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; +} +.prototype-drilldown .analytics-case__filters select:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +@media screen and (min-width: 600px) { + .prototype-drilldown .analytics-case__filters { + grid-template-columns: repeat(2, minmax(0, 220px)); + } +} +.prototype-drilldown .analytics-case__loading { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + padding: 48px 12px; + color: var(--dark-grey); +} +.prototype-drilldown .analytics-case .analytics-drilldown__search-controls { + max-width: 100%; +} +.prototype-drilldown .analytics-case .analytics-drilldown__search-controls input { + flex-basis: 100%; + border-radius: var(--rounded-xxl); +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-case .analytics-drilldown__search-controls input { + flex-basis: 220px; + } +} +.prototype-drilldown .analytics-case .analytics-case__table small { + display: block; + margin-top: 4px; + font-size: var(--ui-font-small); +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-case .analytics-case__table th, + .prototype-drilldown .analytics-case .analytics-case__table td { + padding: 10px 8px; + } + .prototype-drilldown .analytics-case .analytics-case__table th { + font-size: 13px; + } + .prototype-drilldown .analytics-case .analytics-case__table th:first-child, + .prototype-drilldown .analytics-case .analytics-case__table td:first-child { + width: 32%; + } + .prototype-drilldown .analytics-case .analytics-case__table th:nth-child(2), + .prototype-drilldown .analytics-case .analytics-case__table td:nth-child(2) { + width: 28%; + } + .prototype-drilldown .analytics-case .analytics-case__table th:nth-child(3), + .prototype-drilldown .analytics-case .analytics-case__table td:nth-child(3) { + width: 20%; + } + .prototype-drilldown .analytics-case .analytics-case__table th:last-child, + .prototype-drilldown .analytics-case .analytics-case__table td:last-child { + width: 20%; + } +} +.prototype-drilldown .modal__body.analytics-drilldown-body { + box-sizing: border-box; + width: min(880px, 100vw - 24px); + padding: 20px; + overflow-y: auto; +} +@media screen and (max-width: 999px) { + .prototype-drilldown .modal__body.analytics-drilldown-body { + max-height: calc(var(--visual-viewport-height, 100dvh) - 32px); + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .modal__body.analytics-drilldown-body { + max-height: calc(100dvh - 32px); + } +} +.prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments { + width: min(980px, 100vw - 32px); + padding: 16px; +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments { + padding: 28px; + } +} +.prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__header { + align-items: center; + margin-bottom: 24px; +} +.prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__header + h2 { + line-height: 30px; +} +@media screen and (max-width: 999px) { + .prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__header + h2 { + font-size: var(--ui-font-h1); + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__header + h2 { + font-size: 24px; + } +} +.prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__close { + display: grid; + place-items: center; + padding: 0; + line-height: 1; +} +.prototype-drilldown + .modal__body.analytics-drilldown-body.analytics-drilldown-body--assignments + .analytics-drilldown__close:hover { + background: var(--light-gray); +} +.prototype-drilldown .analytics-drilldown { + width: 100%; + min-width: 0; + font-size: var(--ui-font-body); + line-height: 1.5; + color: var(--black); + overflow-wrap: anywhere; +} +.prototype-drilldown .analytics-drilldown h2 { + font-size: 20px; + font-weight: 600; +} +.prototype-drilldown .analytics-drilldown h3 { + font-size: var(--ui-font-h3); + font-weight: 600; +} +.prototype-drilldown .analytics-drilldown ul { + padding: 0; + list-style: none; +} +.prototype-drilldown .analytics-drilldown button { + font: inherit; + cursor: pointer; +} +.prototype-drilldown .analytics-drilldown button:focus-visible, +.prototype-drilldown .analytics-drilldown a:focus-visible, +.prototype-drilldown .analytics-drilldown input:focus-visible, +.prototype-drilldown .analytics-drilldown h2:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} +.prototype-drilldown .analytics-drilldown__header { + display: flex; + gap: 12px; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 20px; +} +.prototype-drilldown .analytics-drilldown__close { + flex: 0 0 32px; + width: 32px; + height: 32px; + font-size: 26px !important; + color: var(--dark-grey); + background: transparent; + border: 0; + border-radius: 8px; +} +.prototype-drilldown .analytics-drilldown__action { + padding: 6px 0; + color: var(--accent-dark); + text-align: left; + background: transparent; + border: 0; +} +.prototype-drilldown .analytics-drilldown__action:hover { + color: var(--accent-dark); +} +.prototype-drilldown .analytics-drilldown__action:disabled { + color: var(--dark-grey); + text-decoration: none; + cursor: default; + opacity: 0.6; +} +.prototype-drilldown .analytics-drilldown__search { + display: grid; + gap: 6px; + margin-bottom: 16px; +} +.prototype-drilldown .analytics-drilldown__search-controls { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} +.prototype-drilldown .analytics-drilldown__search-controls input { + box-sizing: border-box; + min-width: 0; + max-width: 100%; + min-height: var(--ui-control-height); + padding: var(--ui-control-padding); + font: inherit; + font-size: var(--ui-font-body); + line-height: 1.4; + color: var(--ui-text); + background: var(--ui-surface-subtle); + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-control); + outline: none; + transition: + border-color 0.2s, + box-shadow 0.2s; + flex: 1 1 220px; + width: 100%; + font-size: 12px; +} +.prototype-drilldown .analytics-drilldown__search-controls input:is(select) { + padding-right: 44px; + appearance: none; + background-image: url("/assets/icons/svg/chevron-down.svg"); + background-repeat: no-repeat; + background-position: right 16px center; + background-size: 14px; +} +.prototype-drilldown .analytics-drilldown__search-controls input::placeholder { + color: var(--dark-grey); +} +.prototype-drilldown .analytics-drilldown__search-controls input:hover:not(:disabled) { + border-color: var(--dark-grey); +} +.prototype-drilldown .analytics-drilldown__search-controls input:focus-visible, +.prototype-drilldown .analytics-drilldown__search-controls input:focus-within, +.prototype-drilldown .analytics-drilldown__search-controls input--open { + border-color: var(--ui-accent); + box-shadow: var(--ui-focus-ring); +} +.prototype-drilldown .analytics-drilldown__search-controls input--error, +.prototype-drilldown .analytics-drilldown__search-controls input--overflow, +.prototype-drilldown .analytics-drilldown__search-controls input[aria-invalid="true"] { + border: 1px solid var(--ui-danger); +} +.prototype-drilldown .analytics-drilldown__search-controls input--error:focus, +.prototype-drilldown .analytics-drilldown__search-controls input--error:focus-within, +.prototype-drilldown .analytics-drilldown__search-controls input--overflow:focus, +.prototype-drilldown .analytics-drilldown__search-controls input--overflow:focus-within, +.prototype-drilldown .analytics-drilldown__search-controls input[aria-invalid="true"]:focus, +.prototype-drilldown .analytics-drilldown__search-controls input[aria-invalid="true"]:focus-within { + box-shadow: var(--ui-error-ring); +} +.prototype-drilldown .analytics-drilldown__search-controls input:disabled, +.prototype-drilldown .analytics-drilldown__search-controls input--disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); +} +@media (max-width: 749px) { + .prototype-drilldown .analytics-drilldown__search-controls input { + font-size: 16px; + } +} +.prototype-drilldown .analytics-drilldown__count { + margin-bottom: 12px; +} +.prototype-drilldown .analytics-drilldown__pagination { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + margin-top: 16px; +} +.prototype-drilldown .analytics-drilldown__identity { + display: flex; + gap: 8px; + align-items: center; + min-width: 0; +} +.prototype-drilldown .analytics-drilldown app-avatar { + flex-shrink: 0; +} +.prototype-drilldown .analytics-drilldown__secondary { + color: var(--dark-grey); +} +.prototype-drilldown .analytics-drilldown__metadata { + margin-top: 8px; + font-weight: 600; +} +.prototype-drilldown .analytics-drilldown__empty { + padding: 32px 12px; + text-align: center; +} +.prototype-drilldown .analytics-drilldown__error { + padding: 16px; + margin-block: 12px; + border: 1px solid var(--red); + border-radius: 12px; +} +.prototype-drilldown .analytics-drilldown__section { + margin-block: 20px 12px; +} +.prototype-drilldown .analytics-drilldown__criteria, +.prototype-drilldown .analytics-drilldown__experts { + display: grid; + gap: 12px; +} +.prototype-drilldown .analytics-drilldown__criteria li { + display: grid; + gap: 6px; + padding: 14px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 12px; +} +.prototype-drilldown .analytics-drilldown__criteria strong { + white-space: pre-wrap; +} +.prototype-drilldown .analytics-drilldown__expert { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + width: 100%; + padding: 14px; + color: var(--black); + text-align: left; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 12px; +} +.prototype-drilldown .analytics-drilldown__expert:hover { + border-color: var(--accent); +} +.prototype-drilldown .analytics-drilldown__expert > .analytics-drilldown__identity { + flex-basis: 100%; +} +.prototype-drilldown .analytics-drilldown__badge { + display: inline-block; + padding: 3px 7px; + font-size: var(--ui-font-small); + font-weight: 600; + line-height: 18px; + color: var(--dark-grey); + background: var(--light-gray); + border-radius: 6px; +} +.prototype-drilldown .analytics-drilldown__badge--pending { + color: var(--gold-dark); + background: var(--gold-light); +} +.prototype-drilldown .analytics-drilldown__badge--in_progress { + color: var(--blue-dark); + background: var(--accent-light); +} +.prototype-drilldown .analytics-drilldown__badge--completed { + color: var(--green-dark); + background: var(--green-light); +} +.prototype-drilldown .analytics-drilldown__badge--critical { + color: var(--red-dark); + background: var(--red-light); +} +.prototype-drilldown .analytics-drilldown__table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; +} +.prototype-drilldown .analytics-drilldown__table thead { + display: none; +} +.prototype-drilldown .analytics-drilldown__table tbody { + display: grid; + gap: 12px; +} +.prototype-drilldown .analytics-drilldown__table tr { + display: block; + padding: 12px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 12px; +} +.prototype-drilldown .analytics-drilldown__table td { + display: block; + padding-block: 4px; +} +.prototype-drilldown .analytics-drilldown__table td::before { + display: block; + font-size: var(--ui-font-small); + font-weight: 600; + color: var(--dark-grey); + content: attr(data-label); +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table thead { + display: table-header-group; + } + .prototype-drilldown .analytics-drilldown__table tbody { + display: table-row-group; + } + .prototype-drilldown .analytics-drilldown__table tr { + display: table-row; + padding: 0; + border: 0; + } + .prototype-drilldown .analytics-drilldown__table th, + .prototype-drilldown .analytics-drilldown__table td { + display: table-cell; + padding: 12px 8px; + text-align: left; + vertical-align: top; + border-bottom: 1px solid var(--medium-grey-for-outline); + } + .prototype-drilldown .analytics-drilldown__table th { + font-size: 13px; + font-weight: 600; + } + .prototype-drilldown .analytics-drilldown__table td::before { + display: none; + } + .prototype-drilldown .analytics-drilldown__table th:first-child { + width: 25%; + } + .prototype-drilldown .analytics-drilldown__table th:last-child { + width: 15%; + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table--projects th:first-child { + width: 18%; + } +} +.prototype-drilldown .analytics-drilldown__table--assignments tr { + display: grid; + gap: 12px; + padding: 14px; +} +.prototype-drilldown .analytics-drilldown__table--assignments td { + padding: 0; + line-height: 22px; +} +.prototype-drilldown .analytics-drilldown__table--assignments td::before { + margin-bottom: 4px; +} +.prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__identity { + gap: 12px; + font-weight: 500; +} +.prototype-drilldown + .analytics-drilldown__table--assignments + .analytics-drilldown__assignment-status { + display: flex; + flex-flow: column wrap; + gap: 12px 14px; + align-items: flex-start; +} +.prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__badge { + display: inline-flex; + gap: 6px; + align-items: center; + min-height: 28px; + padding: 4px 9px; + border-radius: 8px; +} +.prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__badge::before { + flex: 0 0 6px; + width: 6px; + height: 6px; + content: ""; + background: currentcolor; + border-radius: 50%; +} +.prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__action { + padding: 4px 0; + line-height: 20px; +} +@media screen and (max-width: 999px) { + .prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__action { + font-size: var(--ui-font-body); + font-weight: 500; + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table--assignments .analytics-drilldown__action { + font-size: 13px; + font-weight: 500; + } +} +@media screen and (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table--assignments { + border-spacing: 0; + border-collapse: separate; + } + .prototype-drilldown + .analytics-drilldown__table--assignments + .analytics-drilldown__assignment-status { + flex-direction: row; + align-items: center; + } + .prototype-drilldown .analytics-drilldown__table--assignments tr { + display: table-row; + height: 72px; + padding: 0; + } + .prototype-drilldown .analytics-drilldown__table--assignments th, + .prototype-drilldown .analytics-drilldown__table--assignments td { + padding: 16px 12px; + vertical-align: middle; + } + .prototype-drilldown .analytics-drilldown__table--assignments thead tr { + height: auto; + } + .prototype-drilldown .analytics-drilldown__table--assignments th { + padding-block: 13px; + color: var(--dark-grey); + background: var(--light-gray); + border-bottom: 0; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:first-child { + width: 27%; + border-radius: 8px 0 0 8px; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(2) { + width: 31%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(3) { + width: 29%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:last-child { + width: 13%; + border-radius: 0 8px 8px 0; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(3), + .prototype-drilldown .analytics-drilldown__table--assignments td:nth-child(3) { + padding-inline: 8px; + } +} +.prototype-drilldown .analytics-drilldown .ui-button.ui-button { + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; +} +.prototype-drilldown .analytics-drilldown .ui-button.ui-button.ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-drilldown .analytics-drilldown .ui-button.ui-button { + padding-inline: 16px; + } + .prototype-drilldown .analytics-drilldown .ui-button.ui-button::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-drilldown .analytics-drilldown .ui-button.ui-button.ui-button--icon::after { + inset: -6px; + } +} +.prototype-drilldown .analytics-drilldown app-button > button.button.button { + position: relative; + box-sizing: border-box; + min-height: 32px; + padding: 5px 20px; + font-size: 12px; + font-weight: 400; + line-height: 20px; + min-width: 0; + max-height: none; +} +.prototype-drilldown .analytics-drilldown app-button > button.button.button.ui-button--icon { + width: 32px; + min-width: 32px; + padding: 5px; +} +@media (max-width: 999px) { + .prototype-drilldown .analytics-drilldown app-button > button.button.button { + padding-inline: 16px; + } + .prototype-drilldown .analytics-drilldown app-button > button.button.button::after { + position: absolute; + inset: -6px 0; + content: ""; + } + .prototype-drilldown + .analytics-drilldown + app-button + > button.button.button.ui-button--icon::after { + inset: -6px; + } +} +.prototype-drilldown .modal__body.analytics-drilldown-body.analytics-drilldown-body--attention { + box-sizing: border-box; + width: min(1100px, 100vw - 24px); + max-height: calc(100dvh - 32px); + padding: 20px; + overflow-y: auto; +} +.prototype-drilldown .attention-selection { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + align-items: center; + margin: 16px 0; +} +.prototype-drilldown .attention-sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} +.prototype-drilldown .analytics-drilldown__table th.attention-check-cell, +.prototype-drilldown .analytics-drilldown__table td.attention-check-cell { + width: 44px; + padding: 0; +} +.prototype-drilldown .analytics-drilldown__table th.attention-check-cell::before, +.prototype-drilldown .analytics-drilldown__table td.attention-check-cell::before { + display: none; +} +.prototype-drilldown .attention-check { + display: grid; + place-items: center; + width: 44px; + height: 44px; + cursor: pointer; +} +.prototype-drilldown .attention-check input { + width: 18px; + height: 18px; + margin: 0; + cursor: inherit; + accent-color: var(--accent); +} +.prototype-drilldown .attention-check input:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} +.prototype-drilldown .attention-check__label { + display: none; +} +@media (max-width: 999px) { + .prototype-drilldown .analytics-drilldown__table:has(th.attention-check-cell) thead { + display: block; + } + .prototype-drilldown .analytics-drilldown__table:has(th.attention-check-cell) thead tr { + padding: 0; + border: 0; + } + .prototype-drilldown + .analytics-drilldown__table:has(th.attention-check-cell) + thead + th:not(.attention-check-cell) { + display: none; + } + .prototype-drilldown + .analytics-drilldown__table:has(th.attention-check-cell) + th.attention-check-cell { + width: auto; + } + .prototype-drilldown + .analytics-drilldown__table:has(th.attention-check-cell) + thead + .attention-check { + display: flex; + gap: 12px; + width: auto; + font-size: 12px; + } + .prototype-drilldown + .analytics-drilldown__table:has(th.attention-check-cell) + .attention-check__label { + display: inline; + } +} +.prototype-drilldown .analytics-drilldown__table tr.attention-selected-row { + background: color-mix(in srgb, var(--accent-light) 35%, var(--light-white)); +} +.prototype-drilldown .attention-reminder { + margin: 20px 0; +} +.prototype-drilldown .attention-reminder h3 { + margin-bottom: 16px; + font-size: 18px; + font-weight: 600; +} +.prototype-drilldown .attention-reminder h4 { + margin: 24px 0 8px; + font-size: 13px; + font-weight: 600; +} +.prototype-drilldown .attention-reminder__summary { + display: flex; + gap: 24px; + margin: 0 0 12px; +} +.prototype-drilldown .attention-reminder__summary dt { + font-size: 12px; + color: var(--grey-for-text); +} +.prototype-drilldown .attention-reminder__summary dd { + margin: 4px 0 0; + font-size: 24px; + font-weight: 600; +} +.prototype-drilldown .attention-reminder__channels { + color: var(--green-dark); +} +.prototype-drilldown .attention-reminder__recipients { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 0; + margin: 16px 0; + list-style: none; +} +.prototype-drilldown .attention-reminder__recipients li { + padding: 5px 8px; + font-size: 12px; + background: var(--accent-light); + border-radius: 6px; +} +.prototype-drilldown .attention-reminder__recipient-details { + margin: 16px 0; +} +.prototype-drilldown .attention-reminder__recipient-details summary { + min-height: 32px; + padding-block: 6px; + font-size: 12px; + color: var(--accent); + cursor: pointer; +} +.prototype-drilldown .attention-reminder__message { + padding: 16px; + margin: 0 0 16px; + font-size: 14px; + line-height: 1.6; + white-space: pre-line; + background: var(--light-gray); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 10px; +} +.prototype-drilldown .attention-reminder__recipient-count { + margin: 0 0 20px; + font-size: 14px; +} +.prototype-drilldown .attention-reminder__email { + display: grid; + gap: 16px; + padding: 20px; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 10px; +} +.prototype-drilldown .attention-reminder__email p { + margin: 0; + font-size: 14px; + line-height: 1.6; + white-space: pre-line; +} +.prototype-drilldown .attention-reminder__email small { + color: var(--grey-for-text); +} +.prototype-drilldown .attention-reminder__email-button { + justify-self: start; + padding: 12px 18px; + color: var(--light-white); + background: var(--accent); + border-radius: 8px; +} +.prototype-drilldown .attention-list-tools { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} +.prototype-drilldown .attention-list-tools .analytics-drilldown__count { + margin: 0; +} +.prototype-drilldown .analytics-drilldown .ui-button.ui-button.attention-list-tools__export { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + min-height: 32px; + padding: 0; + color: var(--accent); + background: transparent; + border: 0; +} +@media (min-width: 1000px) { + .prototype-drilldown .analytics-drilldown__table--assignments th:first-child { + width: 24%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(2) { + width: 27%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(3) { + width: 18%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:nth-child(4) { + width: 19%; + } + .prototype-drilldown .analytics-drilldown__table--assignments th:last-child { + width: 12%; + } +} +.prototype-drilldown .attention-actions { + position: sticky; + bottom: -20px; + z-index: 2; + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + padding: 16px 0 20px; + margin-top: 16px; + background: var(--light-white); + border-top: 1px solid var(--medium-grey-for-outline); +} +.prototype-drilldown .attention-actions__buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} +.prototype-drilldown .attention-actions__count { + font-size: 12px; + font-weight: 600; +} +.prototype-drilldown .attention-actions__error, +.prototype-drilldown .attention-actions__success { + flex-basis: 100%; + margin: 0; + font-size: 12px; +} +.prototype-drilldown .attention-actions__error { + color: var(--red-dark); +} +.prototype-drilldown .attention-actions__success { + color: var(--green-dark); +} +.prototype-drilldown .attention-actions .ui-button.ui-button.attention-actions__send { + min-height: 40px; + padding: 9px 16px; + color: var(--light-white); + background: var(--accent); + border: 1px solid var(--accent); + border-radius: 8px; +} +.prototype-drilldown .attention-actions .ui-button.ui-button.attention-actions__send:disabled { + color: var(--grey-for-text); + background: var(--light-gray); + border-color: var(--medium-grey-for-outline); +} +@media (max-width: 599px) { + .prototype-drilldown .attention-actions__buttons { + flex-basis: 100%; + width: 100%; + } + .prototype-drilldown .attention-actions__buttons .ui-button.ui-button { + flex: 1 1 100%; + min-height: 44px; + } +} diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/global-reference.css b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/global-reference.css new file mode 100644 index 000000000..52ee381d9 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/global-reference.css @@ -0,0 +1,4685 @@ +/** + * projects/social_platform/src/styles.scss + * + * @format + */ + +:root { + --gradient: linear-gradient(90deg, #242424 0%, #8a63e6 50%); + --gradient-mild: linear-gradient(90deg, #501d53 0.3%, #3b1f72 22%, #8a63e6 100%); + --accent: #8a63e6; + --accent-dark: rgb(122.8167938931, 99, 179); + --accent-mild: rgba(108, 39, 255, 0.6); + --accent-light: #e8e0fa; + --accent-medium: #9764ba; + --gold: #e5b25d; + --gold-light: #fdf8f0; + --gold-dark: #c69849; + --light-white: #fff; + --white: #fafafa; + --black: #333; + --dark-grey: #a59fb9; + --gray: #d3d3d3; + --light-gray: #f9f9f9; + --grey-button: #e5e5e5e5; + --medium-grey-for-outline: #eee; + --grey-for-text: #827e80; + --green: #88c9a1; + --green-light: #e3f0e8; + --green-dark: #297373; + --lime: #d6ff54; + --red: #d48a9e; + --red-light: #f4e2e7; + --red-dark: rgb(186.5, 138, 151.1081081081); + --blue-dark: #2f36aa; + --purple: #9a80e6; + --cyan: #4cd9f1; +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 900; + src: + local(""), + url("./media/Mont-Black-CKKAF2O3.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Black-ZXUYXEJ6.woff2") format("woff2"), + url("./media/Mont-Black-UEG4C46Y.woff") format("woff"), + url("./media/Mont-Black-O4OJXHSC.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 800; + src: url("./media/Mont-Heavy-CZVFDIWI.eot"); + src: + local(""), + url("./media/Mont-Heavy-CZVFDIWI.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Heavy-SAJ5B6FS.woff2") format("woff2"), + url("./media/Mont-Heavy-WMPUKQNL.woff") format("woff"), + url("./media/Mont-Heavy-JVDMIVZJ.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: bold; + src: url("./media/Mont-Bold-JCFB53E2.eot"); + src: + local(""), + url("./media/Mont-Bold-JCFB53E2.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Bold-7L6HWBWW.woff2") format("woff2"), + url("./media/Mont-Bold-A3OSQYTY.woff") format("woff"), + url("./media/Mont-Bold-VNDQNQDW.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 600; + src: url("./media/Mont-SemiBold-BZSF3DKB.eot"); + src: + local(""), + url("./media/Mont-SemiBold-BZSF3DKB.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-SemiBold-7XOUXTAI.woff2") format("woff2"), + url("./media/Mont-SemiBold-WNUGA77E.woff") format("woff"), + url("./media/Mont-SemiBold-M6WLAC2K.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: normal; + src: url("./media/Mont-Regular-RSRKQCMK.eot"); + src: + local(""), + url("./media/Mont-Regular-RSRKQCMK.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Regular-UA5RCVYG.woff2") format("woff2"), + url("./media/Mont-Regular-T6L2NNOO.woff") format("woff"), + url("./media/Mont-Regular-5LPDNI7X.ttf") format("truetype"); +} +.text-heading-1 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-1-bold { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-2 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-2-bold { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-3 { + font-family: Mont, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-heading-4 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-body-bold-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-body-12 { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.text-body-12-bold { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 130%; +} +@media screen and (max-width: 999px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--ui-text-muted); + } +} +@media screen and (min-width: 1000px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--grey-button); + } +} +@media screen and (max-width: 999px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +@media screen and (min-width: 1000px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: 6px; + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} +ul, +li, +ol { + list-style-type: none; +} +fieldset { + border: none; + outline: none; +} +img { + display: block; + max-width: 100%; +} +a, +.link { + color: var(--accent); + text-decoration: none; + transition: color 0.2s; +} +a:hover, +.link:hover { + color: var(--accent-hover); +} +.edit-link { + position: relative; +} +.edit-link__remove { + position: absolute; + top: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + width: 50px; + color: var(--white); + cursor: pointer; + background-color: var(--gray); + border-radius: 0 8px 8px 0; +} +button { + background-color: transparent; + border: none; + outline: none; +} +label.field-label { + display: block; + margin-bottom: 6px; + margin-left: 12px; + color: var(--black); + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.error { + display: block; + gap: 10px; + margin-top: 6px; + color: var(--red) !important; +} +.error i { + color: var(--red) !important; +} +.error p:not(:last-child) { + margin-bottom: 10px; +} +:root { + --rounded-sm: 3px; + --rounded-md: 5px; + --rounded-lg: 8px; + --rounded-xl: 15px; + --rounded-xxl: 45px; +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 900; + src: + local(""), + url("./media/Mont-Black-CKKAF2O3.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Black-ZXUYXEJ6.woff2") format("woff2"), + url("./media/Mont-Black-UEG4C46Y.woff") format("woff"), + url("./media/Mont-Black-O4OJXHSC.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 800; + src: url("./media/Mont-Heavy-CZVFDIWI.eot"); + src: + local(""), + url("./media/Mont-Heavy-CZVFDIWI.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Heavy-SAJ5B6FS.woff2") format("woff2"), + url("./media/Mont-Heavy-WMPUKQNL.woff") format("woff"), + url("./media/Mont-Heavy-JVDMIVZJ.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: bold; + src: url("./media/Mont-Bold-JCFB53E2.eot"); + src: + local(""), + url("./media/Mont-Bold-JCFB53E2.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Bold-7L6HWBWW.woff2") format("woff2"), + url("./media/Mont-Bold-A3OSQYTY.woff") format("woff"), + url("./media/Mont-Bold-VNDQNQDW.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 600; + src: url("./media/Mont-SemiBold-BZSF3DKB.eot"); + src: + local(""), + url("./media/Mont-SemiBold-BZSF3DKB.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-SemiBold-7XOUXTAI.woff2") format("woff2"), + url("./media/Mont-SemiBold-WNUGA77E.woff") format("woff"), + url("./media/Mont-SemiBold-M6WLAC2K.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: normal; + src: url("./media/Mont-Regular-RSRKQCMK.eot"); + src: + local(""), + url("./media/Mont-Regular-RSRKQCMK.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Regular-UA5RCVYG.woff2") format("woff2"), + url("./media/Mont-Regular-T6L2NNOO.woff") format("woff"), + url("./media/Mont-Regular-5LPDNI7X.ttf") format("truetype"); +} +.text-heading-1 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-1-bold { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-2 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-2-bold { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-3 { + font-family: Mont, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-heading-4 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-body-bold-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-body-12 { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.text-body-12-bold { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 130%; +} +@media screen and (max-width: 999px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--ui-text-muted); + } +} +@media screen and (min-width: 1000px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--grey-button); + } +} +@media screen and (max-width: 999px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +@media screen and (min-width: 1000px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: 6px; + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +.tag-list { + display: flex; + flex-wrap: wrap; +} +.tag-list__item { + display: block; + margin-bottom: 12px; +} +.tag-list__item:not(:last-child) { + margin-right: 10px; +} +.tag-list__remove-icon { + color: var(--red); + cursor: pointer; +} +.tag-list-form { + display: flex; +} +.tag-list-form__input { + flex-grow: 1; + margin-right: 6px; +} +.tag-list-form__button { + width: 52px; +} +.nav--vertical .nav__list { + flex-direction: column; + align-items: flex-start; +} +.nav--vertical .nav__link:not(:last-child) { + margin-right: 0; + margin-bottom: 30px; +} +.nav__list { + display: flex; + align-items: center; +} +.nav__link { + color: var(--dark-grey); + transition: color 0.2s; +} +.nav__link:not(:last-child) { + margin-right: 38px; +} +.nav__link:hover { + color: var(--black); +} +.nav__link--active { + color: var(--black); +} +.nav__item { + display: flex; + align-items: center; + color: inherit; + cursor: pointer; +} +.nav__item i { + margin-right: 12px; +} +.contact-link { + color: var(--accent); + cursor: pointer; + transition: color 0.2s; +} +.contact-link:hover { + color: var(--accent-dark); +} +.contact-link__link { + display: flex; + align-items: center; + cursor: pointer; +} +.contact-link__icon { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + margin-right: 6px; + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-xxl); +} +.image-preview-dialog { + position: fixed; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100vw; + max-width: 100vw; + height: 100vh; + max-height: 100vh; + padding: 40px; + margin: 0; + background: transparent; + border: none; +} +.image-preview-dialog::backdrop { + background: var(--ui-preview-scrim); + animation: fade-in 0.25s ease-out; +} +.image-preview-dialog__img { + max-width: 90vw; + max-height: 85vh; + object-fit: contain; + border-radius: var(--rounded-lg); + animation: zoom-in 0.25s ease-out; +} +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes zoom-in { + from { + opacity: 0; + transform: scale(0.85); + } + to { + opacity: 1; + transform: scale(1); + } +} +.members { + padding-bottom: 100px; +} +.members__bar { + padding: 20px; + margin-bottom: 12px; + background-color: var(--white); + border-radius: var(--rounded-xl); +} +.members__list { + display: grid; + grid-template-columns: 1fr; + grid-gap: 16px; + align-items: start; +} +@media screen and (min-width: 1000px) { + .members__list { + grid-template-columns: repeat(2, 1fr); + } +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 900; + src: + local(""), + url("./media/Mont-Black-CKKAF2O3.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Black-ZXUYXEJ6.woff2") format("woff2"), + url("./media/Mont-Black-UEG4C46Y.woff") format("woff"), + url("./media/Mont-Black-O4OJXHSC.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 800; + src: url("./media/Mont-Heavy-CZVFDIWI.eot"); + src: + local(""), + url("./media/Mont-Heavy-CZVFDIWI.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Heavy-SAJ5B6FS.woff2") format("woff2"), + url("./media/Mont-Heavy-WMPUKQNL.woff") format("woff"), + url("./media/Mont-Heavy-JVDMIVZJ.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: bold; + src: url("./media/Mont-Bold-JCFB53E2.eot"); + src: + local(""), + url("./media/Mont-Bold-JCFB53E2.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Bold-7L6HWBWW.woff2") format("woff2"), + url("./media/Mont-Bold-A3OSQYTY.woff") format("woff"), + url("./media/Mont-Bold-VNDQNQDW.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 600; + src: url("./media/Mont-SemiBold-BZSF3DKB.eot"); + src: + local(""), + url("./media/Mont-SemiBold-BZSF3DKB.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-SemiBold-7XOUXTAI.woff2") format("woff2"), + url("./media/Mont-SemiBold-WNUGA77E.woff") format("woff"), + url("./media/Mont-SemiBold-M6WLAC2K.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: normal; + src: url("./media/Mont-Regular-RSRKQCMK.eot"); + src: + local(""), + url("./media/Mont-Regular-RSRKQCMK.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Regular-UA5RCVYG.woff2") format("woff2"), + url("./media/Mont-Regular-T6L2NNOO.woff") format("woff"), + url("./media/Mont-Regular-5LPDNI7X.ttf") format("truetype"); +} +.text-heading-1 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-1-bold { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-2 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-2-bold { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-3 { + font-family: Mont, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-heading-4 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-body-bold-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-body-12 { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.text-body-12-bold { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 130%; +} +@media screen and (max-width: 999px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--ui-text-muted); + } +} +@media screen and (min-width: 1000px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--grey-button); + } +} +@media screen and (max-width: 999px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +@media screen and (min-width: 1000px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: 6px; + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +.auth { + padding: 100px 0; +} +.auth__logo { + position: fixed; + top: 45px; + left: 18px; + display: block; + width: 95px; +} +@media screen and (min-width: 1000px) { + .auth__logo { + top: 120px; + left: 200px; + width: 157px; + height: 30px; + } +} +.auth__greeting { + max-width: 510px; + margin-bottom: 20px; +} +@media screen and (min-width: 1000px) { + .auth__greeting { + margin-bottom: 25px; + } +} +@media screen and (min-width: 1000px) { + .auth__wrapper { + max-width: 333px; + } +} +.auth__title { + margin-bottom: 6px; + color: var(--black); + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.auth__title--register { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +@media screen and (min-width: 1000px) { + .auth__title--register { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; + } +} +.auth__info { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.auth__field { + margin-bottom: 12px; +} +.auth__field app-input, +.auth__field app-select { + display: block; +} +.auth__field app-input.auth__password-input { + --auth-password-eye-inset: 8px; + --auth-password-eye-size: 25px; + --auth-password-error-size: 20px; + --auth-password-error-gap: 4px; +} +.auth__field app-input.auth__password-input .field { + display: flex; + align-items: center; + background: var(--white); + border: 0.5px solid var(--gray); + border-radius: var(--rounded-xxl); +} +.auth__field app-input.auth__password-input .field:focus-within { + border-color: var(--accent); + box-shadow: 0 0 6px rgba(109, 40, 255, 0.3); +} +.auth__field app-input.auth__password-input .field--error { + border-color: var(--red); +} +.auth__field app-input.auth__password-input .field--error:focus-within { + box-shadow: 0 0 6px rgba(255, 40, 40, 0.308); +} +.auth__field app-input.auth__password-input .field__input { + box-sizing: border-box; + flex: 1 1 0; + width: 0; + min-width: 0; + padding-right: 4px; + border: 0; + border-radius: var(--rounded-xxl) 0 0 var(--rounded-xxl); + box-shadow: none; +} +.auth__field app-input.auth__password-input .field__right-icon { + position: relative; + top: auto; + right: auto !important; + flex: 0 0 var(--auth-password-eye-size); + width: var(--auth-password-eye-size); + height: var(--auth-password-eye-size); + padding: 0; + margin-right: var(--auth-password-eye-inset); + pointer-events: none; + background-color: transparent; + transform: none; +} +.auth__field app-input.auth__password-input .field__right-icon button { + pointer-events: auto; + cursor: pointer; +} +.auth__field app-input.auth__password-input .field__right-icon::before { + position: absolute; + top: 4px; + bottom: 4px; + left: 0; + width: 1px; + content: ""; + background: var(--medium-grey-for-outline); +} +.auth__field app-input.auth__password-input .field__error-icon { + position: static; + flex: 0 0 var(--auth-password-error-size); + width: var(--auth-password-error-size); + height: var(--auth-password-error-size); + margin: 0 var(--auth-password-error-gap); + transform: none; +} +.auth__password-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + padding: 0; + color: var(--dark-grey); + background: transparent; + border: 0; + border-radius: 50%; +} +.auth__password-toggle:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} +.auth__password-toggle--muted { + color: var(--dark-grey); +} +.auth__row { + display: flex; +} +.auth__row fieldset { + width: 50%; +} +.auth__row fieldset:not(:last-child) { + margin-right: 15px; +} +.auth__agreement { + display: flex; + align-items: center; + margin-bottom: 30px; + cursor: pointer; +} +.auth__checkbox { + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; +} +.auth__agreement-text { + color: var(--gray-100); +} +.auth__button { + display: block; + width: 100%; + margin-top: 35px; +} +.auth__button-typography { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +@media screen and (min-width: 1000px) { + .auth__button-typography { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; + } +} +@media screen and (min-width: 1000px) { + .auth__button { + margin-top: 30px; + } +} +.auth__button-icon { + display: none !important; + margin-left: 10px; + transform: rotate(90deg); +} +@media screen and (min-width: 1000px) { + .auth__button-icon { + display: block !important; + } +} +.auth__toggle { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; + margin-top: 4vh; +} +.auth a { + color: var(--accent); + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 3px; + transition: text-decoration-color 0.2s; +} +.auth a:hover { + text-decoration-color: var(--accent); +} +@media screen and (max-width: 749px) { + .auth { + padding: 24px 0; + } + .auth__logo { + position: static; + margin-bottom: 24px; + } + .auth__row { + flex-direction: column; + gap: 12px; + } + .auth__row fieldset { + width: 100%; + margin-right: 0; + } + .auth__field app-input.auth__password-input { + --auth-password-eye-size: 44px; + } + .auth__password-toggle { + width: 44px; + height: 44px; + } + .auth__agreement, + .auth__checkbox { + min-height: 44px; + } +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 900; + src: + local(""), + url("./media/Mont-Black-CKKAF2O3.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Black-ZXUYXEJ6.woff2") format("woff2"), + url("./media/Mont-Black-UEG4C46Y.woff") format("woff"), + url("./media/Mont-Black-O4OJXHSC.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 800; + src: url("./media/Mont-Heavy-CZVFDIWI.eot"); + src: + local(""), + url("./media/Mont-Heavy-CZVFDIWI.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Heavy-SAJ5B6FS.woff2") format("woff2"), + url("./media/Mont-Heavy-WMPUKQNL.woff") format("woff"), + url("./media/Mont-Heavy-JVDMIVZJ.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: bold; + src: url("./media/Mont-Bold-JCFB53E2.eot"); + src: + local(""), + url("./media/Mont-Bold-JCFB53E2.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Bold-7L6HWBWW.woff2") format("woff2"), + url("./media/Mont-Bold-A3OSQYTY.woff") format("woff"), + url("./media/Mont-Bold-VNDQNQDW.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: 600; + src: url("./media/Mont-SemiBold-BZSF3DKB.eot"); + src: + local(""), + url("./media/Mont-SemiBold-BZSF3DKB.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-SemiBold-7XOUXTAI.woff2") format("woff2"), + url("./media/Mont-SemiBold-WNUGA77E.woff") format("woff"), + url("./media/Mont-SemiBold-M6WLAC2K.ttf") format("truetype"); +} +@font-face { + font-family: Mont; + font-style: normal; + font-weight: normal; + src: url("./media/Mont-Regular-RSRKQCMK.eot"); + src: + local(""), + url("./media/Mont-Regular-RSRKQCMK.eot?#iefix") format("embedded-opentype"), + url("./media/Mont-Regular-UA5RCVYG.woff2") format("woff2"), + url("./media/Mont-Regular-T6L2NNOO.woff") format("woff"), + url("./media/Mont-Regular-5LPDNI7X.ttf") format("truetype"); +} +.text-heading-1 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-1-bold { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-2 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-heading-2-bold { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-heading-3 { + font-family: Mont, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-heading-4 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-body-bold-18 { + font-family: Mont, sans-serif; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-16 { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; +} +.text-bold-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +.text-body-14 { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} +.text-body-12 { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; +} +.text-body-12-bold { + font-family: Mont, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 130%; +} +@media screen and (max-width: 999px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--ui-text-muted); + } +} +@media screen and (min-width: 1000px) { + .text-body-10 { + font-family: Mont, sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--grey-button); + } +} +@media screen and (max-width: 999px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: var(--ui-font-caption); + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +@media screen and (min-width: 1000px) { + .text-body-6 { + font-family: Mont, sans-serif; + font-size: 6px; + font-style: normal; + font-weight: 400; + line-height: 130%; + } +} +.unsubscribe-modal { + display: flex; + flex-direction: column; + align-items: center; + margin: 4px 20px; +} +.unsubscribe-modal h3 { + margin-bottom: 40px; + text-align: center; +} +.unsubscribe-modal app-avatar { + margin-bottom: 30px; +} +@media screen and (min-width: 1000px) { + .unsubscribe-modal app-button ::ng-deep .button--inline { + min-height: 52px; + } +} +.unsubscribe-modal__buttons { + display: flex; + flex-direction: column; + gap: 15px; + justify-content: space-between; + width: 100%; +} +@media screen and (min-width: 1000px) { + .unsubscribe-modal__buttons { + flex-direction: row; + } +} +.unsubscribe-modal-btn__typography { + font-family: Mont, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} +@media screen and (min-width: 1000px) { + .unsubscribe-modal-btn__typography { + font-family: Mont, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; + } +} +:root { + --app-height: 100vh; +} +@media screen and (max-width: 999px) { + :root { + --page-gutter: 16px; + } + @supports (height: 100dvh) { + :root { + --app-height: 100dvh; + } + } +} +html { + scrollbar-gutter: stable; +} +html, +body { + min-width: 280px; + height: var(--app-height, 100%); +} +body { + margin: 0; + overflow-x: hidden; + font-family: Mont, sans-serif; +} +button, +input, +textarea, +select { + font-family: inherit; +} +.mat-elevation-z0, +.mat-mdc-elevation-specific.mat-elevation-z0 { + box-shadow: var( + --mat-app-elevation-shadow-level-0, + 0px 0px 0px 0px --mat-sys-shadow, + 0px 0px 0px 0px --mat-sys-shadow, + 0px 0px 0px 0px --mat-sys-shadow + ); +} +.mat-elevation-z1, +.mat-mdc-elevation-specific.mat-elevation-z1 { + box-shadow: var( + --mat-app-elevation-shadow-level-1, + 0px 2px 1px -1px --mat-sys-shadow, + 0px 1px 1px 0px --mat-sys-shadow, + 0px 1px 3px 0px --mat-sys-shadow + ); +} +.mat-elevation-z2, +.mat-mdc-elevation-specific.mat-elevation-z2 { + box-shadow: var( + --mat-app-elevation-shadow-level-2, + 0px 3px 1px -2px --mat-sys-shadow, + 0px 2px 2px 0px --mat-sys-shadow, + 0px 1px 5px 0px --mat-sys-shadow + ); +} +.mat-elevation-z3, +.mat-mdc-elevation-specific.mat-elevation-z3 { + box-shadow: var( + --mat-app-elevation-shadow-level-3, + 0px 3px 3px -2px --mat-sys-shadow, + 0px 3px 4px 0px --mat-sys-shadow, + 0px 1px 8px 0px --mat-sys-shadow + ); +} +.mat-elevation-z4, +.mat-mdc-elevation-specific.mat-elevation-z4 { + box-shadow: var( + --mat-app-elevation-shadow-level-4, + 0px 2px 4px -1px --mat-sys-shadow, + 0px 4px 5px 0px --mat-sys-shadow, + 0px 1px 10px 0px --mat-sys-shadow + ); +} +.mat-elevation-z5, +.mat-mdc-elevation-specific.mat-elevation-z5 { + box-shadow: var( + --mat-app-elevation-shadow-level-5, + 0px 3px 5px -1px --mat-sys-shadow, + 0px 5px 8px 0px --mat-sys-shadow, + 0px 1px 14px 0px --mat-sys-shadow + ); +} +.mat-elevation-z6, +.mat-mdc-elevation-specific.mat-elevation-z6 { + box-shadow: var( + --mat-app-elevation-shadow-level-6, + 0px 3px 5px -1px --mat-sys-shadow, + 0px 6px 10px 0px --mat-sys-shadow, + 0px 1px 18px 0px --mat-sys-shadow + ); +} +.mat-elevation-z7, +.mat-mdc-elevation-specific.mat-elevation-z7 { + box-shadow: var( + --mat-app-elevation-shadow-level-7, + 0px 4px 5px -2px --mat-sys-shadow, + 0px 7px 10px 1px --mat-sys-shadow, + 0px 2px 16px 1px --mat-sys-shadow + ); +} +.mat-elevation-z8, +.mat-mdc-elevation-specific.mat-elevation-z8 { + box-shadow: var( + --mat-app-elevation-shadow-level-8, + 0px 5px 5px -3px --mat-sys-shadow, + 0px 8px 10px 1px --mat-sys-shadow, + 0px 3px 14px 2px --mat-sys-shadow + ); +} +.mat-elevation-z9, +.mat-mdc-elevation-specific.mat-elevation-z9 { + box-shadow: var( + --mat-app-elevation-shadow-level-9, + 0px 5px 6px -3px --mat-sys-shadow, + 0px 9px 12px 1px --mat-sys-shadow, + 0px 3px 16px 2px --mat-sys-shadow + ); +} +.mat-elevation-z10, +.mat-mdc-elevation-specific.mat-elevation-z10 { + box-shadow: var( + --mat-app-elevation-shadow-level-10, + 0px 6px 6px -3px --mat-sys-shadow, + 0px 10px 14px 1px --mat-sys-shadow, + 0px 4px 18px 3px --mat-sys-shadow + ); +} +.mat-elevation-z11, +.mat-mdc-elevation-specific.mat-elevation-z11 { + box-shadow: var( + --mat-app-elevation-shadow-level-11, + 0px 6px 7px -4px --mat-sys-shadow, + 0px 11px 15px 1px --mat-sys-shadow, + 0px 4px 20px 3px --mat-sys-shadow + ); +} +.mat-elevation-z12, +.mat-mdc-elevation-specific.mat-elevation-z12 { + box-shadow: var( + --mat-app-elevation-shadow-level-12, + 0px 7px 8px -4px --mat-sys-shadow, + 0px 12px 17px 2px --mat-sys-shadow, + 0px 5px 22px 4px --mat-sys-shadow + ); +} +.mat-elevation-z13, +.mat-mdc-elevation-specific.mat-elevation-z13 { + box-shadow: var( + --mat-app-elevation-shadow-level-13, + 0px 7px 8px -4px --mat-sys-shadow, + 0px 13px 19px 2px --mat-sys-shadow, + 0px 5px 24px 4px --mat-sys-shadow + ); +} +.mat-elevation-z14, +.mat-mdc-elevation-specific.mat-elevation-z14 { + box-shadow: var( + --mat-app-elevation-shadow-level-14, + 0px 7px 9px -4px --mat-sys-shadow, + 0px 14px 21px 2px --mat-sys-shadow, + 0px 5px 26px 4px --mat-sys-shadow + ); +} +.mat-elevation-z15, +.mat-mdc-elevation-specific.mat-elevation-z15 { + box-shadow: var( + --mat-app-elevation-shadow-level-15, + 0px 8px 9px -5px --mat-sys-shadow, + 0px 15px 22px 2px --mat-sys-shadow, + 0px 6px 28px 5px --mat-sys-shadow + ); +} +.mat-elevation-z16, +.mat-mdc-elevation-specific.mat-elevation-z16 { + box-shadow: var( + --mat-app-elevation-shadow-level-16, + 0px 8px 10px -5px --mat-sys-shadow, + 0px 16px 24px 2px --mat-sys-shadow, + 0px 6px 30px 5px --mat-sys-shadow + ); +} +.mat-elevation-z17, +.mat-mdc-elevation-specific.mat-elevation-z17 { + box-shadow: var( + --mat-app-elevation-shadow-level-17, + 0px 8px 11px -5px --mat-sys-shadow, + 0px 17px 26px 2px --mat-sys-shadow, + 0px 6px 32px 5px --mat-sys-shadow + ); +} +.mat-elevation-z18, +.mat-mdc-elevation-specific.mat-elevation-z18 { + box-shadow: var( + --mat-app-elevation-shadow-level-18, + 0px 9px 11px -5px --mat-sys-shadow, + 0px 18px 28px 2px --mat-sys-shadow, + 0px 7px 34px 6px --mat-sys-shadow + ); +} +.mat-elevation-z19, +.mat-mdc-elevation-specific.mat-elevation-z19 { + box-shadow: var( + --mat-app-elevation-shadow-level-19, + 0px 9px 12px -6px --mat-sys-shadow, + 0px 19px 29px 2px --mat-sys-shadow, + 0px 7px 36px 6px --mat-sys-shadow + ); +} +.mat-elevation-z20, +.mat-mdc-elevation-specific.mat-elevation-z20 { + box-shadow: var( + --mat-app-elevation-shadow-level-20, + 0px 10px 13px -6px --mat-sys-shadow, + 0px 20px 31px 3px --mat-sys-shadow, + 0px 8px 38px 7px --mat-sys-shadow + ); +} +.mat-elevation-z21, +.mat-mdc-elevation-specific.mat-elevation-z21 { + box-shadow: var( + --mat-app-elevation-shadow-level-21, + 0px 10px 13px -6px --mat-sys-shadow, + 0px 21px 33px 3px --mat-sys-shadow, + 0px 8px 40px 7px --mat-sys-shadow + ); +} +.mat-elevation-z22, +.mat-mdc-elevation-specific.mat-elevation-z22 { + box-shadow: var( + --mat-app-elevation-shadow-level-22, + 0px 10px 14px -6px --mat-sys-shadow, + 0px 22px 35px 3px --mat-sys-shadow, + 0px 8px 42px 7px --mat-sys-shadow + ); +} +.mat-elevation-z23, +.mat-mdc-elevation-specific.mat-elevation-z23 { + box-shadow: var( + --mat-app-elevation-shadow-level-23, + 0px 11px 14px -7px --mat-sys-shadow, + 0px 23px 36px 3px --mat-sys-shadow, + 0px 9px 44px 8px --mat-sys-shadow + ); +} +.mat-elevation-z24, +.mat-mdc-elevation-specific.mat-elevation-z24 { + box-shadow: var( + --mat-app-elevation-shadow-level-24, + 0px 11px 15px -7px --mat-sys-shadow, + 0px 24px 38px 3px --mat-sys-shadow, + 0px 9px 46px 8px --mat-sys-shadow + ); +} +html { + --mat-sys-on-surface: initial; +} +.mat-app-background { + background-color: var(--mat-app-background-color, var(--mat-sys-background, transparent)); + color: var(--mat-app-text-color, var(--mat-sys-on-background, inherit)); +} +.mat-progress-bar { + z-index: 200; +} +html { + --mat-app-background-color: #fafafa; + --mat-app-text-color: rgba(0, 0, 0, 0.87); + --mat-app-elevation-shadow-level-0: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-1: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-2: + 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-3: + 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), + 0px 1px 8px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-4: + 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), + 0px 1px 10px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-5: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14), + 0px 1px 14px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-6: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), + 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-7: + 0px 4px 5px -2px rgba(0, 0, 0, 0.2), 0px 7px 10px 1px rgba(0, 0, 0, 0.14), + 0px 2px 16px 1px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-8: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-9: + 0px 5px 6px -3px rgba(0, 0, 0, 0.2), 0px 9px 12px 1px rgba(0, 0, 0, 0.14), + 0px 3px 16px 2px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-10: + 0px 6px 6px -3px rgba(0, 0, 0, 0.2), 0px 10px 14px 1px rgba(0, 0, 0, 0.14), + 0px 4px 18px 3px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-11: + 0px 6px 7px -4px rgba(0, 0, 0, 0.2), 0px 11px 15px 1px rgba(0, 0, 0, 0.14), + 0px 4px 20px 3px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-12: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), + 0px 5px 22px 4px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-13: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 13px 19px 2px rgba(0, 0, 0, 0.14), + 0px 5px 24px 4px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-14: + 0px 7px 9px -4px rgba(0, 0, 0, 0.2), 0px 14px 21px 2px rgba(0, 0, 0, 0.14), + 0px 5px 26px 4px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-15: + 0px 8px 9px -5px rgba(0, 0, 0, 0.2), 0px 15px 22px 2px rgba(0, 0, 0, 0.14), + 0px 6px 28px 5px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-16: + 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), + 0px 6px 30px 5px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-17: + 0px 8px 11px -5px rgba(0, 0, 0, 0.2), 0px 17px 26px 2px rgba(0, 0, 0, 0.14), + 0px 6px 32px 5px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-18: + 0px 9px 11px -5px rgba(0, 0, 0, 0.2), 0px 18px 28px 2px rgba(0, 0, 0, 0.14), + 0px 7px 34px 6px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-19: + 0px 9px 12px -6px rgba(0, 0, 0, 0.2), 0px 19px 29px 2px rgba(0, 0, 0, 0.14), + 0px 7px 36px 6px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-20: + 0px 10px 13px -6px rgba(0, 0, 0, 0.2), 0px 20px 31px 3px rgba(0, 0, 0, 0.14), + 0px 8px 38px 7px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-21: + 0px 10px 13px -6px rgba(0, 0, 0, 0.2), 0px 21px 33px 3px rgba(0, 0, 0, 0.14), + 0px 8px 40px 7px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-22: + 0px 10px 14px -6px rgba(0, 0, 0, 0.2), 0px 22px 35px 3px rgba(0, 0, 0, 0.14), + 0px 8px 42px 7px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-23: + 0px 11px 14px -7px rgba(0, 0, 0, 0.2), 0px 23px 36px 3px rgba(0, 0, 0, 0.14), + 0px 9px 44px 8px rgba(0, 0, 0, 0.12); + --mat-app-elevation-shadow-level-24: + 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), + 0px 9px 46px 8px rgba(0, 0, 0, 0.12); +} +html { + --mat-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); +} +html { + --mat-option-selected-state-label-text-color: #3f51b5; + --mat-option-label-text-color: rgba(0, 0, 0, 0.87); + --mat-option-hover-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 4%, transparent); + --mat-option-focus-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-option-selected-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); +} +.mat-accent { + --mat-option-selected-state-label-text-color: #ff4081; + --mat-option-label-text-color: rgba(0, 0, 0, 0.87); + --mat-option-hover-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 4%, transparent); + --mat-option-focus-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-option-selected-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); +} +.mat-warn { + --mat-option-selected-state-label-text-color: #f44336; + --mat-option-label-text-color: rgba(0, 0, 0, 0.87); + --mat-option-hover-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 4%, transparent); + --mat-option-focus-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-option-selected-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); +} +html { + --mat-optgroup-label-text-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-pseudo-checkbox-full-selected-icon-color: #ff4081; + --mat-pseudo-checkbox-full-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-pseudo-checkbox-full-disabled-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-full-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: #ff4081; + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); +} +.mat-primary { + --mat-pseudo-checkbox-full-selected-icon-color: #3f51b5; + --mat-pseudo-checkbox-full-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-pseudo-checkbox-full-disabled-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-full-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: #3f51b5; + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); +} +.mat-accent { + --mat-pseudo-checkbox-full-selected-icon-color: #ff4081; + --mat-pseudo-checkbox-full-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-pseudo-checkbox-full-disabled-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-full-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: #ff4081; + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); +} +.mat-warn { + --mat-pseudo-checkbox-full-selected-icon-color: #f44336; + --mat-pseudo-checkbox-full-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-pseudo-checkbox-full-disabled-selected-checkmark-color: #fafafa; + --mat-pseudo-checkbox-full-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-full-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: #f44336; + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); +} +html { + --mat-option-label-text-font: Mont, sans-serif; + --mat-option-label-text-line-height: 24px; + --mat-option-label-text-size: 16px; + --mat-option-label-text-tracking: 0.03125em; + --mat-option-label-text-weight: 400; +} +html { + --mat-optgroup-label-text-font: Mont, sans-serif; + --mat-optgroup-label-text-line-height: 24px; + --mat-optgroup-label-text-size: 16px; + --mat-optgroup-label-text-tracking: 0.03125em; + --mat-optgroup-label-text-weight: 400; +} +html { + --mat-card-elevated-container-shape: 4px; + --mat-card-outlined-container-shape: 4px; + --mat-card-filled-container-shape: 4px; + --mat-card-outlined-outline-width: 1px; +} +html { + --mat-card-elevated-container-color: white; + --mat-card-elevated-container-elevation: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + --mat-card-outlined-container-color: white; + --mat-card-outlined-container-elevation: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + --mat-card-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-card-subtitle-text-color: rgba(0, 0, 0, 0.54); + --mat-card-filled-container-color: white; + --mat-card-filled-container-elevation: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); +} +html { + --mat-card-title-text-font: Mont, sans-serif; + --mat-card-title-text-line-height: 32px; + --mat-card-title-text-size: 20px; + --mat-card-title-text-tracking: 0.0125em; + --mat-card-title-text-weight: 500; + --mat-card-subtitle-text-font: Mont, sans-serif; + --mat-card-subtitle-text-line-height: 22px; + --mat-card-subtitle-text-size: 14px; + --mat-card-subtitle-text-tracking: 0.0071428571em; + --mat-card-subtitle-text-weight: 500; +} +html { + --mat-progress-bar-active-indicator-height: 4px; + --mat-progress-bar-track-height: 4px; + --mat-progress-bar-track-shape: 0; +} +.mat-mdc-progress-bar { + --mat-progress-bar-active-indicator-color: #3f51b5; + --mat-progress-bar-track-color: rgba(63, 81, 181, 0.25); +} +.mat-mdc-progress-bar.mat-accent { + --mat-progress-bar-active-indicator-color: #ff4081; + --mat-progress-bar-track-color: rgba(255, 64, 129, 0.25); +} +.mat-mdc-progress-bar.mat-warn { + --mat-progress-bar-active-indicator-color: #f44336; + --mat-progress-bar-track-color: rgba(244, 67, 54, 0.25); +} +html { + --mat-tooltip-container-shape: 4px; + --mat-tooltip-supporting-text-line-height: 16px; +} +html { + --mat-tooltip-container-color: #424242; + --mat-tooltip-supporting-text-color: white; +} +html { + --mat-tooltip-supporting-text-font: Mont, sans-serif; + --mat-tooltip-supporting-text-size: 12px; + --mat-tooltip-supporting-text-weight: 400; + --mat-tooltip-supporting-text-tracking: 0.0333333333em; +} +html { + --mat-form-field-filled-active-indicator-height: 1px; + --mat-form-field-filled-focus-active-indicator-height: 2px; + --mat-form-field-filled-container-shape: 4px; + --mat-form-field-outlined-outline-width: 1px; + --mat-form-field-outlined-focus-outline-width: 2px; + --mat-form-field-outlined-container-shape: 4px; +} +html { + --mat-form-field-focus-select-arrow-color: color-mix(in srgb, #3f51b5 87%, transparent); + --mat-form-field-filled-caret-color: #3f51b5; + --mat-form-field-filled-focus-active-indicator-color: #3f51b5; + --mat-form-field-filled-focus-label-text-color: color-mix(in srgb, #3f51b5 87%, transparent); + --mat-form-field-outlined-caret-color: #3f51b5; + --mat-form-field-outlined-focus-outline-color: #3f51b5; + --mat-form-field-outlined-focus-label-text-color: color-mix(in srgb, #3f51b5 87%, transparent); + --mat-form-field-disabled-input-text-placeholder-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-form-field-error-text-color: #f44336; + --mat-form-field-select-option-text-color: inherit; + --mat-form-field-select-disabled-option-text-color: GrayText; + --mat-form-field-leading-icon-color: unset; + --mat-form-field-disabled-leading-icon-color: unset; + --mat-form-field-trailing-icon-color: unset; + --mat-form-field-disabled-trailing-icon-color: unset; + --mat-form-field-error-focus-trailing-icon-color: unset; + --mat-form-field-error-hover-trailing-icon-color: unset; + --mat-form-field-error-trailing-icon-color: unset; + --mat-form-field-enabled-select-arrow-color: rgba(0, 0, 0, 0.54); + --mat-form-field-disabled-select-arrow-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-hover-state-layer-opacity: 0.04; + --mat-form-field-focus-state-layer-opacity: 0.12; + --mat-form-field-filled-container-color: #f6f6f6; + --mat-form-field-filled-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 4%, + transparent + ); + --mat-form-field-filled-label-text-color: rgba(0, 0, 0, 0.54); + --mat-form-field-filled-hover-label-text-color: rgba(0, 0, 0, 0.54); + --mat-form-field-filled-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-filled-input-text-color: rgba(0, 0, 0, 0.87); + --mat-form-field-filled-disabled-input-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-filled-input-text-placeholder-color: rgba(0, 0, 0, 0.54); + --mat-form-field-filled-error-hover-label-text-color: #f44336; + --mat-form-field-filled-error-focus-label-text-color: #f44336; + --mat-form-field-filled-error-label-text-color: #f44336; + --mat-form-field-filled-error-caret-color: #f44336; + --mat-form-field-filled-active-indicator-color: rgba(0, 0, 0, 0.54); + --mat-form-field-filled-disabled-active-indicator-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-form-field-filled-hover-active-indicator-color: rgba(0, 0, 0, 0.87); + --mat-form-field-filled-error-active-indicator-color: #f44336; + --mat-form-field-filled-error-focus-active-indicator-color: #f44336; + --mat-form-field-filled-error-hover-active-indicator-color: #f44336; + --mat-form-field-outlined-label-text-color: rgba(0, 0, 0, 0.54); + --mat-form-field-outlined-hover-label-text-color: rgba(0, 0, 0, 0.87); + --mat-form-field-outlined-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-outlined-input-text-color: rgba(0, 0, 0, 0.87); + --mat-form-field-outlined-disabled-input-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-form-field-outlined-input-text-placeholder-color: rgba(0, 0, 0, 0.54); + --mat-form-field-outlined-error-caret-color: #f44336; + --mat-form-field-outlined-error-focus-label-text-color: #f44336; + --mat-form-field-outlined-error-label-text-color: #f44336; + --mat-form-field-outlined-error-hover-label-text-color: #f44336; + --mat-form-field-outlined-outline-color: rgba(0, 0, 0, 0.38); + --mat-form-field-outlined-disabled-outline-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-form-field-outlined-hover-outline-color: rgba(0, 0, 0, 0.87); + --mat-form-field-outlined-error-focus-outline-color: #f44336; + --mat-form-field-outlined-error-hover-outline-color: #f44336; + --mat-form-field-outlined-error-outline-color: #f44336; +} +.mat-mdc-form-field.mat-accent { + --mat-form-field-focus-select-arrow-color: color-mix(in srgb, #ff4081 87%, transparent); + --mat-form-field-filled-caret-color: #ff4081; + --mat-form-field-filled-focus-active-indicator-color: #ff4081; + --mat-form-field-filled-focus-label-text-color: color-mix(in srgb, #ff4081 87%, transparent); + --mat-form-field-outlined-caret-color: #ff4081; + --mat-form-field-outlined-focus-outline-color: #ff4081; + --mat-form-field-outlined-focus-label-text-color: color-mix(in srgb, #ff4081 87%, transparent); +} +.mat-mdc-form-field.mat-warn { + --mat-form-field-focus-select-arrow-color: color-mix(in srgb, #f44336 87%, transparent); + --mat-form-field-filled-caret-color: #f44336; + --mat-form-field-filled-focus-active-indicator-color: #f44336; + --mat-form-field-filled-focus-label-text-color: color-mix(in srgb, #f44336 87%, transparent); + --mat-form-field-outlined-caret-color: #f44336; + --mat-form-field-outlined-focus-outline-color: #f44336; + --mat-form-field-outlined-focus-label-text-color: color-mix(in srgb, #f44336 87%, transparent); +} +html { + --mat-form-field-container-height: 56px; + --mat-form-field-filled-label-display: block; + --mat-form-field-container-vertical-padding: 16px; + --mat-form-field-filled-with-label-container-padding-top: 24px; + --mat-form-field-filled-with-label-container-padding-bottom: 8px; +} +html { + --mat-form-field-container-text-font: Mont, sans-serif; + --mat-form-field-container-text-line-height: 24px; + --mat-form-field-container-text-size: 16px; + --mat-form-field-container-text-tracking: 0.03125em; + --mat-form-field-container-text-weight: 400; + --mat-form-field-outlined-label-text-populated-size: 16px; + --mat-form-field-subscript-text-font: Mont, sans-serif; + --mat-form-field-subscript-text-line-height: 20px; + --mat-form-field-subscript-text-size: 12px; + --mat-form-field-subscript-text-tracking: 0.0333333333em; + --mat-form-field-subscript-text-weight: 400; + --mat-form-field-filled-label-text-font: Mont, sans-serif; + --mat-form-field-filled-label-text-size: 16px; + --mat-form-field-filled-label-text-tracking: 0.03125em; + --mat-form-field-filled-label-text-weight: 400; + --mat-form-field-outlined-label-text-font: Mont, sans-serif; + --mat-form-field-outlined-label-text-size: 16px; + --mat-form-field-outlined-label-text-tracking: 0.03125em; + --mat-form-field-outlined-label-text-weight: 400; +} +html { + --mat-select-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} +html { + --mat-select-panel-background-color: white; + --mat-select-enabled-trigger-text-color: rgba(0, 0, 0, 0.87); + --mat-select-disabled-trigger-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-select-placeholder-text-color: rgba(0, 0, 0, 0.54); + --mat-select-enabled-arrow-color: rgba(0, 0, 0, 0.54); + --mat-select-disabled-arrow-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-select-focused-arrow-color: #3f51b5; + --mat-select-invalid-arrow-color: #f44336; +} +.mat-mdc-form-field.mat-accent { + --mat-select-panel-background-color: white; + --mat-select-enabled-trigger-text-color: rgba(0, 0, 0, 0.87); + --mat-select-disabled-trigger-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-select-placeholder-text-color: rgba(0, 0, 0, 0.54); + --mat-select-enabled-arrow-color: rgba(0, 0, 0, 0.54); + --mat-select-disabled-arrow-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-select-focused-arrow-color: #ff4081; + --mat-select-invalid-arrow-color: #f44336; +} +.mat-mdc-form-field.mat-warn { + --mat-select-panel-background-color: white; + --mat-select-enabled-trigger-text-color: rgba(0, 0, 0, 0.87); + --mat-select-disabled-trigger-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-select-placeholder-text-color: rgba(0, 0, 0, 0.54); + --mat-select-enabled-arrow-color: rgba(0, 0, 0, 0.54); + --mat-select-disabled-arrow-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-select-focused-arrow-color: #f44336; + --mat-select-invalid-arrow-color: #f44336; +} +html { + --mat-select-arrow-transform: translateY(-8px); +} +html { + --mat-select-trigger-text-font: Mont, sans-serif; + --mat-select-trigger-text-line-height: 24px; + --mat-select-trigger-text-size: 16px; + --mat-select-trigger-text-tracking: 0.03125em; + --mat-select-trigger-text-weight: 400; +} +html { + --mat-autocomplete-container-shape: 4px; + --mat-autocomplete-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} +html { + --mat-autocomplete-background-color: white; +} +html { + --mat-dialog-container-shape: 4px; + --mat-dialog-container-elevation-shadow: + 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), + 0px 9px 46px 8px rgba(0, 0, 0, 0.12); + --mat-dialog-container-max-width: 80vw; + --mat-dialog-container-small-max-width: 80vw; + --mat-dialog-container-min-width: 0; + --mat-dialog-actions-alignment: start; + --mat-dialog-actions-padding: 8px; + --mat-dialog-content-padding: 20px 24px; + --mat-dialog-with-actions-content-padding: 20px 24px; + --mat-dialog-headline-padding: 0 24px 9px; +} +html { + --mat-dialog-container-color: white; + --mat-dialog-subhead-color: rgba(0, 0, 0, 0.87); + --mat-dialog-supporting-text-color: rgba(0, 0, 0, 0.54); +} +html { + --mat-dialog-subhead-font: Mont, sans-serif; + --mat-dialog-subhead-line-height: 32px; + --mat-dialog-subhead-size: 20px; + --mat-dialog-subhead-weight: 500; + --mat-dialog-subhead-tracking: 0.0125em; + --mat-dialog-supporting-text-font: Mont, sans-serif; + --mat-dialog-supporting-text-line-height: 24px; + --mat-dialog-supporting-text-size: 16px; + --mat-dialog-supporting-text-weight: 400; + --mat-dialog-supporting-text-tracking: 0.03125em; +} +.mat-mdc-standard-chip { + --mat-chip-container-shape-radius: 16px; + --mat-chip-disabled-container-opacity: 0.4; + --mat-chip-disabled-outline-color: transparent; + --mat-chip-flat-selected-outline-width: 0; + --mat-chip-focus-outline-color: transparent; + --mat-chip-hover-state-layer-opacity: 0.04; + --mat-chip-outline-color: transparent; + --mat-chip-outline-width: 0; + --mat-chip-selected-hover-state-layer-opacity: 0.04; + --mat-chip-selected-trailing-action-state-layer-color: transparent; + --mat-chip-trailing-action-focus-opacity: 1; + --mat-chip-trailing-action-focus-state-layer-opacity: 0; + --mat-chip-trailing-action-hover-state-layer-opacity: 0; + --mat-chip-trailing-action-opacity: 0.54; + --mat-chip-trailing-action-state-layer-color: transparent; + --mat-chip-with-avatar-avatar-shape-radius: 14px; + --mat-chip-with-avatar-avatar-size: 28px; + --mat-chip-with-avatar-disabled-avatar-opacity: 1; + --mat-chip-with-icon-disabled-icon-opacity: 1; + --mat-chip-with-icon-icon-size: 18px; + --mat-chip-with-trailing-icon-disabled-trailing-icon-opacity: 1; +} +.mat-mdc-standard-chip { + --mat-chip-disabled-label-text-color: rgba(0, 0, 0, 0.87); + --mat-chip-elevated-container-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-chip-elevated-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-chip-elevated-selected-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-chip-flat-disabled-selected-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-chip-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-focus-state-layer-opacity: 0.12; + --mat-chip-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-label-text-color: rgba(0, 0, 0, 0.87); + --mat-chip-selected-disabled-trailing-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-selected-focus-state-layer-color: 0.12; + --mat-chip-selected-focus-state-layer-opacity: 0.12; + --mat-chip-selected-hover-state-layer-color: 0.04; + --mat-chip-selected-label-text-color: rgba(0, 0, 0, 0.87); + --mat-chip-selected-trailing-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-icon-disabled-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-icon-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-icon-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-trailing-icon-disabled-trailing-icon-color: rgba(0, 0, 0, 0.87); + --mat-chip-with-trailing-icon-trailing-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-standard-chip.mat-mdc-chip-selected.mat-primary, +.mat-mdc-standard-chip.mat-mdc-chip-highlighted.mat-primary { + --mat-chip-disabled-label-text-color: white; + --mat-chip-elevated-container-color: #3f51b5; + --mat-chip-elevated-disabled-container-color: #3f51b5; + --mat-chip-elevated-selected-container-color: #3f51b5; + --mat-chip-flat-disabled-selected-container-color: #3f51b5; + --mat-chip-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-focus-state-layer-opacity: 0.12; + --mat-chip-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-label-text-color: white; + --mat-chip-selected-disabled-trailing-icon-color: white; + --mat-chip-selected-focus-state-layer-color: 0.12; + --mat-chip-selected-focus-state-layer-opacity: 0.12; + --mat-chip-selected-hover-state-layer-color: 0.04; + --mat-chip-selected-label-text-color: white; + --mat-chip-selected-trailing-icon-color: white; + --mat-chip-with-icon-disabled-icon-color: white; + --mat-chip-with-icon-icon-color: white; + --mat-chip-with-icon-selected-icon-color: white; + --mat-chip-with-trailing-icon-disabled-trailing-icon-color: white; + --mat-chip-with-trailing-icon-trailing-icon-color: white; +} +.mat-mdc-standard-chip.mat-mdc-chip-selected.mat-accent, +.mat-mdc-standard-chip.mat-mdc-chip-highlighted.mat-accent { + --mat-chip-disabled-label-text-color: white; + --mat-chip-elevated-container-color: #ff4081; + --mat-chip-elevated-disabled-container-color: #ff4081; + --mat-chip-elevated-selected-container-color: #ff4081; + --mat-chip-flat-disabled-selected-container-color: #ff4081; + --mat-chip-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-focus-state-layer-opacity: 0.12; + --mat-chip-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-label-text-color: white; + --mat-chip-selected-disabled-trailing-icon-color: white; + --mat-chip-selected-focus-state-layer-color: 0.12; + --mat-chip-selected-focus-state-layer-opacity: 0.12; + --mat-chip-selected-hover-state-layer-color: 0.04; + --mat-chip-selected-label-text-color: white; + --mat-chip-selected-trailing-icon-color: white; + --mat-chip-with-icon-disabled-icon-color: white; + --mat-chip-with-icon-icon-color: white; + --mat-chip-with-icon-selected-icon-color: white; + --mat-chip-with-trailing-icon-disabled-trailing-icon-color: white; + --mat-chip-with-trailing-icon-trailing-icon-color: white; +} +.mat-mdc-standard-chip.mat-mdc-chip-selected.mat-warn, +.mat-mdc-standard-chip.mat-mdc-chip-highlighted.mat-warn { + --mat-chip-disabled-label-text-color: white; + --mat-chip-elevated-container-color: #f44336; + --mat-chip-elevated-disabled-container-color: #f44336; + --mat-chip-elevated-selected-container-color: #f44336; + --mat-chip-flat-disabled-selected-container-color: #f44336; + --mat-chip-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-focus-state-layer-opacity: 0.12; + --mat-chip-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-chip-label-text-color: white; + --mat-chip-selected-disabled-trailing-icon-color: white; + --mat-chip-selected-focus-state-layer-color: 0.12; + --mat-chip-selected-focus-state-layer-opacity: 0.12; + --mat-chip-selected-hover-state-layer-color: 0.04; + --mat-chip-selected-label-text-color: white; + --mat-chip-selected-trailing-icon-color: white; + --mat-chip-with-icon-disabled-icon-color: white; + --mat-chip-with-icon-icon-color: white; + --mat-chip-with-icon-selected-icon-color: white; + --mat-chip-with-trailing-icon-disabled-trailing-icon-color: white; + --mat-chip-with-trailing-icon-trailing-icon-color: white; +} +.mat-mdc-chip.mat-mdc-standard-chip { + --mat-chip-container-height: 32px; +} +.mat-mdc-standard-chip { + --mat-chip-label-text-font: Mont, sans-serif; + --mat-chip-label-text-line-height: 20px; + --mat-chip-label-text-size: 14px; + --mat-chip-label-text-tracking: 0.0178571429em; + --mat-chip-label-text-weight: 400; +} +html { + --mat-slide-toggle-disabled-handle-opacity: 0.38; + --mat-slide-toggle-disabled-selected-handle-opacity: 0.38; + --mat-slide-toggle-disabled-selected-icon-opacity: 0.38; + --mat-slide-toggle-disabled-track-opacity: 0.12; + --mat-slide-toggle-disabled-unselected-handle-opacity: 0.38; + --mat-slide-toggle-disabled-unselected-icon-opacity: 0.38; + --mat-slide-toggle-disabled-unselected-track-outline-color: transparent; + --mat-slide-toggle-disabled-unselected-track-outline-width: 1px; + --mat-slide-toggle-handle-height: 20px; + --mat-slide-toggle-handle-shape: 10px; + --mat-slide-toggle-handle-width: 20px; + --mat-slide-toggle-hidden-track-opacity: 1; + --mat-slide-toggle-hidden-track-transition: transform 75ms 0ms cubic-bezier(0.4, 0, 0.6, 1); + --mat-slide-toggle-pressed-handle-size: 20px; + --mat-slide-toggle-selected-focus-state-layer-opacity: 0.12; + --mat-slide-toggle-selected-handle-horizontal-margin: 0; + --mat-slide-toggle-selected-handle-size: 20px; + --mat-slide-toggle-selected-hover-state-layer-opacity: 0.04; + --mat-slide-toggle-selected-icon-size: 18px; + --mat-slide-toggle-selected-pressed-handle-horizontal-margin: 0; + --mat-slide-toggle-selected-pressed-state-layer-opacity: 0.12; + --mat-slide-toggle-selected-track-outline-color: transparent; + --mat-slide-toggle-selected-track-outline-width: 1px; + --mat-slide-toggle-selected-with-icon-handle-horizontal-margin: 0; + --mat-slide-toggle-track-height: 14px; + --mat-slide-toggle-track-outline-color: transparent; + --mat-slide-toggle-track-outline-width: 1px; + --mat-slide-toggle-track-shape: 7px; + --mat-slide-toggle-track-width: 36px; + --mat-slide-toggle-unselected-focus-state-layer-opacity: 0.12; + --mat-slide-toggle-unselected-handle-horizontal-margin: 0; + --mat-slide-toggle-unselected-handle-size: 20px; + --mat-slide-toggle-unselected-hover-state-layer-opacity: 0.12; + --mat-slide-toggle-unselected-icon-size: 18px; + --mat-slide-toggle-unselected-pressed-handle-horizontal-margin: 0; + --mat-slide-toggle-unselected-pressed-state-layer-opacity: 0.1; + --mat-slide-toggle-unselected-with-icon-handle-horizontal-margin: 0; + --mat-slide-toggle-visible-track-opacity: 1; + --mat-slide-toggle-visible-track-transition: transform 75ms 0ms cubic-bezier(0, 0, 0.2, 1); + --mat-slide-toggle-with-icon-handle-size: 20px; + --mat-slide-toggle-touch-target-size: 48px; +} +html { + --mat-slide-toggle-selected-icon-color: white; + --mat-slide-toggle-disabled-selected-icon-color: white; + --mat-slide-toggle-selected-focus-state-layer-color: #3f51b5; + --mat-slide-toggle-selected-handle-color: #3f51b5; + --mat-slide-toggle-selected-hover-state-layer-color: #3f51b5; + --mat-slide-toggle-selected-pressed-state-layer-color: #3f51b5; + --mat-slide-toggle-selected-focus-handle-color: #3f51b5; + --mat-slide-toggle-selected-hover-handle-color: #3f51b5; + --mat-slide-toggle-selected-pressed-handle-color: #3f51b5; + --mat-slide-toggle-selected-focus-track-color: #7986cb; + --mat-slide-toggle-selected-hover-track-color: #7986cb; + --mat-slide-toggle-selected-pressed-track-color: #7986cb; + --mat-slide-toggle-selected-track-color: #7986cb; + --mat-slide-toggle-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-slide-toggle-disabled-handle-elevation-shadow: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + --mat-slide-toggle-disabled-selected-handle-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-disabled-selected-track-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-disabled-unselected-handle-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-disabled-unselected-icon-color: #f6f6f6; + --mat-slide-toggle-disabled-unselected-track-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-handle-elevation-shadow: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + --mat-slide-toggle-handle-surface-color: white; + --mat-slide-toggle-label-text-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-unselected-hover-handle-color: #424242; + --mat-slide-toggle-unselected-focus-handle-color: #424242; + --mat-slide-toggle-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-unselected-focus-track-color: rgba(0, 0, 0, 0.12); + --mat-slide-toggle-unselected-icon-color: #f6f6f6; + --mat-slide-toggle-unselected-handle-color: rgba(0, 0, 0, 0.54); + --mat-slide-toggle-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-unselected-hover-track-color: rgba(0, 0, 0, 0.12); + --mat-slide-toggle-unselected-pressed-handle-color: #424242; + --mat-slide-toggle-unselected-pressed-track-color: rgba(0, 0, 0, 0.12); + --mat-slide-toggle-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-slide-toggle-unselected-track-color: rgba(0, 0, 0, 0.12); +} +.mat-mdc-slide-toggle.mat-accent { + --mat-slide-toggle-selected-icon-color: white; + --mat-slide-toggle-disabled-selected-icon-color: white; + --mat-slide-toggle-selected-focus-state-layer-color: #ff4081; + --mat-slide-toggle-selected-handle-color: #ff4081; + --mat-slide-toggle-selected-hover-state-layer-color: #ff4081; + --mat-slide-toggle-selected-pressed-state-layer-color: #ff4081; + --mat-slide-toggle-selected-focus-handle-color: #ff4081; + --mat-slide-toggle-selected-hover-handle-color: #ff4081; + --mat-slide-toggle-selected-pressed-handle-color: #ff4081; + --mat-slide-toggle-selected-focus-track-color: #f06292; + --mat-slide-toggle-selected-hover-track-color: #f06292; + --mat-slide-toggle-selected-pressed-track-color: #f06292; + --mat-slide-toggle-selected-track-color: #f06292; +} +.mat-mdc-slide-toggle.mat-warn { + --mat-slide-toggle-selected-icon-color: white; + --mat-slide-toggle-disabled-selected-icon-color: white; + --mat-slide-toggle-selected-focus-state-layer-color: #f44336; + --mat-slide-toggle-selected-handle-color: #f44336; + --mat-slide-toggle-selected-hover-state-layer-color: #f44336; + --mat-slide-toggle-selected-pressed-state-layer-color: #f44336; + --mat-slide-toggle-selected-focus-handle-color: #f44336; + --mat-slide-toggle-selected-hover-handle-color: #f44336; + --mat-slide-toggle-selected-pressed-handle-color: #f44336; + --mat-slide-toggle-selected-focus-track-color: #e57373; + --mat-slide-toggle-selected-hover-track-color: #e57373; + --mat-slide-toggle-selected-pressed-track-color: #e57373; + --mat-slide-toggle-selected-track-color: #e57373; +} +html { + --mat-slide-toggle-state-layer-size: 40px; + --mat-slide-toggle-touch-target-display: block; +} +html { + --mat-slide-toggle-label-text-font: Mont, sans-serif; + --mat-slide-toggle-label-text-line-height: 20px; + --mat-slide-toggle-label-text-size: 14px; + --mat-slide-toggle-label-text-tracking: 0.0178571429em; + --mat-slide-toggle-label-text-weight: 400; +} +html .mat-mdc-slide-toggle { + --mat-slide-toggle-label-text-font: Mont, sans-serif; + --mat-slide-toggle-label-text-line-height: 20px; + --mat-slide-toggle-label-text-size: 14px; + --mat-slide-toggle-label-text-tracking: 0.0178571429em; + --mat-slide-toggle-label-text-weight: 400; +} +html { + --mat-radio-disabled-selected-icon-opacity: 0.38; + --mat-radio-disabled-unselected-icon-opacity: 0.38; + --mat-radio-state-layer-size: 40px; + --mat-radio-touch-target-size: 48px; +} +.mat-mdc-radio-button.mat-primary { + --mat-radio-checked-ripple-color: #3f51b5; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #3f51b5; + --mat-radio-selected-hover-icon-color: #3f51b5; + --mat-radio-selected-icon-color: #3f51b5; + --mat-radio-selected-pressed-icon-color: #3f51b5; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-radio-button.mat-accent { + --mat-radio-checked-ripple-color: #ff4081; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #ff4081; + --mat-radio-selected-hover-icon-color: #ff4081; + --mat-radio-selected-icon-color: #ff4081; + --mat-radio-selected-pressed-icon-color: #ff4081; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-radio-button.mat-warn { + --mat-radio-checked-ripple-color: #f44336; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #f44336; + --mat-radio-selected-hover-icon-color: #f44336; + --mat-radio-selected-icon-color: #f44336; + --mat-radio-selected-pressed-icon-color: #f44336; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-radio-state-layer-size: 40px; + --mat-radio-touch-target-display: block; +} +html { + --mat-radio-label-text-font: Mont, sans-serif; + --mat-radio-label-text-line-height: 20px; + --mat-radio-label-text-size: 14px; + --mat-radio-label-text-tracking: 0.0178571429em; + --mat-radio-label-text-weight: 400; +} +html { + --mat-slider-active-track-height: 6px; + --mat-slider-active-track-shape: 9999px; + --mat-slider-handle-elevation: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + --mat-slider-handle-height: 20px; + --mat-slider-handle-shape: 50%; + --mat-slider-handle-width: 20px; + --mat-slider-inactive-track-height: 4px; + --mat-slider-inactive-track-shape: 9999px; + --mat-slider-value-indicator-border-radius: 4px; + --mat-slider-value-indicator-caret-display: block; + --mat-slider-value-indicator-container-transform: translateX(-50%); + --mat-slider-value-indicator-height: 32px; + --mat-slider-value-indicator-padding: 0 12px; + --mat-slider-value-indicator-text-transform: none; + --mat-slider-value-indicator-width: auto; + --mat-slider-with-overlap-handle-outline-width: 1px; + --mat-slider-with-tick-marks-active-container-opacity: 0.6; + --mat-slider-with-tick-marks-container-shape: 50%; + --mat-slider-with-tick-marks-container-size: 2px; + --mat-slider-with-tick-marks-inactive-container-opacity: 0.6; + --mat-slider-value-indicator-transform-origin: bottom; +} +html { + --mat-slider-active-track-color: #3f51b5; + --mat-slider-focus-handle-color: #3f51b5; + --mat-slider-handle-color: #3f51b5; + --mat-slider-hover-handle-color: #3f51b5; + --mat-slider-focus-state-layer-color: color-mix(in srgb, #3f51b5 12%, transparent); + --mat-slider-hover-state-layer-color: color-mix(in srgb, #3f51b5 4%, transparent); + --mat-slider-inactive-track-color: #3f51b5; + --mat-slider-ripple-color: #3f51b5; + --mat-slider-with-tick-marks-active-container-color: white; + --mat-slider-with-tick-marks-inactive-container-color: #3f51b5; + --mat-slider-disabled-active-track-color: rgba(0, 0, 0, 0.87); + --mat-slider-disabled-handle-color: rgba(0, 0, 0, 0.87); + --mat-slider-disabled-inactive-track-color: rgba(0, 0, 0, 0.87); + --mat-slider-label-container-color: #424242; + --mat-slider-label-label-text-color: white; + --mat-slider-value-indicator-opacity: 1; + --mat-slider-with-overlap-handle-outline-color: rgba(0, 0, 0, 0.87); + --mat-slider-with-tick-marks-disabled-container-color: rgba(0, 0, 0, 0.87); +} +.mat-accent { + --mat-slider-active-track-color: #ff4081; + --mat-slider-focus-handle-color: #ff4081; + --mat-slider-handle-color: #ff4081; + --mat-slider-hover-handle-color: #ff4081; + --mat-slider-focus-state-layer-color: color-mix(in srgb, #ff4081 12%, transparent); + --mat-slider-hover-state-layer-color: color-mix(in srgb, #ff4081 4%, transparent); + --mat-slider-inactive-track-color: #ff4081; + --mat-slider-ripple-color: #ff4081; + --mat-slider-with-tick-marks-active-container-color: white; + --mat-slider-with-tick-marks-inactive-container-color: #ff4081; +} +.mat-warn { + --mat-slider-active-track-color: #f44336; + --mat-slider-focus-handle-color: #f44336; + --mat-slider-handle-color: #f44336; + --mat-slider-hover-handle-color: #f44336; + --mat-slider-focus-state-layer-color: color-mix(in srgb, #f44336 12%, transparent); + --mat-slider-hover-state-layer-color: color-mix(in srgb, #f44336 4%, transparent); + --mat-slider-inactive-track-color: #f44336; + --mat-slider-ripple-color: #f44336; + --mat-slider-with-tick-marks-active-container-color: white; + --mat-slider-with-tick-marks-inactive-container-color: #f44336; +} +html { + --mat-slider-label-label-text-font: Mont, sans-serif; + --mat-slider-label-label-text-size: 14px; + --mat-slider-label-label-text-line-height: 22px; + --mat-slider-label-label-text-tracking: 0.0071428571em; + --mat-slider-label-label-text-weight: 500; +} +html { + --mat-menu-container-shape: 4px; + --mat-menu-divider-bottom-spacing: 0; + --mat-menu-divider-top-spacing: 0; + --mat-menu-item-spacing: 16px; + --mat-menu-item-icon-size: 24px; + --mat-menu-item-leading-spacing: 16px; + --mat-menu-item-trailing-spacing: 16px; + --mat-menu-item-with-icon-leading-spacing: 16px; + --mat-menu-item-with-icon-trailing-spacing: 16px; + --mat-menu-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} +html { + --mat-menu-item-label-text-color: rgba(0, 0, 0, 0.87); + --mat-menu-item-icon-color: rgba(0, 0, 0, 0.87); + --mat-menu-item-hover-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 4%, transparent); + --mat-menu-item-focus-state-layer-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-menu-container-color: white; + --mat-menu-divider-color: rgba(0, 0, 0, 0.12); +} +html { + --mat-menu-item-label-text-font: Mont, sans-serif; + --mat-menu-item-label-text-size: 16px; + --mat-menu-item-label-text-tracking: 0.03125em; + --mat-menu-item-label-text-line-height: 24px; + --mat-menu-item-label-text-weight: 400; +} +html { + --mat-list-active-indicator-color: transparent; + --mat-list-active-indicator-shape: 4px; + --mat-list-list-item-container-shape: 0; + --mat-list-list-item-leading-avatar-shape: 50%; + --mat-list-list-item-container-color: transparent; + --mat-list-list-item-selected-container-color: transparent; + --mat-list-list-item-leading-avatar-color: transparent; + --mat-list-list-item-leading-icon-size: 24px; + --mat-list-list-item-leading-avatar-size: 40px; + --mat-list-list-item-trailing-icon-size: 24px; + --mat-list-list-item-disabled-state-layer-color: transparent; + --mat-list-list-item-disabled-state-layer-opacity: 0; + --mat-list-list-item-disabled-label-text-opacity: 0.38; + --mat-list-list-item-disabled-leading-icon-opacity: 0.38; + --mat-list-list-item-disabled-trailing-icon-opacity: 0.38; +} +html { + --mat-list-list-item-label-text-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-supporting-text-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-leading-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-trailing-supporting-text-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-trailing-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-selected-trailing-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-disabled-label-text-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-disabled-leading-icon-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-disabled-trailing-icon-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-hover-label-text-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-hover-leading-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-hover-state-layer-opacity: 0.04; + --mat-list-list-item-hover-trailing-icon-color: rgba(0, 0, 0, 0.54); + --mat-list-list-item-focus-label-text-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-list-list-item-focus-state-layer-opacity: 0.12; +} +.mdc-list-item__start, +.mdc-list-item__end { + --mat-radio-checked-ripple-color: #3f51b5; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #3f51b5; + --mat-radio-selected-hover-icon-color: #3f51b5; + --mat-radio-selected-icon-color: #3f51b5; + --mat-radio-selected-pressed-icon-color: #3f51b5; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-accent .mdc-list-item__start, +.mat-accent .mdc-list-item__end { + --mat-radio-checked-ripple-color: #ff4081; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #ff4081; + --mat-radio-selected-hover-icon-color: #ff4081; + --mat-radio-selected-icon-color: #ff4081; + --mat-radio-selected-pressed-icon-color: #ff4081; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-warn .mdc-list-item__start, +.mat-warn .mdc-list-item__end { + --mat-radio-checked-ripple-color: #f44336; + --mat-radio-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-radio-disabled-selected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-disabled-unselected-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-label-text-color: rgba(0, 0, 0, 0.87); + --mat-radio-ripple-color: rgba(0, 0, 0, 0.87); + --mat-radio-selected-focus-icon-color: #f44336; + --mat-radio-selected-hover-icon-color: #f44336; + --mat-radio-selected-icon-color: #f44336; + --mat-radio-selected-pressed-icon-color: #f44336; + --mat-radio-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-list-option { + --mat-checkbox-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-checkbox-label-text-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #3f51b5; + --mat-checkbox-selected-hover-icon-color: #3f51b5; + --mat-checkbox-selected-icon-color: #3f51b5; + --mat-checkbox-selected-pressed-icon-color: #3f51b5; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #3f51b5; + --mat-checkbox-selected-hover-state-layer-color: #3f51b5; + --mat-checkbox-selected-pressed-state-layer-color: #3f51b5; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-list-option.mat-accent { + --mat-checkbox-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-checkbox-label-text-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #ff4081; + --mat-checkbox-selected-hover-icon-color: #ff4081; + --mat-checkbox-selected-icon-color: #ff4081; + --mat-checkbox-selected-pressed-icon-color: #ff4081; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #ff4081; + --mat-checkbox-selected-hover-state-layer-color: #ff4081; + --mat-checkbox-selected-pressed-state-layer-color: #ff4081; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-list-option.mat-warn { + --mat-checkbox-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-checkbox-label-text-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #f44336; + --mat-checkbox-selected-hover-icon-color: #f44336; + --mat-checkbox-selected-icon-color: #f44336; + --mat-checkbox-selected-pressed-icon-color: #f44336; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #f44336; + --mat-checkbox-selected-hover-state-layer-color: #f44336; + --mat-checkbox-selected-pressed-state-layer-color: #f44336; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-list-base.mat-mdc-list-base .mdc-list-item--selected .mdc-list-item__primary-text, +.mat-mdc-list-base.mat-mdc-list-base .mdc-list-item--selected .mdc-list-item__start, +.mat-mdc-list-base.mat-mdc-list-base .mdc-list-item--activated .mdc-list-item__primary-text, +.mat-mdc-list-base.mat-mdc-list-base .mdc-list-item--activated .mdc-list-item__start { + color: #3f51b5; +} +.mat-mdc-list-base .mdc-list-item--disabled .mdc-list-item__start, +.mat-mdc-list-base .mdc-list-item--disabled .mdc-list-item__content, +.mat-mdc-list-base .mdc-list-item--disabled .mdc-list-item__end { + opacity: 1; +} +html { + --mat-list-list-item-leading-icon-start-space: 16px; + --mat-list-list-item-leading-icon-end-space: 32px; + --mat-list-list-item-one-line-container-height: 48px; + --mat-list-list-item-two-line-container-height: 64px; + --mat-list-list-item-three-line-container-height: 88px; +} +.mdc-list-item__start, +.mdc-list-item__end { + --mat-radio-state-layer-size: 40px; + --mat-radio-touch-target-display: block; +} +.mat-mdc-list-item.mdc-list-item--with-leading-avatar.mdc-list-item--with-one-line, +.mat-mdc-list-item.mdc-list-item--with-leading-checkbox.mdc-list-item--with-one-line, +.mat-mdc-list-item.mdc-list-item--with-leading-icon.mdc-list-item--with-one-line { + height: 56px; +} +.mat-mdc-list-item.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines, +.mat-mdc-list-item.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines, +.mat-mdc-list-item.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines { + height: 72px; +} +html { + --mat-list-list-item-label-text-font: Mont, sans-serif; + --mat-list-list-item-label-text-line-height: 24px; + --mat-list-list-item-label-text-size: 16px; + --mat-list-list-item-label-text-tracking: 0.03125em; + --mat-list-list-item-label-text-weight: 400; + --mat-list-list-item-supporting-text-font: Mont, sans-serif; + --mat-list-list-item-supporting-text-line-height: 20px; + --mat-list-list-item-supporting-text-size: 14px; + --mat-list-list-item-supporting-text-tracking: 0.0178571429em; + --mat-list-list-item-supporting-text-weight: 400; + --mat-list-list-item-trailing-supporting-text-font: Mont, sans-serif; + --mat-list-list-item-trailing-supporting-text-line-height: 20px; + --mat-list-list-item-trailing-supporting-text-size: 12px; + --mat-list-list-item-trailing-supporting-text-tracking: 0.0333333333em; + --mat-list-list-item-trailing-supporting-text-weight: 400; +} +.mdc-list-group__subheader { + font: + 400 16px / 28px Mont, + sans-serif; + letter-spacing: 0.009375em; +} +html { + --mat-paginator-page-size-select-width: 84px; + --mat-paginator-page-size-select-touch-target-height: 48px; +} +html { + --mat-paginator-container-text-color: rgba(0, 0, 0, 0.87); + --mat-paginator-container-background-color: white; + --mat-paginator-enabled-icon-color: rgba(0, 0, 0, 0.54); + --mat-paginator-disabled-icon-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); +} +html { + --mat-paginator-container-size: 56px; + --mat-paginator-form-field-container-height: 40px; + --mat-paginator-form-field-container-vertical-padding: 8px; + --mat-paginator-touch-target-display: block; +} +html { + --mat-paginator-container-text-font: Mont, sans-serif; + --mat-paginator-container-text-line-height: 20px; + --mat-paginator-container-text-size: 12px; + --mat-paginator-container-text-tracking: 0.0333333333em; + --mat-paginator-container-text-weight: 400; + --mat-paginator-select-trigger-text-size: 12px; +} +html { + --mat-tab-container-height: 48px; + --mat-tab-divider-color: transparent; + --mat-tab-divider-height: 0; + --mat-tab-active-indicator-height: 2px; + --mat-tab-active-indicator-shape: 0; +} +.mat-mdc-tab-group, +.mat-mdc-tab-nav-bar { + --mat-tab-disabled-ripple-color: rgba(0, 0, 0, 0.54); + --mat-tab-pagination-icon-color: rgba(0, 0, 0, 0.87); + --mat-tab-inactive-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-label-text-color: #3f51b5; + --mat-tab-active-ripple-color: #3f51b5; + --mat-tab-inactive-ripple-color: #3f51b5; + --mat-tab-inactive-focus-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-inactive-hover-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-focus-label-text-color: #3f51b5; + --mat-tab-active-hover-label-text-color: #3f51b5; + --mat-tab-active-focus-indicator-color: #3f51b5; + --mat-tab-active-hover-indicator-color: #3f51b5; + --mat-tab-active-indicator-color: #3f51b5; +} +.mat-mdc-tab-group.mat-accent, +.mat-mdc-tab-nav-bar.mat-accent { + --mat-tab-disabled-ripple-color: rgba(0, 0, 0, 0.54); + --mat-tab-pagination-icon-color: rgba(0, 0, 0, 0.87); + --mat-tab-inactive-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-label-text-color: #ff4081; + --mat-tab-active-ripple-color: #ff4081; + --mat-tab-inactive-ripple-color: #ff4081; + --mat-tab-inactive-focus-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-inactive-hover-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-focus-label-text-color: #ff4081; + --mat-tab-active-hover-label-text-color: #ff4081; + --mat-tab-active-focus-indicator-color: #ff4081; + --mat-tab-active-hover-indicator-color: #ff4081; + --mat-tab-active-indicator-color: #ff4081; +} +.mat-mdc-tab-group.mat-warn, +.mat-mdc-tab-nav-bar.mat-warn { + --mat-tab-disabled-ripple-color: rgba(0, 0, 0, 0.54); + --mat-tab-pagination-icon-color: rgba(0, 0, 0, 0.87); + --mat-tab-inactive-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-label-text-color: #f44336; + --mat-tab-active-ripple-color: #f44336; + --mat-tab-inactive-ripple-color: #f44336; + --mat-tab-inactive-focus-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-inactive-hover-label-text-color: rgba(0, 0, 0, 0.54); + --mat-tab-active-focus-label-text-color: #f44336; + --mat-tab-active-hover-label-text-color: #f44336; + --mat-tab-active-focus-indicator-color: #f44336; + --mat-tab-active-hover-indicator-color: #f44336; + --mat-tab-active-indicator-color: #f44336; +} +.mat-mdc-tab-group.mat-background-primary, +.mat-mdc-tab-nav-bar.mat-background-primary { + --mat-tab-background-color: #3f51b5; + --mat-tab-foreground-color: white; +} +.mat-mdc-tab-group.mat-background-accent, +.mat-mdc-tab-nav-bar.mat-background-accent { + --mat-tab-background-color: #ff4081; + --mat-tab-foreground-color: white; +} +.mat-mdc-tab-group.mat-background-warn, +.mat-mdc-tab-nav-bar.mat-background-warn { + --mat-tab-background-color: #f44336; + --mat-tab-foreground-color: white; +} +.mat-mdc-tab-header { + --mat-tab-container-height: 48px; +} +.mat-mdc-tab-header { + --mat-tab-label-text-font: Mont, sans-serif; + --mat-tab-label-text-size: 14px; + --mat-tab-label-text-tracking: 0.0892857143em; + --mat-tab-label-text-line-height: 36px; + --mat-tab-label-text-weight: 500; +} +html { + --mat-checkbox-disabled-selected-checkmark-color: white; + --mat-checkbox-selected-focus-state-layer-opacity: 0.12; + --mat-checkbox-selected-hover-state-layer-opacity: 0.04; + --mat-checkbox-selected-pressed-state-layer-opacity: 0.12; + --mat-checkbox-unselected-focus-state-layer-opacity: 0.12; + --mat-checkbox-unselected-hover-state-layer-opacity: 0.04; + --mat-checkbox-unselected-pressed-state-layer-opacity: 0.12; + --mat-checkbox-touch-target-size: 48px; +} +html { + --mat-checkbox-disabled-label-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-checkbox-label-text-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #ff4081; + --mat-checkbox-selected-hover-icon-color: #ff4081; + --mat-checkbox-selected-icon-color: #ff4081; + --mat-checkbox-selected-pressed-icon-color: #ff4081; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #ff4081; + --mat-checkbox-selected-hover-state-layer-color: #ff4081; + --mat-checkbox-selected-pressed-state-layer-color: #ff4081; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-checkbox.mat-primary { + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #3f51b5; + --mat-checkbox-selected-hover-icon-color: #3f51b5; + --mat-checkbox-selected-icon-color: #3f51b5; + --mat-checkbox-selected-pressed-icon-color: #3f51b5; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #3f51b5; + --mat-checkbox-selected-hover-state-layer-color: #3f51b5; + --mat-checkbox-selected-pressed-state-layer-color: #3f51b5; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-checkbox.mat-warn { + --mat-checkbox-disabled-selected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-disabled-unselected-icon-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-checkbox-selected-checkmark-color: white; + --mat-checkbox-selected-focus-icon-color: #f44336; + --mat-checkbox-selected-hover-icon-color: #f44336; + --mat-checkbox-selected-icon-color: #f44336; + --mat-checkbox-selected-pressed-icon-color: #f44336; + --mat-checkbox-unselected-focus-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-icon-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mat-checkbox-selected-focus-state-layer-color: #f44336; + --mat-checkbox-selected-hover-state-layer-color: #f44336; + --mat-checkbox-selected-pressed-state-layer-color: #f44336; + --mat-checkbox-unselected-focus-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-hover-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-checkbox-unselected-pressed-state-layer-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-checkbox-touch-target-display: block; + --mat-checkbox-state-layer-size: 40px; +} +html { + --mat-checkbox-label-text-font: Mont, sans-serif; + --mat-checkbox-label-text-line-height: 20px; + --mat-checkbox-label-text-size: 14px; + --mat-checkbox-label-text-tracking: 0.0178571429em; + --mat-checkbox-label-text-weight: 400; +} +html { + --mat-button-filled-container-shape: 4px; + --mat-button-filled-horizontal-padding: 16px; + --mat-button-filled-icon-offset: -4px; + --mat-button-filled-icon-spacing: 8px; + --mat-button-filled-touch-target-size: 48px; + --mat-button-outlined-container-shape: 4px; + --mat-button-outlined-horizontal-padding: 15px; + --mat-button-outlined-icon-offset: -4px; + --mat-button-outlined-icon-spacing: 8px; + --mat-button-outlined-keep-touch-target: false; + --mat-button-outlined-outline-width: 1px; + --mat-button-outlined-touch-target-size: 48px; + --mat-button-protected-container-elevation-shadow: + 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + --mat-button-protected-container-shape: 4px; + --mat-button-protected-disabled-container-elevation-shadow: + 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), + 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + --mat-button-protected-focus-container-elevation-shadow: + 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), + 0px 1px 10px 0px rgba(0, 0, 0, 0.12); + --mat-button-protected-horizontal-padding: 16px; + --mat-button-protected-hover-container-elevation-shadow: + 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), + 0px 1px 10px 0px rgba(0, 0, 0, 0.12); + --mat-button-protected-icon-offset: -4px; + --mat-button-protected-icon-spacing: 8px; + --mat-button-protected-pressed-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-button-protected-touch-target-size: 48px; + --mat-button-text-container-shape: 4px; + --mat-button-text-horizontal-padding: 8px; + --mat-button-text-icon-offset: 0; + --mat-button-text-icon-spacing: 8px; + --mat-button-text-with-icon-horizontal-padding: 8px; + --mat-button-text-touch-target-size: 48px; + --mat-button-tonal-container-shape: 4px; + --mat-button-tonal-horizontal-padding: 16px; + --mat-button-tonal-icon-offset: -4px; + --mat-button-tonal-icon-spacing: 8px; + --mat-button-tonal-touch-target-size: 48px; +} +html { + --mat-button-filled-container-color: white; + --mat-button-filled-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-filled-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-filled-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-filled-focus-state-layer-opacity: 0.12; + --mat-button-filled-hover-state-layer-opacity: 0.04; + --mat-button-filled-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-filled-pressed-state-layer-opacity: 0.12; + --mat-button-filled-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-filled-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-outlined-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-outlined-disabled-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-outlined-focus-state-layer-opacity: 0.12; + --mat-button-outlined-hover-state-layer-opacity: 0.04; + --mat-button-outlined-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-pressed-state-layer-opacity: 0.12; + --mat-button-outlined-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-outlined-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-protected-container-color: white; + --mat-button-protected-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-protected-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-protected-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-protected-focus-state-layer-opacity: 0.12; + --mat-button-protected-hover-state-layer-opacity: 0.04; + --mat-button-protected-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-protected-pressed-state-layer-opacity: 0.12; + --mat-button-protected-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-protected-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-text-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-text-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-text-focus-state-layer-opacity: 0.12; + --mat-button-text-hover-state-layer-opacity: 0.04; + --mat-button-text-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-text-pressed-state-layer-opacity: 0.12; + --mat-button-text-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-text-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-tonal-container-color: white; + --mat-button-tonal-disabled-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-tonal-disabled-label-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-tonal-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-button-tonal-focus-state-layer-opacity: 0.12; + --mat-button-tonal-hover-state-layer-opacity: 0.04; + --mat-button-tonal-label-text-color: rgba(0, 0, 0, 0.87); + --mat-button-tonal-pressed-state-layer-opacity: 0.12; + --mat-button-tonal-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-button-tonal-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-button.mat-primary, +.mat-mdc-unelevated-button.mat-primary, +.mat-mdc-raised-button.mat-primary, +.mat-mdc-outlined-button.mat-primary, +.mat-tonal-button.mat-primary { + --mat-button-filled-container-color: #3f51b5; + --mat-button-filled-label-text-color: white; + --mat-button-filled-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-filled-state-layer-color: white; + --mat-button-outlined-label-text-color: #3f51b5; + --mat-button-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-ripple-color: color-mix(in srgb, #3f51b5 12%, transparent); + --mat-button-outlined-state-layer-color: #3f51b5; + --mat-button-protected-container-color: #3f51b5; + --mat-button-protected-label-text-color: white; + --mat-button-protected-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-protected-state-layer-color: white; + --mat-button-text-label-text-color: #3f51b5; + --mat-button-text-ripple-color: color-mix(in srgb, #3f51b5 12%, transparent); + --mat-button-text-state-layer-color: #3f51b5; + --mat-button-tonal-container-color: #3f51b5; + --mat-button-tonal-label-text-color: white; + --mat-button-tonal-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-tonal-state-layer-color: white; +} +.mat-mdc-button.mat-accent, +.mat-mdc-unelevated-button.mat-accent, +.mat-mdc-raised-button.mat-accent, +.mat-mdc-outlined-button.mat-accent, +.mat-tonal-button.mat-accent { + --mat-button-filled-container-color: #ff4081; + --mat-button-filled-label-text-color: white; + --mat-button-filled-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-filled-state-layer-color: white; + --mat-button-outlined-label-text-color: #ff4081; + --mat-button-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-ripple-color: color-mix(in srgb, #ff4081 12%, transparent); + --mat-button-outlined-state-layer-color: #ff4081; + --mat-button-protected-container-color: #ff4081; + --mat-button-protected-label-text-color: white; + --mat-button-protected-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-protected-state-layer-color: white; + --mat-button-text-label-text-color: #ff4081; + --mat-button-text-ripple-color: color-mix(in srgb, #ff4081 12%, transparent); + --mat-button-text-state-layer-color: #ff4081; + --mat-button-tonal-container-color: #ff4081; + --mat-button-tonal-label-text-color: white; + --mat-button-tonal-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-tonal-state-layer-color: white; +} +.mat-mdc-button.mat-warn, +.mat-mdc-unelevated-button.mat-warn, +.mat-mdc-raised-button.mat-warn, +.mat-mdc-outlined-button.mat-warn, +.mat-tonal-button.mat-warn { + --mat-button-filled-container-color: #f44336; + --mat-button-filled-label-text-color: white; + --mat-button-filled-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-filled-state-layer-color: white; + --mat-button-outlined-label-text-color: #f44336; + --mat-button-outlined-outline-color: rgba(0, 0, 0, 0.12); + --mat-button-outlined-ripple-color: color-mix(in srgb, #f44336 12%, transparent); + --mat-button-outlined-state-layer-color: #f44336; + --mat-button-protected-container-color: #f44336; + --mat-button-protected-label-text-color: white; + --mat-button-protected-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-protected-state-layer-color: white; + --mat-button-text-label-text-color: #f44336; + --mat-button-text-ripple-color: color-mix(in srgb, #f44336 12%, transparent); + --mat-button-text-state-layer-color: #f44336; + --mat-button-tonal-container-color: #f44336; + --mat-button-tonal-label-text-color: white; + --mat-button-tonal-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-button-tonal-state-layer-color: white; +} +html { + --mat-button-filled-container-height: 36px; + --mat-button-filled-touch-target-display: block; + --mat-button-outlined-container-height: 36px; + --mat-button-outlined-touch-target-display: block; + --mat-button-protected-container-height: 36px; + --mat-button-protected-touch-target-display: block; + --mat-button-text-container-height: 36px; + --mat-button-text-touch-target-display: block; + --mat-button-tonal-container-height: 36px; + --mat-button-tonal-touch-target-display: block; +} +html { + --mat-button-filled-label-text-font: Mont, sans-serif; + --mat-button-filled-label-text-size: 14px; + --mat-button-filled-label-text-tracking: 0.0892857143em; + --mat-button-filled-label-text-transform: none; + --mat-button-filled-label-text-weight: 500; + --mat-button-outlined-label-text-font: Mont, sans-serif; + --mat-button-outlined-label-text-size: 14px; + --mat-button-outlined-label-text-tracking: 0.0892857143em; + --mat-button-outlined-label-text-transform: none; + --mat-button-outlined-label-text-weight: 500; + --mat-button-protected-label-text-font: Mont, sans-serif; + --mat-button-protected-label-text-size: 14px; + --mat-button-protected-label-text-tracking: 0.0892857143em; + --mat-button-protected-label-text-transform: none; + --mat-button-protected-label-text-weight: 500; + --mat-button-text-label-text-font: Mont, sans-serif; + --mat-button-text-label-text-size: 14px; + --mat-button-text-label-text-tracking: 0.0892857143em; + --mat-button-text-label-text-transform: none; + --mat-button-text-label-text-weight: 500; + --mat-button-tonal-label-text-font: Mont, sans-serif; + --mat-button-tonal-label-text-size: 14px; + --mat-button-tonal-label-text-tracking: 0.0892857143em; + --mat-button-tonal-label-text-transform: none; + --mat-button-tonal-label-text-weight: 500; +} +html { + --mat-icon-button-icon-size: 24px; + --mat-icon-button-container-shape: 50%; + --mat-icon-button-touch-target-size: 48px; +} +html { + --mat-icon-button-disabled-icon-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); + --mat-icon-button-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-icon-button-focus-state-layer-opacity: 0.12; + --mat-icon-button-hover-state-layer-opacity: 0.04; + --mat-icon-button-icon-color: inherit; + --mat-icon-button-pressed-state-layer-opacity: 0.12; + --mat-icon-button-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-icon-button-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-icon-button.mat-primary { + --mat-icon-button-icon-color: #3f51b5; + --mat-icon-button-state-layer-color: #3f51b5; + --mat-icon-button-ripple-color: color-mix(in srgb, #3f51b5 12%, transparent); +} +.mat-mdc-icon-button.mat-accent { + --mat-icon-button-icon-color: #ff4081; + --mat-icon-button-state-layer-color: #ff4081; + --mat-icon-button-ripple-color: color-mix(in srgb, #ff4081 12%, transparent); +} +.mat-mdc-icon-button.mat-warn { + --mat-icon-button-icon-color: #f44336; + --mat-icon-button-state-layer-color: #f44336; + --mat-icon-button-ripple-color: color-mix(in srgb, #f44336 12%, transparent); +} +html { + --mat-icon-button-touch-target-display: block; +} +.mat-mdc-icon-button.mat-mdc-button-base { + --mdc-icon-button-state-layer-size: 48px; + --mat-icon-button-state-layer-size: 48px; + width: var(--mat-icon-button-state-layer-size); + height: var(--mat-icon-button-state-layer-size); + padding: 12px; +} +html { + --mat-fab-container-elevation-shadow: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), + 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + --mat-fab-container-shape: 50%; + --mat-fab-touch-target-size: 48px; + --mat-fab-extended-container-elevation-shadow: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), + 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + --mat-fab-extended-container-height: 48px; + --mat-fab-extended-container-shape: 24px; + --mat-fab-extended-focus-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-extended-hover-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-extended-pressed-container-elevation-shadow: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), + 0px 5px 22px 4px rgba(0, 0, 0, 0.12); + --mat-fab-focus-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-hover-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-pressed-container-elevation-shadow: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), + 0px 5px 22px 4px rgba(0, 0, 0, 0.12); + --mat-fab-small-container-elevation-shadow: + 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), + 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + --mat-fab-small-container-shape: 50%; + --mat-fab-small-touch-target-size: 48px; + --mat-fab-small-focus-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-small-hover-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + --mat-fab-small-pressed-container-elevation-shadow: + 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), + 0px 5px 22px 4px rgba(0, 0, 0, 0.12); +} +html { + --mat-fab-container-color: white; + --mat-fab-disabled-state-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-fab-disabled-state-foreground-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-fab-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-fab-focus-state-layer-opacity: 0.12; + --mat-fab-foreground-color: rgba(0, 0, 0, 0.87); + --mat-fab-hover-state-layer-opacity: 0.04; + --mat-fab-pressed-state-layer-opacity: 0.12; + --mat-fab-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-fab-small-container-color: white; + --mat-fab-small-disabled-state-container-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-fab-small-disabled-state-foreground-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-fab-small-disabled-state-layer-color: rgba(0, 0, 0, 0.54); + --mat-fab-small-focus-state-layer-opacity: 0.12; + --mat-fab-small-foreground-color: rgba(0, 0, 0, 0.87); + --mat-fab-small-hover-state-layer-opacity: 0.04; + --mat-fab-small-pressed-state-layer-opacity: 0.12; + --mat-fab-small-ripple-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 12%, transparent); + --mat-fab-small-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-fab-state-layer-color: rgba(0, 0, 0, 0.87); +} +.mat-mdc-fab.mat-primary, +.mat-mdc-mini-fab.mat-primary { + --mat-fab-container-color: #3f51b5; + --mat-fab-foreground-color: white; + --mat-fab-ripple-color: color-mix(in srgb, #3f51b5 12%, transparent); + --mat-fab-small-container-color: #3f51b5; + --mat-fab-small-foreground-color: white; + --mat-fab-small-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-fab-small-state-layer-color: white; + --mat-fab-state-layer-color: white; +} +.mat-mdc-fab.mat-accent, +.mat-mdc-mini-fab.mat-accent { + --mat-fab-container-color: #ff4081; + --mat-fab-foreground-color: white; + --mat-fab-ripple-color: color-mix(in srgb, #ff4081 12%, transparent); + --mat-fab-small-container-color: #ff4081; + --mat-fab-small-foreground-color: white; + --mat-fab-small-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-fab-small-state-layer-color: white; + --mat-fab-state-layer-color: white; +} +.mat-mdc-fab.mat-warn, +.mat-mdc-mini-fab.mat-warn { + --mat-fab-container-color: #f44336; + --mat-fab-foreground-color: white; + --mat-fab-ripple-color: color-mix(in srgb, #f44336 12%, transparent); + --mat-fab-small-container-color: #f44336; + --mat-fab-small-foreground-color: white; + --mat-fab-small-ripple-color: color-mix(in srgb, white 12%, transparent); + --mat-fab-small-state-layer-color: white; + --mat-fab-state-layer-color: white; +} +html { + --mat-fab-small-touch-target-display: block; + --mat-fab-touch-target-display: block; +} +html { + --mat-fab-extended-label-text-font: Mont, sans-serif; + --mat-fab-extended-label-text-size: 14px; + --mat-fab-extended-label-text-tracking: 0.0892857143em; + --mat-fab-extended-label-text-weight: 500; +} +html { + --mat-snack-bar-container-shape: 4px; +} +html { + --mat-snack-bar-container-color: #424242; + --mat-snack-bar-supporting-text-color: white; + --mat-snack-bar-button-color: #7986cb; +} +html { + --mat-snack-bar-supporting-text-font: Mont, sans-serif; + --mat-snack-bar-supporting-text-line-height: 20px; + --mat-snack-bar-supporting-text-size: 14px; + --mat-snack-bar-supporting-text-weight: 400; +} +html { + --mat-table-row-item-outline-width: 1px; +} +html { + --mat-table-background-color: white; + --mat-table-header-headline-color: rgba(0, 0, 0, 0.87); + --mat-table-row-item-label-text-color: rgba(0, 0, 0, 0.87); + --mat-table-row-item-outline-color: rgba(0, 0, 0, 0.12); +} +html { + --mat-table-header-container-height: 56px; + --mat-table-footer-container-height: 52px; + --mat-table-row-item-container-height: 52px; +} +html { + --mat-table-header-headline-font: Mont, sans-serif; + --mat-table-header-headline-line-height: 22px; + --mat-table-header-headline-size: 14px; + --mat-table-header-headline-weight: 500; + --mat-table-header-headline-tracking: 0.0071428571em; + --mat-table-row-item-label-text-font: Mont, sans-serif; + --mat-table-row-item-label-text-line-height: 20px; + --mat-table-row-item-label-text-size: 14px; + --mat-table-row-item-label-text-weight: 400; + --mat-table-row-item-label-text-tracking: 0.0178571429em; + --mat-table-footer-supporting-text-font: Mont, sans-serif; + --mat-table-footer-supporting-text-line-height: 20px; + --mat-table-footer-supporting-text-size: 14px; + --mat-table-footer-supporting-text-weight: 400; + --mat-table-footer-supporting-text-tracking: 0.0178571429em; +} +html { + --mat-progress-spinner-active-indicator-width: 4px; + --mat-progress-spinner-size: 48px; +} +html { + --mat-progress-spinner-active-indicator-color: #3f51b5; +} +.mat-accent { + --mat-progress-spinner-active-indicator-color: #ff4081; +} +.mat-warn { + --mat-progress-spinner-active-indicator-color: #f44336; +} +html { + --mat-badge-container-shape: 50%; + --mat-badge-container-size: unset; + --mat-badge-small-size-container-size: unset; + --mat-badge-large-size-container-size: unset; + --mat-badge-legacy-container-size: 22px; + --mat-badge-legacy-small-size-container-size: 16px; + --mat-badge-legacy-large-size-container-size: 28px; + --mat-badge-container-offset: -11px 0; + --mat-badge-small-size-container-offset: -8px 0; + --mat-badge-large-size-container-offset: -14px 0; + --mat-badge-container-overlap-offset: -11px; + --mat-badge-small-size-container-overlap-offset: -8px; + --mat-badge-large-size-container-overlap-offset: -14px; + --mat-badge-container-padding: 0; + --mat-badge-small-size-container-padding: 0; + --mat-badge-large-size-container-padding: 0; +} +html { + --mat-badge-background-color: #3f51b5; + --mat-badge-text-color: white; + --mat-badge-disabled-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-badge-disabled-state-text-color: color-mix(in srgb, rgba(0, 0, 0, 0.87) 38%, transparent); +} +.mat-badge-accent { + --mat-badge-background-color: #ff4081; + --mat-badge-text-color: white; +} +.mat-badge-warn { + --mat-badge-background-color: #f44336; + --mat-badge-text-color: white; +} +html { + --mat-badge-text-font: Mont, sans-serif; + --mat-badge-line-height: 22px; + --mat-badge-text-size: 12px; + --mat-badge-text-weight: 600; + --mat-badge-small-size-text-size: 9px; + --mat-badge-small-size-line-height: 16px; + --mat-badge-large-size-text-size: 24px; + --mat-badge-large-size-line-height: 28px; +} +html { + --mat-bottom-sheet-container-shape: 4px; +} +html { + --mat-bottom-sheet-container-text-color: rgba(0, 0, 0, 0.87); + --mat-bottom-sheet-container-background-color: white; +} +html { + --mat-bottom-sheet-container-text-font: Mont, sans-serif; + --mat-bottom-sheet-container-text-line-height: 20px; + --mat-bottom-sheet-container-text-size: 14px; + --mat-bottom-sheet-container-text-tracking: 0.0178571429em; + --mat-bottom-sheet-container-text-weight: 400; +} +html { + --mat-button-toggle-focus-state-layer-opacity: 0.12; + --mat-button-toggle-hover-state-layer-opacity: 0.04; + --mat-button-toggle-legacy-focus-state-layer-opacity: 1; + --mat-button-toggle-legacy-height: 36px; + --mat-button-toggle-legacy-shape: 2px; + --mat-button-toggle-shape: 4px; +} +html { + --mat-button-toggle-background-color: white; + --mat-button-toggle-disabled-selected-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-disabled-selected-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-toggle-disabled-state-background-color: white; + --mat-button-toggle-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-toggle-divider-color: rgba(0, 0, 0, 0.12); + --mat-button-toggle-legacy-disabled-selected-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-legacy-disabled-state-background-color: white; + --mat-button-toggle-legacy-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-button-toggle-legacy-selected-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-legacy-selected-state-text-color: rgba(0, 0, 0, 0.87); + --mat-button-toggle-legacy-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-legacy-text-color: rgba(0, 0, 0, 0.87); + --mat-button-toggle-selected-state-background-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-button-toggle-selected-state-text-color: rgba(0, 0, 0, 0.87); + --mat-button-toggle-state-layer-color: rgba(0, 0, 0, 0.87); + --mat-button-toggle-text-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-button-toggle-height: 48px; +} +html { + --mat-button-toggle-label-text-font: Mont, sans-serif; + --mat-button-toggle-label-text-line-height: 24px; + --mat-button-toggle-label-text-size: 16px; + --mat-button-toggle-label-text-tracking: 0.03125em; + --mat-button-toggle-label-text-weight: 400; + --mat-button-toggle-legacy-label-text-font: Mont, sans-serif; + --mat-button-toggle-legacy-label-text-line-height: 24px; + --mat-button-toggle-legacy-label-text-size: 16px; + --mat-button-toggle-legacy-label-text-tracking: 0.03125em; + --mat-button-toggle-legacy-label-text-weight: 400; +} +html { + --mat-datepicker-calendar-container-shape: 4px; + --mat-datepicker-calendar-container-touch-shape: 4px; + --mat-datepicker-calendar-container-elevation-shadow: + 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), + 0px 1px 10px 0px rgba(0, 0, 0, 0.12); + --mat-datepicker-calendar-container-touch-elevation-shadow: + 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), + 0px 9px 46px 8px rgba(0, 0, 0, 0.12); +} +html { + --mat-datepicker-calendar-date-in-range-state-background-color: color-mix( + in srgb, + #3f51b5 20%, + transparent + ); + --mat-datepicker-calendar-date-in-comparison-range-state-background-color: color-mix( + in srgb, + #ff4081 20%, + transparent + ); + --mat-datepicker-calendar-date-in-overlap-range-state-background-color: #a8dab5; + --mat-datepicker-calendar-date-in-overlap-range-selected-state-background-color: rgb( + 69.5241935484, + 163.4758064516, + 93.9516129032 + ); + --mat-datepicker-calendar-date-selected-state-text-color: white; + --mat-datepicker-calendar-date-selected-state-background-color: #3f51b5; + --mat-datepicker-calendar-date-selected-disabled-state-background-color: color-mix( + in srgb, + #3f51b5 38%, + transparent + ); + --mat-datepicker-calendar-date-today-selected-state-outline-color: white; + --mat-datepicker-calendar-date-focus-state-background-color: color-mix( + in srgb, + #3f51b5 12%, + transparent + ); + --mat-datepicker-calendar-date-hover-state-background-color: color-mix( + in srgb, + #3f51b5 4%, + transparent + ); + --mat-datepicker-toggle-active-state-icon-color: #3f51b5; + --mat-datepicker-toggle-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-body-label-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-period-button-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-period-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-navigation-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-header-divider-color: rgba(0, 0, 0, 0.12); + --mat-datepicker-calendar-header-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-disabled-state-outline-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-date-outline-color: transparent; + --mat-datepicker-calendar-date-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-preview-state-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-range-input-separator-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-range-input-disabled-state-separator-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-range-input-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-container-background-color: white; + --mat-datepicker-calendar-container-text-color: rgba(0, 0, 0, 0.87); +} +.mat-datepicker-content.mat-accent, +.mat-datepicker-toggle-active.mat-accent { + --mat-datepicker-calendar-date-in-range-state-background-color: color-mix( + in srgb, + #ff4081 20%, + transparent + ); + --mat-datepicker-calendar-date-in-comparison-range-state-background-color: color-mix( + in srgb, + #ff4081 20%, + transparent + ); + --mat-datepicker-calendar-date-in-overlap-range-state-background-color: #a8dab5; + --mat-datepicker-calendar-date-in-overlap-range-selected-state-background-color: rgb( + 69.5241935484, + 163.4758064516, + 93.9516129032 + ); + --mat-datepicker-calendar-date-selected-state-text-color: white; + --mat-datepicker-calendar-date-selected-state-background-color: #ff4081; + --mat-datepicker-calendar-date-selected-disabled-state-background-color: color-mix( + in srgb, + #ff4081 38%, + transparent + ); + --mat-datepicker-calendar-date-today-selected-state-outline-color: white; + --mat-datepicker-calendar-date-focus-state-background-color: color-mix( + in srgb, + #ff4081 12%, + transparent + ); + --mat-datepicker-calendar-date-hover-state-background-color: color-mix( + in srgb, + #ff4081 4%, + transparent + ); + --mat-datepicker-toggle-active-state-icon-color: #ff4081; + --mat-datepicker-toggle-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-body-label-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-period-button-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-period-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-navigation-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-header-divider-color: rgba(0, 0, 0, 0.12); + --mat-datepicker-calendar-header-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-disabled-state-outline-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-date-outline-color: transparent; + --mat-datepicker-calendar-date-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-preview-state-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-range-input-separator-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-range-input-disabled-state-separator-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-range-input-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-container-background-color: white; + --mat-datepicker-calendar-container-text-color: rgba(0, 0, 0, 0.87); +} +.mat-datepicker-content.mat-warn, +.mat-datepicker-toggle-active.mat-warn { + --mat-datepicker-calendar-date-in-range-state-background-color: color-mix( + in srgb, + #f44336 20%, + transparent + ); + --mat-datepicker-calendar-date-in-comparison-range-state-background-color: color-mix( + in srgb, + #ff4081 20%, + transparent + ); + --mat-datepicker-calendar-date-in-overlap-range-state-background-color: #a8dab5; + --mat-datepicker-calendar-date-in-overlap-range-selected-state-background-color: rgb( + 69.5241935484, + 163.4758064516, + 93.9516129032 + ); + --mat-datepicker-calendar-date-selected-state-text-color: white; + --mat-datepicker-calendar-date-selected-state-background-color: #f44336; + --mat-datepicker-calendar-date-selected-disabled-state-background-color: color-mix( + in srgb, + #f44336 38%, + transparent + ); + --mat-datepicker-calendar-date-today-selected-state-outline-color: white; + --mat-datepicker-calendar-date-focus-state-background-color: color-mix( + in srgb, + #f44336 12%, + transparent + ); + --mat-datepicker-calendar-date-hover-state-background-color: color-mix( + in srgb, + #f44336 4%, + transparent + ); + --mat-datepicker-toggle-active-state-icon-color: #f44336; + --mat-datepicker-toggle-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-body-label-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-period-button-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-period-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-navigation-button-icon-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-header-divider-color: rgba(0, 0, 0, 0.12); + --mat-datepicker-calendar-header-text-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-calendar-date-today-disabled-state-outline-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-text-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-calendar-date-outline-color: transparent; + --mat-datepicker-calendar-date-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-date-preview-state-outline-color: rgba(0, 0, 0, 0.54); + --mat-datepicker-range-input-separator-color: rgba(0, 0, 0, 0.87); + --mat-datepicker-range-input-disabled-state-separator-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-range-input-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-datepicker-calendar-container-background-color: white; + --mat-datepicker-calendar-container-text-color: rgba(0, 0, 0, 0.87); +} +.mat-calendar-controls { + --mat-icon-button-touch-target-display: none; +} +.mat-calendar-controls .mat-mdc-icon-button.mat-mdc-button-base { + --mdc-icon-button-state-layer-size: 40px; + --mat-icon-button-state-layer-size: 40px; + width: var(--mat-icon-button-state-layer-size); + height: var(--mat-icon-button-state-layer-size); + padding: 8px; +} +html { + --mat-datepicker-calendar-text-font: Mont, sans-serif; + --mat-datepicker-calendar-text-size: 13px; + --mat-datepicker-calendar-body-label-text-size: 14px; + --mat-datepicker-calendar-body-label-text-weight: 500; + --mat-datepicker-calendar-period-button-text-size: 14px; + --mat-datepicker-calendar-period-button-text-weight: 500; + --mat-datepicker-calendar-header-text-size: 11px; + --mat-datepicker-calendar-header-text-weight: 400; +} +html { + --mat-divider-width: 1px; +} +html { + --mat-divider-color: rgba(0, 0, 0, 0.12); +} +html { + --mat-expansion-container-shape: 4px; + --mat-expansion-container-elevation-shadow: + 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + --mat-expansion-legacy-header-indicator-display: inline-block; + --mat-expansion-header-indicator-display: none; +} +html { + --mat-expansion-container-background-color: white; + --mat-expansion-container-text-color: rgba(0, 0, 0, 0.87); + --mat-expansion-actions-divider-color: rgba(0, 0, 0, 0.12); + --mat-expansion-header-hover-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 4%, + transparent + ); + --mat-expansion-header-focus-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-expansion-header-disabled-state-text-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 38%, + transparent + ); + --mat-expansion-header-text-color: rgba(0, 0, 0, 0.87); + --mat-expansion-header-description-color: rgba(0, 0, 0, 0.54); + --mat-expansion-header-indicator-color: rgba(0, 0, 0, 0.54); +} +html { + --mat-expansion-header-collapsed-state-height: 48px; + --mat-expansion-header-expanded-state-height: 64px; +} +html { + --mat-expansion-header-text-font: Mont, sans-serif; + --mat-expansion-header-text-size: 14px; + --mat-expansion-header-text-weight: 500; + --mat-expansion-header-text-line-height: inherit; + --mat-expansion-header-text-tracking: inherit; + --mat-expansion-container-text-font: Mont, sans-serif; + --mat-expansion-container-text-line-height: 20px; + --mat-expansion-container-text-size: 14px; + --mat-expansion-container-text-tracking: 0.0178571429em; + --mat-expansion-container-text-weight: 400; +} +html { + --mat-grid-list-tile-header-primary-text-size: 14px; + --mat-grid-list-tile-header-secondary-text-size: 12px; + --mat-grid-list-tile-footer-primary-text-size: 14px; + --mat-grid-list-tile-footer-secondary-text-size: 12px; +} +html { + --mat-icon-color: inherit; +} +.mat-icon.mat-primary { + --mat-icon-color: #3f51b5; +} +.mat-icon.mat-accent { + --mat-icon-color: #ff4081; +} +.mat-icon.mat-warn { + --mat-icon-color: #f44336; +} +html { + --mat-sidenav-container-shape: 0; + --mat-sidenav-container-elevation-shadow: + 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), + 0px 6px 30px 5px rgba(0, 0, 0, 0.12); + --mat-sidenav-container-width: auto; +} +html { + --mat-sidenav-container-divider-color: rgba(0, 0, 0, 0.12); + --mat-sidenav-container-background-color: white; + --mat-sidenav-container-text-color: rgba(0, 0, 0, 0.87); + --mat-sidenav-content-background-color: #fafafa; + --mat-sidenav-content-text-color: rgba(0, 0, 0, 0.87); + --mat-sidenav-scrim-color: rgba(0, 0, 0, 0.6); +} +html { + --mat-stepper-header-focus-state-layer-shape: 0; + --mat-stepper-header-hover-state-layer-shape: 0; +} +html { + --mat-stepper-header-icon-foreground-color: white; + --mat-stepper-header-selected-state-icon-background-color: #3f51b5; + --mat-stepper-header-selected-state-icon-foreground-color: white; + --mat-stepper-header-done-state-icon-background-color: #3f51b5; + --mat-stepper-header-done-state-icon-foreground-color: white; + --mat-stepper-header-edit-state-icon-background-color: #3f51b5; + --mat-stepper-header-edit-state-icon-foreground-color: white; + --mat-stepper-container-color: white; + --mat-stepper-line-color: rgba(0, 0, 0, 0.12); + --mat-stepper-header-hover-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 4%, + transparent + ); + --mat-stepper-header-focus-state-layer-color: color-mix( + in srgb, + rgba(0, 0, 0, 0.87) 12%, + transparent + ); + --mat-stepper-header-label-text-color: rgba(0, 0, 0, 0.54); + --mat-stepper-header-optional-label-text-color: rgba(0, 0, 0, 0.54); + --mat-stepper-header-selected-state-label-text-color: rgba(0, 0, 0, 0.87); + --mat-stepper-header-error-state-label-text-color: #f44336; + --mat-stepper-header-icon-background-color: rgba(0, 0, 0, 0.54); + --mat-stepper-header-error-state-icon-foreground-color: #f44336; + --mat-stepper-header-error-state-icon-background-color: transparent; +} +.mat-step-header.mat-accent { + --mat-stepper-header-icon-foreground-color: white; + --mat-stepper-header-selected-state-icon-background-color: #ff4081; + --mat-stepper-header-selected-state-icon-foreground-color: white; + --mat-stepper-header-done-state-icon-background-color: #ff4081; + --mat-stepper-header-done-state-icon-foreground-color: white; + --mat-stepper-header-edit-state-icon-background-color: #ff4081; + --mat-stepper-header-edit-state-icon-foreground-color: white; +} +.mat-step-header.mat-warn { + --mat-stepper-header-icon-foreground-color: white; + --mat-stepper-header-selected-state-icon-background-color: #f44336; + --mat-stepper-header-selected-state-icon-foreground-color: white; + --mat-stepper-header-done-state-icon-background-color: #f44336; + --mat-stepper-header-done-state-icon-foreground-color: white; + --mat-stepper-header-edit-state-icon-background-color: #f44336; + --mat-stepper-header-edit-state-icon-foreground-color: white; +} +html { + --mat-stepper-header-height: 72px; +} +html { + --mat-stepper-container-text-font: Mont, sans-serif; + --mat-stepper-header-label-text-font: Mont, sans-serif; + --mat-stepper-header-label-text-size: 14px; + --mat-stepper-header-label-text-weight: 400; + --mat-stepper-header-error-state-label-text-size: 16px; + --mat-stepper-header-selected-state-label-text-size: 16px; + --mat-stepper-header-selected-state-label-text-weight: 400; +} +html { + --mat-sort-arrow-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-toolbar-container-background-color: white; + --mat-toolbar-container-text-color: rgba(0, 0, 0, 0.87); +} +.mat-toolbar.mat-primary { + --mat-toolbar-container-background-color: #3f51b5; + --mat-toolbar-container-text-color: white; +} +.mat-toolbar.mat-accent { + --mat-toolbar-container-background-color: #ff4081; + --mat-toolbar-container-text-color: white; +} +.mat-toolbar.mat-warn { + --mat-toolbar-container-background-color: #f44336; + --mat-toolbar-container-text-color: white; +} +html { + --mat-toolbar-standard-height: 64px; + --mat-toolbar-mobile-height: 56px; +} +html { + --mat-toolbar-title-text-font: Mont, sans-serif; + --mat-toolbar-title-text-line-height: 32px; + --mat-toolbar-title-text-size: 20px; + --mat-toolbar-title-text-tracking: 0.0125em; + --mat-toolbar-title-text-weight: 500; +} +html { + --mat-tree-container-background-color: white; + --mat-tree-node-text-color: rgba(0, 0, 0, 0.87); +} +html { + --mat-tree-node-min-height: 48px; +} +html { + --mat-tree-node-text-font: Mont, sans-serif; + --mat-tree-node-text-size: 14px; + --mat-tree-node-text-weight: 400; +} +html { + --mat-timepicker-container-shape: 4px; + --mat-timepicker-container-elevation-shadow: + 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} +html { + --mat-timepicker-container-background-color: white; +} +@media screen and (max-width: 999px) { + app-root, + app-input, + app-textarea, + app-select, + app-autocomplete-input, + app-search, + app-upload-file, + app-file-item, + app-file-upload-item, + app-button, + ui-button { + min-width: 0; + max-width: 100%; + } +} +@media screen and (max-width: 999px) { + h1, + h2, + h3, + h4, + p, + label, + .error { + overflow-wrap: anywhere; + } +} +@media screen and (max-width: 999px) { + video, + iframe, + canvas { + max-width: 100%; + } +} +@media screen and (max-width: 999px) { + .cdk-overlay-container { + z-index: 10010; + } +} +@media screen and (max-width: 999px) { + .responsive-header, + .responsive-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + min-width: 0; + } +} +@media screen and (max-width: 999px) { + .responsive-table { + min-width: 0; + max-width: 100%; + overflow-x: auto; + overscroll-behavior-inline: contain; + } +} +@media screen and (max-width: 999px) { + .responsive-dialog-header, + .responsive-dialog-actions { + position: sticky; + z-index: 2; + background: inherit; + } +} +@media screen and (max-width: 999px) { + .responsive-dialog-header { + top: 0; + } +} +@media screen and (max-width: 999px) { + .responsive-dialog-actions { + bottom: 0; + padding-bottom: env(safe-area-inset-bottom, 0); + } +} +@media screen and (max-width: 999px) and (max-width: 749px) { + html { + scrollbar-gutter: auto; + } + .text-body-10 { + font-size: 12px; + line-height: 18px; + } + .text-body-12 { + font-size: 14px; + line-height: 20px; + } + input:not([type="checkbox"], [type="radio"]), + textarea, + select { + min-width: 0; + max-width: 100%; + font-size: 16px; + } + .responsive-header, + .responsive-actions { + flex-direction: column; + align-items: stretch; + } + .cdk-overlay-pane { + max-width: calc(100vw - 24px); + } + .mat-datepicker-content { + max-width: 100%; + max-height: calc(100dvh - 24px); + overflow: auto; + } +} +@media screen and (max-width: 999px) and (max-width: 359px) { + :root { + --page-gutter: 12px; + } +} +:root { + --ui-accent: var(--accent); + --accent-hover: var(--accent-medium); + --ui-text: var(--black); + --ui-text-muted: var(--grey-for-text); + --ui-surface: var(--light-white); + --ui-surface-subtle: var(--white); + --ui-border: var(--gray); + --ui-border-subtle: var(--medium-grey-for-outline); + --ui-danger: var(--red); + --ui-success: var(--green); + --ui-warning: var(--gold); + --ui-radius-control: var(--rounded-xxl); + --ui-radius-card: var(--rounded-xl); + --ui-radius-dialog: var(--rounded-xl); + --ui-control-height: 44px; + --ui-compact-sidebar-width: 157px; + --ui-mobile-entity-card-width: 260px; + --ui-control-padding: 12px 16px; + --ui-card-padding: 16px; + --ui-dialog-padding: 24px; + --ui-dialog-sm: 400px; + --ui-dialog-md: 560px; + --ui-dialog-lg: 800px; + --ui-space-xs: 4px; + --ui-space-sm: 8px; + --ui-space-md: 16px; + --ui-space-lg: 24px; + --ui-focus-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); + --ui-error-ring: 0 0 0 3px color-mix(in srgb, var(--red) 25%, transparent); + --ui-card-shadow: none; + --ui-card-hover-shadow: 0 4px 12px color-mix(in srgb, var(--black) 8%, transparent); + --ui-backdrop: color-mix(in srgb, var(--black) 50%, transparent); + --ui-font-h1: 24px; + --ui-font-h2: 18px; + --ui-font-h3: 16px; + --ui-font-body: 14px; + --ui-font-small: 12px; + --ui-font-caption: 12px; + --ui-disabled-opacity: 0.5; + --ui-nav-gradient-end: #4d3780; + --ui-board-surface: #d9d9d9; + --ui-error-text: #b42318; + --ui-crop-surface: #f8f9fa; + --ui-message-surface: #f7f3ff; + --ui-media-button: #81818180; + --ui-media-button-active: #37373780; + --ui-media-backdrop: #333b; + --ui-overlay-shadow-color: #0002; + --ui-scrim: rgb(51 46 45 / 50%); + --ui-preview-scrim: rgb(0 0 0 / 60%); + --ui-crop-scrim: rgb(0 0 0 / 50%); + --ui-crop-border: rgb(255 255 255 / 50%); + --ui-success-glow: rgb(0 200 147 / 51%); + --ui-success-glow-end: rgb(0 200 147 / 0%); + --ui-accent-tint-subtle: rgb(109 40 255 / 6%); + --ui-accent-tint-hover: rgb(109 40 255 / 12%); + --ui-accent-tint-chart: rgb(109 40 255 / 8%); + --ui-accent-gradient-end: rgb(138 99 230 / 28%); + --ui-panel-shadow: 0 2px 8px rgb(51 51 51 / 8%); + --ui-chart-shadow: 0 6px 22px rgb(51 51 51 / 4%); + --ui-tooltip-shadow: 0 4px 12px rgb(0 0 0 / 15%); + --ui-crop-shadow: 0 4px 12px rgb(0 0 0 / 10%); + --ui-crop-handle-shadow: 0 2px 4px rgb(0 0 0 / 20%); + --ui-message-shadow: 5px 5px 25px rgb(159 159 159 / 15%); + --ui-header-shadow: 5px 5px 50px rgb(196 196 196 / 15%); + --ui-composer-shadow: 4px -4px 4px rgb(51 51 51 / 20%); +} +@media (max-width: 749px) { + :root { + --ui-dialog-padding: 16px; + --ui-font-h1: 18px; + } +} +@media screen and (min-width: 1000px) { + :where(button.ui-button) { + padding: 0; + font: inherit; + color: inherit; + text-align: inherit; + cursor: pointer; + background: transparent; + border: 0; + } +} +@media screen and (max-width: 999px) { + button.ui-button.ui-button { + --button-color: var(--ui-accent); + --button-hover: var(--accent-medium); + --button-text: var(--light-white); + min-width: 0; + max-width: 100%; + min-height: var(--ui-control-height); + padding: 10px 24px; + font: inherit; + font-size: var(--ui-font-small); + font-weight: 600; + line-height: 1.4; + color: var(--button-text); + text-align: center; + text-decoration: none; + overflow-wrap: anywhere; + white-space: normal; + cursor: pointer; + background: var(--button-color); + border: 1px solid var(--button-color); + border-radius: var(--ui-radius-control); + transition: + background-color 0.2s, + border-color 0.2s, + box-shadow 0.2s; + } + button.ui-button.ui-button:hover:not(:disabled) { + text-decoration: none; + background: var(--button-hover); + border-color: var(--button-hover); + } + button.ui-button.ui-button:active:not(:disabled) { + background: var(--accent-dark); + } + button.ui-button.ui-button:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; + } + button.ui-button.ui-button:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); + } + button.ui-button.ui-button--secondary, + button.ui-button.ui-button--outline { + color: var(--button-color); + background: transparent; + } + button.ui-button.ui-button--secondary:hover:not(:disabled), + button.ui-button.ui-button--secondary:active:not(:disabled), + button.ui-button.ui-button--outline:hover:not(:disabled), + button.ui-button.ui-button--outline:active:not(:disabled) { + color: var(--button-color); + background: var(--accent-light); + } + button.ui-button.ui-button--danger, + button.ui-button.ui-button--red { + --button-color: var(--ui-danger); + --button-hover: var(--red-dark); + } + button.ui-button.ui-button--danger:active:not(:disabled), + button.ui-button.ui-button--red:active:not(:disabled) { + background: var(--red-dark); + } + button.ui-button.ui-button--danger.button--outline:hover:not(:disabled), + button.ui-button.ui-button--red.button--outline:hover:not(:disabled) { + background: var(--red-light); + } + button.ui-button.ui-button--grey { + --button-color: var(--grey-button); + --button-hover: var(--gray); + --button-text: var(--black); + } + button.ui-button.ui-button--green { + --button-color: var(--green); + --button-hover: var(--green-dark); + } + button.ui-button.ui-button--gold { + --button-color: var(--gold); + --button-hover: var(--gold-dark); + } + button.ui-button.ui-button--white { + --button-color: var(--light-white); + --button-hover: var(--white); + --button-text: var(--black); + } + button.ui-button.ui-button--gradient { + background: var(--gradient); + } + button.ui-button.ui-button--no-border { + border-color: transparent; + } + button.ui-button.ui-button--medium { + min-width: min(157px, 100%); + } + button.ui-button.ui-button--big { + width: 100%; + } + button.ui-button.ui-button--extra-small { + padding-inline: 12px; + } + button.ui-button.ui-button--icon, + button.ui-button.ui-button--text { + color: var(--ui-accent); + background: transparent; + border-color: transparent; + } + button.ui-button.ui-button--icon:hover:not(:disabled), + button.ui-button.ui-button--icon:active:not(:disabled), + button.ui-button.ui-button--text:hover:not(:disabled), + button.ui-button.ui-button--text:active:not(:disabled) { + color: var(--accent-dark); + background: var(--accent-light); + border-color: transparent; + } + button.ui-button.ui-button--icon { + min-width: 44px; + padding: 10px; + } + button.ui-button.ui-button--text { + padding-inline: 12px; + } +} +@media screen and (max-width: 999px) and (max-width: 749px) { + button.ui-button.ui-button { + padding-inline: 16px; + } + button.ui-button.ui-button--icon { + padding: 9px; + } + button.ui-button.ui-button--text { + padding-inline: 12px; + } +} +@media screen and (max-width: 999px) { + .ui-button { + display: inline-flex; + gap: var(--ui-space-sm); + align-items: center; + justify-content: center; + } +} +@media screen and (max-width: 999px) { + button.ui-button[role="radio"] { + justify-content: space-between; + width: 100%; + padding: var(--ui-control-padding); + color: var(--ui-text); + text-align: left; + background: var(--ui-surface-subtle); + border-color: var(--ui-border-subtle); + border-radius: var(--ui-radius-card); + } + button.ui-button[role="radio"][aria-checked="true"] { + background: var(--accent-light); + border-color: var(--ui-accent); + } +} +@media screen and (max-width: 999px) { + .ui-field { + box-sizing: border-box; + min-width: 0; + max-width: 100%; + min-height: var(--ui-control-height); + padding: var(--ui-control-padding); + font: inherit; + font-size: var(--ui-font-body); + line-height: 1.4; + color: var(--ui-text); + background: var(--ui-surface-subtle); + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-control); + outline: none; + transition: + border-color 0.2s, + box-shadow 0.2s; + } + .ui-field:is(select) { + padding-right: 44px; + appearance: none; + background-image: url(/assets/icons/svg/chevron-down.svg); + background-repeat: no-repeat; + background-position: right 16px center; + background-size: 14px; + } + .ui-field::placeholder { + color: var(--dark-grey); + } + .ui-field:hover:not(:disabled) { + border-color: var(--dark-grey); + } + .ui-field:focus-visible, + .ui-field:focus-within, + .ui-field--open { + border-color: var(--ui-accent); + box-shadow: var(--ui-focus-ring); + } + .ui-field--error, + .ui-field--overflow, + .ui-field[aria-invalid="true"] { + border: 1px solid var(--ui-danger); + } + .ui-field--error:focus, + .ui-field--error:focus-within, + .ui-field--overflow:focus, + .ui-field--overflow:focus-within, + .ui-field[aria-invalid="true"]:focus, + .ui-field[aria-invalid="true"]:focus-within { + box-shadow: var(--ui-error-ring); + } + .ui-field:disabled, + .ui-field--disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); + } +} +@media screen and (max-width: 999px) and (max-width: 749px) { + .ui-field { + font-size: 16px; + } +} +@media screen and (max-width: 999px) { + input.ui-choice.ui-choice { + min-height: 18px; + padding: 0; + background: transparent; + flex-shrink: 0; + width: 18px; + height: 18px; + margin: 0; + accent-color: var(--ui-accent); + cursor: pointer; + appearance: auto; + } + input.ui-choice.ui-choice:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; + } + input.ui-choice.ui-choice:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); + } +} +@media screen and (max-width: 999px) { + .ui-card { + min-width: 0; + max-width: 100%; + padding: var(--ui-card-padding); + color: var(--ui-text); + background: var(--ui-surface); + border: 1px solid var(--ui-border-subtle); + border-radius: var(--ui-radius-card); + box-shadow: var(--ui-card-shadow); + } + .ui-card--interactive { + transition: + box-shadow 0.2s, + border-color 0.2s; + } + .ui-card--interactive:hover { + border-color: var(--ui-accent); + box-shadow: var(--ui-card-hover-shadow); + } + .ui-card:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; + } +} +@media screen and (max-width: 999px) { + .ui-form { + min-width: 0; + } +} +@media screen and (max-width: 999px) { + .ui-form label.field-label, + label.field-label { + margin-bottom: var(--ui-space-sm); + font-size: var(--ui-font-small); + line-height: 1.5; + color: var(--ui-text); + } +} +@media screen and (max-width: 999px) { + .ui-form .error, + .error { + font-size: var(--ui-font-small); + line-height: 1.5; + color: var(--ui-danger); + } +} +@media screen and (max-width: 999px) { + .ui-filters { + display: flex; + flex-direction: column; + gap: var(--ui-space-md); + min-width: 0; + } +} +@media screen and (max-width: 999px) { + .ui-filters__header { + display: flex; + flex-wrap: wrap; + gap: var(--ui-space-xs); + align-items: center; + justify-content: space-between; + } + .ui-filters__header > p, + .ui-filters__header > .ui-button { + flex-shrink: 0; + } +} +@media screen and (max-width: 999px) { + .ui-dialog-body { + min-width: 0; + font-size: var(--ui-font-body); + line-height: 1.5; + overflow-wrap: anywhere; + } +} +@media screen and (max-width: 999px) { + .ui-dialog-footer { + display: flex; + flex-wrap: wrap; + gap: var(--ui-space-sm); + align-items: center; + justify-content: flex-end; + padding-top: var(--ui-space-md); + margin-top: var(--ui-space-md); + } +} +@media screen and (max-width: 999px) { + .ui-alert { + padding: var(--ui-space-md); + color: var(--ui-text); + background: var(--accent-light); + border-radius: var(--ui-radius-card); + } +} +@media screen and (max-width: 999px) { + .ui-alert--error { + color: var(--red-dark); + background: var(--red-light); + } +} +@media screen and (max-width: 999px) { + .ui-alert--success { + color: var(--green-dark); + background: var(--green-light); + } +} +@media screen and (max-width: 999px) { + .ui-alert--warning { + color: var(--gold-dark); + background: var(--gold-light); + } +} +@media screen and (max-width: 999px) { + .ui-drawer { + min-width: 0; + max-width: 100vw; + overscroll-behavior: contain; + } +} +@media screen and (max-width: 999px) { + table.ui-table { + width: 100%; + font-size: var(--ui-font-small); + line-height: 1.5; + color: var(--ui-text); + border-collapse: collapse; + } + table.ui-table th { + padding: 12px 16px; + font-weight: 600; + text-align: left; + background: var(--ui-surface-subtle); + border-bottom: 1px solid var(--ui-border); + } + table.ui-table td { + padding: 12px 16px; + border-bottom: 1px solid var(--ui-border-subtle); + } + table.ui-table tbody tr:hover { + background: var(--accent-light); + } +} +@media screen and (max-width: 999px) { + .ui-h1 { + font-size: var(--ui-font-h1); + font-weight: 600; + line-height: 1.3; + } +} +@media screen and (max-width: 999px) { + .ui-h2 { + font-size: var(--ui-font-h2); + font-weight: 600; + line-height: 1.4; + } +} +@media screen and (max-width: 999px) { + .ui-h3 { + font-size: var(--ui-font-h3); + font-weight: 600; + line-height: 1.4; + } +} +@media screen and (max-width: 999px) { + .ui-body { + font-size: var(--ui-font-body); + line-height: 1.5; + } +} +@media screen and (max-width: 999px) { + .ui-small { + font-size: var(--ui-font-small); + line-height: 1.5; + } +} +@media screen and (max-width: 999px) { + .ui-caption { + font-size: var(--ui-font-caption); + line-height: 1.5; + color: var(--ui-text-muted); + } +} + +/* angular:styles/global:styles */ +/*# sourceMappingURL=styles-AOKJA37L.css.map */ diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/icons.js b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/icons.js new file mode 100644 index 000000000..4ebb11781 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/icons.js @@ -0,0 +1,22 @@ +/** @format */ + +window.prototypeIcons = { + person: + '\r\n\r\n\r\n', + folder: + '\r\n \r\n\r\n', + download: + '\r\n \r\n\r\n', + filter: + '\r\n \r\n\r\n', + search: + '\r\n \r\n\r\n', + reload: + '\r\n \r\n \r\n\r\n', + hint: '\r\n\r\n\r\n', + "analytics-team": + '\r\n \r\n \r\n \r\n \r\n \r\n\r\n', + "analytics-submitted": + '\r\n \r\n \r\n\r\n', + pen: '\r\n\r\n\r\n', +}; diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/index.html b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/index.html new file mode 100644 index 000000000..61bba805b --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/index.html @@ -0,0 +1,116 @@ + + + + + + + + Решения программы — интерфейс организатора + + + + + + + +
+ Макет организатора / вымышленные данные +
+ + + + + +
+ Моделируется контур заявок. Остальные разделы программы + сохраняются. +
+
+ +
+
+ PROCOLLABУправление программой +
+
+

Управление программой

+

Кейс-чемпионат PROCOLLAB

+
+ +
+
+
+
+ +
+ + + + + + diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-5LPDNI7X.ttf b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-5LPDNI7X.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ee599fc441cc1a3907c1243460001c8215cd506f GIT binary patch literal 144348 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�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 literal 0 HcmV?d00001 diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-WNUGA77E.woff b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-WNUGA77E.woff new file mode 100644 index 0000000000000000000000000000000000000000..b18fe0f1bc942975750b636ca20ef4d39a7c7d8f GIT binary patch literal 65064 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 KSU=bueaD{Eb8alNg?U-{>)F{U8nUtn2_F# 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�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 JK|= span:last-child { + color: var(--accent); +} +.prototype-results .results__attention--active { + border-color: var(--gold); +} +.prototype-results .results__attention:focus-visible, +.prototype-results .results__row:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-results .results__group { + position: relative; + min-width: 0; +} +.prototype-results .results__group > h3 { + margin-bottom: 12px; +} +.prototype-results .results__columns, +.prototype-results .results__row { + display: grid; + grid-template-columns: 40px minmax(0, 1fr) 180px 128px 24px; + gap: 16px; + align-items: center; +} +.prototype-results .results__columns { + padding-block: 8px; + color: var(--grey-for-text); +} +.prototype-results .results__row { + width: 100%; + min-height: 72px; + padding: 12px 0; + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + background: transparent; + border: 0; + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.prototype-results .results__row:hover, +.prototype-results .results__row--selected { + background: var(--light-gray); +} +.prototype-results .results__name { + min-width: 0; + overflow-wrap: anywhere; +} +.prototype-results .results__count { + display: grid; + gap: 4px; + justify-items: start; + font-size: 11px; +} +.prototype-results .results__count .results__badge { + max-width: 100%; +} +.prototype-results .results__score { + display: grid; + gap: 4px; + font-variant-numeric: tabular-nums; + text-align: right; +} +.prototype-results .results__score small { + font-size: 11px; +} +.prototype-results .results__score strong { + font-size: 14px; + font-weight: 600; +} +.prototype-results .results__score-mobile-label { + display: none; +} +.prototype-results .results__number { + text-align: right; +} +.prototype-results .results__arrow { + justify-self: center; + transition: transform 120ms; +} +.prototype-results .results__arrow--open { + transform: rotate(180deg); +} +.prototype-results .results__formula.ui-button.ui-button { + position: absolute; + top: 32px; + right: 0; + width: 24px; + min-width: 24px; + height: 24px; + min-height: 24px; + padding: 4px; +} +.prototype-results .results--unscored .results__columns, +.prototype-results .results--unscored .results__row { + grid-template-columns: minmax(0, 1fr) 180px 24px; +} +.prototype-results .results__detail { + min-width: 0; + padding: 16px; + background: var(--light-gray); + border-radius: 10px; +} +.prototype-results .results__pagination { + gap: 20px; + justify-content: flex-end; + color: var(--grey-for-text); +} +.prototype-results .results__empty, +.prototype-results .results__sheet-info { + display: grid; + gap: 12px; + padding-block: 16px; +} +.prototype-results .results__empty button { + justify-self: start; +} +.prototype-results .results__sheet-info { + padding: 16px; + background: var(--light-gray); + border-radius: 10px; +} +.prototype-results .results__scope { + display: grid; + gap: 4px; + padding: 0; + margin: 0; + border: 0; +} +.prototype-results .results__scope label { + display: flex; + gap: 12px; + align-items: center; + min-height: 44px; + cursor: pointer; +} +.prototype-results .results__scope label > span { + display: flex; + flex: 1; + gap: 12px; + justify-content: space-between; +} +.prototype-results .results__download { + justify-self: end; + min-width: 180px; +} +.prototype-results .results__filter-label { + display: grid; + gap: 8px; +} +.prototype-results .results__sr { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); +} +.prototype-results .results__sort-menu { + box-sizing: border-box; + display: grid; + gap: 4px; + width: 260px; + max-width: calc(100vw - 32px); + padding: 8px; + font-size: 12px; + color: var(--black); + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); +} +.prototype-results .results__sort-option { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + min-height: 44px; + padding: 8px; + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + background: transparent; + border: 0; + border-radius: 8px; +} +.prototype-results .results__sort-option:hover, +.prototype-results .results__sort-option:focus-visible { + background: var(--light-gray); + outline: none; +} +.prototype-results ::ng-deep .modal .modal__body.results-modal { + width: 480px; + max-width: calc(100vw - 40px); + max-height: calc(var(--visual-viewport-height, 100dvh) - 40px); + padding: 24px; + overflow-y: auto; +} +.prototype-results .ui-alert.ui-alert.ui-alert { + box-sizing: border-box; + padding: 12px 16px; + margin: 0; + font-size: 12px; + line-height: 1.5; + color: var(--black); + background: var(--accent-light); + border-radius: 10px; +} +.prototype-results .ui-alert.ui-alert.ui-alert.ui-alert--success { + background: var(--green-light); +} +.prototype-results .ui-alert.ui-alert.ui-alert.ui-alert--warning { + background: color-mix(in srgb, var(--gold) 28%, var(--light-white)); +} +.prototype-results .ui-alert.ui-alert.ui-alert.ui-alert--error { + background: var(--red-light); +} +.prototype-results .ui-alert.ui-alert.ui-alert p, +.prototype-results .ui-alert.ui-alert.ui-alert strong, +.prototype-results .ui-alert.ui-alert.ui-alert summary { + color: inherit; +} +.prototype-results .ui-alert.ui-alert.ui-alert p { + margin: 4px 0 0; +} +.prototype-results .results__badge { + display: inline-block; + padding: 3px 8px; + font-size: 11px; + line-height: 16px; + color: var(--black); + background: color-mix(in srgb, var(--gold) 28%, var(--light-white)); + border-radius: 6px; +} +.prototype-results .results__badge--ready { + background: var(--green-light); +} +.prototype-results .results__badge--error { + background: var(--red-light); +} +@media (max-width: 999px) { + .prototype-results .results__score-mobile-label { + display: inline; + } + .prototype-results .results { + padding: 16px; + } + .prototype-results .results__toolbar { + flex-wrap: wrap; + } + .prototype-results .results__search { + flex-basis: 100%; + } + .prototype-results .results__toolbar-actions { + flex-wrap: wrap; + width: 100%; + } + .prototype-results .results__toolbar-actions .ui-button { + flex: 1 0 auto; + } + .prototype-results .results__columns, + .prototype-results .results__formula.ui-button { + display: none; + } + .prototype-results .results__formula-mobile.ui-button { + display: inline-flex; + } + .prototype-results .results__row { + grid-template-columns: 24px minmax(0, 1fr) auto 16px; + gap: 8px; + min-height: 100px; + } + .prototype-results .results__place { + grid-row: 1; + grid-column: 1; + font-weight: 600; + } + .prototype-results .results__name { + grid-row: 1; + grid-column: 2/4; + } + .prototype-results .results__arrow { + grid-row: 1; + grid-column: 4; + } + .prototype-results .results__count { + grid-row: 2; + grid-column: 2/4; + } + .prototype-results .results__score { + display: flex; + grid-row: 3; + grid-column: 2/4; + gap: 12px; + align-items: center; + justify-content: space-between; + } + .prototype-results .results--unscored .results__row { + grid-template-columns: minmax(0, 1fr) 16px; + min-height: 80px; + } + .prototype-results .results--unscored .results__name { + grid-column: 1; + } + .prototype-results .results--unscored .results__count { + grid-column: 1; + } + .prototype-results .results--unscored .results__arrow { + grid-column: 2; + } + .prototype-results .results__detail { + padding: 12px 0; + background: transparent; + } + .prototype-results .results__pagination { + gap: 12px; + } + .prototype-results .results__dialog .results__heading { + align-items: start; + } + .prototype-results .results__download { + width: 100%; + } +} +@media (prefers-reduced-motion: reduce) { + .prototype-results .results__arrow { + transition: none; + } +} diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/styles.css b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/styles.css new file mode 100644 index 000000000..79dbcbf69 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/styles.css @@ -0,0 +1,1178 @@ +/** @format */ + +@font-face { + font-family: Mont; + src: url("../assets/Mont-Regular.woff2"); + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: Mont; + src: url("../assets/Mont-SemiBold.woff2"); + font-weight: 600; + font-display: swap; +} +@font-face { + font-family: Mont; + src: url("../assets/Mont-Bold.woff2"); + font-weight: 700; + font-display: swap; +} +* { + box-sizing: border-box; +} +[hidden] { + display: none !important; +} +body { + margin: 0; + font: + 14px/1.5 Mont, + sans-serif; + background: var(--light-gray, #f8f8f8); + color: var(--black, #171717); +} +button, +input, +select, +textarea { + font: inherit; + color: inherit; +} +button, +a, +select { + touch-action: manipulation; +} +button { + cursor: pointer; +} +button:disabled { + cursor: default; + opacity: 0.5; +} +button:focus-visible, +a:focus-visible, +input:focus-visible, +select:focus-visible { + outline: 2px solid var(--accent, #8862ec); + outline-offset: 3px; +} +button, +input, +select { + min-width: 0; +} +a, +.link { + color: var(--accent, #8862ec); + text-decoration: none; +} +.link { + padding: 0; + border: 0; + background: none; + text-align: left; + line-height: 1.6; + overflow-wrap: anywhere; +} +h1, +h2, +h3, +p { + margin: 0; +} +h1 { + font-size: 22px; + font-weight: 600; + line-height: 1.4; + overflow-wrap: anywhere; +} +h2 { + font-size: 16px; + font-weight: 600; + line-height: 1.5; +} +h3 { + font-size: 14px; + font-weight: 600; +} +.muted { + color: var(--grey-for-text, #858585); + font-size: 12px; + line-height: 1.6; +} +.prototype-bar { + padding: 12px 24px; + display: flex; + gap: 16px; + justify-content: space-between; + align-items: center; + background: #fff; + border-bottom: 1px solid var(--medium-grey-for-outline, #ddd); + font-size: 12px; +} +.prototype-controls { + display: flex; + gap: 16px; + flex-wrap: wrap; +} +.prototype-controls label { + display: flex; + gap: 8px; + align-items: center; +} +.prototype-controls select { + padding: 6px 8px; + background: #fff; + border: 1px solid var(--medium-grey-for-outline, #ddd); + border-radius: 8px; + max-width: 240px; +} +.office { + display: grid; + grid-template-columns: 176px minmax(0, 1fr); + gap: 20px; + max-width: 1200px; + margin: auto; +} +.sidebar { + padding: 32px 12px; +} +.logo { + width: 112px; + margin-bottom: 28px; +} +.sidebar nav { + display: grid; + gap: 20px; + font-size: 12px; +} +.sidebar .active { + color: var(--accent); +} +.workspace { + min-width: 0; + padding: 24px 20px 48px; +} +.program-header { + margin-bottom: 20px; +} +.program-header p { + margin-bottom: 4px; +} +.mobile-header { + display: none; +} +.section-tabs { + display: flex; + flex-wrap: wrap; + gap: 6px 20px; + margin: 0 0 20px; + padding-bottom: 10px; + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.section-tabs button { + background: none; + border: 0; + padding: 7px 0; + color: var(--grey-for-text); + font-size: 13px; +} +.section-tabs button[aria-current="page"] { + color: var(--accent); + font-weight: 600; +} +.section-tabs button[aria-current="page"]::after { + content: ""; + display: block; + height: 2px; + margin-top: 6px; + background: var(--accent); +} +.icon { + display: inline-flex; + width: 18px; + height: 18px; + flex: none; + align-items: center; + justify-content: center; +} +.icon svg { + width: 100%; + height: 100%; +} +.summary-card__icon .icon { + width: 24px; + height: 24px; +} +.primary, +.secondary, +.quiet { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 32px; + padding: 6px 16px; + font-size: 12px; + line-height: 1.4; + border-radius: 20px; + white-space: normal; + text-align: center; +} +.primary { + border: 1px solid var(--accent); + background: var(--accent); + color: #fff; +} +.secondary { + border: 1px solid var(--accent); + background: transparent; + color: var(--accent); +} +.quiet { + border: 1px solid var(--medium-grey-for-outline); + background: transparent; +} +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 0; + background: none; + border-radius: 8px; + flex: none; +} +.icon-button:hover, +.quiet:hover { + background: var(--light-gray); +} +.heading-row, +.toolbar, +.actions, +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.actions { + justify-content: flex-start; + flex-wrap: wrap; +} +.page-panel { + padding: 20px; + background: var(--light-white, #fff); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; + display: grid; + gap: 16px; + min-width: 0; +} +.toolbar { + flex-wrap: wrap; +} +.search-form { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 180px; +} +.search-form input { + width: 100%; + min-height: 36px; + padding: 8px 12px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 18px; + background: transparent; + font-size: 12px; +} +.toolbar-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.filters { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + padding: 16px 0; + border-top: 1px solid var(--medium-grey-for-outline); + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.field { + display: grid; + gap: 6px; + font-size: 12px; + align-content: start; +} +.field select, +.field input { + width: 100%; + padding: 9px 12px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 18px; + min-height: 36px; + background: transparent; +} +.filters-note { + grid-column: 1/-1; +} +.filter-reset { + justify-self: start; +} +.table-wrap { + width: 100%; + min-width: 0; +} +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + line-height: 1.6; +} +.data-table th { + padding: 10px 8px; + font-weight: 400; + color: var(--grey-for-text); + text-align: left; + white-space: normal; + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.data-table td { + padding: 16px 8px; + vertical-align: top; + border-bottom: 1px solid var(--light-gray, #eee); + text-align: left; + overflow-wrap: anywhere; +} +.data-table th:first-child, +.data-table td:first-child { + padding-left: 0; +} +.data-table .name-cell { + width: 35%; +} +.subtext { + display: block; + color: var(--grey-for-text); + font-size: 11px; + margin-top: 3px; +} +.status { + display: inline-block; + font-size: 12px; + line-height: 1.5; +} +.status--good { + color: var(--green-dark, #407b53); +} +.status--warning { + color: var(--orange-dark, #936e32); +} +.status--muted { + color: var(--grey-for-text); +} +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + font-size: 12px; + color: var(--grey-for-text); + margin-top: 8px; +} +.pagination .actions { + gap: 8px; +} +.empty-state { + padding: 36px 16px; + text-align: center; + display: grid; + gap: 10px; + justify-items: center; +} +.empty-state p { + max-width: 480px; +} +.inline-note { + font-size: 12px; + color: var(--grey-for-text); + line-height: 1.6; +} +.state-error { + color: var(--red-dark, #aa4343); +} +.prototype-analytics .analytics { + padding: 0 0 32px; +} +.prototype-analytics .summary-card__value { + font-variant-numeric: tabular-nums; +} +.prototype-analytics .summary-card { + min-width: 0; +} +.prototype-analytics .summary-card__label { + overflow-wrap: anywhere; +} +.prototype-analytics .analytics__header { + padding: 18px 20px; +} +.prototype-analytics .analytics-card, +.prototype-analytics .attention { + min-width: 0; +} +.prototype-analytics .analytics-grid--priority { + align-items: stretch; +} +.attention-links { + list-style: none; + padding: 0; + margin: 12px 0 0; + display: grid; + gap: 8px; +} +.attention-links button { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 0; + border: 0; + background: none; + text-align: left; + font-size: 12px; + line-height: 1.6; +} +.attention-links button strong { + font-size: 16px; + font-weight: 600; + color: var(--accent); +} +.dates-list { + margin: 12px 0 0; + padding: 0; + list-style: none; + font-size: 12px; + display: grid; + gap: 8px; +} +.dates-list li { + display: flex; + justify-content: space-between; + gap: 10px; +} +.dates-list time { + color: var(--grey-for-text); +} +.funnel-bars { + display: grid; + gap: 14px; + margin-top: 18px; +} +.funnel-line { + display: grid; + grid-template-columns: 140px minmax(0, 1fr) 30px; + gap: 12px; + align-items: center; + font-size: 12px; +} +.bar-track { + height: 9px; + border-radius: 5px; + background: var(--light-gray); +} +.bar-value { + height: 100%; + border-radius: 5px; + background: var(--accent); + min-width: 0; +} +.progress-track { + height: 10px; + background: var(--light-gray); + border-radius: 8px; + margin: 14px 0; +} +.progress-track span { + display: block; + height: 100%; + background: var(--accent); + border-radius: 8px; +} +.metric-links { + display: flex; + gap: 16px; + flex-wrap: wrap; + font-size: 12px; +} +.metric-links button { + font-size: 12px; +} +.metric-links strong { + margin-right: 4px; +} +.cases-list { + display: grid; + margin-top: 14px; + gap: 0; +} +.case-heading, +.case-data { + display: grid; + grid-template-columns: minmax(0, 1.8fr) repeat(3, minmax(0, 0.7fr)); + gap: 12px; + padding: 10px 0; + font-size: 12px; + align-items: center; +} +.case-heading { + color: var(--grey-for-text); + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.case-data { + border: 0; + border-bottom: 1px solid var(--light-gray); + background: none; + text-align: left; + width: 100%; +} +.case-data span:first-child { + color: var(--accent); + overflow-wrap: anywhere; +} +.case-data--saved { + grid-template-columns: minmax(0, 1.8fr) repeat(2, minmax(0, 0.7fr)); +} +.region-row { + display: grid; + grid-template-columns: 90px minmax(0, 1fr) 26px; + gap: 12px; + align-items: center; + margin: 12px 0; + font-size: 12px; +} +.activity-bars { + display: flex; + height: 72px; + align-items: flex-end; + gap: 8px; + margin-top: 20px; +} +.activity-bars span { + flex: 1; + background: var(--accent); + border-radius: 3px 3px 0 0; +} +.activity-labels { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--grey-for-text); + margin-top: 8px; +} +.coverage-table td, +.coverage-table th { + padding: 10px 8px; +} +.exports-menu { + position: relative; +} +.exports-menu summary { + list-style: none; + cursor: pointer; +} +.exports-menu summary::-webkit-details-marker { + display: none; +} +.exports-popup { + position: absolute; + top: 100%; + right: 0; + width: 216px; + z-index: 5; + display: grid; + padding: 6px; + background: #fff; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 10px; + box-shadow: 0 4px 12px #0000000d; +} +.exports-popup button { + background: none; + border: 0; + padding: 10px 12px; + text-align: left; + font-size: 12px; +} +.exports-popup button:hover { + background: var(--light-gray); +} +dialog { + margin: auto; + padding: 20px; + border: 0; + border-radius: 15px; + background: var(--light-white, #fff); + color: var(--black); + width: min(880px, calc(100vw - 24px)); + max-width: calc(100vw - 24px); + max-height: calc(100dvh - 32px); + overflow-y: auto; + overscroll-behavior: contain; + font: + 14px/1.5 Mont, + sans-serif; +} +dialog::backdrop { + background: #0006; +} +.prototype-drilldown dialog.modal__body { + width: min(880px, calc(100vw - 24px)); + padding: 20px; +} +.modal-header { + align-items: flex-start; + margin-bottom: 18px; +} +.modal-header h2 { + font-size: 18px; + font-weight: 400; + overflow-wrap: anywhere; +} +.modal-header .muted { + margin-top: 6px; +} +.close-button { + font-size: 23px; + line-height: 1; + min-width: 32px; +} +.modal-body { + display: grid; + gap: 16px; + min-width: 0; +} +.modal-back { + margin-bottom: 14px; +} +.metrics-row { + display: flex; + gap: 24px; + flex-wrap: wrap; + padding: 10px 12px; + background: var(--light-gray); + border-radius: 8px; + font-size: 12px; +} +.metrics-row strong { + margin-left: 6px; + font-weight: 600; +} +.modal-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + border-bottom: 1px solid var(--medium-grey-for-outline); + padding-bottom: 10px; +} +.modal-tabs button { + border: 0; + background: none; + padding: 6px 0; + font-size: 12px; +} +.modal-tabs button[aria-selected="true"] { + color: var(--accent); + font-weight: 600; +} +.answer-list { + display: grid; + gap: 20px; +} +.answer h3 { + font-size: 12px; + margin-bottom: 6px; + color: var(--grey-for-text); + font-weight: 400; +} +.answer p { + font-size: 14px; + line-height: 1.6; +} +.team-row { + display: grid; + grid-template-columns: 40px minmax(0, 1fr) minmax(90px, 0.7fr); + gap: 12px; + align-items: center; + padding: 14px 0; + border-bottom: 1px solid var(--light-gray); + font-size: 12px; +} +.avatar { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: var(--light-gray); + border-radius: 50%; + color: var(--accent); + font-size: 12px; +} +.score-list { + display: grid; + gap: 0; +} +.score-row { + padding: 16px 0; + border-bottom: 1px solid var(--light-gray); +} +.score-row h3 { + font-size: 13px; + font-weight: 400; + margin-bottom: 8px; +} +.score-row strong { + font-size: 18px; +} +.score-row p { + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; +} +.allocation-summary { + display: grid; + gap: 12px; +} +.allocation-summary dl { + margin: 0; + display: grid; + grid-template-columns: 1fr auto; + gap: 12px; + font-size: 13px; +} +.allocation-summary dd { + margin: 0; + font-weight: 600; +} +.notice { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 30; + padding: 12px 20px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 12px; + background: #fff; + font-size: 12px; + max-width: calc(100vw - 32px); +} +.skeleton { + height: 20px; + width: 70%; + border-radius: 6px; + background: var(--light-gray); + margin: 8px 0; +} +.skeleton--large { + height: 100px; + width: 100%; +} +.loading-panels { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} +@media (min-width: 1000px) { + .prototype-analytics .summary { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} +@media (max-width: 999px) { + .office { + display: block; + } + .sidebar { + display: none; + } + .workspace { + padding: 16px; + } + .mobile-header { + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + font-size: 12px; + } + .mobile-header img { + width: 100px; + } + .prototype-bar { + padding: 12px 16px; + align-items: flex-start; + flex-wrap: wrap; + } + .program-header { + margin-top: 8px; + } + .filters { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .prototype-analytics .summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .prototype-analytics .analytics-grid { + grid-template-columns: 1fr; + } +} +@media (max-width: 600px) { + .prototype-controls { + width: 100%; + gap: 8px; + display: grid; + grid-template-columns: 1fr; + } + .prototype-controls label { + display: grid; + grid-template-columns: 75px minmax(0, 1fr); + } + .prototype-controls select { + width: 100%; + max-width: none; + } + .workspace { + padding: 16px; + } + h1 { + font-size: 20px; + } + .section-tabs { + gap: 4px 16px; + } + .section-tabs button { + font-size: 12px; + min-height: 40px; + } + .page-panel { + padding: 16px; + } + .toolbar-actions { + width: 100%; + } + .search-form { + min-width: 0; + flex-basis: 100%; + } + .primary, + .secondary, + .quiet { + min-height: 40px; + padding: 8px 14px; + } + .filters { + grid-template-columns: 1fr; + } + .prototype-analytics .summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .prototype-analytics .summary-card { + padding: 12px; + gap: 8px; + } + .prototype-analytics .summary-card__icon { + display: none; + } + .prototype-analytics .summary-card__value { + font-size: 24px; + } + .funnel-line { + grid-template-columns: 115px minmax(0, 1fr) 25px; + gap: 8px; + } + .case-heading, + .case-data { + gap: 8px; + grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 0.65fr)); + font-size: 11px; + } + .case-data--saved { + grid-template-columns: minmax(0, 1.6fr) repeat(2, minmax(0, 0.65fr)); + } + .data-table thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + } + .data-table, + .data-table tbody, + .data-table tr { + display: block; + width: 100%; + } + .data-table tr { + padding: 14px 0; + border-bottom: 1px solid var(--medium-grey-for-outline); + } + .data-table td { + display: grid; + grid-template-columns: 90px minmax(0, 1fr); + gap: 10px; + padding: 5px 0; + border: 0; + min-width: 0; + width: 100%; + } + .data-table td::before { + content: attr(data-label); + color: var(--grey-for-text); + font-size: 11px; + } + .data-table .name-cell { + width: 100%; + } + .data-table td.name-cell { + display: block; + font-size: 14px; + padding-top: 0; + margin-bottom: 6px; + } + .data-table td.name-cell::before { + content: none; + } + .pagination { + align-items: flex-start; + flex-wrap: wrap; + } + .coverage-table td.name-cell { + font-size: 13px; + } + .dates-list li { + display: grid; + gap: 2px; + } + .prototype-drilldown dialog.modal__body { + padding: 16px; + max-height: calc(100dvh - 24px); + } + .modal-header h2 { + font-size: 17px; + } + .team-row { + grid-template-columns: 36px minmax(0, 1fr); + gap: 10px; + } + .team-row > span:last-child { + grid-column: 2; + } + .loading-panels { + grid-template-columns: 1fr; + } + .metrics-row { + gap: 12px; + } + .metric-links { + gap: 12px; + } + .heading-row { + align-items: flex-start; + } + .exports-popup { + right: -2px; + } + .region-row { + grid-template-columns: 75px minmax(0, 1fr) 25px; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation: none !important; + transition: none !important; + } +} +@media (max-width: 600px) { + .icon-button { + min-width: 44px; + min-height: 44px; + } + .modal-header .close-button { + margin-top: -4px; + margin-right: -4px; + } +} +.modal-tabs [aria-pressed="true"] { + color: var(--accent); + font-weight: 600; +} +.admin-list-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} +.admin-list-toolbar .modal-tabs { + border: 0; + padding: 0; +} +.admin-list-toolbar .actions { + gap: 16px; +} +.admin-footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; + border-top: 1px solid var(--medium-grey-for-outline); + padding-top: 16px; +} +.admin-effects { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + margin: 0; + font-size: 13px; +} +.admin-effects dd { + margin: 0; + font-weight: 600; + overflow-wrap: anywhere; +} +.admin-effects dt { + overflow-wrap: anywhere; +} +.admin-section { + display: grid; + gap: 8px; + padding-top: 16px; + border-top: 1px solid var(--medium-grey-for-outline); +} +.admin-effect-item { + display: grid; + gap: 4px; + margin-top: 8px; + font-size: 13px; +} +.admin-effect-item strong { + font-weight: 600; +} +.admin-selection { + display: flex; + align-items: flex-start; + gap: 12px; + cursor: pointer; + min-height: 32px; +} +.admin-selection input { + flex: none; + accent-color: var(--accent); + margin-top: 4px; + width: 16px; + height: 16px; +} +.admin-history { + list-style: none; + margin: 0; + padding: 0; + display: grid; +} +.admin-history li { + display: grid; + gap: 8px; + padding: 16px 0; + border-bottom: 1px solid var(--medium-grey-for-outline); +} +.admin-history li:first-child { + padding-top: 0; +} +.admin-history li > button { + justify-self: start; +} +.admin-error { + display: grid; + gap: 8px; + border-left: 2px solid var(--red-dark, #aa4343); + padding: 12px 16px; + background: var(--light-gray); + font-size: 13px; +} +.admin-error > button { + justify-self: start; +} +.admin-preservation { + padding: 12px 16px; + background: var(--light-gray); + border-radius: 8px; +} +.admin-team-row { + grid-template-columns: 36px minmax(0, 1fr) minmax(90px, 0.7fr); +} +.admin-team-row > .link { + justify-self: start; +} +.prototype-controls .quiet { + min-height: 32px; +} +.prototype-bar { + align-items: flex-start; +} +.prototype-controls { + justify-content: flex-end; +} +@media (max-width: 600px) { + .admin-list-toolbar, + .admin-footer { + align-items: flex-start; + flex-direction: column; + } + .admin-list-toolbar .actions { + gap: 12px; + } + .admin-effects { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + } + .admin-effects dd { + margin-bottom: 8px; + } + .admin-team-row { + grid-template-columns: 36px minmax(0, 1fr); + } + .admin-team-row > .link, + .admin-team-row > .muted { + grid-column: 2; + } + .admin-selection { + align-items: center; + min-height: 44px; + } + .admin-selection input { + margin: 0; + } + .admin-footer .link, + .admin-list-toolbar .link { + min-height: 44px; + display: inline-flex; + align-items: center; + } + .prototype-controls { + justify-content: stretch; + } + .prototype-controls label { + grid-template-columns: 100px minmax(0, 1fr); + } +} diff --git a/docs/design/program-solutions-scope-20261025/prototype/public-v1/app.js b/docs/design/program-solutions-scope-20261025/prototype/public-v1/app.js new file mode 100644 index 000000000..b7852e0f0 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/public-v1/app.js @@ -0,0 +1,103 @@ +/* Только синтетические данные. Сеть и реальные API не используются. */ +(() => { + 'use strict'; + const $ = selector => document.querySelector(selector); + const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + const formats = { + noncompetitive: {name:'Практикум командных решений',label:'Неконкурсная программа',names:['Сервис поддержки студентов','Карта городских инициатив','Помощник волонтерских команд','Мастерская цифровых навыков']}, + kch: {name:'Кейс-чемпионат PROCOLLAB',label:'Кейс-чемпионат',names:['Сервис поддержки студентов','Доступный городской транспорт','Платформа развития команд','Новая модель стажировок']}, + hack: {name:'Хакатон PROCOLLAB',label:'Хакатон',names:['Помощник волонтерских команд','Городские данные: открытый API','Сервис распределения задач','Платформа цифровых навыков']} + }; + const people = [ + {user:{id:101,first_name:'Иван',last_name:'Токарев',avatar_url:null},role:'captain',specialization:'Product Manager',is_captain:true}, + {user:{id:102,first_name:'Анна',last_name:'Смирнова',avatar_url:null},role:'member',specialization:'Дизайнер',is_captain:false}, + {user:{id:103,first_name:'Дмитрий',last_name:'Иванов',avatar_url:null},role:'member',specialization:'Backend-разработчик',is_captain:false} + ]; + let format = 'noncompetitive', scenario = 'ready'; + const limit = 20; + function current() { + const q = new URLSearchParams(location.search); + return {search:q.get('search') || '',offset:Math.max(0,Number(q.get('offset'))||0),solution:q.get('solution'),tab:q.get('tab')==='team'?'team':'materials'}; + } + function url(changes) { + const q = new URLSearchParams(location.search); + for(const [key,value] of Object.entries(changes)) value===null||value==='' ? q.delete(key) : q.set(key,String(value)); + return location.pathname+(q.size?'?'+q:''); + } + function navigate(changes, focus = true) {history.pushState({},'',url(changes));render();if(focus)$('#content').focus();} + const fullName = person => [person.first_name,person.last_name].filter(Boolean).join(' ') || 'Участник'; + function cards() { + return Array.from({length:23},(_,i)=>({application:{kind:'solution',program_id:6,solution_id:501+i,name:i<4?formats[format].names[i]:`Командное решение ${i+1}`},published_at:'2026-10-25T06:00:00+03:00',captain:people[i%3].user,accepted_count:i%3+1})); + } + function title(card) {return scenario==='long' ? 'Разработка комплексного решения для поддержки студенческих команд и взаимодействия с партнерами программы в регионах России' : card.application.name;} + function searchForm(search) {return `

V!2RRr`cn3=Bz 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%0Dbgle2vT3l+3G-UW8U@w~ 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

-
-
-
-

Воронка участников

- -
- @if (hasValues(participantFunnel())) { - - } @else { -

Пока нет данных по участникам

- } -
+
+
+
+

Требует внимания

+ +
+ @if (hasValues(attention()) || data()?.attention?.projectsNotSubmitted?.applicable) { +
    + @for (item of attention(); track item.key) { +
  • + + +
  • + } +
+ } + @if (!hasValues(attention())) { +

+ Ничего не требует внимания +

+ } +
+
+
+

Сроки программы

+ +
+ @if (deadlines().length) { +
    + @for (deadline of deadlines(); track deadline.key) { +
  • +
    +

    {{ deadline.label }}

    + + @if (deadline.usesRegistrationDeadline) { + По сроку регистрации + } +
    + {{ deadline.status }} +
  • + } +
+ } @else { +

Сроки программы пока не заданы

+ } +
+
-
-
-

Воронка проектов

- -
- @if (hasValues(solutionFunnel())) { - - } @else { -

Нет решений для отображения

- } -
-
+
+
+
+

Воронка участников

+ +
+ @if (hasValues(participantFunnel())) { + + } @else { +

Пока нет данных по участникам

+ } +
-
-
-
-
-

Ход оценивания

-

{{ evaluationModeLabel() }}

+
+
+

Воронка проектов

+
- -
- -
-
+ @if (hasValues(solutionFunnel())) { + + } @else { +

Нет решений для отображения

+ } + +
-
-
-
-

Кейсы

- -
- @if (caseEmptyMessage(); as message) { -

{{ message }}

- } - @if (caseRows().length) { -
-
- @for (card of regionCards(); track card.key) { -
+
-

{{ card.title }}

- +
+

Динамика активности

+ @if (hasActivity()) { +

Последние 30 дней

+ } +
- @if (regionExportState()[card.key].pending) { - - } - @if (regionExportState()[card.key].failed) { - - } - @if (card.items.length > 0) { - + @if (hasActivity()) { + } @else { -

{{ card.emptyMessage }}

+

+ За последние 30 дней активности не было +

}
- } -
- } - - +
+ +
+ @for (card of regionCards(); track card.key) { +
+
+

{{ card.title }}

+ + +
+ @if (regionExportState()[card.key].pending) { + + } + @if (regionExportState()[card.key].failed) { + + } + @if (card.items.length > 0) { + + } @else { +

{{ card.emptyMessage }}

+ } +
+ } +
+ } + + +} diff --git a/projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.spec.ts index a9b4c9187..04200d612 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.spec.ts @@ -124,6 +124,7 @@ describe("ProgramAnalyticsComponent", () => { const error = signal(null); const analytics = { programId: signal(12), + solutionMode: signal(false), programName: signal("Тестовая программа"), scheduleDates: signal(null), data, diff --git a/projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.ts b/projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.ts index ab7f23263..2f0448093 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.ts @@ -36,6 +36,7 @@ import { AnalyticsFunnelStage, } from "./funnel-chart/funnel-chart.component"; import { AnalyticsEvaluationProgressComponent } from "./evaluation-progress/evaluation-progress.component"; +import { SolutionAnalyticsComponent } from "./solution-analytics.component"; interface AnalyticsMetric { key: string; @@ -77,6 +78,7 @@ interface AnalyticsCaseRow extends ProgramAnalyticsCaseMetrics { AnalyticsActivityChartComponent, AnalyticsFunnelChartComponent, AnalyticsEvaluationProgressComponent, + SolutionAnalyticsComponent, ], providers: [ProgramAnalyticsInfoService, ExportFileInfoService], changeDetection: ChangeDetectionStrategy.OnPush, @@ -97,6 +99,8 @@ export class ProgramAnalyticsComponent implements OnInit { private readonly pluralize = new PluralizePipe(); protected readonly data = this.analytics.data; + protected readonly solutionMode = this.analytics.solutionMode; + protected readonly scheduleDates = this.analytics.scheduleDates; protected readonly programId = this.analytics.programId; protected readonly programName = this.analytics.programName; protected readonly pending = this.analytics.pending; diff --git a/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.html b/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.html new file mode 100644 index 000000000..c0ccc1f3d --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.html @@ -0,0 +1,661 @@ + +
+
+
+

Аналитика

+ + @if (data()) { + +
+ + + @if (data()?.solutionFunnel?.submissionApplicable) { + + + } +
+ } +
+ @if (exporting()) { +

Подготавливаем Excel…

+ } + @if (exportError()) { +

{{ exportError() }}

+ } +
+ @if (pending()) { +

Загружаем аналитику программы…

+ } @else if (error()) { + + } @else if (data(); as overview) { +
+
+
+

Участники

+ {{ + overview.summary.participants.total + }} +
+
+
+
+

Заявки

+ {{ + overview.summary.solutions.total + }} +
+
+
+
+

+ {{ overview.solutionFunnel.submissionApplicable ? "Сдано" : "В командах" }} +

+ {{ + overview.solutionFunnel.submissionApplicable + ? overview.solutionFunnel.submitted + : overview.participantFunnel.withTeam + }} +
+
+
+
+

Эксперты

+ {{ overview.summary.experts.total }} +
+
+
+
+
+

Требует внимания

+
    +
  • + +
  • +
  • + +
  • + @if (overview.solutionFunnel.submissionApplicable) { +
  • + +
  • +
  • + +
  • + } +
+
+
+

Сроки программы

+
    + @for (deadline of deadlines(); track deadline.key) { +
  • +
    +

    {{ deadline.label }}

    + +
    + {{ deadline.status }} +
  • + } +
+ @if (!deadlines().length) { +

Сроки программы пока не заданы

+ } +
+
+
+
+

Воронка участников

+ +
+
+

Воронка заявок

+ + @if (!overview.solutionFunnel.submissionApplicable) { +

Для этой программы сдача и оценивание не требуются.

+ } +
+
+ @if (overview.evaluationStatus.applicable) { +
+
+

Ход оценивания

+

+ {{ + overview.evaluationStatus.mode === "distributed" + ? "Распределенное оценивание" + : "Открытое оценивание" + }} +

+ @if (overview.evaluationStatus.assignments; as totals) { +
+ + + +
+ } + @if (overview.evaluationStatus.solutions; as solutions) { +

+ Сдано: {{ solutions.submitted }} · Оценено: {{ solutions.evaluated }} · Частично + оценено: {{ solutions.partiallyEvaluated }} +

+ } + @for (expert of overview.attention.delayedExperts.items; track expert.expertId) { + + } +
+
+ } +
+
+

Кейсы

+ @if (!overview.cases.configured) { +

Кейсы не настроены для этой программы

+ } +
    + @for (row of overview.cases.items; track row.name) { +
  • + +
  • + } + @if (overview.cases.withoutCase.solutionsTotal) { +
  • + +
  • + } +
+
+
+

Материалы решений

+

+ Учитываются только поля, применимые к заявке. Число 0 и ответ «Нет» считаются + заполненными. +

+ @for (field of overview.fieldCoverage; track field.fieldId) { + + } + @if (!overview.fieldCoverage.length) { +

Поля формы пока не настроены

+ } +
+
+
+
+

Регионы участников

+ @if (overview.summary.participantRegions.items.length) { + + } @else { +

У участников пока не указаны регионы

+ } +
+
+

Динамика активности

+ @if (activityPoints().length) { + + } @else { +

За последние 30 дней активности не было

+ } +
+
+ } +
+ +
+
+

{{ title() }}

+ +
+ @if (message()) { +

{{ message() }}

+ } + @if (actionError()) { +

{{ actionError() }}

+ } + @if (preview(); as reviewed) { +

Отправить напоминание

+

+ Получателей: {{ reviewed.recipientCount }} · Без назначений: + {{ reviewed.withoutAssignmentsCount }} · Недоступно: {{ reviewed.unavailableCount }} +

+

{{ reviewed.previewText }}

+
    + @for (recipient of reviewed.recipients; track recipient.userId) { +
  • {{ recipient.fullName }}
  • + } +
+
+ +
+ } @else if (modal() === "assignments") { + @if (detailPending()) { +

Загружаем оценку…

+ } + @if (detailError()) { +

{{ detailError() }}

+ } + @if (detail(); as scores) { + +

{{ scores.application.name }} · {{ scores.expert.fullName }}

+
+ + + + + + + + + @for (criterion of scores.scores; track criterion.criterionId) { + + + + + } + +
КритерийОценка
{{ criterion.name }}{{ criterionValue(criterion) }}
+
+ } @else { + @if (listPending()) { +

Загружаем назначения…

+ } + @if (listError()) { +

{{ listError() }}

+ + } +
+ + + + + + + + + + + @for (row of visibleAssignments(); track row.assignmentId) { + + + + + + + } + +
ЭкспертРешениеСтатус
{{ row.expert.fullName }} + {{ row.application.name }} + {{ statusLabel[row.status] }} + @if (row.status === "completed") { + + } +
+
+ @if (!listPending() && !listError() && !visibleAssignments().length) { +

Назначений не найдено

+ } + } + } @else { +
+ +
+ @if (listPending()) { +

Загружаем список…

+ } + @if (listError()) { +

{{ listError() }}

+ + } + @if (!listPending() && !listError()) { +
+ + + + @if (canRemind()) { + + } + + + + + + + @if (modal() === "solutions") { + @for (row of list()?.results; track row.application.solutionId) { + + @if (canRemind()) { + + } + + + + + } + } @else { + @for (row of participants(); track row.userId) { + + + + + + + } + } + +
Выбрать{{ modal() === "solutions" ? "Решение" : "Участник" }}{{ modal() === "solutions" ? "Кейс" : "Город" }}Статус
+ + + {{ row.application.name }} + {{ row.caseName ?? "Без кейса" }} + {{ + row.evaluationStatus === "not_applicable" + ? "Без оценивания" + : row.submissionStatus === "draft" + ? "Не сдано" + : row.evaluationStatus === "evaluated" + ? "Оценено" + : "Ожидает оценки" + }} +
+ + + {{ + row.fullName + }} + {{ row.city ?? "—" }}Зарегистрирован
+
+

+ {{ count() ? offset() + 1 : 0 }}–{{ + modal() === "solutions" + ? offset() + (list()?.results?.length ?? 0) + : offset() + participants().length + }} + из {{ count() }} +

+
+ + @if (modal() === "solutions") { + + } + @if (canRemind() && count()) { + + } +
+ @if (!count()) { +

Ничего не найдено. Измените поиск.

+ } + } + } +
+
diff --git a/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.scss b/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.scss new file mode 100644 index 000000000..0b077ef50 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.scss @@ -0,0 +1,85 @@ +/** @format */ + +@use "styles/ui-foundation"; + +:host { + display: block; + min-width: 0; +} + +.ui-field { + @include ui-foundation.control; +} + +.ui-button:not(.attention__action).ui-button { + @include ui-foundation.button; +} + +.ui-choice { + @include ui-foundation.choice; +} + +.solution-analytics__actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-block: 16px; +} + +.solution-analytics__actions input { + flex: 1; + min-width: 140px; +} + +.solution-analytics__dialog { + display: grid; + gap: 16px; + max-height: 78dvh; + padding: 24px; + overflow: auto; + font-family: Mont, sans-serif; +} + +.solution-analytics__heading { + display: flex; + gap: 16px; + align-items: flex-start; + justify-content: space-between; +} + +.solution-analytics__table { + min-width: 0; + overflow-x: auto; +} + +.solution-analytics__table table { + width: 100%; + border-collapse: collapse; +} + +.solution-analytics__table th, +.solution-analytics__table td { + padding: 12px; + text-align: left; + overflow-wrap: anywhere; + border-bottom: 1px solid var(--light-gray); +} + +.solution-analytics__table th { + overflow-wrap: normal; + white-space: nowrap; +} + +.solution-analytics__sr { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); +} + +@media (max-width: 600px) { + .solution-analytics__dialog { + padding: 16px; + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.spec.ts new file mode 100644 index 000000000..e7ac71590 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.spec.ts @@ -0,0 +1,182 @@ +/** @format */ +import { TestBed } from "@angular/core/testing"; +import { of, Subject } from "rxjs"; +import { ok } from "@domain/shared/result.type"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { + SolutionManagerAnalytics, + SolutionManagerSolutions, + SolutionReminderPreview, +} from "@domain/solution/solution-contract.model"; +import { SolutionAnalyticsComponent } from "./solution-analytics.component"; +import { + GetSolutionManagerAnalyticsUseCase, + GetSolutionManagerSolutionsUseCase, + GetSolutionManagerAssignmentsUseCase, + GetSolutionManagerAssignmentUseCase, + ExportSolutionAnalyticsUseCase, + PreviewSolutionExpertReminderUseCase, + ConfirmSolutionExpertReminderUseCase, +} from "@api/solution/use-cases/solution-manager-analytics.use-cases"; +import { GetProgramManagerParticipantsWithoutTeamUseCase } from "@api/program/use-cases/get-program-manager-participants-without-team.use-case"; +import { GetProgramManagerParticipantsWithoutVisitUseCase } from "@api/program/use-cases/get-program-manager-participants-without-visit.use-case"; +import { ProgramAttentionActionsUseCase } from "@api/program/use-cases/program-attention-actions.use-case"; +import { saveAs } from "file-saver"; +vi.mock("file-saver", () => ({ saveAs: vi.fn() })); +function value(operationId: string): T { + return structuredClone( + solutionContractScenarios.find(s => s.operationId === operationId && s.status === 200)! + .response, + ) as T; +} +function setup() { + const overview = { + execute: vi.fn(() => of(ok(value("getSolutionManagerAnalytics")))), + }; + const list = { + execute: vi.fn(() => of(ok(value("getSolutionManagerSolutions")))), + }; + const preview = { + execute: vi.fn(() => of(ok(value("previewSolutionExpertReminder")))), + }; + const confirm = { + execute: vi.fn( + () => new Subject>>(), + ), + }; + const exportApi = { execute: vi.fn(() => of(ok(new Blob(["xlsx"])))) }; + const unused = { execute: vi.fn(() => of(ok({ results: [] }))), preview: vi.fn(), send: vi.fn() }; + TestBed.configureTestingModule({ + imports: [SolutionAnalyticsComponent], + providers: [ + { provide: GetSolutionManagerAnalyticsUseCase, useValue: overview }, + { provide: GetSolutionManagerSolutionsUseCase, useValue: list }, + { provide: GetSolutionManagerAssignmentsUseCase, useValue: unused }, + { provide: GetSolutionManagerAssignmentUseCase, useValue: unused }, + { provide: ExportSolutionAnalyticsUseCase, useValue: exportApi }, + { provide: PreviewSolutionExpertReminderUseCase, useValue: preview }, + { provide: ConfirmSolutionExpertReminderUseCase, useValue: confirm }, + { provide: GetProgramManagerParticipantsWithoutTeamUseCase, useValue: unused }, + { provide: GetProgramManagerParticipantsWithoutVisitUseCase, useValue: unused }, + { provide: ProgramAttentionActionsUseCase, useValue: unused }, + ], + }); + TestBed.overrideComponent(SolutionAnalyticsComponent, { set: { template: "" } }); + const fixture = TestBed.createComponent(SolutionAnalyticsComponent); + fixture.componentRef.setInput("programId", 12); + fixture.detectChanges(); + return { + fixture, + component: fixture.componentInstance, + overview, + list, + preview, + confirm, + exportApi, + }; +} +describe("Solution organizer analytics integration", () => { + it("reads the authoritative solution overview and preserves null applicability", () => { + const { component, overview } = setup(); + expect(overview.execute).toHaveBeenCalledExactlyOnceWith(12); + const data = value("getSolutionManagerAnalytics"); + component.data.set({ + ...data, + solutionFunnel: { + ...data.solutionFunnel, + submissionApplicable: false, + submitted: null, + evaluated: null, + }, + }); + expect(component.solutionFunnel().map(item => item.key)).toEqual(["created"]); + expect(component.data()?.solutionFunnel.submitted).toBeNull(); + }); + it("exports the same case/field/search filters without page limits or Project IDs", () => { + const { component, list, exportApi } = setup(); + const filters = { + caseScope: "selected" as const, + caseName: "Город", + fieldId: 41, + answerState: "missing" as const, + }; + component.openSolutions("Материалы", filters, document.createElement("button")); + component.search.setValue(" Кейс "); + component.applySearch(); + component.download("solutions", true); + expect(list.execute).toHaveBeenLastCalledWith(12, { + ...filters, + search: "Кейс", + limit: 20, + offset: 0, + }); + expect(exportApi.execute).toHaveBeenCalledExactlyOnceWith(12, { + kind: "solutions", + ...filters, + search: "Кейс", + }); + expect(saveAs).toHaveBeenCalled(); + }); + it("drops stale list responses and all selection when the program changes", () => { + const { fixture, component, list } = setup(); + const old = new Subject>>(); + list.execute.mockReturnValue(old); + component.openSolutions("Решения", {}, document.createElement("button")); + component.toggle(301); + fixture.componentRef.setInput("programId", 13); + fixture.detectChanges(); + old.next(ok(value("getSolutionManagerSolutions"))); + old.complete(); + expect(component.modal()).toBeNull(); + expect(component.list()).toBeNull(); + expect(component.selectedCount()).toBe(0); + }); + it("uses solution IDs for reminder review and sends only the reviewed token once", () => { + const { component, preview, confirm } = setup(); + component.data.update(data => ({ + ...data!, + evaluationStatus: { ...data!.evaluationStatus, mode: "distributed" }, + })); + component.openSolutions( + "Ожидают", + { scope: "awaiting_evaluation" }, + document.createElement("button"), + ); + component.toggle(301); + component.previewReminder(); + expect(preview.execute).toHaveBeenCalledExactlyOnceWith(12, { + search: "", + selection: { mode: "selected", ids: [301], excludedIds: [] }, + }); + const token = component.preview()!.token; + component.confirmReminder(); + component.confirmReminder(); + component.close(); + expect(confirm.execute).toHaveBeenCalledExactlyOnceWith(12, token); + expect(component.modal()).toBe("solutions"); + }); + it("changing selection cancels old reminder preview without sending", () => { + const { component, preview, confirm } = setup(); + const old = new Subject>>(); + preview.execute.mockReturnValue(old); + component.data.update(data => ({ + ...data!, + evaluationStatus: { ...data!.evaluationStatus, mode: "distributed" }, + })); + component.openSolutions( + "Ожидают", + { scope: "awaiting_evaluation" }, + document.createElement("button"), + ); + component.toggle(301); + component.previewReminder(); + expect(component.actionPending()).toBe(true); + component.toggle(302); + expect(component.selection().ids).toEqual([301]); + old.next(ok(value("previewSolutionExpertReminder"))); + old.complete(); + component.toggle(302); + component.confirmReminder(); + expect(confirm.execute).not.toHaveBeenCalled(); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.ts b/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.ts new file mode 100644 index 000000000..818775d4c --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.ts @@ -0,0 +1,506 @@ +/** @format */ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + computed, + effect, + inject, + input, + signal, + untracked, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { RouterLink } from "@angular/router"; +import { Observable, Subject, takeUntil, timer } from "rxjs"; +import { CdkMenuModule } from "@angular/cdk/menu"; +import { IconComponent } from "@ui/primitives/icon/icon.component"; +import { AnalyticsActivityChartComponent } from "./activity-chart/activity-chart.component"; +import { Result } from "@domain/shared/result.type"; +import { saveAs } from "file-saver"; +import { A11yModule } from "@angular/cdk/a11y"; +import { ButtonDirective, ChoiceDirective, FieldDirective, TableDirective } from "@uilib"; +import { AppRoutes } from "@api/paths/app-routes"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; +import { AnalyticsFunnelChartComponent } from "./funnel-chart/funnel-chart.component"; +import { AnalyticsRegionChartComponent } from "./region-chart/region-chart.component"; +import { programDeadlines, ProgramScheduleDates } from "@utils/program-deadlines"; +import { + GetSolutionManagerAnalyticsUseCase, + GetSolutionManagerSolutionsUseCase, + GetSolutionManagerAssignmentsUseCase, + GetSolutionManagerAssignmentUseCase, + ExportSolutionAnalyticsUseCase, + PreviewSolutionExpertReminderUseCase, + ConfirmSolutionExpertReminderUseCase, +} from "@api/solution/use-cases/solution-manager-analytics.use-cases"; +import { GetProgramManagerParticipantsWithoutTeamUseCase } from "@api/program/use-cases/get-program-manager-participants-without-team.use-case"; +import { GetProgramManagerParticipantsWithoutVisitUseCase } from "@api/program/use-cases/get-program-manager-participants-without-visit.use-case"; +import { ProgramAttentionActionsUseCase } from "@api/program/use-cases/program-attention-actions.use-case"; +import { ProgramAnalyticsAttentionParticipant } from "@domain/program/program-analytics-attention.model"; +import { + SolutionManagerAnalytics, + SolutionManagerSolutions, + SolutionManagerAssignment, + SolutionManagerAssignmentDetail, + SolutionReminderPreview, +} from "@domain/solution/solution-contract.model"; +import { SolutionManagerSolutionFilters } from "@domain/solution/ports/solution.repository.port"; +import { ProgramAttentionPreview } from "@domain/program/program-attention-actions.model"; +import { + assignmentCriterionValue, + assignmentStatusLabels, +} from "@utils/program-analytics-assignment"; + +type View = + | "solutions" + | "assignments" + | "participants-without-team" + | "participants-without-visit"; +@Component({ + selector: "app-solution-analytics", + imports: [ + ReactiveFormsModule, + RouterLink, + A11yModule, + ButtonDirective, + ChoiceDirective, + FieldDirective, + TableDirective, + ModalComponent, + AnalyticsFunnelChartComponent, + AnalyticsRegionChartComponent, + AnalyticsActivityChartComponent, + CdkMenuModule, + IconComponent, + ], + templateUrl: "./solution-analytics.component.html", + styleUrls: ["./analytics.component.scss", "./solution-analytics.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionAnalyticsComponent { + readonly programId = input.required(); + readonly dates = input(null); + private readonly overviewApi = inject(GetSolutionManagerAnalyticsUseCase); + private readonly solutionsApi = inject(GetSolutionManagerSolutionsUseCase); + private readonly assignmentsApi = inject(GetSolutionManagerAssignmentsUseCase); + private readonly detailApi = inject(GetSolutionManagerAssignmentUseCase); + private readonly exportApi = inject(ExportSolutionAnalyticsUseCase); + private readonly previewApi = inject(PreviewSolutionExpertReminderUseCase); + private readonly confirmApi = inject(ConfirmSolutionExpertReminderUseCase); + private readonly withoutTeam = inject(GetProgramManagerParticipantsWithoutTeamUseCase); + private readonly withoutVisit = inject(GetProgramManagerParticipantsWithoutVisitUseCase); + private readonly participantActions = inject(ProgramAttentionActionsUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly cancel = new Subject(); + private readonly cancelList = new Subject(); + private readonly cancelAction = new Subject(); + private readonly cancelDetail = new Subject(); + private readonly cancelExport = new Subject(); + private trigger: HTMLElement | null = null; + readonly data = signal(null); + readonly pending = signal(false); + readonly error = signal(""); + readonly exporting = signal(false); + readonly exportError = signal(""); + readonly modal = signal(null); + readonly title = signal(""); + readonly query = signal({}); + readonly search = new FormControl("", { nonNullable: true }); + readonly appliedSearch = signal(""); + readonly offset = signal(0); + readonly list = signal(null); + readonly participants = signal([]); + readonly participantCount = signal(0); + readonly assignments = signal([]); + readonly assignmentScope = signal<"all" | "completed" | "pending">("all"); + readonly selectedExpert = signal(null); + readonly detail = signal(null); + readonly listPending = signal(false); + readonly listError = signal(""); + readonly detailPending = signal(false); + readonly detailError = signal(""); + readonly selection = signal<{ mode: "selected" | "all"; ids: number[]; excludedIds: number[] }>({ + mode: "selected", + ids: [], + excludedIds: [], + }); + readonly preview = signal(null); + readonly actionPending = signal(false); + readonly actionError = signal(""); + readonly message = signal(""); + readonly paths = AppRoutes; + private readonly currentTime = signal(Date.now()); + // The second chart is absent when submission is not applicable; raw API nulls remain in data(). + readonly activityPoints = computed(() => + (this.data()?.activity ?? []).map(point => ({ + ...point, + submittedSolutions: point.submittedSolutions ?? 0, + })), + ); + readonly statusLabel = assignmentStatusLabels; + readonly criterionValue = assignmentCriterionValue; + readonly deadlines = computed(() => + programDeadlines( + this.dates(), + this.data()?.solutionFunnel.submissionApplicable ?? false, + new Date(this.currentTime()), + ), + ); + readonly count = computed(() => + this.modal() === "solutions" ? (this.list()?.count ?? 0) : this.participantCount(), + ); + readonly selectedCount = computed(() => + this.selection().mode === "all" + ? Math.max(0, this.count() - this.selection().excludedIds.length) + : this.selection().ids.length, + ); + readonly visibleAssignments = computed(() => + this.assignments().filter( + a => this.selectedExpert() === null || a.expert.expertId === this.selectedExpert(), + ), + ); + readonly participantFunnel = computed(() => { + const f = this.data()?.participantFunnel; + return f + ? [ + { + key: "registered", + label: "Зарегистрировались", + value: f.uniqueParticipants, + tooltip: "Уникальные регистрации программы.", + axisLabel: ["Регистрация"], + }, + { + key: "visited", + label: "Заходили на платформу", + value: f.visitedParticipants, + tooltip: "Зафиксированный вход или активность.", + axisLabel: ["Заходили на", "платформу"], + }, + { + key: "team", + label: "Вступили в команду", + value: f.withTeam, + tooltip: "Капитаны и принятые участники активных заявок. Приглашения не учитываются.", + axisLabel: ["Вступили", "в команду"], + }, + ] + : []; + }); + readonly solutionFunnel = computed(() => { + const f = this.data()?.solutionFunnel; + return f + ? [ + { + key: "created", + label: "Создано", + value: f.created, + tooltip: "Активные заявки программы.", + axisLabel: ["Создано"], + }, + ...(f.submissionApplicable + ? [ + { + key: "submitted", + label: "Сдано", + value: f.submitted, + tooltip: "Сданные решения.", + axisLabel: ["Сдано"], + }, + { + key: "evaluated", + label: "Оценено", + value: f.evaluated, + tooltip: "Завершенность определяет сервер по правилам оценивания программы.", + axisLabel: ["Оценено"], + }, + ] + : []), + ] + : []; + }); + constructor() { + timer(60_000, 60_000) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.currentTime.set(Date.now())); + effect(() => { + this.programId(); + untracked(() => { + this.close(true); + this.load(); + }); + }); + } + load(): void { + this.cancel.next(); + this.cancelExport.next(); + this.exporting.set(false); + this.exportError.set(""); + this.pending.set(true); + this.error.set(""); + this.data.set(null); + this.overviewApi + .execute(this.programId()) + .pipe(takeUntil(this.cancel), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.pending.set(false); + if (result.ok && result.value.programId === this.programId()) this.data.set(result.value); + else this.error.set("Не удалось загрузить аналитику этой программы."); + }); + } + download(kind: "program" | "solutions" | "submitted" | "scores", filtered = false): void { + if (this.exporting()) return; + this.exporting.set(true); + this.exportError.set(""); + const query = + filtered && kind === "solutions" + ? { kind, ...this.query(), search: this.appliedSearch() } + : { kind }; + this.exportApi + .execute(this.programId(), query) + .pipe( + takeUntil(this.cancel), + takeUntil(this.cancelExport), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(result => { + this.exporting.set(false); + if (result.ok) saveAs(result.value, `program-${this.programId()}-${kind}.xlsx`); + else this.exportError.set("Не удалось подготовить Excel. Повторите попытку."); + }); + } + openSolutions(title: string, query: SolutionManagerSolutionFilters, trigger: HTMLElement): void { + if (!this.start("solutions", title, trigger)) return; + this.query.set(query); + this.loadList(); + } + openParticipants( + view: "participants-without-team" | "participants-without-visit", + trigger: HTMLElement, + ): void { + if ( + !this.start( + view, + view === "participants-without-team" + ? "Участники без команды" + : "Участники еще не заходили на платформу", + trigger, + ) + ) + return; + this.loadList(); + } + openAssignments( + scope: "all" | "completed" | "pending", + trigger: HTMLElement, + expertId: number | null = null, + ): void { + if (!this.start("assignments", "Назначения экспертов", trigger)) return; + this.assignmentScope.set(scope); + this.selectedExpert.set(expertId); + this.loadList(); + } + private start(view: View, title: string, trigger: HTMLElement): boolean { + if (this.modal() || this.actionPending()) return false; + this.close(true); + this.trigger = trigger; + this.modal.set(view); + this.title.set(title); + return true; + } + applySearch(): void { + if (this.actionPending()) return; + this.resetSelection(); + this.appliedSearch.set(this.search.value.trim()); + this.offset.set(0); + this.loadList(); + } + changePage(step: number): void { + const next = this.offset() + step * 20; + if (this.listPending() || this.actionPending() || next < 0 || next >= this.count()) return; + this.offset.set(next); + this.loadList(); + } + loadList(): void { + if (!this.modal()) return; + this.cancelList.next(); + this.cancelDetail.next(); + this.detail.set(null); + this.listPending.set(true); + this.listError.set(""); + this.list.set(null); + this.participants.set([]); + this.assignments.set([]); + const id = this.programId(), + query = { search: this.appliedSearch(), limit: 20, offset: this.offset() }; + if (this.modal() === "solutions") { + this.solutionsApi + .execute(id, { ...this.query(), ...query }) + .pipe(takeUntil(this.cancelList), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.listPending.set(false); + if (result.ok) this.list.set(result.value); + else this.listError.set("Не удалось загрузить решения."); + }); + } else if (this.modal() === "assignments") { + this.assignmentsApi + .execute(id, { scope: this.assignmentScope() }) + .pipe(takeUntil(this.cancelList), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.listPending.set(false); + if (result.ok) this.assignments.set(result.value.results); + else this.listError.set("Не удалось загрузить назначения."); + }); + } else { + const api = + this.modal() === "participants-without-team" ? this.withoutTeam : this.withoutVisit; + api + .execute(id, query) + .pipe(takeUntil(this.cancelList), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.listPending.set(false); + if (result.ok) { + this.participants.set(result.value.results); + this.participantCount.set(result.value.count); + } else this.listError.set("Не удалось загрузить участников."); + }); + } + } + showScores(row: SolutionManagerAssignment): void { + if (row.status !== "completed" || !this.assignments().includes(row)) return; + this.cancelDetail.next(); + this.detail.set(null); + this.detailPending.set(true); + this.detailError.set(""); + this.detailApi + .execute(this.programId(), row.assignmentId) + .pipe(takeUntil(this.cancelDetail), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.detailPending.set(false); + if (result.ok) this.detail.set(result.value); + else this.detailError.set("Не удалось загрузить оценку."); + }); + } + backToAssignments(): void { + this.cancelDetail.next(); + this.detail.set(null); + this.detailPending.set(false); + this.detailError.set(""); + } + isSelected(id: number): boolean { + return this.selection().mode === "all" + ? !this.selection().excludedIds.includes(id) + : this.selection().ids.includes(id); + } + toggle(id: number): void { + if (this.actionPending()) return; + this.preview.set(null); + this.actionError.set(""); + const s = this.selection(), + key = s.mode === "all" ? "excludedIds" : "ids"; + this.selection.set({ + ...s, + [key]: s[key].includes(id) ? s[key].filter(value => value !== id) : [...s[key], id], + }); + } + selectAll(): void { + if (!this.actionPending()) { + this.preview.set(null); + this.selection.set({ mode: "all", ids: [], excludedIds: [] }); + } + } + resetSelection(): void { + this.cancelAction.next(); + this.selection.set({ mode: "selected", ids: [], excludedIds: [] }); + this.preview.set(null); + this.actionError.set(""); + } + canRemind(): boolean { + return ( + this.modal() !== "assignments" && + (this.modal() !== "solutions" || + (this.query().scope === "awaiting_evaluation" && + this.data()?.evaluationStatus.mode === "distributed")) + ); + } + previewReminder(): void { + if (!this.canRemind() || !this.selectedCount() || this.actionPending() || this.listPending()) + return; + this.cancelAction.next(); + this.preview.set(null); + this.actionPending.set(true); + this.actionError.set(""); + const command = { search: this.appliedSearch(), selection: this.selection() }; + const request: Observable> = + this.modal() === "solutions" + ? this.previewApi.execute(this.programId(), command) + : this.participantActions.preview( + this.programId(), + this.modal() as "participants-without-team" | "participants-without-visit", + command, + ); + request + .pipe(takeUntil(this.cancelAction), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.actionPending.set(false); + if (result.ok) this.preview.set(result.value); + else this.actionError.set("Не удалось подготовить напоминание. Повторите попытку."); + }); + } + confirmReminder(): void { + const token = this.preview()?.token; + if (!token || this.actionPending()) return; + this.actionPending.set(true); + this.actionError.set(""); + const request: Observable> = + this.modal() === "solutions" + ? this.confirmApi.execute(this.programId(), token) + : this.participantActions.send( + this.programId(), + this.modal() as "participants-without-team" | "participants-without-visit", + token, + ); + request + .pipe(takeUntil(this.cancelAction), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.actionPending.set(false); + this.preview.set(null); + if (result.ok) { + this.resetSelection(); + this.message.set( + `Напоминания созданы: ${result.value.createdCount}. Пропущено: ${result.value.skippedCount}.`, + ); + } else + this.actionError.set( + "Напоминание не отправлено. Обновите список и подготовьте его заново.", + ); + }); + } + close(force = false): void { + if (this.actionPending() && !force) return; + this.cancelList.next(); + this.cancelAction.next(); + this.cancelDetail.next(); + this.actionPending.set(false); + this.cancelExport.next(); + this.exporting.set(false); + this.modal.set(null); + this.list.set(null); + this.participants.set([]); + this.participantCount.set(0); + this.assignments.set([]); + this.detail.set(null); + this.detailPending.set(false); + this.detailError.set(""); + this.search.setValue(""); + this.appliedSearch.set(""); + this.offset.set(0); + this.query.set({}); + this.message.set(""); + this.resetSelection(); + if (!force && this.trigger?.isConnected) this.trigger.focus(); + this.trigger = null; + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.html b/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.html new file mode 100644 index 000000000..9ddc821fc --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.html @@ -0,0 +1,417 @@ + +
+ + @if (message()) { +

{{ message() }}

+ } + @if (error()) { +
+ {{ error() }} + +
+ } + @if (loading()) { +

Загружаем экспертов и решения…

+ } +
+
+

{{ tab() === "experts" ? "Эксперты программы" : "Распределение экспертов" }}

+
+ @if (tab() === "experts") { + + } @else { + + } +
+
+
+ + @if (tab() === "experts") { + + } @else { + + } +
+ @if (tab() === "experts" && selectedExperts().size) { +
+ Выбрано: {{ selectedExperts().size }} +
+ } +
+ + @if (tab() === "experts") { + + + + + + + + + + + + + + @for (expert of filtered(); track expert.id) { + + + + + + + + + + + } + + } @else { + + + + + + + + + + + @for (row of filteredSolutions(); track row.application.solutionId) { + + + + + + + + } + + } +
ВыбратьЭкспертКейсыСтатусНазначеноОцененоДействия
+ + +
+ {{ expert.name || expert.email }} +
+
{{ expert.allCases ? "Все кейсы" : expert.caseNames?.join(", ") }} + {{ + expert.status === "joined" + ? "В программе" + : expert.status === "pending" + ? "Ожидает подтверждения" + : expert.status === "expired" + ? "Срок приглашения истек" + : "Приглашение отозвано" + }} + @if (expert.deliveryStatus === "failed") { + Не удалось отправить письмо + } @else if (expert.deliveryStatus === "queued") { + Письмо в очереди + } + {{ expert.assigned ?? "—" }}{{ expert.completed ?? "—" }} + +
РешениеКейсНазначеноОцененоЭксперты
+ {{ row.application.name }} + {{ row.caseName ?? "Без кейса" }}{{ counts(row.application.solutionId).assigned }} из {{ required() }}{{ counts(row.application.solutionId).completed }} + @for (assignment of assignments(); track assignment.assignmentId) { + @if (assignment.application.solutionId === row.application.solutionId) { + {{ assignment.expert.fullName + }}{{ assignment.status === "completed" ? " · оценено" : "" }} + } + } +
+
+ @if (!loading() && (tab() === "experts" ? !filtered().length : !filteredSolutions().length)) { +

+ {{ + tab() === "experts" + ? "Экспертов не найдено" + : "Сданных решений для распределения не найдено" + }} +

+ } +
+
+
+
+

+ {{ + modal() === "invite" + ? "Пригласить эксперта" + : modal() === "scope" + ? "Кейсы эксперта" + : modal() === "quota" + ? "Число оценщиков" + : "Назначить решения" + }} +

+ +
+ @if (dialogError()) { +

{{ dialogError() }}

+ + } + @if (modal() === "invite" || modal() === "scope") { + @if (modal() === "invite") { + + @if (email.touched && email.invalid) { +

Введите корректный email эксперта.

+ } + } + + @for (name of cases(); track name) { + + } + + + } @else if (modal() === "assignments") { +

+ {{ selected()?.name || selected()?.email }}. Сохраненные оценки защищены; их назначения + снять нельзя. +

+ @if (options(); as loaded) { + @for (solution of loaded.solutions; track solution.solutionId) { + + } + + } @else if (!dialogError()) { +

Загружаем решения…

+ } + } @else if (modal() === "quota") { + + @if (quotaPreview(); as p) { +

+ Готовых решений: {{ p.readyBefore }} → {{ p.readyAfter }}. Не хватает назначений: + {{ p.missingAssignments }}. Лишних назначений: {{ p.surplusAssignments }}. Сохраненные + оценки и назначения остаются. +

+ + } @else { + + } + } +
+ diff --git a/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.spec.ts new file mode 100644 index 000000000..be93d0c46 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.spec.ts @@ -0,0 +1,204 @@ +/** @format */ +import { TestBed } from "@angular/core/testing"; +import { of, Subject } from "rxjs"; +import { ok, fail } from "@domain/shared/result.type"; +import { SolutionExpertsComponent } from "./solution-experts.component"; +import { EvaluationUseCase } from "@api/program/use-cases/evaluation.use-case"; +import { + GetSolutionExpertAssignmentsUseCase, + SaveSolutionExpertAssignmentsUseCase, + PreviewSolutionQuotaUseCase, + SaveSolutionQuotaUseCase, +} from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { + GetSolutionManagerAssignmentsUseCase, + GetSolutionManagerSolutionsUseCase, +} from "@api/solution/use-cases/solution-manager-analytics.use-cases"; +import { SolutionAssignmentOptions, SolutionQuota } from "@domain/solution/solution-contract.model"; +const option = (id: number, assigned: boolean, protectedScore = false) => ({ + solutionId: id, + name: `Решение ${id}`, + caseName: "Город", + submitted: true, + assigned, + protected: protectedScore, + isCompleted: false, + ownTeam: false, + eligible: true, +}); +function setup() { + const expert = { + id: 11, + userId: 7, + name: "Эксперт", + email: "fixture@example.test", + status: "joined", + assigned: 999, + completed: 999, + remaining: 999, + allCases: true, + caseNames: [], + }; + const roster = { + workspace: vi.fn(() => + of({ + revision: "r1", + experts: [expert], + cases: ["Город", "Образование"], + requiredEvaluators: 1, + }), + ), + saveCaseScope: vi.fn(() => of({})), + }; + const assignments = { + execute: vi.fn(() => + of( + ok({ + results: [ + { + assignmentId: 1, + expert: { expertId: 11 }, + application: { solutionId: 301 }, + status: "pending", + }, + ], + }), + ), + ), + }; + const solutions = { execute: vi.fn(() => of(ok({ count: 0, results: [] }))) }; + const options = { + execute: vi.fn(() => + of( + ok({ + programId: 12, + expertId: 11, + revision: "r1", + solutions: [option(301, true, true), option(302, false)], + } satisfies SolutionAssignmentOptions), + ), + ), + }; + const save = { execute: vi.fn(() => new Subject>()) }; + const quota = { + execute: vi.fn(() => of(ok({ revision: "r1", requiredEvaluators: 2 } as SolutionQuota))), + }; + TestBed.configureTestingModule({ + imports: [SolutionExpertsComponent], + providers: [ + { provide: EvaluationUseCase, useValue: roster }, + { provide: GetSolutionManagerAssignmentsUseCase, useValue: assignments }, + { provide: GetSolutionManagerSolutionsUseCase, useValue: solutions }, + { provide: GetSolutionExpertAssignmentsUseCase, useValue: options }, + { provide: SaveSolutionExpertAssignmentsUseCase, useValue: save }, + { provide: PreviewSolutionQuotaUseCase, useValue: quota }, + { provide: SaveSolutionQuotaUseCase, useValue: save }, + ], + }); + TestBed.overrideComponent(SolutionExpertsComponent, { set: { template: "" } }); + const fixture = TestBed.createComponent(SolutionExpertsComponent); + fixture.componentRef.setInput("programId", 12); + fixture.componentRef.setInput("confirmDiscard", async () => true); + fixture.detectChanges(); + return { + fixture, + c: fixture.componentInstance, + expert: fixture.componentInstance.experts()[0], + roster, + assignments, + options, + save, + quota, + }; +} +describe("Solution expert management", () => { + it("ignores every Project counter in the existing expert roster", () => { + const { c } = setup(); + expect(c.experts()[0]).toMatchObject({ assigned: 1, completed: 0, remaining: 1 }); + expect(c.counts(301)).toEqual({ assigned: 1, completed: 0 }); + }); + it("retains scored assignments and saves explicit solution IDs exactly once", () => { + const { c, expert, save } = setup(); + c.open("assignments", expert); + c.toggleSolution(301); + c.toggleSolution(302); + c.saveAssignments(); + c.saveAssignments(); + expect(save.execute).toHaveBeenCalledExactlyOnceWith(12, 11, { + revision: "r1", + solutionIds: [301, 302], + }); + expect(c.busy()).toBe(true); + }); + it("cancels old options when changing program and clears the selected roster", () => { + const { fixture, c, expert, options } = setup(); + const old = new Subject>>(); + options.execute.mockReturnValue(old); + c.open("assignments", expert); + fixture.componentRef.setInput("programId", 13); + fixture.detectChanges(); + old.next(ok({ programId: 12, expertId: 11, revision: "old", solutions: [option(999, true)] })); + old.complete(); + expect(c.options()).toBeNull(); + expect(c.modal()).toBeNull(); + }); + it("does not apply a quota preview after the number changes", () => { + const { fixture, c, save } = setup(); + c.open("quota"); + c.quota.setValue(2); + c.previewQuota(); + c.quota.setValue(3); + fixture.detectChanges(); + c.saveQuota(); + expect(save.execute).not.toHaveBeenCalled(); + }); + it("unchecking one case from all cases keeps the other cases", () => { + const { c } = setup(); + c.open("invite"); + c.toggleScope("Город"); + expect(c.scopeAll()).toBe(false); + expect([...c.scopeCases()]).toEqual(["Образование", null]); + }); + it("asks about unsaved changes only after changing the reviewed values", () => { + const { c, expert } = setup(); + c.open("scope", expert); + expect(c.dirty()).toBe(false); + c.toggleScope("Город"); + expect(c.dirty()).toBe(true); + c.discard(); + c.open("assignments", expert); + expect(c.dirty()).toBe(false); + c.toggleSolution(302); + expect(c.dirty()).toBe(true); + c.discard(); + c.open("quota"); + expect(c.dirty()).toBe(false); + c.quota.setValue(2); + TestBed.flushEffects(); + expect(c.dirty()).toBe(true); + }); + it("refreshes a rejected assignment revision while retaining valid choices and without automatic apply", () => { + const { c, expert, save, options } = setup(); + c.open("assignments", expert); + c.toggleSolution(302); + save.execute.mockReturnValue( + of(fail({ code: "stale_assignments", detail: "Изменилось", errors: [], meta: {} })), + ); + c.saveAssignments(); + expect(c.options()).toBeNull(); + options.execute.mockReturnValue( + of( + ok({ + programId: 12, + expertId: 11, + revision: "r2", + solutions: [option(301, true, true), option(302, false)], + }), + ), + ); + c.refreshDialog(); + expect([...c.selectedSolutions()]).toEqual([301, 302]); + expect(c.options()?.revision).toBe("r2"); + expect(save.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.ts b/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.ts new file mode 100644 index 000000000..036419491 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.ts @@ -0,0 +1,643 @@ +/** @format */ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + computed, + effect, + inject, + input, + output, + signal, + untracked, +} from "@angular/core"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; +import { FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; +import { RouterLink } from "@angular/router"; +import { + catchError, + EMPTY, + exhaustMap, + filter, + forkJoin, + map, + Observable, + Subject, + takeUntil, + timer, +} from "rxjs"; +import { A11yModule } from "@angular/cdk/a11y"; +import { + AvatarComponent, + ButtonDirective, + ChoiceDirective, + FieldDirective, + TableDirective, +} from "@uilib"; +import { + RowAction, + RowActionEvent, + RowActionsComponent, +} from "@ui/primitives/row-actions/row-actions.component"; +import { presentationHref } from "@utils/program-case-analytics"; +import { AppRoutes } from "@api/paths/app-routes"; +import { EvaluationUseCase } from "@api/program/use-cases/evaluation.use-case"; +import { + GetSolutionExpertAssignmentsUseCase, + SaveSolutionExpertAssignmentsUseCase, + PreviewSolutionQuotaUseCase, + SaveSolutionQuotaUseCase, +} from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { + GetSolutionManagerAssignmentsUseCase, + GetSolutionManagerSolutionsUseCase, +} from "@api/solution/use-cases/solution-manager-analytics.use-cases"; +import { ExpertRow } from "@domain/program/evaluation-workspace.model"; +import { + SolutionAssignmentOptions, + SolutionManagerAssignment, + SolutionManagerSolutionRow, + SolutionQuota, +} from "@domain/solution/solution-contract.model"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; +import { SolutionAllocationComponent } from "../solution-applications/solution-allocation.component"; +import { exportExperts } from "@utils/export-experts"; + +/** Program experts keep their existing invitation/case-scope API. Assignment IDs and counters come only from solutions. */ +@Component({ + selector: "app-solution-experts", + imports: [ + ReactiveFormsModule, + RouterLink, + A11yModule, + ButtonDirective, + ChoiceDirective, + FieldDirective, + TableDirective, + AvatarComponent, + RowActionsComponent, + ModalComponent, + SolutionAllocationComponent, + ], + templateUrl: "./solution-experts.component.html", + styleUrls: [ + "./experts.component.scss", + "../../detail/analytics/solution-analytics.component.scss", + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionExpertsComponent { + readonly programId = input.required(); + readonly confirmDiscard = input.required<() => Promise>(); + readonly statusChange = output<{ dirty: boolean; busy: boolean }>(); + private readonly roster = inject(EvaluationUseCase); + private readonly assignmentsApi = inject(GetSolutionManagerAssignmentsUseCase); + private readonly solutionsApi = inject(GetSolutionManagerSolutionsUseCase); + private readonly optionsApi = inject(GetSolutionExpertAssignmentsUseCase); + private readonly saveApi = inject(SaveSolutionExpertAssignmentsUseCase); + private readonly quotaApi = inject(PreviewSolutionQuotaUseCase); + private readonly saveQuotaApi = inject(SaveSolutionQuotaUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly cancel = new Subject(); + private readonly cancelDialog = new Subject(); + readonly experts = signal([]); + readonly assignments = signal([]); + readonly solutions = signal([]); + readonly revision = signal(""); + readonly required = signal(1); + readonly loading = signal(false); + readonly busy = signal(false); + readonly allocationBusy = signal(false); + readonly error = signal(""); + readonly dialogError = signal(""); + readonly message = signal(""); + readonly tab = signal<"experts" | "distribution">("experts"); + readonly modal = signal<"invite" | "scope" | "assignments" | "quota" | null>(null); + readonly selected = signal(null); + readonly selectedExperts = signal(new Set()); + readonly bulkScope = signal(false); + readonly options = signal(null); + readonly selectedSolutions = signal(new Set()); + readonly allocation = signal<{ programId: number; ids: number[] } | null>(null); + readonly quotaPreview = signal(null); + readonly scopeAll = signal(true); + readonly scopeCases = signal(new Set()); + readonly cases = signal([]); + readonly search = new FormControl("", { nonNullable: true }); + readonly searchValue = toSignal(this.search.valueChanges, { initialValue: "" }); + readonly status = new FormControl("all", { nonNullable: true }); + readonly statusValue = toSignal(this.status.valueChanges, { initialValue: "all" }); + readonly caseFilter = new FormControl("all", { nonNullable: true }); + readonly caseValue = toSignal(this.caseFilter.valueChanges, { initialValue: "all" }); + readonly email = new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.email, Validators.maxLength(254)], + }); + readonly emailValue = toSignal(this.email.valueChanges, { initialValue: "" }); + readonly quota = new FormControl(1, { + nonNullable: true, + validators: [ + Validators.required, + Validators.min(1), + Validators.max(1000), + Validators.pattern(/^\d+$/), + ], + }); + readonly quotaValue = toSignal(this.quota.valueChanges, { initialValue: 1 }); + readonly paths = AppRoutes; + readonly href = presentationHref; + initials(expert: ExpertRow): string { + return (expert.name || expert.email) + .split(/\s+/) + .slice(0, 2) + .map(part => part[0]) + .join(""); + } + rowActions(expert: ExpertRow): RowAction[] { + return [ + { id: "scope", label: "Изменить кейсы" }, + ...(expert.status === "joined" + ? [{ id: "assignments", label: "Назначить решения" }] + : [ + { id: "resend", label: "Отправить повторно" }, + { + id: "revoke", + label: "Отозвать приглашение", + destructive: true, + disabled: expert.status === "revoked", + }, + ]), + ]; + } + rowAction(expert: ExpertRow, event: RowActionEvent): void { + if (this.busy()) return; + if (event.id === "scope" || event.id === "assignments") this.open(event.id, expert); + else if (event.id === "resend" || event.id === "revoke") + this.invitationAction(expert, event.id); + } + readonly caseKey = JSON.stringify; + private readonly initialScope = signal("all"); + private scopeKey(): string { + return this.scopeAll() ? "all" : JSON.stringify([...this.scopeCases()].sort()); + } + readonly dirty = computed(() => { + switch (this.modal()) { + case "invite": + return !!this.emailValue() || this.scopeKey() !== "all"; + case "scope": + return this.scopeKey() !== this.initialScope(); + case "quota": + return this.quotaValue() !== this.required(); + case "assignments": { + const before = + this.options() + ?.solutions.filter(item => item.assigned) + .map(item => item.solutionId) + .sort((a, b) => a - b) ?? []; + return ( + JSON.stringify(before) !== + JSON.stringify([...this.selectedSolutions()].sort((a, b) => a - b)) + ); + } + default: + return false; + } + }); + readonly filtered = computed(() => + this.experts().filter(expert => { + const q = this.searchValue().trim().toLocaleLowerCase("ru"); + return ( + `${expert.name ?? ""} ${expert.email}`.toLocaleLowerCase("ru").includes(q) && + (this.statusValue() === "all" || expert.status === this.statusValue()) + ); + }), + ); + readonly filteredSolutions = computed(() => + this.solutions().filter( + row => + row.application.name + .toLocaleLowerCase("ru") + .includes(this.searchValue().trim().toLocaleLowerCase("ru")) && + (this.caseValue() === "all" || JSON.stringify(row.caseName) === this.caseValue()), + ), + ); + toggleExpert(id: number | string): void { + if (this.busy()) return; + const ids = new Set(this.selectedExperts()); + ids.has(id) ? ids.delete(id) : ids.add(id); + this.selectedExperts.set(ids); + } + readonly canBulkInvite = computed(() => { + const rows = this.filtered().filter(expert => this.selectedExperts().has(expert.id)); + return ( + rows.length > 0 && rows.every(expert => expert.status !== "joined" && !!expert.invitationId) + ); + }); + clearSelectedExperts(): void { + this.selectedExperts.set(new Set()); + } + bulk(action: "cases" | "resend" | "revoke"): void { + const rows = this.filtered().filter(expert => this.selectedExperts().has(expert.id)); + if (!rows.length || this.busy()) return; + if (action !== "cases" && !this.canBulkInvite()) return; + if (action === "cases" && !this.bulkScope()) { + this.open("scope"); + this.bulkScope.set(true); + return; + } + this.write( + this.roster.bulkExperts(this.programId(), { + action, + revision: this.revision(), + expertIds: rows.filter(e => e.status === "joined").map(e => Number(e.id)), + invitationIds: rows + .filter(e => e.status !== "joined" && e.invitationId) + .map(e => e.invitationId!), + allCases: this.scopeAll(), + caseNames: this.scopeAll() ? [] : [...this.scopeCases()], + }), + "Изменения для выбранных экспертов сохранены.", + ); + } + constructor() { + timer(4000, 4000) + .pipe( + filter( + () => + !this.busy() && + !this.loading() && + this.experts().some(e => e.status === "pending" && e.deliveryStatus === "queued"), + ), + exhaustMap(() => { + const id = this.programId(); + return this.roster.workspace(id).pipe( + map(data => ({ id, data })), + catchError(() => EMPTY), + ); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(({ id, data }) => { + if (id !== this.programId() || this.busy()) return; + this.selectedExperts.update( + ids => new Set([...ids].filter(id => data.experts.some(expert => expert.id === id))), + ); + this.experts.update(rows => + data.experts.map(expert => { + const counts = rows.find(e => e.id === expert.id); + return expert.status === "joined" + ? { + ...expert, + assigned: counts?.assigned ?? 0, + completed: counts?.completed ?? 0, + remaining: counts?.remaining ?? 0, + } + : expert; + }), + ); + }); + effect(() => { + this.programId(); + untracked(() => { + this.cancelDialog.next(); + this.discard(); + this.load(); + }); + }); + effect(() => { + this.statusChange.emit({ dirty: this.dirty(), busy: this.busy() || this.allocationBusy() }); + }); + effect(() => { + this.quotaValue(); + untracked(() => this.quotaPreview.set(null)); + }); + } + load(): void { + if (this.busy() || this.allocationBusy()) return; + this.cancel.next(); + this.loading.set(true); + this.error.set(""); + this.experts.set([]); + this.assignments.set([]); + this.solutions.set([]); + this.selectedExperts.set(new Set()); + const id = this.programId(); + forkJoin({ roster: this.roster.workspace(id), assignments: this.assignmentsApi.execute(id) }) + .pipe(takeUntil(this.cancel), takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: result => { + if (!result.assignments.ok) { + this.loading.set(false); + this.error.set("Не удалось загрузить назначения решений."); + return; + } + this.revision.set(result.roster.revision); + this.required.set(result.roster.requiredEvaluators); + this.cases.set(result.roster.cases ?? []); + this.assignments.set(result.assignments.value.results); + this.experts.set( + result.roster.experts.map(expert => { + if (expert.status !== "joined") return expert; + const rows = result.assignments.ok + ? result.assignments.value.results.filter(a => a.expert.expertId === expert.id) + : []; + const completed = rows.filter(a => a.status === "completed").length; + return { + ...expert, + assigned: rows.length, + completed, + remaining: rows.length - completed, + }; + }), + ); + this.loadSolutions(0, []); + }, + error: () => { + this.loading.set(false); + this.error.set("Не удалось загрузить экспертов программы."); + }, + }); + } + private loadSolutions(offset: number, rows: SolutionManagerSolutionRow[]): void { + this.solutionsApi + .execute(this.programId(), { submission: "submitted", limit: 100, offset }) + .pipe(takeUntil(this.cancel), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + if (!result.ok) { + this.loading.set(false); + this.error.set("Не удалось загрузить решения для распределения."); + return; + } + const all = [...rows, ...result.value.results]; + if (all.length < result.value.count && result.value.results.length) + this.loadSolutions(offset + result.value.results.length, all); + else { + this.solutions.set(all); + this.loading.set(false); + } + }); + } + counts(solutionId: number): { assigned: number; completed: number } { + const rows = this.assignments().filter(a => a.application.solutionId === solutionId); + return { assigned: rows.length, completed: rows.filter(a => a.status === "completed").length }; + } + open(kind: "invite" | "scope" | "assignments" | "quota", expert: ExpertRow | null = null): void { + if (this.busy() || this.loading()) return; + this.cancelDialog.next(); + this.selected.set(expert); + this.bulkScope.set(false); + this.options.set(null); + this.selectedSolutions.set(new Set()); + this.dialogError.set(""); + this.quotaPreview.set(null); + this.email.reset(""); + this.scopeAll.set(expert?.allCases ?? true); + this.scopeCases.set(new Set(expert?.caseNames ?? [])); + this.initialScope.set(this.scopeKey()); + this.quota.setValue(this.required()); + this.modal.set(kind); + if (kind === "assignments" && expert?.status === "joined") { + this.optionsApi + .execute(this.programId(), Number(expert.id)) + .pipe(takeUntil(this.cancelDialog), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + if (result.ok) { + this.options.set(result.value); + this.selectedSolutions.set( + new Set(result.value.solutions.filter(s => s.assigned).map(s => s.solutionId)), + ); + } else this.dialogError.set("Не удалось загрузить назначения эксперта."); + }); + } + } + toggleScope(name: string | null): void { + if (this.busy()) return; + const values = this.scopeAll() + ? new Set([...this.cases(), null]) + : new Set(this.scopeCases()); + values.has(name) ? values.delete(name) : values.add(name); + this.scopeCases.set(values); + this.scopeAll.set(false); + } + toggleSolution(id: number): void { + if (this.busy()) return; + const option = this.options()?.solutions.find(s => s.solutionId === id); + if ( + !option || + option.protected || + (!option.assigned && (!option.eligible || option.ownTeam || !option.submitted)) + ) + return; + const values = new Set(this.selectedSolutions()); + values.has(id) ? values.delete(id) : values.add(id); + this.selectedSolutions.set(values); + } + refreshDialog(): void { + if (this.busy() || !this.modal()) return; + this.cancelDialog.next(); + this.dialogError.set(""); + const expert = this.selected(); + if (this.modal() === "assignments" && expert?.status === "joined") { + this.options.set(null); + this.optionsApi + .execute(this.programId(), Number(expert.id)) + .pipe(takeUntil(this.cancelDialog), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + if (!result.ok) { + this.dialogError.set("Не удалось обновить назначения. Повторите попытку."); + return; + } + const chosen = this.selectedSolutions(), + fresh = result.value; + const ids = new Set( + fresh.solutions + .filter( + item => + item.protected || + (chosen.has(item.solutionId) && + (item.assigned || (item.submitted && item.eligible && !item.ownTeam))), + ) + .map(item => item.solutionId), + ); + this.options.set(fresh); + this.selectedSolutions.set(ids); + if ([...chosen].some(id => !ids.has(id))) + this.dialogError.set( + "Часть выбранных решений стала недоступна. Проверьте обновленный выбор.", + ); + }); + } else { + this.roster + .workspace(this.programId()) + .pipe(takeUntil(this.cancelDialog), takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: data => { + this.revision.set(data.revision); + this.cases.set(data.cases ?? []); + this.quotaPreview.set(null); + }, + error: () => this.dialogError.set("Не удалось обновить данные. Повторите попытку."), + }); + } + } + private write(request: Observable, message: string): void { + if (this.busy()) return; + this.busy.set(true); + this.dialogError.set(""); + this.error.set(""); + request.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ + next: () => { + this.busy.set(false); + this.discard(); + this.message.set(message); + this.load(); + }, + error: () => { + this.busy.set(false); + this.quotaPreview.set(null); + this.options.set(null); + this.dialogError.set("Изменение не сохранено. Обновите данные и повторите попытку."); + }, + }); + } + invite(): void { + if (this.busy()) return; + if (this.email.invalid) { + this.email.markAsTouched(); + return; + } + if (!this.scopeAll() && !this.scopeCases().size) { + this.dialogError.set("Выберите кейсы эксперта."); + return; + } + this.write( + this.roster.invite(this.programId(), this.email.value.trim(), { + allCases: this.scopeAll(), + caseNames: this.scopeAll() ? [] : [...this.scopeCases()], + }), + "Приглашение создано. Письмо поставлено в очередь.", + ); + } + invitationAction(expert: ExpertRow, action: "resend" | "revoke"): void { + if (this.busy()) return; + if (expert.invitationId) + this.write( + this.roster.invitationAction(this.programId(), expert.invitationId, action), + action === "resend" ? "Приглашение отправлено повторно." : "Приглашение отозвано.", + ); + } + saveScope(): void { + if (this.busy()) return; + if (this.bulkScope()) { + this.bulk("cases"); + return; + } + const expert = this.selected(); + if (!expert || (!this.scopeAll() && !this.scopeCases().size)) return; + const command = { + revision: this.revision(), + allCases: this.scopeAll(), + caseNames: this.scopeAll() ? [] : [...this.scopeCases()], + }; + this.write( + expert.status === "joined" + ? this.roster.saveCaseScope(this.programId(), Number(expert.id), command) + : this.roster.saveInvitationCaseScope(this.programId(), expert.invitationId!, command), + "Кейсы эксперта сохранены. Текущие назначения остаются.", + ); + } + saveAssignments(): void { + if (this.busy()) return; + const options = this.options(); + if (!options) return; + this.write( + this.saveApi + .execute(this.programId(), options.expertId, { + revision: options.revision, + solutionIds: [...this.selectedSolutions()], + }) + .pipe( + map(result => { + if (!result.ok) throw result.error; + return result.value; + }), + ), + "Решения назначены эксперту.", + ); + } + previewQuota(): void { + if (this.quota.invalid || this.busy()) return; + this.cancelDialog.next(); + this.quotaPreview.set(null); + this.dialogError.set(""); + const value = this.quota.value; + this.quotaApi + .execute(this.programId(), value) + .pipe(takeUntil(this.cancelDialog), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + if (value !== this.quota.value) return; + if (result.ok) this.quotaPreview.set(result.value); + else this.dialogError.set("Не удалось проверить новое число оценщиков."); + }); + } + saveQuota(): void { + if (this.busy()) return; + const p = this.quotaPreview(); + if (!p || p.requiredEvaluators !== this.quota.value) return; + this.write( + this.saveQuotaApi + .execute(this.programId(), { + revision: p.revision, + requiredEvaluators: p.requiredEvaluators, + }) + .pipe( + map(result => { + if (!result.ok) throw result.error; + return result.value; + }), + ), + "Число оценщиков изменено. Готовность решений пересчитана.", + ); + } + startAllocation(): void { + if (!this.busy() && !this.loading() && this.solutions().length) + this.allocation.set({ + programId: this.programId(), + ids: this.filteredSolutions().map(row => row.application.solutionId), + }); + } + async close(): Promise { + if (this.busy()) return; + if (this.dirty() && !(await this.confirmDiscard()())) return; + this.discard(); + } + discard(): void { + this.cancelDialog.next(); + this.modal.set(null); + this.options.set(null); + this.quotaPreview.set(null); + this.selected.set(null); + this.dialogError.set(""); + } + async downloadExperts(): Promise { + try { + await exportExperts( + this.programId(), + this.filtered().map(e => ({ + name: e.name ?? "", + email: e.email, + cases: e.allCases + ? "Все кейсы" + : (e.caseNames ?? []).map(c => c ?? "Без кейса").join(", "), + status: e.status === "joined" ? "В программе" : "Приглашен", + delivery: e.deliveryStatus ?? "", + assigned: e.assigned, + completed: e.completed, + remaining: e.remaining, + })), + ); + } catch { + this.error.set("Не удалось выгрузить экспертов."); + } + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/management/management.component.html b/projects/social_platform/src/app/ui/pages/program/management/management.component.html index c93fdcb14..dddd51fae 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/management.component.html +++ b/projects/social_platform/src/app/ui/pages/program/management/management.component.html @@ -57,15 +57,27 @@

{{ requestError() }}

/> } @if (section() === "results") { - - } - @if (section() === "experts") { - } + @if (section() === "experts") { + @if (program.solutionStorageMode === "solution") { + + } @else { + + } + } @if (section() === "projects" && program.solutionStorageMode === "solution") { Изменятся команды заявок
} +@if (solutionRemovalRequest()) { + +} (null); readonly removingParticipants = signal(false); + readonly solutionRemovalRequest = signal<{ programId: number; ids: number[] } | null>(null); readonly tableError = signal(null); readonly tableMessage = signal(null, { equal: () => false }); private selectionKey = ""; @@ -253,6 +258,7 @@ export class ProgramManagementComponent implements OnInit { readonly criteriaStatus = signal({ dirty: false, busy: false }); readonly expertsStatus = signal({ dirty: false, busy: false }); readonly expertsForm = viewChild(ManagementExpertsComponent); + readonly solutionExpertsForm = viewChild(SolutionExpertsComponent); readonly organizersForm = viewChild(ManagementOrganizersComponent); readonly organizersStatus = signal({ dirty: false, busy: false }); readonly registrationForm = viewChild(ManagementRegistrationComponent); @@ -484,6 +490,7 @@ export class ProgramManagementComponent implements OnInit { this.clearRows(); this.projectActionRequest.set(null); this.participantRemoval.set(null); + this.solutionRemovalRequest.set(null); this.tableError.set(null); this.tableMessage.set(null); this.selectionKey = selectionKey; @@ -869,7 +876,11 @@ export class ProgramManagementComponent implements OnInit { } previewParticipantRemoval(ids: number[]): void { const ctx = this.state.context; - if (!ctx || !ids.length || this.removingParticipants()) return; + if (!ctx || !ids.length || this.removingParticipants() || this.solutionActionBusy()) return; + if (this.state.program()?.solutionStorageMode === "solution") { + this.solutionRemovalRequest.set({ programId: ctx.programId, ids: [...ids] }); + return; + } const key = this.selectionKey; this.removingParticipants.set(true); this.tableError.set(null); @@ -920,6 +931,15 @@ export class ProgramManagementComponent implements OnInit { selectFromRow(event: MouseEvent, project: ManagementProject): void { if (!(event.target as HTMLElement).closest("a,button,input,label")) this.select(project); } + solutionRegistrationsRemoved(count: number): void { + this.solutionRemovalRequest.set(null); + this.clearRows(); + this.tableMessage.set(`Удалено участников из программы: ${count}.`); + const lastOffset = + Math.max(0, Math.ceil((this.count() - count) / this.pageSize) - 1) * this.pageSize; + if (this.offset() > lastOffset) void this.changeQuery({ offset: lastOffset }); + else this.state.retry(); + } clearProjectSelection(): void { const selectedId = this.state.selected()?.id; this.state.clearSelection(); @@ -1131,6 +1151,7 @@ export class ProgramManagementComponent implements OnInit { this.criteriaForm()?.discard(); this.criteriaStatus.set({ dirty: false, busy: false }); this.expertsForm()?.discard(); + this.solutionExpertsForm()?.discard(); this.expertsStatus.set({ dirty: false, busy: false }); this.organizersForm()?.discard(); this.organizersStatus.set({ dirty: false, busy: false }); diff --git a/projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.html b/projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.html index ab448ffed..59fea219d 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.html +++ b/projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.html @@ -1,7 +1,7 @@ @if (!project().experts.length) {

Оценок пока нет.

diff --git a/projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.ts b/projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.ts index 4100c6730..d12aefa00 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.ts @@ -4,7 +4,7 @@ import { AvatarComponent, IconComponent } from "@uilib"; import { RouterLink } from "@angular/router"; import { AppRoutes } from "@api/paths/app-routes"; import { ManagementCriterion } from "@domain/program/program-criteria.model"; -import { ResultExpert, ResultProject } from "@domain/program/program-results.model"; +import { ResultExpert, ResultEntry } from "@domain/program/program-results.model"; import { presentationHref } from "@utils/program-case-analytics"; @Component({ @@ -15,7 +15,17 @@ import { presentationHref } from "@utils/program-case-analytics"; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ResultProjectScoresComponent { - readonly project = input.required(); + readonly project = input.required(); + readonly solution = computed(() => { + const item = this.project(); + return "application" in item ? item.application : null; + }); + readonly detailLink = computed(() => { + const item = this.project(); + return "application" in item + ? AppRoutes.program.solution(item.application.programId, item.application.solutionId) + : AppRoutes.projects.detail(item.projectId); + }); readonly criteria = input.required(); readonly expandedExperts = model(new Set()); readonly href = presentationHref; diff --git a/projects/social_platform/src/app/ui/pages/program/management/results/results.component.html b/projects/social_platform/src/app/ui/pages/program/management/results/results.component.html index 87b4a0128..1f19902c8 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/results/results.component.html +++ b/projects/social_platform/src/app/ui/pages/program/management/results/results.component.html @@ -7,7 +7,13 @@

{{ - snapshot() && !snapshot()?.hasNumericCriteria ? "Результаты проектов" : "Рейтинг проектов" + snapshot() && !snapshot()?.hasNumericCriteria + ? solutionMode() + ? "Результаты решений" + : "Результаты проектов" + : solutionMode() + ? "Рейтинг решений" + : "Рейтинг проектов" }}

@@ -50,12 +56,14 @@

@if (snapshot(); as data) {
@@ -86,7 +94,13 @@

Оценено {{ readyCount() }} из {{ matchingCount() }} - {{ matchingCount() | pluralize: ["проекта", "проектов", "проектов"] }} @if (updatedTime(); as time) { @@ -114,8 +128,22 @@

} @if (!data.groups.length) {
-

Сданных проектов пока нет

-

Результаты появятся после сдачи решений.

+

+ {{ + solutionMode() && !submissionApplicable() + ? "Оценивание не требуется" + : solutionMode() + ? "Сданных решений пока нет" + : "Сданных проектов пока нет" + }} +

+

+ {{ + solutionMode() && !submissionApplicable() + ? "В этой программе материалы сохраняются без сдачи решения." + : "Результаты появятся после сдачи решений." + }} +

} @else if (!projectCount()) {
@@ -137,7 +165,8 @@

{{ group.caseName ?? "Без кейса" }}

@if (data.hasNumericCriteria) { Место } - ПроектОценивание + {{ solutionMode() ? "Решение" : "Проект" }}Оценивание @if (data.hasNumericCriteria) { Балл{{ data.maximum === null ? "" : " из " + data.maximum }}{{ group.caseName ?? "Без кейса" }}

>

Два листа: рейтинг и оценки экспертов.

@if (exportScope() === "current" && !projectCount()) { -

В текущей выборке нет проектов.

+

+ {{ + solutionMode() ? "В текущей выборке нет решений." : "В текущей выборке нет проектов." + }} +

} @if (exportError()) {

Не удалось подготовить файл. Попробуйте еще раз.

diff --git a/projects/social_platform/src/app/ui/pages/program/management/results/results.component.ts b/projects/social_platform/src/app/ui/pages/program/management/results/results.component.ts index dbdf3e3f4..75c69a637 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/results/results.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/management/results/results.component.ts @@ -6,6 +6,7 @@ import { computed, effect, inject, + Injector, input, signal, untracked, @@ -26,7 +27,10 @@ import { IconComponent, } from "@uilib"; import { ProgramManagementUseCase } from "@api/program/use-cases/program-management.use-case"; -import { ProgramResults, ResultProject, ResultsQuery } from "@domain/program/program-results.model"; +import { ProgramResults, ResultEntry, ResultsQuery } from "@domain/program/program-results.model"; +import { GetSolutionResultsUseCase } from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { ExportSolutionResultsUseCase } from "@api/solution/use-cases/solution-results-export.use-case"; +import { map } from "rxjs"; import { ModalComponent } from "@ui/primitives/modal/modal.component"; import { ResultProjectScoresComponent } from "./result-project-scores.component"; @@ -54,6 +58,9 @@ import { TransientNoticeComponent } from "@ui/primitives/transient-notice/transi }) export class ManagementResultsComponent { readonly programId = input.required(); + readonly solutionMode = input(false); + readonly submissionApplicable = input(true); + private readonly injector = inject(Injector); private readonly api = inject(ProgramManagementUseCase); private readonly destroyRef = inject(DestroyRef); private readonly fb = inject(FormBuilder).nonNullable; @@ -89,7 +96,7 @@ export class ManagementResultsComponent { readonly filterCount = computed( () => Number(this.caseValue() !== "all") + Number(this.statusValue() !== "all"), ); - readonly isComplete = (project: ResultProject): boolean => + readonly isComplete = (project: ResultEntry): boolean => project.evaluationComplete ?? project.ready; readonly caseKey = (name: string): string => JSON.stringify(name); readonly format = (value: number | null | undefined): string => @@ -197,7 +204,28 @@ export class ManagementResultsComponent { switchMap(id => { this.loading.set(true); this.error.set(false); - return this.api.results(id).pipe( + const request = this.solutionMode() + ? this.injector + .get(GetSolutionResultsUseCase) + .execute(id) + .pipe( + map(result => { + if (!result.ok) throw result.error; + const data = result.value; + return { + ...data, + groups: data.groups.map(group => ({ + caseName: group.caseName, + projects: group.solutions.map(solution => ({ + ...solution, + id: solution.solutionId, + })), + })), + } satisfies ProgramResults; + }), + ) + : this.api.results(id); + return request.pipe( tap(data => { this.snapshot.set(data); this.page.set(this.currentPage()); @@ -214,6 +242,7 @@ export class ManagementResultsComponent { .subscribe(); effect(() => { const id = this.programId(); + this.solutionMode(); untracked(() => { this.snapshot.set(null); this.expanded.set(new Set()); @@ -250,7 +279,7 @@ export class ManagementResultsComponent { this.statusFilter.setValue(this.draftStatus.value); this.modal.set(null); } - private compareNames(a: ResultProject, b: ResultProject): number { + private compareNames(a: ResultEntry, b: ResultEntry): number { const first = a.name.toLowerCase(), second = b.name.toLowerCase(); return first === second ? a.id - b.id : first < second ? -1 : 1; @@ -286,8 +315,18 @@ export class ManagementResultsComponent { this.exporting.set(true); this.exportError.set(false); this.exportSuccess.set(false); - this.api - .exportResults(this.programId(), query) + const request = this.solutionMode() + ? this.injector + .get(ExportSolutionResultsUseCase) + .execute(this.programId(), query) + .pipe( + map(result => { + if (!result.ok) throw result.error; + return result.value; + }), + ) + : this.api.exportResults(this.programId(), query); + request .pipe( takeUntil(this.requests), takeUntilDestroyed(this.destroyRef), diff --git a/projects/social_platform/src/app/ui/pages/program/management/results/solution-results.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/management/results/solution-results.component.spec.ts new file mode 100644 index 000000000..e6b834f07 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/results/solution-results.component.spec.ts @@ -0,0 +1,56 @@ +/** @format */ +import { TestBed } from "@angular/core/testing"; +import { provideRouter } from "@angular/router"; +import { of } from "rxjs"; +import { ok } from "@domain/shared/result.type"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { SolutionResults } from "@domain/solution/solution-contract.model"; +import { GetSolutionResultsUseCase } from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { ExportSolutionResultsUseCase } from "@api/solution/use-cases/solution-results-export.use-case"; +import { ProgramManagementUseCase } from "@api/program/use-cases/program-management.use-case"; +import { ManagementResultsComponent } from "./results.component"; +vi.mock("file-saver", () => ({ saveAs: vi.fn() })); +it("uses real solution results, score links and filtered XLSX while never invoking Project results", () => { + const data = solutionContractScenarios.find( + s => s.operationId === "getSolutionResults" && s.status === 200, + )!.response as SolutionResults; + const results = { execute: vi.fn(() => of(ok(structuredClone(data)))) }, + exportApi = { execute: vi.fn(() => of(ok(new Blob()))) }; + const legacy = { results: vi.fn(), exportResults: vi.fn() }; + TestBed.configureTestingModule({ + imports: [ManagementResultsComponent], + providers: [ + provideRouter([]), + { provide: ProgramManagementUseCase, useValue: legacy }, + { provide: GetSolutionResultsUseCase, useValue: results }, + { provide: ExportSolutionResultsUseCase, useValue: exportApi }, + ], + }); + const fixture = TestBed.createComponent(ManagementResultsComponent); + fixture.componentRef.setInput("programId", 12); + fixture.componentRef.setInput("solutionMode", true); + fixture.detectChanges(); + const c = fixture.componentInstance, + item = c.snapshot()!.groups.flatMap(g => g.projects)[0]; + expect(item).not.toHaveProperty("projectId"); + expect(item).toHaveProperty("application.kind", "solution"); + c.toggle(item.id); + fixture.detectChanges(); + expect( + fixture.nativeElement.querySelector(`a[href='/office/program/12/solutions/${item.id}']`), + ).toBeTruthy(); + c.search.setValue("Город"); + c.caseFilter.setValue(JSON.stringify("Город")); + c.statusFilter.setValue("pending"); + c.download(); + expect(exportApi.execute).toHaveBeenCalledExactlyOnceWith(12, { + search: "Город", + caseScope: "selected", + caseName: "Город", + order: "desc", + scope: "current", + status: "pending", + }); + expect(legacy.results).not.toHaveBeenCalled(); + expect(legacy.exportResults).not.toHaveBeenCalled(); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.ts index 636e7b71f..dfe85e392 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.ts @@ -84,7 +84,12 @@ import { ModalComponent } from "@ui/primitives/modal/modal.component"; Отмена @if (plan()) { - } @else { @@ -197,7 +202,7 @@ export class SolutionAllocationComponent { confirm(): void { const request = this.request(), plan = this.plan(); - if (!request || !plan?.planToken || this.busy()) return; + if (!request || !plan?.planToken || plan.shortages.length || this.busy()) return; this.busy.set(true); this.busyChange.emit(true); this.apply diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-registration-removal.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-registration-removal.component.spec.ts new file mode 100644 index 000000000..0ea1236c2 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-registration-removal.component.spec.ts @@ -0,0 +1,73 @@ +/** @format */ +import { TestBed } from "@angular/core/testing"; +import { of, Subject } from "rxjs"; +import { ok, fail } from "@domain/shared/result.type"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { SolutionRegistrationRemovalPreview } from "@domain/solution/solution-contract.model"; +import { + PreviewSolutionRegistrationRemovalUseCase, + ApplySolutionRegistrationRemovalUseCase, +} from "@api/solution/use-cases/solution-registration-removal.use-cases"; +import { SolutionRegistrationRemovalComponent } from "./solution-registration-removal.component"; +const data = () => + structuredClone( + solutionContractScenarios.find( + s => s.operationId === "previewSolutionRegistrationRemoval" && s.status === 200, + )!.response, + ) as SolutionRegistrationRemovalPreview; +function setup() { + const preview = { execute: vi.fn(() => of(ok(data()))) }, + apply = { execute: vi.fn(() => new Subject>()) }; + TestBed.configureTestingModule({ + imports: [SolutionRegistrationRemovalComponent], + providers: [ + { provide: PreviewSolutionRegistrationRemovalUseCase, useValue: preview }, + { provide: ApplySolutionRegistrationRemovalUseCase, useValue: apply }, + ], + }); + const fixture = TestBed.createComponent(SolutionRegistrationRemovalComponent); + fixture.componentRef.setInput("request", { programId: 12, ids: [91, 92] }); + fixture.detectChanges(); + return { fixture, c: fixture.componentInstance, preview, apply }; +} +describe("Confirmed solution registration removal", () => { + it("applies the reviewed registrations/token once and blocks closing while writing", () => { + const { c, preview, apply } = setup(); + const closed = vi.fn(); + c.closed.subscribe(closed); + const token = c.preview()!.previewToken; + expect(preview.execute).toHaveBeenCalledExactlyOnceWith(12, { registrationIds: [91, 92] }); + c.confirm(); + c.confirm(); + c.close(); + expect(apply.execute).toHaveBeenCalledExactlyOnceWith(12, { + registrationIds: [91, 92], + previewToken: token, + }); + expect(closed).not.toHaveBeenCalled(); + }); + it("drops a stale token when the selection changes", () => { + const { fixture, c, preview, apply } = setup(); + const old = new Subject>>(); + preview.execute.mockReturnValueOnce(old).mockReturnValue(new Subject()); + c.load(); + fixture.componentRef.setInput("request", { programId: 13, ids: [111] }); + fixture.detectChanges(); + old.next(ok(data())); + old.complete(); + expect(c.preview()).toBeNull(); + c.confirm(); + expect(apply.execute).not.toHaveBeenCalled(); + }); + it("invalidates a rejected preview and requires a fresh review before another apply", () => { + const { c, apply } = setup(); + apply.execute.mockReturnValue( + of(fail({ code: "selection_changed", detail: "Выбор изменился", errors: [], meta: {} })), + ); + c.confirm(); + c.confirm(); + expect(c.preview()).toBeNull(); + expect(c.error()).toBe("Выбор изменился"); + expect(apply.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-registration-removal.component.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-registration-removal.component.ts new file mode 100644 index 000000000..58514f66b --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-registration-removal.component.ts @@ -0,0 +1,164 @@ +/** @format */ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + effect, + inject, + input, + output, + signal, + untracked, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { A11yModule } from "@angular/cdk/a11y"; +import { Subject, takeUntil } from "rxjs"; +import { ButtonDirective } from "@uilib"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; +import { + PreviewSolutionRegistrationRemovalUseCase, + ApplySolutionRegistrationRemovalUseCase, +} from "@api/solution/use-cases/solution-registration-removal.use-cases"; +import { SolutionRegistrationRemovalPreview } from "@domain/solution/solution-contract.model"; + +@Component({ + selector: "app-solution-registration-removal", + imports: [A11yModule, ButtonDirective, ModalComponent], + styleUrl: "./solution-applications.component.scss", + template: `
+

Удалить участников из программы?

+ @if (pending()) { +

Проверяем регистрации и связанные решения…

+ } + @if (error()) { +

{{ error() }}

+ + } + @if (preview(); as p) { +

Будет удалено регистраций: {{ p.deletedCount }}. Аккаунты пользователей сохранятся.

+

+ Заявок в архив: {{ p.archivedCount }}. Исключений из команды: {{ p.removedMemberCount }}. + Назначений снимется: {{ p.removedAssignmentsCount }}. Приглашений будет отозвано: + {{ p.revokedInvitationCount }}. +

+

+ Сданные материалы, прежний состав и оценки сохранятся в истории. Архивные решения + перестанут участвовать в оценивании и публикации. +

+
    + @for (registration of p.registrations; track registration.registrationId) { +
  • {{ registration.user.firstName }} {{ registration.user.lastName }}
  • + } +
+
    + @for (effect of p.effects; track effect.application.solutionId) { +
  • + {{ effect.application.name }}: + {{ + effect.effect === "archive" + ? "архивировать заявку капитана" + : effect.effect === "remove_members" + ? "исключить участников" + : "отозвать приглашения" + }}{{ effect.isSubmitted ? " · решение сдано" : "" }}. Сохраненных оценок: + {{ effect.scoreCount }}. +
  • + } +
+ @for (issue of p.errors; track $index) { +

{{ issue.detail }}

+ } + } +
+ +
+
`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionRegistrationRemovalComponent { + readonly request = input<{ programId: number; ids: number[] } | null>(null); + readonly closed = output(); + readonly completed = output(); + readonly busyChange = output(); + readonly pending = signal(false); + readonly busy = signal(false); + readonly error = signal(""); + readonly preview = signal(null); + private readonly getPreview = inject(PreviewSolutionRegistrationRemovalUseCase); + private readonly apply = inject(ApplySolutionRegistrationRemovalUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly cancel = new Subject(); + constructor() { + effect(() => { + this.request(); + untracked(() => { + this.cancel.next(); + this.preview.set(null); + this.error.set(""); + this.pending.set(false); + if (this.request()) this.load(); + }); + }); + } + load(): void { + const request = this.request(); + if (!request || this.busy()) return; + this.cancel.next(); + this.preview.set(null); + this.error.set(""); + this.pending.set(true); + this.getPreview + .execute(request.programId, { registrationIds: request.ids }) + .pipe(takeUntil(this.cancel), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.pending.set(false); + if (result.ok) this.preview.set(result.value); + else this.error.set(result.error.detail); + }); + } + confirm(): void { + const request = this.request(), + p = this.preview(); + if (!request || !p?.canApply || !p.previewToken || this.busy()) return; + this.busy.set(true); + this.busyChange.emit(true); + this.error.set(""); + this.apply + .execute(request.programId, { registrationIds: request.ids, previewToken: p.previewToken }) + .pipe(takeUntil(this.cancel), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.busy.set(false); + this.busyChange.emit(false); + this.preview.set(null); + if (result.ok) this.completed.emit(result.value.deletedCount); + else this.error.set(result.error.detail); + }); + } + close(): void { + if (this.busy()) return; + this.cancel.next(); + this.closed.emit(); + } +}