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/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md index 91faad638..1d9a0d6ff 100644 --- a/docs/contracts/program-solutions-v1/README.md +++ b/docs/contracts/program-solutions-v1/README.md @@ -1,9 +1,325 @@ -# 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.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 нет. -`python tools/generate-solution-contract.py` генерирует 47 доменных типов и 43 независимых сценария в `domain/solution/`. После генерации выполнить scoped Prettier этих двух файлов. Фикстуры используются только тестами; production mock provider не включается. +Рабочая версия 1.20.1 от 25 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE09 и FE01–FE08. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; BE02 добавляет удаление участника, выход и передачу капитанства, BE03 — цикл приглашений, BE04 — чтение/изменение формы и стартовые шаблоны, BE05 — частичное сохранение ответов и привязку файлов, BE06 — атомарную сдачу со снимком. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. -Изменение контракта требует синхронного изменения копий backend/frontend и повторной проверки wire boundary. Реализация FE01 ограничена контекстом, созданием и чтением; все сценарии C00 не означают реализацию будущих команд. +Изменение 1.20.1: исправлен код отказа оценивания архивного решения для пользователя с совмещенными ролями организатора/эксперта: solution_archived/409 проверяется до criteria/quota/assignment. Обычный эксперт без сохраненного доступа по-прежнему получает404. Существующая форма D01 закрывает материалы и ввод при таком отказе. Payload/59 операций/156 схем и297 прежних примеров не меняются; добавлены2 независимых примера. Миграций и изменений макета нет. + +Изменение 1.20.0: BE08C2 добавляет подтверждаемое удаление регистраций по registration_ids и отдельный аудит, включая регистрации без решения. Капитан снимает заявку с участия с reason=registration_removed; участник удаляется из текущего состава/снимка, pending отзываются. Архивные работы, материалы, оценки, Auth User, роли и другие программы сохраняются. Смешанная связь с Project блокирует всю команду. Старые 272 сценария не меняются; добавлены 25 независимых сценариев и 4 операции. Миграция 0038 только добавляет аудит/связи истории, без удаления данных. Новый UI остается за согласованием D02. + +Изменение 1.19.0: bulk archive preview/apply снимает работу с активного участия, сохраняя solution/материалы/UserFile/состав/оценки и исходную сдачу. Pending отзываются, все назначения снимаются; архив не участвует в текущей заявке/эксклюзивности/оценивании/аналитике/публикации. Собственный архив читается капитаном/составом/manager, participation=active(default)/archived/all. Optional participation добавлен в Solution/Summary; прежние payloads остаются валидны. Draft получает archive snapshot без submit. История дополнена archive ветвью и private archive_snapshot detail.9 новых схем/2 операции; итог145 схем/55 операций/272 сценариев,250 старых сценариев сохранены. Регистрации не удаляются этим action; их отдельная административная команда еще в объеме BE08. Backend/миграция → frontend API; новый UI ожидает D02, legacy Project не меняется. + +Изменение 1.18.0: organizer/admin получает отдельные signed preview/apply исключения принятого участника до/после сдачи. Капитан не исключается, регистрация/ответы/файлы/оценки/назначения сохраняются; pending приглашения выбранного участника отзываются, обработанные сохраняются без доступа к материалам. Текущий submitted snapshot меняет только members; исходный и новый snapshot и исключенный Member сохраняются в истории. SolutionAdminChange теперь discriminated union change_case/remove_member; прежние case payloads и все230 сценариев неизменны.5 новых схем/2 операции, итог136 схем/53 операции/250 независимых сценариев. Обычные writers остаются frozen. Удаление регистрации/архив заявки далее; Project endpoints/данные не меняются. Backend/миграция → frontend API, новый UI ожидает D02. + +Изменение 1.17.0: согласованные административные исключения начинают со смены кейса после сдачи. Signed preview/apply сохраняет оценки и оцененные назначения, снимает только несовместимые неоцененные; snapshot до/после и actor/version/effects сохраняются в неизменяемой истории. Обычные participant/model writers остаются frozen. Preview дополнен is_submitted/removed_assignments_count/score_count; два старых submitted mocks намеренно меняют прежний запрет на разрешенное действие. GET history/list и detail доступны только organizer/admin своей программы; snapshot использует существующую SubmissionSnapshot схему.3 новых схемы/2 операции, итог131 схем/51 операция/230 сценариев. Legacy endpoints/Project модели не меняются; удаления/отвязка/исключение участников остаются следующим объемом BE08. Backend/миграция → frontend; нового UI до D02 approval нет. + +Изменение 1.16.0: роль нового приглашения обязательна и содержит 1–128 символов, включая хотя бы один непробельный. PATCH может не передавать role при изменении других полей; переданная role имеет те же ограничения и не очищается в null. Это намеренное ужесточение двух request-схем CreateInvitation/UpdateInvitation под согласованный D00 v3/AC26; старые nullable role в ответах/истории/составе сохранены, accept/decline/revoke не требуют заполнения старой роли. Три прежних create error mocks дополнены корректной ролью, чтобы по-прежнему проверять duplicate/max/frozen; остальные206 старых сценариев неизменны. Добавлены8 независимых сценариев: границы/400/историческая null role. Итог128 схем/49 операций/217 сценариев. Схемы ответов и маршруты прежние; UI уже требует роль, новых экранов/миграций нет. Backend перед frontend. + +Изменение 1.15.0: отдельные solution case preview/apply переносят существующую смену кейса черновика организатором. Signed proof10min, exact case, program/solution/fields locks и state recheck; только case answer/version/time меняются. Submitted остается frozen до отдельного решения об административных исключениях. Старые125 схем/47 операций/185 сценариев, включая submit action, неизменны; добавлены3 схемы/2 операции/24 независимых сценария. Неконкурсные черновики поддерживаются, искусственной сдачи нет. Новый UI/удаление/отвязка этим API не реализуются. + +Изменение 1.13.0: REL01 получает отдельный persisted `PartnerProgram.solution_creation_enabled`, default=False, без изменения storage mode. GET context добавляет значение blocked_reason=solution_creation_disabled; POST нового решения — 409 с тем же code. Повтор капитана возвращает прежнюю заявку с 200; права и currentApplication имеют прежний приоритет. Чтение, ответы, файлы, команда, приглашения, сдача, оценки и публикация существующих работ не отключаются этим флагом. Техническая команда `set_solution_creation --program-id ID --enable|--disable` делает preview, `--apply` меняет только выбранную программу под row lock. Нового API настройки нет. 120 схем/45 операций сохранены, 161 независимый сценарий (все 158 прежних неизменны). Миграция 0034 закрывает новое создание по умолчанию, не меняя данные работ. Backend/миграция → frontend; новый клиент уточняет текст существующего error state, старый использует canCreate. Полный REL01 и QA01 не объявлены завершенными. + +Изменение 1.12.0: manager list добавляет field_id/answer_state по applied snapshot/current форме (0/false present, новое поле не missing у старой сдачи). kind=solutions/submitted XLSX принимает те же search/scope/submission/case/field фильтры и выгружает все найденные без pagination; остальные kind отклоняют непустые фильтры. Readonly manager permissions/no-store, прежние ответы и 120 схем сохранены; 45 операций, 158 независимых сценариев. Angular UI ожидает design approval. + +Изменение 1.11.0: два anonymous read-only GET published-solutions для опубликованных неконкурсных материалов и accepted team. Nullable published_at сохраняет прежнюю одноразовую публикацию после срока/флага; отдельный job не создает Project и не требует submit. Public allowlist не содержит оценок, регистрации, контактных полей профиля или приглашений. Current saved или immutable submitted snapshot, 0/False/file URL, no-store и C00 errors. Прежние 116 схем, 43 операции и 128 сценариев неизменны; итог 120 схем, 45 операций, 144 сценария. Конкурсная публикация ожидает ранее заданного решения о черновиках; публичный UI требует отдельного design approval. + +Изменение 1.10.0: manager-only POST reminder-preview/reminders для ожидающих решений использует настоящие solution IDs и SolutionExpertAssignment, snapshot search, selected/all/exclusions и существующий подписанный preview/outbox. Получатели — только активные эксперты программы с незавершенными назначениями; 0/False учитываются общими правилами. Confirm/delivery пересекают текущие связи с утвержденными IDs, новые назначения не добавляются, повтор token идемпотентен. Private, no-store, C00 errors; legacy/open/noncompetitive неприменимы. Прежние 110 схем, 41 операция и 113 сценариев неизменны; итог 116 схем, 43 операции, 128 сценариев. Angular API layer внедряется независимо, новый UI D02 ожидает design approval. + +Изменение 1.9.0: пять manager-only read/export операций solution-analytics дают сводку, реальные назначения/детали, scoped список решений и XLSX программы. Snapshot/current форма определяет применимость полей; 0/False answered, noncompetitive submission/evaluation и open assignment aggregates null. Старые Project endpoints/DTO не меняются. Прежние 88 схем, 36 операций и 80 сценариев сохранены; итог 110 схем, 41 операций, 113 сценариев. Backend → frontend API layer; UI/напоминания/разрушающие команды отдельно. + +Изменение 1.8.0: GET /programs/{program_id}/management/solution-cases/ дает организатору агрегаты ProgramSolution по текущим options кейса. Submitted использует snapshot, draft — собственные ответы; люди уникальны внутри bucket и только зарегистрированные captain/accepted members. Noncompetitive submission counts null, без требования submit. Старые 85 схем, 35 операций и 72 сценария сохранены; итог 88 схем, 36 операций, 80 сценариев. Старый Project overview не изменяется; новый UI и остальная аналитика внедряются отдельно. + +Изменение 1.7.0: существующий GET /programs/{program_id}/analytics-widget/ добавлен в C00 перед адаптацией. Legacy payload сохраняется. Новый solution payload содержит storage_mode=solution, SolutionRef участника и отдельные счетчики solutions/participants_without_solution организатора; экспертные assigned/remaining читают настоящие назначения. Стадии и null неприменимых показателей прежние, оценки и регистрации не сериализуются. Ошибки сохраняют существующий DRF detail и status, не переводятся молча в C00 Error. Все прежние схемы, операции и 59 примеров неизменны: 85 схем, 35 операций, 72 сценария. Backend → frontend; реальные программы не переключаются. Это контракт ролевого виджета, не завершенная основная аналитика BE09. + +Изменение 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; остальная аналитика, ролевой виджет и выгрузки еще в работе. + +Изменение 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 сохранен. + +Изменение 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. + +Источники: ТЗ «Решение вместо проекта в программах 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 проверяют технический режим программы. Наличие серверного кода при выключенном режиме не дает создать решение. Дополнительно новое создание требует solution_creation_enabled=True; default=False. Откат creation flag не меняет режим, текущую заявку, права или данные уже созданных работ. + +## Метаданные существующей страницы программы + +GET /programs/{id}/ дополнительно передает read-only `solution_storage_mode: legacy_project | solution` в member и unregistered вариантах. Это только выбор маршрута; поле не открывает описание, форму, состав, ответы или оценки. `current_application` сохраняет прежний Project DTO. Верхний экспертный вход и ссылка виджета ведут в `solutions-rating` только при явном `solution`, иначе в `projects-rating`. Старый клиент игнорирует добавочное поле; новый клиент при его отсутствии сохраняет старый путь. Rollout: backend → frontend, без миграций. Эта совместимая метаинформация существующего API сама по себе не добавляет операций решения и не изменяет существующие схемы C00. + +## Модель хранения + +| Объект | Данные и связи | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 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 остаются историческими строками. + +BE04 добавляет nullable JSON-колонку PartnerProgramField.solution_options для точного хранения вариантов, включая символ |. При null чтение использует прежний options; старые определения автоматически не переписываются. Новая форма сохраняется целиком через solution-form. Прежний project-form не изменяет определения нового конструктора, в том числе после технического выключения режима. + +## Ревизии + +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, solution_creation_disabled, 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 1.16 создание по recipient_user_id требует заполненную role1–128 с непробельным символом; PATCH не очищает переданную role, но может менять другие поля без нее. Nullable role сохраняется в ответах и исторических записях: их чтение/принятие/отклонение/отзыв не требуют искусственного заполнения. 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, solution_creation_disabled, 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, file_in_use | + +Это 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`. + +## Изменение 1.14 Первая сдача организатором + +Два отдельных POST management/solutions/actions/preview и apply сохраняют существующее organizer/admin исключение поздней первой сдачи. action=submit и 1–10000 safe Solution IDs; signed preview на 10 минут привязан к actor/program/IDs и сохраненным материалам/составу/форме/policy/files. Предпросмотр не пишет, apply повторяет проверки атомарно и создает обычный accepted-only snapshot. Уже сданные данные/дата и captain deadline неизменны. Nullable token/can_apply/counts/per-solution Issue[] и changed_count/already_submitted_count зафиксированы. Ошибки C00/private,no-store. 125 schemas/47 operations/185 scenarios; прежние 120/45/161 структурно неизменны. Миграций нет. UI ожидает D02; post-submission case/detach/removal сюда не входят. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json index 4d3a86d5a..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.0.0", + "contract_version": "1.21.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, @@ -2156,7 +2166,8 @@ "status": 409, "request": { "expected_solution_version": 8, - "recipient_user_id": 9 + "recipient_user_id": 9, + "role": "Аналитик" }, "response": { "code": "duplicate_pending_invite", @@ -2171,7 +2182,8 @@ "status": 409, "request": { "expected_solution_version": 8, - "recipient_user_id": 9 + "recipient_user_id": 9, + "role": "Аналитик" }, "response": { "code": "team_max_size", @@ -3218,7 +3230,8 @@ "can_edit": true, "can_manage_team": true, "can_invite": true, - "can_submit": true + "can_submit": true, + "can_leave": false }, "readiness": { "is_ready": true, @@ -3623,7 +3636,8 @@ "status": 409, "request": { "expected_solution_version": 10, - "recipient_user_id": 9 + "recipient_user_id": 9, + "role": "Аналитик" }, "response": { "code": "team_frozen", @@ -3811,7 +3825,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 +4026,8 @@ "can_edit": false, "can_manage_team": false, "can_invite": false, - "can_submit": false + "can_submit": false, + "can_leave": false }, "readiness": null } @@ -4031,6 +4047,8604 @@ "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 + } + ] + } + }, + { + "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 + } + ] + } + }, + { + "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": {} + } + }, + { + "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": [ + "Цифровой сервис" + ] + } + }, + { + "name": "solution_widget_participant_snapshot", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "participant", + "is_competitive": true, + "storage_mode": "solution", + "participant": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "case_provided": true, + "case_name": "Цифровой сервис", + "stage": "evaluated", + "submission_open": false + } + } + }, + { + "name": "solution_widget_participant_draft_without_case", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "participant", + "is_competitive": true, + "storage_mode": "solution", + "participant": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "case_provided": true, + "case_name": null, + "stage": "not_submitted", + "submission_open": true + } + } + }, + { + "name": "solution_widget_participant_noncompetitive", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "participant", + "is_competitive": false, + "storage_mode": "solution", + "participant": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "case_provided": true, + "case_name": "Цифровой сервис", + "stage": "not_applicable", + "submission_open": false + } + } + }, + { + "name": "solution_widget_organizer", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "organizer", + "is_competitive": true, + "storage_mode": "solution", + "organizer": { + "participants": 5, + "solutions": 2, + "submitted_solutions": 1, + "participants_without_solution": 2 + } + } + }, + { + "name": "solution_widget_organizer_noncompetitive", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "organizer", + "is_competitive": false, + "storage_mode": "solution", + "organizer": { + "participants": 5, + "solutions": 2, + "submitted_solutions": null, + "participants_without_solution": 2 + } + } + }, + { + "name": "solution_widget_expert_zero_complete", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "expert", + "is_competitive": true, + "storage_mode": "solution", + "expert": { + "mode": "distributed", + "assigned": 1, + "remaining": 0, + "evaluation_ends": "2026-10-24T18:00:00Z" + } + } + }, + { + "name": "solution_widget_expert_open", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "expert", + "is_competitive": true, + "storage_mode": "solution", + "expert": { + "mode": "open", + "assigned": null, + "remaining": null, + "evaluation_ends": "2026-10-24T18:00:00Z" + } + } + }, + { + "name": "solution_widget_expert_empty", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "expert", + "is_competitive": true, + "storage_mode": "solution", + "expert": { + "mode": "distributed", + "assigned": 0, + "remaining": 0, + "evaluation_ends": "2026-10-24T18:00:00Z" + } + } + }, + { + "name": "legacy_widget_participant", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "participant", + "is_competitive": true, + "participant": { + "case_provided": true, + "case_name": "Цифровой сервис", + "stage": "evaluated", + "submission_open": false, + "participant_project": { + "id": 55, + "name": "Исторический проект", + "program_link_id": 65 + } + } + } + }, + { + "name": "legacy_widget_organizer", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "organizer", + "is_competitive": true, + "organizer": { + "participants": 5, + "projects": 2, + "submitted_solutions": 1, + "participants_without_project": 2 + } + } + }, + { + "name": "legacy_widget_expert", + "operation_id": "getProgramRoleWidget", + "status": 200, + "response": { + "program_id": 12, + "role": "expert", + "is_competitive": true, + "expert": { + "mode": "distributed", + "assigned": 1, + "remaining": 0, + "evaluation_ends": "2026-10-24T18:00:00Z" + } + } + }, + { + "name": "widget_forbidden", + "operation_id": "getProgramRoleWidget", + "status": 403, + "response": { + "detail": "Аналитика доступна только участникам этой программы." + } + }, + { + "name": "widget_ambiguous_membership", + "operation_id": "getProgramRoleWidget", + "status": 409, + "response": { + "detail": "Связи заявки требуют проверки организатором." + } + }, + { + "name": "solution_case_analytics_competitive", + "operation_id": "getSolutionCaseAnalytics", + "status": 200, + "response": { + "program_id": 12, + "configured": true, + "submission_applicable": true, + "items": [ + { + "name": "Экология", + "participants_total": 3, + "solutions_total": 2, + "not_submitted": 1, + "submitted": 1 + }, + { + "name": "Технологии", + "participants_total": 0, + "solutions_total": 0, + "not_submitted": 0, + "submitted": 0 + } + ], + "without_case": { + "participants_total": 1, + "solutions_total": 1, + "not_submitted": 1, + "submitted": 0 + } + } + }, + { + "name": "solution_case_analytics_noncompetitive", + "operation_id": "getSolutionCaseAnalytics", + "status": 200, + "response": { + "program_id": 12, + "configured": true, + "submission_applicable": false, + "items": [ + { + "name": "Экология", + "participants_total": 3, + "solutions_total": 2, + "not_submitted": null, + "submitted": null + }, + { + "name": "Технологии", + "participants_total": 0, + "solutions_total": 0, + "not_submitted": null, + "submitted": null + } + ], + "without_case": { + "participants_total": 1, + "solutions_total": 1, + "not_submitted": null, + "submitted": null + } + } + }, + { + "name": "solution_case_analytics_unconfigured", + "operation_id": "getSolutionCaseAnalytics", + "status": 200, + "response": { + "program_id": 12, + "configured": false, + "submission_applicable": true, + "items": [], + "without_case": { + "participants_total": 0, + "solutions_total": 0, + "not_submitted": 0, + "submitted": 0 + } + } + }, + { + "name": "solution_case_analytics_forbidden", + "operation_id": "getSolutionCaseAnalytics", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Аналитика доступна организатору программы.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_case_analytics_anonymous", + "operation_id": "getSolutionCaseAnalytics", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Требуется авторизация.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_case_analytics_missing", + "operation_id": "getSolutionCaseAnalytics", + "status": 404, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_case_analytics_legacy", + "operation_id": "getSolutionCaseAnalytics", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Программа использует заявки на основе проектов.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_case_analytics_unexpected_query", + "operation_id": "getSolutionCaseAnalytics", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_solution_analytics_distributed", + "operation_id": "getSolutionManagerAnalytics", + "status": 200, + "response": { + "program_id": 12, + "summary": { + "participants": { + "total": 3 + }, + "solutions": { + "total": 1 + }, + "experts": { + "total": 1 + }, + "participant_regions": { + "total": 1, + "items": [ + { + "name": "Москва", + "count": 3 + } + ] + } + }, + "participant_funnel": { + "registrations": 3, + "unique_participants": 3, + "visited_participants": 2, + "onboarded_participants": 2, + "with_team": 2, + "captains": 1, + "submitted_captains": 1 + }, + "solution_funnel": { + "created": 1, + "submission_applicable": true, + "not_submitted": 0, + "submitted": 1, + "evaluated": 0 + }, + "evaluation_status": { + "applicable": true, + "mode": "distributed", + "required_evaluators": 2, + "max_evaluations_per_solution": 3, + "assignments": { + "total": 1, + "pending": 1, + "evaluated": 0 + }, + "solutions": { + "submitted": 1, + "awaiting_evaluation": 1, + "partially_evaluated": 0, + "evaluated": 0 + } + }, + "attention": { + "participants_without_visit": 1, + "participants_without_onboarding": 1, + "participants_without_team": 1, + "solutions_awaiting_evaluation": 1, + "solutions_not_submitted": 0, + "delayed_experts": { + "total": 0, + "items": [] + } + }, + "activity": [ + { + "date": "2026-10-24", + "registrations": 3, + "submitted_solutions": 1 + } + ], + "cases": { + "program_id": 12, + "configured": true, + "submission_applicable": true, + "items": [ + { + "name": "Экология", + "participants_total": 2, + "solutions_total": 1, + "not_submitted": 0, + "submitted": 1 + } + ], + "without_case": { + "participants_total": 0, + "solutions_total": 0, + "not_submitted": 0, + "submitted": 0 + } + }, + "field_coverage": [ + { + "field_id": 9, + "label": "Презентация", + "type": "file", + "purpose": "presentation", + "applicable": 1, + "answered": 0, + "required_applicable": 0, + "required_unanswered": 0 + } + ] + } + }, + { + "name": "manager_solution_analytics_open", + "operation_id": "getSolutionManagerAnalytics", + "status": 200, + "response": { + "program_id": 12, + "summary": { + "participants": { + "total": 3 + }, + "solutions": { + "total": 1 + }, + "experts": { + "total": 1 + }, + "participant_regions": { + "total": 1, + "items": [ + { + "name": "Москва", + "count": 3 + } + ] + } + }, + "participant_funnel": { + "registrations": 3, + "unique_participants": 3, + "visited_participants": 2, + "onboarded_participants": 2, + "with_team": 2, + "captains": 1, + "submitted_captains": 1 + }, + "solution_funnel": { + "created": 1, + "submission_applicable": true, + "not_submitted": 0, + "submitted": 1, + "evaluated": 0 + }, + "evaluation_status": { + "applicable": true, + "mode": "open", + "required_evaluators": 2, + "max_evaluations_per_solution": 3, + "assignments": null, + "solutions": { + "submitted": 1, + "awaiting_evaluation": 1, + "partially_evaluated": 0, + "evaluated": 0 + } + }, + "attention": { + "participants_without_visit": 1, + "participants_without_onboarding": 1, + "participants_without_team": 1, + "solutions_awaiting_evaluation": 1, + "solutions_not_submitted": 0, + "delayed_experts": { + "total": 0, + "items": [] + } + }, + "activity": [ + { + "date": "2026-10-24", + "registrations": 3, + "submitted_solutions": 1 + } + ], + "cases": { + "program_id": 12, + "configured": true, + "submission_applicable": true, + "items": [ + { + "name": "Экология", + "participants_total": 2, + "solutions_total": 1, + "not_submitted": 0, + "submitted": 1 + } + ], + "without_case": { + "participants_total": 0, + "solutions_total": 0, + "not_submitted": 0, + "submitted": 0 + } + }, + "field_coverage": [ + { + "field_id": 9, + "label": "Презентация", + "type": "file", + "purpose": "presentation", + "applicable": 1, + "answered": 0, + "required_applicable": 0, + "required_unanswered": 0 + } + ] + } + }, + { + "name": "manager_solution_analytics_noncompetitive", + "operation_id": "getSolutionManagerAnalytics", + "status": 200, + "response": { + "program_id": 12, + "summary": { + "participants": { + "total": 3 + }, + "solutions": { + "total": 1 + }, + "experts": { + "total": 1 + }, + "participant_regions": { + "total": 1, + "items": [ + { + "name": "Москва", + "count": 3 + } + ] + } + }, + "participant_funnel": { + "registrations": 3, + "unique_participants": 3, + "visited_participants": 2, + "onboarded_participants": 2, + "with_team": 2, + "captains": 1, + "submitted_captains": null + }, + "solution_funnel": { + "created": 1, + "submission_applicable": false, + "not_submitted": null, + "submitted": null, + "evaluated": null + }, + "evaluation_status": { + "applicable": false, + "mode": "distributed", + "required_evaluators": 2, + "max_evaluations_per_solution": 3, + "assignments": null, + "solutions": null + }, + "attention": { + "participants_without_visit": 1, + "participants_without_onboarding": 1, + "participants_without_team": 1, + "solutions_awaiting_evaluation": null, + "solutions_not_submitted": null, + "delayed_experts": { + "total": 0, + "items": [] + } + }, + "activity": [ + { + "date": "2026-10-24", + "registrations": 3, + "submitted_solutions": null + } + ], + "cases": { + "program_id": 12, + "configured": true, + "submission_applicable": false, + "items": [ + { + "name": "Экология", + "participants_total": 2, + "solutions_total": 1, + "not_submitted": null, + "submitted": null + } + ], + "without_case": { + "participants_total": 0, + "solutions_total": 0, + "not_submitted": null, + "submitted": null + } + }, + "field_coverage": [ + { + "field_id": 9, + "label": "Презентация", + "type": "file", + "purpose": "presentation", + "applicable": 1, + "answered": 0, + "required_applicable": 0, + "required_unanswered": 0 + } + ] + } + }, + { + "name": "manager_solution_assignments_pending", + "operation_id": "getSolutionManagerAssignments", + "status": 200, + "response": { + "applicable": true, + "mode": "distributed", + "results": [ + { + "assignment_id": 801, + "expert": { + "expert_id": 40, + "user_id": 71, + "first_name": "Иван", + "last_name": "Тест", + "full_name": "Иван Тест", + "avatar": null + }, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "status": "pending", + "assigned_at": "2026-10-25T00:00:00Z", + "submitted": true, + "submitted_at": "2026-10-24T00:00:00Z", + "waiting_since": "2026-10-25T00:00:00Z", + "waiting_seconds": 3600 + } + ] + } + }, + { + "name": "manager_solution_assignments_completed", + "operation_id": "getSolutionManagerAssignments", + "status": 200, + "response": { + "applicable": true, + "mode": "distributed", + "results": [ + { + "assignment_id": 801, + "expert": { + "expert_id": 40, + "user_id": 71, + "first_name": "Иван", + "last_name": "Тест", + "full_name": "Иван Тест", + "avatar": null + }, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "status": "completed", + "assigned_at": "2026-10-25T00:00:00Z", + "submitted": true, + "submitted_at": "2026-10-24T00:00:00Z", + "waiting_since": null, + "waiting_seconds": null + } + ] + } + }, + { + "name": "manager_solution_assignment_zero_false_comment", + "operation_id": "getSolutionManagerAssignment", + "status": 200, + "response": { + "assignment_id": 801, + "expert": { + "expert_id": 40, + "user_id": 71, + "first_name": "Иван", + "last_name": "Тест", + "full_name": "Иван Тест", + "avatar": null + }, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "status": "completed", + "assigned_at": "2026-10-25T00:00:00Z", + "submitted": true, + "submitted_at": "2026-10-24T00:00:00Z", + "waiting_since": null, + "waiting_seconds": null, + "scores": [ + { + "criterion_id": 7, + "name": "Оценка", + "description": null, + "type": "int", + "min_value": 0, + "max_value": 10, + "value": "0", + "is_scored": true + }, + { + "criterion_id": 8, + "name": "Условие", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False", + "is_scored": true + }, + { + "criterion_id": 9, + "name": "Комментарий", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "", + "is_scored": true + } + ] + } + }, + { + "name": "manager_solution_list_awaiting", + "operation_id": "getSolutionManagerSolutions", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "submission_applicable": true, + "mode": "distributed", + "results": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "captain": { + "user_id": 5, + "full_name": "Капитан Тест", + "avatar": null + }, + "case_name": "Экология", + "submission_status": "submitted", + "evaluation_status": "awaiting_evaluation", + "created_at": "2026-10-23T00:00:00Z", + "submitted_at": "2026-10-24T00:00:00Z", + "assignments_total": 1, + "assignments_completed": 0, + "waiting_reason": "no_completed_evaluations" + } + ] + } + }, + { + "name": "manager_solution_list_empty", + "operation_id": "getSolutionManagerSolutions", + "status": 200, + "response": { + "count": 0, + "next": null, + "previous": null, + "submission_applicable": false, + "mode": "open", + "results": [] + } + }, + { + "name": "getSolutionManagerAnalytics_forbidden", + "operation_id": "getSolutionManagerAnalytics", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAnalytics_anonymous", + "operation_id": "getSolutionManagerAnalytics", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAnalytics_missing", + "operation_id": "getSolutionManagerAnalytics", + "status": 404, + "response": { + "code": "not_found", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAnalytics_legacy", + "operation_id": "getSolutionManagerAnalytics", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAnalytics_invalid", + "operation_id": "getSolutionManagerAnalytics", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignments_forbidden", + "operation_id": "getSolutionManagerAssignments", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignments_anonymous", + "operation_id": "getSolutionManagerAssignments", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignments_missing", + "operation_id": "getSolutionManagerAssignments", + "status": 404, + "response": { + "code": "not_found", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignments_legacy", + "operation_id": "getSolutionManagerAssignments", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignments_invalid", + "operation_id": "getSolutionManagerAssignments", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignment_forbidden", + "operation_id": "getSolutionManagerAssignment", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignment_anonymous", + "operation_id": "getSolutionManagerAssignment", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignment_missing", + "operation_id": "getSolutionManagerAssignment", + "status": 404, + "response": { + "code": "not_found", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignment_legacy", + "operation_id": "getSolutionManagerAssignment", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerAssignment_invalid", + "operation_id": "getSolutionManagerAssignment", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerSolutions_forbidden", + "operation_id": "getSolutionManagerSolutions", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerSolutions_anonymous", + "operation_id": "getSolutionManagerSolutions", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerSolutions_missing", + "operation_id": "getSolutionManagerSolutions", + "status": 404, + "response": { + "code": "not_found", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerSolutions_legacy", + "operation_id": "getSolutionManagerSolutions", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "getSolutionManagerSolutions_invalid", + "operation_id": "getSolutionManagerSolutions", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "exportSolutionProgramAnalytics_forbidden", + "operation_id": "exportSolutionProgramAnalytics", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "exportSolutionProgramAnalytics_anonymous", + "operation_id": "exportSolutionProgramAnalytics", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "exportSolutionProgramAnalytics_missing", + "operation_id": "exportSolutionProgramAnalytics", + "status": 404, + "response": { + "code": "not_found", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "exportSolutionProgramAnalytics_legacy", + "operation_id": "exportSolutionProgramAnalytics", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "exportSolutionProgramAnalytics_invalid", + "operation_id": "exportSolutionProgramAnalytics", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Запрос недоступен.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_reminder_preview_selected", + "operation_id": "previewSolutionExpertReminder", + "status": 200, + "response": { + "token": "opaque-signed-example-token", + "selected_count": 3, + "recipient_count": 1, + "without_assignments_count": 1, + "unavailable_count": 0, + "preview_text": "Эксперт, здравствуйте! В программе остались незавершенные оценки решений.", + "recipients": [ + { + "user_id": 27, + "full_name": "Анна Петрова" + } + ], + "expires_in": 900 + }, + "request": { + "selection": { + "mode": "selected", + "ids": [ + 301, + 302, + 303 + ] + } + } + }, + { + "name": "solution_reminder_preview_all_search_excluded", + "operation_id": "previewSolutionExpertReminder", + "status": 200, + "response": { + "token": "opaque-signed-example-token", + "selected_count": 2, + "recipient_count": 1, + "without_assignments_count": 0, + "unavailable_count": 0, + "preview_text": "Эксперт, здравствуйте! В программе остались незавершенные оценки решений.", + "recipients": [ + { + "user_id": 27, + "full_name": "Анна Петрова" + } + ], + "expires_in": 900 + }, + "request": { + "search": "Сервис", + "selection": { + "mode": "all", + "excluded_ids": [ + 304 + ] + } + } + }, + { + "name": "solution_reminder_preview_no_assignments", + "operation_id": "previewSolutionExpertReminder", + "status": 200, + "response": { + "token": null, + "selected_count": 1, + "recipient_count": 0, + "without_assignments_count": 1, + "unavailable_count": 0, + "preview_text": "", + "recipients": [], + "expires_in": 900 + }, + "request": { + "selection": { + "mode": "selected", + "ids": [ + 303 + ] + } + } + }, + { + "name": "solution_reminder_preview_missing_email", + "operation_id": "previewSolutionExpertReminder", + "status": 200, + "response": { + "token": null, + "selected_count": 1, + "recipient_count": 0, + "without_assignments_count": 0, + "unavailable_count": 1, + "preview_text": "", + "recipients": [], + "expires_in": 900 + }, + "request": { + "selection": { + "mode": "selected", + "ids": [ + 302 + ] + } + } + }, + { + "name": "solution_reminder_preview_no_auth", + "operation_id": "previewSolutionExpertReminder", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_reminder_preview_forbidden", + "operation_id": "previewSolutionExpertReminder", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_reminder_preview_not_found", + "operation_id": "previewSolutionExpertReminder", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_reminder_preview_legacy", + "operation_id": "previewSolutionExpertReminder", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_reminder_preview_not_applicable", + "operation_id": "previewSolutionExpertReminder", + "status": 409, + "response": { + "code": "reminders_not_applicable", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_reminder_preview_invalid_selection", + "operation_id": "previewSolutionExpertReminder", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_reminder_confirm", + "operation_id": "confirmSolutionExpertReminder", + "status": 200, + "response": { + "created_count": 1, + "skipped_count": 0 + }, + "request": { + "token": "opaque-signed-example-token" + } + }, + { + "name": "solution_reminder_confirm_idempotent", + "operation_id": "confirmSolutionExpertReminder", + "status": 200, + "response": { + "created_count": 1, + "skipped_count": 0 + }, + "request": { + "token": "opaque-signed-example-token" + } + }, + { + "name": "solution_reminder_confirm_completed_before_confirm", + "operation_id": "confirmSolutionExpertReminder", + "status": 200, + "response": { + "created_count": 0, + "skipped_count": 1 + }, + "request": { + "token": "opaque-signed-example-token" + } + }, + { + "name": "solution_reminder_confirm_bad_token", + "operation_id": "confirmSolutionExpertReminder", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + }, + "request": { + "token": "bad-token" + } + }, + { + "name": "solution_reminder_confirm_cross_actor", + "operation_id": "confirmSolutionExpertReminder", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + }, + "request": { + "token": "another-actor-token" + } + }, + { + "name": "published_solutions_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_solutions_empty", + "operation_id": "getPublishedSolutions", + "status": 200, + "response": { + "program_id": 14, + "count": 0, + "next": null, + "previous": null, + "results": [] + } + }, + { + "name": "published_solutions_search_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_solution_zero_false_file", + "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_solution_saved_partial_without_submit", + "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": [], + "team": { + "members": [ + { + "user": { + "id": 11, + "first_name": "Иван", + "last_name": "Токарев", + "avatar_url": null + }, + "role": "Капитан", + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + } + }, + { + "name": "published_solution_empty_form_captain_only", + "operation_id": "getPublishedSolution", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 14, + "solution_id": 501, + "name": "Прототип сервиса" + }, + "published_at": "2026-10-25T06:00:00Z", + "fields": [], + "answers": [], + "team": { + "members": [ + { + "user": { + "id": 11, + "first_name": "Иван", + "last_name": "Токарев", + "avatar_url": null + }, + "role": "Капитан", + "specialization": null, + "is_captain": true + } + ], + "accepted_count": 1 + } + } + }, + { + "name": "published_solution_from_immutable_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_solution_list_legacy", + "operation_id": "getPublishedSolutions", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_solution_list_competitive_pending_rule", + "operation_id": "getPublishedSolutions", + "status": 200, + "response": { + "program_id": 14, + "count": 0, + "next": null, + "previous": null, + "results": [] + } + }, + { + "name": "published_solution_list_missing_program", + "operation_id": "getPublishedSolutions", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_solution_list_invalid_query", + "operation_id": "getPublishedSolutions", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_solution_detail_unpublished", + "operation_id": "getPublishedSolution", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_solution_detail_other_program", + "operation_id": "getPublishedSolution", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_solution_detail_unsafe_id", + "operation_id": "getPublishedSolution", + "status": 404, + "response": { + "code": "not_found", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_solution_detail_bad_authorization", + "operation_id": "getPublishedSolution", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "published_solution_list_bad_authorization", + "operation_id": "getPublishedSolutions", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Контролируемая ошибка.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_solutions_field_present_zero", + "operation_id": "getSolutionManagerSolutions", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "submission_applicable": true, + "mode": "distributed", + "results": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "captain": { + "user_id": 5, + "full_name": "Капитан Тест", + "avatar": null + }, + "case_name": "Экология", + "submission_status": "submitted", + "evaluation_status": "awaiting_evaluation", + "created_at": "2026-10-23T00:00:00Z", + "submitted_at": "2026-10-24T00:00:00Z", + "assignments_total": 1, + "assignments_completed": 0, + "waiting_reason": "no_completed_evaluations" + } + ] + } + }, + { + "name": "manager_solutions_field_present_false", + "operation_id": "getSolutionManagerSolutions", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "submission_applicable": true, + "mode": "distributed", + "results": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "captain": { + "user_id": 5, + "full_name": "Капитан Тест", + "avatar": null + }, + "case_name": "Экология", + "submission_status": "submitted", + "evaluation_status": "awaiting_evaluation", + "created_at": "2026-10-23T00:00:00Z", + "submitted_at": "2026-10-24T00:00:00Z", + "assignments_total": 1, + "assignments_completed": 0, + "waiting_reason": "no_completed_evaluations" + } + ] + } + }, + { + "name": "manager_solutions_field_missing", + "operation_id": "getSolutionManagerSolutions", + "status": 200, + "response": { + "count": 0, + "next": null, + "previous": null, + "submission_applicable": false, + "mode": "open", + "results": [] + } + }, + { + "name": "manager_solutions_field_not_applicable_snapshot", + "operation_id": "getSolutionManagerSolutions", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "submission_applicable": true, + "mode": "distributed", + "results": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "captain": { + "user_id": 5, + "full_name": "Капитан Тест", + "avatar": null + }, + "case_name": "Экология", + "submission_status": "submitted", + "evaluation_status": "awaiting_evaluation", + "created_at": "2026-10-23T00:00:00Z", + "submitted_at": "2026-10-24T00:00:00Z", + "assignments_total": 1, + "assignments_completed": 0, + "waiting_reason": "no_completed_evaluations" + } + ] + } + }, + { + "name": "manager_solutions_field_historical_snapshot", + "operation_id": "getSolutionManagerSolutions", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "submission_applicable": true, + "mode": "distributed", + "results": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "captain": { + "user_id": 5, + "full_name": "Капитан Тест", + "avatar": null + }, + "case_name": "Экология", + "submission_status": "submitted", + "evaluation_status": "awaiting_evaluation", + "created_at": "2026-10-23T00:00:00Z", + "submitted_at": "2026-10-24T00:00:00Z", + "assignments_total": 1, + "assignments_completed": 0, + "waiting_reason": "no_completed_evaluations" + } + ] + } + }, + { + "name": "manager_solutions_field_noncompetitive_empty", + "operation_id": "getSolutionManagerSolutions", + "status": 200, + "response": { + "count": 0, + "next": null, + "previous": null, + "submission_applicable": false, + "mode": "open", + "results": [] + } + }, + { + "name": "manager_solutions_field_required", + "operation_id": "getSolutionManagerSolutions", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры фильтра.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_solutions_field_foreign_or_unknown", + "operation_id": "getSolutionManagerSolutions", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры фильтра.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_solutions_field_unsafe", + "operation_id": "getSolutionManagerSolutions", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры фильтра.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_solutions_answer_state_invalid", + "operation_id": "getSolutionManagerSolutions", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры фильтра.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_export_field_required", + "operation_id": "exportSolutionProgramAnalytics", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры фильтра.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_export_field_foreign_or_unknown", + "operation_id": "exportSolutionProgramAnalytics", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры фильтра.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_export_filters_unsupported_kind", + "operation_id": "exportSolutionProgramAnalytics", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры фильтра.", + "errors": [], + "meta": {} + } + }, + { + "name": "manager_export_answer_state_invalid", + "operation_id": "exportSolutionProgramAnalytics", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры фильтра.", + "errors": [], + "meta": {} + } + }, + { + "name": "context_creation_disabled", + "operation_id": "getSolutionContext", + "status": 200, + "response": { + "program_id": 12, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-30T18:00:00Z", + "can_create": false, + "blocked_reason": "solution_creation_disabled", + "current_application": null + } + }, + { + "name": "create_creation_disabled", + "operation_id": "createSolution", + "status": 409, + "request": {}, + "response": { + "code": "solution_creation_disabled", + "detail": "Создание новых решений временно приостановлено. Ранее созданные заявки доступны.", + "errors": [], + "meta": {} + } + }, + { + "name": "create_existing_creation_disabled", + "operation_id": "createSolution", + "status": 200, + "request": {}, + "response": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00: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": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "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 + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "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": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + { + "name": "solution_action_preview_ready", + "operation_id": "previewSolutionSubmitAction", + "status": 200, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Готовое решение" + }, + "errors": [] + } + ], + "ready_count": 1, + "already_submitted_count": 0, + "can_apply": true, + "preview_token": "opaque-preview-token" + }, + "request": { + "action": "submit", + "ids": [ + 301 + ] + } + }, + { + "name": "solution_action_preview_incomplete", + "operation_id": "previewSolutionSubmitAction", + "status": 200, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Готовое решение" + }, + "errors": [ + { + "path": "answers", + "code": "required", + "field_id": 1001, + "detail": "Заполните обязательное поле." + } + ] + } + ], + "ready_count": 0, + "already_submitted_count": 0, + "can_apply": false, + "preview_token": null + }, + "request": { + "action": "submit", + "ids": [ + 301 + ] + } + }, + { + "name": "solution_action_preview_already", + "operation_id": "previewSolutionSubmitAction", + "status": 200, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Готовое решение" + }, + "errors": [] + } + ], + "ready_count": 0, + "already_submitted_count": 1, + "can_apply": false, + "preview_token": null + }, + "request": { + "action": "submit", + "ids": [ + 301 + ] + } + }, + { + "name": "solution_action_preview_mixed", + "operation_id": "previewSolutionSubmitAction", + "status": 200, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Готовое решение" + }, + "errors": [] + }, + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 302, + "name": "Готовое решение" + }, + "errors": [] + } + ], + "ready_count": 1, + "already_submitted_count": 1, + "can_apply": true, + "preview_token": "opaque-preview-token" + }, + "request": { + "action": "submit", + "ids": [ + 301, + 302 + ] + } + }, + { + "name": "solution_action_preview_noncompetitive", + "operation_id": "previewSolutionSubmitAction", + "status": 409, + "response": { + "code": "not_competitive", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ] + } + }, + { + "name": "solution_action_preview_forbidden", + "operation_id": "previewSolutionSubmitAction", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ] + } + }, + { + "name": "solution_action_preview_anonymous", + "operation_id": "previewSolutionSubmitAction", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ] + } + }, + { + "name": "solution_action_preview_foreign", + "operation_id": "previewSolutionSubmitAction", + "status": 404, + "response": { + "code": "not_found", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ] + } + }, + { + "name": "solution_action_preview_invalid", + "operation_id": "previewSolutionSubmitAction", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_action_preview_legacy", + "operation_id": "previewSolutionSubmitAction", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ] + } + }, + { + "name": "solution_action_apply_ready", + "operation_id": "applySolutionSubmitAction", + "status": 200, + "response": { + "changed_count": 1, + "already_submitted_count": 0 + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_mixed", + "operation_id": "applySolutionSubmitAction", + "status": 200, + "response": { + "changed_count": 1, + "already_submitted_count": 1 + }, + "request": { + "action": "submit", + "ids": [ + 301, + 302 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_replay", + "operation_id": "applySolutionSubmitAction", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_bad_signature", + "operation_id": "applySolutionSubmitAction", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_expired", + "operation_id": "applySolutionSubmitAction", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_changed_state", + "operation_id": "applySolutionSubmitAction", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_other_manager", + "operation_id": "applySolutionSubmitAction", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_forbidden", + "operation_id": "applySolutionSubmitAction", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_foreign", + "operation_id": "applySolutionSubmitAction", + "status": 404, + "response": { + "code": "not_found", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_noncompetitive", + "operation_id": "applySolutionSubmitAction", + "status": 409, + "response": { + "code": "not_competitive", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_legacy", + "operation_id": "applySolutionSubmitAction", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_invalid", + "operation_id": "applySolutionSubmitAction", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + } + }, + { + "name": "solution_action_apply_file_invalid", + "operation_id": "applySolutionSubmitAction", + "status": 400, + "response": { + "code": "invalid_answers", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "solution_action_apply_anonymous", + "operation_id": "applySolutionSubmitAction", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Проверьте действие.", + "errors": [], + "meta": {} + }, + "request": { + "action": "submit", + "ids": [ + 301 + ], + "preview_token": "opaque-preview-token" + } + }, + { + "name": "case_action_preview_ready", + "operation_id": "previewSolutionCaseChange", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "case_field_id": 401, + "current_case": "Город", + "case_name": "Образование | наука", + "solution_version": 7, + "errors": [], + "can_apply": true, + "preview_token": "opaque-case-preview-token", + "is_submitted": false, + "removed_assignments_count": 0, + "score_count": 0 + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_empty_current", + "operation_id": "previewSolutionCaseChange", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "case_field_id": 401, + "current_case": null, + "case_name": "Образование | наука", + "solution_version": 7, + "errors": [], + "can_apply": true, + "preview_token": "opaque-case-preview-token", + "is_submitted": false, + "removed_assignments_count": 0, + "score_count": 0 + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_noncompetitive", + "operation_id": "previewSolutionCaseChange", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 13, + "solution_id": 302, + "name": "Решение команды" + }, + "case_field_id": 401, + "current_case": "Город", + "case_name": "Образование | наука", + "solution_version": 7, + "errors": [], + "can_apply": true, + "preview_token": "opaque-case-preview-token", + "is_submitted": false, + "removed_assignments_count": 0, + "score_count": 0 + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_same", + "operation_id": "previewSolutionCaseChange", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "case_field_id": 401, + "current_case": "Образование | наука", + "case_name": "Образование | наука", + "solution_version": 7, + "errors": [ + { + "path": "case_name", + "code": "case_unchanged", + "detail": "Выберите другой кейс.", + "field_id": 401 + } + ], + "can_apply": false, + "preview_token": null, + "is_submitted": false, + "removed_assignments_count": 0, + "score_count": 0 + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_submitted", + "operation_id": "previewSolutionCaseChange", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "case_field_id": 401, + "current_case": "Город", + "case_name": "Образование | наука", + "solution_version": 7, + "errors": [], + "can_apply": true, + "preview_token": "opaque-submitted-case-preview-token", + "is_submitted": true, + "removed_assignments_count": 0, + "score_count": 0 + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_evaluation", + "operation_id": "previewSolutionCaseChange", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "case_field_id": 401, + "current_case": "Город", + "case_name": "Образование | наука", + "solution_version": 7, + "errors": [ + { + "path": "case_name", + "code": "evaluation_started", + "detail": "У решения уже есть данные оценивания.", + "field_id": 401 + } + ], + "can_apply": false, + "preview_token": null, + "is_submitted": false, + "removed_assignments_count": 0, + "score_count": 0 + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_bad_case", + "operation_id": "previewSolutionCaseChange", + "status": 400, + "response": { + "code": "invalid_answers", + "detail": "Выберите актуальный кейс.", + "errors": [ + { + "path": "case_name", + "code": "case_unavailable", + "detail": "Вариант недоступен.", + "field_id": 401 + } + ], + "meta": {} + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_anonymous", + "operation_id": "previewSolutionCaseChange", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Войдите в аккаунт.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_foreign_manager", + "operation_id": "previewSolutionCaseChange", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_foreign_solution", + "operation_id": "previewSolutionCaseChange", + "status": 404, + "response": { + "code": "not_found", + "detail": "Решение не найдено.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_legacy", + "operation_id": "previewSolutionCaseChange", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Программа использует проектную заявку.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_preview_unconfigured", + "operation_id": "previewSolutionCaseChange", + "status": 409, + "response": { + "code": "form_not_configured", + "detail": "Настройте форму решения.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_apply_changed", + "operation_id": "applySolutionCaseChange", + "status": 200, + "response": { + "solution_id": 301, + "solution_version": 8, + "updated_at": "2026-10-25T10:00:00Z" + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_stale", + "operation_id": "applySolutionCaseChange", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решения изменились. Повторите предпросмотр.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_replay", + "operation_id": "applySolutionCaseChange", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решения изменились. Повторите предпросмотр.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_expired", + "operation_id": "applySolutionCaseChange", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решения изменились. Повторите предпросмотр.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_cross_actor", + "operation_id": "applySolutionCaseChange", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решения изменились. Повторите предпросмотр.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_cross_solution", + "operation_id": "applySolutionCaseChange", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решения изменились. Повторите предпросмотр.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_cross_case", + "operation_id": "applySolutionCaseChange", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решения изменились. Повторите предпросмотр.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_submitted", + "operation_id": "applySolutionCaseChange", + "status": 200, + "response": { + "solution_id": 301, + "solution_version": 8, + "updated_at": "2026-10-25T12:00:00Z" + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_bad_case", + "operation_id": "applySolutionCaseChange", + "status": 400, + "response": { + "code": "invalid_answers", + "detail": "Выберите актуальный кейс.", + "errors": [ + { + "path": "case_name", + "code": "case_unavailable", + "detail": "Вариант недоступен.", + "field_id": 401 + } + ], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_foreign_manager", + "operation_id": "applySolutionCaseChange", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "case_action_apply_invalid_payload", + "operation_id": "applySolutionCaseChange", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [], + "meta": {} + } + }, + { + "name": "case_action_apply_unknown_query", + "operation_id": "applySolutionCaseChange", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Неизвестный параметр.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "opaque-case-preview-token" + } + }, + { + "name": "invitation_role_one_character", + "operation_id": "createSolutionInvitation", + "status": 201, + "request": { + "expected_solution_version": 8, + "recipient_user_id": 9, + "role": "Р" + }, + "response": { + "invitation": { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Р", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 9 + } + }, + { + "name": "invitation_role_maximum", + "operation_id": "createSolutionInvitation", + "status": 201, + "request": { + "expected_solution_version": 8, + "recipient_user_id": 9, + "role": "РРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРР" + }, + "response": { + "invitation": { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "РРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРР", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 9 + } + }, + { + "name": "invitation_missing_role", + "operation_id": "createSolutionInvitation", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте поля приглашения.", + "errors": [ + { + "path": "role", + "code": "invalid", + "detail": "Укажите роль от 1 до 128 символов.", + "field_id": null + } + ], + "meta": {} + } + }, + { + "name": "invitation_blank_role", + "operation_id": "createSolutionInvitation", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте поля приглашения.", + "errors": [ + { + "path": "role", + "code": "invalid", + "detail": "Укажите роль от 1 до 128 символов.", + "field_id": null + } + ], + "meta": {} + } + }, + { + "name": "invitation_null_role", + "operation_id": "createSolutionInvitation", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте поля приглашения.", + "errors": [ + { + "path": "role", + "code": "invalid", + "detail": "Укажите роль от 1 до 128 символов.", + "field_id": null + } + ], + "meta": {} + } + }, + { + "name": "invitation_update_null_role", + "operation_id": "updateSolutionInvitation", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте поля приглашения.", + "errors": [ + { + "path": "role", + "code": "invalid", + "detail": "Укажите роль от 1 до 128 символов.", + "field_id": null + } + ], + "meta": {} + } + }, + { + "name": "invitation_legacy_null_role", + "operation_id": "getSolutionInvitation", + "status": 200, + "response": { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": null, + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + } + }, + { + "name": "invitation_legacy_null_role_accept", + "operation_id": "acceptSolutionInvitation", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 701, + "version": 2, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": null, + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "accepted", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "solution_version": 9 + } + }, + { + "name": "case_action_preview_submitted_assignment_effects", + "operation_id": "previewSolutionCaseChange", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "case_field_id": 401, + "current_case": "Город", + "case_name": "Образование | наука", + "solution_version": 7, + "errors": [], + "can_apply": true, + "preview_token": "opaque-submitted-case-preview-token", + "is_submitted": true, + "removed_assignments_count": 1, + "score_count": 2 + }, + "request": { + "case_name": "Образование | наука" + } + }, + { + "name": "case_action_apply_submitted_replay", + "operation_id": "applySolutionCaseChange", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решения изменились. Подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "case_name": "Образование | наука", + "preview_token": "used-case-preview-token" + } + }, + { + "name": "admin_history_list", + "operation_id": "listSolutionAdminChanges", + "status": 200, + "response": { + "count": 1, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 71, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "action": "change_case", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "case_field_id": 1001, + "previous_case": "Цифровой сервис", + "case_name": "Образование | наука", + "removed_assignments_count": 1, + "score_count": 2, + "was_submitted": true + } + ] + }, + "query": { + "offset": 0, + "limit": 20 + } + }, + { + "name": "admin_history_detail", + "operation_id": "readSolutionAdminChange", + "status": 200, + "response": { + "change": { + "id": 71, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "action": "change_case", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "case_field_id": 1001, + "previous_case": "Цифровой сервис", + "case_name": "Образование | наука", + "removed_assignments_count": 1, + "score_count": 2, + "was_submitted": true + }, + "before_snapshot": { + "name": "Сервис для студентов", + "captain_id": 7, + "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 + } + ], + "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" + } + } + ], + "submitted_at": "2026-10-23T10:05:00Z" + }, + "after_snapshot": { + "name": "Сервис для студентов", + "captain_id": 7, + "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 + } + ], + "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" + } + } + ], + "submitted_at": "2026-10-23T10:05:00Z" + } + } + }, + { + "name": "admin_history_draft_detail", + "operation_id": "readSolutionAdminChange", + "status": 200, + "response": { + "change": { + "id": 72, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "action": "change_case", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "case_field_id": 1001, + "previous_case": "Цифровой сервис", + "case_name": "Образование | наука", + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false + }, + "before_snapshot": null, + "after_snapshot": null + } + }, + { + "name": "admin_history_empty", + "operation_id": "listSolutionAdminChanges", + "status": 200, + "response": { + "count": 0, + "offset": 100, + "limit": 20, + "results": [] + }, + "query": { + "offset": 100, + "limit": 20 + } + }, + { + "name": "admin_history_anonymous", + "operation_id": "listSolutionAdminChanges", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Требуется авторизация.", + "errors": [], + "meta": {} + } + }, + { + "name": "admin_history_foreign_manager", + "operation_id": "listSolutionAdminChanges", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "История доступна организатору программы.", + "errors": [], + "meta": {} + } + }, + { + "name": "admin_history_foreign_solution", + "operation_id": "listSolutionAdminChanges", + "status": 404, + "response": { + "code": "not_found", + "detail": "Решение не найдено.", + "errors": [], + "meta": {} + } + }, + { + "name": "admin_history_foreign_change", + "operation_id": "readSolutionAdminChange", + "status": 404, + "response": { + "code": "not_found", + "detail": "Изменение не найдено.", + "errors": [], + "meta": {} + } + }, + { + "name": "admin_history_invalid_pagination", + "operation_id": "listSolutionAdminChanges", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте параметры запроса.", + "errors": [], + "meta": {} + }, + "query": { + "limit": 0 + } + }, + { + "name": "admin_history_unknown_detail_query", + "operation_id": "readSolutionAdminChange", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Неизвестный параметр запроса.", + "errors": [], + "meta": {} + }, + "query": { + "all": true + } + }, + { + "name": "admin_history_legacy", + "operation_id": "listSolutionAdminChanges", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Программа использует заявки на основе проектов.", + "errors": [], + "meta": {} + } + }, + { + "name": "member_removal_preview_submitted", + "operation_id": "previewSolutionMemberRemoval", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "member": { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + }, + "solution_version": 7, + "is_submitted": true, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 2, + "errors": [], + "can_apply": true, + "preview_token": "opaque-member-removal-preview-token" + }, + "request": { + "user_id": 10 + } + }, + { + "name": "member_removal_preview_draft_pending", + "operation_id": "previewSolutionMemberRemoval", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "member": { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + }, + "solution_version": 7, + "is_submitted": false, + "remaining_member_count": 1, + "revoked_invitation_count": 1, + "score_count": 0, + "errors": [], + "can_apply": true, + "preview_token": "opaque-member-removal-preview-token" + }, + "request": { + "user_id": 10 + } + }, + { + "name": "member_removal_preview_captain", + "operation_id": "previewSolutionMemberRemoval", + "status": 200, + "response": { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "member": { + "user": { + "id": 9, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": true + }, + "solution_version": 7, + "is_submitted": true, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 2, + "errors": [ + { + "path": "user_id", + "code": "captain_cannot_be_removed", + "detail": "Капитана нельзя исключить из состава. Для него снимите заявку с участия.", + "field_id": null + } + ], + "can_apply": false, + "preview_token": null + }, + "request": { + "user_id": 9 + } + }, + { + "name": "member_removal_apply_submitted", + "operation_id": "applySolutionMemberRemoval", + "status": 200, + "response": { + "solution_id": 301, + "solution_version": 8, + "updated_at": "2026-10-25T15:00:00Z" + }, + "request": { + "user_id": 10, + "preview_token": "opaque-member-removal-preview-token" + } + }, + { + "name": "member_removal_anonymous", + "operation_id": "previewSolutionMemberRemoval", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10 + } + }, + { + "name": "member_removal_captain_denied", + "operation_id": "previewSolutionMemberRemoval", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10 + } + }, + { + "name": "member_removal_foreign_manager", + "operation_id": "previewSolutionMemberRemoval", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10 + } + }, + { + "name": "member_removal_foreign_solution", + "operation_id": "previewSolutionMemberRemoval", + "status": 404, + "response": { + "code": "not_found", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10 + } + }, + { + "name": "member_removal_missing_member", + "operation_id": "previewSolutionMemberRemoval", + "status": 404, + "response": { + "code": "not_found", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10 + } + }, + { + "name": "member_removal_legacy", + "operation_id": "previewSolutionMemberRemoval", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10 + } + }, + { + "name": "member_removal_replay", + "operation_id": "applySolutionMemberRemoval", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10, + "preview_token": "opaque-member-removal-preview-token" + } + }, + { + "name": "member_removal_stale", + "operation_id": "applySolutionMemberRemoval", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10, + "preview_token": "opaque-member-removal-preview-token" + } + }, + { + "name": "member_removal_expired", + "operation_id": "applySolutionMemberRemoval", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10, + "preview_token": "opaque-member-removal-preview-token" + } + }, + { + "name": "member_removal_changed_target", + "operation_id": "applySolutionMemberRemoval", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10, + "preview_token": "opaque-member-removal-preview-token" + } + }, + { + "name": "member_removal_missing_token", + "operation_id": "applySolutionMemberRemoval", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "invalid_request": { + "user_id": 10 + } + }, + { + "name": "member_removal_invalid_user", + "operation_id": "previewSolutionMemberRemoval", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "invalid_request": { + "user_id": 0 + } + }, + { + "name": "member_removal_unknown_query", + "operation_id": "previewSolutionMemberRemoval", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "user_id": 10 + }, + "query": { + "all": true + } + }, + { + "name": "member_removal_history_detail", + "operation_id": "readSolutionAdminChange", + "status": 200, + "response": { + "change": { + "id": 73, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "action": "remove_member", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_member": { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + }, + "revoked_invitation_count": 0 + }, + "before_snapshot": { + "name": "Сервис для студентов", + "captain_id": 7, + "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 + }, + { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + } + ], + "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" + } + } + ], + "submitted_at": "2026-10-23T10:05:00Z" + }, + "after_snapshot": { + "name": "Сервис для студентов", + "captain_id": 7, + "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 + } + ], + "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" + } + } + ], + "submitted_at": "2026-10-23T10:05:00Z" + } + } + }, + { + "name": "member_removal_history_draft", + "operation_id": "readSolutionAdminChange", + "status": 200, + "response": { + "change": { + "id": 73, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "action": "remove_member", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "removed_member": { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + }, + "revoked_invitation_count": 1 + }, + "before_snapshot": null, + "after_snapshot": null + } + }, + { + "name": "member_removal_history_mixed_list", + "operation_id": "listSolutionAdminChanges", + "status": 200, + "response": { + "count": 2, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 73, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "action": "remove_member", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_member": { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + }, + "revoked_invitation_count": 0 + }, + { + "id": 71, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "action": "change_case", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "case_field_id": 1001, + "previous_case": "Цифровой сервис", + "case_name": "Образование | наука", + "removed_assignments_count": 1, + "score_count": 2, + "was_submitted": true + } + ] + } + }, + { + "name": "archive_preview_submitted", + "operation_id": "previewSolutionArchive", + "status": 200, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 7, + "is_submitted": true, + "is_archived": false, + "score_count": 2, + "removed_assignments_count": 2, + "revoked_invitation_count": 1, + "errors": [] + } + ], + "score_count": 2, + "removed_assignments_count": 2, + "revoked_invitation_count": 1, + "can_apply": true, + "preview_token": "opaque-archive-preview-token" + }, + "request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_preview_draft", + "operation_id": "previewSolutionArchive", + "status": 200, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 7, + "is_submitted": false, + "is_archived": false, + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "errors": [] + } + ], + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "can_apply": true, + "preview_token": "opaque-archive-preview-token" + }, + "request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_preview_already_archived", + "operation_id": "previewSolutionArchive", + "status": 200, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 7, + "is_submitted": true, + "is_archived": true, + "score_count": 2, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [ + { + "path": "ids", + "code": "solution_archived", + "field_id": null, + "detail": "Решение уже снято с участия." + } + ] + } + ], + "score_count": 2, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "can_apply": false, + "preview_token": null + }, + "request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_apply", + "operation_id": "applySolutionArchive", + "status": 200, + "response": { + "changed_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 8, + "archived_at": "2026-10-25T15:00:00Z" + } + ] + }, + "request": { + "ids": [ + 301 + ], + "preview_token": "opaque-archive-preview-token" + } + }, + { + "name": "archive_anonymous", + "operation_id": "previewSolutionArchive", + "status": 401, + "response": { + "code": "authentication_required", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_captain_denied", + "operation_id": "previewSolutionArchive", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_foreign_manager", + "operation_id": "previewSolutionArchive", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_foreign_solution", + "operation_id": "previewSolutionArchive", + "status": 404, + "response": { + "code": "not_found", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_legacy", + "operation_id": "previewSolutionArchive", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_replay", + "operation_id": "applySolutionArchive", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "ids": [ + 301 + ], + "preview_token": "opaque-archive-preview-token" + } + }, + { + "name": "archive_stale", + "operation_id": "applySolutionArchive", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "ids": [ + 301 + ], + "preview_token": "opaque-archive-preview-token" + } + }, + { + "name": "archive_expired", + "operation_id": "applySolutionArchive", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "request": { + "ids": [ + 301 + ], + "preview_token": "opaque-archive-preview-token" + } + }, + { + "name": "archive_invalid_empty_selection", + "operation_id": "previewSolutionArchive", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "invalid_request": { + "ids": [] + } + }, + { + "name": "archive_invalid_duplicate_selection", + "operation_id": "previewSolutionArchive", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "invalid_request": { + "ids": [ + 301, + 301 + ] + } + }, + { + "name": "archive_missing_token", + "operation_id": "applySolutionArchive", + "status": 400, + "response": { + "code": "validation_error", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "invalid_request": { + "ids": [ + 301 + ] + } + }, + { + "name": "archive_read_submitted_owner", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 301, + "program_id": 12, + "version": 11, + "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": 0, + "reserved_count": 2, + "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": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "field_id": null, + "detail": "Решение снято с участия." + } + ] + }, + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:00:00Z", + "reason": "detached" + } + } + }, + { + "name": "archive_read_draft_owner", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 301, + "program_id": 12, + "version": 11, + "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": 0, + "reserved_count": 2, + "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": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "field_id": null, + "detail": "Решение снято с участия." + } + ] + }, + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:00:00Z", + "reason": "detached" + } + } + }, + { + "name": "archive_list_owned", + "operation_id": "listSolutions", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 301, + "program_id": 12, + "name": "Сервис для студентов", + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "accepted_count": 2, + "case_value": "Цифровой сервис", + "status": "draft", + "submitted_at": null, + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:00:00Z", + "reason": "detached" + } + } + ] + }, + "query": { + "participation": "archived" + } + }, + { + "name": "archive_default_active_empty", + "operation_id": "listSolutions", + "status": 200, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [] + } + }, + { + "name": "archive_owner_patch_frozen", + "operation_id": "updateSolution", + "status": 409, + "response": { + "code": "solution_archived", + "detail": "Проверьте данные и подтвердите действие заново.", + "errors": [], + "meta": {} + } + }, + { + "name": "archive_history_detail", + "operation_id": "readSolutionAdminChange", + "status": 200, + "response": { + "change": { + "id": 71, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T15:00:00Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 2, + "score_count": 2, + "was_submitted": true, + "archived_at": "2026-10-25T15:00:00Z", + "reason": "detached", + "revoked_invitation_count": 1 + }, + "before_snapshot": { + "name": "Сервис для студентов", + "captain_id": 7, + "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 + } + ], + "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" + } + } + ], + "submitted_at": "2026-10-23T10:05:00Z" + }, + "after_snapshot": { + "name": "Сервис для студентов", + "captain_id": 7, + "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 + } + ], + "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" + } + } + ], + "submitted_at": "2026-10-23T10:05:00Z" + }, + "archive_snapshot": { + "name": "Сервис для студентов", + "captain_id": 7, + "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 + } + ], + "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" + } + } + ], + "captured_at": "2026-10-25T15:00:00Z" + } + } + }, + { + "name": "archive_history_draft", + "operation_id": "readSolutionAdminChange", + "status": 200, + "response": { + "change": { + "id": 71, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T15:00:00Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T15:00:00Z", + "reason": "detached", + "revoked_invitation_count": 1 + }, + "before_snapshot": null, + "after_snapshot": null, + "archive_snapshot": { + "name": "Сервис для студентов", + "captain_id": 7, + "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 + } + ], + "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" + } + } + ], + "captured_at": "2026-10-25T15:00:00Z" + } + } + }, + { + "name": "registration_removal_preview_member_submitted", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 200, + "response": { + "registrations": [ + { + "registration_id": 601, + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 7, + "is_submitted": true, + "effect": "remove_members", + "removed_members": [ + { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + } + ], + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "score_count": 2 + } + ], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 1, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "score_count": 2, + "can_apply": true, + "errors": [], + "preview_token": "opaque-registration-removal-proof" + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_preview_pending_only", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 200, + "response": { + "registrations": [ + { + "registration_id": 601, + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 7, + "is_submitted": true, + "effect": "revoke_invitations", + "removed_members": [], + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "score_count": 2 + } + ], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "score_count": 2, + "can_apply": true, + "errors": [], + "preview_token": "opaque-registration-removal-proof" + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_preview_captain_submitted", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 200, + "response": { + "registrations": [ + { + "registration_id": 601, + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 7, + "is_submitted": true, + "effect": "archive", + "removed_members": [], + "removed_assignments_count": 2, + "revoked_invitation_count": 1, + "score_count": 2 + } + ], + "deleted_count": 1, + "archived_count": 1, + "removed_member_count": 0, + "removed_assignments_count": 2, + "revoked_invitation_count": 1, + "score_count": 2, + "can_apply": true, + "errors": [], + "preview_token": "opaque-registration-removal-proof" + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_preview_member_draft", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 200, + "response": { + "registrations": [ + { + "registration_id": 601, + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 7, + "is_submitted": false, + "effect": "remove_members", + "removed_members": [ + { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + } + ], + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "score_count": 0 + } + ], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 1, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "score_count": 0, + "can_apply": true, + "errors": [], + "preview_token": "opaque-registration-removal-proof" + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_preview_without_solution", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 200, + "response": { + "registrations": [ + { + "registration_id": 601, + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + } + } + ], + "effects": [], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0, + "can_apply": true, + "errors": [], + "preview_token": "opaque-registration-removal-proof" + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_apply_member", + "operation_id": "applySolutionRegistrationRemoval", + "status": 200, + "response": { + "audit_id": 801, + "deleted_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 8, + "change_id": 901, + "action": "remove_registrations" + } + ] + }, + "request": { + "registration_ids": [ + 601 + ], + "preview_token": "opaque-registration-removal-proof" + } + }, + { + "name": "registration_removal_apply_captain", + "operation_id": "applySolutionRegistrationRemoval", + "status": 200, + "response": { + "audit_id": 801, + "deleted_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "solution_version": 8, + "change_id": 901, + "action": "archive" + } + ] + }, + "request": { + "registration_ids": [ + 601 + ], + "preview_token": "opaque-registration-removal-proof" + } + }, + { + "name": "registration_removal_apply_without_solution", + "operation_id": "applySolutionRegistrationRemoval", + "status": 200, + "response": { + "audit_id": 801, + "deleted_count": 1, + "items": [] + }, + "request": { + "registration_ids": [ + 601 + ], + "preview_token": "opaque-registration-removal-proof" + } + }, + { + "name": "registration_removal_audit_list", + "operation_id": "listSolutionRegistrationRemovals", + "status": 200, + "response": { + "count": 1, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 801, + "program_id": 12, + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "registrations": [ + { + "registration_id": 601, + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + } + } + ], + "deleted_count": 1 + } + ] + } + }, + { + "name": "registration_removal_audit_detail", + "operation_id": "readSolutionRegistrationRemoval", + "status": 200, + "response": { + "audit": { + "id": 801, + "program_id": 12, + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "registrations": [ + { + "registration_id": 601, + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + } + } + ], + "deleted_count": 1 + }, + "changes": [ + { + "id": 73, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "revoked_invitation_count": 1, + "removed_members": [ + { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + } + ], + "registration_removal_id": 801 + } + ] + } + }, + { + "name": "registration_removal_audit_no_solution", + "operation_id": "readSolutionRegistrationRemoval", + "status": 200, + "response": { + "audit": { + "id": 801, + "program_id": 12, + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "registrations": [ + { + "registration_id": 601, + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + } + } + ], + "deleted_count": 1 + }, + "changes": [] + } + }, + { + "name": "registration_removal_solution_history", + "operation_id": "readSolutionAdminChange", + "status": 200, + "response": { + "change": { + "id": 73, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Решение команды" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Организатор", + "last_name": "Программы", + "avatar_url": null + }, + "actor_name": "Организатор Программы", + "created_at": "2026-10-25T12:00:00Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "revoked_invitation_count": 1, + "removed_members": [ + { + "user": { + "id": 10, + "first_name": "Участник", + "last_name": "Команды", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-24T10:00:00Z", + "is_captain": false + } + ], + "registration_removal_id": 801 + }, + "before_snapshot": null, + "after_snapshot": null + } + }, + { + "name": "registration_removal_stale", + "operation_id": "applySolutionRegistrationRemoval", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Срок сдачи завершен.", + "errors": [], + "meta": {} + }, + "request": { + "registration_ids": [ + 601 + ], + "preview_token": "opaque-registration-removal-proof" + } + }, + { + "name": "registration_removal_replay", + "operation_id": "applySolutionRegistrationRemoval", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Срок сдачи завершен.", + "errors": [], + "meta": {} + }, + "request": { + "registration_ids": [ + 601 + ], + "preview_token": "opaque-registration-removal-proof" + } + }, + { + "name": "registration_removal_expired", + "operation_id": "applySolutionRegistrationRemoval", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Срок сдачи завершен.", + "errors": [], + "meta": {} + }, + "request": { + "registration_ids": [ + 601 + ], + "preview_token": "opaque-registration-removal-proof" + } + }, + { + "name": "registration_removal_other_actor", + "operation_id": "applySolutionRegistrationRemoval", + "status": 409, + "response": { + "code": "solution_action_conflict", + "detail": "Срок сдачи завершен.", + "errors": [], + "meta": {} + }, + "request": { + "registration_ids": [ + 601 + ], + "preview_token": "opaque-registration-removal-proof" + } + }, + { + "name": "registration_removal_legacy_mode", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 409, + "response": { + "code": "solution_mode_unavailable", + "detail": "Срок сдачи завершен.", + "errors": [], + "meta": {} + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_mixed_legacy", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 409, + "response": { + "code": "legacy_application_conflict", + "detail": "Срок сдачи завершен.", + "errors": [], + "meta": {} + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_foreign_registration", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 404, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_participant", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 403, + "response": { + "code": "permission_denied", + "detail": "Сдать решение может только капитан.", + "errors": [], + "meta": {} + }, + "request": { + "registration_ids": [ + 601 + ] + } + }, + { + "name": "registration_removal_foreign_audit", + "operation_id": "readSolutionRegistrationRemoval", + "status": 404, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + } + }, + { + "name": "registration_removal_invalid_empty", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 400, + "response": { + "code": "required_program_fields", + "detail": "Заполните обязательные поля.", + "errors": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ], + "meta": {} + }, + "invalid_request": { + "registration_ids": [] + } + }, + { + "name": "registration_removal_invalid_duplicate", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 400, + "response": { + "code": "required_program_fields", + "detail": "Заполните обязательные поля.", + "errors": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ], + "meta": {} + }, + "invalid_request": { + "registration_ids": [ + 601, + 601 + ] + } + }, + { + "name": "registration_removal_invalid_user_id_instead", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 400, + "response": { + "code": "required_program_fields", + "detail": "Заполните обязательные поля.", + "errors": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ], + "meta": {} + }, + "invalid_request": { + "user_id": 10 + } + }, + { + "name": "registration_removal_invalid_unknown", + "operation_id": "previewSolutionRegistrationRemoval", + "status": 400, + "response": { + "code": "required_program_fields", + "detail": "Заполните обязательные поля.", + "errors": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ], + "meta": {} + }, + "invalid_request": { + "registration_ids": [ + 601 + ], + "project_id": 1 + } + }, + { + "name": "rate_archived_solution_open", + "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": "solution_archived", + "detail": "Решение снято с участия. Материалы доступны для просмотра.", + "errors": [], + "meta": {} + } + }, + { + "name": "rate_archived_solution_distributed", + "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": "solution_archived", + "detail": "Решение снято с участия. Материалы доступны для просмотра.", + "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 f61c50d2a..c425736cd 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.0.0", - "description": "Контракт для реализации и моков. Новые endpoints пока не реализованы. API base URL задается окружением. snake_case на проводе. Старые API не меняются." + "version": "1.21.0", + "description": "Контракт решений в программах для реализации и моков BE01–BE09. API base URL задается окружением. snake_case на проводе. Старые API не меняются." }, "servers": [ { @@ -21,7 +21,7 @@ "get": { "operationId": "getSolutionContext", "summary": "Режим программы и текущая заявка", - "description": "", + "description": "Existing current_application сохраняет приоритет. Для eligible пользователя без заявки выключенный program-scoped creation flag дает can_create=false и blocked_reason=solution_creation_disabled; режим/права/сроки сохраняются.", "parameters": [ { "name": "program_id", @@ -92,6 +92,19 @@ "blocked_reason": null, "current_application": null } + }, + "creation_disabled": { + "value": { + "program_id": 12, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-30T18:00:00Z", + "can_create": false, + "blocked_reason": "solution_creation_disabled", + "current_application": null + } } } } @@ -154,7 +167,7 @@ "get": { "operationId": "listSolutions", "summary": "Решения программы", - "description": "", + "description": "По умолчанию active. participation=archived/all дает собственные архивные работы капитану/сохраненному составу и organizer/admin; эксперт получает только активные назначенные работы. Архив исключен из current_application.", "parameters": [ { "name": "program_id", @@ -207,6 +220,19 @@ ], "default": "all" } + }, + { + "name": "participation", + "in": "query", + "required": false, + "schema": { + "enum": [ + "active", + "archived", + "all" + ], + "default": "active" + } } ], "responses": { @@ -301,7 +327,7 @@ "post": { "operationId": "createSolution", "summary": "Создать или вернуть заявку капитана", - "description": "JSON {} создает заявку с начальным названием. Участие в другой команде дает 409.", + "description": "JSON {} создает заявку с начальным названием. Участие в другой команде дает 409. При выключенном solution_creation_enabled новая заявка получает 409/solution_creation_disabled после проверки прав; captain repeat по-прежнему возвращает 200. Флаг не передается клиентом и не блокирует команды существующего решения.", "parameters": [ { "name": "program_id", @@ -513,6 +539,198 @@ ] } } + }, + "creation_disabled_existing": { + "value": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00: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": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "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 + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "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": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } } } } @@ -756,6 +974,16 @@ "application/json": { "schema": { "$ref": "#/components/schemas/Error" + }, + "examples": { + "creation_disabled": { + "value": { + "code": "solution_creation_disabled", + "detail": "Создание новых решений временно приостановлено. Ранее созданные заявки доступны.", + "errors": [], + "meta": {} + } + } } } } @@ -5795,39 +6023,7356 @@ } } } - } - } - } - }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" + }, + "parameters": [ + { + "name": "preserve_original", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "description": "true сохраняет исходные байты и формат материалов решения без перекодирования изображений; без параметра общий upload работает как прежде." + } + ] } }, - "schemas": { - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "/programs/{program_id}/solution-evaluation/": { + "get": { + "operationId": "getSolutionRatings", + "summary": "Работы для оценки экспертом", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } }, - "first_name": { - "type": "string" + { + "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" + } + } + } + } + }, + "description": "Сданные решения в текущей видимости эксперта. evaluation_counts содержит собственные pending/completed после search/case_scope/assigned_only, до status и limit/offset, как действующий список оценивания проектов. Счетчики не вычисляются из загруженной страницы." + } + }, + "/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" + } + } + } + } + } + } + }, + "/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." + } + }, + "/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." + } + }, + "/programs/{program_id}/analytics-widget/": { + "get": { + "operationId": "getProgramRoleWidget", + "summary": "Ролевой виджет текущего пользователя программы", + "description": "Прежний read-only endpoint. В legacy сохраняется ответ Project, в solution явный storage_mode и настоящая ссылка/счетчики решения. Роль organizer → expert → participant проверяется сервером. GET не меняет данные. Скрытые оценки, эксперты и регистрационные ответы не возвращаются. Ошибки сохраняют существующий DRF detail, не C00 Error.", + "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/RoleWidget" + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + }, + "description": "private, no-store" + } + } + }, + "401": { + "description": "Существующая ошибка виджета", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WidgetError" + } + } + } + }, + "403": { + "description": "Существующая ошибка виджета", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WidgetError" + } + } + } + }, + "404": { + "description": "Существующая ошибка виджета", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WidgetError" + } + } + } + }, + "409": { + "description": "Существующая ошибка виджета", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WidgetError" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-cases/": { + "get": { + "operationId": "getSolutionCaseAnalytics", + "summary": "Read program-scoped solution case aggregates, manager only", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Current case options and unique registered accepted teams; noncompetitive submission counts are null", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseSolutionAnalytics" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-analytics/": { + "get": { + "operationId": "getSolutionManagerAnalytics", + "summary": "getSolutionManagerAnalytics", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Program-scoped manager read; private, no-store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagerAnalytics" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-analytics/assignments/": { + "get": { + "operationId": "getSolutionManagerAssignments", + "summary": "getSolutionManagerAssignments", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "scope", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "completed", + "pending" + ], + "default": "all" + } + } + ], + "responses": { + "200": { + "description": "Program-scoped manager read; private, no-store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagerAssignments" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-analytics/assignments/{assignment_id}/": { + "get": { + "operationId": "getSolutionManagerAssignment", + "summary": "getSolutionManagerAssignment", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "assignment_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Program-scoped manager read; private, no-store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagerAssignmentDetail" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-analytics/solutions/": { + "get": { + "operationId": "getSolutionManagerSolutions", + "summary": "getSolutionManagerSolutions", + "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, + "default": "" + } + }, + { + "name": "scope", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "not_submitted", + "awaiting_evaluation" + ], + "default": "all" + } + }, + { + "name": "submission", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "submitted", + "not_submitted" + ], + "default": "all" + } + }, + { + "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": "field_id", + "in": "query", + "required": false, + "description": "Stable ID из current формы или submitted snapshot своей программы; неизвестный/чужой ID: 400.", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "answer_state", + "in": "query", + "required": false, + "description": "Applied snapshot/current форма; present/missing только для применимого поля; 0/false present. Не all требует field_id.", + "schema": { + "type": "string", + "enum": [ + "all", + "present", + "missing", + "not_applicable" + ], + "default": "all" + } + } + ], + "responses": { + "200": { + "description": "Program-scoped manager read; private, no-store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagerSolutions" + } + } + } + }, + "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": "Manager-only private/no-store. Все фильтры пересекаются до count/pagination. Применимость и присутствие совпадают с field_coverage; нет нового readiness правила." + } + }, + "/programs/{program_id}/management/solution-analytics/export/": { + "get": { + "operationId": "exportSolutionProgramAnalytics", + "summary": "exportSolutionProgramAnalytics", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "kind", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "program", + "solutions", + "submitted", + "scores" + ], + "default": "program" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string", + "maxLength": 200, + "default": "" + } + }, + { + "name": "scope", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "not_submitted", + "awaiting_evaluation" + ], + "default": "all" + } + }, + { + "name": "submission", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "submitted", + "not_submitted" + ], + "default": "all" + } + }, + { + "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": "field_id", + "in": "query", + "required": false, + "description": "Stable ID из current формы или submitted snapshot своей программы; неизвестный/чужой ID: 400.", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "answer_state", + "in": "query", + "required": false, + "description": "Applied snapshot/current форма; present/missing только для применимого поля; 0/false present. Не all требует field_id.", + "schema": { + "type": "string", + "enum": [ + "all", + "present", + "missing", + "not_applicable" + ], + "default": "all" + } + } + ], + "responses": { + "200": { + "description": "Program-scoped manager read; private, no-store", + "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": "kind=solutions/submitted: те же фильтры, что JSON list, все найденные без pagination; submitted пересекает status. kind=program/scores: полный scope, непустые фильтры 400. limit/offset не поддерживаются. Private/no-store; безопасный XLSX writer." + } + }, + "/programs/{program_id}/management/solution-analytics/attention/solutions-awaiting-evaluation/reminder-preview/": { + "post": { + "operationId": "previewSolutionExpertReminder", + "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/SolutionReminderPreview" + } + } + } + }, + "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/SolutionReminderPreviewRequest" + } + } + } + }, + "description": "Организатор/администратор своей solution-программы; только competitive distributed. Selection IDs — настоящие solution IDs. Private, no-store." + } + }, + "/programs/{program_id}/management/solution-analytics/attention/solutions-awaiting-evaluation/reminders/": { + "post": { + "operationId": "confirmSolutionExpertReminder", + "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/SolutionReminderReceipt" + } + } + } + }, + "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/SolutionReminderConfirm" + } + } + } + }, + "description": "Организатор/администратор своей solution-программы; только competitive distributed. Selection IDs — настоящие solution IDs. Private, no-store." + } + }, + "/programs/{program_id}/published-solutions/": { + "get": { + "operationId": "getPublishedSolutions", + "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, + "default": "" + } + } + ], + "responses": { + "200": { + "description": "Program-scoped manager read; private, no-store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishedSolutionPage" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "security": [], + "description": "Публичное read-only чтение опубликованных материалов solution программы. Неконкурсная программа: сохраненные материалы без искусственной сдачи; конкурсная: уже сданные решения и действующий снимок сдачи. Eligibility несданных конкурсных черновиков остается открытым вопросом, пока они не публикуются. Доступ определяет сохраненный published_at, архивы исключаются; GET не публикует. Пустой конкурсный список дает200, legacy mode404. Закрытые оценки, назначения, pending и личные данные регистрации не раскрываются. Cache-Control no-store." + } + }, + "/programs/{program_id}/published-solutions/{solution_id}/": { + "get": { + "operationId": "getPublishedSolution", + "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": "Program-scoped manager read; private, no-store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishedSolution" + } + } + } + }, + "400": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "security": [], + "description": "Публичное read-only чтение опубликованных материалов solution программы. Неконкурсная программа: сохраненные материалы без искусственной сдачи; конкурсная: уже сданные решения и действующий снимок сдачи. Eligibility несданных конкурсных черновиков остается открытым вопросом, пока они не публикуются. Доступ определяет сохраненный published_at, архивы исключаются; GET не публикует. Пустой конкурсный список дает200, legacy mode404. Закрытые оценки, назначения, pending и личные данные регистрации не раскрываются. Cache-Control no-store." + } + }, + "/programs/{program_id}/management/solutions/actions/preview/": { + "post": { + "operationId": "previewSolutionSubmitAction", + "summary": "Предварительная проверка первой сдачи организатором", + "description": "Только существующие organizer/admin права. Signed preview привязан к actor/program/IDs и сохраненному состоянию, срок 10 минут. Поздняя первая сдача допускается; обычный submit и уже сданный snapshot не меняются. Только action=submit. private, no-store.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionActionPreviewRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionActionPreview" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/actions/apply/": { + "post": { + "operationId": "applySolutionSubmitAction", + "summary": "Подтверждение первой сдачи организатором", + "description": "Только существующие organizer/admin права. Signed preview привязан к actor/program/IDs и сохраненному состоянию, срок 10 минут. Поздняя первая сдача допускается; обычный submit и уже сданный snapshot не меняются. Только action=submit. private, no-store.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionActionApplyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionActionResult" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/{solution_id}/case/preview/": { + "post": { + "operationId": "previewSolutionCaseChange", + "summary": "Предпросмотр смены кейса черновика", + "description": "Organizer/admin своей программы. Signed proof10min и program→solution locks; state recheck включает оценки/назначения/scopes. После сдачи меняется только кейс, исходный snapshot сохраняется в append-only истории до/после; остальные материалы/состав/дата сдачи и оценки сохраняются. Снимаются только несовместимые неоцененные назначения. Обычные participant writers остаются frozen. Legacy project handlers не меняются. private, no-store.", + "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 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionCaseChangePreviewRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionCaseChangePreview" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/{solution_id}/case/apply/": { + "post": { + "operationId": "applySolutionCaseChange", + "summary": "Подтверждение смены кейса черновика", + "description": "Organizer/admin своей программы. Signed proof10min и program→solution locks; state recheck включает оценки/назначения/scopes. После сдачи меняется только кейс, исходный snapshot сохраняется в append-only истории до/после; остальные материалы/состав/дата сдачи и оценки сохраняются. Снимаются только несовместимые неоцененные назначения. Обычные participant writers остаются frozen. Legacy project handlers не меняются. private, no-store.", + "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 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionCaseChangeApplyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationReceipt" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/{solution_id}/history/": { + "get": { + "operationId": "listSolutionAdminChanges", + "summary": "История административных изменений решения", + "description": "Только organizer/admin своей программы и solution mode. История ограничена настоящей парой program/solution/change; before/after показывают сохраненную сдачу без регистрации или иных private DTO. Списки newest first, limit/offset. Неизвестные query запрещены. Чтение доступно независимо от текущей формы/creation flag/deadline. private, no-store.", + "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 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionAdminChangePage" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/{solution_id}/history/{change_id}/": { + "get": { + "operationId": "readSolutionAdminChange", + "summary": "История административных изменений решения", + "description": "Только organizer/admin своей программы и solution mode. История ограничена настоящей парой program/solution/change; before/after показывают сохраненную сдачу без регистрации или иных private DTO. Списки newest first, limit/offset. Неизвестные query запрещены. Чтение доступно независимо от текущей формы/creation flag/deadline. private, no-store.", + "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 + } + }, + { + "name": "change_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionAdminChangeDetail" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/{solution_id}/members/remove/preview/": { + "post": { + "operationId": "previewSolutionMemberRemoval", + "summary": "Исключение участника организатором", + "description": "Только organizer/admin своей программы, solution mode. Signed proof10min связан с actor/program/solution/user и текущими данными. Общие program→solution locks и recheck. До и после сдачи исключается принятый участник, кроме капитана; min_size остается проверкой сдачи. Регистрация, ответы, файлы, оценки и назначения сохраняются. Pending приглашения исключенного отзываются; accepted/declined история не дает доступа к материалам и сохраняется. Submitted snapshot меняет только members; исходный snapshot и исключенный Member сохраняются в append-only истории. Обычные writers остаются frozen. Независимо от формы/срока/creation flag; legacy handlers не меняются. Неизвестные поля/query запрещены. private, no-store.", + "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 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionMemberRemovalPreviewRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionMemberRemovalPreview" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/{solution_id}/members/remove/apply/": { + "post": { + "operationId": "applySolutionMemberRemoval", + "summary": "Исключение участника организатором", + "description": "Только organizer/admin своей программы, solution mode. Signed proof10min связан с actor/program/solution/user и текущими данными. Общие program→solution locks и recheck. До и после сдачи исключается принятый участник, кроме капитана; min_size остается проверкой сдачи. Регистрация, ответы, файлы, оценки и назначения сохраняются. Pending приглашения исключенного отзываются; accepted/declined история не дает доступа к материалам и сохраняется. Submitted snapshot меняет только members; исходный snapshot и исключенный Member сохраняются в append-only истории. Обычные writers остаются frozen. Независимо от формы/срока/creation flag; legacy handlers не меняются. Неизвестные поля/query запрещены. private, no-store.", + "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 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionMemberRemovalApplyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationReceipt" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/archive/preview/": { + "post": { + "operationId": "previewSolutionArchive", + "summary": "Снятие решений с участия с сохранением работ", + "description": "Только organizer/admin своей программы, solution mode. Bulk signed proof10min связан с actor/program/ids и актуальными работами/составом/приглашениями/оценками/назначениями. Program→solutions locks/recheck; stale/replay409. Снятие до/после сдачи сохраняет solution/answers/UserFile/roster/scores и исходную сдачу. Pending отзываются, все назначения снимаются, включая оцененные; оценки сохраняются. Архив не участвует в текущей заявке/эксклюзивности/оценивании/аналитике/результатах/публикации. Captain/members/manager читают архив отдельно; новые заявки подчиняются текущим правилам регистрации/срока/creation flag. Draft получает archive snapshot без искусственного submit; submitted snapshot не меняется. Неизвестные body/query запрещены. private, no-store. Старые Project handlers/данные не меняются.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionArchivePreviewRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionArchivePreview" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solutions/archive/apply/": { + "post": { + "operationId": "applySolutionArchive", + "summary": "Снятие решений с участия с сохранением работ", + "description": "Только organizer/admin своей программы, solution mode. Bulk signed proof10min связан с actor/program/ids и актуальными работами/составом/приглашениями/оценками/назначениями. Program→solutions locks/recheck; stale/replay409. Снятие до/после сдачи сохраняет solution/answers/UserFile/roster/scores и исходную сдачу. Pending отзываются, все назначения снимаются, включая оцененные; оценки сохраняются. Архив не участвует в текущей заявке/эксклюзивности/оценивании/аналитике/результатах/публикации. Captain/members/manager читают архив отдельно; новые заявки подчиняются текущим правилам регистрации/срока/creation flag. Draft получает archive snapshot без искусственного submit; submitted snapshot не меняется. Неизвестные body/query запрещены. private, no-store. Старые Project handlers/данные не меняются.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionArchiveApplyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionArchiveReceipt" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/registrations/remove/preview/": { + "post": { + "operationId": "previewSolutionRegistrationRemoval", + "summary": "Удаление регистраций с сохранением решений", + "description": "Организатор/admin своей программы в solution mode. registration_ids — ID PartnerProgramUserProfile, не User/solution. Signed preview 10 минут связан с actor/program/selection и текущими регистрациями/материалами/командой/приглашениями/оценками/назначениями. Program → profiles → ordered solutions; атомарные recheck, эффекты, удаление регистраций и неизменяемый аудит. Капитан: архив reason=registration_removed, состав/материалы/оценки сохранены, pending и назначения сняты. Принятый участник: удалить из текущего состава и снимка, история до/после сохранена, оценки и назначения остаются. Только приглашенный: отозвать pending без изменения состава. Архивные заявки не меняются. Без решения: регистрация удаляется с отдельным аудитом. Auth User/manager/expert, другие программы, Project/UserFile не удаляются. Смешанная legacy-связь блокирует всю команду: legacy_application_conflict. Старые handlers не меняются. Deadline/creation flag не блокируют административное исключение. Stale/replay409, чужая регистрация404; private, no-store. Unknown body/query запрещены. История удаления доступна только организатору/admin своей программы.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionRegistrationRemovalPreviewRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionRegistrationRemovalPreview" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/registrations/remove/apply/": { + "post": { + "operationId": "applySolutionRegistrationRemoval", + "summary": "Удаление регистраций с сохранением решений", + "description": "Организатор/admin своей программы в solution mode. registration_ids — ID PartnerProgramUserProfile, не User/solution. Signed preview 10 минут связан с actor/program/selection и текущими регистрациями/материалами/командой/приглашениями/оценками/назначениями. Program → profiles → ordered solutions; атомарные recheck, эффекты, удаление регистраций и неизменяемый аудит. Капитан: архив reason=registration_removed, состав/материалы/оценки сохранены, pending и назначения сняты. Принятый участник: удалить из текущего состава и снимка, история до/после сохранена, оценки и назначения остаются. Только приглашенный: отозвать pending без изменения состава. Архивные заявки не меняются. Без решения: регистрация удаляется с отдельным аудитом. Auth User/manager/expert, другие программы, Project/UserFile не удаляются. Смешанная legacy-связь блокирует всю команду: legacy_application_conflict. Старые handlers не меняются. Deadline/creation flag не блокируют административное исключение. Stale/replay409, чужая регистрация404; private, no-store. Unknown body/query запрещены. История удаления доступна только организатору/admin своей программы.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionRegistrationRemovalApplyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionRegistrationRemovalReceipt" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/registrations/removals/": { + "get": { + "operationId": "listSolutionRegistrationRemovals", + "summary": "Аудит удаления регистраций", + "description": "Организатор/admin своей программы в solution mode. registration_ids — ID PartnerProgramUserProfile, не User/solution. Signed preview 10 минут связан с actor/program/selection и текущими регистрациями/материалами/командой/приглашениями/оценками/назначениями. Program → profiles → ordered solutions; атомарные recheck, эффекты, удаление регистраций и неизменяемый аудит. Капитан: архив reason=registration_removed, состав/материалы/оценки сохранены, pending и назначения сняты. Принятый участник: удалить из текущего состава и снимка, история до/после сохранена, оценки и назначения остаются. Только приглашенный: отозвать pending без изменения состава. Архивные заявки не меняются. Без решения: регистрация удаляется с отдельным аудитом. Auth User/manager/expert, другие программы, Project/UserFile не удаляются. Смешанная legacy-связь блокирует всю команду: legacy_application_conflict. Старые handlers не меняются. Deadline/creation flag не блокируют административное исключение. Stale/replay409, чужая регистрация404; private, no-store. Unknown body/query запрещены. История удаления доступна только организатору/admin своей программы.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionRegistrationRemovalAuditPage" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/registrations/removals/{audit_id}/": { + "get": { + "operationId": "readSolutionRegistrationRemoval", + "summary": "Аудит удаления регистраций", + "description": "Организатор/admin своей программы в solution mode. registration_ids — ID PartnerProgramUserProfile, не User/solution. Signed preview 10 минут связан с actor/program/selection и текущими регистрациями/материалами/командой/приглашениями/оценками/назначениями. Program → profiles → ordered solutions; атомарные recheck, эффекты, удаление регистраций и неизменяемый аудит. Капитан: архив reason=registration_removed, состав/материалы/оценки сохранены, pending и назначения сняты. Принятый участник: удалить из текущего состава и снимка, история до/после сохранена, оценки и назначения остаются. Только приглашенный: отозвать pending без изменения состава. Архивные заявки не меняются. Без решения: регистрация удаляется с отдельным аудитом. Auth User/manager/expert, другие программы, Project/UserFile не удаляются. Смешанная legacy-связь блокирует всю команду: legacy_application_conflict. Старые handlers не меняются. Deadline/creation flag не блокируют административное исключение. Stale/replay409, чужая регистрация404; private, no-store. Unknown body/query запрещены. История удаления доступна только организатору/admin своей программы.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "audit_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Результат", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionRegistrationRemovalAuditDetail" + } + } + } + }, + "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": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "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" + } + ] + }, + "participation": { + "$ref": "#/components/schemas/SolutionParticipation" + } + }, + "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" + }, + "captain": { + "$ref": "#/components/schemas/User" + }, + "accepted_count": { + "type": "integer", + "minimum": 0 + }, + "case_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": [ + "draft", + "submitted" + ] + }, + "submitted_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "participation": { + "$ref": "#/components/schemas/SolutionParticipation" + } + }, + "required": [ + "id", + "program_id", + "name", + "captain", + "accepted_count", + "case_value", + "status", + "submitted_at" + ], + "additionalProperties": false + }, + "CreateSolution": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [], + "additionalProperties": false + }, + "UpdateSolution": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 1 + }, + "expected_form_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AnswerPatch" + } + } + }, + "required": [ + "expected_version", + "expected_form_revision" + ], + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "answers" + ] + } + ] + }, + "Submit": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 1 + }, + "expected_form_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "expected_version", + "expected_form_revision" + ], + "additionalProperties": false + }, + "CaptainCommand": { + "type": "object", + "properties": { + "expected_solution_version": { + "type": "integer", + "minimum": 1 + }, + "new_captain_user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "expected_solution_version", + "new_captain_user_id" + ], + "additionalProperties": false + }, + "TeamCommand": { + "type": "object", + "properties": { + "expected_solution_version": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "expected_solution_version" + ], + "additionalProperties": false + }, + "InvitationPermissions": { + "type": "object", + "properties": { + "can_edit": { + "type": "boolean" + }, + "can_revoke": { + "type": "boolean" + }, + "can_accept": { + "type": "boolean" + }, + "can_decline": { + "type": "boolean" + } + }, + "required": [ + "can_edit", + "can_revoke", + "can_accept", + "can_decline" + ], + "additionalProperties": false + }, + "Invitation": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "version": { + "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" + ] + }, + "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" + ], + "additionalProperties": false + }, + "CreateInvitation": { + "type": "object", + "properties": { + "expected_solution_version": { + "type": "integer", + "minimum": 1 + }, + "recipient_user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] + }, + "motivational_letter": { + "anyOf": [ + { + "type": "string", + "maxLength": 4096 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "expected_solution_version", + "recipient_user_id", + "role" + ], + "additionalProperties": false + }, + "UpdateInvitation": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 1 + }, + "expected_solution_version": { + "type": "integer", + "minimum": 1 + }, + "role": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] + }, + "motivational_letter": { + "anyOf": [ + { + "type": "string", + "maxLength": 4096 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "expected_version", + "expected_solution_version" + ], + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "role" + ] + }, + { + "required": [ + "specialization" + ] + }, + { + "required": [ + "motivational_letter" + ] + } + ] + }, + "InvitationAction": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "expected_version" + ], + "additionalProperties": false + }, + "InvitationCreated": { + "type": "object", + "properties": { + "invitation": { + "$ref": "#/components/schemas/Invitation" + }, + "solution_version": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "invitation", + "solution_version" + ], + "additionalProperties": false + }, + "InvitationActionResult": { + "type": "object", + "properties": { + "invitation": { + "$ref": "#/components/schemas/Invitation" + }, + "application": { + "anyOf": [ + { + "$ref": "#/components/schemas/SolutionRef" + }, + { + "type": "null" + } + ] + }, + "solution_version": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "invitation", + "application", + "solution_version" + ], + "additionalProperties": false + }, + "MutationReceipt": { + "type": "object", + "properties": { + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "solution_version": { + "type": "integer", + "minimum": 1 + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "solution_id", + "solution_version", + "updated_at" + ], + "additionalProperties": false + }, + "SolutionPage": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0 + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionSummary" + } + } + }, + "required": [ + "count", + "next", + "previous", + "results" + ], + "additionalProperties": false + }, + "InvitationPage": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0 + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "required": [ + "count", + "next", + "previous", + "results" + ], + "additionalProperties": false + }, + "UploadResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https?://" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "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" + } + }, + "form": { + "$ref": "#/components/schemas/Form" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Answer" + } + }, + "submitted_at": { + "type": "string", + "format": "date-time" + } + } + }, + "RatingCriterion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "int", + "float", + "bool", + "str" + ] + }, + "min_value": { + "type": [ + "number", + "null" + ] + }, + "max_value": { + "type": [ + "number", + "null" + ] + }, + "value": { + "type": [ + "string", + "null" + ], + "maxLength": 1000, + "description": "Строка/null по прежнему типу критерия. Текстовый комментарий допускает до 1000 символов, как текущий ProjectScore; числовой диапазон и True/False продолжают проверяться отдельно." + } + }, + "required": [ + "id", + "name", + "description", + "type", + "min_value", + "max_value", + "value" + ], + "additionalProperties": false + }, + "RatingValue": { + "type": "object", + "properties": { + "criterion_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "value": { + "type": [ + "string", + "null" + ], + "maxLength": 1000, + "description": "Строка/null по прежнему типу критерия. Текстовый комментарий допускает до 1000 символов, как текущий ProjectScore; числовой диапазон и True/False продолжают проверяться отдельно." + } + }, + "required": [ + "criterion_id", + "value" + ], + "additionalProperties": false + }, + "RateSolution": { + "type": "object", + "properties": { + "expected_criteria_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "scores": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RatingValue" + }, + "minItems": 1 + } + }, + "required": [ + "expected_criteria_revision", + "scores" + ], + "additionalProperties": false + }, + "RatingSummary": { + "type": "object", + "properties": { + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "case_name": { + "type": [ + "string", + "null" + ] + }, + "assigned": { + "type": "boolean" + }, + "is_completed": { + "type": "boolean" + }, + "can_rate": { + "type": "boolean" + }, + "blocked_reason": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "application", + "case_name", + "assigned", + "is_completed", + "can_rate", + "blocked_reason" + ], + "additionalProperties": false + }, + "RatingPage": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0 + }, + "next": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "previous": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "results": { + "type": "array", + "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": [ + "count", + "next", + "previous", + "results" + ], + "additionalProperties": false + }, + "RatingFrame": { + "type": "object", + "properties": { + "program": { + "$ref": "#/components/schemas/ProgramRef" + }, + "solution": { + "$ref": "#/components/schemas/Solution" + }, + "criteria_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "criteria": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RatingCriterion" + } + }, + "managed_evaluation": { + "type": "boolean" + }, + "can_rate": { + "type": "boolean" + }, + "blocked_reason": { + "type": [ + "string", + "null" + ] + }, + "is_completed": { + "type": "boolean" + }, + "rated_count": { + "type": "integer", + "minimum": 0 + }, + "max_rates": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "required_evaluators": { + "type": "integer", + "minimum": 1 + }, + "deadline": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "program", + "solution", + "criteria_revision", + "criteria", + "managed_evaluation", + "can_rate", + "blocked_reason", + "is_completed", + "rated_count", + "max_rates", + "required_evaluators", + "deadline" + ], + "additionalProperties": false + }, + "AssignmentOption": { + "type": "object", + "properties": { + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "case_name": { + "type": [ + "string", + "null" + ] + }, + "submitted": { + "type": "boolean" + }, + "assigned": { + "type": "boolean" + }, + "protected": { + "type": "boolean" + }, + "is_completed": { + "type": "boolean" + }, + "own_team": { + "type": "boolean" + }, + "eligible": { + "type": "boolean" + } + }, + "required": [ + "solution_id", + "name", + "case_name", + "submitted", + "assigned", + "protected", + "is_completed", + "own_team", + "eligible" + ], + "additionalProperties": false + }, + "AssignmentOptions": { + "type": "object", + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "expert_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "solutions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssignmentOption" + } + } + }, + "required": [ + "program_id", + "expert_id", + "revision", + "solutions" + ], + "additionalProperties": false + }, + "SolutionAssignmentsWrite": { + "type": "object", + "properties": { + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "solution_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "uniqueItems": true + } + }, + "required": [ + "revision", + "solution_ids" + ], + "additionalProperties": false + }, + "SolutionCasePool": { + "type": "object", + "properties": { + "case_name": { + "type": [ + "string", + "null" + ] + }, + "expert_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "uniqueItems": true, + "maxItems": 1000 + } + }, + "required": [ + "case_name", + "expert_ids" + ], + "additionalProperties": false + }, + "SolutionAssignmentOverride": { + "type": "object", + "properties": { + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "expert_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "uniqueItems": true, + "maxItems": 1000 + } + }, + "required": [ + "solution_id", + "expert_ids" + ], + "additionalProperties": false + }, + "SolutionAllocationWrite": { + "type": "object", + "properties": { + "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 + }, + "seed": { + "type": "integer", + "minimum": 0, + "maximum": 2147483647 + }, + "previous_plan_token": { + "type": "string", + "maxLength": 500000 + }, + "assignment_overrides": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionAssignmentOverride" + }, + "maxItems": 10000 + } + }, + "required": [ + "revision", + "expert_ids" + ], + "additionalProperties": false + }, + "SolutionPlannedAssignment": { + "type": "object", + "properties": { + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "solution_name": { + "type": "string" + }, + "expert_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "expert_name": { + "type": "string" + }, + "case_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "solution_id", + "solution_name", + "expert_id", + "expert_name", + "case_name" + ], + "additionalProperties": false + }, + "SolutionAssignmentShortage": { + "type": "object", + "properties": { + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "solution_name": { + "type": "string" + }, + "missing": { + "type": "integer", + "minimum": 1 + }, + "case_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "solution_id", + "solution_name", + "missing", + "case_name" + ], + "additionalProperties": false + }, + "SolutionExpertLoad": { + "type": "object", + "properties": { + "expert_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "assigned_before": { + "type": "integer", + "minimum": 0 + }, + "assigned_after": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "expert_id", + "name", + "before", + "after", + "assigned_before", + "assigned_after" + ], + "additionalProperties": false + }, + "SolutionAllocationPlan": { + "type": "object", + "properties": { + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "plan_token": { + "type": "string", + "maxLength": 500000 + }, + "assignments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionPlannedAssignment" + } + }, + "shortages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionAssignmentShortage" + } + }, + "can_reroll": { + "type": "boolean" + }, + "load": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionExpertLoad" + } + } + }, + "required": [ + "revision", + "plan_token", + "assignments", + "shortages", + "can_reroll", + "load" + ], + "additionalProperties": false + }, + "SolutionAllocationApply": { + "type": "object", + "properties": { + "plan_token": { + "type": "string", + "maxLength": 500000 + } + }, + "required": [ + "plan_token" + ], + "additionalProperties": false + }, + "SolutionAssignmentReceipt": { + "type": "object", + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "created": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "program_id", + "revision", + "created" + ], + "additionalProperties": false + }, + "SolutionQuotaWrite": { + "type": "object", + "properties": { + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "required_evaluators": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + }, + "required": [ + "revision", + "required_evaluators" + ], + "additionalProperties": false + }, + "SolutionQuota": { + "type": "object", + "properties": { + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "required_evaluators": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + }, + "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": [ + "revision", + "required_evaluators", + "submitted_solutions", + "solutions_missing_experts", + "ready_before", + "ready_after", + "missing_assignments", + "surplus_assignments" + ], + "additionalProperties": false + }, + "ResultCriterion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "int", + "float", + "bool", + "str" + ] + }, + "min_value": { + "type": [ + "number", + "null" + ] + }, + "max_value": { + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "id", + "name", + "description", + "type", + "min_value", + "max_value" + ], + "additionalProperties": false + }, + "ResultExpert": { + "type": "object", + "properties": { + "user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "avatar": { + "type": [ + "string", + "null" + ] + }, + "complete": { + "type": "boolean" + }, + "total": { + "type": [ + "number", + "null" + ] + }, + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RatingValue" + } + } + }, + "required": [ + "user_id", + "name", + "email", + "avatar", + "complete", + "total", + "values" + ], + "additionalProperties": false + }, + "SolutionResult": { + "type": "object", + "properties": { + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "case_name": { + "type": [ + "string", + "null" + ] + }, + "completed": { + "type": "integer", + "minimum": 0 + }, + "required": { + "type": "integer", + "minimum": 1 + }, + "evaluation_complete": { + "type": "boolean" + }, + "ready": { + "type": "boolean" + }, + "score": { + "type": [ + "number", + "null" + ] + }, + "place": { + "type": [ + "integer", + "null" + ], + "minimum": 1 + }, + "averages": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "experts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResultExpert" + } + } + }, + "required": [ + "application", + "solution_id", + "name", + "case_name", + "completed", + "required", + "evaluation_complete", + "ready", + "score", + "place", + "averages", + "experts" + ], + "additionalProperties": false + }, + "SolutionResultGroup": { + "type": "object", + "properties": { + "case_name": { + "type": [ + "string", + "null" + ] + }, + "solutions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionResult" + } + } + }, + "required": [ + "case_name", + "solutions" + ], + "additionalProperties": false + }, + "SolutionResults": { + "type": "object", + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "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": { + "type": "string" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionResultGroup" + } + }, + "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 + }, + "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 + }, + "WidgetProjectRef": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "program_link_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "id", + "name", + "program_link_id" + ] + }, + "WidgetStage": { + "type": "string", + "enum": [ + "none", + "not_submitted", + "submitted", + "review", + "evaluated", + "not_applicable" + ] + }, + "WidgetLegacyParticipant": { + "type": "object", + "additionalProperties": false, + "properties": { + "participant_project": { + "anyOf": [ + { + "$ref": "#/components/schemas/WidgetProjectRef" + }, + { + "type": "null" + } + ] + }, + "case_provided": { + "type": "boolean" + }, + "case_name": { + "type": [ + "string", + "null" + ] + }, + "stage": { + "$ref": "#/components/schemas/WidgetStage" + }, + "submission_open": { + "type": "boolean" + } + }, + "required": [ + "participant_project", + "case_provided", + "case_name", + "stage", + "submission_open" + ] + }, + "WidgetParticipant": { + "type": "object", + "additionalProperties": false, + "properties": { + "application": { + "anyOf": [ + { + "$ref": "#/components/schemas/SolutionRef" + }, + { + "type": "null" + } + ] + }, + "case_provided": { + "type": "boolean" + }, + "case_name": { + "type": [ + "string", + "null" + ] + }, + "stage": { + "$ref": "#/components/schemas/WidgetStage" + }, + "submission_open": { + "type": "boolean" + } + }, + "required": [ + "application", + "case_provided", + "case_name", + "stage", + "submission_open" + ] + }, + "WidgetLegacyOrganizer": { + "type": "object", + "additionalProperties": false, + "properties": { + "participants": { + "type": "integer", + "minimum": 0 + }, + "projects": { + "type": "integer", + "minimum": 0 + }, + "submitted_solutions": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "participants_without_project": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "participants", + "projects", + "submitted_solutions", + "participants_without_project" + ] + }, + "WidgetOrganizer": { + "type": "object", + "additionalProperties": false, + "properties": { + "participants": { + "type": "integer", + "minimum": 0 + }, + "solutions": { + "type": "integer", + "minimum": 0 + }, + "submitted_solutions": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "participants_without_solution": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "participants", + "solutions", + "submitted_solutions", + "participants_without_solution" + ] + }, + "WidgetExpert": { + "type": "object", + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "enum": [ + "open", + "distributed" + ] + }, + "assigned": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "remaining": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "evaluation_ends": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "mode", + "assigned", + "remaining", + "evaluation_ends" + ] + }, + "SolutionWidget": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { + "const": "participant" + }, + "is_competitive": { + "type": "boolean" + }, + "storage_mode": { + "const": "solution" + }, + "participant": { + "$ref": "#/components/schemas/WidgetParticipant" + } + }, + "required": [ + "program_id", + "role", + "is_competitive", + "storage_mode", + "participant" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { + "const": "organizer" + }, + "is_competitive": { + "type": "boolean" + }, + "storage_mode": { + "const": "solution" + }, + "organizer": { + "$ref": "#/components/schemas/WidgetOrganizer" + } + }, + "required": [ + "program_id", + "role", + "is_competitive", + "storage_mode", + "organizer" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { + "const": "expert" + }, + "is_competitive": { + "type": "boolean" + }, + "storage_mode": { + "const": "solution" + }, + "expert": { + "$ref": "#/components/schemas/WidgetExpert" + } + }, + "required": [ + "program_id", + "role", + "is_competitive", + "storage_mode", + "expert" + ] + } + ] + }, + "LegacyWidget": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { + "const": "participant" + }, + "is_competitive": { + "type": "boolean" + }, + "storage_mode": { + "const": "legacy_project" + }, + "participant": { + "$ref": "#/components/schemas/WidgetLegacyParticipant" + } + }, + "required": [ + "program_id", + "role", + "is_competitive", + "participant" + ] }, - "last_name": { + { + "type": "object", + "additionalProperties": false, + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { + "const": "organizer" + }, + "is_competitive": { + "type": "boolean" + }, + "storage_mode": { + "const": "legacy_project" + }, + "organizer": { + "$ref": "#/components/schemas/WidgetLegacyOrganizer" + } + }, + "required": [ + "program_id", + "role", + "is_competitive", + "organizer" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { + "const": "expert" + }, + "is_competitive": { + "type": "boolean" + }, + "storage_mode": { + "const": "legacy_project" + }, + "expert": { + "$ref": "#/components/schemas/WidgetExpert" + } + }, + "required": [ + "program_id", + "role", + "is_competitive", + "expert" + ] + } + ] + }, + "RoleWidget": { + "oneOf": [ + { + "$ref": "#/components/schemas/SolutionWidget" + }, + { + "$ref": "#/components/schemas/LegacyWidget" + } + ] + }, + "WidgetError": { + "type": "object", + "properties": { + "detail": { "type": "string" + } + }, + "required": [ + "detail" + ], + "additionalProperties": true, + "description": "Существующий DRF error contract виджета; raw body не показывается в Angular." + }, + "CaseSolutionMetrics": { + "type": "object", + "required": [ + "participants_total", + "solutions_total", + "not_submitted", + "submitted" + ], + "properties": { + "participants_total": { + "type": "integer", + "minimum": 0 }, - "avatar_url": { + "solutions_total": { + "type": "integer", + "minimum": 0 + }, + "not_submitted": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "submitted": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + }, + "additionalProperties": false + }, + "CaseSolutionItem": { + "type": "object", + "required": [ + "name", + "participants_total", + "solutions_total", + "not_submitted", + "submitted" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "participants_total": { + "type": "integer", + "minimum": 0 + }, + "solutions_total": { + "type": "integer", + "minimum": 0 + }, + "not_submitted": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "submitted": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + }, + "additionalProperties": false + }, + "CaseSolutionAnalytics": { + "type": "object", + "required": [ + "program_id", + "configured", + "submission_applicable", + "items", + "without_case" + ], + "properties": { + "program_id": { + "type": "integer", + "minimum": 1 + }, + "configured": { + "type": "boolean" + }, + "submission_applicable": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseSolutionItem" + } + }, + "without_case": { + "$ref": "#/components/schemas/CaseSolutionMetrics" + } + }, + "additionalProperties": false + }, + "ManagerTotal": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "total" + ], + "additionalProperties": false + }, + "ManagerRegion": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "count": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "name", + "count" + ], + "additionalProperties": false + }, + "ManagerRegions": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagerRegion" + } + } + }, + "required": [ + "total", + "items" + ], + "additionalProperties": false + }, + "ManagerSummary": { + "type": "object", + "properties": { + "participants": { + "$ref": "#/components/schemas/ManagerTotal" + }, + "solutions": { + "$ref": "#/components/schemas/ManagerTotal" + }, + "experts": { + "$ref": "#/components/schemas/ManagerTotal" + }, + "participant_regions": { + "$ref": "#/components/schemas/ManagerRegions" + } + }, + "required": [ + "participants", + "solutions", + "experts", + "participant_regions" + ], + "additionalProperties": false + }, + "ManagerParticipantFunnel": { + "type": "object", + "properties": { + "registrations": { + "type": "integer", + "minimum": 0 + }, + "unique_participants": { + "type": "integer", + "minimum": 0 + }, + "visited_participants": { + "type": "integer", + "minimum": 0 + }, + "onboarded_participants": { + "type": "integer", + "minimum": 0 + }, + "with_team": { + "type": "integer", + "minimum": 0 + }, + "captains": { + "type": "integer", + "minimum": 0 + }, + "submitted_captains": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + }, + "required": [ + "registrations", + "unique_participants", + "visited_participants", + "onboarded_participants", + "with_team", + "captains", + "submitted_captains" + ], + "additionalProperties": false + }, + "ManagerSolutionFunnel": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "minimum": 0 + }, + "submission_applicable": { + "type": "boolean" + }, + "not_submitted": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "submitted": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "evaluated": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + }, + "required": [ + "created", + "submission_applicable", + "not_submitted", + "submitted", + "evaluated" + ], + "additionalProperties": false + }, + "ManagerEvaluationCounters": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "pending": { + "type": "integer", + "minimum": 0 + }, + "evaluated": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "total", + "pending", + "evaluated" + ], + "additionalProperties": false + }, + "ManagerEvaluationSolutions": { + "type": "object", + "properties": { + "submitted": { + "type": "integer", + "minimum": 0 + }, + "awaiting_evaluation": { + "type": "integer", + "minimum": 0 + }, + "partially_evaluated": { + "type": "integer", + "minimum": 0 + }, + "evaluated": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "submitted", + "awaiting_evaluation", + "partially_evaluated", + "evaluated" + ], + "additionalProperties": false + }, + "ManagerEvaluationStatus": { + "type": "object", + "properties": { + "applicable": { + "type": "boolean" + }, + "mode": { + "type": "string", + "enum": [ + "open", + "distributed" + ] + }, + "required_evaluators": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "max_evaluations_per_solution": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "assignments": { "anyOf": [ { - "type": "string", - "format": "uri", - "pattern": "^https?://" + "$ref": "#/components/schemas/ManagerEvaluationCounters" + }, + { + "type": "null" + } + ] + }, + "solutions": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagerEvaluationSolutions" }, { "type": "null" @@ -5836,1164 +13381,1894 @@ } }, "required": [ - "id", + "applicable", + "mode", + "required_evaluators", + "max_evaluations_per_solution", + "assignments", + "solutions" + ], + "additionalProperties": false + }, + "ManagerExpert": { + "type": "object", + "properties": { + "expert_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "avatar": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "expert_id", + "user_id", "first_name", "last_name", - "avatar_url" + "full_name", + "avatar" ], "additionalProperties": false }, - "ProgramRef": { + "ManagerAssignment": { "type": "object", "properties": { - "id": { + "assignment_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, + "expert": { + "$ref": "#/components/schemas/ManagerExpert" + }, + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "status": { + "type": "string", + "enum": [ + "not_ready", + "pending", + "in_progress", + "completed" + ] + }, + "assigned_at": { + "type": "string", + "format": "date-time" + }, + "submitted": { + "type": "boolean" + }, + "submitted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "waiting_since": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "waiting_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + }, + "required": [ + "assignment_id", + "expert", + "application", + "status", + "assigned_at", + "submitted", + "submitted_at", + "waiting_since", + "waiting_seconds" + ], + "additionalProperties": false + }, + "ManagerAssignmentCriterion": { + "type": "object", + "properties": { "name": { "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "int", + "float", + "bool", + "str" + ] + }, + "min_value": { + "type": [ + "number", + "null" + ] + }, + "max_value": { + "type": [ + "number", + "null" + ] + }, + "value": { + "type": [ + "string", + "null" + ], + "maxLength": 1000, + "description": "Строка/null по прежнему типу критерия. Текстовый комментарий допускает до 1000 символов, как текущий ProjectScore; числовой диапазон и True/False продолжают проверяться отдельно." + }, + "criterion_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "is_scored": { + "type": "boolean" } }, "required": [ - "id", - "name" + "name", + "description", + "type", + "min_value", + "max_value", + "value", + "criterion_id", + "is_scored" ], "additionalProperties": false }, - "SolutionRef": { + "ManagerAssignmentDetail": { "type": "object", "properties": { - "kind": { - "const": "solution" + "assignment_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "expert": { + "$ref": "#/components/schemas/ManagerExpert" + }, + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "status": { + "type": "string", + "enum": [ + "not_ready", + "pending", + "in_progress", + "completed" + ] + }, + "assigned_at": { + "type": "string", + "format": "date-time" + }, + "submitted": { + "type": "boolean" + }, + "submitted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "waiting_since": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "waiting_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "scores": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagerAssignmentCriterion" + } + } + }, + "required": [ + "assignment_id", + "expert", + "application", + "status", + "assigned_at", + "submitted", + "submitted_at", + "waiting_since", + "waiting_seconds", + "scores" + ], + "additionalProperties": false + }, + "ManagerAssignments": { + "type": "object", + "properties": { + "applicable": { + "type": "boolean" + }, + "mode": { + "type": "string", + "enum": [ + "open", + "distributed" + ] + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagerAssignment" + } + } + }, + "required": [ + "applicable", + "mode", + "results" + ], + "additionalProperties": false + }, + "ManagerDelayedExpert": { + "type": "object", + "properties": { + "expert_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "avatar": { + "type": [ + "string", + "null" + ] + }, + "assignments_total": { + "type": "integer", + "minimum": 0 + }, + "completed": { + "type": "integer", + "minimum": 0 + }, + "pending": { + "type": "integer", + "minimum": 0 + }, + "overdue_24h": { + "type": "integer", + "minimum": 0 + }, + "overdue_48h": { + "type": "integer", + "minimum": 0 + }, + "oldest_waiting_since": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "oldest_waiting_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "severity": { + "type": "string", + "enum": [ + "critical", + "warning" + ] + } + }, + "required": [ + "expert_id", + "user_id", + "first_name", + "last_name", + "full_name", + "avatar", + "assignments_total", + "completed", + "pending", + "overdue_24h", + "overdue_48h", + "oldest_waiting_since", + "oldest_waiting_seconds", + "severity" + ], + "additionalProperties": false + }, + "ManagerDelayedExperts": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagerDelayedExpert" + } + } + }, + "required": [ + "total", + "items" + ], + "additionalProperties": false + }, + "ManagerAttention": { + "type": "object", + "properties": { + "participants_without_visit": { + "type": "integer", + "minimum": 0 + }, + "participants_without_onboarding": { + "type": "integer", + "minimum": 0 + }, + "participants_without_team": { + "type": "integer", + "minimum": 0 + }, + "solutions_awaiting_evaluation": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "solutions_not_submitted": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "delayed_experts": { + "$ref": "#/components/schemas/ManagerDelayedExperts" + } + }, + "required": [ + "participants_without_visit", + "participants_without_onboarding", + "participants_without_team", + "solutions_awaiting_evaluation", + "solutions_not_submitted", + "delayed_experts" + ], + "additionalProperties": false + }, + "ManagerActivity": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "registrations": { + "type": "integer", + "minimum": 0 + }, + "submitted_solutions": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + }, + "required": [ + "date", + "registrations", + "submitted_solutions" + ], + "additionalProperties": false + }, + "ManagerFieldCoverage": { + "type": "object", + "properties": { + "field_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "label": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/FieldType" + }, + "purpose": { + "$ref": "#/components/schemas/Purpose" }, - "program_id": { + "applicable": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 0 }, - "solution_id": { + "answered": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 0 }, - "name": { - "type": "string" + "required_applicable": { + "type": "integer", + "minimum": 0 + }, + "required_unanswered": { + "type": "integer", + "minimum": 0 } }, "required": [ - "kind", - "program_id", - "solution_id", - "name" + "field_id", + "label", + "type", + "purpose", + "applicable", + "answered", + "required_applicable", + "required_unanswered" ], "additionalProperties": false }, - "LegacyRef": { + "ManagerAnalytics": { "type": "object", "properties": { - "kind": { - "const": "legacy_project" - }, "program_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "project_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "summary": { + "$ref": "#/components/schemas/ManagerSummary" }, - "program_link_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "participant_funnel": { + "$ref": "#/components/schemas/ManagerParticipantFunnel" }, - "name": { - "type": "string" + "solution_funnel": { + "$ref": "#/components/schemas/ManagerSolutionFunnel" + }, + "evaluation_status": { + "$ref": "#/components/schemas/ManagerEvaluationStatus" + }, + "attention": { + "$ref": "#/components/schemas/ManagerAttention" + }, + "activity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagerActivity" + } + }, + "cases": { + "$ref": "#/components/schemas/CaseSolutionAnalytics" + }, + "field_coverage": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagerFieldCoverage" + } } }, "required": [ - "kind", "program_id", - "project_id", - "program_link_id", - "name" + "summary", + "participant_funnel", + "solution_funnel", + "evaluation_status", + "attention", + "activity", + "cases", + "field_coverage" ], "additionalProperties": false }, - "ApplicationRef": { - "oneOf": [ - { - "$ref": "#/components/schemas/SolutionRef" - }, - { - "$ref": "#/components/schemas/LegacyRef" - } - ] - }, - "Issue": { + "ManagerSolutionRow": { "type": "object", "properties": { - "path": { - "type": "string" - }, - "code": { - "type": "string" + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "field_id": { - "anyOf": [ - { + "captain": { + "type": "object", + "properties": { + "user_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - { - "type": "null" + "full_name": { + "type": "string" + }, + "avatar": { + "type": [ + "string", + "null" + ] } + }, + "required": [ + "user_id", + "full_name", + "avatar" + ], + "additionalProperties": false + }, + "case_name": { + "type": [ + "string", + "null" ] }, - "detail": { - "type": "string" - } - }, - "required": [ - "path", - "code", - "field_id", - "detail" - ], - "additionalProperties": false - }, - "Error": { - "type": "object", - "properties": { - "code": { - "type": "string" + "submission_status": { + "type": "string", + "enum": [ + "draft", + "submitted" + ] }, - "detail": { - "type": "string" + "evaluation_status": { + "type": "string", + "enum": [ + "not_applicable", + "not_submitted", + "awaiting_evaluation", + "partially_evaluated", + "evaluated" + ] }, - "errors": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Issue" - } + "created_at": { + "type": "string", + "format": "date-time" }, - "meta": { - "type": "object", - "additionalProperties": true + "submitted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "assignments_total": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "assignments_completed": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "waiting_reason": { + "type": [ + "string", + "null" + ], + "enum": [ + null, + "no_assignments", + "no_completed_evaluations", + "partially_evaluated", + "awaiting_first_evaluation" + ] } }, "required": [ - "code", - "detail", - "errors", - "meta" + "application", + "captain", + "case_name", + "submission_status", + "evaluation_status", + "created_at", + "submitted_at", + "assignments_total", + "assignments_completed", + "waiting_reason" ], "additionalProperties": false }, - "Context": { + "ManagerSolutions": { "type": "object", "properties": { - "program_id": { + "count": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "storage_mode": { - "enum": [ - "legacy_project", - "solution" - ] - }, - "is_registered": { - "type": "boolean" - }, - "is_competitive": { - "type": "boolean" + "minimum": 0 }, - "submission_open": { - "type": "boolean" + "next": { + "type": [ + "string", + "null" + ], + "format": "uri" }, - "submission_deadline": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ] + "previous": { + "type": [ + "string", + "null" + ], + "format": "uri" }, - "can_create": { + "submission_applicable": { "type": "boolean" }, - "blocked_reason": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } + "mode": { + "type": "string", + "enum": [ + "open", + "distributed" ] }, - "current_application": { - "anyOf": [ - { - "$ref": "#/components/schemas/ApplicationRef" - }, - { - "type": "null" - } - ] + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagerSolutionRow" + } } }, "required": [ - "program_id", - "storage_mode", - "is_registered", - "is_competitive", - "submission_open", - "submission_deadline", - "can_create", - "blocked_reason", - "current_application" + "count", + "next", + "previous", + "submission_applicable", + "mode", + "results" ], "additionalProperties": false }, - "Validation": { + "SolutionReminderSelection": { "type": "object", + "additionalProperties": false, + "required": [ + "mode" + ], "properties": { - "min_length": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } + "mode": { + "type": "string", + "enum": [ + "selected", + "all" ] }, - "max_length": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] + "ids": { + "type": "array", + "maxItems": 10000, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "default": [] }, - "minimum": { - "anyOf": [ - { - "type": "number" + "excluded_ids": { + "type": "array", + "maxItems": 10000, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "default": [] + } + }, + "oneOf": [ + { + "required": [ + "mode", + "ids" + ], + "properties": { + "mode": { + "const": "selected" }, - { - "type": "null" - } - ] - }, - "maximum": { - "anyOf": [ - { - "type": "number" + "ids": { + "type": "array", + "maxItems": 10000, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "minItems": 1 }, - { - "type": "null" + "excluded_ids": { + "type": "array", + "maxItems": 0, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "default": [] } - ] - }, - "allowed_extensions": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9]+$" } }, - "max_file_size_bytes": { - "anyOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 104857600 + { + "required": [ + "mode" + ], + "properties": { + "mode": { + "const": "all" }, - { - "type": "null" + "ids": { + "type": "array", + "maxItems": 0, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "default": [] } - ] + } } - }, - "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" - ] + "SolutionReminderPreviewRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "selection" + ], + "properties": { + "search": { + "type": "string", + "maxLength": 200, + "default": "" + }, + "selection": { + "$ref": "#/components/schemas/SolutionReminderSelection" + } + } }, - "Field": { + "SolutionReminderRecipient": { "type": "object", + "additionalProperties": false, + "required": [ + "user_id", + "full_name" + ], "properties": { - "id": { + "user_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "type": { - "$ref": "#/components/schemas/FieldType" - }, - "purpose": { - "$ref": "#/components/schemas/Purpose" - }, - "label": { - "type": "string", - "minLength": 1, - "maxLength": 256 + "full_name": { + "type": "string" + } + } + }, + "SolutionReminderPreview": { + "type": "object", + "additionalProperties": false, + "required": [ + "token", + "selected_count", + "recipient_count", + "without_assignments_count", + "unavailable_count", + "preview_text", + "recipients", + "expires_in" + ], + "properties": { + "token": { + "type": [ + "string", + "null" + ], + "description": "Непрозрачный подписанный token, TTL 900 секунд; клиент не разбирает содержимое." + }, + "selected_count": { + "type": "integer", + "minimum": 0 }, - "help_text": { - "type": "string", - "maxLength": 4000 + "recipient_count": { + "type": "integer", + "minimum": 0 }, - "required": { - "type": "boolean" + "without_assignments_count": { + "type": "integer", + "minimum": 0 }, - "position": { + "unavailable_count": { "type": "integer", "minimum": 0 }, - "options": { + "preview_text": { + "type": "string" + }, + "recipients": { "type": "array", + "maxItems": 20, "items": { - "type": "string", - "minLength": 1, - "maxLength": 1000 + "$ref": "#/components/schemas/SolutionReminderRecipient" } }, - "validation": { - "$ref": "#/components/schemas/Validation" + "expires_in": { + "type": "integer", + "const": 900 } - }, + } + }, + "SolutionReminderConfirm": { + "type": "object", + "additionalProperties": false, "required": [ - "id", - "type", - "purpose", - "label", - "help_text", - "required", - "position", - "options", - "validation" + "token" ], - "additionalProperties": false + "properties": { + "token": { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + } + } }, - "FieldWrite": { + "SolutionReminderReceipt": { "type": "object", + "additionalProperties": false, + "required": [ + "created_count", + "skipped_count" + ], "properties": { - "id": { - "anyOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - { - "type": "null" - } - ] - }, - "type": { - "$ref": "#/components/schemas/FieldType" + "created_count": { + "type": "integer", + "minimum": 0 }, - "purpose": { - "$ref": "#/components/schemas/Purpose" + "skipped_count": { + "type": "integer", + "minimum": 0 + } + } + }, + "PublishedSolutionMember": { + "type": "object", + "additionalProperties": false, + "required": [ + "user", + "role", + "specialization", + "is_captain" + ], + "properties": { + "user": { + "$ref": "#/components/schemas/User" }, - "label": { - "type": "string", - "minLength": 1, - "maxLength": 256 + "role": { + "type": [ + "string", + "null" + ] }, - "help_text": { - "type": "string", - "maxLength": 4000 + "specialization": { + "type": [ + "string", + "null" + ] }, - "required": { + "is_captain": { "type": "boolean" + } + } + }, + "PublishedSolution": { + "type": "object", + "additionalProperties": false, + "required": [ + "application", + "published_at", + "fields", + "answers", + "team" + ], + "properties": { + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "position": { - "type": "integer", - "minimum": 0 + "published_at": { + "type": "string", + "format": "date-time" }, - "options": { + "fields": { "type": "array", "items": { - "type": "string", - "minLength": 1, - "maxLength": 1000 + "$ref": "#/components/schemas/Field" } }, - "validation": { - "$ref": "#/components/schemas/Validation" + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Answer" + } + }, + "team": { + "type": "object", + "additionalProperties": false, + "required": [ + "members", + "accepted_count" + ], + "properties": { + "members": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/PublishedSolutionMember" + } + }, + "accepted_count": { + "type": "integer", + "minimum": 1 + } + } + } + } + }, + "PublishedSolutionCard": { + "type": "object", + "additionalProperties": false, + "required": [ + "application", + "published_at", + "captain", + "accepted_count" + ], + "properties": { + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "published_at": { + "type": "string", + "format": "date-time" + }, + "captain": { + "$ref": "#/components/schemas/User" + }, + "accepted_count": { + "type": "integer", + "minimum": 1 } - }, - "required": [ - "id", - "type", - "purpose", - "label", - "help_text", - "required", - "position", - "options", - "validation" - ], - "additionalProperties": false + } }, - "Form": { + "PublishedSolutionPage": { "type": "object", + "additionalProperties": false, + "required": [ + "program_id", + "count", + "next", + "previous", + "results" + ], "properties": { "program_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "revision": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" + "count": { + "type": "integer", + "minimum": 0 }, - "template": { - "anyOf": [ - { - "$ref": "#/components/schemas/TemplateName" - }, - { - "type": "null" - } + "next": { + "type": [ + "string", + "null" ] }, - "is_configured": { - "type": "boolean" - }, - "is_snapshot": { - "type": "boolean" + "previous": { + "type": [ + "string", + "null" + ] }, - "fields": { + "results": { "type": "array", "items": { - "$ref": "#/components/schemas/Field" + "$ref": "#/components/schemas/PublishedSolutionCard" } } - }, - "required": [ - "program_id", - "revision", - "template", - "is_configured", - "is_snapshot", - "fields" - ], - "additionalProperties": false + } }, - "FormWrite": { + "SolutionActionPreviewRequest": { "type": "object", "properties": { - "expected_revision": { + "action": { "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "template": { - "$ref": "#/components/schemas/TemplateName" + "enum": [ + "submit" + ] }, - "fields": { + "ids": { "type": "array", "items": { - "$ref": "#/components/schemas/FieldWrite" - } + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "minItems": 1, + "maxItems": 10000 } }, "required": [ - "expected_revision", - "template", - "fields" + "action", + "ids" ], "additionalProperties": false }, - "FormManagement": { + "SolutionActionApplyRequest": { "type": "object", "properties": { - "form": { - "$ref": "#/components/schemas/Form" - }, - "has_solutions": { - "type": "boolean" - }, - "has_answers": { - "type": "boolean" - }, - "affected_draft_count": { - "type": "integer", - "minimum": 0 + "action": { + "type": "string", + "enum": [ + "submit" + ] }, - "fields_usage": { + "ids": { "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 - } + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "minItems": 1, + "maxItems": 10000 + }, + "preview_token": { + "type": "string", + "minLength": 1, + "maxLength": 200000 } }, "required": [ - "form", - "has_solutions", - "has_answers", - "affected_draft_count", - "fields_usage" + "action", + "ids", + "preview_token" ], "additionalProperties": false }, - "Template": { + "SolutionActionItem": { "type": "object", "properties": { - "key": { - "$ref": "#/components/schemas/TemplateName" - }, - "label": { - "type": "string" + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "fields": { + "errors": { "type": "array", "items": { - "$ref": "#/components/schemas/FieldWrite" + "$ref": "#/components/schemas/Issue" } } }, "required": [ - "key", - "label", - "fields" + "application", + "errors" ], "additionalProperties": false }, - "Templates": { + "SolutionActionPreview": { "type": "object", "properties": { - "results": { + "items": { "type": "array", "items": { - "$ref": "#/components/schemas/Template" + "$ref": "#/components/schemas/SolutionActionItem" } }, - "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" + "ready_count": { + "type": "integer", + "minimum": 0 }, - { - "type": "number" + "already_submitted_count": { + "type": "integer", + "minimum": 0 }, - { + "can_apply": { "type": "boolean" }, - { - "$ref": "#/components/schemas/FileRef" - } - ] - }, - "Answer": { - "type": "object", - "properties": { - "field_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "value": { - "$ref": "#/components/schemas/AnswerValue" + "preview_token": { + "type": [ + "string", + "null" + ] } }, "required": [ - "field_id", - "value" + "items", + "ready_count", + "already_submitted_count", + "can_apply", + "preview_token" ], "additionalProperties": false }, - "AnswerPatch": { + "SolutionActionResult": { "type": "object", "properties": { - "field_id": { + "changed_count": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 0 }, - "value": { - "anyOf": [ - { - "$ref": "#/components/schemas/AnswerValue" - }, - { - "type": "null" - } - ] + "already_submitted_count": { + "type": "integer", + "minimum": 0 } }, "required": [ - "field_id", - "value" + "changed_count", + "already_submitted_count" ], - "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": { + "additionalProperties": false + }, + "SolutionCaseChangePreviewRequest": { + "type": "object", + "properties": { + "case_name": { "type": "string", - "format": "date-time" + "minLength": 1, + "maxLength": 1000, + "pattern": "\\S" + } + }, + "required": [ + "case_name" + ], + "additionalProperties": false + }, + "SolutionCaseChangeApplyRequest": { + "type": "object", + "properties": { + "case_name": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "pattern": "\\S" }, - "is_captain": { - "type": "boolean" + "preview_token": { + "type": "string", + "minLength": 1, + "maxLength": 200000 } }, "required": [ - "user", - "role", - "specialization", - "joined_at", - "is_captain" + "case_name", + "preview_token" ], "additionalProperties": false }, - "Team": { + "SolutionCaseChangePreview": { "type": "object", "properties": { - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Member" - } + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "accepted_count": { + "case_field_id": { "type": "integer", - "minimum": 0 + "minimum": 1, + "maximum": 9007199254740991 }, - "pending_count": { - "type": "integer", - "minimum": 0 + "current_case": { + "type": [ + "string", + "null" + ] }, - "reserved_count": { + "case_name": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "pattern": "\\S" + }, + "solution_version": { "type": "integer", - "minimum": 0 + "minimum": 1 }, - "min_size": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, - { - "type": "null" - } - ] + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } }, - "max_size": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, - { - "type": "null" - } + "can_apply": { + "type": "boolean" + }, + "preview_token": { + "type": [ + "string", + "null" ] }, - "is_frozen": { + "is_submitted": { "type": "boolean" + }, + "removed_assignments_count": { + "type": "integer", + "minimum": 0 + }, + "score_count": { + "type": "integer", + "minimum": 0 } }, "required": [ - "members", - "accepted_count", - "pending_count", - "reserved_count", - "min_size", - "max_size", - "is_frozen" + "application", + "case_field_id", + "current_case", + "case_name", + "solution_version", + "errors", + "can_apply", + "preview_token", + "is_submitted", + "removed_assignments_count", + "score_count" ], "additionalProperties": false }, - "Permissions": { - "type": "object", - "properties": { - "can_edit": { - "type": "boolean" + "SolutionAdminChange": { + "oneOf": [ + { + "$ref": "#/components/schemas/SolutionCaseAdminChange" }, - "can_manage_team": { - "type": "boolean" + { + "$ref": "#/components/schemas/SolutionMemberAdminChange" }, - "can_invite": { - "type": "boolean" + { + "$ref": "#/components/schemas/SolutionArchiveAdminChange" }, - "can_submit": { - "type": "boolean" + { + "$ref": "#/components/schemas/SolutionRegistrationAdminChange" } - }, - "required": [ - "can_edit", - "can_manage_team", - "can_invite", - "can_submit" ], - "additionalProperties": false + "discriminator": { + "propertyName": "action" + } }, - "Submission": { + "SolutionAdminChangePage": { "type": "object", + "additionalProperties": false, + "required": [ + "count", + "offset", + "limit", + "results" + ], "properties": { - "required": { - "type": "boolean" + "count": { + "type": "integer", + "minimum": 0 }, - "status": { - "enum": [ - "draft", - "submitted" - ] + "offset": { + "type": "integer", + "minimum": 0 }, - "submission_open": { - "type": "boolean" + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100 }, - "deadline": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionAdminChange" + } + } + } + }, + "SolutionAdminChangeDetail": { + "type": "object", + "additionalProperties": false, + "required": [ + "change", + "before_snapshot", + "after_snapshot" + ], + "properties": { + "change": { + "$ref": "#/components/schemas/SolutionAdminChange" + }, + "before_snapshot": { "anyOf": [ { - "type": "string", - "format": "date-time" + "$ref": "#/components/schemas/SubmissionSnapshot" }, { "type": "null" } ] }, - "submitted_at": { + "after_snapshot": { "anyOf": [ { - "type": "string", - "format": "date-time" + "$ref": "#/components/schemas/SubmissionSnapshot" }, { "type": "null" } ] + }, + "archive_snapshot": { + "$ref": "#/components/schemas/SolutionArchiveSnapshot" + } + } + }, + "SolutionMemberRemovalPreviewRequest": { + "type": "object", + "properties": { + "user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 } }, "required": [ - "required", - "status", - "submission_open", - "deadline", - "submitted_at" + "user_id" ], "additionalProperties": false }, - "Readiness": { + "SolutionMemberRemovalApplyRequest": { "type": "object", "properties": { - "is_ready": { - "type": "boolean" + "user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 }, - "issues": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Issue" - } + "preview_token": { + "type": "string", + "minLength": 1, + "maxLength": 200000 } }, "required": [ - "is_ready", - "issues" + "user_id", + "preview_token" ], "additionalProperties": false }, - "Solution": { + "SolutionMemberRemovalPreview": { "type": "object", "properties": { - "id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "program_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "member": { + "$ref": "#/components/schemas/Member" }, - "version": { + "solution_version": { "type": "integer", "minimum": 1 }, - "name": { - "type": "string", - "maxLength": 256 + "is_submitted": { + "type": "boolean" }, - "captain_id": { + "remaining_member_count": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" + "minimum": 0 }, - "team": { - "$ref": "#/components/schemas/Team" + "revoked_invitation_count": { + "type": "integer", + "minimum": 0 }, - "form": { - "$ref": "#/components/schemas/Form" + "score_count": { + "type": "integer", + "minimum": 0 }, - "answers": { + "errors": { "type": "array", "items": { - "$ref": "#/components/schemas/Answer" + "$ref": "#/components/schemas/Issue" } }, - "submission": { - "$ref": "#/components/schemas/Submission" - }, - "permissions": { - "$ref": "#/components/schemas/Permissions" + "can_apply": { + "type": "boolean" }, - "readiness": { - "anyOf": [ - { - "$ref": "#/components/schemas/Readiness" - }, - { - "type": "null" - } + "preview_token": { + "type": [ + "string", + "null" ] - } - }, - "required": [ - "id", - "program_id", - "version", - "name", - "captain_id", - "created_at", - "updated_at", - "team", - "form", - "answers", - "submission", - "permissions", - "readiness" + } + }, + "required": [ + "application", + "member", + "solution_version", + "is_submitted", + "remaining_member_count", + "revoked_invitation_count", + "score_count", + "errors", + "can_apply", + "preview_token" ], "additionalProperties": false }, - "SolutionSummary": { + "SolutionCaseAdminChange": { "type": "object", + "additionalProperties": false, + "required": [ + "id", + "application", + "action", + "actor", + "actor_name", + "created_at", + "before_version", + "after_version", + "case_field_id", + "previous_case", + "case_name", + "removed_assignments_count", + "score_count", + "was_submitted" + ], "properties": { "id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "program_id": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "name": { - "type": "string" - }, - "captain": { - "$ref": "#/components/schemas/User" + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "accepted_count": { - "type": "integer", - "minimum": 0 + "action": { + "const": "change_case" }, - "case_value": { + "actor": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/User" }, { "type": "null" } ] }, - "status": { - "enum": [ - "draft", - "submitted" + "actor_name": { + "type": "string", + "maxLength": 512 + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "before_version": { + "type": "integer", + "minimum": 1 + }, + "after_version": { + "type": "integer", + "minimum": 2 + }, + "case_field_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "previous_case": { + "type": [ + "string", + "null" ] }, - "submitted_at": { + "case_name": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "removed_assignments_count": { + "type": "integer", + "minimum": 0 + }, + "score_count": { + "type": "integer", + "minimum": 0 + }, + "was_submitted": { + "type": "boolean" + } + } + }, + "SolutionMemberAdminChange": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "action": { + "const": "remove_member" + }, + "actor": { "anyOf": [ { - "type": "string", - "format": "date-time" + "$ref": "#/components/schemas/User" }, { "type": "null" } ] + }, + "actor_name": { + "type": "string", + "maxLength": 512 + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "before_version": { + "type": "integer", + "minimum": 1 + }, + "after_version": { + "type": "integer", + "minimum": 2 + }, + "removed_assignments_count": { + "const": 0 + }, + "score_count": { + "type": "integer", + "minimum": 0 + }, + "was_submitted": { + "type": "boolean" + }, + "removed_member": { + "$ref": "#/components/schemas/Member" + }, + "revoked_invitation_count": { + "type": "integer", + "minimum": 0 } }, "required": [ "id", - "program_id", - "name", - "captain", - "accepted_count", - "case_value", - "status", - "submitted_at" + "application", + "action", + "actor", + "actor_name", + "created_at", + "before_version", + "after_version", + "removed_assignments_count", + "score_count", + "was_submitted", + "removed_member", + "revoked_invitation_count" ], "additionalProperties": false }, - "CreateSolution": { + "SolutionParticipation": { "type": "object", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 256 + "status": { + "enum": [ + "active", + "archived" + ] + }, + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "reason": { + "enum": [ + "detached", + "registration_removed", + null + ] } }, - "required": [], + "required": [ + "status", + "archived_at", + "reason" + ], "additionalProperties": false }, - "UpdateSolution": { + "SolutionArchiveSnapshot": { "type": "object", + "additionalProperties": false, + "required": [ + "name", + "captain_id", + "members", + "form", + "answers", + "captured_at" + ], "properties": { - "expected_version": { - "type": "integer", - "minimum": 1 - }, - "expected_form_revision": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, "name": { "type": "string", - "minLength": 1, "maxLength": 256 }, + "captain_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Member" + } + }, + "form": { + "$ref": "#/components/schemas/Form" + }, "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/AnswerPatch" + "$ref": "#/components/schemas/Answer" } + }, + "captured_at": { + "type": "string", + "format": "date-time" + } + } + }, + "SolutionArchivePreviewRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "minItems": 1, + "maxItems": 10000, + "uniqueItems": true } }, "required": [ - "expected_version", - "expected_form_revision" + "ids" ], - "additionalProperties": false, - "anyOf": [ - { - "required": [ - "name" - ] + "additionalProperties": false + }, + "SolutionArchiveApplyRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "minItems": 1, + "maxItems": 10000, + "uniqueItems": true }, - { - "required": [ - "answers" - ] + "preview_token": { + "type": "string", + "minLength": 1, + "maxLength": 200000 } - ] + }, + "required": [ + "ids", + "preview_token" + ], + "additionalProperties": false }, - "Submit": { + "SolutionArchivePreviewItem": { "type": "object", "properties": { - "expected_version": { + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "solution_version": { "type": "integer", "minimum": 1 }, - "expected_form_revision": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" + "is_submitted": { + "type": "boolean" + }, + "is_archived": { + "type": "boolean" + }, + "score_count": { + "type": "integer", + "minimum": 0 + }, + "removed_assignments_count": { + "type": "integer", + "minimum": 0 + }, + "revoked_invitation_count": { + "type": "integer", + "minimum": 0 + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } } }, "required": [ - "expected_version", - "expected_form_revision" + "application", + "solution_version", + "is_submitted", + "is_archived", + "score_count", + "removed_assignments_count", + "revoked_invitation_count", + "errors" ], "additionalProperties": false }, - "CaptainCommand": { + "SolutionArchivePreview": { "type": "object", "properties": { - "expected_solution_version": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionArchivePreviewItem" + } + }, + "score_count": { "type": "integer", - "minimum": 1 + "minimum": 0 }, - "new_captain_user_id": { + "removed_assignments_count": { "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 + "minimum": 0 + }, + "revoked_invitation_count": { + "type": "integer", + "minimum": 0 + }, + "can_apply": { + "type": "boolean" + }, + "preview_token": { + "type": [ + "string", + "null" + ] } }, "required": [ - "expected_solution_version", - "new_captain_user_id" + "items", + "score_count", + "removed_assignments_count", + "revoked_invitation_count", + "can_apply", + "preview_token" ], "additionalProperties": false }, - "TeamCommand": { + "SolutionArchiveResult": { "type": "object", "properties": { - "expected_solution_version": { + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "solution_version": { "type": "integer", "minimum": 1 + }, + "archived_at": { + "type": "string", + "format": "date-time" } }, "required": [ - "expected_solution_version" + "application", + "solution_version", + "archived_at" ], "additionalProperties": false }, - "InvitationPermissions": { + "SolutionArchiveReceipt": { "type": "object", "properties": { - "can_edit": { - "type": "boolean" - }, - "can_revoke": { - "type": "boolean" - }, - "can_accept": { - "type": "boolean" + "changed_count": { + "type": "integer", + "minimum": 0 }, - "can_decline": { - "type": "boolean" + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionArchiveResult" + } } }, "required": [ - "can_edit", - "can_revoke", - "can_accept", - "can_decline" + "changed_count", + "items" ], "additionalProperties": false }, - "Invitation": { + "SolutionArchiveAdminChange": { "type": "object", "properties": { "id": { @@ -7001,439 +15276,515 @@ "minimum": 1, "maximum": 9007199254740991 }, - "version": { - "type": "integer", - "minimum": 1 - }, - "program": { - "$ref": "#/components/schemas/ProgramRef" - }, - "solution": { + "application": { "$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" - } - ] + "action": { + "const": "archive" }, - "motivational_letter": { + "actor": { "anyOf": [ { - "type": "string", - "maxLength": 4096 + "$ref": "#/components/schemas/User" }, { "type": "null" } ] }, - "status": { - "enum": [ - "pending", - "accepted", - "declined" - ] + "actor_name": { + "type": "string", + "maxLength": 512 }, "created_at": { "type": "string", "format": "date-time" }, - "updated_at": { + "before_version": { + "type": "integer", + "minimum": 1 + }, + "after_version": { + "type": "integer", + "minimum": 2 + }, + "removed_assignments_count": { + "type": "integer", + "minimum": 0 + }, + "score_count": { + "type": "integer", + "minimum": 0 + }, + "was_submitted": { + "type": "boolean" + }, + "archived_at": { "type": "string", "format": "date-time" }, - "permissions": { - "$ref": "#/components/schemas/InvitationPermissions" + "reason": { + "enum": [ + "detached", + "registration_removed" + ] + }, + "revoked_invitation_count": { + "type": "integer", + "minimum": 0 + }, + "registration_removal_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 } }, "required": [ "id", - "version", - "program", - "solution", - "captain", - "recipient", - "role", - "specialization", - "motivational_letter", - "status", + "application", + "action", + "actor", + "actor_name", "created_at", - "updated_at", - "permissions" + "before_version", + "after_version", + "removed_assignments_count", + "score_count", + "was_submitted", + "archived_at", + "reason", + "revoked_invitation_count" ], "additionalProperties": false }, - "CreateInvitation": { + "SolutionRegistrationRemovalPreviewRequest": { "type": "object", "properties": { - "expected_solution_version": { - "type": "integer", - "minimum": 1 + "registration_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "minItems": 1, + "maxItems": 10000, + "uniqueItems": true + } + }, + "required": [ + "registration_ids" + ], + "additionalProperties": false + }, + "SolutionRegistrationRemovalApplyRequest": { + "type": "object", + "properties": { + "registration_ids": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "minItems": 1, + "maxItems": 10000, + "uniqueItems": true }, - "recipient_user_id": { + "preview_token": { + "type": "string", + "minLength": 1, + "maxLength": 200000 + } + }, + "required": [ + "registration_ids", + "preview_token" + ], + "additionalProperties": false + }, + "SolutionRemovedRegistration": { + "type": "object", + "properties": { + "registration_id": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "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" - } - ] + "user": { + "$ref": "#/components/schemas/User" } }, "required": [ - "expected_solution_version", - "recipient_user_id" + "registration_id", + "user" ], "additionalProperties": false }, - "UpdateInvitation": { + "SolutionRegistrationRemovalEffect": { "type": "object", "properties": { - "expected_version": { - "type": "integer", - "minimum": 1 + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "expected_solution_version": { + "solution_version": { "type": "integer", - "minimum": 1 + "minimum": 1, + "maximum": 9007199254740991 }, - "role": { - "anyOf": [ - { - "type": "string", - "maxLength": 128 - }, - { - "type": "null" - } - ] + "is_submitted": { + "type": "boolean" }, - "specialization": { - "anyOf": [ - { - "type": "string", - "maxLength": 100 - }, - { - "type": "null" - } + "effect": { + "enum": [ + "archive", + "remove_members", + "revoke_invitations" ] }, - "motivational_letter": { - "anyOf": [ - { - "type": "string", - "maxLength": 4096 - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "expected_version", - "expected_solution_version" - ], - "additionalProperties": false, - "anyOf": [ - { - "required": [ - "role" - ] + "removed_members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Member" + } }, - { - "required": [ - "specialization" - ] + "removed_assignments_count": { + "type": "integer", + "minimum": 0 }, - { - "required": [ - "motivational_letter" - ] - } - ] - }, - "InvitationAction": { - "type": "object", - "properties": { - "expected_version": { + "revoked_invitation_count": { + "type": "integer", + "minimum": 0 + }, + "score_count": { "type": "integer", - "minimum": 1 + "minimum": 0 } }, "required": [ - "expected_version" + "application", + "solution_version", + "is_submitted", + "effect", + "removed_members", + "removed_assignments_count", + "revoked_invitation_count", + "score_count" ], "additionalProperties": false }, - "InvitationCreated": { + "SolutionRegistrationRemovalPreview": { "type": "object", "properties": { - "invitation": { - "$ref": "#/components/schemas/Invitation" + "registrations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionRemovedRegistration" + } }, - "solution_version": { + "effects": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionRegistrationRemovalEffect" + } + }, + "deleted_count": { "type": "integer", - "minimum": 1 - } - }, - "required": [ - "invitation", - "solution_version" - ], - "additionalProperties": false - }, - "InvitationActionResult": { - "type": "object", - "properties": { - "invitation": { - "$ref": "#/components/schemas/Invitation" + "minimum": 0 }, - "application": { - "anyOf": [ - { - "$ref": "#/components/schemas/SolutionRef" - }, - { - "type": "null" - } - ] + "archived_count": { + "type": "integer", + "minimum": 0 }, - "solution_version": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, - { - "type": "null" - } + "removed_member_count": { + "type": "integer", + "minimum": 0 + }, + "removed_assignments_count": { + "type": "integer", + "minimum": 0 + }, + "revoked_invitation_count": { + "type": "integer", + "minimum": 0 + }, + "score_count": { + "type": "integer", + "minimum": 0 + }, + "can_apply": { + "type": "boolean" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } + }, + "preview_token": { + "type": [ + "string", + "null" ] } }, "required": [ - "invitation", - "application", - "solution_version" + "registrations", + "effects", + "deleted_count", + "archived_count", + "removed_member_count", + "removed_assignments_count", + "revoked_invitation_count", + "score_count", + "can_apply", + "errors", + "preview_token" ], "additionalProperties": false }, - "MutationReceipt": { + "SolutionRegistrationRemovalResult": { "type": "object", "properties": { - "solution_id": { + "application": { + "$ref": "#/components/schemas/SolutionRef" + }, + "solution_version": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, - "solution_version": { + "change_id": { "type": "integer", - "minimum": 1 + "minimum": 1, + "maximum": 9007199254740991 }, - "updated_at": { - "type": "string", - "format": "date-time" + "action": { + "enum": [ + "archive", + "remove_registrations" + ] } }, "required": [ - "solution_id", + "application", "solution_version", - "updated_at" + "change_id", + "action" ], "additionalProperties": false }, - "SolutionPage": { + "SolutionRegistrationRemovalReceipt": { "type": "object", "properties": { - "count": { + "audit_id": { "type": "integer", - "minimum": 0 - }, - "next": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "minimum": 1, + "maximum": 9007199254740991 }, - "previous": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "deleted_count": { + "type": "integer", + "minimum": 0 }, - "results": { + "items": { "type": "array", "items": { - "$ref": "#/components/schemas/SolutionSummary" + "$ref": "#/components/schemas/SolutionRegistrationRemovalResult" } } }, "required": [ - "count", - "next", - "previous", - "results" + "audit_id", + "deleted_count", + "items" ], "additionalProperties": false }, - "InvitationPage": { + "SolutionRegistrationRemovalAudit": { "type": "object", "properties": { - "count": { + "id": { "type": "integer", - "minimum": 0 + "minimum": 1, + "maximum": 9007199254740991 }, - "next": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 }, - "previous": { + "actor": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/User" }, { "type": "null" } ] }, - "results": { + "actor_name": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "registrations": { "type": "array", "items": { - "$ref": "#/components/schemas/Invitation" + "$ref": "#/components/schemas/SolutionRemovedRegistration" } + }, + "deleted_count": { + "type": "integer", + "minimum": 0 } }, "required": [ - "count", - "next", - "previous", - "results" + "id", + "program_id", + "actor", + "actor_name", + "created_at", + "registrations", + "deleted_count" ], "additionalProperties": false }, - "UploadResponse": { + "SolutionRegistrationRemovalAuditDetail": { "type": "object", "properties": { - "url": { - "type": "string", - "format": "uri", - "pattern": "^https?://" + "audit": { + "$ref": "#/components/schemas/SolutionRegistrationRemovalAudit" + }, + "changes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionAdminChange" + } } }, "required": [ - "url" + "audit", + "changes" ], "additionalProperties": false }, - "SubmissionSnapshot": { + "SolutionRegistrationRemovalAuditPage": { "type": "object", "additionalProperties": false, "required": [ - "name", - "captain_id", - "members", - "form", - "answers", - "submitted_at" + "count", + "offset", + "limit", + "results" ], "properties": { - "name": { - "type": "string", - "maxLength": 256 + "count": { + "type": "integer", + "minimum": 0 }, - "captain_id": { + "offset": { + "type": "integer", + "minimum": 0 + }, + "limit": { "type": "integer", "minimum": 1, - "maximum": 9007199254740991 + "maximum": 100 }, - "members": { + "results": { "type": "array", "items": { - "$ref": "#/components/schemas/Member" + "$ref": "#/components/schemas/SolutionRegistrationRemovalAudit" } + } + } + }, + "SolutionRegistrationAdminChange": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 }, - "form": { - "$ref": "#/components/schemas/Form" + "application": { + "$ref": "#/components/schemas/SolutionRef" }, - "answers": { + "action": { + "const": "remove_registrations" + }, + "actor": { + "anyOf": [ + { + "$ref": "#/components/schemas/User" + }, + { + "type": "null" + } + ] + }, + "actor_name": { + "type": "string", + "maxLength": 512 + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "before_version": { + "type": "integer", + "minimum": 1 + }, + "after_version": { + "type": "integer", + "minimum": 2 + }, + "removed_assignments_count": { + "const": 0 + }, + "score_count": { + "type": "integer", + "minimum": 0 + }, + "was_submitted": { + "type": "boolean" + }, + "revoked_invitation_count": { + "type": "integer", + "minimum": 0 + }, + "removed_members": { "type": "array", "items": { - "$ref": "#/components/schemas/Answer" + "$ref": "#/components/schemas/Member" } }, - "submitted_at": { - "type": "string", - "format": "date-time" + "registration_removal_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 } - } + }, + "required": [ + "id", + "application", + "action", + "actor", + "actor_name", + "created_at", + "before_version", + "after_version", + "removed_assignments_count", + "score_count", + "was_submitted", + "revoked_invitation_count", + "removed_members", + "registration_removal_id" + ], + "additionalProperties": false } } } diff --git a/docs/design/program-solutions-manager-d02-20261025/README.md b/docs/design/program-solutions-manager-d02-20261025/README.md new file mode 100644 index 000000000..e6c564118 --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/README.md @@ -0,0 +1,25 @@ + + +# D02: административные действия организатора + +Статус: **DESIGN APPROVAL REQUIRED**. Существующий запрос D02 остается открытым; D00v3/D01 согласованы, D03 отдельно. Новый Angular UI не реализован этим checkpoint. + +[Открыть интерактивный макет](http://127.0.0.1:8769/manager-v1/?screen=solutions). В программе сохранены четыре раздела; команда остается второй вкладкой заявки. Добавлены первая сдача организатором, смена кейса, исключение участника, архив, удаление регистраций и история с материалами/составом до и после. Все данные синтетические и живут только в текущей вкладке. + +Действующие Project-программы, реальные данные и runtime код не менялись. Действия с решениями не создают Project. Конфликт связи с проектом блокирует всю административную команду. Архив сохраняет материалы, состав и оценки; архивный UI предлагает поддержанные текущими readers поиск и чтение материалов/состава, без фильтра поля/XLSX и отдельного просмотра значений архивных оценок. + +[Полный handoff: сценарии, API mapping, AC, states и quality review](prototype/manager-v1/design-handoff.md). [Результаты браузера](prototype/manager-v1/admin-qa-evidence.json), [source hashes](source-manifest.json), [исходный HTML](prototype/manager-v1/index.html). Нижний исторический handoff отмечен отдельно; его старые QA ссылки не входят в текущую проверку. + +Завершенный 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 тесты. + +## Просмотр и повтор проверки + +Существующий сервер на 8769 менять не требуется. Для автономного просмотра можно поднять статический сервер на свободном порту с корнем `prototype` и открыть `/manager-v1/`. `verify-prototype.cjs` принимает `D02_BASE_URL`, `D02_OUTPUT_DIR` и `PLAYWRIGHT_MODULE` (путь к уже установленному Playwright). Проверка использует установленный Chrome через `channel: "chrome"`, собственный headless context, блокирует внешние запросы, записывает только synthetic screenshots/evidence; backend и БД ей не нужны. Выходные файлы по умолчанию перезаписывают только evidence/screenshots текущего bundle; для сохранения исходного отчета используйте отдельный существующий `D02_OUTPUT_DIR`. + +В панели макета: «Состояние» → «Готовый черновик» дает успешную первую сдачу работы «Лаборатория практики». «Ответ на действие» воспроизводит загрузку, устаревший предпросмотр, ошибку, потерю прав или конфликт с Project. Смена формата/состояния и перезагрузка сбрасывают локальные демонстрационные изменения. + +## Граница результата + +Это дизайн и проверки прототипа. Последние runtime refs остаются BE `394bf166` / FE `b4191a8b`, C00 1.20.1; прежние 420 native/2949 FE относятся к отдельному implementation checkpoint, здесь не повторялись и не складываются с 130 проверками макета. Исходные 26 AC не менялись: 23 PASS, AC18/19 OPEN, AC21 отдельно BE10. Полный первый выпуск и QA01/REL01 не завершены. Merge/deploy/переключение действующих программ не выполнялись. + +[Проектный design skill](Z:/Документы/PROCOLLAB-dev/procollab-design-system-v1/.codex/skills/procollab-design-screen/SKILL.md) требует: «После Design QA, visual continuity и Impeccable предъяви конкретный дизайн и остановись со статусом **DESIGN APPROVAL REQUIRED** … Дождись явного approval пользователя». Поэтому новый Angular интерфейс следует после уже ожидаемого D02 approval. Отдельный вопрос не отправлялся. diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-Bold.woff2 b/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-Bold.woff2 new file mode 100644 index 000000000..1093fe0da Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-Bold.woff2 differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-Regular.woff2 b/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-Regular.woff2 new file mode 100644 index 000000000..0b682c4d2 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-Regular.woff2 differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-SemiBold.woff2 b/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-SemiBold.woff2 new file mode 100644 index 000000000..7c2fb176b Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/assets/Mont-SemiBold.woff2 differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/assets/logo.svg b/docs/design/program-solutions-manager-d02-20261025/prototype/assets/logo.svg new file mode 100644 index 000000000..37bad9f3b --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/assets/logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archive-preview-1440.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archive-preview-1440.png new file mode 100644 index 000000000..1d5891abc Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archive-preview-1440.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archive-preview-390.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archive-preview-390.png new file mode 100644 index 000000000..47569dafb Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archive-preview-390.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archived-team-mobile.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archived-team-mobile.png new file mode 100644 index 000000000..581cf230d Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-archived-team-mobile.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-history-desktop.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-history-desktop.png new file mode 100644 index 000000000..b2b12ddb6 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-history-desktop.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-preview-1440.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-preview-1440.png new file mode 100644 index 000000000..7b0059c17 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-preview-1440.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-preview-390.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-preview-390.png new file mode 100644 index 000000000..6df239a6f Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-case-preview-390.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-before-1440.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-before-1440.png new file mode 100644 index 000000000..0eef02b32 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-before-1440.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-before-390.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-before-390.png new file mode 100644 index 000000000..784f4b9c2 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-before-390.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-current-1440.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-current-1440.png new file mode 100644 index 000000000..a82f407c7 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-current-1440.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-current-390.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-current-390.png new file mode 100644 index 000000000..0e3e4c6da Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-continuity-current-390.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-legacy-conflict-mobile.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-legacy-conflict-mobile.png new file mode 100644 index 000000000..5a46d12af Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-legacy-conflict-mobile.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-qa-evidence.json b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-qa-evidence.json new file mode 100644 index 000000000..af6ddaecd --- /dev/null +++ b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-qa-evidence.json @@ -0,0 +1,825 @@ +{ + "kind": "Synthetic HTML prototype only; no backend or Angular verification", + "started": "2026-10-25T17:58:47.690Z", + "checks": [ + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "solutions", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "solutions", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "solutions", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "solutions", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "evaluation", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "evaluation", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "evaluation", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "evaluation", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "results", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "results", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "results", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "results", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "analytics", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "analytics", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "analytics", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "kch", + "screen": "analytics", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "solutions", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "solutions", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "solutions", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "solutions", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "evaluation", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "evaluation", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "evaluation", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "evaluation", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "results", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "results", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "results", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "results", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "analytics", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "analytics", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "analytics", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "hack", + "screen": "analytics", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "solutions", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "solutions", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "solutions", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "solutions", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "evaluation", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "evaluation", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "evaluation", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "evaluation", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "results", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "results", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "results", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "results", + "width": 1440 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "analytics", + "width": 320 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "analytics", + "width": 390 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "analytics", + "width": 768 + }, + { + "name": "Existing four-section layout", + "status": "PASS", + "format": "noncompetitive", + "screen": "analytics", + "width": 1440 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "ready-draft", + "width": 320 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "ready-draft", + "width": 390 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "ready-draft", + "width": 768 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "ready-draft", + "width": 1440 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "loading", + "width": 320 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "loading", + "width": 390 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "loading", + "width": 768 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "loading", + "width": 1440 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "empty", + "width": 320 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "empty", + "width": 390 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "empty", + "width": 768 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "empty", + "width": 1440 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "no-results", + "width": 320 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "no-results", + "width": 390 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "no-results", + "width": 768 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "no-results", + "width": 1440 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "error", + "width": 320 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "error", + "width": 390 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "error", + "width": 768 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "error", + "width": 1440 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "no-access", + "width": 320 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "no-access", + "width": 390 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "no-access", + "width": 768 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "no-access", + "width": 1440 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "long", + "width": 320 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "long", + "width": 390 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "long", + "width": 768 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "long", + "width": 1440 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "open-mode", + "width": 320 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "open-mode", + "width": 390 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "open-mode", + "width": 768 + }, + { + "name": "Existing page state", + "status": "PASS", + "state": "open-mode", + "width": 1440 + }, + { + "name": "Case change preserves materials/zero/false; before and after", + "status": "PASS", + "format": "kch", + "width": 390 + }, + { + "name": "Accepted member removal; captain protected; original roster retained", + "status": "PASS", + "format": "kch", + "width": 390 + }, + { + "name": "Archive readonly; materials/team retained, ratings preservation explained; active list excludes it", + "status": "PASS", + "format": "kch", + "width": 390 + }, + { + "name": "Bulk registration removal: captain/member/pending/unaffiliated, independent audit", + "status": "PASS", + "format": "kch", + "width": 390 + }, + { + "name": "Case change preserves materials/zero/false; before and after", + "status": "PASS", + "format": "kch", + "width": 1440 + }, + { + "name": "Accepted member removal; captain protected; original roster retained", + "status": "PASS", + "format": "kch", + "width": 1440 + }, + { + "name": "Archive readonly; materials/team retained, ratings preservation explained; active list excludes it", + "status": "PASS", + "format": "kch", + "width": 1440 + }, + { + "name": "Bulk registration removal: captain/member/pending/unaffiliated, independent audit", + "status": "PASS", + "format": "kch", + "width": 1440 + }, + { + "name": "Case change preserves materials/zero/false; before and after", + "status": "PASS", + "format": "hack", + "width": 390 + }, + { + "name": "Accepted member removal; captain protected; original roster retained", + "status": "PASS", + "format": "hack", + "width": 390 + }, + { + "name": "Archive readonly; materials/team retained, ratings preservation explained; active list excludes it", + "status": "PASS", + "format": "hack", + "width": 390 + }, + { + "name": "Bulk registration removal: captain/member/pending/unaffiliated, independent audit", + "status": "PASS", + "format": "hack", + "width": 390 + }, + { + "name": "Case change preserves materials/zero/false; before and after", + "status": "PASS", + "format": "hack", + "width": 1440 + }, + { + "name": "Accepted member removal; captain protected; original roster retained", + "status": "PASS", + "format": "hack", + "width": 1440 + }, + { + "name": "Archive readonly; materials/team retained, ratings preservation explained; active list excludes it", + "status": "PASS", + "format": "hack", + "width": 1440 + }, + { + "name": "Bulk registration removal: captain/member/pending/unaffiliated, independent audit", + "status": "PASS", + "format": "hack", + "width": 1440 + }, + { + "name": "Case change preserves materials/zero/false; before and after", + "status": "PASS", + "format": "noncompetitive", + "width": 390 + }, + { + "name": "Accepted member removal; captain protected; original roster retained", + "status": "PASS", + "format": "noncompetitive", + "width": 390 + }, + { + "name": "Archive readonly; materials/team retained, ratings preservation explained; active list excludes it", + "status": "PASS", + "format": "noncompetitive", + "width": 390 + }, + { + "name": "Bulk registration removal: captain/member/pending/unaffiliated, independent audit", + "status": "PASS", + "format": "noncompetitive", + "width": 390 + }, + { + "name": "Case change preserves materials/zero/false; before and after", + "status": "PASS", + "format": "noncompetitive", + "width": 1440 + }, + { + "name": "Accepted member removal; captain protected; original roster retained", + "status": "PASS", + "format": "noncompetitive", + "width": 1440 + }, + { + "name": "Archive readonly; materials/team retained, ratings preservation explained; active list excludes it", + "status": "PASS", + "format": "noncompetitive", + "width": 1440 + }, + { + "name": "Bulk registration removal: captain/member/pending/unaffiliated, independent audit", + "status": "PASS", + "format": "noncompetitive", + "width": 1440 + }, + { + "name": "Pending-only/no-solution independent audit", + "status": "PASS", + "ids": [9] + }, + { + "name": "Pending-only/no-solution independent audit", + "status": "PASS", + "ids": [10] + }, + { + "name": "Removing registration after archive keeps its roster and earlier history", + "status": "PASS" + }, + { + "name": "Action failure is recoverable and requires fresh confirmation", + "status": "PASS", + "mode": "stale" + }, + { + "name": "Action failure is recoverable and requires fresh confirmation", + "status": "PASS", + "mode": "error" + }, + { + "name": "Action failure is recoverable and requires fresh confirmation", + "status": "PASS", + "mode": "no-access" + }, + { + "name": "Mixed Project conflict blocks entire command, selection retained", + "status": "PASS" + }, + { + "name": "Preview loading can be dismissed without mutation", + "status": "PASS" + }, + { + "name": "Keyboard tabs, focus containment, Escape, return focus", + "status": "PASS" + }, + { + "name": "Apply blocks repeat/close while busy; completed state stays visible", + "status": "PASS" + }, + { + "name": "Existing field filters preserve zero, false, not-applicable", + "status": "PASS" + }, + { + "name": "Existing analytics drilldown / team / back", + "status": "PASS" + }, + { + "name": "Existing allocation preview and apply", + "status": "PASS" + }, + { + "name": "Existing reminder preview", + "status": "PASS" + }, + { + "name": "Existing completed zero/false score", + "status": "PASS" + }, + { + "name": "Manager first submission: ready draft, no second-submit control", + "status": "PASS", + "format": "kch", + "width": 390 + }, + { + "name": "Incomplete first submission is blocked with field names", + "status": "PASS", + "format": "kch", + "width": 390 + }, + { + "name": "Manager first submission: ready draft, no second-submit control", + "status": "PASS", + "format": "kch", + "width": 1440 + }, + { + "name": "Incomplete first submission is blocked with field names", + "status": "PASS", + "format": "kch", + "width": 1440 + }, + { + "name": "Manager first submission: ready draft, no second-submit control", + "status": "PASS", + "format": "hack", + "width": 390 + }, + { + "name": "Incomplete first submission is blocked with field names", + "status": "PASS", + "format": "hack", + "width": 390 + }, + { + "name": "Manager first submission: ready draft, no second-submit control", + "status": "PASS", + "format": "hack", + "width": 1440 + }, + { + "name": "Incomplete first submission is blocked with field names", + "status": "PASS", + "format": "hack", + "width": 1440 + }, + { + "name": "Noncompetitive format has no artificial submit action", + "status": "PASS" + }, + { + "name": "Changing review format resets local mutations and team counters", + "status": "PASS" + }, + { + "name": "Archive uses separate supported search without active-only fields/export", + "status": "PASS" + } + ], + "screenshots": [ + "admin-case-preview-390.png", + "admin-archive-preview-390.png", + "admin-archived-team-mobile.png", + "admin-registration-preview-390.png", + "admin-case-preview-1440.png", + "admin-case-history-desktop.png", + "admin-archive-preview-1440.png", + "admin-registration-preview-1440.png", + "admin-stale-mobile.png", + "admin-legacy-conflict-mobile.png", + "admin-submit-preview-mobile.png" + ], + "pageErrors": [], + "failedRequests": [], + "nonStaticRequests": [], + "externalBlocked": { + "gc.kis.v2.scr.kaspersky-labs.com:GET": 130 + }, + "finished": "2026-10-25T17:59:41.934Z", + "status": "PASS", + "count": 130 +} diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-registration-preview-1440.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-registration-preview-1440.png new file mode 100644 index 000000000..47146d40b Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-registration-preview-1440.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-registration-preview-390.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-registration-preview-390.png new file mode 100644 index 000000000..8328101d5 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-registration-preview-390.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-stale-mobile.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-stale-mobile.png new file mode 100644 index 000000000..02dfd5e78 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-stale-mobile.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-submit-preview-mobile.png b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-submit-preview-mobile.png new file mode 100644 index 000000000..3fe405bc5 Binary files /dev/null and b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin-submit-preview-mobile.png differ diff --git a/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin.js b/docs/design/program-solutions-manager-d02-20261025/prototype/manager-v1/admin.js new file mode 100644 index 000000000..d442663ec --- /dev/null +++ b/docs/design/program-solutions-manager-d02-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}
`; + 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-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/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 000000000..1093fe0da Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-Bold.woff2 differ 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 000000000..0b682c4d2 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-Regular.woff2 differ diff --git a/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-SemiBold.woff2 b/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-SemiBold.woff2 new file mode 100644 index 000000000..7c2fb176b Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/assets/Mont-SemiBold.woff2 differ diff --git a/docs/design/program-solutions-scope-20261025/prototype/assets/logo.svg b/docs/design/program-solutions-scope-20261025/prototype/assets/logo.svg new file mode 100644 index 000000000..885fbae9c --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/assets/logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + 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}
`; + 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 000000000..ee599fc44 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-5LPDNI7X.ttf differ diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-T6L2NNOO.woff b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-T6L2NNOO.woff new file mode 100644 index 000000000..b39fea5ca Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-T6L2NNOO.woff differ diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-UA5RCVYG.woff2 b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-UA5RCVYG.woff2 new file mode 100644 index 000000000..0b682c4d2 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-Regular-UA5RCVYG.woff2 differ diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-7XOUXTAI.woff2 b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-7XOUXTAI.woff2 new file mode 100644 index 000000000..7c2fb176b Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-7XOUXTAI.woff2 differ diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-M6WLAC2K.ttf b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-M6WLAC2K.ttf new file mode 100644 index 000000000..5ca92c503 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-M6WLAC2K.ttf differ 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 000000000..b18fe0f1b Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/media/Mont-SemiBold-WNUGA77E.woff differ diff --git a/docs/design/program-solutions-scope-20261025/prototype/manager-v1/results-reference.css b/docs/design/program-solutions-scope-20261025/prototype/manager-v1/results-reference.css new file mode 100644 index 000000000..b788591df --- /dev/null +++ b/docs/design/program-solutions-scope-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-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 ``;} + function state(title,copy,action='') {return `

${title}

${copy}

${action}
`;} + function retry(detail=false) {return ``;} + function list() { + const q=current(), all=cards(), found=all.filter(card=>card.application.name.toLocaleLowerCase('ru').includes(q.search.toLocaleLowerCase('ru'))); + const rows=found.slice(q.offset,q.offset+limit); + let body=''; + if(scenario==='loading')body=`

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

`; + else if(scenario==='error')body=state('Не удалось загрузить решения','Попробуйте еще раз. Поиск сохранен.',retry()); + else if(scenario==='empty')body=state('Опубликованных решений пока нет','Когда работы программы будут опубликованы, они появятся здесь.'); + else if(scenario==='no-results'||!found.length)body=state('Решения не найдены','Попробуйте другое название или сбросьте поиск.',''); + else body=`
Найдено решений · ${found.length}${q.search?'':''}
`; + return `

Решения

Опубликованные работы участников программы

${searchForm(q.search)}${body}
`; + } + function fields() { + const shared=[{id:1,name:'О решении',type:'textarea',value:'Сервис помогает студенческим командам находить поддержку, обмениваться опытом и организовывать совместную работу.\n\nВ материалах описаны сценарий использования, подход к реализации и результаты пилота.'},{id:2,name:'Размер пилота',type:'number',value:0},{id:3,name:'Пилот завершен',type:'checkbox',value:false}]; + if(format==='hack')shared.push({id:4,name:'Репозиторий',type:'url',value:'https://example.com/team/repository'},{id:5,name:'Демонстрация',type:'url',value:'https://example.com/demo'}); + else shared.push({id:4,name:'Презентация',type:'file',value:{url:'https://example.com/materials/solution.pdf'}},{id:5,name:'Направление',type:'select',value:'Образование'}); + if(scenario==='partial')for(const field of shared)if([1,4].includes(field.id))field.value=null; + if(scenario==='long'){shared[0].name='Подробное описание предлагаемого решения и результатов проверки основных гипотез';shared[0].value+='\n\n'+('Развитие продукта, работа команды и обратная связь участников. '.repeat(35));shared[4].value=format==='hack'?'https://example.com/demo/'+('long-path-'.repeat(20)):'Образование и развитие студенческих инициатив';} + return shared; + } + function materials() { + return `
${fields().map(field=>{ + let answer; + if(field.value===null||field.value==='')answer='

Не добавлено

'; + else if(field.type==='checkbox')answer=`

${field.value?'Да':'Нет'}

`; + else if(field.type==='file')answer=`Открыть материал , новая вкладка`; + else if(field.type==='url')answer=`${esc(field.value)} , новая вкладка`; + else answer=`

${esc(field.value)}

`; + return `

${esc(field.name)}

${answer}
`; + }).join('')}
`; + } + function team(card) { + const members=[{user:card.captain,specialization:people.find(p=>p.user.id===card.captain.id).specialization},...people.filter(p=>p.user.id!==card.captain.id)].slice(0,card.accepted_count); + return `

Участников · ${members.length}

`; + } + function detail() { + const q=current(), card=cards().find(item=>String(item.application.solution_id)===q.solution), back=`← Все решения программы`; + if(scenario==='not-found'||!card)return `
${back}${state('Решение недоступно','Возможно, ссылка изменилась или материалы не опубликованы. Посмотрите другие решения программы.')}
`; + const nav=``; + let content=q.tab==='team'?team(card):materials(); + if(scenario==='detail-error')content=state('Не удалось открыть материалы','Попробуйте еще раз. Решение и выбранная вкладка сохранены.',retry(true)); + if(scenario==='loading')content='

Загружаем материалы…

'; + return `
${back}

${esc(title(card))}

Опубликовано 25 октября 2026

${nav}
${content}
`; + } + function render() { + $('#demo-notice').hidden=true; + $('#program-name').textContent=formats[format].name; + $('#program-format').textContent=formats[format].label; + $('#scope-note').textContent=format==='noncompetitive'?'Публичный API сохраненных неконкурсных решений готов. Подключение этого экрана ожидает согласования дизайна.':'Публичный API сданных конкурсных решений готов. Публикация несданных черновиков остается открытым вопросом; дизайн ожидает согласования.'; + $('#content').innerHTML=current().solution?detail():list(); + const form=$('#search-form');if(form)form.addEventListener('submit',event=>{event.preventDefault();navigate({search:new FormData(form).get('search').trim(),offset:null});}); + document.title=(current().solution?'Решение':'Решения')+' — '+formats[format].name+' — PROCOLLAB'; + } + document.addEventListener('click',event=>{ + const target=event.target.closest('a,button');if(!target)return; + if(target.matches('[data-demo-link]')){event.preventDefault();$('#demo-notice').hidden=false;return;} + if(target.matches('[data-reset]')){scenario='ready';$('#scenario').value='ready';navigate({search:null,offset:null});} + if(target.matches('[data-page]'))navigate({offset:Number(target.dataset.page)||null}); + if(target.matches('[data-retry]')){scenario='ready';$('#scenario').value='ready';render();$('#content').focus();} + if((event.ctrlKey||event.metaKey||event.shiftKey||event.altKey)||event.button!==0)return; + if(target.matches('[data-open]')){event.preventDefault();navigate({solution:target.dataset.open,tab:null});window.scrollTo(0,0);} + if(target.matches('[data-back]')){event.preventDefault();const id=current().solution;navigate({solution:null,tab:null},false);const card=$(`[data-open="${id}"]`);if(card)card.focus();else $('#content').focus();} + if(target.matches('[data-tab]')){event.preventDefault();const tab=target.dataset.tab;navigate({tab:tab==='team'?'team':null},false);$(`[data-tab="${tab}"]`).focus();} + }); + $('#format').addEventListener('change',event=>{format=event.target.value;history.replaceState({},'',location.pathname);render();}); + $('#scenario').addEventListener('change',event=>{scenario=event.target.value;if(['not-found','detail-error','partial'].includes(scenario)&&!current().solution)history.pushState({},'',url({solution:503,tab:null}));render();}); + window.addEventListener('popstate',()=>{render();$('#content').focus();}); + render(); +})(); diff --git a/docs/design/program-solutions-scope-20261025/prototype/public-v1/index.html b/docs/design/program-solutions-scope-20261025/prototype/public-v1/index.html new file mode 100644 index 000000000..7232c4941 --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/public-v1/index.html @@ -0,0 +1,30 @@ + + + + + + Решения программы — PROCOLLAB + + + + + + + +
+
PROCOLLABПрограмма
+

+
+ + +
+ + + diff --git a/docs/design/program-solutions-scope-20261025/prototype/public-v1/source-reference.css b/docs/design/program-solutions-scope-20261025/prototype/public-v1/source-reference.css new file mode 100644 index 000000000..f977594bf --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/public-v1/source-reference.css @@ -0,0 +1,1205 @@ +/** @format */ +.prototype-solution { + /** @format */ +} +.prototype-solution .source-host { + display: block; + min-width: 0; +} +.prototype-solution .solution { + max-width: 864px; + padding: 0 20px; + margin: 28px auto 64px; + font-size: 14px; + color: var(--black); +} +.prototype-solution a { + color: var(--accent); + text-decoration: none; +} +.prototype-solution header { + display: flex; + gap: 20px; + align-items: start; + justify-content: space-between; + margin: 28px 0; +} +.prototype-solution h1 { + margin: 0; + font-size: 24px; + line-height: 1.4; + overflow-wrap: anywhere; +} +.prototype-solution h2 { + margin: 0 0 12px; + font-size: 18px; + font-weight: 500; +} +.prototype-solution p { + line-height: 1.5; +} +.prototype-solution .muted { + color: var(--grey-for-text); +} +.prototype-solution nav { + display: flex; + gap: 4px; + width: min(100%, 432px); + padding: 4px; + margin-bottom: 28px; + background: var(--medium-grey-for-outline); + border-radius: 24px; +} +.prototype-solution nav a { + display: flex; + flex: 1; + gap: 8px; + align-items: center; + justify-content: center; + min-height: 40px; + padding: 8px 12px; + color: var(--black); + border-radius: 20px; +} +.prototype-solution nav a[aria-current] { + color: var(--accent); + background: var(--white, #fff); +} +.prototype-solution nav a:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.prototype-solution .solution-name { + display: grid; + gap: 8px; + margin: 24px 0; +} +.prototype-solution .solution-name .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%; +} +.prototype-solution .solution-name .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-solution .solution-name .ui-field::placeholder { + color: var(--dark-grey); +} +.prototype-solution .solution-name .ui-field:hover:not(:disabled) { + border-color: var(--dark-grey); +} +.prototype-solution .solution-name .ui-field:focus-visible, .prototype-solution .solution-name .ui-field:focus-within, .prototype-solution .solution-name .ui-field--open { + border-color: var(--ui-accent); + box-shadow: var(--ui-focus-ring); +} +.prototype-solution .solution-name .ui-field--error, .prototype-solution .solution-name .ui-field--overflow, .prototype-solution .solution-name .ui-field[aria-invalid=true] { + border: 1px solid var(--ui-danger); +} +.prototype-solution .solution-name .ui-field--error:focus, .prototype-solution .solution-name .ui-field--error:focus-within, .prototype-solution .solution-name .ui-field--overflow:focus, .prototype-solution .solution-name .ui-field--overflow:focus-within, .prototype-solution .solution-name .ui-field[aria-invalid=true]:focus, .prototype-solution .solution-name .ui-field[aria-invalid=true]:focus-within { + box-shadow: var(--ui-error-ring); +} +.prototype-solution .solution-name .ui-field:disabled, .prototype-solution .solution-name .ui-field--disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); +} +@media (max-width: 749px) { + .prototype-solution .solution-name .ui-field { + font-size: 16px; + } +} +.prototype-solution .error { + color: var(--error, var(--ui-error-text)); +} +.prototype-solution .local-answers { + padding: 20px 0; + border-top: 1px solid var(--medium-grey-for-outline); +} +.prototype-solution .local-answers p { + overflow-wrap: anywhere; + white-space: pre-wrap; +} +.prototype-solution .draft-actions { + padding-top: 20px; + margin-top: 24px; + border-top: 1px solid var(--medium-grey-for-outline); +} +.prototype-solution .draft-actions__buttons { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} +.prototype-solution .submission-confirmation-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} +.prototype-solution .submission-issues { + padding-left: 20px; +} +.prototype-solution .submission-issues .ui-button { + height: auto; + padding: 4px 0; + text-align: left; + white-space: normal; +} +.prototype-solution h2:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 4px; +} +.prototype-solution .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; +} +.prototype-solution .ui-button:hover:not(:disabled) { + text-decoration: none; + background: var(--button-hover); + border-color: var(--button-hover); +} +.prototype-solution .ui-button:active:not(:disabled) { + background: var(--accent-dark); +} +.prototype-solution .ui-button:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; +} +.prototype-solution .ui-button:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); +} +.prototype-solution .ui-button--secondary, .prototype-solution .ui-button--outline { + color: var(--button-color); + background: transparent; +} +.prototype-solution .ui-button--secondary:hover:not(:disabled), .prototype-solution .ui-button--secondary:active:not(:disabled), .prototype-solution .ui-button--outline:hover:not(:disabled), .prototype-solution .ui-button--outline:active:not(:disabled) { + color: var(--button-color); + background: var(--accent-light); +} +.prototype-solution .ui-button--danger, .prototype-solution .ui-button--red { + --button-color: var(--ui-danger); + --button-hover: var(--red-dark); +} +.prototype-solution .ui-button--danger:active:not(:disabled), .prototype-solution .ui-button--red:active:not(:disabled) { + background: var(--red-dark); +} +.prototype-solution .ui-button--danger.button--outline:hover:not(:disabled), .prototype-solution .ui-button--red.button--outline:hover:not(:disabled) { + background: var(--red-light); +} +.prototype-solution .ui-button--grey { + --button-color: var(--grey-button); + --button-hover: var(--gray); + --button-text: var(--black); +} +.prototype-solution .ui-button--green { + --button-color: var(--green); + --button-hover: var(--green-dark); +} +.prototype-solution .ui-button--gold { + --button-color: var(--gold); + --button-hover: var(--gold-dark); +} +.prototype-solution .ui-button--white { + --button-color: var(--light-white); + --button-hover: var(--white); + --button-text: var(--black); +} +.prototype-solution .ui-button--gradient { + background: var(--gradient); +} +.prototype-solution .ui-button--no-border { + border-color: transparent; +} +.prototype-solution .ui-button--medium { + min-width: min(157px, 100%); +} +.prototype-solution .ui-button--big { + width: 100%; +} +.prototype-solution .ui-button--extra-small { + padding-inline: 12px; +} +.prototype-solution .ui-button--icon, .prototype-solution .ui-button--text { + color: var(--ui-accent); + background: transparent; + border-color: transparent; +} +.prototype-solution .ui-button--icon:hover:not(:disabled), .prototype-solution .ui-button--icon:active:not(:disabled), .prototype-solution .ui-button--text:hover:not(:disabled), .prototype-solution .ui-button--text:active:not(:disabled) { + color: var(--accent-dark); + background: var(--accent-light); + border-color: transparent; +} +.prototype-solution .ui-button--icon { + min-width: 44px; + padding: 10px; +} +.prototype-solution .ui-button--text { + padding-inline: 12px; +} +@media (max-width: 749px) { + .prototype-solution .ui-button { + padding-inline: 16px; + } + .prototype-solution .ui-button--icon { + padding: 9px; + } + .prototype-solution .ui-button--text { + padding-inline: 12px; + } +} +@media (max-width: 600px) { + .prototype-solution header { + flex-direction: column; + } + .prototype-solution .draft-actions__buttons { + flex-direction: column; + align-items: stretch; + } +} +/** @format */ +/** @format */ +.prototype-cards { + /** @format */ +} +.prototype-cards .card__body { + position: relative; + display: flex; + flex-direction: column; + width: 156px; +} +@media screen and (max-width: 999px) { + .prototype-cards .card__body { + 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); + } +} +@media screen and (max-width: 999px) { + .prototype-cards .card__body { + height: auto; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__body { + height: 180px; + } +} +@media screen and (max-width: 999px) { + .prototype-cards .card__body { + min-height: 180px; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__body { + padding: 15px 0 12px; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__body { + background-color: var(--light-white); + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__body { + border: 0.5px solid var(--medium-grey-for-outline); + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__body { + border-radius: var(--rounded-xl); + } +} +.prototype-cards .card__body--empty { + background: transparent; + border: 0.5px dashed var(--accent); +} +.prototype-cards .card__info { + display: flex; + flex-shrink: 0; + gap: 50px; + align-items: center; + justify-content: space-evenly; +} +.prototype-cards .card__info--program { + display: flex; + gap: 5px; + align-items: center; + justify-content: center; +} +.prototype-cards .card__project-link { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + border-radius: inherit; +} +.prototype-cards .card__project-link:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 2px; +} +.prototype-cards .card__project-action { + width: 100%; +} +@media screen and (max-width: 999px) { + .prototype-cards .card__project-action { + --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; + } + .prototype-cards .card__project-action:hover:not(:disabled) { + text-decoration: none; + background: var(--button-hover); + border-color: var(--button-hover); + } + .prototype-cards .card__project-action:active:not(:disabled) { + background: var(--accent-dark); + } + .prototype-cards .card__project-action:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; + } + .prototype-cards .card__project-action:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); + } + .prototype-cards .card__project-action--secondary, .prototype-cards .card__project-action--outline { + color: var(--button-color); + background: transparent; + } + .prototype-cards .card__project-action--secondary:hover:not(:disabled), .prototype-cards .card__project-action--secondary:active:not(:disabled), .prototype-cards .card__project-action--outline:hover:not(:disabled), .prototype-cards .card__project-action--outline:active:not(:disabled) { + color: var(--button-color); + background: var(--accent-light); + } + .prototype-cards .card__project-action--danger, .prototype-cards .card__project-action--red { + --button-color: var(--ui-danger); + --button-hover: var(--red-dark); + } + .prototype-cards .card__project-action--danger:active:not(:disabled), .prototype-cards .card__project-action--red:active:not(:disabled) { + background: var(--red-dark); + } + .prototype-cards .card__project-action--danger.button--outline:hover:not(:disabled), .prototype-cards .card__project-action--red.button--outline:hover:not(:disabled) { + background: var(--red-light); + } + .prototype-cards .card__project-action--grey { + --button-color: var(--grey-button); + --button-hover: var(--gray); + --button-text: var(--black); + } + .prototype-cards .card__project-action--green { + --button-color: var(--green); + --button-hover: var(--green-dark); + } + .prototype-cards .card__project-action--gold { + --button-color: var(--gold); + --button-hover: var(--gold-dark); + } + .prototype-cards .card__project-action--white { + --button-color: var(--light-white); + --button-hover: var(--white); + --button-text: var(--black); + } + .prototype-cards .card__project-action--gradient { + background: var(--gradient); + } + .prototype-cards .card__project-action--no-border { + border-color: transparent; + } + .prototype-cards .card__project-action--medium { + min-width: min(157px, 100%); + } + .prototype-cards .card__project-action--big { + width: 100%; + } + .prototype-cards .card__project-action--extra-small { + padding-inline: 12px; + } + .prototype-cards .card__project-action--icon, .prototype-cards .card__project-action--text { + color: var(--ui-accent); + background: transparent; + border-color: transparent; + } + .prototype-cards .card__project-action--icon:hover:not(:disabled), .prototype-cards .card__project-action--icon:active:not(:disabled), .prototype-cards .card__project-action--text:hover:not(:disabled), .prototype-cards .card__project-action--text:active:not(:disabled) { + color: var(--accent-dark); + background: var(--accent-light); + border-color: transparent; + } + .prototype-cards .card__project-action--icon { + min-width: 44px; + padding: 10px; + } + .prototype-cards .card__project-action--text { + padding-inline: 12px; + } +} +@media screen and (max-width: 999px) and (max-width: 749px) { + .prototype-cards .card__project-action { + padding-inline: 16px; + } + .prototype-cards .card__project-action--icon { + padding: 9px; + } + .prototype-cards .card__project-action--text { + padding-inline: 12px; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + display: inline-flex; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + align-items: center; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + justify-content: center; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + padding: 4px 24px; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + color: var(--white); + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + background: var(--accent); + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + border: 0.5px solid transparent; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + border-radius: var(--rounded-xxl); + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action { + transition: all 0.2s; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card__project-action:hover { + background-color: var(--accent-medium); + box-shadow: -2px 3px 3px rgba(51, 51, 51, 0.2); + } +} +.prototype-cards .card__subscription-action { + position: absolute; + top: 6px; + right: 6px; + z-index: 3; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + color: var(--accent-dark); + cursor: pointer; + background: var(--light-white); + border: 0; + border-radius: var(--rounded-lg); +} +.prototype-cards .card__subscription-action:hover { + background: var(--accent-light); +} +.prototype-cards .card__subscription-action:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 2px; +} +.prototype-cards .card__photo { + position: absolute; + top: -35px; + left: 50%; + z-index: 2; + transform: translateX(-50%); +} +.prototype-cards .card__content { + position: relative; + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding-top: 30px; + overflow: hidden; + text-align: center; +} +.prototype-cards .card__content--empty { + padding-top: 60px; +} +.prototype-cards .card__head { + display: flex; + flex-direction: column; + flex-shrink: 0; + align-items: center; + margin-bottom: 8px; +} +.prototype-cards .card__name { + margin-bottom: 3px; + overflow: hidden; + color: var(--black); + text-overflow: ellipsis; + white-space: nowrap; +} +.prototype-cards .card__additional-info { + color: var(--accent); +} +.prototype-cards .card__description { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + width: 100px; + overflow: hidden; + color: var(--dark-grey); + text-align: center; + word-break: break-word; +} +.prototype-cards .card__description--project { + flex: 0 0 24px; + height: 24px; + min-height: 24px; +} +.prototype-cards .card__description.invite-description { + justify-content: center; +} +.prototype-cards .card__footer--project { + display: flex; + flex-shrink: 0; + align-items: flex-end; + justify-content: center; + margin-top: auto; +} +.prototype-cards .card__invite-actions { + display: flex; + flex-direction: column; + gap: 3px; + align-items: center; + justify-content: center; +} +.prototype-cards .card__user { + margin-bottom: 2px; + color: var(--grey-for-text); +} +.prototype-cards .card__user--invite { + color: var(--accent); +} +.prototype-cards .card__project { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: auto; + border-radius: var(--rounded-xl); +} +.prototype-cards .card__project--text { + color: var(--white); +} +.prototype-cards .card__empty { + position: relative; + padding: 35px 0 12px; + margin-top: 20px; + cursor: pointer; + background: transparent; + opacity: 1; + transition: opacity 0.2s ease; +} +.prototype-cards .card__empty:hover { + opacity: 0.7; +} +.prototype-cards .card__empty--image { + position: absolute; + top: 15%; + right: 20%; +} +.prototype-cards .card__empty--name { + display: flex; + justify-content: center; + width: 90%; + margin-left: 8px; + color: var(--accent); +} +.prototype-cards .card__empty--icon { + color: var(--accent); +} +.prototype-cards .card__skills { + display: flex; + flex-flow: wrap; + flex-grow: 1; + gap: 2px; + justify-content: center; + margin-bottom: 9px; +} +.prototype-cards .card__skill { + width: 100px; +} +.prototype-cards .card--project .card__content { + padding: 27px 12px 0; +} +.prototype-cards .card--project .card__head { + width: 100%; + margin-bottom: 2px; +} +.prototype-cards .card--project .card__head > div { + width: 100%; + min-width: 0; +} +.prototype-cards .card--project .card__name { + /* stylelint-disable-next-line value-no-vendor-prefix */ + display: -webkit-box; + max-height: 30px; + margin: 0; + font-size: var(--ui-font-small); + font-weight: 600; + line-height: 15px; + overflow-wrap: anywhere; + white-space: normal; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} +.prototype-cards .card--project .card__description { + flex: 0 0 52px; + width: 100%; + height: 52px; + min-height: 52px; +} +.prototype-cards .card--project .card__description p { + /* stylelint-disable-next-line value-no-vendor-prefix */ + display: -webkit-box; + overflow: hidden; + line-height: 13px; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; +} +@media screen and (max-width: 999px) { + .prototype-cards .card--project .card__description p { + font-size: var(--ui-font-caption); + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card--project .card__description p { + font-size: 10px; + } +} +.prototype-cards .card--project .card__project-action-label { + line-height: 12px; + white-space: nowrap; +} +@media screen and (max-width: 999px) { + .prototype-cards .card--project .card__project-action-label { + font-size: var(--ui-font-caption); + font-weight: 600; + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card--project .card__project-action-label { + font-size: 10px; + font-weight: 600; + } +} +.prototype-cards .card--project .card__footer, +.prototype-cards .card--project .card__info, +.prototype-cards .card--project .card__info--text { + width: 100%; +} +.prototype-cards .card--my-project .card__body { + padding-bottom: 8px; +} +.prototype-cards .card--my-project .card__content { + padding: 22px 10px 0; +} +.prototype-cards .card--my-project .card__context { + margin-bottom: 3px; +} +.prototype-cards .card--my-project .card__head { + margin-bottom: 1px; +} +.prototype-cards .card--my-project .card__name { + max-height: 28px; + line-height: 14px; + text-align: center; +} +@media screen and (max-width: 999px) { + .prototype-cards .card--my-project .card__name { + font-size: var(--ui-font-caption); + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card--my-project .card__name { + font-size: 11px; + } +} +.prototype-cards .card--my-project .card__description { + flex-basis: 24px; + height: 24px; + min-height: 24px; + color: var(--grey-for-text); +} +.prototype-cards .card--my-project .card__description p { + font-size: 9px; + line-height: 12px; + -webkit-line-clamp: 2; +} +.prototype-cards .card--my-project .card__description--with-program { + flex-basis: 12px; + height: 12px; + min-height: 12px; +} +.prototype-cards .card--my-project .card__description--with-program p { + -webkit-line-clamp: 1; +} +.prototype-cards .card--my-project .card__program-name { + width: 100%; + overflow: hidden; + font-size: 9px; + line-height: 12px; + color: var(--grey-for-text); + text-overflow: ellipsis; + white-space: nowrap; +} +.prototype-cards .card--my-project .card__footer { + flex-direction: column; + gap: 4px; + align-items: stretch; + width: 100%; +} +.prototype-cards .card--invite .card__body { + padding-bottom: 6px; +} +.prototype-cards .card--invite .card__content { + padding: 20px 10px 0; +} +.prototype-cards .card--invite .card__head { + width: 100%; + margin-bottom: 2px; +} +.prototype-cards .card--invite .card__head > div { + width: 100%; + min-width: 0; +} +.prototype-cards .card--invite .card__name { + /* stylelint-disable-next-line value-no-vendor-prefix */ + display: -webkit-box; + max-height: 30px; + overflow: hidden; + line-height: 15px; + overflow-wrap: anywhere; + white-space: normal; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} +.prototype-cards .card--invite .card__description { + flex: 0 0 auto; + width: 100%; +} +.prototype-cards .card--invite .card__user { + line-height: 13px; +} +@media screen and (max-width: 999px) { + .prototype-cards .card--invite .card__user { + font-size: var(--ui-font-caption); + } +} +@media screen and (min-width: 1000px) { + .prototype-cards .card--invite .card__user { + font-size: 10px; + } +} +.prototype-cards .card--invite .card__user--invite { + /* stylelint-disable-next-line value-no-vendor-prefix */ + display: -webkit-box; + overflow: hidden; + font-size: var(--ui-font-small); + font-weight: 600; + line-height: 15px; + overflow-wrap: anywhere; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} +.prototype-cards .card--invite .card__footer { + width: 100%; + margin-top: auto; +} +.prototype-cards .card--invite .card__invite-actions { + gap: 4px; + width: 100%; +} +.prototype-cards .card--invite .card__invite-actions app-button { + width: 100%; +} +.prototype-cards .card__access { + flex-shrink: 0; + padding-top: 4px; + text-align: center; + border-top: 1px solid var(--medium-grey-for-outline); +} +.prototype-cards .card__role { + font-size: 9px; + font-weight: 600; + line-height: 11px; + color: var(--black); +} +.prototype-cards .card__access-label { + font-size: 8px; + line-height: 10px; + color: var(--grey-for-text); +} +.prototype-cards .card__context { + display: inline-flex; + flex-shrink: 0; + gap: 5px; + align-items: center; + max-width: 100%; + height: 20px; + padding: 3px 8px; + margin-bottom: 6px; + font-size: 9px; + font-weight: 600; + line-height: 14px; + color: var(--dark-grey); + white-space: nowrap; + background: var(--light-gray); + border-radius: 11px; +} +.prototype-cards .card__status { + gap: 4px; + padding-inline: 5px; + color: var(--black); +} +.prototype-cards .card__status i { + display: flex; + flex-shrink: 0; +} +.prototype-cards .card__status::before { + flex: 0 0 6px; + width: 6px; + height: 6px; + content: ""; + background: currentcolor; + border-radius: 50%; +} +.prototype-cards .card__status--published { + color: var(--green-dark); + background: var(--green-light); +} +.prototype-cards .card__status--program::before { + display: none; +} +.prototype-cards .card__status--submitted { + color: var(--green-dark); + background: var(--green-light); +} +.prototype-cards .card__status--submitted::before { + display: none; +} +.prototype-cards .message-modal { + display: flex; + flex-direction: column; + align-items: center; + max-width: 402px; +} +.prototype-cards .message-modal__title { + margin: 18px 0; + color: var(--black); + text-align: center; +} +.prototype-cards .message-modal__text { + color: var(--dark-grey); + text-align: center; +} +.prototype-cards .message-modal__button { + margin-top: 18px; +} +.prototype-cards .unsubscribe-modal { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + text-align: center; +} +.prototype-cards .unsubscribe-modal h3 { + margin-bottom: 16px; + color: var(--black); +} +.prototype-cards .unsubscribe-modal__buttons { + display: flex; + gap: 12px; + margin-top: 16px; +} +@media screen and (max-width: 999px) and (max-width: 999px) { + .prototype-cards .source-host { + display: block; + width: min(100%, var(--ui-mobile-entity-card-width)); + min-width: 0; + } + .prototype-cards .card__body { + width: 100%; + height: auto; + min-height: 244px; + } + .prototype-cards .card__content { + padding-right: 16px; + padding-left: 16px; + overflow: visible; + } + .prototype-cards .card__head { + width: 100%; + } + .prototype-cards .card__name { + max-width: 100%; + overflow-wrap: anywhere; + white-space: normal; + } + .prototype-cards .card--project .card__name, + .prototype-cards .card--my-project .card__name { + display: block; + max-height: none; + font-size: var(--ui-font-body); + line-height: 20px; + -webkit-line-clamp: unset; + } + .prototype-cards .card--project .card__description, + .prototype-cards .card--my-project .card__description { + height: auto; + font-size: var(--ui-font-body); + line-height: 20px; + } + .prototype-cards .card__context { + height: auto; + min-height: 24px; + overflow-wrap: anywhere; + white-space: normal; + } + .prototype-cards .card__role, + .prototype-cards .card__access-label { + font-size: var(--ui-font-small); + line-height: 16px; + } + .prototype-cards .card__description { + flex-basis: auto; + width: 100%; + height: auto; + min-height: 24px; + } + .prototype-cards .card__project-action { + align-self: center; + width: auto; + min-height: 44px; + } + .prototype-cards .card--my-project .card__program-name { + font-size: var(--ui-font-small); + line-height: 16px; + } + .prototype-cards .card--project .card__description p, + .prototype-cards .card--my-project .card__description p { + font-size: var(--ui-font-small); + line-height: 18px; + } + .prototype-cards .card__subscription-action { + width: 44px; + height: 44px; + } +} +/** @format */ +.public-shell .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; + min-height: 32px; + padding: 0 16px; + font-size: 12px; +} +.public-shell .ui-button:hover:not(:disabled) { + text-decoration: none; + background: var(--button-hover); + border-color: var(--button-hover); +} +.public-shell .ui-button:active:not(:disabled) { + background: var(--accent-dark); +} +.public-shell .ui-button:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; +} +.public-shell .ui-button:disabled { + cursor: not-allowed; + opacity: var(--ui-disabled-opacity); +} +.public-shell .ui-button--secondary, .public-shell .ui-button--outline { + color: var(--button-color); + background: transparent; +} +.public-shell .ui-button--secondary:hover:not(:disabled), .public-shell .ui-button--secondary:active:not(:disabled), .public-shell .ui-button--outline:hover:not(:disabled), .public-shell .ui-button--outline:active:not(:disabled) { + color: var(--button-color); + background: var(--accent-light); +} +.public-shell .ui-button--danger, .public-shell .ui-button--red { + --button-color: var(--ui-danger); + --button-hover: var(--red-dark); +} +.public-shell .ui-button--danger:active:not(:disabled), .public-shell .ui-button--red:active:not(:disabled) { + background: var(--red-dark); +} +.public-shell .ui-button--danger.button--outline:hover:not(:disabled), .public-shell .ui-button--red.button--outline:hover:not(:disabled) { + background: var(--red-light); +} +.public-shell .ui-button--grey { + --button-color: var(--grey-button); + --button-hover: var(--gray); + --button-text: var(--black); +} +.public-shell .ui-button--green { + --button-color: var(--green); + --button-hover: var(--green-dark); +} +.public-shell .ui-button--gold { + --button-color: var(--gold); + --button-hover: var(--gold-dark); +} +.public-shell .ui-button--white { + --button-color: var(--light-white); + --button-hover: var(--white); + --button-text: var(--black); +} +.public-shell .ui-button--gradient { + background: var(--gradient); +} +.public-shell .ui-button--no-border { + border-color: transparent; +} +.public-shell .ui-button--medium { + min-width: min(157px, 100%); +} +.public-shell .ui-button--big { + width: 100%; +} +.public-shell .ui-button--extra-small { + padding-inline: 12px; +} +.public-shell .ui-button--icon, .public-shell .ui-button--text { + color: var(--ui-accent); + background: transparent; + border-color: transparent; +} +.public-shell .ui-button--icon:hover:not(:disabled), .public-shell .ui-button--icon:active:not(:disabled), .public-shell .ui-button--text:hover:not(:disabled), .public-shell .ui-button--text:active:not(:disabled) { + color: var(--accent-dark); + background: var(--accent-light); + border-color: transparent; +} +.public-shell .ui-button--icon { + min-width: 44px; + padding: 10px; +} +.public-shell .ui-button--text { + padding-inline: 12px; +} +@media (max-width: 749px) { + .public-shell .ui-button { + padding-inline: 16px; + } + .public-shell .ui-button--icon { + padding: 9px; + } + .public-shell .ui-button--text { + padding-inline: 12px; + } +} \ No newline at end of file diff --git a/docs/design/program-solutions-scope-20261025/prototype/public-v1/styles.css b/docs/design/program-solutions-scope-20261025/prototype/public-v1/styles.css new file mode 100644 index 000000000..96a296d1b --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/prototype/public-v1/styles.css @@ -0,0 +1,20 @@ +/* D03: локальный прототип. Геометрия и Mont из существующего Angular. */ +*,*::before,*::after{box-sizing:border-box} +body{margin:0;min-width:280px;color:var(--black);background:var(--white,#fff);font:14px/1.5 Mont,sans-serif} +button,input,select{font:inherit}button,a,input,select{touch-action:manipulation}button{cursor:pointer}a{color:var(--accent);text-decoration:none}button:disabled{cursor:default;opacity:.45} +button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,[tabindex="-1"]:focus-visible{outline:2px solid var(--accent);outline-offset:3px} +h1,h2,h3,p,ul,dl,dd{margin:0}ul{list-style:none;padding:0}.muted{color:var(--grey-for-text)} +.skip{position:fixed;top:0;left:16px;z-index:100;padding:12px;background:var(--white);transform:translateY(-150%)}.skip:focus{transform:none} +.prototype-bar{display:grid;gap:10px;padding:12px 24px;background:var(--light-gray);border-bottom:1px solid var(--medium-grey-for-outline);font-size:12px}.prototype-controls{display:flex;flex-wrap:wrap;gap:12px 24px}.prototype-controls label{display:flex;align-items:center;gap:8px}.prototype-controls select{max-width:260px;padding:5px 8px;border:1px solid var(--medium-grey-for-outline);border-radius:6px;background:var(--white)}#scope-note{color:var(--grey-for-text);font-size:11px} +.public-shell{max-width:1000px;margin:0 auto;padding:0 36px}.brand-header{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:32px 0;color:var(--grey-for-text);font-size:12px}.brand-header img{object-fit:contain;object-position:left}.program-heading{padding:20px 0 24px;border-bottom:1px solid var(--medium-grey-for-outline)}.program-heading h1{font-size:24px;font-weight:600;line-height:1.4;overflow-wrap:anywhere}.program-heading>p{font-size:12px;margin-bottom:8px} +#content{padding:28px 0 0;min-height:420px}.section-heading{display:flex;align-items:baseline;flex-wrap:wrap;gap:12px;margin-bottom:12px}.section-heading h2{font-size:18px;font-weight:400}.section-heading span{font-size:12px;color:var(--grey-for-text)}.section-note{font-size:12px;color:var(--grey-for-text);margin-bottom:24px;max-width:65ch} +.search{display:flex;gap:12px;max-width:520px;margin-bottom:24px}.search-field{display:flex;align-items:center;gap:10px;flex:1;min-width:0;height:41px;padding:0 14px;border:1px solid var(--medium-grey-for-outline);border-radius:24px}.search-field img{width:16px;height:16px}.search-field:focus-within{outline:2px solid var(--accent);outline-offset:2px}.search-field input{width:100%;min-width:0;padding:0;border:0;outline:none;background:transparent;font-size:12px}.search-field input:focus-visible{outline:none}.search .ui-button{min-height:41px} +.result-summary{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;font-size:12px;margin-bottom:24px}.text-button{border:0;background:none;padding:6px 0;color:var(--accent);font-size:12px}.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(156px,1fr));gap:24px 20px;align-items:stretch}.prototype-cards .card__body.published-card{display:flex;width:100%;height:auto;min-height:180px;padding:16px;border:1px solid var(--medium-grey-for-outline);border-radius:15px;background:var(--light-white);color:var(--black)}.published-card:hover{border-color:var(--accent)}.published-card h3{font-size:14px;font-weight:600;line-height:1.5;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}.card-captain{margin-top:16px;overflow-wrap:anywhere;font-size:12px;color:var(--grey-for-text)}.card-count{margin-top:8px;font-size:12px}.card-open{margin-top:auto;padding-top:16px;color:var(--accent);font-size:12px} +.pagination{display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;margin-top:28px;font-size:12px}.pagination-actions{display:flex;gap:8px}.public-footer{padding:48px 0 24px;color:var(--grey-for-text);font-size:11px} +.demo-notice{margin-top:24px;padding:12px 0;border-top:1px solid var(--medium-grey-for-outline);font-size:12px;color:var(--grey-for-text)} +.prototype-cards .card__body.published-card{height:100%} +.state{display:grid;justify-items:start;gap:12px;padding:28px 0;max-width:560px}.state h2{font-size:18px;font-weight:400}.state p{font-size:14px;line-height:1.6;color:var(--grey-for-text)}.state .ui-button{margin-top:4px}.skeleton{height:16px;max-width:100%;width:70%;background:var(--light-gray);border-radius:5px;margin-bottom:16px}.skeleton-card{height:180px;width:100%;border:1px solid var(--medium-grey-for-outline);border-radius:15px;background:var(--light-gray)}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0} +.prototype-solution .solution{max-width:none;padding:0;margin:0}.prototype-solution header{display:block;margin:24px 0}.prototype-solution header h2{font-size:24px;font-weight:600;line-height:1.4;margin-bottom:8px;overflow-wrap:anywhere}.prototype-solution header p{font-size:12px}.back-link{display:inline-block;font-size:12px;padding:6px 0}.prototype-solution nav{width:min(100%,432px)}.prototype-solution nav a{font-size:12px}.answer-list{display:grid;gap:24px}.answer h3{font-size:12px;font-weight:400;color:var(--grey-for-text);margin-bottom:8px}.answer p{font-size:14px;white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.6;max-width:75ch}.answer a{overflow-wrap:anywhere}.answer .file-link{display:inline-flex;align-items:center;gap:8px;font-size:12px;max-width:100%;padding:8px 0}.file-link img{width:16px;height:16px}.team-intro{font-size:12px;color:var(--grey-for-text);margin-bottom:12px}.team-row{display:grid;grid-template-columns:40px minmax(0,1fr) minmax(140px,.6fr);gap:16px;align-items:center;padding:16px 0;border-bottom:1px solid var(--medium-grey-for-outline);font-size:12px}.avatar{display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:var(--light-gray);color:var(--accent)}.member-name{font-size:14px;overflow-wrap:anywhere}.member-role{color:var(--grey-for-text);margin-top:4px}.specialization{overflow-wrap:anywhere} +@media(max-width:999px){.public-shell{padding:0 24px}.brand-header{padding:24px 0}.cards{grid-template-columns:repeat(auto-fill,minmax(156px,1fr))}} +@media(max-width:600px){.prototype-bar{padding:12px 16px}.prototype-controls{display:grid;gap:8px}.prototype-controls label{display:grid;grid-template-columns:75px minmax(0,1fr)}.prototype-controls select{width:100%;max-width:none}.public-shell{padding:0 16px}.brand-header{padding:24px 0 16px}.program-heading{padding:16px 0 20px}.program-heading h1{font-size:20px}#content{padding-top:24px}.cards{grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 12px}.prototype-cards .card__body.published-card{padding:12px;min-height:190px}.published-card h3{font-size:13px}.card-captain,.card-count,.card-open{font-size:11px}.search{gap:8px}.search .ui-button{padding:0 14px;min-width:70px}.section-note{margin-bottom:20px}.result-summary{margin-bottom:20px}.pagination{gap:12px}.pagination-actions{width:100%}.pagination-actions .ui-button{flex:1;min-height:40px}.prototype-solution header h2{font-size:20px}.prototype-solution nav{width:100%}.team-row{grid-template-columns:36px minmax(0,1fr);gap:12px}.avatar{width:36px;height:36px}.specialization{grid-column:2}.prototype-solution nav a{min-height:44px}.back-link,.text-button{min-height:40px;display:inline-flex;align-items:center}.answer .file-link{min-height:44px}.state{padding:20px 0}} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;animation:none!important;transition:none!important}} diff --git a/docs/design/program-solutions-scope-20261025/screenshots-clean/analytics-1440.png b/docs/design/program-solutions-scope-20261025/screenshots-clean/analytics-1440.png new file mode 100644 index 000000000..bbeeed925 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/screenshots-clean/analytics-1440.png differ diff --git a/docs/design/program-solutions-scope-20261025/screenshots-clean/analytics-390.png b/docs/design/program-solutions-scope-20261025/screenshots-clean/analytics-390.png new file mode 100644 index 000000000..dc4c19f22 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/screenshots-clean/analytics-390.png differ diff --git a/docs/design/program-solutions-scope-20261025/screenshots-clean/solutions-1440.png b/docs/design/program-solutions-scope-20261025/screenshots-clean/solutions-1440.png new file mode 100644 index 000000000..5e39b454c Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/screenshots-clean/solutions-1440.png differ diff --git a/docs/design/program-solutions-scope-20261025/screenshots-clean/solutions-390.png b/docs/design/program-solutions-scope-20261025/screenshots-clean/solutions-390.png new file mode 100644 index 000000000..477433099 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/screenshots-clean/solutions-390.png differ diff --git a/docs/design/program-solutions-scope-20261025/screenshots-historical-run/analytics-1440.png b/docs/design/program-solutions-scope-20261025/screenshots-historical-run/analytics-1440.png new file mode 100644 index 000000000..0bed8ce1d Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/screenshots-historical-run/analytics-1440.png differ diff --git a/docs/design/program-solutions-scope-20261025/screenshots-historical-run/analytics-390.png b/docs/design/program-solutions-scope-20261025/screenshots-historical-run/analytics-390.png new file mode 100644 index 000000000..e95ca0ef5 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/screenshots-historical-run/analytics-390.png differ diff --git a/docs/design/program-solutions-scope-20261025/screenshots-historical-run/solutions-1440.png b/docs/design/program-solutions-scope-20261025/screenshots-historical-run/solutions-1440.png new file mode 100644 index 000000000..eb34ae258 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/screenshots-historical-run/solutions-1440.png differ diff --git a/docs/design/program-solutions-scope-20261025/screenshots-historical-run/solutions-390.png b/docs/design/program-solutions-scope-20261025/screenshots-historical-run/solutions-390.png new file mode 100644 index 000000000..811e0cc70 Binary files /dev/null and b/docs/design/program-solutions-scope-20261025/screenshots-historical-run/solutions-390.png differ diff --git a/docs/design/program-solutions-scope-20261025/source-manifest.json b/docs/design/program-solutions-scope-20261025/source-manifest.json new file mode 100644 index 000000000..28b2c087c --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/source-manifest.json @@ -0,0 +1,45 @@ +{ + "scope": "Runnable D02 seven-tab navigation correction and copy-only D03 annotation; synthetic data", + "contract": "1.21.0", + "designApproval": "PENDING; existing D02/D03 requests, no approval inferred", + "sourcesRawSha256": { + "assets/logo.svg": "bcb52226e6536dbafa001c3c4acd947634617753093204fb3531385b4d0ffa1c", + "assets/Mont-Bold.woff2": "141fa4004c0413987f1638c827f2faba62b54bd1f618796c49a5cf8f5ac7c4bd", + "assets/Mont-Regular.woff2": "e01372b13985c09f2a168382ad044312db914801a595b4306abd7d686b0cc64b", + "assets/Mont-SemiBold.woff2": "ea0681c9068fb473d3606e012e76728eb44b2e04c276266894aeca5fc1934b52", + "assets/search.svg": "d0ab63e0b09e49973a8dc8936b40ba7e45467f81dd6f09bf6eed85f10150cce6", + "manager-v1/admin.js": "73ab1ab7846cde895b1336c8dd35be114aa707bd0de56789c61e49bf358c4bbc", + "manager-v1/analytics-reference.css": "5c22d9d89a5f7998e4db3331834906ead8b35e5c23d9a992e235ca7c91c0320e", + "manager-v1/app.js": "33d8d4b3c36ea2004a1b9dee2f4dde24f70c86cee06cee09a0b4636401cf7d8b", + "manager-v1/drilldown-reference.css": "6fdaf18d7cf3a7d0f95f61ed5498361634d86605e668547ffd8d2ab5a19cb868", + "manager-v1/global-reference.css": "ac2b519c5e7d05310332298736f9e5fd19b634d21d2766b00bb8361c74e76138", + "manager-v1/icons.js": "7bb1132b94391e3ab52123e6e528d38057d222722d4ec675711ba398dc12662f", + "manager-v1/index.html": "bb28ce87f3c982df53d593c9d7507d946bbc0d74dad2c47568af18dec9c81d20", + "manager-v1/media/Mont-Regular-5LPDNI7X.ttf": "0bb5e1336fbeb2c78e049cf3a21101289c7b6c906d7335a5859acedfb8aa5875", + "manager-v1/media/Mont-Regular-T6L2NNOO.woff": "29c81b198136bf8a929f52133314319cb6d8375a526da5c97197df1960ceebb3", + "manager-v1/media/Mont-Regular-UA5RCVYG.woff2": "e01372b13985c09f2a168382ad044312db914801a595b4306abd7d686b0cc64b", + "manager-v1/media/Mont-SemiBold-7XOUXTAI.woff2": "ea0681c9068fb473d3606e012e76728eb44b2e04c276266894aeca5fc1934b52", + "manager-v1/media/Mont-SemiBold-M6WLAC2K.ttf": "a0d30d71b1ed99f8ff6cd6e5a83b93dc832e4a00f6cf402f587351c1ba8f7dc2", + "manager-v1/media/Mont-SemiBold-WNUGA77E.woff": "1d01cc3f8fafbed1582b3182c40eafa607fb6670a179f23116d8eed2e8907481", + "manager-v1/results-reference.css": "a92e429596fc8ddd9c382f2c38ea2d486ad2c90aa84e8f9ce294a8214419e4f2", + "manager-v1/styles.css": "1d32f0bb7e80462359a864cd27f747684726ab9dd09b117f07d760cd5ce37418", + "public-v1/app.js": "bd5a92a4428ad22a24afe5fba3d3dc5006dceafea33793d1697b15bac9abeff7", + "public-v1/index.html": "8817a0e6b0a8e958d7db4b70225ca0f567803bcce57b3298b561c7da22ab50c7", + "public-v1/source-reference.css": "9cd80d17494d8e8c3ffe8211023c77def047e0ef1c1020ad820ed779eddbf545", + "public-v1/styles.css": "cab6bb39aa0d588d691ee39c351f7ab3b62f6796f74be60458abb52a34cb7693" + }, + "sourceFormattingNotApplied": true, + "historicalD02": "d59998e4091b57ee834d0bafb662d61bc6062874/docs/design/program-solutions-manager-d02-20261025", + "historical130ChecksDoNotVerifyNewNavigation": true, + "newNavigationQA": { + "layoutChecks": 48, + "productBodiesCompared": 12, + "unrelatedClicks": 12 + }, + "d03QA": { + "cases": 81, + "method": "jsdom; not browser or visual QA" + }, + "screenshots": "Existing navigation QA run; may show transient preservation notice. Clean screenshots recorded separately if generated.", + "packagingCorrection": "Copied existing stylesheet media dependencies and search.svg; no HTML/JS/CSS changes" +} diff --git a/docs/design/program-solutions-scope-20261025/verify-bundle.cjs b/docs/design/program-solutions-scope-20261025/verify-bundle.cjs new file mode 100644 index 000000000..3b509640d --- /dev/null +++ b/docs/design/program-solutions-scope-20261025/verify-bundle.cjs @@ -0,0 +1,142 @@ +/** @format */ + +const fs = require("node:fs"); +const path = require("node:path"); +const os = require("node:os"); +const assert = require("node:assert/strict"); +const crypto = require("node:crypto"); +const { chromium } = require(process.env.PLAYWRIGHT_MODULE || "playwright"); +const root = path.join(__dirname, "prototype"); +const output = + process.env.SCOPE_QA_OUTPUT || path.join(os.tmpdir(), `procollab-scope-qa-${Date.now()}`); +assert(!fs.existsSync(output), "Use a new output directory; historical evidence is immutable"); +fs.mkdirSync(output, { recursive: true }); +const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "source-manifest.json"), "utf8")); +const errors = []; +const deniedRequests = []; +const checks = []; +const screenshots = []; +const sha = file => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +for (const [file, expected] of Object.entries(manifest.sourcesRawSha256)) { + assert.equal(sha(path.join(root, file)), expected, file); +} +let browser; +async function run() { + browser = await chromium.launch({ + headless: true, + channel: process.env.BROWSER_CHANNEL || "msedge", + }); + const page = await browser.newPage({ + viewport: { width: 1440, height: 1000 }, + reducedMotion: "reduce", + }); + page.on("pageerror", error => errors.push(error.message)); + await page.route("**/*", async route => { + const request = route.request(); + const url = new URL(request.url()); + const relative = decodeURIComponent(url.pathname).replace(/^\/+/, ""); + const target = path.resolve(root, relative.endsWith("/") ? relative + "index.html" : relative); + if ( + url.origin !== "http://127.0.0.1:8769" || + request.method() !== "GET" || + ["fetch", "xhr"].includes(request.resourceType()) || + !target.startsWith(root + path.sep) || + !fs.existsSync(target) + ) { + deniedRequests.push(url.origin + url.pathname); + return route.abort(); + } + const mime = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".svg": "image/svg+xml", + ".woff2": "font/woff2", + }; + await route.fulfill({ body: fs.readFileSync(target), contentType: mime[path.extname(target)] }); + }); + for (const screen of ["solutions", "analytics"]) { + await page.goto(`http://127.0.0.1:8769/manager-v1/?screen=${screen}&format=kch&state=ready`); + await page.evaluate(() => document.fonts.ready); + assert.deepEqual( + (await page.locator("nav.section-tabs button").allTextContents()).map(label => label.trim()), + ["Участники", "Заявки", "Эксперты", "Результаты", "Аналитика", "Настройки", "Сертификаты"], + ); + assert.equal(await page.locator('nav.section-tabs [data-screen="evaluation"]').count(), 0); + assert.match( + await page.locator("body").evaluate(node => getComputedStyle(node).fontFamily), + /Mont/i, + ); + assert( + await page.evaluate(() => + [...document.fonts].some(font => font.family === "Mont" && font.status === "loaded"), + ), + ); + for (const width of [320, 390, 768, 1440]) { + await page.setViewportSize({ width, height: 1000 }); + assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); + checks.push({ screen, width, noOverflow: true }); + if ([390, 1440].includes(width)) { + const name = `${screen}-${width}.png`; + await page.screenshot({ path: path.join(output, name), fullPage: true }); + screenshots.push({ name, sha256: sha(path.join(output, name)) }); + } + } + } + await page.locator("#prototype-screen").selectOption("evaluation"); + assert.equal( + await page.locator('nav.section-tabs [aria-current="page"]').getAttribute("data-screen"), + "solutions", + ); + const body = await page.locator("#manager-main").innerHTML(); + const url = page.url(); + for (const section of ["participants", "experts", "settings", "certificates"]) { + await page.locator(`[data-preserved-section="${section}"]`).click(); + assert.equal(await page.locator("#manager-main").innerHTML(), body); + assert.equal(page.url(), url); + } + await page.goto("http://127.0.0.1:8769/public-v1/"); + for (const format of ["kch", "hack", "noncompetitive"]) { + await page.locator("#format").selectOption(format); + assert(await page.locator(".public-shell").isVisible()); + assert.match(await page.locator("#scope-note").innerText(), /API/); + for (const width of [320, 390, 1440]) { + await page.setViewportSize({ width, height: 1000 }); + assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); + checks.push({ screen: "public", format, width, noOverflow: true }); + } + } + assert.deepEqual(errors, []); + assert.deepEqual(deniedRequests, []); +} +run() + .then(() => { + fs.writeFileSync( + path.join(output, "bundle-qa.json"), + JSON.stringify( + { + status: "PASS", + sourceManifestSha256: sha(path.join(__dirname, "source-manifest.json")), + sourceFiles: Object.keys(manifest.sourcesRawSha256).length, + browser: "headless " + (process.env.BROWSER_CHANNEL || "msedge"), + scope: "Frozen synthetic bundle smoke; not HTTP/API/Angular or complete design QA", + checks, + errors, + deniedRequests, + screenshots, + }, + null, + 2, + ) + "\n", + ); + console.log( + JSON.stringify({ status: "PASS", checks: checks.length, output, errors, deniedRequests }), + ); + }) + .catch(error => { + console.error(error); + process.exitCode = 1; + }) + .finally(async () => { + if (browser) await browser.close(); + }); diff --git a/docs/product/program-solutions-fe01.md b/docs/product/program-solutions-fe01.md index e370aef41..84c8503aa 100644 --- a/docs/product/program-solutions-fe01.md +++ b/docs/product/program-solutions-fe01.md @@ -46,3 +46,15 @@ UI review: иерархия и два раздела согласованног Review: исправлена нормализация meta ошибок под те же camelCase моки. BLOCKER/MAJOR/MINOR в scope не осталось. Not checked: screen reader, четыре роли в браузере, настоящие загрузки/приглашения/сдача; последние относятся к FE03–FE08 и QA01. Негативный доступ проверен server-side в BE01 и в тестах ошибки UI. Отдельный коммит форматирования проверен по diff: только переносы строк, отступы и эквивалентная запись TypeScript union. Внедрение: сначала BE01 с миграциями и default legacy_project, затем FE01. Новый режим для реальных участников не включать до REL01. Merge и deployment выполняет пользователь. + +## REL01: отдельная приостановка создания, C00 1.13.0 + +Backend добавляет persisted program-scoped solution_creation_enabled=False и 409/blockedReason=solution_creation_disabled. Context и все 120 DTO schemas прежние, 45 операций/161 сценарий, старые 158 сохранены; новые mocks/types сгенерированы. Flag не передается клиентом. Существующие currentApplication/direct detail имеют приоритет; старый frontend уже не создает при canCreate=False. + +Изменен только текст существующего error state в direct new route и program entry через solutionCreationBlockedMessage. При technical pause показано «Создание новых решений временно приостановлено. Ранее созданные заявки доступны.». Для остальных причин прежний общий текст сохранен. Новых controls/layout/SCSS нет; D00 v3 не меняется, D02/D03 не реализуются этим этапом. + +6 новых tests: 3 actual interceptor wire сценария (disabled context/error/repeat), direct new route без POST/legacy fallback, direct existing read без проверки creation context, program entry с понятным сообщением. 55 focused PASS; полный npm run test:ci — 2760 tests/478 files и strict C00 types PASS. Development build/scoped ESLint/locked Prettier 3.8.3 по 1969 files PASS. Только три C00 файла (README/openapi/examples) общие с backend, byte-identical; validator остается в backend. + +Actual Edge/Angular/JWT/Django/PostgreSQL rehearsal КЧ/хакатона на программах 9/10: off direct route/entry → enable/create → disable → save/приглашения/accept/submit/frozen → re-enable/новая draft → disable. Mont, JS errors=0; 37 mutating HTTP requests (включая expected conflicts), это не zero writes. Отдельно 30 HTTP/C00 checks подтвердили итоговые snapshots/права/PDF bytes и unchanged прежний домен; сам verifier не менял данные. Основной путь mobile 390, состояния team/materials/readonly на 320/390/768/1440 и desktop материалы; paused states просмотрены визуально. Полный desktop/a11y/zoom/production CDN не заявляются; default remote avatars блокировались local harness. + +Spec/audit/evidence находятся в парном backend PR #822: docs/product/program-solutions-rel01.md и docs/qa/program-solutions-creation-switch-20261025/. Backend migration 0034 → frontend; flag остается False до отдельного утвержденного пилота, откат выключает только новое создание. Общие QA01/REL01 и полная feature не завершены; PR остается draft, merge/deploy не выполнялись. diff --git a/docs/product/program-solutions-fe02.md b/docs/product/program-solutions-fe02.md new file mode 100644 index 000000000..3732d0c0a --- /dev/null +++ b/docs/product/program-solutions-fe02.md @@ -0,0 +1,21 @@ + + +# 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 и отмена локальных изменений покрыты отдельными проверками. + +Этот блок не завершает полный выпуск новой сущности: редактор ответов, сдача, оценивание и общий список решений остаются следующими этапами. + +## Приемка конструктора 25.10.2026 + +Реальные manager/captain paths КЧ, хакатона и неконкурсной программы подтвердили настройку формы, пустой draft при одном капитане, 0/false/отсутствующие ответы и scope прав. Найден и исправлен initial dirty при template=null: сравнение больше не использует fallback PUT для состояния редактора. Первое применение шаблона не требует discard, реальные правки защищены прежним подтверждением. Красные 2/7 →9 component PASS, final test:ci2796/479+strict types/build/scoped lint PASS. В неконкурсном редакторе также убрана подсказка о будущей сдаче; API и макеты не менялись. + +[FORM01–FORM07, raw provenance и ограничения](../qa/program-solutions-form-settings-20261025/README.md). Суммарно86 C00 responses/22 JSON requests с финальным read-only copy check; baseline относится к моменту после fixture setup, не до него. Общие QA01/REL01 и первый выпуск остаются открытыми. 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/docs/product/program-solutions-fe04-fe05.md b/docs/product/program-solutions-fe04-fe05.md new file mode 100644 index 000000000..d5905ade6 --- /dev/null +++ b/docs/product/program-solutions-fe04-fe05.md @@ -0,0 +1,23 @@ + + +# 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. Эта поставка сама по себе не завершает всю новую сущность. + +## QA01: приглашения от 25.10.2026 + +На текущих backend 6db9050/runtime 2ad4743 и frontend 0415dddd проверены последнее место с разными получателями, роль 1–128/blank/129, недоступные участники, настоящий version conflict изменения роли с сохранением ввода и явным повтором, отзыв с подтверждением. Обе формы используют существующие согласованные controls; продуктовый код не менялся. Фактическая строка с ролью длиной 128 и финальная команда проверены на 320/390/768/1440 px. 120 ответов и 18 JSON запросов соответствуют C00; 20 HTTP событий race harness и неизменность прежней domain выборки подтверждены отдельно. [Отчет, артефакты и ограничения](../qa/program-solutions-invitations-20261025/README.md). Общие QA01/REL01 остаются открытыми. diff --git a/docs/product/program-solutions-fe06.md b/docs/product/program-solutions-fe06.md new file mode 100644 index 000000000..78c13f160 --- /dev/null +++ b/docs/product/program-solutions-fe06.md @@ -0,0 +1,21 @@ + + +# 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 во время следующих интеграций. + +## QA01 от 25.10.2026: ошибки, новая форма и потерянный ответ + +[NEG01–NEG08 и evidence](../qa/program-solutions-negative-20261025/README.md): реальные КЧ/хакатон, пустая заявка и обе вкладки/focus, accepted-only минимум, required PDF/repository, закрытие срока после подтверждения, сохранение после срока, потеря PATCH без потери ввода, actual manager PUT при несохраненных name/text/0/False/case, form revision conflict/rebase и новое required без изменения прежней сдачи. Domain verifiers: 42 настоящих HTTP/C00 checks с неизменным hash прежних данных, readonly snapshots, правами и actual PDF bytes. Частичные ранние попытки harness и before-fix FAIL сохранены отдельно. + +Найден и исправлен stale network alert после lost POST response: сервер уже коммитил сдачу, GET возвращал submitted, а footer продолжал показывать ошибку. Теперь принятый submitted GET переводит существующий UI в success notice и убирает старую командную ошибку, сохраняя локальные несохраненные данные. GET draft или failed GET не подтверждают успех; automatic POST retry не добавляется. API/C00/permissions/layout/SCSS не менялись. + +Усиленный interrupted-POST regression был красным; после fix 31 focused и полный test:ci 2763 tests/478 files + strict types PASS, development build/scoped ESLint PASS. На новых программах 13/14 повторен actual browser lost-response сценарий обеих форм: реальный commit → намеренный обрыв JSON response → GET → readonly/«Решение сдано.» без сетевой ошибки, по одному POST. Mont/0/False сохранены. Основной путь mobile, final responsive состояния отдельно; полный desktop/a11y/CDN не заявляется. Общий QA01/REL01 еще открыт. diff --git a/docs/product/program-solutions-fe07-api.md b/docs/product/program-solutions-fe07-api.md new file mode 100644 index 000000000..cbdffa2ef --- /dev/null +++ b/docs/product/program-solutions-fe07-api.md @@ -0,0 +1,15 @@ + + +# FE07: подключение контрактов экспертной оценки + +Исторический отчет 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 и результаты организатора. + +Изменения: 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/docs/product/program-solutions-fe07-ui.md b/docs/product/program-solutions-fe07-ui.md new file mode 100644 index 000000000..c9fdf1d51 --- /dev/null +++ b/docs/product/program-solutions-fe07-ui.md @@ -0,0 +1,118 @@ + + +# 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: подключить программные входы к нужному формату, административные интерфейсы и результаты/выгрузки, остальную аналитику и публикацию. Не объявлять всю 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 или схему БД. + +## Входы со страницы программы + +Before/after контракт записан в backend docs/product/program-solutions-be08.md до реализации и синхронизирован в README C00: read-only solution_storage_mode существующего GET /programs/{id}/, без изменения операций решения. Оба serializer варианта передают режим, не открывая unregistered эксперту описание/links. Program.solutionStorageMode optional для совместимости; только явный solution переключает путь. Верхняя кнопка «оценка решений» и ссылка экспертного виджета используют один routing helper, старый Project путь сохраняется. Геометрия/controls прежние, новая страница или redesign не создаются. + +ENT01–ENT03: проверяются API/readonly/privacy обеих ролей, оба реальных browser входа КЧ/хакатона/legacy и regression. Метрики ролевого виджета в solution mode пока читают Project и остаются незавершенной BE09; исправление маршрута не объявляется исправлением счетчиков. Actual evidence добавляется после проверки. + +ENT01 PASS по 30 backend tests detail/widget/routing и actual HTTP. ENT02 PASS: 17 widget tests, в том числе явный solution/legacy и отсутствие режима; обе ссылки используют общий helper. ENT03 PASS: реальные Angular/Django/PostgreSQL/JWT КЧ=3, хакатон=4 и legacy=5, header/widget → правильный список → возврат; 320/390/768/1440 без overflow/JS errors. Закрытое описание/links незарегистрированного эксперта не возвращаются. Angular development build PASS, ESLint измененных TS — 0 errors. Evidence: program-entry-browser-evidence.json в том же implementation каталоге. Environment restored к обычному dev. Не добавлены новые визуальные компоненты или макеты; source review не обнаружил оставшихся блокирующих замечаний routing scope. + +Финальная проверка ENT: полный npm run test:ci — 2601 tests в 472 файлах и strict C00 types PASS. Locked Prettier 3.8.3 — 1960 LF-normalized файлов без нарушений; финальный MD форматируется отдельно. Backend Black/Flake8, migration consistency (No changes detected), C00 74/34/59 и git diff --check PASS. Полный runtime старого Project оценивания не повторялся сверх ранее проверенного чтения/возврата CAB; публикация и метрики не включены в этот verdict. + +ENT04, найдено при review входов: верхний participant CTA должен открывать уже существующее решение после срока подачи. Для явного solution подпись «мое решение», действие всегда разрешает прочитать C00 context (кроме pending); currentApplication/canCreate определяет сервер. Старый Project submitted/deadline disable сохраняется только в legacy. Смешанный/противоречивый legacy current_application не должен обходить C00 выбор. Просмотр не дает права редактировать или сдавать. До проверки этого случая ENT04 не считается завершенным. + +ENT04 реализован и PASS: 38 targeted tests сервиса/виджета (включая открытие после срока со stale legacy reference и запрет создания), actual Angular/Django/PostgreSQL/JWT КЧ и хакатона при временно закрытых реальных локальных дедлайнах. В КЧ принятому участнику, в хакатоне капитану открывается неизменяемое сданное решение; новый зарегистрированный пользователь без заявки получает отказ на странице программы, POST создания отсутствует. 320/390/768/1440 без overflow/JS errors. Дедлайны и временно снятая локальная роль менеджера капитана хакатона восстановлены в finally; принятый состав/snapshot не менялись. Обычные dev URLs восстановлены. Evidence: participant-entry-browser-evidence.json. Предварительные helper failures были пропущенным приветствием, неправильным предположением об актере хакатона и role=dialog у старого unlabelled modal; исправлены fixture/селекторы, итоговый прогон PASS. Приветствие нового формата теперь описывает «мое решение» и доступ после срока; старый текст остается в legacy ветке. Заголовок ошибки относится к открытию решения, лишняя точка убрана. Это не миграция общего Modal или полный a11y аудит. + +Итог после ENT04: npm run test:ci — 2603 tests / 472 файла и strict C00 types PASS; повторный обычный Angular development build после review PASS. Измененные TS проходят ESLint (0 errors), финальные TS/HTML/MD проходят locked Prettier, git diff --check PASS. Source review: роли, actual context и old Project путь сохранены, новых blocking findings нет. Контракт не расширялся дополнительно: используется существующий C00 context. Это не финальная готовность всей FE07/FE08 или feature. + +## WID01–WID08: данные решения в существующем ролевом виджете + +До реализации зафиксированы AC в backend docs/product/program-solutions-be09.md и C00 1.7.0. Сохранены геометрия, порядок, controls, shared компоненты и SCSS существующего виджета. Изменены источник данных, подписи и реальные ссылки. Новый composed screen не добавляется; это не административный дизайн D02. + +ProgramRoleWidget теперь использует сгенерированный C00 union. Ветка solution получает SolutionRef, проекты сохраняют id/programLinkId. Presentation не превращает решение в Project: участник открывает /solutions/:solutionId, эксперт — solutions-rating, организатор видит solutions/уникальных людей без команды. Noncompetitive submitted и личные неприменимые counts остаются null, 0 не теряется. Приоритет завершенности и локальные часы/cleanup прежние; дополнительных HTTP или polling нет. 409 показывает «Данные заявки требуют проверки», без сырого DRF body; автоматическое создание отсутствует. + +- Checked by source: template и TS различают storageMode, currentApplication Project DTO не используется. Legacy queryParams сохранены, права остаются на backend. C00: прежние 74 schemas / 34 operations / 59 scenarios не изменены; 1.7.0 содержит 85 / 35 / 72, обе копии byte-identical. +- Checked by tests: 21 component + 8 real HTTP adapter/CamelcaseInterceptor tests PASS; старые helper/state tests проходят. Новый strict type test отклоняет projectId и participantProject в solution ветке. Полный npm run test:ci — 2615 tests / 473 files и strict C00 types PASS. +- Checked by build: обычный npm run build:social:dev PASS после local QA и восстановления dev URLs; scoped ESLint 0 errors. Locked Prettier 3.8.3 проверил 1961 LF-normalized файлов без нарушений; git diff --check PASS. SCSS не менялся. +- Checked in browser: настоящий Angular/Django/PostgreSQL/JWT трех ролей КЧ/хакатона/noncompetitive, свои ссылки, empty/case-not-selected, 0/null/completed, Mont, размеры 320/390/768/1440 без overflow. Controlled network/retry и 409 без raw detail, legacy эксперт и маршрут проектов PASS; 0 solution writes и JS errors. Скриншоты widget-\*.png и widget-browser-evidence.json находятся в design-artifacts/program-solutions-20261024/expert-v1/implementation. Первый прогон имел timeout на белой странице; повтор с диагностикой HTTP/status/JS прошел полностью, причина первого сбоя не подтверждена. Временная локальная роль менеджера восстановлена. +- Not checked: содержание основной manager analytics, новые administrative layouts, публикация и полный a11y audit; их готовность из успешного виджета не следует. + +Requirement audit WID08 PASS вместе с API WID01–WID07. Review: виджет сохраняет иерархию и плотность, одно основное действие, прежнюю группировку и tab order; отсутствуют локальные дубликаты controls или новые декоративные блоки. Длинное имя остается ellipsis с полным tooltip, роль/метрики не смешиваются с состояниями error/empty. Изменения адаптируют данные существующего согласованного UI. Blocking findings блока не обнаружены; вся FE07/FE08 и общий первый выпуск еще не готовы. + +## QA01: настоящий criteria refresh и отказ экспертной роли + +Проверены распределенное оценивание КЧ и открытое хакатона: actual manager PUT во время ввода → POST409/criteria_changed → GET/rebase с 0/False по ID/типу и копией старых 1000 → новый required/focus → один подтвержденный POST с 0/False/0/1000 → собственные счетчики и возврат q/case/status. После закрытия срока в подтверждении сохраненный 0 остается, локальная 2 доступна для копирования. Второй эксперт получает только null собственных значений, капитану экспертный frame закрыт. + +Найден реальный пропущенный код not_program_expert после отзыва экспертной роли при сохраненной роли организатора: POST403 оставлял старую форму. Код добавлен в существующее очищение frame/form/previous. Права, C00, layout и controls сохранены; один параметризованный regression был красным, после fix 9 component и final test:ci 2794/479 + strict types, development build/scoped lint PASS. [Before/after, 16 checkpoints, 52 C00 responses/11 requests, domain verifier и точная история частичных запусков](../qa/program-solutions-expert-revision-20261025/README.md). Ранний dynamic chunk error при HTTP200 сохранен, его причина не установлена; финальный actor check отдельно прошел без JS errors. Общая FE07/QA01/REL01 не объявляется завершенной. diff --git a/docs/product/program-solutions-fe08-api.md b/docs/product/program-solutions-fe08-api.md new file mode 100644 index 000000000..30cd03863 --- /dev/null +++ b/docs/product/program-solutions-fe08-api.md @@ -0,0 +1,101 @@ + + +# FE08: API общего кабинета и выгрузки результатов + +## C00 1.12.0: фильтр поля и XLSX найденных решений + +Before: manager list принимал search/case/scope/submission/pagination, analytics export — только kind. After: list добавляет fieldId/answerState=all/present/missing/not_applicable; export kind=solutions/submitted получает те же непагинационные фильтры. Backend применяет snapshot/current форму и существующий empty_value (0/false present, новое поле не missing у старой сдачи); JSON и четыре листа книги используют одну выборку, XLSX включает все найденные, submitted — дополнительное пересечение. Неизвестное/чужое поле, invalid pair/state/blank, фильтры program/scores и export pagination дают C00 400. Права/no-store прежние, публикация не затрагивается. + +Реализовано: общий SolutionManagerSolutionFilters и строгий SolutionAnalyticsExportQuery; старый вызов с любым kind без фильтров остается допустимым, filtered program/scores и limit/offset в XLSX запрещены типами. Adapter передает field_id/answer_state через существующий API/interceptor, unsafe field ID отклоняет до HTTP. Repository/use cases и общий Blob error decoder сохраняются. Компоненты/фасады UI/route/SCSS этим checkpoint не меняются; D02 и D03 еще требуют approval. + +Checked by tests: 20 новых wire tests + 61 manager/C00 + 5 прежних export = 86 PASS; все состояния ответа, пересечение прежних query, original XLSX Blob, C00 JSON в Blob и unsafe IDs проверены. Полный npm run test:ci: **2754 tests / 478 files**, strict C00 types PASS; типовые negative cases для program/scores/pagination/unknown answer state. C00 1.12.0: **120 schemas / 45 operations / 158 scenarios**, обе копии трех документов byte-identical, прежние schemas/responses/144 сценария сохранены. + +Checked by build/static: Angular development build PASS (прежние Sass warnings), scoped ESLint 0 errors, locked Prettier 3.8.3 по 1969 LF-normalized files PASS. Backend: 15 новых PostgreSQL tests, общий прогон 244 PASS; 143 настоящих HTTP/JWT/anonymous checks трех форматов и filtered XLSX, без domain mutations. Evidence: design-artifacts/program-solutions-20261024/expert-v1/implementation/field-filters-http-evidence.json. Роли/размеры/сохраненные данные не менялись. Новый browser UI, production/CDN/Excel пересчет и большой memory benchmark не проверялись; это API scope, первый выпуск не завершен. + +Статус первого блока: API-слой общего входа подготовлен. D00 v3 и D01 согласованы, первый UI FE07 реализован; новые manager/public UI ожидают D02/D03. Первый выпуск не завершен. + +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, включая общую пагинацию и совместимую старую проекцию. + +Следующий блок 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. Публикация неконкурсных сохраненных материалов внутри «Решений» программы подтверждена пользователем 25.10.2026; хранение остается ProgramSolution. Первый экспертный UI, кабинет, входы и ролевой виджет уже проверены в program-solutions-fe07-ui.md. Новые заглушки или автоматически включенный режим не добавлялись. + +## C00 1.8.0: агрегаты решений по кейсам, API + +Контракт CAS01–CAS07 зафиксирован в backend program-solutions-be09.md до реализации. Новый GET /programs/{program_id}/management/solution-cases/ не меняет прежний Project overview. GetSolutionCaseAnalyticsUseCase → port/repository/adapter получает только реальные program-scoped агрегаты, 0/null, current case order и withoutCase. Имя счетчика solutionsTotal; фиктивный Project DTO не вводится. Unsafe program IDs отклоняются до HTTP, C00 ошибки возвращаются обычным Result. + +Checked by source: те же ApiService/CamelcaseInterceptor и общий solutionRequestResult; UI пока не подключен. C00 1.8.0: 88 schemas / 36 operations / 80 scenarios; прежние 85/35/72 сохранены, обе копии byte-identical. Checked by tests: 12 real HTTP wire tests (все 8 сценариев + 4 unsafe IDs) PASS; полный npm run test:ci — 2627 tests / 474 files и strict C00 types PASS. Checked by build: Angular development build и scoped ESLint 0 errors PASS. Locked Prettier 3.8.3: 1963 LF-normalized files без нарушений; итоговый git diff --check PASS. + +Checked through running API: настоящий Django/PostgreSQL/JWT КЧ/хакатона/noncompetitive, реальные счетчики 1 решение и 2/1/2 зарегистрированных принятых участников, participant/expert 403, anonymous 401, noncompetitive null без submit. Backend 37 PostgreSQL tests (9 новых, legacy cases и solution widget) PASS. Review CAS07 PASS: query/DTO и ошибки сохраняются, прав клиент не добавляет. Browser нового продуктового экрана не проверялся: этот блок добавляет API-слой, без нового UI. Основная аналитика/attention, формы управления/результатов и остальные FE08 остаются в работе. + +## C00 1.9.0: основная manager analytics, API + +До runtime реализации записаны ANA01–ANA12 в backend program-solutions-be09.md и независимые схемы/сценарии. Добавлены use cases/port/repository/adapter для overview, actual assignment list/detail, scoped solution list и XLSX program/solutions/submitted/scores. Настоящий SolutionRef, assignmentId и Expert.user не преобразуются в Project DTO. Search/case/submission/scope/limit/offset передаются на проводе; unsafe IDs отклоняются до HTTP. Новые 47 wire tests проходят через настоящие ApiService/HttpClient/CamelcaseInterceptor и включают все 33 C00 сценария новых операций, 0/null, фильтры, четыре binary успеха и ошибки. + +Обработка JSON ошибки в Blob вынесена в общий solutionFileRequestResult и используется новой и прежней выгрузкой результатов. Прежние 5 export tests PASS; семантика ошибок/исходного Blob сохранена. Strict type tests запрещают projectId в assignment reference и legacy export kind, принимают noncompetitive null funnel. + +Checked by source: C00 1.9.0 — 110 schemas / 41 operations / 113 scenarios, старые 88/36/80 неизменны, обе копии byte-identical; UI не подключался и SCSS не менялся. Checked by tests: 52 targeted wire/export и полный npm run test:ci — 2674 tests / 475 files, strict C00 types PASS. Checked by build: обычный Angular development build, scoped ESLint 0 errors PASS. Locked Prettier 3.8.3: 1965 LF-normalized files PASS; git diff --check обоих repos PASS. + +Checked through running API: настоящий Django/PostgreSQL/JWT обеих форм/noncompetitive, сводка/cases/list/assignment detail, четыре настоящих XLSX и отрицательные роли PASS; 0 изменяющих запросов. Backend 114 PostgreSQL tests и 15 повторных после review PASS. API data больше не требуют Project проекции. Requirement audit ANA12 PASS. Новый экран/действия напоминаний/разрушающие команды/публикация и QA01/REL01 еще в работе; browser готовность из API-проверок не выводится. + +## C00 1.10 Напоминания экспертам о решениях + +До реализации зафиксированы REM01–REM09 в backend program-solutions-be09.md и C00 1.10.0: **116 schemas / 43 operations / 128 scenarios**. Прежние 110/41/113 неизменны. Добавлены manager-only POST reminder-preview/reminders под management/solution-analytics/attention/solutions-awaiting-evaluation. Selection использует настоящие solution IDs, selected/all/exclusions, поиск по snapshot name; signed token остается непрозрачным клиенту. Preview содержит null при отсутствии получателей, counts и первые 20 имен без email, confirm — created/skipped counts. Legacy endpoints/Project DTO не меняются. + +API layer: generated types/fixtures → SolutionRepositoryPort → repository/HTTP adapter → PreviewSolutionExpertReminderUseCase/ConfirmSolutionExpertReminderUseCase. Настоящий CamelcaseInterceptor преобразует excludedIds на проводе, не меняя значения IDs/token. Условная schema/strict types требуют ids для selected. Unsafe program ID отклоняется до HTTP. Ошибки C00 и 0/null сохраняются общим solutionRequestResult. + +Checked by tests: **21 новых wire tests** включают все 15 независимых C00 сценариев, selection/search/exclusions, opaque token, zero skipped/created и unsafe IDs; вместе с 47 analytics wire tests — 68 PASS. Полный `npm run test:ci`: **2695 tests / 476 files PASS** и strict C00 types PASS. Development build и scoped ESLint PASS; только прежние Sass deprecation warnings. C00 в обоих repos byte-identical, исходные схемы/операции/сценарии сохранены. + +Backend evidence: 22 новых tests, общий PostgreSQL прогон с предыдущими reminders/analytics — 144 PASS; Barrier подтверждает два одновременных confirm без дублей. Locmem tests проверяют recheck/отмену/фильтрацию и решение в action path. Реальный HTTP/JWT: 24 checks КЧ/хакатона/noncompetitive, empty preview/invalid confirm/roles, private headers и 0 domain mutations. Успешная доставка pending recipients в этом HTTP запуске не выполнялась. + +REM09: PASS для контракта и API layer. Review: реальных Project проекций, скрытой отправки или обхода server permissions нет. Новый manager экран/фасады/SCSS не менялись: [D02 макет](http://127.0.0.1:8769/manager-v1/) подготовлен и прошел prototype QA, но ожидает явного design approval. Not checked: runtime UI reminder interaction и production. Destructive actions, публичные решения, оставшиеся FE07/FE08 и QA01/REL01 впереди; API checkpoint не означает завершения feature. + +## C00 1.11 Публичные неконкурсные материалы + +До реализации зафиксированы PUB01–PUB10 в backend program-solutions-be08.md. C00 1.11.0 содержит **120 schemas / 45 operations / 144 scenarios**, прежние 116/43/128 структурно сохранены. Два anonymous GET `/programs/{program_id}/published-solutions/` и detail дают только опубликованные данные из ProgramSolution, без создания Project. На этом этапе поддерживается подтвержденная неконкурсная ветка; конкурсная публикация не объявляется завершенной. + +API layer: generated public page/card/materials/member → SolutionRepositoryPort → repository/adapter → GetPublishedSolutionsUseCase/GetPublishedSolutionUseCase. Query только limit/offset/search, настоящие program/solution IDs и immutable publishedAt. Team содержит accepted members/captain с минимальным User. Закрытые permissions/readiness/оценки/регистрация и контакты профиля не входят в DTO. Strict types не допускают закрытые submission filters и доступ к user.email. Наличие client DTO не заменяет server allowlist. + +Checked by tests: **25 новых wire tests** включают все 16 независимых C00 сценариев, anonymous запрос без ручной Authorization, 0/False/file/nullable pagination, errors/query и unsafe IDs до HTTP. Вместе с прежними 21 reminder tests — 46 PASS. Полный `npm run test:ci`: **2720 tests / 477 files PASS** и strict C00 types PASS. Development build и scoped ESLint PASS; только прежние Sass warnings. Backend: 19 новых publication tests, итоговая широкая PostgreSQL regression 229 PASS и 16 actual anonymous HTTP checks с восстановлением локальной фикстуры. + +PUB09 PASS для контракта/API layer; private program solution routes не становятся публичными. Этот checkpoint не добавляет Angular screen/route/layout/SCSS. Публичный интерфейс требует отдельного дизайна и явного approval, D02 организатора остается pending. Not checked: runtime public UI/browser и production. Migration backend 0033 (nullable marker/index) → API → frontend; реальные программы не переключались. Конкурсное правило по черновикам, остальные административные действия и QA01/REL01 остаются в работе. + +## C00 1.14 Первая сдача организатором + +Добавлен API слой существующего organizer/admin сценария поздней первой сдачи: POST management/solutions/actions/preview и apply. Настоящие Solution IDs/action=submit, nullable signed token и per-item issues/counts; apply передает previewToken без интерпретации. Schema/strict types требуют previewToken, отклоняют поле projectIds и действия detach/change_case; подпись проверяет сервер. Adapter отклоняет unsafe IDs/empty/oversized selection до HTTP. Все права, повторные проверки и атомарность находятся на сервере; клиент не добавляет force flag или автоматический повтор. + +C00 125 schemas/47 operations/185 scenarios; прежние 120/45/161 структурно неизменны, три contract файла byte-identical. Generated DTO/fixtures → repository port → repository/adapter → два use cases. 30 новых wire tests используют настоящий CamelcaseInterceptor, сохраняют scenario IDs, snake_case токена, null/0 и все C00 errors. Final npm run test:ci: 2793 tests/479 files и strict types PASS; development build/scoped ESLint PASS. Backend final 325 PostgreSQL tests (17 новых), два наблюдаемых confirm races и 42 actual HTTP/JWT checks двух форм PASS. + +На бэке общий finalize сохраняет captain deadline, ready validation и accepted-only snapshot, а confirmed manager command использует прежнее late-first-submit исключение. Сданные данные не перезаписываются. File locks захватываются до signed state fingerprint. Подробные spec/audit/evidence находятся в API repo docs/product/program-solutions-be08-submit-actions.md и docs/qa/program-solutions-submit-actions-20261025. + +Новых Angular screen/route/control/SCSS нет: D02 по-прежнему ожидает approval. Post-submission case/detach/removal, D03/конкурсная публикация и общий QA01/REL01 остаются открытыми. Это готовность API слоя, не всего административного интерфейса. + +## C00 1.15 Смена кейса черновика организатором + +Добавлены отдельные single-solution case preview/apply: DTO → port/repository/adapter → Preview/ApplySolutionCaseChangeUseCase. Точные caseName/opaque previewToken, настоящие program/solution IDs, nullable currentCase/token, field ID и MutationReceipt. Safe ID validation до HTTP; подпись, состояние и права проверяет сервер. Старые submit action schemas/scenarios не расширялись, Project ID не подменяет solution. + +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/реальных переключений. + +## 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/docs/qa/program-solutions-access-20261025/README.md b/docs/qa/program-solutions-access-20261025/README.md new file mode 100644 index 000000000..24999c5e6 --- /dev/null +++ b/docs/qa/program-solutions-access-20261025/README.md @@ -0,0 +1,27 @@ + + +# QA01 — AC06: права и смена аккаунта + +25.10.2026. **PASS только AC06; весь QA01 и первый выпуск остаются открытыми.** [Каноническое ТЗ](https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175), исходный текст AC06 сохранен. [Структурированное evidence](evidence.json). + +## Что проверено + +На настоящем Angular → Django/JWT → PostgreSQL18 участник и эксперт не могут изменить ответы или сдать решение за капитана. Посторонний получает404, аноним401. Для обеих форм проверены submitted участник (программы7/8, решения6/7), submitted эксперт (23/24,34/36), скрытый draft для эксперта и постороннего (30/31,45/47). Все36 прямых ответов и20 корректных тел запросов соответствуют C001.15.0. После запрещенных команд совпал полный hash15 выбранных доменных моделей, включая ответы, членов, приглашения, оценки, назначения, снимки сдачи и Project. + +В настоящем браузере капитан открывает решение, меняет название локально, переходит на вторую вкладку «Команда» и выходит. Обе формы возвращают страницу входа без unsaved confirm; прежний интерфейс исчезает. Вход другим пользователем по тому же адресу дает404 без редактора, команды и прежнего названия. Нет solution/program writes. Дополнительные5 browser GET responses проверены по C00: один baseline и четыре после изменения. Всего41 response; logout205 и JWT login находятся вне C00 и в это число не входят. Новых fixtures не создавали; существующие флаги/дедлайны и пользовательские материалы не меняли. Повторный domain audit после браузера совпал с исходным hash. + +## Изменение компонента и границы вывода + +`ProgramSolutionComponent` теперь самостоятельно очищает локальные данные, подтверждение сдачи и отменяет текущие загрузки/команды при `LoggedOut` или изменении/сбросе известного user ID. Ранее загруженный профиль не разрешает повторить запрос после logout. Новый аккаунт явно перечитывает заявку и права; локальные правки прежнего не переносятся. Обновление профиля того же пользователя и первоначальная загрузка профиля сохраняют правки. + +**Обычный выход уже был безопасен на baseline:** внешний `OfficeComponent` удаляет private router outlet при сбросе профиля. Не утверждаем, что браузер показывал чужие данные после обычного выхода. Добавленные изолированные component tests обнаружили отсутствие собственной очистки/отмены до уничтожения контейнером:7 failures/21 passes на старом коде,28 passes после изменения. Это укрепление жизненного цикла страницы; общий auth/token протокол не менялся. Смена cookie в другой вкладке без события или обновления профиля этим checkpoint не проверена. + +## Проверки и происхождение + +- Frontend base8a2dc896ca400db7b339ee643e1ea1d9789e19d8 + два измененных source files; точные SHA256 находятся в evidence. Полный `npm run test:ci`:2837 tests/480 files и strict C00 types; development build, scoped ESLint и locked Prettier3.8.3 PASS. HTML/SCSS/routes/C00 не менялись. +- Backend runtime d4709651e45b7d3411e19001ee4eada516afea48. Добавлены2 регрессионных access tests со сравнением состояния после каждого запрещенного PATCH/submit; вместе с answers/submission42 tests PASS на отдельной PostgreSQL test DB. Менялись только tests/docs; runtime backend совпадает с указанным commit. +- Baseline build и новый build скопированы в разные Temp каталоги. В каждом изменен один generated environment chunk только для localhost API/ws; source environments не менялись. Hash до/после и source hashes записаны в evidence. Реальные production CDN/zoom/a11y этим прогоном не проверялись; внешние ресурсы браузера блокировались. +- Две неуспешные ранние попытки baseline сохранены отдельно: сначала helper ожидал logout200 вместо корректного205; затем ожидал unsaved confirm, которого в реальном кабинете нет. После чтения Office template ожидание исправлено: baseline PASS. Эти ошибки harness не объявляются дефектами продукта и не скрыты последующим PASS. +- Raw wire/screenshots и hashes: `Z:/Документы/PROCOLLAB-dev/design-artifacts/program-solutions-20261024/qa01-access`. В репозитории только сводка; токены и пароли не сохранены. Основные локальные логи `procollab-access-*` в Temp. Новых миграций, переключения реальных программ, merge или deploy нет. + +Не закрывает AC18/19, manager/public screens, post-submission policy, competitive draft publication, retention/detach, остальные строки QA01 или REL01. BE10 остается отдельным этапом после первого выпуска. diff --git a/docs/qa/program-solutions-access-20261025/evidence.json b/docs/qa/program-solutions-access-20261025/evidence.json new file mode 100644 index 000000000..e9d0d586c --- /dev/null +++ b/docs/qa/program-solutions-access-20261025/evidence.json @@ -0,0 +1,216 @@ +{ + "status": "PASS", + "scope": "AC06 only; overall QA01 remains open", + "backendRuntime": "d4709651e45b7d3411e19001ee4eada516afea48", + "frontendBase": "8a2dc896ca400db7b339ee643e1ea1d9789e19d8", + "frontendSources": { + "projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts": "00357e0a65d86761170ddc889e9567cbe53573cbb0766d5cabeb3890cb727d5c", + "projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts": "fb1cb0c98611cc726ebe5a99e5db7bf8298307006ee8f3bf422b1c0960cd4f90" + }, + "backendTestSource": "6386411f23de7b6c715941732791d6813e3225900792046cabf7077abae29e33", + "c00": "1.15.0", + "contractChanged": false, + "backendTests": 42, + "frontendTests": 2837, + "frontendTestFiles": 480, + "routeTests": 28, + "newRouteTests": 10, + "beforeRouteFailed": 7, + "beforeRoutePassed": 21, + "afterRoutePassed": 28, + "wireResponses": 41, + "roleResponses": 36, + "validCommandRequests": 20, + "browserResponses": 5, + "browserChecks": [ + { + "program": 30, + "key": "case_championship", + "result": "PASS", + "ordinaryLogoutAlreadySafe": true, + "privateViewRemoved": true, + "dialogs": 0 + }, + { + "program": 30, + "key": "case_championship", + "result": "PASS", + "logoutReachedLogin": true, + "dialogs": 0, + "foreignAccountRead": 404, + "privateViewRemoved": true + }, + { + "program": 31, + "key": "hackathon", + "result": "PASS", + "logoutReachedLogin": true, + "dialogs": 0, + "foreignAccountRead": 404, + "privateViewRemoved": true + } + ], + "roleChecks": [ + { + "program": 7, + "solution": 6, + "kind": "submitted-member", + "deniedMember": 403, + "deniedOutsider": 404, + "anonymous": 401 + }, + { + "program": 8, + "solution": 7, + "kind": "submitted-member", + "deniedMember": 403, + "deniedOutsider": 404, + "anonymous": 401 + }, + { + "program": 23, + "solution": 34, + "kind": "submitted-expert", + "deniedExpert": 403 + }, + { + "program": 24, + "solution": 36, + "kind": "submitted-expert", + "deniedExpert": 403 + }, + { + "program": 30, + "solution": 45, + "kind": "draft", + "hiddenFromExpertAndOutsider": true + }, + { + "program": 31, + "solution": 47, + "kind": "draft", + "hiddenFromExpertAndOutsider": true + } + ], + "ordinaryLogoutAlreadySafeInBaseline": true, + "baselineBrowserCaveat": "Office outer profile condition already destroys the private view. Isolated component cleanup failures do not demonstrate an ordinary logout exposure.", + "attempts": [ + { + "file": "before-browser-attempt1.json", + "reason": "Harness expected logout200; actual correct endpoint status205." + }, + { + "file": "before-browser-attempt2.json", + "reason": "Harness expected a blocked route/confirm, but actual Office container already removed the view. Corrected baseline is PASS." + } + ], + "domainBefore": "f15e4e8a179c7a4e3e046a89d4b244973da0aa7f024c789b4b0964eb41423d9b", + "domainAfter": "f15e4e8a179c7a4e3e046a89d4b244973da0aa7f024c789b4b0964eb41423d9b", + "domainModels": [ + "partner_programs.PartnerProgram", + "partner_programs.PartnerProgramField", + "partner_programs.PartnerProgramUserProfile", + "partner_programs.ProgramSolution", + "partner_programs.ProgramSolutionAnswer", + "partner_programs.ProgramSolutionMember", + "partner_programs.SolutionInvitation", + "project_rates.SolutionScore", + "project_rates.SolutionExpertAssignment", + "project_rates.Criteria", + "users.Expert", + "files.UserFile", + "projects.Project", + "users.Expert_programs", + "partner_programs.PartnerProgram_managers" + ], + "domainMutations": 0, + "browserSolutionWrites": 0, + "browserProgramWrites": 0, + "newFixtures": 0, + "projectCount": 2, + "buildBefore": { + "source": "8a2dc896ca400db7b339ee643e1ea1d9789e19d8", + "target": "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-access-angular-before", + "changes": [ + { + "name": "chunk-CHX6T2XY.js", + "before": "4707c125db5485fa8b5eeb9e30e23c6f90759d9f2daf86b56a5a0b332da287dd", + "after": "d8252699c1d7f5986df9684f6f5b9be81943d068050e6537958be0680cac6380" + } + ] + }, + "buildAfter": { + "base": "8a2dc896ca400db7b339ee643e1ea1d9789e19d8", + "sources": { + "solution.component.ts": "00357e0a65d86761170ddc889e9567cbe53573cbb0766d5cabeb3890cb727d5c", + "solution.component.spec.ts": "fb1cb0c98611cc726ebe5a99e5db7bf8298307006ee8f3bf422b1c0960cd4f90" + }, + "target": "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-access-angular-after", + "changes": [ + { + "name": "chunk-HYHMJF3H.js", + "before": "4f800d7385e186870ceaa53d2c933b989bbc4ea2b9cab02c69d94ba8bb221eb8", + "after": "5b11f6d62c68f4bfb450ce7e1e054996865dafa61bd84756b141c067977919f2" + } + ] + }, + "rawArtifactLocation": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-access", + "artifacts": [ + { + "name": "after-browser.json", + "bytes": 15930, + "sha256": "6113fa915e2d6a57dac33f6114462c84cacd6b1e0db2938b7fc87e66dac551b1" + }, + { + "name": "after-case_championship-logout.png", + "bytes": 54300, + "sha256": "e9cdc0d09b2901138ca884daf0d9a18d4bf982567d7699bc21c566259ea20bbd" + }, + { + "name": "after-case_championship-other-account.png", + "bytes": 51220, + "sha256": "7ee7706ca6876980e9f561c4149c8b0d24bf4ee532670d21c45d56988ba2986b" + }, + { + "name": "after-hackathon-logout.png", + "bytes": 54302, + "sha256": "6ff65a38431d43f03cc15c25606d2cf078e48b726d0758bab8e20ff6af00fc6d" + }, + { + "name": "after-hackathon-other-account.png", + "bytes": 51215, + "sha256": "fefa5eebecfeb2bce08f13c70ec62b0a4e90775a18f84080c73d6a70db12cfe1" + }, + { + "name": "before-browser-attempt1.json", + "bytes": 136, + "sha256": "432361e07ce876913fd3473c0f96ed164c610f8c8ad49bc5451ca845207eb2ce" + }, + { + "name": "before-browser-attempt2.json", + "bytes": 136, + "sha256": "432361e07ce876913fd3473c0f96ed164c610f8c8ad49bc5451ca845207eb2ce" + }, + { + "name": "before-browser.json", + "bytes": 6595, + "sha256": "0dcab9e5bd88f849642d2266fb27c6b2c34312519e037347b9788d4f15126cc4" + }, + { + "name": "before-case_championship-logout.png", + "bytes": 54863, + "sha256": "410c8f969774c18b52ab43d6b597058716c70aee9235b0ad0dfbb51a7844c8bc" + }, + { + "name": "http-evidence.json", + "bytes": 1841, + "sha256": "0587059e5da872d8285326a2687a228a2f4ddf463af8db4e48f771f1518d20a8" + }, + { + "name": "http-wire.json", + "bytes": 76266, + "sha256": "a96f1815f3ab23cdb4c236e28a490f1f839fd1eca9a2a1b0d0501f70068a47d4" + } + ], + "domainAfterBrowser": "f15e4e8a179c7a4e3e046a89d4b244973da0aa7f024c789b4b0964eb41423d9b" +} diff --git a/docs/qa/program-solutions-archive-20261025/README.md b/docs/qa/program-solutions-archive-20261025/README.md new file mode 100644 index 000000000..d88e456dd --- /dev/null +++ b/docs/qa/program-solutions-archive-20261025/README.md @@ -0,0 +1,17 @@ + + +# BE08C1/FE API: архивное участие и сохраненные материалы + +25.10.2026. Frontend productbe380c5aad2f25522149999d709e7a75126a6397, backend product8bb1c28fb9d72ba7981b24e571f8b438deb9b0ab. C00 1.19.0:145 схем/55 операций/272 независимых сценария, три shared files byte-identical. [Контракт/retention scope](https://github.com/PROCOLLAB-github/api/blob/8bb1c28fb9d72ba7981b24e571f8b438deb9b0ab/docs/product/program-solutions-be08-archive.md). + +Generated metadata различает участие active/archived и сдачу draft/submitted. Archive snapshot использует captured_at и не имитирует submitted_at. Bulk preview/apply port/repository/adapter/use cases сохраняют ids/proof/effects/errors, проверяют safe/unique selection до сети. Existing list query поддерживает participation active/archived/all. History union дополнен archive и private archive_snapshot, старые case/member payloads сохранены. + +Финальный full suite после коррекции архивных mocks:2916 tests/483 files +strict C00 types PASS.103 case/member/archive/history wire tests:33/22/27/21; focused archive/history48 — подмножество. Development build/scoped ESLint/locked Prettier3.8.3 по2030 files PASS до отчета. Build выполнен до fixture-only correction; production source/types неизменны, fixture не используется вне tests. После коррекции readiness/pending/effects финальный full suite повторен. + +Настоящий backend HTTP:109 responses/53 request bodies по C00. КЧ40/решения55–56 сняты bulk после сдачи/закрытия срока; хакатон41/draft57 сохранен без submit, создана новая58 с тем же участником; неконкурсная42/draft59 снята после штатной публикации. Материалы/0/false/состав/оценки/регистрации и исходная сдача сохраняются. Старые строки23 моделей и4 case/member events совпали, новых Project=0.391 native solution и152 legacy tests/OK;0037 local-only. + +Новый organizer layout/routes/SCSS и навигация архива не добавлялись; backend permissions/read-only snapshot доступны существующим API потребителям. D02/D03 и соответствующие screens остаются в первом выпуске. Удаление регистрации/аудит — следующий BE08C2, полный QA01/REL01 открыт. После первого archive event backend/worker должен сохранять совместимость C00 1.19; archive schema/history не откатывается вниз. + +Evidence JSON имеет одинаковые значения с backend report. Git wire содержит11 redacted signed proofs; авторизация/пароли не записывались. HTTP resumed после активации только нового synthetic extra captain18, успешные submit/archive не повторялись. Backend/0037 → frontend; без merge/deploy/реальных переключений/переноса Project. + +Финальная locked format проверка с отчетом:2035 files, failures=[]. diff --git a/docs/qa/program-solutions-archive-20261025/contract-evidence.json b/docs/qa/program-solutions-archive-20261025/contract-evidence.json new file mode 100644 index 000000000..e0de57ce8 --- /dev/null +++ b/docs/qa/program-solutions-archive-20261025/contract-evidence.json @@ -0,0 +1,12 @@ +{ + "version": "1.19.0", + "schemas": 145, + "operations": 55, + "scenarios": 272, + "oldSchemasUnchanged": 132, + "oldOperationsUnchanged": 52, + "expandedOldOperation": "listSolutions/participation", + "oldScenariosUnchanged": 250, + "sharedByteIdentical": 3, + "originalQaRequirementRowsUnchanged": 26 +} diff --git a/docs/qa/program-solutions-archive-20261025/evidence.json b/docs/qa/program-solutions-archive-20261025/evidence.json new file mode 100644 index 000000000..5a3758713 --- /dev/null +++ b/docs/qa/program-solutions-archive-20261025/evidence.json @@ -0,0 +1,253 @@ +{ + "status": "PASS", + "scope": "BE08C1 archive/retention and frontend API, registration removal C2 and full QA01/REL01 still open", + "backendProduct": "8bb1c28fb9d72ba7981b24e571f8b438deb9b0ab", + "backendRuntime": "f85f610c579dd1595d085d95b07e02c1dbf96213", + "frontendProduct": "be380c5aad2f25522149999d709e7a75126a6397", + "frontendBuildRuntime": "2ac2e7438b1efe716c546f46612ef0a3f99c00fb", + "c00": { + "version": "1.19.0", + "schemas": 145, + "operations": 55, + "scenarios": 272, + "oldSchemasUnchanged": 132, + "oldOperationsUnchanged": 52, + "expandedOldOperation": "listSolutions/participation", + "oldScenariosUnchanged": 250, + "sharedByteIdentical": 3, + "originalQaRequirementRowsUnchanged": 26 + }, + "tests": { + "nativePostgresSolution": 391, + "newArchive": 20, + "newObservedRaces": 3, + "historicalMigrationSubset": 2, + "legacy": 152, + "legacyModules": 12, + "frontend": 2916, + "frontendFiles": 483, + "archiveHistoryWire": 48, + "allAdminWire": 103, + "newArchiveWire": 27, + "newHistoryWire": 2, + "strictTypes": true, + "developmentBuild": true, + "scopedEslint": true, + "black": true, + "flake8": true, + "djangoCheck": true, + "migrationConsistency": true, + "lockedFormat": { + "prettier": "3.8.3", + "locked": "3.8.3", + "checked": 2030, + "failures": [], + "filesWithReport": 2035, + "finalFailures": [] + } + }, + "http": { + "status": "PASS", + "c00": "1.19.0", + "validation": { + "responses": 109, + "validRequests": 53 + }, + "responses": 109, + "programs": [ + { + "id": 40, + "kind": "case", + "solutions": [ + { + "id": 55, + "change": 5 + }, + { + "id": 56, + "change": 6 + } + ], + "replacement": null, + "state": { + "evaluationSeeded": true, + "closed": true, + "applied": true, + "complete": true + } + }, + { + "id": 41, + "kind": "hack", + "solutions": [ + { + "id": 57, + "change": 7 + } + ], + "replacement": 58, + "state": { + "applied": true, + "formChanged": true, + "replacementComplete": true, + "complete": true + } + }, + { + "id": 42, + "kind": "public", + "solutions": [ + { + "id": 59, + "change": 8 + } + ], + "replacement": null, + "state": { + "published": true, + "applied": true, + "complete": true + } + } + ], + "oldModelsUnchanged": 23, + "oldRowsHash": "fc500f1028354a9e8697edd516762157f9907574c4dfc4467560c21d8d0cbe0a", + "originalAdminEventsPreserved": 4, + "newProjects": 0, + "newSolutions": 5, + "newArchiveEvents": 4, + "onlyFreshLocalPrograms": true, + "creationFlagsFinalFalse": true, + "noBinaryOrCdnOperations": true + }, + "migration": { + "status": "PASS", + "migration": "0037_solution_archive_retention", + "database": "local-procollab_ci", + "oldModelsUnchanged": 23, + "hashBefore": "fc500f1028354a9e8697edd516762157f9907574c4dfc4467560c21d8d0cbe0a", + "hashAfter": "fc500f1028354a9e8697edd516762157f9907574c4dfc4467560c21d8d0cbe0a", + "rowCounts": { + "partner_programs.PartnerProgram": 37, + "partner_programs.PartnerProgramField": 268, + "partner_programs.PartnerProgramUserProfile": 106, + "partner_programs.PartnerProgramProject": 3, + "partner_programs.PartnerProgramFieldValue": 2, + "partner_programs.ProgramSolution": 53, + "partner_programs.ProgramSolutionAnswer": 255, + "partner_programs.ProgramSolutionMember": 71, + "partner_programs.SolutionInvitation": 34, + "project_rates.SolutionScore": 32, + "project_rates.SolutionExpertAssignment": 20, + "project_rates.Criteria": 64, + "users.Expert": 7, + "files.UserFile": 8, + "projects.Project": 4, + "projects.Collaborator": 6, + "invites.Invite": 4, + "project_rates.ProjectScore": 3, + "project_rates.ProjectExpertAssignment": 1, + "industries.Industry": 1, + "users.Expert_programs": 25, + "partner_programs.PartnerProgram_managers": 41, + "partner_programs.SolutionAdministrativeChange": 4 + }, + "existingAdminEventsPreserved": 4, + "existingSolutionsArchivedOrDeleted": 0, + "existingProjectsMigratedOrDeleted": 0, + "onlyLocalSchema": true + }, + "proofRedactions": 11, + "sourceHashes": { + "docs/contracts/program-solutions-v1/validate_contract.py": "7168b95906c1f2759bf5b82c73a145569e80eae4babc20e4f8617d5b84eb1766", + "files/admin.py": "72595b9d8ba3188bc4892db263d126466e6e55bcb9f676c412dcb1ad40cdbe72", + "partner_programs/migrations/0037_solution_archive_retention.py": "ba3010e9df7bacfeaedc72309efe714a19508d4f8242404b3e74f23581e11377", + "partner_programs/services/solution_archive_actions.py": "70fb4ab7e3d8dd784e867ace121da93603e0d1eb90e63571d429db93093141d2", + "partner_programs/services/solution_attention.py": "df5fb2f786f0b2a80a2872c53ce7ceced635105f6d9a71099a972e53ad3b4b3c", + "partner_programs/services/solution_role_analytics.py": "25008ae0381d927655c29331be67424040c013dc829d7297547052e773946f3b", + "partner_programs/solution_admin.py": "19aba5273777fe7e2a73c367010546bd5ac2fc505f09e48a4ea324ed7ac2ef09", + "partner_programs/solution_api.py": "65aabdb05e7e633be98664bc13612148b2e4cb24e84baf69c642b377872d4fdb", + "partner_programs/solution_archive_action_api.py": "d7e4cd44a30a23df3b12419774e27e9c1bc56fe0b9d751da531c1571c26ff0fd", + "partner_programs/solution_file_signals.py": "af7ebe245acdb1216c03f7be597de2daa55067369a62d81fd0eff0a4d31ff919", + "partner_programs/solution_files.py": "9d76e69bbdec292d44a75fb21d0f2dbe182079b769be2a427c75bc262970c9b1", + "partner_programs/solution_history_api.py": "75cb49f32cf630628124706a1d8bc011e995c5ecfe39413943671646206af580", + "partner_programs/solution_invitation_api.py": "4b71780a995bf74299695ce4f055e5b5c753df941089640a2d46fa0cedc41dee", + "partner_programs/solution_invitations.py": "b15e67348f6367aa62fa8ad37c4498346a2fd8075cbbf20de647e74f11d1eba5", + "partner_programs/solution_lifecycle_signals.py": "6ac757b56cf4f24bcb0954796c6042492df5b215d40df54f5021ef28bc98425a", + "partner_programs/solution_models.py": "11ffd9375d4469764b3d916f11e56baff6623364d3cb25975abbb96d3b03d9df", + "partner_programs/solution_participants.py": "085578ea902f5c50f4f8d2dce589cee3bbe8caa7570f471239488d1f930c347d", + "partner_programs/solution_team.py": "5a1abd184887b82ff249be5f3ac102145cdabdf744aaeb5123f5bd2f34c7802b", + "partner_programs/tests/test_solution_archive_actions.py": "440cdad58fa782028f7a8935b11c40e4cce9572e8a6e7a9885eed4c87343ff67", + "partner_programs/tests/test_solution_creation_switch.py": "375f83779a2a24528755952e43488898023b6cf6c9fc4a09a9fc8270b146ed27", + "partner_programs/tests/test_solution_publication.py": "45a9cfb4836bb8e380c91f9d0f06ae1a869d6e89a0de8d9916549054cb15896f", + "partner_programs/urls.py": "774dd797fe53ef8556d8995c35fe8a60e3fccc9fb05f71a08ea9bfe2e0b24536", + "project_rates/solution_allocation.py": "e3f1948804bdd45b2022cba1f758f9a2893750458228d47f8adbbbb59cd9f075", + "project_rates/solution_assignments.py": "54bea9618b9f068b197177a4f2aa98e65706cf42c2c6ddf9d88c28db47809dbb", + "project_rates/solution_evaluation.py": "1128a5d0d5637d4d143b7deec023d98cdb9a22f88f2e4e8c474c7055401558d2", + "project_rates/solution_models.py": "18d1425aab284df38dda08e11df8b6f646db40fcb7b1507f6ef35f80ba970776" + }, + "rawArtifacts": { + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\contract-evidence.json": "441e86e07245f49183b50646b77bf0126793e5ba34c18385f6f11ba8915c6109", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\http-evidence.json": "7ddb3be60fffc409b3b2108ed127c2aff89706d6d0d3fbff64ffcc7f398f56f9", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\http-wire.json": "f96be14e1e5ac5f3a8d3689de8348c0fa865ee77bb88b95574d33c6cc888a935", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\migration-evidence.json": "532f5b61c6391e3ae2d5d0639f93fd4c4a58702fdc74fa5a94447fede6db6ca8", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-black.log": "bdff422390c0bebd756246d312c08c0b6286dc97ca3e8b713be43dcb5c44249b", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-build.log": "b8e4764a1288f597dfcb86bf3f62a9b031c30759c1ae03391371117ca44d5697", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-check.log": "f87ceb15410bde28ff826ce26228e0576558258fa64d145847921a593a61ee63", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-eslint.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-final-contract.log": "a205d39f53ef64f1ea64a2cf56ee600c34ab1602900bf3694c37e3d4598e1bc9", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-flake8.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-focused-verified.log": "9be1b0f6a522b3648fb447b6a84bf1baf892dab95ecc12d4b1759f063921b6f6", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-focused.log": "72e5d96b86f3907bf8a7184c5cd718fd9bcd73606412ab97d5cd1d07214c2809", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-format.log": "993ed0551f1efe9b448c1f2bd856b7ae89a46e16aab4722d2850c454aa80d758", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-frontend-suite-verified.log": "3dba7acde7c06d93df48bd35f7bdad5636baa48b07550322b7e8e6af0e09e8ad", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-frontend-suite.log": "d77435672f0990b17f5a9d7dfb90605022cbf090765afda87bd136baa88c10d7", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-historical-migration-tests.log": "1a9958c529fa8219907f608e464c7f65aea2255a9dfaa6c12515205f13566485", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-http-resumed.log": "f9bbe98459e17eece6057a4c420c5ada06eb23bf6ad8f5771d0c705a7d298c84", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-http.log": "be36657711cd16f88f91c9bb9255223ad259ed2299bf39a995ba2e363c06fc70", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-legacy.log": "b3be35dd6025ffe5a46058eaa2c2c92127d52863422911f3d67442eb5a60c8ec", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-migrate.log": "5424e3997f4cd934ecf7c883c0f41501cff8505da39d74600df0d8c745908f8b", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-migration-check.log": "93e6e2d5be3c054add80c0f19101608803f7ca8a7df1fa41f76bbb9a1f570f4a", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-preceding-general-ci.log": "fc594849b196dd11cf9c520cf1521cf37bc754217d634883dc9fab3b597b8d88", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-preceding-pg-ci.log": "f9237d37874bdf7d7311dbacf582fe264f5324611a3a6aa2fb1f38a54a80fcb8", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-preceding-tests.log": "2dc8ff14fbac7dc2b0644b17fc704d749329879dbc1c3367d477e5c73d7d0700", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-solution-suite-verified.log": "b397e31a34b5d3f7fb9988e723bce36bc8fd4d57bfc39c85edb4e801a8057ea5", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-solution-suite.log": "49afd06ab3b39e462bf67e3be700c2efdd09b920d04428835c546e8c7633899c", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-wire.log": "9d94349d4bc6cc1c2f16d08e4caff3c83fd8fd03a72e68c1aaf6fe4f1ab19ec4", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-solution-archive\\procollab-solution-archive-final-format.log": "efe9afb94da42b6f5eaf765b19394204a82e2ac57347ce641dd014b4feb5b554" + }, + "helpers": { + "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-solution-archive-contract.py": "2d89c33b0af3dc4abcdb592b63ad432cc4f6e763526ff9961f66d2c0d3717982", + "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-solution-archive-migrate.py": "f7659b22a53cf0d1ec92281dc70a31073442b9fe28ff4d5c04bf07e8b7e12b9a", + "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-solution-archive-http.py": "50dff3e90a9a2420efdba5194aeb0176bbb6588724784d7d9e236317e6818afa", + "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-solution-archive-package.py": "504060636589935259b65fd2d2afc49659980fd65fac7275d47cd80b807e2546" + }, + "precedingCi": { + "head": "b829e526df91718412174bfd3d272c163eca6c72", + "postgres": { + "id": 40202219920, + "status": "completed/success", + "lockingTests": 39, + "fullTests": 1759, + "skips": 0 + }, + "general": { + "id": 40202216975, + "status": "completed/success", + "fullTests": 1748, + "skipped": 64 + }, + "notProofForNewProduct": true + }, + "limitations": [ + "New UI/archive navigation still follows pending D02/D03 design gates", + "Registration removal and audit remain BE08C2; archive retains registrations", + "Full QA01/REL01 still open, BE10 separate", + "No merge/deploy/real-program switching or Project conversion", + "First native failures were a stale fixture assertion and two historical-schema tests using the current model; corrected, checks preserved", + "Actual HTTP resumed after activating only the new synthetic extra captain18; unexpected401 retained and validated, no repeated submit or confirmed archive", + "Final C00 mocks corrected pending/readiness/effect examples using actual archive state; backend runtime unchanged, final frontend suite repeated", + "Development build predates fixture-only correction; production source/types unchanged and fixtures have no non-test consumers", + "Own8877 API stopped after HTTP, old8769/8871/8876 retained" + ] +} diff --git a/docs/qa/program-solutions-archive-20261025/http-wire.json b/docs/qa/program-solutions-archive-20261025/http-wire.json new file mode 100644 index 000000000..6e6008bd4 --- /dev/null +++ b/docs/qa/program-solutions-archive-20261025/http-wire.json @@ -0,0 +1,9149 @@ +[ + { + "actor": "captain", + "method": "POST", + "path": "/programs/40/solutions/", + "status": 201, + "request": { + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "response": { + "id": 55, + "program_id": 40, + "version": 1, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:13:49.319268Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 280 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 281 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 282 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 283 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/40/solutions/55/", + "status": 200, + "request": null, + "response": { + "id": 55, + "program_id": 40, + "version": 1, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:13:49.319268Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 280 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 281 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 282 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 283 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/40/solutions/55/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 10, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 58, + "version": 1, + "program": { + "id": 40, + "name": "Архив решений case qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T15:13:49.623020Z", + "updated_at": "2026-10-25T15:13:49.623020Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/58/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 58, + "version": 2, + "program": { + "id": 40, + "name": "Архив решений case qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T15:13:49.623020Z", + "updated_at": "2026-10-25T15:13:49.709941Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/40/solutions/55/", + "status": 200, + "request": null, + "response": { + "id": 55, + "program_id": 40, + "version": 3, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:13:49.712967Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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": 280 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 281 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 282 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 283 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/40/solutions/55/invitations/", + "status": 201, + "request": { + "expected_solution_version": 3, + "recipient_user_id": 17, + "role": "Дизайнер" + }, + "response": { + "invitation": { + "id": 59, + "version": 1, + "program": { + "id": 40, + "name": "Архив решений case qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 17, + "first_name": "Rate", + "last_name": "User", + "avatar_url": null + }, + "role": "Дизайнер", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T15:13:49.867351Z", + "updated_at": "2026-10-25T15:13:49.867351Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 4 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/40/solutions/55/", + "status": 200, + "request": null, + "response": { + "id": 55, + "program_id": 40, + "version": 4, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:13:49.870368Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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": 280 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 281 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 282 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 283 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/40/solutions/55/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "answers": [ + { + "field_id": 280, + "value": "Город" + }, + { + "field_id": 281, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 282, + "value": 0 + }, + { + "field_id": 283, + "value": false + } + ] + }, + "response": { + "id": 55, + "program_id": 40, + "version": 5, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:13:50.055290Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 280, + "value": "Город" + }, + { + "field_id": 281, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 282, + "value": 0 + }, + { + "field_id": 283, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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 + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/40/solutions/55/submit/", + "status": 200, + "request": { + "expected_version": 5, + "expected_form_revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673" + }, + "response": { + "id": 55, + "program_id": 40, + "version": 6, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:13:50.167023Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 280, + "value": "Город" + }, + { + "field_id": 281, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 282, + "value": 0 + }, + { + "field_id": 283, + "value": false + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": "2026-10-25T15:13:50.163992Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "actor": "extra", + "method": "POST", + "path": "/programs/40/solutions/", + "status": 401, + "request": { + "name": "Работа extra case qa01arc-20261025-0674eb82" + }, + "response": { + "code": "authentication_required", + "detail": "{'detail': ErrorDetail(string='User is inactive', code='authentication_failed'), 'code': ErrorDetail(string='user_inactive', code='authentication_failed')}", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "extra", + "method": "POST", + "path": "/programs/40/solutions/", + "status": 201, + "request": { + "name": "Работа extra case qa01arc-20261025-0674eb82" + }, + "response": { + "id": 56, + "program_id": 40, + "version": 1, + "name": "Работа extra case qa01arc-20261025-0674eb82", + "captain_id": 18, + "created_at": "2026-10-25T15:15:58.958912Z", + "updated_at": "2026-10-25T15:15:58.958912Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 18, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:15:58.962976Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 280 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 281 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 282 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 283 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "extra", + "method": "GET", + "path": "/programs/40/solutions/56/", + "status": 200, + "request": null, + "response": { + "id": 56, + "program_id": 40, + "version": 1, + "name": "Работа extra case qa01arc-20261025-0674eb82", + "captain_id": 18, + "created_at": "2026-10-25T15:15:58.958912Z", + "updated_at": "2026-10-25T15:15:58.958912Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 18, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:15:58.962976Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 280 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 281 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 282 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 283 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/40/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [55, 56] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "solution_version": 6, + "is_submitted": true, + "is_archived": false, + "score_count": 2, + "removed_assignments_count": 2, + "revoked_invitation_count": 1, + "errors": [] + }, + { + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 56, + "name": "Работа extra case qa01arc-20261025-0674eb82" + }, + "solution_version": 1, + "is_submitted": false, + "is_archived": false, + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [] + } + ], + "score_count": 2, + "removed_assignments_count": 2, + "revoked_invitation_count": 1, + "can_apply": true, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/40/solutions/55/", + "status": 200, + "request": null, + "response": { + "id": 55, + "program_id": 40, + "version": 6, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:13:50.167023Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ], + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "program_id": 40, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 280 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 281 + }, + { + "value": 0, + "field_id": 282 + }, + { + "value": false, + "field_id": 283 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T15:15:59.213094Z", + "submitted_at": "2026-10-25T15:13:50.163992Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "actor": "extra", + "method": "GET", + "path": "/programs/40/solutions/56/", + "status": 200, + "request": null, + "response": { + "id": 56, + "program_id": 40, + "version": 1, + "name": "Работа extra case qa01arc-20261025-0674eb82", + "captain_id": 18, + "created_at": "2026-10-25T15:15:58.958912Z", + "updated_at": "2026-10-25T15:15:58.958912Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 18, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:15:58.962976Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 40, + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": false, + "deadline": "2026-10-24T15:15:59.213094Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 280 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 281 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 282 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 283 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/40/management/solutions/archive/apply/", + "status": 409, + "request": { + "ids": [55, 56], + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/40/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [55, 56] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "member", + "method": "POST", + "path": "/programs/40/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [55, 56] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "expert", + "method": "POST", + "path": "/programs/40/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [55, 56] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": null, + "method": "POST", + "path": "/programs/40/management/solutions/archive/preview/", + "status": 401, + "request": { + "ids": [55, 56] + }, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/40/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [56, 55] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "solution_version": 6, + "is_submitted": true, + "is_archived": false, + "score_count": 2, + "removed_assignments_count": 2, + "revoked_invitation_count": 1, + "errors": [] + }, + { + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 56, + "name": "Работа extra case qa01arc-20261025-0674eb82" + }, + "solution_version": 1, + "is_submitted": false, + "is_archived": false, + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [] + } + ], + "score_count": 2, + "removed_assignments_count": 2, + "revoked_invitation_count": 1, + "can_apply": true, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/40/management/solutions/archive/apply/", + "status": 200, + "request": { + "ids": [56, 55], + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "changed_count": 2, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "solution_version": 7, + "archived_at": "2026-10-25T15:15:59.971534Z" + }, + { + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 56, + "name": "Работа extra case qa01arc-20261025-0674eb82" + }, + "solution_version": 2, + "archived_at": "2026-10-25T15:15:59.971534Z" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/40/management/solutions/archive/apply/", + "status": 409, + "request": { + "ids": [56, 55], + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/40/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [55, 56] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "solution_version": 7, + "is_submitted": true, + "is_archived": true, + "score_count": 2, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [ + { + "path": "ids", + "code": "solution_archived", + "detail": "Решение уже снято с участия.", + "field_id": null + } + ] + }, + { + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 56, + "name": "Работа extra case qa01arc-20261025-0674eb82" + }, + "solution_version": 2, + "is_submitted": false, + "is_archived": true, + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [ + { + "path": "ids", + "code": "solution_archived", + "detail": "Решение уже снято с участия.", + "field_id": null + } + ] + } + ], + "score_count": 2, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "can_apply": false, + "preview_token": null + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/40/solutions/55/", + "status": 200, + "request": null, + "response": { + "id": 55, + "program_id": 40, + "version": 7, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:15:59.971534Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ], + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "program_id": 40, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 280 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 281 + }, + { + "value": 0, + "field_id": 282 + }, + { + "value": false, + "field_id": 283 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T15:15:59.213094Z", + "submitted_at": "2026-10-25T15:13:50.163992Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/40/solutions/55/", + "status": 200, + "request": null, + "response": { + "id": 55, + "program_id": 40, + "version": 7, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:13:49.319268Z", + "updated_at": "2026-10-25T15:15:59.971534Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ], + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "program_id": 40, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 280 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 281 + }, + { + "value": 0, + "field_id": 282 + }, + { + "value": false, + "field_id": 283 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T15:15:59.213094Z", + "submitted_at": "2026-10-25T15:13:50.163992Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/solution-invitations/58/", + "status": 200, + "request": null, + "response": { + "id": 58, + "version": 2, + "program": { + "id": 40, + "name": "Архив решений case qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T15:13:49.623020Z", + "updated_at": "2026-10-25T15:13:49.709941Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/40/management/solutions/55/history/", + "status": 200, + "request": null, + "response": { + "count": 1, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 5, + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T15:15:59.985728Z", + "before_version": 6, + "after_version": 7, + "removed_assignments_count": 2, + "score_count": 2, + "was_submitted": true, + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached", + "revoked_invitation_count": 1 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/40/management/solutions/55/history/5/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 5, + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 55, + "name": "Работа captain case qa01arc-20261025-0674eb82" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T15:15:59.985728Z", + "before_version": 6, + "after_version": 7, + "removed_assignments_count": 2, + "score_count": 2, + "was_submitted": true, + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached", + "revoked_invitation_count": 1 + }, + "before_snapshot": { + "form": { + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ], + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "program_id": 40, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "answers": [ + { + "value": "Город", + "field_id": 280 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 281 + }, + { + "value": 0, + "field_id": 282 + }, + { + "value": false, + "field_id": 283 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 9, + "submitted_at": "2026-10-25T15:13:50.163992Z" + }, + "after_snapshot": { + "form": { + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ], + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "program_id": 40, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "answers": [ + { + "value": "Город", + "field_id": 280 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 281 + }, + { + "value": 0, + "field_id": 282 + }, + { + "value": false, + "field_id": 283 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 9, + "submitted_at": "2026-10-25T15:13:50.163992Z" + }, + "archive_snapshot": { + "form": { + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ], + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "program_id": 40, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "answers": [ + { + "value": "Город", + "field_id": 280 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 281 + }, + { + "value": 0, + "field_id": 282 + }, + { + "value": false, + "field_id": 283 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:13:49.324074Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:13:49.705223Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 9, + "captured_at": "2026-10-25T15:15:59.971534Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/40/management/solutions/55/history/", + "status": 403, + "request": null, + "response": { + "code": "permission_denied", + "detail": "История доступна организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/40/solutions/55/", + "status": 409, + "request": { + "expected_version": 7, + "expected_form_revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "name": "Недоступная правка" + }, + "response": { + "code": "solution_archived", + "detail": "Решение снято с участия. Материалы доступны для просмотра.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "extra", + "method": "GET", + "path": "/programs/40/solutions/56/", + "status": 200, + "request": null, + "response": { + "id": 56, + "program_id": 40, + "version": 2, + "name": "Работа extra case qa01arc-20261025-0674eb82", + "captain_id": 18, + "created_at": "2026-10-25T15:15:58.958912Z", + "updated_at": "2026-10-25T15:15:59.971534Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 18, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T15:15:58.962976Z", + "is_captain": true, + "specialization": null + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ], + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "program_id": 40, + "is_snapshot": true, + "is_configured": true + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": false, + "deadline": "2026-10-24T15:15:59.213094Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/40/management/solutions/56/history/", + "status": 200, + "request": null, + "response": { + "count": 1, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 6, + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 56, + "name": "Работа extra case qa01arc-20261025-0674eb82" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T15:15:59.996926Z", + "before_version": 1, + "after_version": 2, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached", + "revoked_invitation_count": 0 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/40/management/solutions/56/history/6/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 6, + "application": { + "kind": "solution", + "program_id": 40, + "solution_id": 56, + "name": "Работа extra case qa01arc-20261025-0674eb82" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T15:15:59.996926Z", + "before_version": 1, + "after_version": 2, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached", + "revoked_invitation_count": 0 + }, + "before_snapshot": null, + "after_snapshot": null, + "archive_snapshot": { + "form": { + "fields": [ + { + "id": 280, + "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": 281, + "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": 282, + "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": 283, + "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 + } + } + ], + "revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "template": "case_championship", + "program_id": 40, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа extra case qa01arc-20261025-0674eb82", + "answers": [], + "members": [ + { + "role": null, + "user": { + "id": 18, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T15:15:58.962976Z", + "is_captain": true, + "specialization": null + } + ], + "captain_id": 18, + "captured_at": "2026-10-25T15:15:59.971534Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "extra", + "method": "GET", + "path": "/programs/40/management/solutions/56/history/", + "status": 403, + "request": null, + "response": { + "code": "permission_denied", + "detail": "История доступна организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "extra", + "method": "PATCH", + "path": "/programs/40/solutions/56/", + "status": 409, + "request": { + "expected_version": 2, + "expected_form_revision": "d3bc34b553e60e98aa4a0525fbf46f412a3626cec43b6962e920110351587673", + "name": "Недоступная правка" + }, + "response": { + "code": "solution_archived", + "detail": "Решение снято с участия. Материалы доступны для просмотра.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/40/solutions/", + "status": 200, + "request": null, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/40/solutions/?participation=archived", + "status": 200, + "request": null, + "response": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": 55, + "program_id": 40, + "name": "Работа captain case qa01arc-20261025-0674eb82", + "captain": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "accepted_count": 2, + "case_value": "Город", + "status": "submitted", + "submitted_at": "2026-10-25T15:13:50.163992Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached" + } + }, + { + "id": 56, + "program_id": 40, + "name": "Работа extra case qa01arc-20261025-0674eb82", + "captain": { + "id": 18, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "accepted_count": 1, + "case_value": null, + "status": "draft", + "submitted_at": null, + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:15:59.971534Z", + "reason": "detached" + } + } + ] + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/40/solution-context/", + "status": 200, + "request": null, + "response": { + "program_id": 40, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": false, + "submission_deadline": "2026-10-24T15:15:59.213094Z", + "can_create": false, + "blocked_reason": "solution_creation_disabled", + "current_application": null + }, + "cacheControl": null + }, + { + "actor": "expert", + "method": "GET", + "path": "/programs/40/solutions/55/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "expert", + "method": "GET", + "path": "/programs/40/solutions/55/evaluation/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/41/solutions/", + "status": 201, + "request": { + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "response": { + "id": 57, + "program_id": 41, + "version": 1, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:01.789735Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T15:13:49.124223Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 284 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 285 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 286 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 287 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 288 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/57/", + "status": 200, + "request": null, + "response": { + "id": 57, + "program_id": 41, + "version": 1, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:01.789735Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T15:13:49.124223Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 284 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 285 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 286 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 287 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 288 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/41/solutions/57/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 10, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 60, + "version": 1, + "program": { + "id": 41, + "name": "Архив решений hack qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T15:16:02.016892Z", + "updated_at": "2026-10-25T15:16:02.016892Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/60/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 60, + "version": 2, + "program": { + "id": 41, + "name": "Архив решений hack qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T15:16:02.016892Z", + "updated_at": "2026-10-25T15:16:02.176162Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/57/", + "status": 200, + "request": null, + "response": { + "id": 57, + "program_id": 41, + "version": 3, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:02.179179Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:02.169575Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T15:13:49.124223Z", + "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": 284 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 285 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 286 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 287 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 288 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/41/solutions/57/invitations/", + "status": 201, + "request": { + "expected_solution_version": 3, + "recipient_user_id": 17, + "role": "Дизайнер" + }, + "response": { + "invitation": { + "id": 61, + "version": 1, + "program": { + "id": 41, + "name": "Архив решений hack qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 17, + "first_name": "Rate", + "last_name": "User", + "avatar_url": null + }, + "role": "Дизайнер", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T15:16:02.389535Z", + "updated_at": "2026-10-25T15:16:02.389535Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 4 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/57/", + "status": 200, + "request": null, + "response": { + "id": 57, + "program_id": 41, + "version": 4, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:02.393558Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:02.169575Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T15:13:49.124223Z", + "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": 284 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 285 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 286 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 287 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 288 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/41/solutions/57/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "answers": [ + { + "field_id": 284, + "value": "Город" + }, + { + "field_id": 285, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 286, + "value": 0 + }, + { + "field_id": 287, + "value": false + }, + { + "field_id": 288, + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82" + } + ] + }, + "response": { + "id": 57, + "program_id": 41, + "version": 5, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:02.618573Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:02.169575Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 284, + "value": "Город" + }, + { + "field_id": 285, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 286, + "value": 0 + }, + { + "field_id": 287, + "value": false + }, + { + "field_id": 288, + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82" + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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 + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/57/", + "status": 200, + "request": null, + "response": { + "id": 57, + "program_id": 41, + "version": 5, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:02.618573Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:02.169575Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 284, + "value": "Город" + }, + { + "field_id": 285, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 286, + "value": 0 + }, + { + "field_id": 287, + "value": false + }, + { + "field_id": 288, + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82" + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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 + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/41/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [57] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "member", + "method": "POST", + "path": "/programs/41/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [57] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "expert", + "method": "POST", + "path": "/programs/41/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [57] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": null, + "method": "POST", + "path": "/programs/41/management/solutions/archive/preview/", + "status": 401, + "request": { + "ids": [57] + }, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/41/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [57] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "solution_version": 5, + "is_submitted": false, + "is_archived": false, + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "errors": [] + } + ], + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "can_apply": true, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/41/management/solutions/archive/apply/", + "status": 200, + "request": { + "ids": [57], + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "changed_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "solution_version": 6, + "archived_at": "2026-10-25T15:16:03.341730Z" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/41/management/solutions/archive/apply/", + "status": 409, + "request": { + "ids": [57], + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/41/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [57] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "solution_version": 6, + "is_submitted": false, + "is_archived": true, + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [ + { + "path": "ids", + "code": "solution_archived", + "detail": "Решение уже снято с участия.", + "field_id": null + } + ] + } + ], + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "can_apply": false, + "preview_token": null + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/57/", + "status": 200, + "request": null, + "response": { + "id": 57, + "program_id": 41, + "version": 6, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:03.341730Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:16:03.341730Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:16:02.169575Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "program_id": 41, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 284 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 285 + }, + { + "value": 0, + "field_id": 286 + }, + { + "value": false, + "field_id": 287 + }, + { + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82", + "field_id": 288 + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/41/solutions/57/", + "status": 200, + "request": null, + "response": { + "id": 57, + "program_id": 41, + "version": 6, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:03.341730Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:16:03.341730Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:16:02.169575Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "program_id": 41, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 284 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 285 + }, + { + "value": 0, + "field_id": 286 + }, + { + "value": false, + "field_id": 287 + }, + { + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82", + "field_id": 288 + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/solution-invitations/60/", + "status": 200, + "request": null, + "response": { + "id": 60, + "version": 2, + "program": { + "id": 41, + "name": "Архив решений hack qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T15:16:02.016892Z", + "updated_at": "2026-10-25T15:16:02.176162Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/41/management/solutions/57/history/", + "status": 200, + "request": null, + "response": { + "count": 1, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 7, + "application": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T15:16:03.353901Z", + "before_version": 5, + "after_version": 6, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T15:16:03.341730Z", + "reason": "detached", + "revoked_invitation_count": 1 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/41/management/solutions/57/history/7/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 7, + "application": { + "kind": "solution", + "program_id": 41, + "solution_id": 57, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T15:16:03.353901Z", + "before_version": 5, + "after_version": 6, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T15:16:03.341730Z", + "reason": "detached", + "revoked_invitation_count": 1 + }, + "before_snapshot": null, + "after_snapshot": null, + "archive_snapshot": { + "form": { + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "program_id": 41, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "answers": [ + { + "value": "Город", + "field_id": 284 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 285 + }, + { + "value": 0, + "field_id": 286 + }, + { + "value": false, + "field_id": 287 + }, + { + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82", + "field_id": 288 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:16:02.169575Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 9, + "captured_at": "2026-10-25T15:16:03.341730Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/management/solutions/57/history/", + "status": 403, + "request": null, + "response": { + "code": "permission_denied", + "detail": "История доступна организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/41/solutions/57/", + "status": 409, + "request": { + "expected_version": 6, + "expected_form_revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "name": "Недоступная правка" + }, + "response": { + "code": "solution_archived", + "detail": "Решение снято с участия. Материалы доступны для просмотра.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/41/solutions/", + "status": 200, + "request": null, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/41/solutions/?participation=archived", + "status": 200, + "request": null, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 57, + "program_id": 41, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "accepted_count": 2, + "case_value": "Город", + "status": "draft", + "submitted_at": null, + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:16:03.341730Z", + "reason": "detached" + } + } + ] + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solution-context/", + "status": 200, + "request": null, + "response": { + "program_id": 41, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-26T15:13:49.124223Z", + "can_create": true, + "blocked_reason": null, + "current_application": null + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/57/", + "status": 200, + "request": null, + "response": { + "id": 57, + "program_id": 41, + "version": 6, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:01.789735Z", + "updated_at": "2026-10-25T15:16:03.341730Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:16:03.341730Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:16:01.793752Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:16:02.169575Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "2511a9be005b5bad6b5e75d3ae6ebd9281d08f1d58e7969b61488f37dc1280bb", + "template": "hackathon", + "program_id": 41, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 284 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 285 + }, + { + "value": 0, + "field_id": 286 + }, + { + "value": false, + "field_id": 287 + }, + { + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82", + "field_id": 288 + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/41/solutions/", + "status": 201, + "request": { + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "response": { + "id": 58, + "program_id": 41, + "version": 1, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:04.681246Z", + "updated_at": "2026-10-25T15:16:04.681246Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:04.684851Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "0153799845a3ecb88717f1c6620ef08a28b8a99839ddd4649ff61fcd43ebe7df", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T15:13:49.124223Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 284 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 285 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 286 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 287 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 288 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/58/", + "status": 200, + "request": null, + "response": { + "id": 58, + "program_id": 41, + "version": 1, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:04.681246Z", + "updated_at": "2026-10-25T15:16:04.681246Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:04.684851Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "0153799845a3ecb88717f1c6620ef08a28b8a99839ddd4649ff61fcd43ebe7df", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T15:13:49.124223Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 284 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 285 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 286 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 287 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 288 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/41/solutions/58/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 10, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 62, + "version": 1, + "program": { + "id": 41, + "name": "Архив решений hack qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 41, + "solution_id": 58, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T15:16:04.927516Z", + "updated_at": "2026-10-25T15:16:04.927516Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/62/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 62, + "version": 2, + "program": { + "id": 41, + "name": "Архив решений hack qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 41, + "solution_id": 58, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T15:16:04.927516Z", + "updated_at": "2026-10-25T15:16:05.158491Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 41, + "solution_id": 58, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/58/", + "status": 200, + "request": null, + "response": { + "id": 58, + "program_id": 41, + "version": 3, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:04.681246Z", + "updated_at": "2026-10-25T15:16:05.161575Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:04.684851Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:05.153381Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "0153799845a3ecb88717f1c6620ef08a28b8a99839ddd4649ff61fcd43ebe7df", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T15:13:49.124223Z", + "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": 284 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 285 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 286 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 287 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 288 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/41/solutions/58/", + "status": 200, + "request": { + "expected_version": 3, + "expected_form_revision": "0153799845a3ecb88717f1c6620ef08a28b8a99839ddd4649ff61fcd43ebe7df", + "answers": [ + { + "field_id": 284, + "value": "Город" + }, + { + "field_id": 285, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 286, + "value": 0 + }, + { + "field_id": 287, + "value": false + }, + { + "field_id": 288, + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82" + } + ] + }, + "response": { + "id": 58, + "program_id": 41, + "version": 4, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:04.681246Z", + "updated_at": "2026-10-25T15:16:05.390790Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:04.684851Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:05.153381Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 41, + "revision": "0153799845a3ecb88717f1c6620ef08a28b8a99839ddd4649ff61fcd43ebe7df", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 284, + "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": 285, + "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": 286, + "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": 287, + "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": 288, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 284, + "value": "Город" + }, + { + "field_id": 285, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 286, + "value": 0 + }, + { + "field_id": 287, + "value": false + }, + { + "field_id": 288, + "value": "https://example.com/archive-code/qa01arc-20261025-0674eb82" + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "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 + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/41/solution-context/", + "status": 200, + "request": null, + "response": { + "program_id": 41, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-26T15:13:49.124223Z", + "can_create": false, + "blocked_reason": "already_has_application", + "current_application": { + "kind": "solution", + "program_id": 41, + "solution_id": 58, + "name": "Работа captain hack qa01arc-20261025-0674eb82" + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/", + "status": 200, + "request": null, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 58, + "program_id": 41, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "accepted_count": 2, + "case_value": "Город", + "status": "draft", + "submitted_at": null, + "participation": { + "status": "active", + "archived_at": null, + "reason": null + } + } + ] + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/41/solutions/?participation=all", + "status": 200, + "request": null, + "response": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": 57, + "program_id": 41, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "accepted_count": 2, + "case_value": "Город", + "status": "draft", + "submitted_at": null, + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:16:03.341730Z", + "reason": "detached" + } + }, + { + "id": 58, + "program_id": 41, + "name": "Работа captain hack qa01arc-20261025-0674eb82", + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "accepted_count": 2, + "case_value": "Город", + "status": "draft", + "submitted_at": null, + "participation": { + "status": "active", + "archived_at": null, + "reason": null + } + } + ] + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/42/solutions/", + "status": 201, + "request": { + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "response": { + "id": 59, + "program_id": 42, + "version": 1, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:05.951934Z", + "updated_at": "2026-10-25T15:16:05.951934Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:05.954977Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 42, + "revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 289, + "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": 290, + "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": 291, + "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": 292, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/42/solutions/59/", + "status": 200, + "request": null, + "response": { + "id": 59, + "program_id": 42, + "version": 1, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:05.951934Z", + "updated_at": "2026-10-25T15:16:05.951934Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:05.954977Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 42, + "revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 289, + "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": 290, + "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": 291, + "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": 292, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/42/solutions/59/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 10, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 63, + "version": 1, + "program": { + "id": 42, + "name": "Архив решений public qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T15:16:06.188824Z", + "updated_at": "2026-10-25T15:16:06.188824Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/63/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 63, + "version": 2, + "program": { + "id": 42, + "name": "Архив решений public qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T15:16:06.188824Z", + "updated_at": "2026-10-25T15:16:06.326385Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/42/solutions/59/", + "status": 200, + "request": null, + "response": { + "id": 59, + "program_id": 42, + "version": 3, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:05.951934Z", + "updated_at": "2026-10-25T15:16:06.328494Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:05.954977Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:06.322271Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 42, + "revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 289, + "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": 290, + "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": 291, + "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": 292, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/42/solutions/59/", + "status": 200, + "request": { + "expected_version": 3, + "expected_form_revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "answers": [ + { + "field_id": 289, + "value": "Город" + }, + { + "field_id": 290, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 291, + "value": 0 + }, + { + "field_id": 292, + "value": false + } + ] + }, + "response": { + "id": 59, + "program_id": 42, + "version": 4, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:05.951934Z", + "updated_at": "2026-10-25T15:16:06.548961Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:05.954977Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:06.322271Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 42, + "revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 289, + "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": 290, + "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": 291, + "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": 292, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 289, + "value": "Город" + }, + { + "field_id": 290, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 291, + "value": 0 + }, + { + "field_id": 292, + "value": false + } + ], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": null, + "method": "GET", + "path": "/programs/42/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 42, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "application": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "published_at": "2026-10-25T15:16:06.630041Z", + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "accepted_count": 2 + } + ] + }, + "cacheControl": "no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/42/solutions/59/", + "status": 200, + "request": null, + "response": { + "id": 59, + "program_id": 42, + "version": 4, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:05.951934Z", + "updated_at": "2026-10-25T15:16:06.548961Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T15:16:05.954977Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T15:16:06.322271Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 42, + "revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 289, + "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": 290, + "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": 291, + "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": 292, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 289, + "value": "Город" + }, + { + "field_id": 290, + "value": "Сохраненные материалы qa01arc-20261025-0674eb82" + }, + { + "field_id": 291, + "value": 0 + }, + { + "field_id": 292, + "value": false + } + ], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/42/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [59] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "member", + "method": "POST", + "path": "/programs/42/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [59] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "expert", + "method": "POST", + "path": "/programs/42/management/solutions/archive/preview/", + "status": 403, + "request": { + "ids": [59] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": null, + "method": "POST", + "path": "/programs/42/management/solutions/archive/preview/", + "status": 401, + "request": { + "ids": [59] + }, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/42/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [59] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "solution_version": 4, + "is_submitted": false, + "is_archived": false, + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [] + } + ], + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "can_apply": true, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/42/management/solutions/archive/apply/", + "status": 200, + "request": { + "ids": [59], + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "changed_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "solution_version": 5, + "archived_at": "2026-10-25T15:16:07.423984Z" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/42/management/solutions/archive/apply/", + "status": 409, + "request": { + "ids": [59], + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/42/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [59] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "solution_version": 5, + "is_submitted": false, + "is_archived": true, + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [ + { + "path": "ids", + "code": "solution_archived", + "detail": "Решение уже снято с участия.", + "field_id": null + } + ] + } + ], + "score_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "can_apply": false, + "preview_token": null + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/42/solutions/59/", + "status": 200, + "request": null, + "response": { + "id": 59, + "program_id": 42, + "version": 5, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:05.951934Z", + "updated_at": "2026-10-25T15:16:07.423984Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:16:07.423984Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:16:05.954977Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:16:06.322271Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 289, + "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": 290, + "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": 291, + "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": 292, + "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 + } + } + ], + "revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "template": "case_championship", + "program_id": 42, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 289 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 290 + }, + { + "value": 0, + "field_id": 291 + }, + { + "value": false, + "field_id": 292 + } + ], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/42/solutions/59/", + "status": 200, + "request": null, + "response": { + "id": 59, + "program_id": 42, + "version": 5, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "captain_id": 9, + "created_at": "2026-10-25T15:16:05.951934Z", + "updated_at": "2026-10-25T15:16:07.423984Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:16:07.423984Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:16:05.954977Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:16:06.322271Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 289, + "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": 290, + "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": 291, + "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": 292, + "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 + } + } + ], + "revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "template": "case_championship", + "program_id": 42, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 289 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 290 + }, + { + "value": 0, + "field_id": 291 + }, + { + "value": false, + "field_id": 292 + } + ], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T15:13:49.124223Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/solution-invitations/63/", + "status": 200, + "request": null, + "response": { + "id": 63, + "version": 2, + "program": { + "id": 42, + "name": "Архив решений public qa01arc-20261025-0674eb82" + }, + "solution": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T15:16:06.188824Z", + "updated_at": "2026-10-25T15:16:06.326385Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/42/management/solutions/59/history/", + "status": 200, + "request": null, + "response": { + "count": 1, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 8, + "application": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T15:16:07.432073Z", + "before_version": 4, + "after_version": 5, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T15:16:07.423984Z", + "reason": "detached", + "revoked_invitation_count": 0 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/42/management/solutions/59/history/8/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 8, + "application": { + "kind": "solution", + "program_id": 42, + "solution_id": 59, + "name": "Работа captain public qa01arc-20261025-0674eb82" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T15:16:07.432073Z", + "before_version": 4, + "after_version": 5, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T15:16:07.423984Z", + "reason": "detached", + "revoked_invitation_count": 0 + }, + "before_snapshot": null, + "after_snapshot": null, + "archive_snapshot": { + "form": { + "fields": [ + { + "id": 289, + "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": 290, + "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": 291, + "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": 292, + "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 + } + } + ], + "revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "template": "case_championship", + "program_id": 42, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "answers": [ + { + "value": "Город", + "field_id": 289 + }, + { + "value": "Сохраненные материалы qa01arc-20261025-0674eb82", + "field_id": 290 + }, + { + "value": 0, + "field_id": 291 + }, + { + "value": false, + "field_id": 292 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T15:16:05.954977Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T15:16:06.322271Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 9, + "captured_at": "2026-10-25T15:16:07.423984Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/42/management/solutions/59/history/", + "status": 403, + "request": null, + "response": { + "code": "permission_denied", + "detail": "История доступна организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/42/solutions/59/", + "status": 409, + "request": { + "expected_version": 5, + "expected_form_revision": "9930aeb51cb91ad9273a6f2954c2979a2cee4f27579609a971a31e7bde7cea94", + "name": "Недоступная правка" + }, + "response": { + "code": "solution_archived", + "detail": "Решение снято с участия. Материалы доступны для просмотра.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/42/solutions/", + "status": 200, + "request": null, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/42/solutions/?participation=archived", + "status": 200, + "request": null, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 59, + "program_id": 42, + "name": "Работа captain public qa01arc-20261025-0674eb82", + "captain": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "accepted_count": 2, + "case_value": "Город", + "status": "draft", + "submitted_at": null, + "participation": { + "status": "archived", + "archived_at": "2026-10-25T15:16:07.423984Z", + "reason": "detached" + } + } + ] + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/42/solution-context/", + "status": 200, + "request": null, + "response": { + "program_id": 42, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": false, + "submission_open": true, + "submission_deadline": "2026-10-26T15:13:49.124223Z", + "can_create": false, + "blocked_reason": "solution_creation_disabled", + "current_application": null + }, + "cacheControl": null + }, + { + "actor": null, + "method": "GET", + "path": "/programs/42/published-solutions/", + "status": 200, + "request": null, + "response": { + "program_id": 42, + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": "no-store" + }, + { + "actor": null, + "method": "GET", + "path": "/programs/42/published-solutions/59/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/34/solution-context/", + "status": 200, + "request": null, + "response": { + "program_id": 34, + "storage_mode": "legacy_project", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-26T11:17:58.692009Z", + "can_create": false, + "blocked_reason": "already_has_application", + "current_application": { + "kind": "legacy_project", + "program_id": 34, + "project_id": 4, + "program_link_id": 3, + "name": "Заявка проекта qa01l-20261025-c6c0e219 сохранен" + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/34/management/solutions/archive/preview/", + "status": 409, + "request": { + "ids": [55] + }, + "response": { + "code": "solution_mode_unavailable", + "detail": "Программа использует заявки на основе проектов.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/40/management/solutions/archive/preview/", + "status": 404, + "request": { + "ids": [57] + }, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + } +] diff --git a/docs/qa/program-solutions-archive-20261025/migration-evidence.json b/docs/qa/program-solutions-archive-20261025/migration-evidence.json new file mode 100644 index 000000000..f97d279eb --- /dev/null +++ b/docs/qa/program-solutions-archive-20261025/migration-evidence.json @@ -0,0 +1,37 @@ +{ + "status": "PASS", + "migration": "0037_solution_archive_retention", + "database": "local-procollab_ci", + "oldModelsUnchanged": 23, + "hashBefore": "fc500f1028354a9e8697edd516762157f9907574c4dfc4467560c21d8d0cbe0a", + "hashAfter": "fc500f1028354a9e8697edd516762157f9907574c4dfc4467560c21d8d0cbe0a", + "rowCounts": { + "partner_programs.PartnerProgram": 37, + "partner_programs.PartnerProgramField": 268, + "partner_programs.PartnerProgramUserProfile": 106, + "partner_programs.PartnerProgramProject": 3, + "partner_programs.PartnerProgramFieldValue": 2, + "partner_programs.ProgramSolution": 53, + "partner_programs.ProgramSolutionAnswer": 255, + "partner_programs.ProgramSolutionMember": 71, + "partner_programs.SolutionInvitation": 34, + "project_rates.SolutionScore": 32, + "project_rates.SolutionExpertAssignment": 20, + "project_rates.Criteria": 64, + "users.Expert": 7, + "files.UserFile": 8, + "projects.Project": 4, + "projects.Collaborator": 6, + "invites.Invite": 4, + "project_rates.ProjectScore": 3, + "project_rates.ProjectExpertAssignment": 1, + "industries.Industry": 1, + "users.Expert_programs": 25, + "partner_programs.PartnerProgram_managers": 41, + "partner_programs.SolutionAdministrativeChange": 4 + }, + "existingAdminEventsPreserved": 4, + "existingSolutionsArchivedOrDeleted": 0, + "existingProjectsMigratedOrDeleted": 0, + "onlyLocalSchema": true +} diff --git a/docs/qa/program-solutions-archived-rating-20261025/README.md b/docs/qa/program-solutions-archived-rating-20261025/README.md new file mode 100644 index 000000000..861781a01 --- /dev/null +++ b/docs/qa/program-solutions-archived-rating-20261025/README.md @@ -0,0 +1,30 @@ + + +# QA01: архивирование решения во время оценки + +25 октября 2026 года. Существующая форма D01 закрывает материалы/ввод при solution_archived; HTML/SCSS и макет сохранены. Это исправление поведения внутри согласованного D01, а не завершение первого выпуска. + +Триггер: эксперт одновременно организатор своей программы, открыл оценку; работа архивирована до подтверждения. До исправления сервер возвращал max_rates_reached/409 в открытом режиме либо not_assigned/403 в распределенном, а явный solution_archived оставлял старую форму на фронте. Теперь POST дает solution_archived/409; материалы, оценочные controls и предыдущий ввод исчезают. Эксперт без сохраненного доступа по-прежнему получает404. Сохраненные значения не переписываются. + +Product [BE 394bf166](https://github.com/PROCOLLAB-github/api/commit/394bf1666825ea87e7985b88c401a4e3c9f8d316) / [FE b4191a8b](https://github.com/PROCOLLAB-github/procollab/commit/b4191a8b082e41b318c67c7605fa2764b647c46c). C00 1.20.1:156 схем/59 операций/299 независимых сценариев; все156 прежних схем/59 операций/297 сценариев неизменны, добавлены2 примера. Три shared файла byte-identical. Миграций нет. + +## Доказательства + +- До fix: native3 —2 FAIL/1 PASS; component10 —1 FAIL/9 PASS. После:420 native PostgreSQL tests/OK без skips,2949 frontend tests/484 files +strict types, development build, scoped ESLint, Black/Flake8/Django check/migration consistency/C00/diff PASS. Focused component/wire21 — subset, не добавлять к2949. Locked Prettier3.8.3 до report:2042 файлов PASS; итог с report:2047 файлов PASS. +- Настоящий Chrome 156.0.8078.47/login, обе формы в одном завершенном запуске: КЧ47 с распределением и хакатон48 с открытым оцениванием, работы64/65.0/False/сохраненные комментарии → локальная2/новый комментарий → вкладка команды сохраняет ввод → подтверждение → archive preview/apply настоящим API → rating POST409/solution_archived → старые поля/форма/команда/ввод убраны → retry GET404. Активные собственные pending/completed counters0/0. Проверены390/1440 без horizontal overflow;8 screenshots,2 просмотрены. Network responses не подменялись; page errors в завершенном запуске0. + -36 ответов/20 JSON bodies проверены C00:18 seed +12 browser +6 readonly verifier. Второй эксперт GET/POST404, капитан читает архив readonly. По каждой работе сохранены все4 ID/value пары (0,False,непустой и системный пустой комментарий), полные hashes scores/answers/members и submission snapshot; попытка записать2 не сохранилась. Назначения сняты только явным архивом. Flags новых программ=False, новые Project не создавались. + -25 прежних моделей, включая Auth User и все записи BE08C2, совпали с baseline `c8196d411a2bbab6838167141baa1ae2b2229df7191f29d97f0d37f66cc56989`. Все7 независимых audit headers и прежние6 C2 events сохранены. Отдельный post-recovery check подтвердил прежний hash23 моделей717c9a66… и состояние C2 до новых fixtures. Новый UI организатора/навигация архива этим checkpoint не добавлялись. + +[Browser checks](browser-checks.json), [domain/C00](domain-and-contract-evidence.json), [wire](http-wire.json), [refs/source/raw hashes](evidence.json). Signed proof4 occurrences скрыты только в Git wire после проверки; JWT/Authorization/password не записывались. Private credentials/manifest находятся в TEMP, в Git не включены. + +## Сохраненные ограничения и попытки + +Первый полный native420 прерван локальными I/O ошибками: postgres перешел в recovery,77 errors. Это не PASS. Также Python не прочитал charmap.py, а первый build copy получил UNKNOWN при чтении chunk. PostgreSQL восстановился самостоятельно, без ручного перезапуска или сброса main DB. После восстановления проверены hashes/все прежние C2 данные. Причина I/O не установлена. Упавшая test_procollab_ci_unicode сохранена; итоговые420 прошли отдельно в новой test_procollab_ci_archived_rating_unicode (ICU ru-RU), без параллельного тяжелого FE прогона. FE2949 завершился успешно. + +Focused65 после guard выявил неверный формат тестовой stale revision; input заменен на валидный64-character hash, проверка archive409 сохранена. Первый fixture setup был атомарно отменен валидатором кейса без show_filter, затем helper исправлен без ослабления product validation. Первая попытка запуска браузера не нашла cached executable и не открыла страницу; итоговый запуск использует установленный Chrome. Readonly verifier сначала ожидал3 оценки, затем сравнен с реальным frame: сохранены все4, включая штатный пустой комментарий. Критерии/оценки/работы ради PASS не сбрасывались. Raw failures/logs/helpers и частичная первая build copy сохранены локально. + +Raw: `Z:/Документы/PROCOLLAB-dev/design-artifacts/program-solutions-20261024/qa01-archived-rating`. Build copy меняет только API/WS origins одного generated chunk; source environments не менялись. Remote avatars блокировались: screenshots могут содержать broken avatar. Полные desktop/a11y/zoom/production CDN/binary-file сценарии не заявляются. Owned API8877/SPA8882 остановлены, прежние8769/8871/8876 не перезапускались. Seed/browser helpers повторно не запускать: новые работы уже архивны. Readonly verifier можно использовать для сверки, не выдавая повтор за новый путь. + +Original26 AC strings сохранены;23 PASS — AC01–17/20/22–26, AC18/19 OPEN, AC21 отдельно BE10. Общий expert/results/public path AC18 еще не доказан. D02/D03 approval, manager/reminder/actions/results/public screens, confirmation/history/archive navigation, competitive unsent drafts и полный QA01/REL01 остаются в первом выпуске. Новых merge/deploy/реальных переключений/BE10 не было. + +Preceding C2 f22ea47: Linux PG40212381737 completed/success,39 locking +1818 full/OK без skips; его General40212377825 еще in_progress при упаковке. Preceding C1 bf5fc5a General40208630839 terminal success,1768/OK(skipped67). Это отдельные результаты предыдущих refs. Новый HEAD Linux CI фиксируется после push; PASS ему этим отчетом не присваивается. diff --git a/docs/qa/program-solutions-archived-rating-20261025/browser-checks.json b/docs/qa/program-solutions-archived-rating-20261025/browser-checks.json new file mode 100644 index 000000000..9b387f9fd --- /dev/null +++ b/docs/qa/program-solutions-archived-rating-20261025/browser-checks.json @@ -0,0 +1,47 @@ +{ + "status": "PASS", + "checks": [ + { + "program": 47, + "solution": 64, + "template": "case_championship", + "distributed": true, + "status": "PASS", + "submittedScoreAfterArchive": 409, + "code": "solution_archived", + "retry": 404, + "fieldsAndControlsRemoved": true, + "noUnsavedCopy": true, + "teamTabRetainedInput": true, + "activeExpertCounts": { + "pending": 0, + "completed": 0 + }, + "widths": [390, 1440] + }, + { + "program": 48, + "solution": 65, + "template": "hackathon", + "distributed": false, + "status": "PASS", + "submittedScoreAfterArchive": 409, + "code": "solution_archived", + "retry": 404, + "fieldsAndControlsRemoved": true, + "noUnsavedCopy": true, + "teamTabRetainedInput": true, + "activeExpertCounts": { + "pending": 0, + "completed": 0 + }, + "widths": [390, 1440] + } + ], + "javascriptErrors": [], + "oneInvocation": true, + "realLogin": true, + "networkResponsesMocked": false, + "browserVersion": "156.0.8078.47", + "channel": "chrome" +} diff --git a/docs/qa/program-solutions-archived-rating-20261025/domain-and-contract-evidence.json b/docs/qa/program-solutions-archived-rating-20261025/domain-and-contract-evidence.json new file mode 100644 index 000000000..66fd11314 --- /dev/null +++ b/docs/qa/program-solutions-archived-rating-20261025/domain-and-contract-evidence.json @@ -0,0 +1,33 @@ +{ + "status": "PASS", + "contract": "1.20.1", + "responses": 36, + "validRequests": 20, + "seedResponses": 18, + "browserResponses": 12, + "readOnlyVerifierResponses": 6, + "oldModelsUnchanged": 25, + "hashBefore": "c8196d411a2bbab6838167141baa1ae2b2229df7191f29d97f0d37f66cc56989", + "hashAfter": "c8196d411a2bbab6838167141baa1ae2b2229df7191f29d97f0d37f66cc56989", + "programs": [ + { + "id": 47, + "solution": 64, + "template": "case_championship", + "retainedOwnScores": 4 + }, + { + "id": 48, + "solution": 65, + "template": "hackathon", + "retainedOwnScores": 4 + } + ], + "globalRegistrationAuditsRetained": 7, + "unchangedAllEarlierC2Rows": true, + "creationFlagsFalse": true, + "savedZeroFalseAndCommentsRetained": true, + "defaultEmptyCommentPreserved": true, + "attemptedTwoNotWritten": true, + "noNewProject": true +} diff --git a/docs/qa/program-solutions-archived-rating-20261025/evidence.json b/docs/qa/program-solutions-archived-rating-20261025/evidence.json new file mode 100644 index 000000000..206ba4708 --- /dev/null +++ b/docs/qa/program-solutions-archived-rating-20261025/evidence.json @@ -0,0 +1,259 @@ +{ + "status": "PASS", + "scope": "D01 archived-rating behavior fix; full first release incomplete", + "backendProduct": "394bf1666825ea87e7985b88c401a4e3c9f8d316", + "frontendProduct": "b4191a8b082e41b318c67c7605fa2764b647c46c", + "contract": { + "version": "1.20.1", + "schemas": 156, + "operations": 59, + "scenarios": 299, + "oldSchemasUnchanged": 156, + "oldOperationsUnchanged": 59, + "oldScenariosUnchanged": 297, + "sharedFilesByteIdentical": 3 + }, + "tests": { + "nativePG": 420, + "newNative": 3, + "frontend": 2949, + "frontendFiles": 484, + "componentAndWireFocused": 21, + "strictTypes": "PASS", + "developmentBuild": "PASS", + "scopedLint": "PASS", + "black": "PASS", + "flake8": "PASS", + "djangoCheck": "PASS", + "migrationConsistency": "PASS", + "prettierBeforeReports": { + "version": "3.8.3", + "files": 2042, + "status": "PASS" + }, + "prettierWithReports": { + "version": "3.8.3", + "files": 2047, + "status": "PASS" + } + }, + "domain": { + "status": "PASS", + "contract": "1.20.1", + "responses": 36, + "validRequests": 20, + "seedResponses": 18, + "browserResponses": 12, + "readOnlyVerifierResponses": 6, + "oldModelsUnchanged": 25, + "hashBefore": "c8196d411a2bbab6838167141baa1ae2b2229df7191f29d97f0d37f66cc56989", + "hashAfter": "c8196d411a2bbab6838167141baa1ae2b2229df7191f29d97f0d37f66cc56989", + "programs": [ + { + "id": 47, + "solution": 64, + "template": "case_championship", + "retainedOwnScores": 4 + }, + { + "id": 48, + "solution": 65, + "template": "hackathon", + "retainedOwnScores": 4 + } + ], + "globalRegistrationAuditsRetained": 7, + "unchangedAllEarlierC2Rows": true, + "creationFlagsFalse": true, + "savedZeroFalseAndCommentsRetained": true, + "defaultEmptyCommentPreserved": true, + "attemptedTwoNotWritten": true, + "noNewProject": true + }, + "browser": { + "status": "PASS", + "checks": [ + { + "program": 47, + "solution": 64, + "template": "case_championship", + "distributed": true, + "status": "PASS", + "submittedScoreAfterArchive": 409, + "code": "solution_archived", + "retry": 404, + "fieldsAndControlsRemoved": true, + "noUnsavedCopy": true, + "teamTabRetainedInput": true, + "activeExpertCounts": { + "pending": 0, + "completed": 0 + }, + "widths": [390, 1440] + }, + { + "program": 48, + "solution": 65, + "template": "hackathon", + "distributed": false, + "status": "PASS", + "submittedScoreAfterArchive": 409, + "code": "solution_archived", + "retry": 404, + "fieldsAndControlsRemoved": true, + "noUnsavedCopy": true, + "teamTabRetainedInput": true, + "activeExpertCounts": { + "pending": 0, + "completed": 0 + }, + "widths": [390, 1440] + } + ], + "javascriptErrors": [], + "oneInvocation": true, + "realLogin": true, + "networkResponsesMocked": false, + "browserVersion": "156.0.8078.47", + "channel": "chrome" + }, + "recovery": { + "status": "PASS", + "oldModelsUnchanged": 23, + "hashBefore": "717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d", + "hashAfter": "717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d", + "c2Audits": 7, + "c2SolutionEvents": 6, + "c2Solutions": [60, 61, 62, 63], + "postgresRecoveredWithoutRestart": true + }, + "sourceHashesNormalizedLF": { + "backend": { + "docs/contracts/program-solutions-v1/README.md": "57b80e1f7b670c8d0ebba5e9da167aa46c0640879cf7bf698a2dd7c1fe3216e3", + "docs/contracts/program-solutions-v1/examples.json": "0ffa46ca633c310a87f021321c519254802e496aee8250379c0d5d60d6455a6f", + "docs/contracts/program-solutions-v1/openapi.json": "a796786fa97d25041b4d2418fa729f422c4fa4d52d97a98a5ee1ad63a88fbb75", + "docs/product/program-solutions-be07.md": "33cf9dba0453a3d22d0434844edd8dc0b107d2b62d844e35c8e34004d10400fa", + "project_rates/solution_evaluation.py": "7caad9c059426a707739a0db88dd26c8ae7818ca1a659f61fb6e03b6a552df01", + "project_rates/tests/test_solution_evaluation.py": "89c2c496713c98dbec50ba02c0d67fa4782b00e0b5ef82075f54c3bba8d30766" + }, + "frontend": { + "docs/contracts/program-solutions-v1/README.md": "57b80e1f7b670c8d0ebba5e9da167aa46c0640879cf7bf698a2dd7c1fe3216e3", + "docs/contracts/program-solutions-v1/examples.json": "0ffa46ca633c310a87f021321c519254802e496aee8250379c0d5d60d6455a6f", + "docs/contracts/program-solutions-v1/openapi.json": "a796786fa97d25041b4d2418fa729f422c4fa4d52d97a98a5ee1ad63a88fbb75", + "projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts": "25493c9a849fdd69574a71ccc2d0580d01690642b2237546f3cfd85ad19e757a", + "projects/social_platform/src/app/domain/solution/solution-contract.model.ts": "a7972d2ad9cbd79813700d81c709136cca1289ef6bd9bf06d3b702828e6f5811", + "projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.spec.ts": "b658e6b4849eb61d5c733867ff31f97e69404374669fa4cafad935aee581d6bd", + "projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts": "aec63cc4382476a888507df6a85fe31d4cea8f92a6f8f84df03f1e72798611c3" + } + }, + "rawArtifacts": { + "browser-evidence.json": "fb12ddd8a3e7b3a0c00e6b21771072667621c55bf600fa87baef366dcffa1822", + "browser-failure-1792948080945.json": "7e1851abdd3c066bb3cd513471cd0a9eaf1212545d8be60fe52cb41b3ba8e2b7", + "build-provenance.json": "4b94da2c7e798a3d413930e128f90615fdc3b4b30721e98ed3afbdffa53dc6d3", + "case_championship-archived-1440.png": "fea927fabaa71ec07a6202ff5c4e785de52174dbd078548a8d3225c9fc85c6f9", + "case_championship-archived-390.png": "c214f3b5c6daa033188c91eeb088f897bc95bc2727a2a2cecaeafdad3c0c6364", + "case_championship-before-confirm-1440.png": "7351376e9e1a79e829240d51f97437c7eb5b44a985641a53950e8c71bbeadf58", + "case_championship-before-confirm-390.png": "b57aafa39084f4da588658e4c144a62421971a868cfdc82fa681fa0cc29f408f", + "domain-and-contract-evidence.json": "11d4e32dcd59de72214b333dbcd46d626356b88be4d7e1c694c595939aa71e27", + "hackathon-archived-1440.png": "fc3de347668d0f8f0fcba4357e5eb85fdd139ebc0fd508804c3466d9f3b248f1", + "hackathon-archived-390.png": "2908c9c8bdf78dc2b2f6167fd677cf3a8220e888ac009fc49bdd791fb658aa57", + "hackathon-before-confirm-1440.png": "c18d073948a38b5e653e1343eb06b4112c7c428cac820d8cbf50537bcd48e48d", + "hackathon-before-confirm-390.png": "1edaf1ecada51e8c26fe3b1fdd82a644ea69f56a62c61bb861c048311f6bc5fa", + "http-wire.json": "b7a6ba74ca06fad2cdd39e8def686258eb653a43ea6fc2722d920d0f0f1a5f36", + "procollab-archived-rating-browser-chrome.log": "d0bb975d7ec253cf1c3958f7479e9c0cf9b5ab2e6fb724c371e20e18a040113e", + "procollab-archived-rating-browser.cjs": "5e59591f0509404e3d143367574b180db4cec17fe77bf0ee1bb8a55ef6063b75", + "procollab-archived-rating-browser.log": "f9f3a83279317430cfae03ffe882848033c0eba9d8fcc77ccff69f7927be167d", + "procollab-archived-rating-build-copy-recovered.log": "8ae0abe2c9bc4b7bc6bb726ace35816e9c803f1fc6fcfaa951f29a6fb685aa51", + "procollab-archived-rating-build.cjs": "136b7c266e3a1d7bfc060a5077af302f6f5a850d085e9a34665e405b49aaf33c", + "procollab-archived-rating-build.log": "824f869360907ad73c864c56445cd9fc0eaafde11b7684d0b8b0bfc7da281a93", + "procollab-archived-rating-contract.py": "8f7d3a88b313dfff7b6bddf0128b96937e8ccfda454d6e3bad911439ad208b0c", + "procollab-archived-rating-fe-final.log": "05228d50f42bdb1ed74bac8eb5faf4377e50dfebe2bf13ee70e1a88fa24713b0", + "procollab-archived-rating-format-before-reports.json": "7b9282c7f10eef7761182843bc43761099da5080beb561f7299ab1fa3954b9b7", + "procollab-archived-rating-main-recovery.json": "6969115b676e524d0f4ff9fb554e821c08206c43b17041310ff9afcc91a9264d", + "procollab-archived-rating-native-after.log": "f1d328be6825fe95a8f01267386a154980b21e6462c1d0e10eb2a389ffaccdc0", + "procollab-archived-rating-native-before.log": "00c462b1c8ff061e938bfeff6929007c7f07de51d4a35aeade9cc857955800b8", + "procollab-archived-rating-native-final.log": "35f441b492bf94978943e285750a69e3937e9049832a851cec79f9b1fc835e03", + "procollab-archived-rating-native-recovered.log": "cd16434e227ab35a0094b4f1e162f6e968deba96416d9bf9792084708cdf3c16", + "procollab-archived-rating-pg-recovery.log": "46d432d365db1bbda5ffd9ce3f46f4b0afb5a39b7e0af4b47ebdc83dc45fb996", + "procollab-archived-rating-preceding-c1-general.log": "31e3da8df467214cd2838877645930033aa767f569e5c7901ca8829a339d4374", + "procollab-archived-rating-preceding-c2-pg.log": "7a3f8b20eb04492889a43cbe1b7e902b4d034f8069f2d6656af4d7f26637863a", + "procollab-archived-rating-recovery-check.py": "6fd9dc9edceea6221b21e81b2521086b485afe20cc38c3b7ccfc204c7313bf0d", + "procollab-archived-rating-seed-final.log": "85d5d3e275f2547046161c148ebcbd6ed62d8743f0e1b9b550497d98c907b549", + "procollab-archived-rating-seed-recovered.log": "55f4e7f60607966410c82a255ff3c9c2686cf033208dcab338c307ef74b0172a", + "procollab-archived-rating-seed.log": "92a16c45f6425312807b5ffaa20619d2bd9346dd94ad1e7297334ac01cc137c0", + "procollab-archived-rating-seed.py": "ff5e7b7b4c5a0b1b40fe70de7d84b96593c24b91790149d08759ee3513172b73", + "procollab-archived-rating-server.py": "2b2edeffed8c144f469726a59db01561f309ddb0c792927cc44a4948a0aa1e27", + "procollab-archived-rating-spa.py": "f9cc77ef44018f7eaf86ff0639be0aa77e80782ef1f8dc8872a1537e54734b4b", + "procollab-archived-rating-ui-after.log": "a00c0ef364598480e32a732726e1e871d66342b00e948aa040e330356496d0ac", + "procollab-archived-rating-ui-before.log": "39e4767ee6208ecc9b78422963a0fcbbca03ff70abda59b8289c1c57a8412c01", + "procollab-archived-rating-verify-final.log": "c46c6964eea653209617c78d824202b10435bcc874cd04943848128b2d772d51", + "procollab-archived-rating-verify.log": "34a0315967f4baa64abe1325d58d78a227a567ec2de5d465c1c2180fac9d35dd", + "procollab-archived-rating-verify.py": "3ba98ef966bb8014e2f801c95535a74dea3fa6eb8b52996917d925d264a07e6f", + "procollab-test-archived-rating-unicode.py": "cae96723152683c3b819efafd59621b5c6a7fcfecf3fa39f476082e8024b8d31", + "procollab-archived-rating-format-final.json": "180757ddaa25f8163dc2514ac4faf704f7f6fe32c4ba81066e3f43a629a4fbb1", + "project-count-evidence.json": "95de71492afacde61c6ea6b3c5312571808c794001f69043d76c6b955711a276" + }, + "redactedSignedProofs": 4, + "attempts": [ + "Before: native3 =>2 product failures, component10 =>1 product failure.", + "Intermediate native65: malformed synthetic stale-revision string correctly got400; test now uses valid64-character stale hash and still asserts archive409.", + "Full420 interrupted by local PostgreSQL recovery and I/O errors =>77 errors; not PASS. Separate fresh ICU ru-RU test DB completed420/OK. Failed test DB retained; main DB not reset.", + "First seed after recovery rejected missing show_filter on a case definition and rolled back its atomic fixture setup. Corrected fixture obeys the existing case rule.", + "Initial Playwright launch had no cached executable and performed no browser actions; completed run used installed Chrome.", + "Initial verifier assumed3 scores; actual captured frame includes default empty comment, so all4 exact ID/value pairs plus complete row hashes are now checked. No work, criteria or score was reset." + ], + "qa": { + "originalACRowsUnchanged": 26, + "passCount": 23, + "passAC": "01-17,20,22-26", + "openAC": ["18", "19"], + "separateBE10": "21" + }, + "priorCI": { + "c2PG": { + "run": 40212381737, + "head": "f22ea47bad4ef68b66a239c5787c23607f8a739e", + "status": "completed/success", + "locking": 39, + "full": 1818, + "skips": 0 + }, + "c2General": { + "run": 40212377825, + "status": "in_progress" + }, + "c1General": { + "run": 40208630839, + "status": "completed/success", + "full": 1768, + "skipped": 67 + } + }, + "limitations": [ + "No new layout/HTML/SCSS/DS change or D02/D03 implementation.", + "Completed browser invocation on Chrome156,390/1440; full a11y/zoom/production CDN not claimed. External avatars were blocked, broken avatars may appear in screenshots.", + "Initial local I/O failure root cause not established; automatic PG recovery observed and old data hashes rechecked.", + "Binary/CDN operations not part of this journey.", + "D02/D03 screens, competitive unsent draft eligibility, full QA01/REL01 remain; BE10 separate.", + "No merge/deploy/real-program switching/Project migration or deletion; new HEAD Linux CI not claimed." + ], + "ownedHelpers": { + "stopped": true, + "apiWrapper": 19568, + "apiChild": 11504, + "spaWrapper": 32128, + "spaChild": 24008, + "releasedPorts": [8877, 8882], + "oldServersUnchanged": { + "8769": 20920, + "8871": 19092, + "8876": 33332 + } + }, + "projectCounts": { + "status": "PASS", + "projectCountBefore": 4, + "projectCountAfter": 4, + "newProjects": 0 + } +} diff --git a/docs/qa/program-solutions-archived-rating-20261025/http-wire.json b/docs/qa/program-solutions-archived-rating-20261025/http-wire.json new file mode 100644 index 000000000..b004a92bc --- /dev/null +++ b/docs/qa/program-solutions-archived-rating-20261025/http-wire.json @@ -0,0 +1,4248 @@ +[ + { + "role": "captain", + "method": "POST", + "path": "/programs/47/solutions/", + "status": 201, + "request": { + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a" + }, + "response": { + "id": 64, + "program_id": 47, + "version": 1, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:07:40.016648Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 47, + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "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": 306 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 307 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 308 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 309 + } + ] + } + } + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/47/solutions/64/", + "status": 200, + "request": null, + "response": { + "id": 64, + "program_id": 47, + "version": 1, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:07:40.016648Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 47, + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "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": 306 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 307 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 308 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 309 + } + ] + } + } + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/47/solutions/64/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 30, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 68, + "version": 1, + "program": { + "id": 47, + "name": "Архив и открытая оценка case_championship qa01ar-20261025-2d84767a" + }, + "solution": { + "kind": "solution", + "program_id": 47, + "solution_id": 64, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a" + }, + "captain": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T17:07:40.193334Z", + "updated_at": "2026-10-25T17:07:40.193334Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + } + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/68/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 68, + "version": 2, + "program": { + "id": 47, + "name": "Архив и открытая оценка case_championship qa01ar-20261025-2d84767a" + }, + "solution": { + "kind": "solution", + "program_id": 47, + "solution_id": 64, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a" + }, + "captain": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T17:07:40.193334Z", + "updated_at": "2026-10-25T17:07:40.278679Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 47, + "solution_id": 64, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a" + }, + "solution_version": 3 + } + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/47/solutions/64/", + "status": 200, + "request": null, + "response": { + "id": 64, + "program_id": 47, + "version": 3, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:07:40.280828Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true + }, + { + "user": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T17:07:40.273637Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 47, + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "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": 306 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 307 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 308 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 309 + } + ] + } + } + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/47/solutions/64/", + "status": 200, + "request": { + "expected_version": 3, + "expected_form_revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "answers": [ + { + "field_id": 306, + "value": "Город" + }, + { + "field_id": 307, + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a" + }, + { + "field_id": 308, + "value": 0 + }, + { + "field_id": 309, + "value": false + } + ] + }, + "response": { + "id": 64, + "program_id": 47, + "version": 4, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:07:40.445560Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true + }, + { + "user": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T17:07:40.273637Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 47, + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 306, + "value": "Город" + }, + { + "field_id": 307, + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a" + }, + { + "field_id": 308, + "value": 0 + }, + { + "field_id": 309, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "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": [] + } + } + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/47/solutions/64/submit/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778" + }, + "response": { + "id": 64, + "program_id": 47, + "version": 5, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:07:40.551010Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true + }, + { + "user": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T17:07:40.273637Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 47, + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 306, + "value": "Город" + }, + { + "field_id": 307, + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a" + }, + { + "field_id": 308, + "value": 0 + }, + { + "field_id": 309, + "value": false + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:40.548989Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/47/solutions/64/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 47, + "name": "Архив и открытая оценка case_championship qa01ar-20261025-2d84767a" + }, + "solution": { + "id": 64, + "program_id": 47, + "version": 5, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:07:40.551010Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 29, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 30, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:40.273637Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ], + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "program_id": 47, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 306 + }, + { + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a", + "field_id": 307 + }, + { + "value": 0, + "field_id": 308 + }, + { + "value": false, + "field_id": 309 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:40.548989Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "4e05c1aa16267989a173957781f526689a67669fc52c26790ae62fc771ce74f4", + "criteria": [ + { + "id": 85, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 86, + "name": "Качество 3", + "description": null, + "type": "int", + "min_value": 0, + "max_value": 10, + "value": null + }, + { + "id": 87, + "name": "Проверено 4", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 88, + "name": "Комментарий 5", + "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": 2, + "required_evaluators": 2, + "deadline": "2026-10-27T17:07:39.820702Z" + } + }, + { + "role": "expert", + "method": "POST", + "path": "/programs/47/solutions/64/evaluation/", + "status": 200, + "request": { + "expected_criteria_revision": "4e05c1aa16267989a173957781f526689a67669fc52c26790ae62fc771ce74f4", + "scores": [ + { + "criterion_id": 86, + "value": "0" + }, + { + "criterion_id": 87, + "value": "False" + }, + { + "criterion_id": 88, + "value": "Сохраненный комментарий qa01ar-20261025-2d84767a" + } + ] + }, + "response": { + "program": { + "id": 47, + "name": "Архив и открытая оценка case_championship qa01ar-20261025-2d84767a" + }, + "solution": { + "id": 64, + "program_id": 47, + "version": 5, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:07:40.551010Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 29, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 30, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:40.273637Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ], + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "program_id": 47, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 306 + }, + { + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a", + "field_id": 307 + }, + { + "value": 0, + "field_id": 308 + }, + { + "value": false, + "field_id": 309 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:40.548989Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "4e05c1aa16267989a173957781f526689a67669fc52c26790ae62fc771ce74f4", + "criteria": [ + { + "id": 85, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": "" + }, + { + "id": 86, + "name": "Качество 3", + "description": null, + "type": "int", + "min_value": 0, + "max_value": 10, + "value": "0" + }, + { + "id": 87, + "name": "Проверено 4", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + }, + { + "id": 88, + "name": "Комментарий 5", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "Сохраненный комментарий qa01ar-20261025-2d84767a" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": true, + "rated_count": 1, + "max_rates": 2, + "required_evaluators": 2, + "deadline": "2026-10-27T17:07:39.820702Z" + } + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/48/solutions/", + "status": 201, + "request": { + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a" + }, + "response": { + "id": 65, + "program_id": 48, + "version": 1, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:07:41.076815Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 48, + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T17:07:39.820702Z", + "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": 310 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 311 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 312 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 313 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 314 + } + ] + } + } + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/48/solutions/65/", + "status": 200, + "request": null, + "response": { + "id": 65, + "program_id": 48, + "version": 1, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:07:41.076815Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 48, + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T17:07:39.820702Z", + "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": 310 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 311 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 312 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 313 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 314 + } + ] + } + } + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/48/solutions/65/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 30, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 69, + "version": 1, + "program": { + "id": 48, + "name": "Архив и открытая оценка hackathon qa01ar-20261025-2d84767a" + }, + "solution": { + "kind": "solution", + "program_id": 48, + "solution_id": 65, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a" + }, + "captain": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T17:07:41.292822Z", + "updated_at": "2026-10-25T17:07:41.292822Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + } + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/69/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 69, + "version": 2, + "program": { + "id": 48, + "name": "Архив и открытая оценка hackathon qa01ar-20261025-2d84767a" + }, + "solution": { + "kind": "solution", + "program_id": 48, + "solution_id": 65, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a" + }, + "captain": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T17:07:41.292822Z", + "updated_at": "2026-10-25T17:07:41.384597Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 48, + "solution_id": 65, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a" + }, + "solution_version": 3 + } + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/48/solutions/65/", + "status": 200, + "request": null, + "response": { + "id": 65, + "program_id": 48, + "version": 3, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:07:41.388626Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true + }, + { + "user": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T17:07:41.381325Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 48, + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T17:07:39.820702Z", + "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": 310 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 311 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 312 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 313 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 314 + } + ] + } + } + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/48/solutions/65/", + "status": 200, + "request": { + "expected_version": 3, + "expected_form_revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "answers": [ + { + "field_id": 310, + "value": "Город" + }, + { + "field_id": 311, + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a" + }, + { + "field_id": 312, + "value": 0 + }, + { + "field_id": 313, + "value": false + }, + { + "field_id": 314, + "value": "https://example.com/archived-rating/qa01ar-20261025-2d84767a" + } + ] + }, + "response": { + "id": 65, + "program_id": 48, + "version": 4, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:07:41.571272Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true + }, + { + "user": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T17:07:41.381325Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 48, + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 310, + "value": "Город" + }, + { + "field_id": 311, + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a" + }, + { + "field_id": 312, + "value": 0 + }, + { + "field_id": 313, + "value": false + }, + { + "field_id": 314, + "value": "https://example.com/archived-rating/qa01ar-20261025-2d84767a" + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "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": [] + } + } + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/48/solutions/65/submit/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b" + }, + "response": { + "id": 65, + "program_id": 48, + "version": 5, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:07:41.685460Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 29, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true + }, + { + "user": { + "id": 30, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T17:07:41.381325Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 48, + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 310, + "value": "Город" + }, + { + "field_id": 311, + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a" + }, + { + "field_id": 312, + "value": 0 + }, + { + "field_id": 313, + "value": false + }, + { + "field_id": 314, + "value": "https://example.com/archived-rating/qa01ar-20261025-2d84767a" + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:41.682257Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/48/solutions/65/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 48, + "name": "Архив и открытая оценка hackathon qa01ar-20261025-2d84767a" + }, + "solution": { + "id": 65, + "program_id": 48, + "version": 5, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:07:41.685460Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 29, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 30, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:41.381325Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "program_id": 48, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 310 + }, + { + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a", + "field_id": 311 + }, + { + "value": 0, + "field_id": 312 + }, + { + "value": false, + "field_id": 313 + }, + { + "value": "https://example.com/archived-rating/qa01ar-20261025-2d84767a", + "field_id": 314 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:41.682257Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "63e6f2a731b85b50a8ca1fb6a479c725e925135f7d0644f67835a39a91fe12ac", + "criteria": [ + { + "id": 89, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 90, + "name": "Качество 6", + "description": null, + "type": "int", + "min_value": 0, + "max_value": 10, + "value": null + }, + { + "id": 91, + "name": "Проверено 7", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 92, + "name": "Комментарий 8", + "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": 2, + "required_evaluators": 2, + "deadline": "2026-10-27T17:07:39.820702Z" + } + }, + { + "role": "expert", + "method": "POST", + "path": "/programs/48/solutions/65/evaluation/", + "status": 200, + "request": { + "expected_criteria_revision": "63e6f2a731b85b50a8ca1fb6a479c725e925135f7d0644f67835a39a91fe12ac", + "scores": [ + { + "criterion_id": 90, + "value": "0" + }, + { + "criterion_id": 91, + "value": "False" + }, + { + "criterion_id": 92, + "value": "Сохраненный комментарий qa01ar-20261025-2d84767a" + } + ] + }, + "response": { + "program": { + "id": 48, + "name": "Архив и открытая оценка hackathon qa01ar-20261025-2d84767a" + }, + "solution": { + "id": 65, + "program_id": 48, + "version": 5, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:07:41.685460Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 29, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 30, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:41.381325Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "program_id": 48, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 310 + }, + { + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a", + "field_id": 311 + }, + { + "value": 0, + "field_id": 312 + }, + { + "value": false, + "field_id": 313 + }, + { + "value": "https://example.com/archived-rating/qa01ar-20261025-2d84767a", + "field_id": 314 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:41.682257Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "63e6f2a731b85b50a8ca1fb6a479c725e925135f7d0644f67835a39a91fe12ac", + "criteria": [ + { + "id": 89, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": "" + }, + { + "id": 90, + "name": "Качество 6", + "description": null, + "type": "int", + "min_value": 0, + "max_value": 10, + "value": "0" + }, + { + "id": 91, + "name": "Проверено 7", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + }, + { + "id": 92, + "name": "Комментарий 8", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "Сохраненный комментарий qa01ar-20261025-2d84767a" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": true, + "rated_count": 1, + "max_rates": 2, + "required_evaluators": 2, + "deadline": "2026-10-27T17:07:39.820702Z" + } + }, + { + "role": "browser-expert", + "method": "GET", + "path": "/programs/47/solutions/64/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 47, + "name": "Архив и открытая оценка case_championship qa01ar-20261025-2d84767a" + }, + "solution": { + "id": 64, + "program_id": 47, + "version": 5, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:07:40.551010Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 29, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 30, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:40.273637Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ], + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "program_id": 47, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 306 + }, + { + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a", + "field_id": 307 + }, + { + "value": 0, + "field_id": 308 + }, + { + "value": false, + "field_id": 309 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:40.548989Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "4e05c1aa16267989a173957781f526689a67669fc52c26790ae62fc771ce74f4", + "criteria": [ + { + "id": 85, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": "" + }, + { + "id": 86, + "name": "Качество 3", + "description": null, + "type": "int", + "min_value": 0, + "max_value": 10, + "value": "0" + }, + { + "id": 87, + "name": "Проверено 4", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + }, + { + "id": 88, + "name": "Комментарий 5", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "Сохраненный комментарий qa01ar-20261025-2d84767a" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": true, + "rated_count": 1, + "max_rates": 2, + "required_evaluators": 2, + "deadline": "2026-10-27T17:07:39.820702Z" + } + }, + { + "role": "dual-role-expert-manager", + "method": "POST", + "path": "/programs/47/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [64] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 47, + "solution_id": 64, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a" + }, + "solution_version": 5, + "is_submitted": true, + "is_archived": false, + "score_count": 4, + "removed_assignments_count": 1, + "revoked_invitation_count": 0, + "errors": [] + } + ], + "score_count": 4, + "removed_assignments_count": 1, + "revoked_invitation_count": 0, + "can_apply": true, + "preview_token": "" + } + }, + { + "role": "dual-role-expert-manager", + "method": "POST", + "path": "/programs/47/management/solutions/archive/apply/", + "status": 200, + "request": { + "ids": [64], + "preview_token": "" + }, + "response": { + "changed_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 47, + "solution_id": 64, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a" + }, + "solution_version": 6, + "archived_at": "2026-10-25T17:09:11.219357Z" + } + ] + } + }, + { + "role": "browser-expert", + "method": "POST", + "path": "/programs/47/solutions/64/evaluation/", + "status": 409, + "request": { + "expected_criteria_revision": "4e05c1aa16267989a173957781f526689a67669fc52c26790ae62fc771ce74f4", + "scores": [ + { + "criterion_id": 85, + "value": "" + }, + { + "criterion_id": 86, + "value": "2" + }, + { + "criterion_id": 87, + "value": "False" + }, + { + "criterion_id": 88, + "value": "Несохраненная оценка qa01ar-20261025-2d84767a" + } + ] + }, + "response": { + "code": "solution_archived", + "detail": "Решение снято с участия. Материалы доступны для просмотра.", + "errors": [], + "meta": {} + } + }, + { + "role": "browser-expert", + "method": "GET", + "path": "/programs/47/solutions/64/evaluation/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + } + }, + { + "role": "dual-role-expert-manager", + "method": "GET", + "path": "/programs/47/solution-evaluation/?status=completed", + "status": 200, + "request": null, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [], + "evaluation_counts": { + "pending": 0, + "completed": 0 + }, + "case_options": ["Город", "Образование"] + } + }, + { + "role": "browser-expert", + "method": "GET", + "path": "/programs/48/solutions/65/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 48, + "name": "Архив и открытая оценка hackathon qa01ar-20261025-2d84767a" + }, + "solution": { + "id": 65, + "program_id": 48, + "version": 5, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:07:41.685460Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 29, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 30, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:41.381325Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "program_id": 48, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 310 + }, + { + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a", + "field_id": 311 + }, + { + "value": 0, + "field_id": 312 + }, + { + "value": false, + "field_id": 313 + }, + { + "value": "https://example.com/archived-rating/qa01ar-20261025-2d84767a", + "field_id": 314 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:41.682257Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "63e6f2a731b85b50a8ca1fb6a479c725e925135f7d0644f67835a39a91fe12ac", + "criteria": [ + { + "id": 89, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": "" + }, + { + "id": 90, + "name": "Качество 6", + "description": null, + "type": "int", + "min_value": 0, + "max_value": 10, + "value": "0" + }, + { + "id": 91, + "name": "Проверено 7", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + }, + { + "id": 92, + "name": "Комментарий 8", + "description": null, + "type": "str", + "min_value": null, + "max_value": null, + "value": "Сохраненный комментарий qa01ar-20261025-2d84767a" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": true, + "rated_count": 1, + "max_rates": 2, + "required_evaluators": 2, + "deadline": "2026-10-27T17:07:39.820702Z" + } + }, + { + "role": "dual-role-expert-manager", + "method": "POST", + "path": "/programs/48/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [65] + }, + "response": { + "items": [ + { + "application": { + "kind": "solution", + "program_id": 48, + "solution_id": 65, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a" + }, + "solution_version": 5, + "is_submitted": true, + "is_archived": false, + "score_count": 4, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "errors": [] + } + ], + "score_count": 4, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "can_apply": true, + "preview_token": "" + } + }, + { + "role": "dual-role-expert-manager", + "method": "POST", + "path": "/programs/48/management/solutions/archive/apply/", + "status": 200, + "request": { + "ids": [65], + "preview_token": "" + }, + "response": { + "changed_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 48, + "solution_id": 65, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a" + }, + "solution_version": 6, + "archived_at": "2026-10-25T17:09:18.327974Z" + } + ] + } + }, + { + "role": "browser-expert", + "method": "POST", + "path": "/programs/48/solutions/65/evaluation/", + "status": 409, + "request": { + "expected_criteria_revision": "63e6f2a731b85b50a8ca1fb6a479c725e925135f7d0644f67835a39a91fe12ac", + "scores": [ + { + "criterion_id": 89, + "value": "" + }, + { + "criterion_id": 90, + "value": "2" + }, + { + "criterion_id": 91, + "value": "False" + }, + { + "criterion_id": 92, + "value": "Несохраненная оценка qa01ar-20261025-2d84767a" + } + ] + }, + "response": { + "code": "solution_archived", + "detail": "Решение снято с участия. Материалы доступны для просмотра.", + "errors": [], + "meta": {} + } + }, + { + "role": "browser-expert", + "method": "GET", + "path": "/programs/48/solutions/65/evaluation/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + } + }, + { + "role": "dual-role-expert-manager", + "method": "GET", + "path": "/programs/48/solution-evaluation/?status=completed", + "status": 200, + "request": null, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [], + "evaluation_counts": { + "pending": 0, + "completed": 0 + }, + "case_options": ["Город", "Образование"] + } + }, + { + "role": "other", + "method": "GET", + "path": "/programs/47/solutions/64/evaluation/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + } + }, + { + "role": "other", + "method": "POST", + "path": "/programs/47/solutions/64/evaluation/", + "status": 404, + "request": { + "expected_criteria_revision": "4e05c1aa16267989a173957781f526689a67669fc52c26790ae62fc771ce74f4", + "scores": [ + { + "criterion_id": 86, + "value": "2" + }, + { + "criterion_id": 87, + "value": "False" + }, + { + "criterion_id": 88, + "value": "Запрещенная запись" + } + ] + }, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + } + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/47/solutions/64/", + "status": 200, + "request": null, + "response": { + "id": 64, + "program_id": 47, + "version": 6, + "name": "Работа для оценки case_championship qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:40.016648Z", + "updated_at": "2026-10-25T17:09:11.219357Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T17:09:11.219357Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 29, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:40.021769Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 30, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:40.273637Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 306, + "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": 307, + "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": 308, + "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": 309, + "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 + } + } + ], + "revision": "75df2b206fd3312e54b034f8d9f032e97092e98b12d8af688a699f676600a778", + "template": "case_championship", + "program_id": 47, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 306 + }, + { + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a", + "field_id": 307 + }, + { + "value": 0, + "field_id": 308 + }, + { + "value": false, + "field_id": 309 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:40.548989Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + } + }, + { + "role": "other", + "method": "GET", + "path": "/programs/48/solutions/65/evaluation/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + } + }, + { + "role": "other", + "method": "POST", + "path": "/programs/48/solutions/65/evaluation/", + "status": 404, + "request": { + "expected_criteria_revision": "63e6f2a731b85b50a8ca1fb6a479c725e925135f7d0644f67835a39a91fe12ac", + "scores": [ + { + "criterion_id": 90, + "value": "2" + }, + { + "criterion_id": 91, + "value": "False" + }, + { + "criterion_id": 92, + "value": "Запрещенная запись" + } + ] + }, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + } + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/48/solutions/65/", + "status": 200, + "request": null, + "response": { + "id": 65, + "program_id": 48, + "version": 6, + "name": "Работа для оценки hackathon qa01ar-20261025-2d84767a", + "captain_id": 29, + "created_at": "2026-10-25T17:07:41.076815Z", + "updated_at": "2026-10-25T17:09:18.327974Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T17:09:18.327974Z", + "reason": "detached" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 29, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:41.081475Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 30, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T17:07:41.381325Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 310, + "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": 311, + "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": 312, + "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": 313, + "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": 314, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "2c2c11723594f7d313c4e646de339469ae81326fb7149fa403e55a5b6fa7a56b", + "template": "hackathon", + "program_id": 48, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 310 + }, + { + "value": "Материалы архивной проверки qa01ar-20261025-2d84767a", + "field_id": 311 + }, + { + "value": 0, + "field_id": 312 + }, + { + "value": false, + "field_id": 313 + }, + { + "value": "https://example.com/archived-rating/qa01ar-20261025-2d84767a", + "field_id": 314 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T17:07:39.820702Z", + "submitted_at": "2026-10-25T17:07:41.682257Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + } + } +] diff --git a/docs/qa/program-solutions-case-revision-20261025/README.md b/docs/qa/program-solutions-case-revision-20261025/README.md new file mode 100644 index 000000000..7d1a52b7b --- /dev/null +++ b/docs/qa/program-solutions-case-revision-20261025/README.md @@ -0,0 +1,13 @@ + + +# BE08A/FE API: история административной смены кейса + +25.10.2026. Product frontend74037240c7f10687b24b5627472e5225fb58a381, backend91fdbc3f6199d6e0b861caa23063766afd18a385. C001.17.0:131 schemas/51 operations/230 независимых сценариев; три общих файла byte-identical. [Модель и backend evidence](https://github.com/PROCOLLAB-github/api/blob/7656b472e263d5e28539a02ea44c2250944c6efb/docs/qa/program-solutions-case-revision-20261025/README.md). + +Generated DTO, port/repository/adapter и два use cases читают историю в контексте program/solution/change. Query limit/offset, null snapshots draft, actor/ref, старый/новый snapshot и ошибки сохраняются через настоящий interceptor/HttpTestingController. Некорректные IDs не отправляются в сеть; ошибок не сопровождает automatic retry. Strict types отделяют SolutionRef от Project и требуют эффекты case preview. + +49 case/history wire tests PASS, включая16 новых history tests. Backend проверяет unknown query у history detail; frontend detail client не принимает произвольные query, поэтому этот негативный сценарий не выдается за такой wire request. Полный frontend:2862 tests/481 files +strict C00 types, development build/scoped ESLint/locked Prettier3.8.3 по2020 source files PASS до добавления этого отчета. + +Настоящий backend HTTP:56 responses/23 request bodies по C00, КЧ35/решение51 и хакатон36/решение52. После сдачи и закрытия срока сохраняются материалы/0/false/состав/оценки, исходный snapshot доступен организатору в истории; снимается только несовместимое неоцененное назначение. Legacy context34 остается Project4/link3; новые solution paths там закрыты. Прежние22 выбранные доменные модели сохранены, новых Project=0. + +Это API integration, новый organizer layout/routes/SCSS не добавлялись. Новый интерфейс зависит от ранее запрошенного D02 approval. Исключение участников, удаление регистрации/архив заявки, остальные screens/полный QA01/REL01 остаются в объеме первого выпуска. Backend/миграция0035 предшествуют frontend; реальных программ/merge/deploy/переноса не было. 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 + } + ] +} 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 +} diff --git a/docs/qa/program-solutions-expert-revision-20261025/README.md b/docs/qa/program-solutions-expert-revision-20261025/README.md new file mode 100644 index 000000000..c5165ba5c --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/README.md @@ -0,0 +1,38 @@ + + +# QA01: изменение критериев и отзыв экспертной роли + +Найден и исправлен пропущенный отказ `not_program_expert`: при потере экспертной роли старая форма оставалась доступной для ввода, хотя сервер запрещал сохранение. Это воспроизведено настоящим POST 403 у синтетического пользователя с двумя ролями — организатора и эксперта. После исправления используется прежнее состояние «Решение недоступно», frame/ввод/предыдущие ответы очищаются. Права сервера и контракт не менялись; новый UI не добавлялся. + +Регрессия сначала была красной, после исправления все 9 component tests прошли. Final `npm run test:ci` — **2794 tests/479 files + strict C00 types PASS**; development build и scoped ESLint PASS. C00 остается 1.14.0, 125 schemas/47 operations/185 scenarios. Backend source 30c0d3e не менялся. + +## Реальный экспертный путь + +В КЧ использовано распределенное оценивание, в хакатоне — открытое; оба с required_evaluators=2. Организатор меняет критерии настоящим PUT во время локального ввода эксперта. Роль и время меняются только контролем новых fixtures; HTTP ответы не подменялись. Подтверждение один раз задержано перед отправкой, затем передано настоящему API. + +| Проверка | Evidence | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ER01: актуальная конфигурация | Manager PUT сохраняет ID числового/булева критериев, удаляет старый комментарий и добавляет обязательное число/новый комментарий. Expert POST со старой revision дает 409/criteria_changed без записи оценки. | +| ER02: сохранение ввода | После настоящего GET/rebase 0 и «Нет» остаются у совместимых ID/типов; старые 1000 символов доступны для копирования. Новое обязательное число пусто, проверка переводит фокус в него; удаленный ID не отправляется. | +| ER03: подтверждение | Новые 0/False/0/1000 отправлены с новой revision, один forwarded POST; pending блокирует кнопку/Escape. Сохраненные значения и собственные счетчики 1/1 подтверждены. | +| ER04: возврат | Сохраняются q/case/status; оцененное решение исчезает из pending и открывается из completed. Вторая вкладка команды сохраняет ввод. | +| ER05: потеря экспертной роли | До fix — настоящий 403/not_program_expert и старая форма; после — frame/материалы/controls скрыты в обеих формах. Роль организатора намеренно сохраняется, чтобы проверить именно этот код отказа. Membership восстанавливается контролем fixture, оценки не меняются. | +| ER06: позднее закрытие срока | После ввода 2 и открытия подтверждения срок закрыт на сервере. POST дает evaluation_closed, сохраненный 0 остается; несохраненная 2 доступна в «Предыдущих ответах». | +| ER07: изоляция ролей | Второй настоящий эксперт получает null по всем своим значениям, UI показывает «Не указано»; капитан не открывает экспертный frame. User ID 15 и Expert ID 5 не смешиваются. | +| ER08: состояние и геометрия | На 320/390/768/1440 проверены заполненная/сохраненная формы и отказ доступа; overflow нет. Часть screenshots просмотрена. Snapshot обеих заявок каждой программы неизменен, creation flag off, Project не создавался. | + +[После исправления](browser-after-checks.json) — 16 уникальных checkpoints двух форм. Программы 23/24, решения 34/36; соседние решения 35/37 не оценены. [Verifier](domain-and-contract-evidence.json): 52 C00 ответа/11 JSON запросов и 6 ответов прежнего management criteria API; дополнительно выполнены 8 read-only GET. Все 15 выбранных domain моделей до/после verifier совпали, прежние baseline строки обеих фаз не менялись. В БД ровно четыре собственных значения на проверенное решение, чужих оценок и старого удаленного критерия нет. + +## Происхождение и ограничения + +[До исправления](browser-before-checks.json) сохраняет настоящий product FAIL на КЧ программы 21; хакатон 22 был подготовлен, но baseline остановился на первом дефекте. Это не baseline обеих форм. + +Итог составлен из нескольких сохраненных запусков. Первый post-fix полностью проверил КЧ, затем harness ошибочно использовал страницу капитана для хакатона. Повтор завершил все основные проверки хакатона, но второму актеру не загрузился dynamic chunk при HTTP 200; причина не установлена, исходная JavaScript ошибка сохранена. Отдельный read-only прогон с настоящим login/returnUrl завершил изоляцию ролей обеих программ без сброса данных. У него 0 JavaScript errors; это не утверждение об отсутствии ошибок во всех попытках. Harness также уточнил ожидание onboarding modal и существующий placeholder «Не указано». Эти поправки не являются исправлениями продукта. + +Полные raw wire, FAIL/partial reports и screenshots сохранены локально в `design-artifacts/program-solutions-20261024/qa01-expert-revision`; [manifest](local-artifacts.json) содержит SHA256. В Git включены компактные checks и независимый verifier; credential manifests/JWT не включены. [Build before](build-before.json)/[after](build-after.json) меняют только API/WS origins одного generated chunk в копии build. Source environments, controls/layout/SCSS, D00/D01 и API сохранены. Remote avatars блокировались local network guard, screenshots могут отражать текущую вертикальную прокрутку. Полные desktop/a11y/zoom/production CDN не заявляются. + +## CI и оставшийся scope + +[PostgreSQL 30c0d3e](https://github.com/PROCOLLAB-github/api/actions/runs/40171399920) SUCCESS: Ran 39 locking/vacancy и Ran 1686 full tests, оба OK по логу. [General](https://github.com/PROCOLLAB-github/api/actions/runs/40171395732) SUCCESS, Tests/Lint: Ran 1686/OK (skipped=56). Первая загрузка General log завершилась TLS timeout; повтор того же run скачан и проверен, CI не перезапускался. + +API/данные не мигрировались. Feature PR остаются draft, merge/deploy и переключение реальных программ не выполнялись. D02/D03, административная политика после сдачи, конкурсная публикация, остальные интеграции и общие QA01/REL01 остаются открытыми. diff --git a/docs/qa/program-solutions-expert-revision-20261025/browser-after-checks.json b/docs/qa/program-solutions-expert-revision-20261025/browser-after-checks.json new file mode 100644 index 000000000..25b2a288f --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/browser-after-checks.json @@ -0,0 +1,107 @@ +{ + "status": "PASS", + "phase": "after", + "prefix": "qa01e-20261025-after-d445c996", + "checks": [ + { + "program": 23, + "check": "reconciled responsive", + "widths": [320, 390, 768, 1440] + }, + { + "program": 23, + "check": "saved responsive", + "widths": [320, 390, 768, 1440] + }, + { + "program": 23, + "check": "real manager criteria PUT / stale409 / rebase IDs-types / copy old1000 / new required focus / confirmed onePOST / saved0-False-0-1000", + "newRevision": "701bea49c2a29ed504f9d699d04dc35cb000411c5d433d4578f2076a82fb116a", + "savedCriterionIds": [39, 40, 45, 46] + }, + { + "program": 23, + "check": "return URL restores q/case/status and actual own counts1/1" + }, + { + "program": 23, + "check": "actual revoked expert role with manager retained clears expert frame", + "serverStatus": 403, + "serverCode": "not_program_expert", + "cleared": true, + "materialHeadingCount": 0, + "criterionControls": 0 + }, + { + "program": 23, + "check": "revoked responsive", + "widths": [320, 390, 768, 1440] + }, + { + "program": 23, + "check": "real deadline closes during confirmation; saved0 remains and unsaved2 available for copy" + }, + { + "program": 23, + "check": "second expert sees only empty own scores; captain cannot open expert frame", + "actualSecondValues": [null, null, null, null], + "captainHeading": "Решение недоступно" + }, + { + "program": 24, + "check": "reconciled responsive", + "widths": [320, 390, 768, 1440] + }, + { + "program": 24, + "check": "saved responsive", + "widths": [320, 390, 768, 1440] + }, + { + "program": 24, + "check": "real manager criteria PUT / stale409 / rebase IDs-types / copy old1000 / new required focus / confirmed onePOST / saved0-False-0-1000", + "newRevision": "7dd87f314b0e3494d680efa04bc27ba72edc1c0dfc8d9bf182f911647769d940", + "savedCriterionIds": [42, 43, 47, 48] + }, + { + "program": 24, + "check": "return URL restores q/case/status and actual own counts1/1" + }, + { + "program": 24, + "check": "actual revoked expert role with manager retained clears expert frame", + "serverStatus": 403, + "serverCode": "not_program_expert", + "cleared": true, + "materialHeadingCount": 0, + "criterionControls": 0 + }, + { + "program": 24, + "check": "revoked responsive", + "widths": [320, 390, 768, 1440] + }, + { + "program": 24, + "check": "real deadline closes during confirmation; saved0 remains and unsaved2 available for copy" + }, + { + "program": 24, + "check": "second expert sees only empty own scores; captain cannot open expert frame", + "actualSecondValues": [null, null, null, null], + "captainHeading": "Решение недоступно" + } + ], + "error": null, + "javascriptErrors": [], + "earlierAttemptJavaScriptErrors": [ + { + "role": "second", + "message": "Failed to fetch dynamically imported module: http://127.0.0.1:8881/chunk-VHK7KRDQ.js" + } + ], + "sourceArtifact": "browser-after-evidence.json", + "sourceRawSha256": "d42c4e107cc9ecd68c64b3486bd98f272f34016fa2ac016090d002c005becd8a", + "capturedResponses": 39, + "provenance": "Core scenarios completed in two preserved runs. The first harness switched to the captain page for the second program; the second completed its core checks but a secondary actor hit a dynamic chunk import failure despite HTTP200 (cause not established). This final read-only actor check uses real login with returnUrl and completes both programs without resetting data. Earlier errors are preserved separately." +} diff --git a/docs/qa/program-solutions-expert-revision-20261025/browser-before-checks.json b/docs/qa/program-solutions-expert-revision-20261025/browser-before-checks.json new file mode 100644 index 000000000..29cd9a920 --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/browser-before-checks.json @@ -0,0 +1,43 @@ +{ + "status": "FAIL", + "phase": "before", + "prefix": "qa01e-20261025-before-1c1271f7", + "checks": [ + { + "program": 21, + "check": "reconciled responsive", + "widths": [320, 390, 768, 1440] + }, + { + "program": 21, + "check": "saved responsive", + "widths": [320, 390, 768, 1440] + }, + { + "program": 21, + "check": "real manager criteria PUT / stale409 / rebase IDs-types / copy old1000 / new required focus / confirmed onePOST / saved0-False-0-1000", + "newRevision": "7714b944936fee9d47bf4ec2ce92f0338a83279232b4d701f80fe5615b2d0cc7", + "savedCriterionIds": [29, 30, 35, 36] + }, + { + "program": 21, + "check": "return URL restores q/case/status and actual own counts1/1" + }, + { + "program": 21, + "check": "actual revoked expert role with manager retained clears expert frame", + "serverStatus": 403, + "serverCode": "not_program_expert", + "cleared": false, + "materialHeadingCount": 1, + "criterionControls": 3 + } + ], + "error": "AssertionError [ERR_ASSERTION]: Actual403/not_program_expert retained the old expert frame after its role was revoked\n at C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-expert-revision-browser.cjs:53:548", + "javascriptErrors": [], + "earlierAttemptJavaScriptErrors": [], + "sourceArtifact": "browser-before-attempt-1792913652078.json", + "sourceRawSha256": "5f93fd2ac4eba20e51068d45fe5d9fa981cde5df0095f32490a588e22081d084", + "capturedResponses": 11, + "provenance": "Baseline invocation; startup attempts stored separately." +} diff --git a/docs/qa/program-solutions-expert-revision-20261025/build-after.json b/docs/qa/program-solutions-expert-revision-20261025/build-after.json new file mode 100644 index 000000000..db058fb49 --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/build-after.json @@ -0,0 +1,28 @@ +{ + "phase": "after", + "source": "Z:\\Документы\\PROCOLLAB-dev\\procollab-dev-program-solutions-fe01-20261024\\dist\\social_platform", + "target": "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-expert-revision-angular-after", + "frontendHead": "091d1ee11b64f5c1037a1a3213d66029355a05c1", + "modifiedGeneratedFiles": [ + { + "file": "chunk-CHX6T2XY.js", + "before": "4707c125db5485fa8b5eeb9e30e23c6f90759d9f2daf86b56a5a0b332da287dd", + "after": "d8252699c1d7f5986df9684f6f5b9be81943d068050e6537958be0680cac6380" + } + ], + "sourceHashes": [ + { + "file": "projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts", + "sha256": "c830f64f1916666228f613fe8daa328cf5fa1ad1a325030299801cafe4e85fbc" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html", + "sha256": "f9400085e4299ac705c93f455586a2e4973be31634f3a2cb286c748b95f5f326" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.ts", + "sha256": "8f59ea76b5ce19fb5cef7a40945f5dfd513b960c70ae679c6a964106247ae29f" + } + ], + "productEnvironmentUnchanged": true +} diff --git a/docs/qa/program-solutions-expert-revision-20261025/build-before.json b/docs/qa/program-solutions-expert-revision-20261025/build-before.json new file mode 100644 index 000000000..0418c4cb4 --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/build-before.json @@ -0,0 +1,28 @@ +{ + "phase": "before", + "source": "Z:\\Документы\\PROCOLLAB-dev\\procollab-dev-program-solutions-fe01-20261024\\dist\\social_platform", + "target": "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-expert-revision-angular-before", + "frontendHead": "091d1ee11b64f5c1037a1a3213d66029355a05c1", + "modifiedGeneratedFiles": [ + { + "file": "chunk-CHX6T2XY.js", + "before": "4707c125db5485fa8b5eeb9e30e23c6f90759d9f2daf86b56a5a0b332da287dd", + "after": "d8252699c1d7f5986df9684f6f5b9be81943d068050e6537958be0680cac6380" + } + ], + "sourceHashes": [ + { + "file": "projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts", + "sha256": "99a15748ee90b0af888cbdbc66d52b23726724bf8ef40491da90e11cbd664471" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html", + "sha256": "f9400085e4299ac705c93f455586a2e4973be31634f3a2cb286c748b95f5f326" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation-list.component.ts", + "sha256": "8f59ea76b5ce19fb5cef7a40945f5dfd513b960c70ae679c6a964106247ae29f" + } + ], + "productEnvironmentUnchanged": true +} diff --git a/docs/qa/program-solutions-expert-revision-20261025/domain-and-contract-evidence.json b/docs/qa/program-solutions-expert-revision-20261025/domain-and-contract-evidence.json new file mode 100644 index 000000000..855313e8e --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/domain-and-contract-evidence.json @@ -0,0 +1,130 @@ +{ + "status": "PASS", + "c00": "1.14.0", + "productFailureArtifact": "browser-before-attempt-1792913652078.json", + "beforeProductFailureObserved": true, + "afterUniqueBrowserChecks": 16, + "afterEarlierAttemptJavascriptErrors": [ + { + "role": "second", + "message": "Failed to fetch dynamically imported module: http://127.0.0.1:8881/chunk-VHK7KRDQ.js" + } + ], + "finalActorJavascriptErrors": [], + "validation": { + "c00Responses": 52, + "c00Requests": 11, + "existingCriteriaResponses": 6 + }, + "verificationGetRequests": 8, + "verificationEvents": [ + { + "role": "expert", + "path": "/programs/23/solutions/34/evaluation/", + "status": 200, + "responseSha256": "7075cb8753c12ab4c96923b319ae84b033c4ec61470f5638ab7922ea81809bc2" + }, + { + "role": "second", + "path": "/programs/23/solutions/34/evaluation/", + "status": 200, + "responseSha256": "4b9eccbaac99626b9334623071ab43bacaeaf22a3f2257eff8441cc13d2263ec" + }, + { + "role": "captain", + "path": "/programs/23/solutions/34/evaluation/", + "status": 403, + "responseSha256": "592c35e77c85ef4a7c4ae3ce75b5ed1bc27ad4fc8e05addc305486fdacfdf731" + }, + { + "role": "expert", + "path": "/programs/23/solution-evaluation/?search=%D0%A0%D0%B5%D0%B2%D0%B8%D0%B7%D0%B8%D1%8F&case_scope=selected&case_name=%D0%93%D0%BE%D1%80%D0%BE%D0%B4&status=completed&limit=20&offset=0", + "status": 200, + "responseSha256": "16789bf874cf198391805ed9f41ba34a3d7a1ba0993d8420e12cab20b23f178b" + }, + { + "role": "expert", + "path": "/programs/24/solutions/36/evaluation/", + "status": 200, + "responseSha256": "a7e71a9177e981c267485484768fbda1b855834eeb515ba3a451577e5a66e1db" + }, + { + "role": "second", + "path": "/programs/24/solutions/36/evaluation/", + "status": 200, + "responseSha256": "b03e31061171d04219087feb8d73c6dfb2fef904d1bef9cd40accbb07b8e7d31" + }, + { + "role": "captain", + "path": "/programs/24/solutions/36/evaluation/", + "status": 403, + "responseSha256": "592c35e77c85ef4a7c4ae3ce75b5ed1bc27ad4fc8e05addc305486fdacfdf731" + }, + { + "role": "expert", + "path": "/programs/24/solution-evaluation/?search=%D0%A0%D0%B5%D0%B2%D0%B8%D0%B7%D0%B8%D1%8F&case_scope=selected&case_name=%D0%93%D0%BE%D1%80%D0%BE%D0%B4&status=completed&limit=20&offset=0", + "status": 200, + "responseSha256": "86962ba2b8783f287f1c5e0d0dd5f21b22103eb851db82128787d853911a7aad" + } + ], + "programs": [ + { + "program": 23, + "template": "case_championship", + "distributed": true, + "solution": 34, + "expertUserId": 15, + "expertModelId": 5, + "criterionIds": [39, 40, 45, 46], + "scores": 4, + "zeroFalseAnd1000Preserved": true, + "secondOwnValuesNull": true, + "captainDenied": true, + "deadlineClosed": true, + "snapshotsUnchanged": true, + "creationFlagOff": true + }, + { + "program": 24, + "template": "hackathon", + "distributed": false, + "solution": 36, + "expertUserId": 15, + "expertModelId": 5, + "criterionIds": [42, 43, 47, 48], + "scores": 4, + "zeroFalseAnd1000Preserved": true, + "secondOwnValuesNull": true, + "captainDenied": true, + "deadlineClosed": true, + "snapshotsUnchanged": true, + "creationFlagOff": true + } + ], + "preexistingSelectedDomainUnchanged": true, + "baselineModels": [ + "partner_programs.PartnerProgram", + "partner_programs.PartnerProgramField", + "partner_programs.PartnerProgramUserProfile", + "partner_programs.ProgramSolution", + "partner_programs.ProgramSolutionAnswer", + "partner_programs.ProgramSolutionMember", + "partner_programs.SolutionInvitation", + "project_rates.SolutionScore", + "project_rates.SolutionExpertAssignment", + "project_rates.Criteria", + "users.Expert", + "files.UserFile", + "projects.Project", + "users.Expert_programs", + "partner_programs.PartnerProgram_managers" + ], + "verificationDomainMutations": 0, + "projectCountUnchanged": true, + "limits": [ + "Core expert checks completed in two explicit runs; final actor checks were read-only and used real login with returnUrl.", + "An earlier actor chunk import failed despite HTTP200; cause is not established. Earlier artifact is retained, not reported as a clean run.", + "Role membership and deadline were changed only by guarded fixture controls; manager criteria changes, score writes and readbacks were actual HTTP.", + "No new manager/public UI, production, comprehensive a11y or zoom checks." + ] +} diff --git a/docs/qa/program-solutions-expert-revision-20261025/local-artifacts.json b/docs/qa/program-solutions-expert-revision-20261025/local-artifacts.json new file mode 100644 index 000000000..dbac0c26a --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/local-artifacts.json @@ -0,0 +1,224 @@ +{ + "sourceDirectory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-expert-revision", + "backendSource": "30c0d3ec1ac8e1236dd669205b97b4a2b4463fda", + "frontendBase": "091d1ee11b64f5c1037a1a3213d66029355a05c1", + "finalComponentRawSha256": "c830f64f1916666228f613fe8daa328cf5fa1ad1a325030299801cafe4e85fbc", + "rawArtifacts": [ + { + "file": "actor-tail-error.png", + "bytes": 43255, + "sha256": "bcb01e7661118278f965fa53fccebf2b025a4f6fbd980d8285bbb592db5c28cf" + }, + { + "file": "after-case_championship-captain-denied-390.png", + "bytes": 17543, + "sha256": "c2274976a8bcf755c28051c9906acc92785f28626679edb3390d057d4e05fd55" + }, + { + "file": "after-case_championship-reconciled-1440.png", + "bytes": 113651, + "sha256": "adf06a84967680ca5b786db6ed347ad11310bee26e989df932feedd5f2312a48" + }, + { + "file": "after-case_championship-reconciled-390.png", + "bytes": 43743, + "sha256": "e0cc0c8d0096d3c4227c3f56bf70d4ede5a1749948e434d524cd7a106b60302f" + }, + { + "file": "after-case_championship-revoked-1440.png", + "bytes": 53574, + "sha256": "ee705c273e5bf2c6581709c522650d2b4c3ae4733be6f64cd91e5dd866d9333c" + }, + { + "file": "after-case_championship-revoked-390.png", + "bytes": 18171, + "sha256": "b15608ac0d4bf0de686b044a566558b7d5d0e5c89bb4d147b176e1d49d2a8220" + }, + { + "file": "after-case_championship-saved-1440.png", + "bytes": 114279, + "sha256": "fc61ebb3487a0d0b9953d3bfc10d38057220ac5b283a3d9328fe8f0142eb2c51" + }, + { + "file": "after-case_championship-saved-390.png", + "bytes": 22643, + "sha256": "ecf25f81056e451982b41e605c8f97ae06be18cff8e24416427656c80e483623" + }, + { + "file": "after-error.png", + "bytes": 15364, + "sha256": "e2d214d6fbdb67bd85fd3f05616620fba9573afcf16ce83f0f5db0810becff21" + }, + { + "file": "after-hackathon-captain-denied-390.png", + "bytes": 17543, + "sha256": "c2274976a8bcf755c28051c9906acc92785f28626679edb3390d057d4e05fd55" + }, + { + "file": "after-hackathon-reconciled-1440.png", + "bytes": 134639, + "sha256": "11726fc0bfacb9b216e1887f1953479e7eec85ef4c588930d439595d5366f210" + }, + { + "file": "after-hackathon-reconciled-390.png", + "bytes": 43678, + "sha256": "f9b82e6ef4c9387eb85150a31cef46fdf0ecc4e7f01ae3a2d58f2e3a91c92257" + }, + { + "file": "after-hackathon-revoked-1440.png", + "bytes": 53574, + "sha256": "ee705c273e5bf2c6581709c522650d2b4c3ae4733be6f64cd91e5dd866d9333c" + }, + { + "file": "after-hackathon-revoked-390.png", + "bytes": 18171, + "sha256": "b15608ac0d4bf0de686b044a566558b7d5d0e5c89bb4d147b176e1d49d2a8220" + }, + { + "file": "after-hackathon-saved-1440.png", + "bytes": 109137, + "sha256": "b74d909fa8f4ada84e203e9a7e57254d486ff77a97468495f2dbed88e5579d94" + }, + { + "file": "after-hackathon-saved-390.png", + "bytes": 22643, + "sha256": "ecf25f81056e451982b41e605c8f97ae06be18cff8e24416427656c80e483623" + }, + { + "file": "before-case_championship-reconciled-1440.png", + "bytes": 113561, + "sha256": "722d2952cafad834dfeb871c861e1f71418d86a5aaaba63bd37c625f1127e7a3" + }, + { + "file": "before-case_championship-reconciled-390.png", + "bytes": 43622, + "sha256": "4024a2824dae0d70ba69d37223de333ca32865e662b915149464cf7f3ecc3074" + }, + { + "file": "before-case_championship-revoked-390.png", + "bytes": 39543, + "sha256": "ec348c3d60c26f08b8888189060db57de63d76827749cd3cd9d6e2387690a5de" + }, + { + "file": "before-case_championship-saved-1440.png", + "bytes": 113828, + "sha256": "31d65a562336c80981ea16da4813b4fc7d24280c58de1648702795c50ee22c91" + }, + { + "file": "before-case_championship-saved-390.png", + "bytes": 22643, + "sha256": "ecf25f81056e451982b41e605c8f97ae06be18cff8e24416427656c80e483623" + }, + { + "file": "before-error.png", + "bytes": 38791, + "sha256": "5ff34502b18072adf8e60470ebe49d6ca009ada5da3865be1e475c853cedb84b" + }, + { + "file": "browser-after-attempt-1792913954144.json", + "bytes": 93770, + "sha256": "e806e70b4da62b5042ca822f20e2d8078d68bae26b6adf8104cb9e9a2a52650d" + }, + { + "file": "browser-after-attempt-1792914129851.json", + "bytes": 193701, + "sha256": "dc705d0d583473633114ca7da20d3eff8976864fc62d65e05e4abb7e8e62c4ac" + }, + { + "file": "browser-after-evidence.json", + "bytes": 246691, + "sha256": "d42c4e107cc9ecd68c64b3486bd98f272f34016fa2ac016090d002c005becd8a" + }, + { + "file": "browser-before-attempt-1792913652078.json", + "bytes": 57146, + "sha256": "5f93fd2ac4eba20e51068d45fe5d9fa981cde5df0095f32490a588e22081d084" + }, + { + "file": "browser-before-failure.json", + "bytes": 2446, + "sha256": "8f7a333e33f43e830082cc84989dfc1a662b8dac1ed39df59dc516f7ec0345fd" + }, + { + "file": "domain-and-contract-evidence.json", + "bytes": 4824, + "sha256": "780413ad889191ee8ddf8fc64fe7b95740d7c8e69e3cec04fdccb406fb697186" + }, + { + "file": "seed-after-evidence.json", + "bytes": 10045, + "sha256": "f86097b10bfa08b0c091962fe07721bbe32dbe4403dddd18d2dafbc2b4a361af" + }, + { + "file": "seed-before-evidence.json", + "bytes": 10046, + "sha256": "4603262eb0b8c6ddde1bb6f6d1b3892f19f7bb09343aef66947449079aa69ea2" + } + ], + "helpers": [ + { + "file": "procollab-expert-revision-seed.py", + "sha256": "f5fefe00a8e6d497888c3a24bd2c41cdc5f0eb3e38752576da076714e61246da" + }, + { + "file": "procollab-expert-revision-browser.cjs", + "sha256": "2e7a0cb322050cf671e4c77b6ae5d2ba2f532254ad96ad3a4ef7d86b09d47517" + }, + { + "file": "procollab-expert-revision-actors.cjs", + "sha256": "46f789bfdcceef2e9a9cf4098f309921ecfb008e624888764df4d8cb7fc79555" + }, + { + "file": "procollab-expert-revision-control.py", + "sha256": "cd7b6735f60436bbe988f896387cc095fa6a9bb240c3b7c054f21ed264bb92be" + }, + { + "file": "procollab-expert-revision-verify.py", + "sha256": "99b111fd9dcff0f5a931de713b01a895be92b7105d9248c6cbf98314d2af99ba" + } + ], + "logs": [ + { + "file": "procollab-expert-revision-unit-before.log", + "sha256": "bc5c10b83b98cb774dc7dc17d9e7c3faa209848c0626eeacefbf403b996e5d19" + }, + { + "file": "procollab-expert-revision-unit-after.log", + "sha256": "d05e1fd584fb211945a5661db9b063a1b0e273d0662f5ea9b94099b0018dc0e2" + }, + { + "file": "procollab-expert-revision-test-all.log", + "sha256": "613c4ba4bde235973695378d02529a2e4ea1a6fdcc59d2ceef65e5b8f1359a75" + }, + { + "file": "procollab-expert-revision-build-after.log", + "sha256": "4ff919f12400432191ca776f67001d7054a78576ceaca1323e682e1f5a6b18cd" + }, + { + "file": "procollab-expert-revision-actors.log", + "sha256": "11242b35dd8c11f75e412110507da4748df06850ed61cf4fb3ca333f7c4d0702" + }, + { + "file": "procollab-expert-revision-actors-final.log", + "sha256": "f839029226d32443ed2a9fd46af30393c93cf073e624278cd0ca290faecfeeed" + }, + { + "file": "procollab-ci-30c0d3e-pg.log", + "sha256": "6191dc732f1a837b767d4ac5b9acb99b30ee607f4bdd7a7d67f6595a3c27c72e" + }, + { + "file": "procollab-ci-30c0d3e-general.log", + "sha256": "f5d321bda53a8fd85330d94b5abd92c40d11b063a92b49e46777917ab77d4531" + } + ], + "credentialsIncluded": false, + "ci": { + "pgRun": 40171399920, + "generalRun": 40171395732, + "status": "SUCCESS", + "postgresFullTests": 1686, + "postgresLockingTests": 39, + "generalTests": 1686, + "generalSkipped": 56 + } +} diff --git a/docs/qa/program-solutions-expert-revision-20261025/seed-after-evidence.json b/docs/qa/program-solutions-expert-revision-20261025/seed-after-evidence.json new file mode 100644 index 000000000..3f3f36ac3 --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/seed-after-evidence.json @@ -0,0 +1,303 @@ +{ + "status": "PASS", + "prefix": "qa01e-20261025-after-d445c996", + "programs": [ + { + "id": 23, + "template": "case_championship", + "distributed": true, + "deadline": "2026-11-01T07:36:13.753456+00:00", + "solutions": [ + { + "id": 34, + "name": "Ревизия 1 case_championship", + "snapshotSha256": "0e5a462bdfa751c7ecff0de3413d6df777d1b6373b884fb34ecd3405a3a4d95c" + }, + { + "id": 35, + "name": "Ревизия 2 case_championship", + "snapshotSha256": "97fb73720c0ccdf0f0e5def8ace44385b62a80f0d44e5e03269cadd8f0755c53" + } + ], + "criteria": [ + { + "id": 39, + "name": "Качество", + "description": "Обоснованность решения", + "type": "int", + "min_value": 0.0, + "max_value": 10.0 + }, + { + "id": 40, + "name": "Реализуемость", + "description": "Возможность реализации", + "type": "bool", + "min_value": null, + "max_value": null + }, + { + "id": 41, + "name": "Комментарий", + "description": "Пояснение эксперта", + "type": "str", + "min_value": null, + "max_value": null + } + ], + "criteriaRevision": "aba9ef880aa8befe896bce5095d974f141c88b24e19341fbbf360880bd3f2504", + "expertModelId": 5, + "initialRatingRevision": "aba9ef880aa8befe896bce5095d974f141c88b24e19341fbbf360880bd3f2504" + }, + { + "id": 24, + "template": "hackathon", + "distributed": false, + "deadline": "2026-11-01T07:36:13.763572+00:00", + "solutions": [ + { + "id": 36, + "name": "Ревизия 1 hackathon", + "snapshotSha256": "381a1e4fbf2dde9fa69970a4d139d21a37851029aa0e832422f8054f291b350c" + }, + { + "id": 37, + "name": "Ревизия 2 hackathon", + "snapshotSha256": "a2e179330bad9a75f57b858a986daf97933d138e7103e9b8f4cdb7ad8b91a0fb" + } + ], + "criteria": [ + { + "id": 42, + "name": "Качество", + "description": "Обоснованность решения", + "type": "int", + "min_value": 0.0, + "max_value": 10.0 + }, + { + "id": 43, + "name": "Реализуемость", + "description": "Возможность реализации", + "type": "bool", + "min_value": null, + "max_value": null + }, + { + "id": 44, + "name": "Комментарий", + "description": "Пояснение эксперта", + "type": "str", + "min_value": null, + "max_value": null + } + ], + "criteriaRevision": "bac8dbc8fc339fe27740ab7590cd9035711f8a42ea94dbe89281367862b8c358", + "expertModelId": 5, + "initialRatingRevision": "bac8dbc8fc339fe27740ab7590cd9035711f8a42ea94dbe89281367862b8c358" + } + ], + "events": [ + { + "role": "manager", + "method": "GET", + "path": "/programs/23/management/solution-form/", + "status": 200, + "contract": "C00", + "responseSha256": "a36890322a8a67974b19c838649405f82e5752f18d71ac1d533586efe9c47d30" + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/23/management/solution-form/", + "status": 200, + "contract": "C00", + "responseSha256": "025668bb5956313b9c345aa12e26b72fb50a23fe217302b529c85df7df023a78" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/23/solutions/", + "status": 201, + "contract": "C00", + "responseSha256": "d66df4dc6e1a1efc047c0d81e30e70cbcbde0effeb73f096b8112c6b47318915" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/23/solutions/34/", + "status": 200, + "contract": "C00", + "responseSha256": "925b8e58978f81dc9c9d4c10116a2c98fea92db8d07df7ce3a72acc8fa0281e9" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/23/solutions/34/submit/", + "status": 200, + "contract": "C00", + "responseSha256": "63739278543ec91cd0a6e4cd91b586bb79675acbc9051173d5678e367e39e19f" + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/23/solutions/", + "status": 201, + "contract": "C00", + "responseSha256": "3f6731b40cdee9e6ea8c9f261ba2cce9fc50ae678e07e6ac7e9e2852aac46813" + }, + { + "role": "pending_a", + "method": "PATCH", + "path": "/programs/23/solutions/35/", + "status": 200, + "contract": "C00", + "responseSha256": "464e4233e4e0511f7c12b5bf88f281ead50e158e92df3d4cf0444afeed7e9764" + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/23/solutions/35/submit/", + "status": 200, + "contract": "C00", + "responseSha256": "216834a3a6d33de7a3364d581dbad267c78d43c016e89215a461806b2ed27d7a" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/23/management/criteria/", + "status": 200, + "contract": "existing management criteria", + "responseSha256": "f27585fd993fd702edd38d590ef2ea635d7b4af8311d08655acbfd6464cc34c4" + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/23/management/criteria/", + "status": 200, + "contract": "existing management criteria", + "responseSha256": "8cf5a76d1a599871f4bd9974e63628b2ac3ad5a5088b038b63c8eb89cc321ea5" + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/23/solutions/34/evaluation/", + "status": 200, + "contract": "C00", + "responseSha256": "8644d1501f804c531bd0b336064babbcbd638fa03074334e9ed585936564efba" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/24/management/solution-form/", + "status": 200, + "contract": "C00", + "responseSha256": "3f620b55d9d6e6abca4cb8e3d0c68735cfe4f8b8cb62afa0a64c7b553c26be37" + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/24/management/solution-form/", + "status": 200, + "contract": "C00", + "responseSha256": "6c5c852983ff6e5efa7f44b506ca91d8fcfde351e5df5f7f244c41da492cb66f" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/24/solutions/", + "status": 201, + "contract": "C00", + "responseSha256": "d874d44f525faca28fb7f4baa386b7f5d0ff9ecb2b755f20594f386b7765c62b" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/24/solutions/36/", + "status": 200, + "contract": "C00", + "responseSha256": "5e33249d9ddc6ff4a65f9a4b75c8f569ad1b2c3922a600bd66582b8c0b448cbf" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/24/solutions/36/submit/", + "status": 200, + "contract": "C00", + "responseSha256": "b540b11b3e86ddc2f9b0b749c4a3507a998926178715a1b22a88cad7bd4b218a" + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/24/solutions/", + "status": 201, + "contract": "C00", + "responseSha256": "0454ea1b9faab27bc76eb34f50f73666ba81a5a99a37ea449b366fa010006129" + }, + { + "role": "pending_a", + "method": "PATCH", + "path": "/programs/24/solutions/37/", + "status": 200, + "contract": "C00", + "responseSha256": "fb0c8572c6def1b415013913d6f02639d2309a92b5c9f689a11b0490bfc5459f" + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/24/solutions/37/submit/", + "status": 200, + "contract": "C00", + "responseSha256": "851bddf8a2188f1c2b0955ad809669c403678806b4e979c74249067ac037be02" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/24/management/criteria/", + "status": 200, + "contract": "existing management criteria", + "responseSha256": "2f21b5b853aa1209196d24b7d213cff75a56b55babce64661ecf97e607436b14" + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/24/management/criteria/", + "status": 200, + "contract": "existing management criteria", + "responseSha256": "19974c0db43c3eb427f10352dea5d073ecb263f1bcd50088a6172f813e6004e1" + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/24/solutions/36/evaluation/", + "status": 200, + "contract": "C00", + "responseSha256": "50b2d4468b42b853236f43dbab3460965473a28f6b1afb279563c2839dad6bf9" + } + ], + "preexistingDomainUnchanged": true, + "baselineModels": [ + "partner_programs.PartnerProgram", + "partner_programs.PartnerProgramField", + "partner_programs.PartnerProgramUserProfile", + "partner_programs.ProgramSolution", + "partner_programs.ProgramSolutionAnswer", + "partner_programs.ProgramSolutionMember", + "partner_programs.SolutionInvitation", + "project_rates.SolutionScore", + "project_rates.SolutionExpertAssignment", + "project_rates.Criteria", + "users.Expert", + "files.UserFile", + "projects.Project", + "users.Expert_programs", + "partner_programs.PartnerProgram_managers" + ], + "projectCountUnchanged": true, + "notes": [ + "Only fresh synthetic programs/accounts changed", + "Expert deliberately also manages these fixtures, so revoked expert membership yields actual not_program_expert instead of a hidden404", + "Assignments are fixture setup through guarded model FK; management criteria/solution writers are real HTTP", + "Verified owned PDF is reused; binary upload/CDN delivery is not repeated" + ] +} diff --git a/docs/qa/program-solutions-expert-revision-20261025/seed-before-evidence.json b/docs/qa/program-solutions-expert-revision-20261025/seed-before-evidence.json new file mode 100644 index 000000000..00133f94f --- /dev/null +++ b/docs/qa/program-solutions-expert-revision-20261025/seed-before-evidence.json @@ -0,0 +1,303 @@ +{ + "status": "PASS", + "prefix": "qa01e-20261025-before-1c1271f7", + "programs": [ + { + "id": 21, + "template": "case_championship", + "distributed": true, + "deadline": "2026-11-01T07:28:09.327209+00:00", + "solutions": [ + { + "id": 30, + "name": "Ревизия 1 case_championship", + "snapshotSha256": "ef4ca717077b4452c3d032a62a6a02d86863953063db27bc98bac89aea3497f3" + }, + { + "id": 31, + "name": "Ревизия 2 case_championship", + "snapshotSha256": "86d7ae0b1a218f7f31cd38abdbe0b0fa79b71432206434b805503b689640f7b1" + } + ], + "criteria": [ + { + "id": 29, + "name": "Качество", + "description": "Обоснованность решения", + "type": "int", + "min_value": 0.0, + "max_value": 10.0 + }, + { + "id": 30, + "name": "Реализуемость", + "description": "Возможность реализации", + "type": "bool", + "min_value": null, + "max_value": null + }, + { + "id": 31, + "name": "Комментарий", + "description": "Пояснение эксперта", + "type": "str", + "min_value": null, + "max_value": null + } + ], + "criteriaRevision": "6a6512c4f3a3192547a09bc54a5ee56fe47547398989e79580bf1064aa5f5a72", + "expertModelId": 3, + "initialRatingRevision": "6a6512c4f3a3192547a09bc54a5ee56fe47547398989e79580bf1064aa5f5a72" + }, + { + "id": 22, + "template": "hackathon", + "distributed": false, + "deadline": "2026-11-01T07:28:09.339436+00:00", + "solutions": [ + { + "id": 32, + "name": "Ревизия 1 hackathon", + "snapshotSha256": "47af63d4503fad0b166dbcb31ceee0f91d23894d1fbb10374ae3faee9d81828c" + }, + { + "id": 33, + "name": "Ревизия 2 hackathon", + "snapshotSha256": "c2ffd3f50445c0134de94509fe7133285413954c52027e70c8c4eeb12446822a" + } + ], + "criteria": [ + { + "id": 32, + "name": "Качество", + "description": "Обоснованность решения", + "type": "int", + "min_value": 0.0, + "max_value": 10.0 + }, + { + "id": 33, + "name": "Реализуемость", + "description": "Возможность реализации", + "type": "bool", + "min_value": null, + "max_value": null + }, + { + "id": 34, + "name": "Комментарий", + "description": "Пояснение эксперта", + "type": "str", + "min_value": null, + "max_value": null + } + ], + "criteriaRevision": "d29a4aebacc499f39d469a2147fb548c67d4e6205548e8d043c0b8f493a96806", + "expertModelId": 3, + "initialRatingRevision": "d29a4aebacc499f39d469a2147fb548c67d4e6205548e8d043c0b8f493a96806" + } + ], + "events": [ + { + "role": "manager", + "method": "GET", + "path": "/programs/21/management/solution-form/", + "status": 200, + "contract": "C00", + "responseSha256": "2e843e211787a92047eb935528bfffbda90a75079e8d49f9b838d03a00158dda" + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/21/management/solution-form/", + "status": 200, + "contract": "C00", + "responseSha256": "eae8e335823786c41dcd355d171dec165c5f921358b7d2285db42405cc15ca4c" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/21/solutions/", + "status": 201, + "contract": "C00", + "responseSha256": "cdf0016797c240796dde6489e67edc7a8b8bb3824d2c18db2021cf0838d968d3" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/21/solutions/30/", + "status": 200, + "contract": "C00", + "responseSha256": "b50db81e5ee447a38b543607b9d9c90a0e7b7a3fffe41e14d769e2556d6a9d7f" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/21/solutions/30/submit/", + "status": 200, + "contract": "C00", + "responseSha256": "0d5c04813ddf865c4c9b3d17a82339d129671d9a6c764d688d136114548be5fc" + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/21/solutions/", + "status": 201, + "contract": "C00", + "responseSha256": "049efb5f3e712c29d825c9175acf3bf266cc57788b9f5228029cdb30b2046d5f" + }, + { + "role": "pending_a", + "method": "PATCH", + "path": "/programs/21/solutions/31/", + "status": 200, + "contract": "C00", + "responseSha256": "742e02fd8cd72785a40fb53589655f87a7401483cae0fbda6033f9e5d28605f4" + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/21/solutions/31/submit/", + "status": 200, + "contract": "C00", + "responseSha256": "b91348f4619ca692a9cf4d8ba81a9c09e55eec20a3ecfa53745e4c3394a717df" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/21/management/criteria/", + "status": 200, + "contract": "existing management criteria", + "responseSha256": "b66d1c3d2c6fdfa725a5c7997b3f159d91a4d61080913b8c2dc2526a86f089e9" + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/21/management/criteria/", + "status": 200, + "contract": "existing management criteria", + "responseSha256": "bbcc1e3712365f6bd4ae547176fc9338d0bc1bbcfbe266596877ebe8df27331c" + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/21/solutions/30/evaluation/", + "status": 200, + "contract": "C00", + "responseSha256": "cdb732520b0339c5abfa9baee79e9491bade7a5604dc1da4b1386689f780654e" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/22/management/solution-form/", + "status": 200, + "contract": "C00", + "responseSha256": "c7b1c0a69794355e23c71cfc5cc0a7fa37337b5d6766701d91e03bb064ac4f18" + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/22/management/solution-form/", + "status": 200, + "contract": "C00", + "responseSha256": "5a6ecd4274ef26461d27cc5cc8ea8d28cd52fa11c4cd86dc28018e470d7c19a0" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/22/solutions/", + "status": 201, + "contract": "C00", + "responseSha256": "ce6c4439d00b9354bbd45274bb597d2c109c6e7a95ab142b36b356ef739e2986" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/22/solutions/32/", + "status": 200, + "contract": "C00", + "responseSha256": "241a2088ca990362a1d272be9b5976363deec01d6f0f7162e65e079519515aa1" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/22/solutions/32/submit/", + "status": 200, + "contract": "C00", + "responseSha256": "71d80ddc643941738d24da2f20768bd3e4370f033c348e56c5c4a6a546666b18" + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/22/solutions/", + "status": 201, + "contract": "C00", + "responseSha256": "c1682e0a5ba2cd87639280c2551e0e18909db84d10df3e6d8bba48d7ce3ffedd" + }, + { + "role": "pending_a", + "method": "PATCH", + "path": "/programs/22/solutions/33/", + "status": 200, + "contract": "C00", + "responseSha256": "3cc1393787646b637556e25e64965627de06419b36caf91551a109658a300ff6" + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/22/solutions/33/submit/", + "status": 200, + "contract": "C00", + "responseSha256": "e47956e1a4630e8bc8742ef6eaf5b95992015a7cb414c78d779ba6025e7686bc" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/22/management/criteria/", + "status": 200, + "contract": "existing management criteria", + "responseSha256": "4b4cd80e8450a28b5aaa83921619854df761368aa907f2d6d71b9d43630e8450" + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/22/management/criteria/", + "status": 200, + "contract": "existing management criteria", + "responseSha256": "b599a8bf0465c5de8ab0d286d1b68795f781c94f53298676135643f7150f8a69" + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/22/solutions/32/evaluation/", + "status": 200, + "contract": "C00", + "responseSha256": "96912cca4a2b280db15d4b93bd9fca0ba394f8f10ea2c330138c494cb3c540aa" + } + ], + "preexistingDomainUnchanged": true, + "baselineModels": [ + "partner_programs.PartnerProgram", + "partner_programs.PartnerProgramField", + "partner_programs.PartnerProgramUserProfile", + "partner_programs.ProgramSolution", + "partner_programs.ProgramSolutionAnswer", + "partner_programs.ProgramSolutionMember", + "partner_programs.SolutionInvitation", + "project_rates.SolutionScore", + "project_rates.SolutionExpertAssignment", + "project_rates.Criteria", + "users.Expert", + "files.UserFile", + "projects.Project", + "users.Expert_programs", + "partner_programs.PartnerProgram_managers" + ], + "projectCountUnchanged": true, + "notes": [ + "Only fresh synthetic programs/accounts changed", + "Expert deliberately also manages these fixtures, so revoked expert membership yields actual not_program_expert instead of a hidden404", + "Assignments are fixture setup through guarded model FK; management criteria/solution writers are real HTTP", + "Verified owned PDF is reused; binary upload/CDN delivery is not repeated" + ] +} diff --git a/docs/qa/program-solutions-final-amendments-20261026/README.md b/docs/qa/program-solutions-final-amendments-20261026/README.md new file mode 100644 index 000000000..4918a9098 --- /dev/null +++ b/docs/qa/program-solutions-final-amendments-20261026/README.md @@ -0,0 +1,40 @@ + + +# Финальные правки заявок и публичных решений + +Исторический checkpoint финальных UI поправок. Последующая интеграция и итог QA01/REL01 описаны в [приемке первого выпуска](../program-solutions-release-acceptance-20261026/README.md); открытые статусы ниже относятся к состоянию до этой приемки. + +Пользователь26.10.2026 утвердил оставшийся дизайн заявок с тремя поправками и прямо указал не ждать следующего согласования. Этот checkpoint реализует их в Angular. Общий первый выпуск остается в работе: AC18/AC19 не закрыты, AC21 относится к отдельному BE10. PR остается draft. + +## Поведение + +- Только у solution-программы прежний пункт управления «Проекты» называется «Заявки». Порядок семи разделов сохраняется. Новая таблица использует существующие management styles, RowActions, поиск, фильтры, выбор и пагинацию; содержит восемь колонок без «Презентации». +- При открытии меню строки загружаются действительные материалы решения. Сохраненная презентация открывается прямой безопасной ссылкой в новой вкладке. Отсутствующий файл не подменяется ссылкой на Project. Название решения открывает материалы, «Посмотреть команду» — вторую вкладку заявки. +- Смена кейса содержит выбор, краткое объяснение сохранения материалов/команды/оценок, число снимаемых назначений без оценок и подтверждение на одном экране. Preview запрашивается автоматически. Применяется только актуальный подписанный токен для выбранного решения и кейса; ошибка сбрасывает токен, сохраняя выбор. Отдельного экрана/кнопки просмотра последствий нет. +- Подключены реальные list/filter/XLSX, readonly материалы/команда/история, preview/apply сдачи/архива/исключения участника и назначения по выбранным solution IDs. Компоненты не создают отдельную сущность команды и не подставляют solution IDs в Project API. Из общего expert workspace используются только roster/revision; назначения идут через solution allocation API. +- Публичный пункт «Решения» находится в существующем месте «проекты-участники» после разделителя действий программы. Старый условный Project branch сохранен. Шапка использует существующий Detail widget. Два точных read-only маршрута опубликованных решений доступны без office shell и аккаунта; остальные office routes продолжают использовать AuthRequiredGuard. +- Материалы и принятый состав читаются исключительно из published allowlist API. Приватный редактор не используется как fallback. Неконкурсные сохраненные решения доступны по существующему publisher; правила конкурсных несданных черновиков не менялись. + +Контракт остается C00 **1.21.0 /156 schemas /59 operations /305 scenarios**. Backend, миграции, данные существующих программ и режимы хранения не менялись. В этом checkpoint нет merge/deploy/переключения реальных программ. + +## Проверки и границы evidence + +**Checked by source:** условные solution branches в management/header; прежняя Project table и ее презентация сохранены; остальные разделы/настройки не перерабатываются. Предварительное получение management context исключает вызов старого projects API для solution-программы. Методы API используют existing ports/use cases и CamelcaseInterceptor. + +**Checked by tests:** полный feature-worktree запуск `npm run test:ci`: **2986 tests /489 files PASS**, затем strict C00 types PASS. Последующая точечная проверка после исправления возврата фокуса: **43 tests /7 files PASS**. Counts разных запусков не складываются. Предыдущие2987 prospective tests имели другой source scope с dev certificate changes. Проверены отмена устаревших ответов, несовпадающие program/solution IDs, stale tokens, отсутствие повторной записи,0/false, экспорт с теми же фильтрами, архив без writers, native menu href и сохранение auth guard у пяти приватных/legacy URL. + +**Checked by build:** Angular development build PASS; существующие Sass deprecation warnings сохранены. ESLint измененных TypeScript и Stylelint двух новых SCSS PASS; Prettier/diff checks выполняются для точного checkpoint. + +**Checked in browser:** [browser receipt](browser-evidence.json),22 checks,0 page errors. Использован новый Angular build, реальные HTTP к локальному Django fdec3dc/PostgreSQL, fresh headless Edge context. Static build bytes отдавались без изменения через локальный browser route; это не remote DEV deployment. Публичный путь без аккаунта прошел list→materials→team→list,0/false сохранены. Management проверен на320/390/768/1440px, включая selected bar, одну модалку кейса, команду и прямую презентацию из меню. Несданная заявка отклонена сервером при preview назначения; запрос содержит solution IDs. Доменный apply в браузере не выполнялся; два POST — read-only case/allocation previews. + +[XLSX receipt](xlsx-evidence.json): реальная выгрузка имеет тот же **solution ID70** и **count1**, что отфильтрованный список. Проверены строки команды, число0 и ответ false. Это точечная интеграция на существующей синтетической fixture, которая сама по себе не закрывает полный AC19. + +[Prototype receipt](prototype-evidence.json):100 layout/state checks,8 screenshots,0 errors/denied requests. Сравнение сохраняет девять колонок старой таблицы и восемь новой. Это отдельный synthetic scope; он не заменяет Angular/API evidence. + +**Not checked:** новая полная цепочка административного apply/архива/исключения через браузер; новое end-to-end опубликование конкурсных КЧ/хакатона; полный expert→organizer/results путь; все manager analytics/results/reminders и единая многоролевая QA01/REL01. Backend unit/CI и существующие прошлые rehearsals имеют отдельные refs. Новых правил доступа или eligibility из отсутствующих проверок не выводится. + +## Review + +Финальные три поправки прослежены до route/template/use-case/API. Исправлены найденные проблемы: доступ внешнего посетителя за office guard, исчезающее поле поиска при loading, использование общего modal viewer вместо прямой ссылки презентации, потеря фокуса после удаления строки и неподключенные native control styles. Старые ветки сохраняются. Действующий signed preview/apply протокол, серверные права, ревизии и история не заменяются frontend проверками. + +Текущие поправки готовы к ревью в существующем draft PR. Общий выпуск **не READY TO MERGE**: необходимы оставшиеся manager analytics/results/reminders, интеграция AC18/AC19 и полный QA01/REL01. Повторное согласование этих поправок не требуется. diff --git a/docs/qa/program-solutions-final-amendments-20261026/browser-evidence.json b/docs/qa/program-solutions-final-amendments-20261026/browser-evidence.json new file mode 100644 index 000000000..a81b6222a --- /dev/null +++ b/docs/qa/program-solutions-final-amendments-20261026/browser-evidence.json @@ -0,0 +1,306 @@ +{ + "checkedAt": "2026-10-25T22:44:57.802Z", + "source": "current Angular development build; real local Django fdec3dc and PostgreSQL; static bytes served verbatim through local route", + "checks": [ + "public-list:320", + "public-list:390", + "public-list:768", + "public-list:1440", + "public-team:320", + "public-team:390", + "public-team:768", + "public-team:1440", + "anonymous-public-header-list-detail-team-roundtrip-no-private-solution-api", + "manager-applications:320", + "manager-applications:390", + "manager-applications:768", + "manager-applications:1440", + "case-one-screen-server-preview-no-apply-no-second-step", + "manager-team-second-tab", + "manager-selection:320", + "manager-selection:390", + "manager-selection:768", + "manager-selection:1440", + "allocation-draft-rejected-by-server-solution-ids-only", + "presentation-menu-is-direct-file-link", + "same-server-field-zero-filter-manager-list-xlsx" + ], + "wire": [ + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/?search=&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/70/", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/51/published-solutions/?search=&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/management/solution-form/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/management/solution-analytics/solutions/?submission=all&case_scope=all&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/solutions/67/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/49/management/solutions/67/case/preview/", + "status": 200, + "request": { + "case_name": "Образование" + } + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/solutions/67/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/solutions/67/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/management/solution-analytics/solutions/?submission=all&case_scope=all&offset=0&limit=100", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/49/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/49/management/solution-evaluation/allocation/", + "status": 400, + "request": { + "revision": "91b655b43ff67d0077701a4764cb64e71884028a021accb587ee7e57c16c6c0a", + "expert_ids": [12], + "solution_ids": [67] + } + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/51/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/51/management/solution-form/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/51/management/solution-analytics/solutions/?submission=all&case_scope=all&field_id=328&answer_state=present&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/51/solutions/70/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/51/management/solution-analytics/export/?kind=solutions&submission=all&case_scope=all&field_id=328&answer_state=present", + "status": 200, + "request": null + } + ], + "errors": [], + "ignoredExternalCount": 19, + "mutations": [ + "/programs/49/management/solutions/67/case/preview/", + "/programs/49/management/solution-evaluation/allocation/" + ], + "export": { + "count": 1, + "solutionIds": [70], + "sha256": "060a71b6ae250cf72697c1e69f8c9299fc30dcd625dcb6a10255a252807a2bf8" + } +} diff --git a/docs/qa/program-solutions-final-amendments-20261026/prototype-evidence.json b/docs/qa/program-solutions-final-amendments-20261026/prototype-evidence.json new file mode 100644 index 000000000..538ad6497 --- /dev/null +++ b/docs/qa/program-solutions-final-amendments-20261026/prototype-evidence.json @@ -0,0 +1,600 @@ +{ + "status": "PASS", + "scope": "Synthetic final amendments only; separate actual Angular/API evidence exists", + "checks": [ + { + "surface": "manager", + "format": "kch", + "width": 320 + }, + { + "surface": "manager", + "format": "kch", + "width": 390 + }, + { + "surface": "manager", + "format": "kch", + "width": 768 + }, + { + "surface": "manager", + "format": "kch", + "width": 1440 + }, + { + "surface": "manager", + "format": "hack", + "width": 320 + }, + { + "surface": "manager", + "format": "hack", + "width": 390 + }, + { + "surface": "manager", + "format": "hack", + "width": 768 + }, + { + "surface": "manager", + "format": "hack", + "width": 1440 + }, + { + "surface": "manager", + "format": "noncompetitive", + "width": 320 + }, + { + "surface": "manager", + "format": "noncompetitive", + "width": 390 + }, + { + "surface": "manager", + "format": "noncompetitive", + "width": 768 + }, + { + "surface": "manager", + "format": "noncompetitive", + "width": 1440 + }, + { + "surface": "manager-state", + "state": "loading", + "width": 320 + }, + { + "surface": "manager-state", + "state": "loading", + "width": 390 + }, + { + "surface": "manager-state", + "state": "loading", + "width": 768 + }, + { + "surface": "manager-state", + "state": "loading", + "width": 1440 + }, + { + "surface": "manager-state", + "state": "empty", + "width": 320 + }, + { + "surface": "manager-state", + "state": "empty", + "width": 390 + }, + { + "surface": "manager-state", + "state": "empty", + "width": 768 + }, + { + "surface": "manager-state", + "state": "empty", + "width": 1440 + }, + { + "surface": "manager-state", + "state": "no-results", + "width": 320 + }, + { + "surface": "manager-state", + "state": "no-results", + "width": 390 + }, + { + "surface": "manager-state", + "state": "no-results", + "width": 768 + }, + { + "surface": "manager-state", + "state": "no-results", + "width": 1440 + }, + { + "surface": "manager-state", + "state": "error", + "width": 320 + }, + { + "surface": "manager-state", + "state": "error", + "width": 390 + }, + { + "surface": "manager-state", + "state": "error", + "width": 768 + }, + { + "surface": "manager-state", + "state": "error", + "width": 1440 + }, + { + "surface": "manager-state", + "state": "no-access", + "width": 320 + }, + { + "surface": "manager-state", + "state": "no-access", + "width": 390 + }, + { + "surface": "manager-state", + "state": "no-access", + "width": 768 + }, + { + "surface": "manager-state", + "state": "no-access", + "width": 1440 + }, + { + "surface": "manager-state", + "state": "long", + "width": 320 + }, + { + "surface": "manager-state", + "state": "long", + "width": 390 + }, + { + "surface": "manager-state", + "state": "long", + "width": 768 + }, + { + "surface": "manager-state", + "state": "long", + "width": 1440 + }, + { + "surface": "manager-state", + "state": "ready-draft", + "width": 320 + }, + { + "surface": "manager-state", + "state": "ready-draft", + "width": 390 + }, + { + "surface": "manager-state", + "state": "ready-draft", + "width": 768 + }, + { + "surface": "manager-state", + "state": "ready-draft", + "width": 1440 + }, + { + "surface": "public", + "format": "kch", + "width": 320 + }, + { + "surface": "public", + "format": "kch", + "width": 390 + }, + { + "surface": "public", + "format": "kch", + "width": 768 + }, + { + "surface": "public", + "format": "kch", + "width": 1440 + }, + { + "surface": "public", + "format": "hack", + "width": 320 + }, + { + "surface": "public", + "format": "hack", + "width": 390 + }, + { + "surface": "public", + "format": "hack", + "width": 768 + }, + { + "surface": "public", + "format": "hack", + "width": 1440 + }, + { + "surface": "public", + "format": "noncompetitive", + "width": 320 + }, + { + "surface": "public", + "format": "noncompetitive", + "width": 390 + }, + { + "surface": "public", + "format": "noncompetitive", + "width": 768 + }, + { + "surface": "public", + "format": "noncompetitive", + "width": 1440 + }, + { + "surface": "public-state", + "format": "kch", + "state": "loading", + "width": 390 + }, + { + "surface": "public-state", + "format": "kch", + "state": "loading", + "width": 1440 + }, + { + "surface": "public-state", + "format": "kch", + "state": "empty", + "width": 390 + }, + { + "surface": "public-state", + "format": "kch", + "state": "empty", + "width": 1440 + }, + { + "surface": "public-state", + "format": "kch", + "state": "no-results", + "width": 390 + }, + { + "surface": "public-state", + "format": "kch", + "state": "no-results", + "width": 1440 + }, + { + "surface": "public-state", + "format": "kch", + "state": "error", + "width": 390 + }, + { + "surface": "public-state", + "format": "kch", + "state": "error", + "width": 1440 + }, + { + "surface": "public-state", + "format": "kch", + "state": "not-found", + "width": 390 + }, + { + "surface": "public-state", + "format": "kch", + "state": "not-found", + "width": 1440 + }, + { + "surface": "public-state", + "format": "kch", + "state": "detail-error", + "width": 390 + }, + { + "surface": "public-state", + "format": "kch", + "state": "detail-error", + "width": 1440 + }, + { + "surface": "public-state", + "format": "kch", + "state": "partial", + "width": 390 + }, + { + "surface": "public-state", + "format": "kch", + "state": "partial", + "width": 1440 + }, + { + "surface": "public-state", + "format": "kch", + "state": "long", + "width": 390 + }, + { + "surface": "public-state", + "format": "kch", + "state": "long", + "width": 1440 + }, + { + "surface": "public-state", + "format": "hack", + "state": "loading", + "width": 390 + }, + { + "surface": "public-state", + "format": "hack", + "state": "loading", + "width": 1440 + }, + { + "surface": "public-state", + "format": "hack", + "state": "empty", + "width": 390 + }, + { + "surface": "public-state", + "format": "hack", + "state": "empty", + "width": 1440 + }, + { + "surface": "public-state", + "format": "hack", + "state": "no-results", + "width": 390 + }, + { + "surface": "public-state", + "format": "hack", + "state": "no-results", + "width": 1440 + }, + { + "surface": "public-state", + "format": "hack", + "state": "error", + "width": 390 + }, + { + "surface": "public-state", + "format": "hack", + "state": "error", + "width": 1440 + }, + { + "surface": "public-state", + "format": "hack", + "state": "not-found", + "width": 390 + }, + { + "surface": "public-state", + "format": "hack", + "state": "not-found", + "width": 1440 + }, + { + "surface": "public-state", + "format": "hack", + "state": "detail-error", + "width": 390 + }, + { + "surface": "public-state", + "format": "hack", + "state": "detail-error", + "width": 1440 + }, + { + "surface": "public-state", + "format": "hack", + "state": "partial", + "width": 390 + }, + { + "surface": "public-state", + "format": "hack", + "state": "partial", + "width": 1440 + }, + { + "surface": "public-state", + "format": "hack", + "state": "long", + "width": 390 + }, + { + "surface": "public-state", + "format": "hack", + "state": "long", + "width": 1440 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "loading", + "width": 390 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "loading", + "width": 1440 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "empty", + "width": 390 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "empty", + "width": 1440 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "no-results", + "width": 390 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "no-results", + "width": 1440 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "error", + "width": 390 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "error", + "width": 1440 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "not-found", + "width": 390 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "not-found", + "width": 1440 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "detail-error", + "width": 390 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "detail-error", + "width": 1440 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "partial", + "width": 390 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "partial", + "width": 1440 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "long", + "width": 390 + }, + { + "surface": "public-state", + "format": "noncompetitive", + "state": "long", + "width": 1440 + } + ], + "sourceHeaderParity": true, + "legacyNineColumnsSolutionEightColumns": true, + "rowMenuParity": true, + "teamSecondTab": true, + "filtersModal": true, + "archiveInsideExistingFilters": true, + "archiveReadonlyAndNoExport": true, + "administrativeCasePreviewInlineOneScreen": true, + "publicEntryInsideProgram": true, + "publicRoundtrip": true, + "errors": [], + "denied": [], + "shots": [ + { + "name": "manager-applications-1440.png", + "sha256": "9a8a1bd6b835710c0b998482f319976ce22987d05b7c86ec072669b4e36a8598" + }, + { + "name": "manager-existing-projects-1440.png", + "sha256": "93bf0b4fb2de0bd11efa69d9451db0fd06a3f3912fca6b2bb7d49033dd1e3a05" + }, + { + "name": "manager-row-menu-1440.png", + "sha256": "fab9002942c8b5a6da7232817f19d9a034147c12863b2fd0f5d8618576953aed" + }, + { + "name": "manager-applications-390.png", + "sha256": "4018a54f9436042ed4e76f1b071b73ceebdf55d9166a775caf7a444cc1c17dd2" + }, + { + "name": "program-public-entry-1440.png", + "sha256": "ea447718e77148f09b63091a1778d3f2dda747ed30f504799a6f1da081047082" + }, + { + "name": "program-public-solutions-1440.png", + "sha256": "6bb1dda52e3bcce9b9edb8acdcd9cb9118b026a00a581370caf43623f55a12c1" + }, + { + "name": "program-public-entry-390.png", + "sha256": "b03df6623b41f87238dc1748893bc0b066c310201a91f4f81dcac0540c4d4a7f" + }, + { + "name": "program-public-solutions-390.png", + "sha256": "cf583a7d6dfd36c0278fa4053c57766263197cc587684d331084061681b4b99d" + } + ] +} diff --git a/docs/qa/program-solutions-final-amendments-20261026/source-manifest.json b/docs/qa/program-solutions-final-amendments-20261026/source-manifest.json new file mode 100644 index 000000000..11a4762dc --- /dev/null +++ b/docs/qa/program-solutions-final-amendments-20261026/source-manifest.json @@ -0,0 +1,187 @@ +{ + "baseHead": "8bf663c7db666ee447cd26742871e66d1b52cb2f", + "observedDev": "08414b794dd5b34de5471dddaa19cd0af7f4a167", + "files": [ + { + "path": "projects/social_platform/src/app/api/paths/app-routes.ts", + "sha256Raw": "f4b6d42369fdde655aa4dfea716e6bbec41103e6a1068ad61804de36898f7fb5", + "sha256LF": "f4b6d42369fdde655aa4dfea716e6bbec41103e6a1068ad61804de36898f7fb5", + "gitBlob": "857e99042a2976db541cbe585e3fe7f7c6ff2cd7" + }, + { + "path": "projects/social_platform/src/app/api/program/facades/detail/program-management.service.spec.ts", + "sha256Raw": "3c4f5b51f7ba34c59d1236c6fd615babe2e072b3f7879357d0391b0492e3b55b", + "sha256LF": "3c4f5b51f7ba34c59d1236c6fd615babe2e072b3f7879357d0391b0492e3b55b", + "gitBlob": "45671cec7500a3391bc5dc97eeabc2a501863f3e" + }, + { + "path": "projects/social_platform/src/app/api/program/facades/detail/program-management.service.ts", + "sha256Raw": "64b0b7796aac50cb115af44200f523c9ce26d6b90ca0c65c8932d2f1076fc870", + "sha256LF": "64b0b7796aac50cb115af44200f523c9ce26d6b90ca0c65c8932d2f1076fc870", + "gitBlob": "4c3984f2178c6c7d3f1902c455c7785558974dff" + }, + { + "path": "projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts", + "sha256Raw": "155598550cd8a1bc0927ef39e25713e417527f3e6afdf3f3dc2f4768fd3d1984", + "sha256LF": "155598550cd8a1bc0927ef39e25713e417527f3e6afdf3f3dc2f4768fd3d1984", + "gitBlob": "e5377e4d41c4adb052938e0d2e542ca4d05f40d1" + }, + { + "path": "projects/social_platform/src/app/app.routes.ts", + "sha256Raw": "93b347e879aa86bde5d01843a820c8990e6c05268f471723b386aa93c73e7454", + "sha256LF": "93b347e879aa86bde5d01843a820c8990e6c05268f471723b386aa93c73e7454", + "gitBlob": "0ad9c45e367fbc706fe7d22426ac99313f4e81f3" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/management.component.html", + "sha256Raw": "b696ff891d71d7d67601f219b06c9a7618675f4eb74a8e46c98aca5c443138b6", + "sha256LF": "b696ff891d71d7d67601f219b06c9a7618675f4eb74a8e46c98aca5c443138b6", + "gitBlob": "c93fdcb140fc758400e2ae4b533032fb20b7c424" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/management.component.ts", + "sha256Raw": "83186522c741152b348926170e6565b3e0497bb355fde2e4d121c92c9ac4391f", + "sha256LF": "83186522c741152b348926170e6565b3e0497bb355fde2e4d121c92c9ac4391f", + "gitBlob": "a365e8dec74fbf7708ade924511ae3e8284db81d" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.spec.ts", + "sha256Raw": "5841731d1eb4c5dad83ef372481f7b8eb6c4db526a1e94d595b9a4ffb16f0093", + "sha256LF": "5841731d1eb4c5dad83ef372481f7b8eb6c4db526a1e94d595b9a4ffb16f0093", + "gitBlob": "890a395f1edd58fdf6ccbbdd0848fef42120ea73" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.ts", + "sha256Raw": "63f5269882c27825e0ef2f0faffabe8fa41b58fdc6f9d5c2a330bac57faf3039", + "sha256LF": "63f5269882c27825e0ef2f0faffabe8fa41b58fdc6f9d5c2a330bac57faf3039", + "gitBlob": "636e7b71f7dfd88e1d5a8af8cab3aea64ae8fe74" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.html", + "sha256Raw": "a706d1c63d42ac92a0f7f34629738c6cd9f3c31db2fd6199b3f74303006073dd", + "sha256LF": "a706d1c63d42ac92a0f7f34629738c6cd9f3c31db2fd6199b3f74303006073dd", + "gitBlob": "a104a67679549d6821f860efe55c7efb4f9f0dda" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.scss", + "sha256Raw": "28048a7efbe080dd179d313533178dc08de6344e1717db093e4633abdc29752c", + "sha256LF": "28048a7efbe080dd179d313533178dc08de6344e1717db093e4633abdc29752c", + "gitBlob": "0c4a6205696f19b8aee95bd0034620ce88386d51" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.spec.ts", + "sha256Raw": "dbf0e22306382c0e191c59bec510676d9eaed54f12ebac4fb81a65ec368c07ee", + "sha256LF": "dbf0e22306382c0e191c59bec510676d9eaed54f12ebac4fb81a65ec368c07ee", + "gitBlob": "9e3b93965a8ffab33517ebf0bcc706ee6b76dba8" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.ts", + "sha256Raw": "6d998e6671397571d3221f1e328f2602b0e50e35137a18b3577bd97d32e0aa55", + "sha256LF": "6d998e6671397571d3221f1e328f2602b0e50e35137a18b3577bd97d32e0aa55", + "gitBlob": "5625aa2158d3dc166464b90827d5cb71ae195be0" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.html", + "sha256Raw": "78f5bfeb0c65f181bfa5e5dc9de5749b705c7ab41a6227814be5db30cbeec5e6", + "sha256LF": "78f5bfeb0c65f181bfa5e5dc9de5749b705c7ab41a6227814be5db30cbeec5e6", + "gitBlob": "97ae60906f296875aaea6c1404b8cc8aa85bd7c1" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.spec.ts", + "sha256Raw": "67d3910bd457c2dd71edadfed458f9da550e83a9f0f20551c7b3e225747f45d4", + "sha256LF": "67d3910bd457c2dd71edadfed458f9da550e83a9f0f20551c7b3e225747f45d4", + "gitBlob": "0cc47ea37d79c5a92ff85788f0aac911b9045936" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.ts", + "sha256Raw": "bb205ae896c4ad5a6e6ebf01f41f0c0c18268e39012f694665829f8dee3bc197", + "sha256LF": "bb205ae896c4ad5a6e6ebf01f41f0c0c18268e39012f694665829f8dee3bc197", + "gitBlob": "92c4b21a27d1778e76c685695a700f4f8f6c6a9d" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/published-solutions/public-program-solutions.component.ts", + "sha256Raw": "0d5067f87929f3385757515e8ee7b3cbf86491fe0c1c45123fabee02d4e229b7", + "sha256LF": "0d5067f87929f3385757515e8ee7b3cbf86491fe0c1c45123fabee02d4e229b7", + "gitBlob": "66ce51f1e4f2159cefdc2205b372235d3ac8a018" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.html", + "sha256Raw": "75047031419058c559bebcd68c660bd04d6868fcc58a0d505e3c28e425453f4a", + "sha256LF": "75047031419058c559bebcd68c660bd04d6868fcc58a0d505e3c28e425453f4a", + "gitBlob": "4a4d097a15584aefefbc0ed643eb276f49aabaf3" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.scss", + "sha256Raw": "c8cd1cd47112a3252390ccb7d4af61930e70f5996a56b736f4db20777865d2d9", + "sha256LF": "c8cd1cd47112a3252390ccb7d4af61930e70f5996a56b736f4db20777865d2d9", + "gitBlob": "926569334dd51c0ceb2b3a958292f37098502f59" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.spec.ts", + "sha256Raw": "d43e0153d52a0a8add25491b76d94419ce73c635ec5bc16d93ff2bf081a3fe12", + "sha256LF": "d43e0153d52a0a8add25491b76d94419ce73c635ec5bc16d93ff2bf081a3fe12", + "gitBlob": "60fb6d80d59663a24e3d05f846a5edf9ea95c259" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.ts", + "sha256Raw": "8f4164358412f7bf810f5b0fb9fc4550983da14a41a90b75465085e11c0a8eba", + "sha256LF": "8f4164358412f7bf810f5b0fb9fc4550983da14a41a90b75465085e11c0a8eba", + "gitBlob": "9620ff39c52555373b05ed72cffb6d792e41e767" + }, + { + "path": "projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.html", + "sha256Raw": "18e987c2a58fd5c6d44fb9c2945febee9091364189615d0df8acfb85ce1fd39b", + "sha256LF": "18e987c2a58fd5c6d44fb9c2945febee9091364189615d0df8acfb85ce1fd39b", + "gitBlob": "1056e41350bacf64f1aa0666c0df67cabea340e5" + }, + { + "path": "projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.spec.ts", + "sha256Raw": "b181850704e3bbc62042d5ff5a1eae56ab16b8911cb7af18bad07c8bf2e49e70", + "sha256LF": "b181850704e3bbc62042d5ff5a1eae56ab16b8911cb7af18bad07c8bf2e49e70", + "gitBlob": "307ded674532519e88967f5b39b034c3f0a9df9d" + }, + { + "path": "projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.ts", + "sha256Raw": "84a48f0c188af0d95b190ff84cccaa573f8aab8be3f5485ae4f46adedadfa282", + "sha256LF": "84a48f0c188af0d95b190ff84cccaa573f8aab8be3f5485ae4f46adedadfa282", + "gitBlob": "96611d91ebeaa0453bf24c49a2d87c9bf99e7190" + }, + { + "path": "projects/social_platform/src/app/ui/routes/program/published-solutions.routes.spec.ts", + "sha256Raw": "ed72dafc709fe26507b0e1976d0cdfd7c797ac8fa8618c5ef5f20f555ce93354", + "sha256LF": "ed72dafc709fe26507b0e1976d0cdfd7c797ac8fa8618c5ef5f20f555ce93354", + "gitBlob": "8d31eda3fb97bf4857568f5efdf769091778c7d7" + }, + { + "path": "projects/social_platform/src/app/ui/routes/program/published-solutions.routes.ts", + "sha256Raw": "61f6d8047128a1c68a2337490f99463a2835992082cef1fc72b7de4acea677ee", + "sha256LF": "61f6d8047128a1c68a2337490f99463a2835992082cef1fc72b7de4acea677ee", + "gitBlob": "f60ced3e2862d04dc2e36185bd9059c4f5afbe3b" + }, + { + "path": "projects/social_platform/src/app/ui/widgets/detail/detail.component.html", + "sha256Raw": "ab4e41d2100e1895d707ae5c624fc8268285ae74e5cbbba2f402550f9d827d0c", + "sha256LF": "ab4e41d2100e1895d707ae5c624fc8268285ae74e5cbbba2f402550f9d827d0c", + "gitBlob": "01bc5050ba89872c66c01d3b9e7cbb27fde0bf9e" + }, + { + "path": "projects/social_platform/src/app/ui/widgets/detail/detail.component.ts", + "sha256Raw": "7ec4127c3e536791d3ea3d56811aa6014f402857ff948ca1478dc43b6f733d61", + "sha256LF": "7ec4127c3e536791d3ea3d56811aa6014f402857ff948ca1478dc43b6f733d61", + "gitBlob": "b46dc0cda4d8a7be449a98ea45222559aa1a11b9" + } + ], + "contractVersion": "1.21.0", + "backendHead": "fdec3dcbd6693ddb487816114301d0df01798f93", + "verification": { + "featureTests": 2986, + "featureTestFiles": 489, + "strictC00": "PASS", + "focusedTestsAfterFocusFix": 43, + "focusedTestFiles": 7, + "angularBuild": "PASS", + "browserChecksBeforeFocusFallback": 22, + "prototypeChecks": 100 + }, + "scope": "Working product source delta, source unchanged after final focused/build checks. Full feature tests precede only the focus fallback; focused checks cover that change. Historical prospective dev tests have a different source scope." +} diff --git a/docs/qa/program-solutions-final-amendments-20261026/xlsx-evidence.json b/docs/qa/program-solutions-final-amendments-20261026/xlsx-evidence.json new file mode 100644 index 000000000..12181e2cc --- /dev/null +++ b/docs/qa/program-solutions-final-amendments-20261026/xlsx-evidence.json @@ -0,0 +1,9 @@ +{ + "solutionIds": [70], + "count": 1, + "teamRows": 3, + "zeroPreserved": true, + "falsePreserved": true, + "sameListAndXlsxIds": true, + "sha256": "060a71b6ae250cf72697c1e69f8c9299fc30dcd625dcb6a10255a252807a2bf8" +} diff --git a/docs/qa/program-solutions-form-settings-20261025/README.md b/docs/qa/program-solutions-form-settings-20261025/README.md new file mode 100644 index 000000000..50e4a7c29 --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/README.md @@ -0,0 +1,39 @@ + + +# QA01: настройка формы и неполные черновики + +Проверены реальные организаторские настройки и сохранение неполного решения для КЧ, хакатона и неконкурсной программы. Исправлены ложное предупреждение о несохраненных изменениях в новой форме и подсказка о сдаче в неконкурсном редакторе. API, права, структура экранов и SCSS не менялись; полный первый выпуск еще не завершен. + +Backend source `ad3945e` (runtime `30c0d3e`); frontend поверх `d2a2cd3f`, точные source hashes в manifest/build provenance; C00 `1.14.0`. Только новые синтетические программы 27/28/29, решения 42/43/44, prefix `qa01f-20261025-a8f2c339`. Настоящие Angular/Django/JWT/PostgreSQL 18, locmem mail; внешние запросы браузера блокировались. Credentials/JWT не включены в Git. + +## Ошибка и исправление + +У формы с template=null и fields=[] dirty сравнивал fallback шаблона для PUT с настоящим null сервера. Поэтому первое явное применение шаблона открывало «Есть несохраненные изменения» и оставляло поля пустыми до подтверждения потери несуществующих правок. [Исходный FAIL](before-finding.json) и screenshot сохранены; реальный baseline UI относится к КЧ27. Обе формы воспроизведены красной параметризованной регрессией: 2 failed/7 passed. + +Теперь dirty сравнивает фактически выбранный template, включая null. Первый шаблон копируется без discard prompt и без автоматического PUT; повторная замена после реальной правки по-прежнему требует подтверждения. Request serializer и save guards сохранены. Профильные 9 tests PASS. У неконкурсного редактора подсказка больше не обещает будущую сдачу; существующий noncompetitive test усилен, отдельный финальный browser read подтвердил текст, отсутствие submit и сохраненные 0/false. + +## Приемка + +| Проверка | Результат | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| FORM01 / AC01 | Создание через Angular дает ProgramSolution со своим капитаном/вложенным составом, без Project; standalone Team модели в runtime нет. В каждой из трех программ одна новая заявка. | +| FORM02 / AC03 | UI сохраняет название пустого draft при одном капитане, min_size=3, без кейса и обязательных материалов. Это обычный PATCH200, не сдача и не обход серверной проверки. | +| FORM03 / AC11 | Существующий конструктор: явный шаблон, подпись/подсказка/варианты кейса, optional presentation PDF/1.5 МиБ, number0–100, text max20, новое checkbox поле и порядок. Все перечитано из API/БД. Участник и организатор другой программы не читают настройки; чужой PUT403 не меняет данные. | +| FORM04 / AC12 | Разные canonical формы КЧ/хакатона скопированы в программы; IDs полей не пересекаются. Изменение каталога смоделировано только в Python процессе verifier через mock: сохраненные form_data трех программ остались прежними. Настоящий API каталог не патчился и до/после GET совпадает. | +| FORM05 / AC13 scope | Пустые кейс/комментарий отсутствуют в answers; number имеет именно integer0, checkbox именно boolean false. Значения переживают save/reload/переход вкладок и JSON read. Общий AC13 с XLSX этим browser checkpoint не закрывается. | +| FORM06 / AC23 | Неконкурсный draft сохраняется с неполными required fields/одним капитаном, submission.required=False, кнопки сдачи нет. Прямой submit409/not_competitive ничего не меняет. Публичный UI этим не проверен. | +| FORM07 | Форсированные PATCH number101 и text21 получают400 с точным field_id; сохраненные ответы/version не меняются. У presentation сохранены allowed_extensions=[pdf] и1572864 bytes; фактическая загрузка файла в этом проходе не повторялась. | + +[Browser checks](browser-checks.json) содержат 19 checkpoints: в том числе 6 guarded CLI creation controls и 1 наблюдение возобновленного чтения уже сохраненных значений. Это не 19 отдельных UI экранов. [Независимый verifier](domain-and-contract-evidence.json): основной проход 85 C00 responses/22 JSON requests, включая61 browser responses и24 verifier requests. Отдельный финальный [noncompetitive read](noncompetitive-copy-final.json) добавляет1 проверенный C00 response; суммарно86/22. Verifier не менял выбранные domain rows. Auth/welcome не включены в этот wire счетчик; zero-write browser не заявляется. + +Final `npm run test:ci`: **2796 tests/479 files + strict C00 types PASS**; development build и scoped ESLint PASS. C00 три файла byte-identical в repos. Backend product code/tests в этом блоке не менялись, отдельный test DB не запускался. Новый frontend renderer текст проверен после финальной сборки; поля и сохранение до этого проверены на build с уже исправленным dirty. + +## Происхождение и границы + +Initial seed committed три пустые программы/регистрации, затем упал при сборке manifest: Expert имеет PK без имени id. Подготовленные строки проверены и сохранены, не пересоздавались. Baseline снят после этого setup, до тестируемых API/UI writes; это явно не pre-seed SHA. [Recovery](setup-recovery.json) перечисляет исключенные новые setup rows. Все прежние выбранные строки 15 domain моделей с этого момента неизменны; новые решения/поля/ответы появились только в изолированных программах. Auth/session/last_activity не входят в утверждение. + +Продуктовый baseline FAIL сохранен отдельно. Первый post-fix harness не нашел preview в app-modal, потому что содержимое вынесено в overlay; позже harness ожидал null вместо отсутствующего пустого ответа. Оба FAIL сохранены. После уже успешного save0/false КЧ выполнено только чтение/reload/tabs, без сброса и повторного PATCH; остальные программы прошли свои фазы. Результат объединяет явно отмеченные фазы, не заявляет один непрерывный green run. JavaScript exceptions в записанных browser checks=0. + +Основной manager/captain путь хакатона выполнен при390, КЧ/неконкурсного — при1440. Сохраненные состояния проверены при320/390/768/1440 без горизонтального overflow. Часть screenshots просмотрена; они могут отражать внутреннюю вертикальную прокрутку. Mont/существующие controls сохранены, remote avatars блокировались. Полные a11y/zoom, invitation lifecycle, competitive submit, публичный/новый manager analytics UI, production CDN/рассылки и общий первый выпуск не заявляются. + +[Build after](build-after.json) и [final](build-final.json) используют копии generated build с заменой origins ровно одного JS chunk. Source environments не патчились. [Manifest](local-artifacts.json) содержит SHA256 raw wire, FAIL reports, screenshots, helpers и финальных sources. Все три решения остаются drafts с одним капитаном, ровно двумя ответами0/false, без приглашений/сдачи/snapshot/публикации/оценок; creation flags=False. Реальные программы не переключались, merge/deploy не выполнялись. diff --git a/docs/qa/program-solutions-form-settings-20261025/before-finding.json b/docs/qa/program-solutions-form-settings-20261025/before-finding.json new file mode 100644 index 000000000..2601baa3e --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/before-finding.json @@ -0,0 +1,18 @@ +{ + "status": "FAIL before fix", + "frontend": "d2a2cd3f80549e7dcbc6b3e32e55fea7ead57175", + "program": 27, + "actual": "Untouched form with template=null/fields=[] was dirty and initial apply opened discard confirmation.", + "artifact": "browser-attempt-1792918390582.json", + "screenshot": "browser-failure-1792918390585.png", + "unitBefore": { + "passed": 7, + "failed": 2 + }, + "unitAfter": { + "passed": 9, + "failed": 0 + }, + "runtimeFix": "Dirty comparison preserves actual null template; request serializer is unchanged.", + "limits": "Actual initial UI finding on case program27; both formats reproduced in component regressions, not two separate baseline browser runs." +} diff --git a/docs/qa/program-solutions-form-settings-20261025/browser-checks.json b/docs/qa/program-solutions-form-settings-20261025/browser-checks.json new file mode 100644 index 000000000..5d23dd622 --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/browser-checks.json @@ -0,0 +1,620 @@ +{ + "status": "PASS for recorded checkpoints", + "prefix": "qa01f-20261025-a8f2c339", + "checks": [ + { + "program": 27, + "key": "case_championship", + "run": "run-1792918810398", + "check": "explicit template, case options, file limits, numeric/text constraints and order persisted", + "widths": [320, 390, 768, 1440], + "fieldIds": [202, 203, 204, 205, 206, 207, 208, 209] + }, + { + "program": 27, + "key": "case_championship", + "run": "run-1792918810398", + "check": "scoped creation control", + "action": "enable" + }, + { + "program": 28, + "key": "hackathon", + "run": "run-1792918810398", + "check": "explicit template, case options, file limits, numeric/text constraints and order persisted", + "widths": [320, 390, 768, 1440], + "fieldIds": [210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221] + }, + { + "program": 28, + "key": "hackathon", + "run": "run-1792918810398", + "check": "scoped creation control", + "action": "enable" + }, + { + "program": 29, + "key": "noncompetitive", + "run": "run-1792918810398", + "check": "explicit template, case options, file limits, numeric/text constraints and order persisted", + "widths": [320, 390, 768, 1440], + "fieldIds": [222, 223, 224, 225, 226, 227, 228, 229] + }, + { + "program": 29, + "key": "noncompetitive", + "run": "run-1792918810398", + "check": "scoped creation control", + "action": "enable" + }, + { + "program": 27, + "key": "case_championship", + "run": "run-1792918810398", + "check": "captain-only empty draft saves without case or required materials", + "solution": 42 + }, + { + "program": 27, + "key": "case_championship", + "run": "run-1792918898397", + "check": "resume completed 0/false save without another write", + "solution": 42 + }, + { + "program": 27, + "key": "case_championship", + "run": "run-1792918898397", + "check": "scoped creation control", + "action": "disable" + }, + { + "program": 27, + "key": "case_championship", + "run": "run-1792918898397", + "check": "0/false/null and name survive save/reload/tabs; no artificial noncompetitive submission", + "solution": 42, + "competitive": true, + "widths": [320, 390, 768, 1440] + }, + { + "program": 28, + "key": "hackathon", + "run": "run-1792918898397", + "check": "captain-only empty draft saves without case or required materials", + "solution": 43 + }, + { + "program": 28, + "key": "hackathon", + "run": "run-1792918898397", + "check": "scoped creation control", + "action": "disable" + }, + { + "program": 28, + "key": "hackathon", + "run": "run-1792918898397", + "check": "0/false/null and name survive save/reload/tabs; no artificial noncompetitive submission", + "solution": 43, + "competitive": true, + "widths": [320, 390, 768, 1440] + }, + { + "program": 29, + "key": "noncompetitive", + "run": "run-1792918898397", + "check": "captain-only empty draft saves without case or required materials", + "solution": 44 + }, + { + "program": 29, + "key": "noncompetitive", + "run": "run-1792918898397", + "check": "scoped creation control", + "action": "disable" + }, + { + "program": 29, + "key": "noncompetitive", + "run": "run-1792918898397", + "check": "0/false/null and name survive save/reload/tabs; no artificial noncompetitive submission", + "solution": 44, + "competitive": false, + "widths": [320, 390, 768, 1440] + }, + { + "program": 27, + "key": "case_championship", + "run": "run-1792918898397", + "check": "manager of another program cannot read or change this form", + "status": 403 + }, + { + "program": 28, + "key": "hackathon", + "run": "run-1792918898397", + "check": "manager of another program cannot read or change this form", + "status": 403 + }, + { + "program": 29, + "key": "noncompetitive", + "run": "run-1792918898397", + "check": "manager of another program cannot read or change this form", + "status": 403 + } + ], + "runs": [ + { + "run": "run-1792918810398", + "head": "d2a2cd3f80549e7dcbc6b3e32e55fea7ead57175", + "origin": "http://127.0.0.1:8882", + "componentSha256": "303183b640190071d0588069ac0ba27e97cbc9d529e4cf3527f2f08688cb3be4" + }, + { + "run": "run-1792918898397", + "head": "d2a2cd3f80549e7dcbc6b3e32e55fea7ead57175", + "origin": "http://127.0.0.1:8882", + "componentSha256": "303183b640190071d0588069ac0ba27e97cbc9d529e4cf3527f2f08688cb3be4" + } + ], + "earlierAttemptArtifacts": [ + "browser-attempt-1792918390582.json", + "browser-attempt-1792918748942.json", + "browser-attempt-1792918843306.json" + ], + "browserResponses": 61, + "javascriptErrors": [], + "diagnosticCount": 84, + "programs": { + "27": { + "solutionId": 42, + "template": "case_championship", + "revision": "7fb45981a34d7b96a995750ff7c1b5fb386e41daf797d4d1b3ad767ded7bdd70", + "configured": true, + "draftChecked": true, + "fields": [ + { + "id": 202, + "type": "select", + "purpose": "case", + "required": true, + "position": 0, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 203, + "type": "textarea", + "purpose": "other", + "required": true, + "position": 1, + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 204, + "type": "file", + "purpose": "presentation", + "required": false, + "position": 2, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": ["pdf"], + "max_file_size_bytes": 1572864 + } + }, + { + "id": 205, + "type": "url", + "purpose": "video", + "required": false, + "position": 3, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 206, + "type": "number", + "purpose": "other", + "required": false, + "position": 4, + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": 100, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 207, + "type": "file", + "purpose": "other", + "required": false, + "position": 5, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": ["pdf", "zip"], + "max_file_size_bytes": 52428800 + } + }, + { + "id": 208, + "type": "checkbox", + "purpose": "other", + "required": false, + "position": 6, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 209, + "type": "textarea", + "purpose": "other", + "required": false, + "position": 7, + "validation": { + "min_length": 0, + "max_length": 20, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "28": { + "solutionId": 43, + "template": "hackathon", + "revision": "b297190ae6fc1323e7ebf062164e9e102365a45aca1bd1531ddcd76f1ef66409", + "configured": true, + "draftChecked": true, + "fields": [ + { + "id": 210, + "type": "select", + "purpose": "case", + "required": true, + "position": 0, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 211, + "type": "textarea", + "purpose": "other", + "required": true, + "position": 1, + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 212, + "type": "textarea", + "purpose": "other", + "required": true, + "position": 2, + "validation": { + "min_length": null, + "max_length": 10000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 213, + "type": "url", + "purpose": "repository", + "required": true, + "position": 3, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 214, + "type": "textarea", + "purpose": "run_instructions", + "required": true, + "position": 4, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 215, + "type": "url", + "purpose": "demo", + "required": false, + "position": 5, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 216, + "type": "file", + "purpose": "presentation", + "required": false, + "position": 6, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": ["pdf"], + "max_file_size_bytes": 1572864 + } + }, + { + "id": 217, + "type": "url", + "purpose": "video", + "required": false, + "position": 7, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 218, + "type": "number", + "purpose": "other", + "required": false, + "position": 8, + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": 100, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 219, + "type": "file", + "purpose": "other", + "required": false, + "position": 9, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": ["pdf", "zip"], + "max_file_size_bytes": 52428800 + } + }, + { + "id": 220, + "type": "checkbox", + "purpose": "other", + "required": false, + "position": 10, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 221, + "type": "textarea", + "purpose": "other", + "required": false, + "position": 11, + "validation": { + "min_length": 0, + "max_length": 20, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "29": { + "solutionId": 44, + "template": "case_championship", + "revision": "0745c0af1268bc1cabe6fb2b6be2f639ee98514138320cad2b45b72481837829", + "configured": true, + "draftChecked": true, + "fields": [ + { + "id": 222, + "type": "select", + "purpose": "case", + "required": true, + "position": 0, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 223, + "type": "textarea", + "purpose": "other", + "required": true, + "position": 1, + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 224, + "type": "file", + "purpose": "presentation", + "required": false, + "position": 2, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": ["pdf"], + "max_file_size_bytes": 1572864 + } + }, + { + "id": 225, + "type": "url", + "purpose": "video", + "required": false, + "position": 3, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 226, + "type": "number", + "purpose": "other", + "required": false, + "position": 4, + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": 100, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 227, + "type": "file", + "purpose": "other", + "required": false, + "position": 5, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": ["pdf", "zip"], + "max_file_size_bytes": 52428800 + } + }, + { + "id": 228, + "type": "checkbox", + "purpose": "other", + "required": false, + "position": 6, + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 229, + "type": "textarea", + "purpose": "other", + "required": false, + "position": 7, + "validation": { + "min_length": 0, + "max_length": 20, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + } + }, + "provenance": "19 checkpoints include 6 guarded CLI creation controls, 1 resumed-save observation, UI configuration/draft checks and direct foreign-manager requests. Completed phases are reused without resetting or re-saving first draft. Initial false-dirty product failure and later harness failures are retained. Only C00 traffic is counted; authentication/welcome requests are outside this wire capture, so zero browser writes is not claimed." +} diff --git a/docs/qa/program-solutions-form-settings-20261025/build-after.json b/docs/qa/program-solutions-form-settings-20261025/build-after.json new file mode 100644 index 000000000..8ae8e7d1d --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/build-after.json @@ -0,0 +1,26 @@ +{ + "source": "Z:\\Документы\\PROCOLLAB-dev\\procollab-dev-program-solutions-fe01-20261024\\dist\\social_platform", + "target": "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-form-qa-angular", + "head": "d2a2cd3f80549e7dcbc6b3e32e55fea7ead57175", + "workingTreeRuntimeFix": true, + "modifiedGeneratedFiles": [ + { + "file": "chunk-CHX6T2XY.js", + "before": "4707c125db5485fa8b5eeb9e30e23c6f90759d9f2daf86b56a5a0b332da287dd", + "after": "d8252699c1d7f5986df9684f6f5b9be81943d068050e6537958be0680cac6380" + } + ], + "sourceHashes": [ + { + "file": "projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts", + "sha256": "303183b640190071d0588069ac0ba27e97cbc9d529e4cf3527f2f08688cb3be4", + "headSha256": "eeda84fe4ad19c39baec8e9d8a6e2695473791b52efb6d4ea903e290c2375ab2" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html", + "sha256": "777602d45216da4aa8da7f8b7d9cf7c5a76e85d825fc0e958421629ddbad66f4", + "headSha256": "777602d45216da4aa8da7f8b7d9cf7c5a76e85d825fc0e958421629ddbad66f4" + } + ], + "productEnvironmentUnchanged": true +} diff --git a/docs/qa/program-solutions-form-settings-20261025/build-final.json b/docs/qa/program-solutions-form-settings-20261025/build-final.json new file mode 100644 index 000000000..cd4f57413 --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/build-final.json @@ -0,0 +1,31 @@ +{ + "source": "Z:\\Документы\\PROCOLLAB-dev\\procollab-dev-program-solutions-fe01-20261024\\dist\\social_platform", + "target": "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-form-qa-angular-final", + "head": "d2a2cd3f80549e7dcbc6b3e32e55fea7ead57175", + "workingTreeRuntimeFix": true, + "modifiedGeneratedFiles": [ + { + "file": "chunk-CHX6T2XY.js", + "before": "4707c125db5485fa8b5eeb9e30e23c6f90759d9f2daf86b56a5a0b332da287dd", + "after": "d8252699c1d7f5986df9684f6f5b9be81943d068050e6537958be0680cac6380" + } + ], + "sourceHashes": [ + { + "file": "projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts", + "sha256": "303183b640190071d0588069ac0ba27e97cbc9d529e4cf3527f2f08688cb3be4", + "headSha256": "eeda84fe4ad19c39baec8e9d8a6e2695473791b52efb6d4ea903e290c2375ab2" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html", + "sha256": "777602d45216da4aa8da7f8b7d9cf7c5a76e85d825fc0e958421629ddbad66f4", + "headSha256": "777602d45216da4aa8da7f8b7d9cf7c5a76e85d825fc0e958421629ddbad66f4" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/solution/solution.component.html", + "sha256": "9a7f7428e8c3884c4fddc478f20f6a3b7f1794f027acd8ed3ac53a39ee09042d", + "headSha256": "20069bd71d1be326a1d9c2d81cc4251fd3f002fd92a56da41315f9fa2246e489" + } + ], + "productEnvironmentUnchanged": true +} diff --git a/docs/qa/program-solutions-form-settings-20261025/domain-and-contract-evidence.json b/docs/qa/program-solutions-form-settings-20261025/domain-and-contract-evidence.json new file mode 100644 index 000000000..cc86f598e --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/domain-and-contract-evidence.json @@ -0,0 +1,285 @@ +{ + "status": "PASS", + "backend": "ad3945ea2ebcd214a26b64ab09c92d334f9d2c77", + "frontendBase": "d2a2cd3f80549e7dcbc6b3e32e55fea7ead57175", + "prefix": "qa01f-20261025-a8f2c339", + "validation": { + "responses": 85, + "requests": 22 + }, + "programs": [ + { + "program": 27, + "key": "case_championship", + "template": "case_championship", + "solution": 42, + "fieldIds": [202, 203, 204, 205, 206, 207, 208, 209], + "numberId": 206, + "falseId": 208, + "captainOnly": true, + "emptyAnswersOmitted": true, + "zeroFalseSaved": true, + "creationFlagOff": true, + "requiredSubmission": true + }, + { + "program": 28, + "key": "hackathon", + "template": "hackathon", + "solution": 43, + "fieldIds": [210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221], + "numberId": 218, + "falseId": 220, + "captainOnly": true, + "emptyAnswersOmitted": true, + "zeroFalseSaved": true, + "creationFlagOff": true, + "requiredSubmission": true + }, + { + "program": 29, + "key": "noncompetitive", + "template": "case_championship", + "solution": 44, + "fieldIds": [222, 223, 224, 225, 226, 227, 228, 229], + "numberId": 226, + "falseId": 228, + "captainOnly": true, + "emptyAnswersOmitted": true, + "zeroFalseSaved": true, + "creationFlagOff": true, + "requiredSubmission": false + } + ], + "browserChecks": 19, + "browserResponses": 61, + "verifierRequests": 24, + "verifierDomainMutations": 0, + "preexistingSelectedDomainUnchangedSinceBaseline": true, + "baselineTiming": "Captured after isolated program/registration setup and before any tested API or browser writes. Initial seed committed three empty programs but failed generating manifest because Expert has a non-id primary key. No pre-seed SHA is claimed.", + "baselineModels": [ + "partner_programs.PartnerProgram", + "partner_programs.PartnerProgramField", + "partner_programs.PartnerProgramUserProfile", + "partner_programs.ProgramSolution", + "partner_programs.ProgramSolutionAnswer", + "partner_programs.ProgramSolutionMember", + "partner_programs.SolutionInvitation", + "project_rates.SolutionScore", + "project_rates.SolutionExpertAssignment", + "project_rates.Criteria", + "users.Expert", + "files.UserFile", + "projects.Project", + "users.Expert_programs", + "partner_programs.PartnerProgram_managers" + ], + "catalogMutationScope": "Changed only inside this verifier Python process via unittest.mock.patch; actual API workers/catalog were not patched. Persisted forms stayed identical; before/after real template GET also matches.", + "originalProjectCountUnchanged": true, + "events": [ + { + "role": "manager", + "method": "GET", + "path": "/solution-form-templates/", + "status": 200, + "code": null, + "responseSha256": "ffb494ccc9660ff3945a3f381f5c5440d7059b82c43b58d9c972167b9605b79f" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/27/solutions/42/", + "status": 200, + "code": null, + "responseSha256": "d7bcbd548ee07c33fa275c5461962c03a05ae85d8598f744cdc7c984552bea5d" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/27/management/solution-form/", + "status": 200, + "code": null, + "responseSha256": "103ab1310e967972fb6f1070c77ceb0906ac8be7a23fb4bc28523398a8bf918a" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/27/management/solution-form/", + "status": 403, + "code": "permission_denied", + "responseSha256": "2fb8879ebaa01000c0cc48921160c73fb1ae3d92dcb96b66d38befee3211ff9b" + }, + { + "role": "foreign_manager", + "method": "GET", + "path": "/programs/27/management/solution-form/", + "status": 403, + "code": "permission_denied", + "responseSha256": "2fb8879ebaa01000c0cc48921160c73fb1ae3d92dcb96b66d38befee3211ff9b" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/27/solutions/42/", + "status": 400, + "code": "invalid_answers", + "responseSha256": "61a1ae3d9cce7dce04e39d64a023b966cbbaae8f6250781d2882930ec099f7c0" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/27/solutions/42/", + "status": 400, + "code": "invalid_answers", + "responseSha256": "95e6625f99e950d4cc3605947ab249e417e548ff01af8ef8575f85b1a1777b14" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/27/solutions/42/", + "status": 200, + "code": null, + "responseSha256": "d7bcbd548ee07c33fa275c5461962c03a05ae85d8598f744cdc7c984552bea5d" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/28/solutions/43/", + "status": 200, + "code": null, + "responseSha256": "b8d9e37a6a29461e121338a343da5ce4a50f02c50a91b8dd12bdfca8d913c012" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/28/management/solution-form/", + "status": 200, + "code": null, + "responseSha256": "27faec73c95aa59da6e9dc48775924a55c8c96ce5d29c2dd2e123f4d1a3fdd2a" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/28/management/solution-form/", + "status": 403, + "code": "permission_denied", + "responseSha256": "2fb8879ebaa01000c0cc48921160c73fb1ae3d92dcb96b66d38befee3211ff9b" + }, + { + "role": "foreign_manager", + "method": "GET", + "path": "/programs/28/management/solution-form/", + "status": 403, + "code": "permission_denied", + "responseSha256": "2fb8879ebaa01000c0cc48921160c73fb1ae3d92dcb96b66d38befee3211ff9b" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/28/solutions/43/", + "status": 400, + "code": "invalid_answers", + "responseSha256": "d54a0cfc57cee62c952c29af17c37e1881904afe459e11883b11f9765e14806b" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/28/solutions/43/", + "status": 400, + "code": "invalid_answers", + "responseSha256": "2cc985ecebf9002a9cd22bcf3a201d887244b1ca374f1276d23b7766f2dc3072" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/28/solutions/43/", + "status": 200, + "code": null, + "responseSha256": "b8d9e37a6a29461e121338a343da5ce4a50f02c50a91b8dd12bdfca8d913c012" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/29/solutions/44/", + "status": 200, + "code": null, + "responseSha256": "6dcbd5572cf4c835b7ae3b463f1af52e6cd4f3eb718b13febf9b203ed8a0473a" + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/29/management/solution-form/", + "status": 200, + "code": null, + "responseSha256": "29d26f65d64afe32866e6b8de7d492c7cbfaaf48b51262f7bdc614c740686d61" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/29/management/solution-form/", + "status": 403, + "code": "permission_denied", + "responseSha256": "2fb8879ebaa01000c0cc48921160c73fb1ae3d92dcb96b66d38befee3211ff9b" + }, + { + "role": "foreign_manager", + "method": "GET", + "path": "/programs/29/management/solution-form/", + "status": 403, + "code": "permission_denied", + "responseSha256": "2fb8879ebaa01000c0cc48921160c73fb1ae3d92dcb96b66d38befee3211ff9b" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/29/solutions/44/", + "status": 400, + "code": "invalid_answers", + "responseSha256": "78d439d129ebce357fcf1fd3192335f8dc1e785694b509075d086a94fb734259" + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/29/solutions/44/", + "status": 400, + "code": "invalid_answers", + "responseSha256": "8003ab9d2e81e0b7a94de6cc712a3bf683a238e80f95c809794c28b939c26691" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/29/solutions/44/submit/", + "status": 409, + "code": "not_competitive", + "responseSha256": "4780f804f123a53f4c0c3ab1045f2fd2ad0951036d46a6514064ec6ec9884101" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/29/solutions/44/", + "status": 200, + "code": null, + "responseSha256": "6dcbd5572cf4c835b7ae3b463f1af52e6cd4f3eb718b13febf9b203ed8a0473a" + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-form-templates/", + "status": 200, + "code": null, + "responseSha256": "ffb494ccc9660ff3945a3f381f5c5440d7059b82c43b58d9c972167b9605b79f" + } + ], + "independentTeamModels": [], + "limits": [ + "Baseline was captured after 3 empty programs/registrations were set up; no pre-seed SHA is claimed.", + "UI config/draft phases resume only saved checkpoints; initial false-dirty product failure and separate harness mistakes retained.", + "No submission of competitive fixtures, full invitation lifecycle, production file upload, new manager analytics/public UI or a11y/zoom acceptance in this checkpoint." + ], + "aggregateValidation": { + "responses": 86, + "requests": 22 + }, + "finalCopyReadResponses": 1, + "curation": "Raw verifier events retained locally; committed event bodies replaced with SHA256. Primary verifier validation remains unchanged; aggregate adds final copy read." +} diff --git a/docs/qa/program-solutions-form-settings-20261025/local-artifacts.json b/docs/qa/program-solutions-form-settings-20261025/local-artifacts.json new file mode 100644 index 000000000..de6211d07 --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/local-artifacts.json @@ -0,0 +1,185 @@ +{ + "rawDirectory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-form-settings", + "artifacts": [ + { + "file": "browser-attempt-1792918390582.json", + "bytes": 32360, + "sha256": "274cfba317ad631b46511fad1fa5126ca0b405694aeec543612354c849a1b608" + }, + { + "file": "browser-attempt-1792918748942.json", + "bytes": 62937, + "sha256": "41ba812fc31694b1f8bf9837f20387d2c005085eeb1992e0a107beabcba77b89" + }, + { + "file": "browser-attempt-1792918843306.json", + "bytes": 213963, + "sha256": "082d72cb73543bd0eba278d7fdc31f19badce0989ba255f2c2213e529cd66bc0" + }, + { + "file": "browser-failure-1792918390585.png", + "bytes": 118548, + "sha256": "ecc11995e06f86690d25d228f5d822127f6903c85f0369445363d09c6e197302" + }, + { + "file": "browser-failure-1792918750574.png", + "bytes": 110674, + "sha256": "1277dd318bf17b076364376af8212617f9a19282fe035b30496ab8dfd64cb2d2" + }, + { + "file": "browser-failure-1792918843311.png", + "bytes": 79453, + "sha256": "501fc73e9d6315217a8c63cd3482c4dafed3b4099dd9808c20574c234488db62" + }, + { + "file": "browser-state.json", + "bytes": 385530, + "sha256": "d61577f16c0beff382398db18106877bc8892db07878bc7e4fbdf4fa0cc47c5c" + }, + { + "file": "build-after.json", + "bytes": 1213, + "sha256": "bffd1fb0fd6d88491b5fb8777bc3d63bfb04fc9a164c45070ef2a78385e4134c" + }, + { + "file": "build-final.json", + "bytes": 1503, + "sha256": "e12985c2b8518b19fa1ba2308061ad35341cfd21da5883cf44a049ad78da89d3" + }, + { + "file": "case_championship-draft-zero-false.png", + "bytes": 92440, + "sha256": "6870e61a45945be75f03dedf9694ec8d43ab95974bb1546933a47bf7517ac47a" + }, + { + "file": "case_championship-manager-configured.png", + "bytes": 150386, + "sha256": "924ba1ee038b066181dc6a4f9134cd16377c5f1ca4926b1c934e897a65a90401" + }, + { + "file": "domain-and-contract-evidence.json", + "bytes": 104502, + "sha256": "a20383a09ae4abffa72ab48653f34b84997fbc20d76a20b50d5687cd1ce78a22" + }, + { + "file": "hackathon-draft-zero-false.png", + "bytes": 50980, + "sha256": "7269f5eb603c89b232a9a0b6f571e9cb2048c1642c7ecf5cdd429a8c6e377f3f" + }, + { + "file": "hackathon-manager-configured.png", + "bytes": 59547, + "sha256": "9723e132d752c35c318da7100177e5c30c1324a2a3fac2ad9908c8608772d292" + }, + { + "file": "noncompetitive-copy-final.json", + "bytes": 6896, + "sha256": "667e1d1227b4c32baa4d72adc82201eab187f286c7232336d90e6aa92c4cc452" + }, + { + "file": "noncompetitive-copy-final.png", + "bytes": 94838, + "sha256": "b233dc71891356b81c60ae4148b58bd80a6d1b16fb65aa6492eba80822bba6be" + }, + { + "file": "noncompetitive-draft-zero-false.png", + "bytes": 94727, + "sha256": "bf5d9c8292f9456b55b315297c4f1becc3cd67e25d1dde11f358dcf602d0005f" + }, + { + "file": "noncompetitive-manager-configured.png", + "bytes": 150386, + "sha256": "924ba1ee038b066181dc6a4f9134cd16377c5f1ca4926b1c934e897a65a90401" + }, + { + "file": "setup-recovery.json", + "bytes": 1562, + "sha256": "910b28be662c6e93b181ece9e11f3ef05cee868858e86c3a034166241adcab9f" + } + ], + "helpers": [ + { + "file": "procollab-form-qa-seed.py", + "sha256": "dc874134d571e7f64fe1c0b6cd2e1d0f87c4fc7010548d3a88d846c03fc1cd08" + }, + { + "file": "procollab-form-qa-recover-seed.py", + "sha256": "7f5a2ebf83da47ee731b47b6ed1af7b3cb7f3a88d928f396efbf9eb5210d9e2d" + }, + { + "file": "procollab-form-qa-control.py", + "sha256": "5a9b1ec2a7326facbda890a58096891ef1194d969c03c8a25a1ac6a096378844" + }, + { + "file": "procollab-form-qa-browser.cjs", + "sha256": "0e64d146ca6bef47cf8d261dec875a61201e736f34a57ab906b4d8073e1c469a" + }, + { + "file": "procollab-form-qa-build.cjs", + "sha256": "acd43db9de6a2d8c34c862ed4b3ac3960eea95718b81de3161eff00b7a74df2f" + }, + { + "file": "procollab-form-qa-spa.py", + "sha256": "8e846d57f360bfa5157f117f0c2cdc90a56fb19e83f6bc7e3338fa0110ed3e38" + }, + { + "file": "procollab-form-qa-copy.cjs", + "sha256": "27b8048873717c0abb1c7981556f9dbac820d04122027f5dff66f08ef8f03411" + }, + { + "file": "procollab-form-qa-verify.py", + "sha256": "a66fa670ad19c8687aaaef4b853fb92b1d3bb7a46f0b33753276d3d3a3777871" + }, + { + "file": "procollab-form-qa-package.py", + "sha256": "f580bc8bed6e8a79bf5d783cffab1be2bb7ea9c7c232bd0fe699515e2963e731" + } + ], + "sources": [ + { + "file": "projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts", + "sha256": "303183b640190071d0588069ac0ba27e97cbc9d529e4cf3527f2f08688cb3be4" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.spec.ts", + "sha256": "74960c9216a2cf1c7956c7b3bc8af625146ee222b494822683066f95f3bcb4f5" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/solution/solution.component.html", + "sha256": "9a7f7428e8c3884c4fddc478f20f6a3b7f1794f027acd8ed3ac53a39ee09042d" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts", + "sha256": "4f68ce31ecfdc87e823bc436795fe42f84a220bdff7e3426dd6d213b734d2781" + } + ], + "logs": [ + { + "file": "procollab-form-qa-unit-before.log", + "sha256": "83ffad217305ff003995183c81c55d57fd0799576ffca3f7b3d6b44ea14c03ef" + }, + { + "file": "procollab-form-qa-unit-after.log", + "sha256": "6361b3809752b5ac26c73e1d833d509f4a6338b00e70cb3ce366f06ad9b33938" + }, + { + "file": "procollab-form-qa-test-final.log", + "sha256": "d0b9435b134c4ed2b46e2ca69fac2b587533bab9307fc213949637c0d3441c86" + }, + { + "file": "procollab-form-qa-build-final.log", + "sha256": "6e5f11f68304dcb7000d62ba5365b03615fb9c2eff64e3e161a4f037cd408268" + }, + { + "file": "procollab-form-qa-verify-final.log", + "sha256": "be155d849c00cd887021916e2cefc57bc2ad2ef1c86335e5c1338cb48d4ddb36" + } + ], + "credentialsIncluded": false, + "notes": [ + "Raw wire and failed attempts kept locally, committed reports are curated.", + "Baseline captured after fixture setup, before tested API/UI writes; no pre-seed SHA claim.", + "9 screenshots: initial product FAIL, 2 harness failures and 6 main states, plus 1 final copy screenshot; actual file inventory is authoritative.", + "Initial false warning, hackathon manager at390 and noncompetitive draft/final copy at1440 visually inspected; screenshots may reflect nested scroll position." + ] +} diff --git a/docs/qa/program-solutions-form-settings-20261025/noncompetitive-copy-final.json b/docs/qa/program-solutions-form-settings-20261025/noncompetitive-copy-final.json new file mode 100644 index 000000000..6db5b0973 --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/noncompetitive-copy-final.json @@ -0,0 +1,10 @@ +{ + "status": "PASS", + "program": 29, + "solution": 44, + "correctHelp": true, + "noSubmitButton": true, + "zeroFalsePreserved": true, + "programMutationRequests": [], + "javascriptErrors": [] +} diff --git a/docs/qa/program-solutions-form-settings-20261025/setup-recovery.json b/docs/qa/program-solutions-form-settings-20261025/setup-recovery.json new file mode 100644 index 000000000..8aaadd8e3 --- /dev/null +++ b/docs/qa/program-solutions-form-settings-20261025/setup-recovery.json @@ -0,0 +1,42 @@ +{ + "prefix": "qa01f-20261025-a8f2c339", + "programs": [ + { + "id": 27, + "key": "case_championship", + "template": "case_championship", + "competitive": true + }, + { + "id": 28, + "key": "hackathon", + "template": "hackathon", + "competitive": true + }, + { + "id": 29, + "key": "noncompetitive", + "template": "case_championship", + "competitive": false + } + ], + "baselineTiming": "Captured after isolated program/registration setup and before any tested API or browser writes. Initial seed committed three empty programs but failed generating manifest because Expert has a non-id primary key. No pre-seed SHA is claimed.", + "baselineExcludedSetupRows": { + "partner_programs.PartnerProgram": [27, 28, 29], + "partner_programs.PartnerProgramField": [], + "partner_programs.PartnerProgramUserProfile": [83, 84, 85], + "partner_programs.ProgramSolution": [], + "partner_programs.ProgramSolutionAnswer": [], + "partner_programs.ProgramSolutionMember": [], + "partner_programs.SolutionInvitation": [], + "project_rates.SolutionScore": [], + "project_rates.SolutionExpertAssignment": [], + "project_rates.Criteria": [], + "users.Expert": [], + "files.UserFile": [], + "projects.Project": [], + "users.Expert_programs": [], + "partner_programs.PartnerProgram_managers": [32, 33, 34] + }, + "source": "ad3945ea2ebcd214a26b64ab09c92d334f9d2c77" +} diff --git a/docs/qa/program-solutions-invitations-20261025/README.md b/docs/qa/program-solutions-invitations-20261025/README.md new file mode 100644 index 000000000..1f9e94127 --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/README.md @@ -0,0 +1,34 @@ + + +# QA01: приглашения и последнее место в команде + +Проверены КЧ и хакатон на настоящих Angular, Django и PostgreSQL. Два конкурентных приглашения разным людям резервируют одно последнее место. Проверены границы роли, недоступные получатели, сохранение ввода при ошибке, изменение роли с конфликтом версий и отзыв. Продуктовый код этого checkpoint не менялся. Общие QA01/REL01 остаются открытыми. + +Backend source: `6db9050` (runtime `2ad4743`, разница только в CI timeout); frontend source/build: `0415dddd`; C00 `1.13.0`. Только синтетические программы 15/16, решения 20/21, prefix `qa01i-20261025-7ed1e705`. Локальные API 8871/8877, frontend 8876, PostgreSQL 18 на 55439; locmem mail, внешняя сеть браузера заблокирована. Пароли, JWT и credential manifests в Git не включены. + +| Проверка | Результат и evidence | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| INV01: разные получатели, последнее место | Два HTTP запроса через разные API процессы с одной версией решения. Удерживалась настоящая строка программы; два ожидающих PostgreSQL PID и пути блокировки до контроллера сохранены. Один ответ 201, второй 409/version_conflict. [Race](last-slot-http-evidence.json). | +| INV02: резерв и принятие | После гонки accepted/pending/reserved = 1/1/2 при max=2. Повтор проигравшего с актуальной версией дает 409/team_max_size без изменения версии. Принятие победителя дает 2/0/2. | +| INV03: получатель | Капитан, принятый участник и pending недоступны с соответствующей причиной. Введенное имя не заменяет выбор участника. Очистка выбора выключает отправку. | +| INV04: роль и ошибка лимита | Пустая/пробельная роль и 129 символов выключают отправку; 1/128 разрешены. Подсказка выбирается клавиатурой, свободная роль сохраняется. Реальный 409/team_max_size оставляет получателя и роль в окне, данные заявки не меняются. | +| INV05: изменение роли | Те же границы в окне изменения. Отдельный HTTP PATCH меняет версию, UI получает настоящий 409/version_conflict и сохраняет свой ввод после refresh. Явный повтор дает 200 и нормализует пробелы. | +| INV06: отзыв | Нативное подтверждение, DELETE 204, pending исчезает. Для проверки свободного места принятый синтетический участник исключен обычным UI DELETE 200. История accepted приглашения сохранена. | +| INV07: адаптив | На 320/390/768/1440 px проверены страница и внутренние строки с фактически отображаемой ролью из 128 символов, затем финальная команда. Переполнения нет. Финальные 390/1440 screenshots просмотрены; хеши в manifest. | +| INV08: контракт и данные | [Verifier](domain-and-contract-evidence.json): 120 захваченных HTTP ответов и 18 JSON запросов соответствуют C00; race harness записал 20 HTTP событий. Hash прежних строк десяти выбранных моделей совпал, число Project не менялось, verifier не писал domain data. | + +Браузерный [отчет](browser-checks.json) содержит 16 уникальных проверок на двух формах и счетчики реальных ответов нескольких запусков; это не один непрерывный прогон. Полный raw wire сохранен локально и захеширован в manifest. JavaScript errors = 0. После проверок оба решения — captain-only drafts, без ответов, pending, сдачи, snapshot, публикации, оценок и назначений. Состояние accepted=2 в race evidence — предшествующий checkpoint, а не финальное состояние fixtures. + +## Воспроизводимость и ограничения + +Три начальных наблюдения гонки не доказывали одновременное ожидание двух запросов: сначала поиск `FOR UPDATE` не учитывал обрезку `pg_stat_activity.query` до 1 KiB; затем наблюдался один worker; третий монитор искал только прямого блокировщика. Финальный монитор учитывает транзитивные `pg_blocking_pids`: второй запрос может ждать первый, который ждет контроллер. Pending от неудачных наблюдений отозваны обычными DELETE 204; решение не удалялось и не создавалось заново. Все три recovery reports сохранены. + +Начальный browser harness ожидал 204 для удаления участника, хотя C00 возвращает 200 с обновленным решением. Другие частичные запуски исправили управление фокусом открытых подсказок и ожидание Angular DOM. Первая съемка адаптива захватила refresh без строки приглашения. Она сохранена отдельно; финальный прогон дожидается роли длиной 128 на каждой ширине и подтверждает ее наличие в JSON отчета. Продуктовых исправлений по этим наблюдениям не потребовалось. Исходные FAIL и отчет до уточнения ожиданий сохранены локально и захешированы. Шесть скопированных evidence JSON равны исходникам по содержимому; raw browser reports в Git не дублируются. + +Скриншоты и raw artifacts находятся в `design-artifacts/program-solutions-20261024/qa01-invitations`; [manifest](local-artifacts.json) содержит их SHA256 и хеши использованных helpers. Remote avatars блокировались локальной сетевой политикой. Полные desktop flows, a11y/zoom, нагрузка, production CDN, организаторский/публичный UI и весь первый выпуск этим checkpoint не закрыты. + +## CI + +[PostgreSQL CI текущего backend 6db9050](https://github.com/PROCOLLAB-github/api/actions/runs/40166190670) — SUCCESS: 39 locking/vacancy tests и полный набор 1669 tests, оба итоговых Ran/OK проверены по логу. [General CI того же commit](https://github.com/PROCOLLAB-github/api/actions/runs/40166187673) — SUCCESS, Tests/Lint; лог подтверждает Ran 1669, OK (skipped=54). Это заменяет прежние pending статусы; старый отмененный 30-минутный run не считается PASS. Backend повторно не менялся и не отправлялся в CI. + +Merge/deploy и переключение реальных программ не выполнялись. D02/D03, административная политика после сдачи и конкурсная публикация остаются открытыми; новые ответы пользователя по ним не подставлялись. diff --git a/docs/qa/program-solutions-invitations-20261025/baseline-evidence.json b/docs/qa/program-solutions-invitations-20261025/baseline-evidence.json new file mode 100644 index 000000000..db428c22e --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/baseline-evidence.json @@ -0,0 +1,15 @@ +{ + "counts": { + "partner_programs.PartnerProgram": 14, + "partner_programs.PartnerProgramField": 94, + "partner_programs.ProgramSolution": 18, + "partner_programs.ProgramSolutionAnswer": 85, + "partner_programs.ProgramSolutionMember": 32, + "partner_programs.SolutionInvitation": 17, + "project_rates.SolutionScore": 12, + "project_rates.SolutionExpertAssignment": 4, + "files.UserFile": 8, + "projects.Project": 2 + }, + "sha256": "c3eaf24667d4655e8b91918ad7167ba9d7777050e9006a9107fe6c141e87363c" +} diff --git a/docs/qa/program-solutions-invitations-20261025/browser-checks.json b/docs/qa/program-solutions-invitations-20261025/browser-checks.json new file mode 100644 index 000000000..bfdcae804 --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/browser-checks.json @@ -0,0 +1,189 @@ +{ + "status": "PASS", + "prefix": "qa01i-20261025-7ed1e705", + "checks": [ + { + "program": 15, + "check": "captain and accepted member unavailable with reasons" + }, + { + "program": 15, + "check": "full team error preserves recipient and role without mutation" + }, + { + "program": 15, + "check": "selection clear; blank/whitespace/1/128/129 role; keyboard suggestion and free role; create201 reserves one slot" + }, + { + "program": 15, + "check": "long-role", + "fits": [ + { + "width": 320, + "page": true, + "rows": true, + "pendingRoles": [128] + }, + { + "width": 390, + "page": true, + "rows": true, + "pendingRoles": [128] + }, + { + "width": 768, + "page": true, + "rows": true, + "pendingRoles": [128] + }, + { + "width": 1440, + "page": true, + "rows": true, + "pendingRoles": [128] + } + ] + }, + { + "program": 15, + "check": "pending recipient unavailable" + }, + { + "program": 15, + "check": "edit bounds; genuine version conflict retains local role; explicit retry200 normalizes whitespace" + }, + { + "program": 15, + "check": "native revoke confirmation DELETE204; captain-only draft; no submit" + }, + { + "program": 15, + "check": "final-team", + "fits": [ + { + "width": 320, + "page": true, + "rows": true, + "pendingRoles": [] + }, + { + "width": 390, + "page": true, + "rows": true, + "pendingRoles": [] + }, + { + "width": 768, + "page": true, + "rows": true, + "pendingRoles": [] + }, + { + "width": 1440, + "page": true, + "rows": true, + "pendingRoles": [] + } + ] + }, + { + "program": 16, + "check": "captain and accepted member unavailable with reasons" + }, + { + "program": 16, + "check": "full team error preserves recipient and role without mutation" + }, + { + "program": 16, + "check": "selection clear; blank/whitespace/1/128/129 role; keyboard suggestion and free role; create201 reserves one slot" + }, + { + "program": 16, + "check": "long-role", + "fits": [ + { + "width": 320, + "page": true, + "rows": true, + "pendingRoles": [128] + }, + { + "width": 390, + "page": true, + "rows": true, + "pendingRoles": [128] + }, + { + "width": 768, + "page": true, + "rows": true, + "pendingRoles": [128] + }, + { + "width": 1440, + "page": true, + "rows": true, + "pendingRoles": [128] + } + ] + }, + { + "program": 16, + "check": "pending recipient unavailable" + }, + { + "program": 16, + "check": "edit bounds; genuine version conflict retains local role; explicit retry200 normalizes whitespace" + }, + { + "program": 16, + "check": "native revoke confirmation DELETE204; captain-only draft; no submit" + }, + { + "program": 16, + "check": "final-team", + "fits": [ + { + "width": 320, + "page": true, + "rows": true, + "pendingRoles": [] + }, + { + "width": 390, + "page": true, + "rows": true, + "pendingRoles": [] + }, + { + "width": 768, + "page": true, + "rows": true, + "pendingRoles": [] + }, + { + "width": 1440, + "page": true, + "rows": true, + "pendingRoles": [] + } + ] + } + ], + "javascriptErrors": [], + "capturedResponses": 120, + "responseCounts": { + "GET 200": 97, + "POST 409": 2, + "POST 201": 4, + "PATCH 200": 8, + "PATCH 409": 4, + "DELETE 204": 4, + "DELETE 200": 1 + }, + "provenance": "Completed functional checkpoints retain earlier actual responses. Initial long-role screenshots caught transient refresh; final run waits for the actual 128-character pending row at every width. Superseded report/screenshots preserved.", + "rawFile": "browser-evidence.json", + "rawSha256": "cd7cf6d47476be4db2021f35e02b851e8c792bcc40eccf396b02556dbe9cc153", + "rawArtifacts": "Full captured wire and partial harness reports remain in sourceDirectory; this file is the reviewable check summary, not their replacement." +} diff --git a/docs/qa/program-solutions-invitations-20261025/domain-and-contract-evidence.json b/docs/qa/program-solutions-invitations-20261025/domain-and-contract-evidence.json new file mode 100644 index 000000000..49b5f9a5d --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/domain-and-contract-evidence.json @@ -0,0 +1,56 @@ +{ + "status": "PASS", + "prefix": "qa01i-20261025-7ed1e705", + "browserCapturedResponses": 120, + "browserCapturedRequestsValidated": 18, + "raceHttpChecks": 20, + "c00": "1.13.0", + "programs": [ + { + "program_id": 15, + "solution_id": 20, + "template": "case_championship", + "version": 18, + "accepted_count": 1, + "pending_count": 0, + "answers": 0, + "submitted": false, + "published": false, + "retainedAcceptedInvitationHistory": true + }, + { + "program_id": 16, + "solution_id": 21, + "template": "hackathon", + "version": 12, + "accepted_count": 1, + "pending_count": 0, + "answers": 0, + "submitted": false, + "published": false, + "retainedAcceptedInvitationHistory": true + } + ], + "previousSelectedDomainSha256": "c3eaf24667d4655e8b91918ad7167ba9d7777050e9006a9107fe6c141e87363c", + "previousSelectedDomainUnchanged": true, + "previousModels": [ + "partner_programs.PartnerProgram", + "partner_programs.PartnerProgramField", + "partner_programs.ProgramSolution", + "partner_programs.ProgramSolutionAnswer", + "partner_programs.ProgramSolutionMember", + "partner_programs.SolutionInvitation", + "project_rates.SolutionScore", + "project_rates.SolutionExpertAssignment", + "files.UserFile", + "projects.Project" + ], + "projectCountUnchanged": true, + "verificationDomainMutations": 0, + "provenance": [ + "Race had three insufficient harness observations; their pending invitations were revoked through ordinary API and the same solution was preserved. The successful probe observes transitive pg_blocking_pids paths for two independent local API processes.", + "Browser report accumulates completed checkpoints from preserved failed harness attempts. Initial member-removal assertion incorrectly expected204 instead of C00 200; later attempts corrected focus/DOM wait handling. These are not product fixes.", + "All final browser checks run against existing frontend0415dddd/backend6db9050(runtime2ad4743); no product code changed for this checkpoint.", + "The race checkpoint accepted2/pending0 precedes browser removal. Final fixtures are captain-only drafts; no submit or publication occurred." + ] +} diff --git a/docs/qa/program-solutions-invitations-20261025/last-slot-http-evidence.json b/docs/qa/program-solutions-invitations-20261025/last-slot-http-evidence.json new file mode 100644 index 000000000..7b1eccce3 --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/last-slot-http-evidence.json @@ -0,0 +1,288 @@ +{ + "status": "PASS", + "prefix": "qa01i-20261025-7ed1e705", + "programs": [ + { + "id": 15, + "template": "case_championship", + "solutionId": 20, + "raceComplete": true, + "winnerRole": "member", + "loserRole": "pending_a", + "winnerInvitationId": 37, + "blockedWorkers": [ + { + "pid": 7336, + "blockers": [38424], + "pathToController": [7336, 38424] + }, + { + "pid": 21196, + "blockers": [7336], + "pathToController": [21196, 7336, 38424] + } + ], + "controllerPid": 38424, + "raceStatuses": [201, 409], + "workerOrigins": ["http://127.0.0.1:8871", "http://127.0.0.1:8877"], + "loserRetryCode": "team_max_size", + "finalTeam": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T05:56:48.633623Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Участник гонки", + "specialization": null, + "joined_at": "2026-10-25T06:08:18.562916Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 2, + "is_frozen": false + } + }, + { + "id": 16, + "template": "hackathon", + "solutionId": 21, + "raceComplete": true, + "winnerRole": "member", + "loserRole": "pending_a", + "winnerInvitationId": 38, + "blockedWorkers": [ + { + "pid": 17932, + "blockers": [33548], + "pathToController": [17932, 33548, 38424] + }, + { + "pid": 33548, + "blockers": [38424], + "pathToController": [33548, 38424] + } + ], + "controllerPid": 38424, + "raceStatuses": [201, 409], + "workerOrigins": ["http://127.0.0.1:8871", "http://127.0.0.1:8877"], + "loserRetryCode": "team_max_size", + "finalTeam": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T06:08:18.730001Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Участник гонки", + "specialization": null, + "joined_at": "2026-10-25T06:08:19.062546Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 2, + "is_frozen": false + } + } + ], + "events": [ + { + "role": "captain", + "method": "POST", + "path": "/programs/15/solutions/", + "status": 201, + "code": null + }, + { + "role": "captain", + "method": "DELETE", + "path": "/solution-invitations/34/?expected_version=1&expected_solution_version=2", + "status": 204, + "code": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/15/solutions/", + "status": 200, + "code": null + }, + { + "role": "captain", + "method": "DELETE", + "path": "/solution-invitations/35/?expected_version=1&expected_solution_version=4", + "status": 204, + "code": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/15/solutions/", + "status": 200, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "DELETE", + "path": "/solution-invitations/36/?expected_version=1&expected_solution_version=6", + "status": 204, + "code": null + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/15/solutions/", + "status": 200, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/15/solutions/20/invitations/", + "status": 201, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/15/solutions/20/invitations/", + "status": 409, + "code": "version_conflict", + "origin": "http://127.0.0.1:8877" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/15/solutions/20/", + "status": 200, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/15/solutions/20/invitations/", + "status": 409, + "code": "team_max_size", + "origin": "http://127.0.0.1:8871" + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/37/accept/", + "status": 200, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/15/solutions/20/", + "status": 200, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/16/solutions/", + "status": 201, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/16/solutions/21/invitations/", + "status": 201, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/16/solutions/21/invitations/", + "status": 409, + "code": "version_conflict", + "origin": "http://127.0.0.1:8877" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/16/solutions/21/", + "status": 200, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/16/solutions/21/invitations/", + "status": 409, + "code": "team_max_size", + "origin": "http://127.0.0.1:8871" + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/38/accept/", + "status": 200, + "code": null, + "origin": "http://127.0.0.1:8871" + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/16/solutions/21/", + "status": 200, + "code": null, + "origin": "http://127.0.0.1:8871" + } + ], + "preexistingDomainUnchanged": true, + "projectCountUnchanged": true, + "limitations": [ + "Real concurrent HTTP requests intentionally mutate only fresh synthetic fixtures", + "Controller holds a real program row lock until both API workers are observed blocked via pg_blocking_pids", + "Role UI will reuse the draft fixtures; this is a captured race checkpoint, not their permanent final state" + ] +} diff --git a/docs/qa/program-solutions-invitations-20261025/local-artifacts.json b/docs/qa/program-solutions-invitations-20261025/local-artifacts.json new file mode 100644 index 000000000..fdaa4fee7 --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/local-artifacts.json @@ -0,0 +1,172 @@ +{ + "sourceDirectory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-invitations", + "backendSource": "6db9050c2d1eb7313006a285566501a104dbc415", + "backendRuntime": "2ad47438e666963819665222253d9b0672df074f", + "frontendSource": "0415ddddc21f465d547835b20d98e7facc7d5a4f", + "json": [ + { + "file": "baseline-evidence.json", + "copied": true, + "sourceRawSha256": "e7f80ffdc397438bc1b5d5971d4630a4d067fbf3c9854984dc35ecf2d5375bdb", + "semanticSha256": "0f516a5146e953cfdee865cdf04d2ba1cfdd85952dba5cc0c02205fb4e51139e" + }, + { + "file": "browser-attempt-1792908609335.json", + "copied": false, + "sourceRawSha256": "f64caa0e15d3f43f92fe0b4221edd4f311d79a501d783f759983a3de50af7a7a", + "semanticSha256": "920552fadedc08ad1381a6880fd8a92a58b3fa7ae2e22993f6fe15388bba08ca" + }, + { + "file": "browser-attempt-1792908651112.json", + "copied": false, + "sourceRawSha256": "a83349de2118d0d0f5e3a3f6e3b3dec352ad2fba81c7cda3fcb3329a370d4cb9", + "semanticSha256": "2017bc00fed5646a057c016470694f4c4a245707accdd78a416bacbb33947369" + }, + { + "file": "browser-attempt-1792908689761.json", + "copied": false, + "sourceRawSha256": "68c59271bf3d1b11c25c924277d41e94dd9589caf3f17436a2b9fc2b69e97ea4", + "semanticSha256": "2d6ced90818b5b1fd9ea21de25c62288078e3eb2d1bb17e489d89635a52cbca9" + }, + { + "file": "browser-attempt-1792908708554.json", + "copied": false, + "sourceRawSha256": "2c7f3c84457f8a2b4cb79a2b80bc2187ec7b92ab38ee44b615855dc36426a30b", + "semanticSha256": "af83b93bf07c0f85c1c13d3602f37fa95d5de59755d8e1a8b84812f7a332a911" + }, + { + "file": "browser-attempt-1792908741122.json", + "copied": false, + "sourceRawSha256": "9ac61768d8d0f99e166a57b91b12c0ee46f046a585090627e30f04765cb405a3", + "semanticSha256": "9bfaecaabf9ce850c7d1995bc6e9df83c1377f2d2178f845f3953fa6098241fc" + }, + { + "file": "browser-evidence-before-stable-layout.json", + "copied": false, + "sourceRawSha256": "67a97b906fde3918a5a011595ff2cd026d7643327618992f4201e9a65df2e71e", + "semanticSha256": "d5d51a47430d79f51667771ba66bbe45ca0a937c2c8474f7ffe41e6f945e5588" + }, + { + "file": "browser-evidence.json", + "copied": false, + "sourceRawSha256": "cd7cf6d47476be4db2021f35e02b851e8c792bcc40eccf396b02556dbe9cc153", + "semanticSha256": "45cea53878e3036cfa511cc5ce9f6909cafb06e1511c9b49e9f7e2013a86c72a" + }, + { + "file": "domain-and-contract-evidence.json", + "copied": true, + "sourceRawSha256": "476a98247ac2a3c3d6dbd5e37ac81fbdbc6a8e8eb49127f2a0c1add749566fa8", + "semanticSha256": "154ae22bc97aaf37516871849b48d301ccdded14e6384c507850418f4aa24dc5" + }, + { + "file": "last-slot-http-evidence.json", + "copied": true, + "sourceRawSha256": "9d360c5e5417d851d9438beddb39c173727986a425a86efe1948d02e573b3a76", + "semanticSha256": "387ee9709a83e53f3f04f8a0bc7f97a2e2e3c1bfb12719d69ecfe50f390b655e" + }, + { + "file": "monitor-recovery-second.json", + "copied": true, + "sourceRawSha256": "bda41d038a80d2d3dee9aab96edd05aedff1d8253784e044426dd5e494608560", + "semanticSha256": "2c8ad956d64d85cb24530b241e803dabfd4e281c2835292633678ab792aa609d" + }, + { + "file": "monitor-recovery-third.json", + "copied": true, + "sourceRawSha256": "e4ae46664e01b5870bd271069c8185204bea45821ceab88f08e962008998fc0e", + "semanticSha256": "ec08ac3565da35f20f155c6be14e2e0b53ee0107cfc4d7166bd742bf6eb8e331" + }, + { + "file": "monitor-recovery.json", + "copied": true, + "sourceRawSha256": "8e76f038a8dfbe2814e939a240ed5152acd5fb41721f9eb5759f0c2aaa17cb46", + "semanticSha256": "f734c8e0d90d2ab19df812d8aaf51407169b131cf252afb99eb20f2e17b4955b" + } + ], + "screenshots": [ + { + "file": "browser-error.png", + "sha256": "bb558d5ef553760ba35b11a9bb7783f0b6731fc1374fba8e05536c9408509268", + "bytes": 44123 + }, + { + "file": "case_championship-final-team-1440.png", + "sha256": "91f1e4092f495332a84360b6b6ece26dbce0769cd042030dbb2379ff1bbfc6e7", + "bytes": 90871 + }, + { + "file": "case_championship-final-team-390.png", + "sha256": "07128d805a87a6453e3529517494637d88a11d8aaee903e125c6f55b02b62c1f", + "bytes": 54617 + }, + { + "file": "case_championship-long-role-1440.png", + "sha256": "12e03f669820a31697bc26969dedb523e03478f0c51b371bfd3e69124c744500", + "bytes": 99139 + }, + { + "file": "case_championship-long-role-390.png", + "sha256": "fde0422c9ad497d5ceba387f279078a97566d06b53ec81fb8db38c28133914b3", + "bytes": 62091 + }, + { + "file": "hackathon-final-team-1440.png", + "sha256": "e8097352a6b5df0fb49179a6b4a92c2fc7b5c8deb0bdd89e72c73ef5539d03e1", + "bytes": 89863 + }, + { + "file": "hackathon-final-team-390.png", + "sha256": "007c749d682d4af411b2392aeec5c74a588fd609c0b7ea940675bff04da66d96", + "bytes": 53080 + }, + { + "file": "hackathon-long-role-1440.png", + "sha256": "968169730d8cffd351d397a6abb603e73a03cc6ec510b661aa5eb0f5de94c1b0", + "bytes": 98009 + }, + { + "file": "hackathon-long-role-390.png", + "sha256": "9a3991d65164c63cb46c32b853533c9fea099c31310cba1a0d6552d6435d211a", + "bytes": 60490 + }, + { + "file": "transient-case_championship-long-role-1440.png", + "sha256": "55ebf7075603adabc794e1a77a5e2e8cb7cdabd78754f586693cb0c81df6ecad", + "bytes": 99121 + }, + { + "file": "transient-case_championship-long-role-390.png", + "sha256": "c964f4edee3a3afb2b37fd71ce25945a9a0f6378c0fd0587fe2c2ffbe6ddb941", + "bytes": 54689 + }, + { + "file": "transient-hackathon-long-role-1440.png", + "sha256": "a555248593cb01a5349fe62bd4449f7a0602e2beb96afe6a3aad96e08a5c7b68", + "bytes": 91757 + }, + { + "file": "transient-hackathon-long-role-390.png", + "sha256": "71e4599481e79db11437f01c16bd10932e6880d44798a336ead79b8d864bde59", + "bytes": 55116 + } + ], + "helpers": [ + { + "file": "procollab-qa01-invitations-race.py", + "sha256": "180384db4b3c7010afd79f294fd34a1bc8b3d04c27f0fec3e513f9d62b801449" + }, + { + "file": "procollab-qa01-invitations-browser.cjs", + "sha256": "e6bebdbfd9416ceed32dc167b81c62f9c359caa67edb35653c7ead44f547ef14" + }, + { + "file": "procollab-qa01-invitations-verify.py", + "sha256": "9c1e0999c3dd626c7d48157bf40815c229445e395332d664418c3a3226e87617" + }, + { + "file": "procollab-qa01-invitations-worker.py", + "sha256": "f3a64a1939c5719343abe58552910bc82ca5717b99e27bfe322c048aa4bfc0b3" + } + ], + "credentialsIncluded": false +} diff --git a/docs/qa/program-solutions-invitations-20261025/monitor-recovery-second.json b/docs/qa/program-solutions-invitations-20261025/monitor-recovery-second.json new file mode 100644 index 000000000..424a7d5d3 --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/monitor-recovery-second.json @@ -0,0 +1,10 @@ +{ + "reason": "Second monitor observed only one blocked API worker; two-worker concurrency was not established. The next probe uses two separate local API processes.", + "program_id": 15, + "solution_id": 20, + "revokedPendingId": 35, + "status": 204, + "solutionPreserved": true, + "sourceVersion": 4, + "resultVersion": 5 +} diff --git a/docs/qa/program-solutions-invitations-20261025/monitor-recovery-third.json b/docs/qa/program-solutions-invitations-20261025/monitor-recovery-third.json new file mode 100644 index 000000000..5e21080ec --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/monitor-recovery-third.json @@ -0,0 +1,10 @@ +{ + "reason": "Third monitor required direct blocking by the controller; this does not observe a second waiter blocked through the first. The next monitor records transitive pg_blocking_pids paths as evidence instead of assuming all waiters have the same immediate blocker.", + "program_id": 15, + "solution_id": 20, + "revokedPendingId": 36, + "status": 204, + "solutionPreserved": true, + "sourceVersion": 6, + "resultVersion": 7 +} diff --git a/docs/qa/program-solutions-invitations-20261025/monitor-recovery.json b/docs/qa/program-solutions-invitations-20261025/monitor-recovery.json new file mode 100644 index 000000000..319dc3917 --- /dev/null +++ b/docs/qa/program-solutions-invitations-20261025/monitor-recovery.json @@ -0,0 +1,11 @@ +{ + "reason": "first monitor incorrectly searched truncated pg_stat_activity SQL for trailing FOR UPDATE", + "trackActivityQuerySize": "1kB", + "actualProgramLockSqlLength": 2003, + "forUpdateOffset": 1993, + "program_id": 15, + "solution_id": 20, + "revokedPendingId": 34, + "status": 204, + "solutionPreserved": true +} diff --git a/docs/qa/program-solutions-legacy-acceptance-20261025/README.md b/docs/qa/program-solutions-legacy-acceptance-20261025/README.md new file mode 100644 index 000000000..7ec5bfcdc --- /dev/null +++ b/docs/qa/program-solutions-legacy-acceptance-20261025/README.md @@ -0,0 +1,17 @@ + + +# QA01: совместимость редактора Project + +25.10.2026. При проверке обычного Project3 и старой конкурсной заявки Project4/program34 обнаружена пустая вкладка «Данные для конкурсов» после перехода с команды. Выбранный additional renderer теперь загружается сразу при выборе вкладки (`@defer (on immediate)`). API, видимые поля и остальные вкладки сохранены. + +Product fix: d0b7ba6ea2087f979ca51fb0f6cb03e28d3c7845. Backend runtime db7ffe4d6124b16232685fb49f77105f1436bacf, C001.16.0 без изменений. [Общая приемка и границы evidence](https://github.com/PROCOLLAB-github/api/blob/f9ca4c45c020b9316a9b48910d77e994a867a29d/docs/qa/program-solutions-legacy-acceptance-20261025/README.md), [каноническое ТЗ](https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175). + +Новый parent integration test проверяет появление выбранной панели без idle callback. Дочерний renderer в нем stub; настоящие case/textarea и отправка проверены браузером. Содержательный red run до fix — один assert FAIL, после4/4 PASS; ранний cancelIdleCallback setup error отделен от этого red run. + +Полный frontend:2844 tests/480 files, strict C00 types, development build, scoped ESLint и locked Prettier3.8.3 по2017 исходным files PASS. Backend148 native PostgreSQL tests проверяют прежние проекты/привязки/команды/приглашения/сдачу/кейсы. Полный backend db7ffe4 CI:1717/OK на PG и1717/OK(skipped59) в General; это refs соответствующего backend source. + +Реальный браузер: сохранение обоих проектов, принятие приглашения, изменение роли, отклонение и отзыв, два принятых участника без pending. Обычный Project опубликован по прежнему save; старая заявка сохраняет конкурсные поля, отменяет подтверждение без сдачи и отправляется одним POST /programs/partner-program-projects/3/submit/. Использованы11 resumed invocations: часть фаз завершена до fix, поля и submit — после. Исторические harness failures/dynamic import сохранены; единый непрерывный зеленый run не заявляется. После submit editor guard возвращает на прежний Project detail. + +Все ранее существовавшие строки22 выбранных доменных моделей совпали с baseline. Намеренно созданы только2 свежих Project для проверки старого пути, новых ProgramSolution=0. Origins заменены только в generated development build copy; исходные environments не менялись. Raw: `Z:/Документы/PROCOLLAB-dev/design-artifacts/program-solutions-20261024/qa01-legacy`. Полная visual/a11y/zoom/CDN приемка не заявляется. + +Пользователь согласовал административные исключения после сдачи с обязательным сохранением старых программ/проектов; они требуют отдельной реализации. QA01/REL01, manager/public UI и отдельные согласования остаются открытыми. Merge/deploy/автоматического переноса данных не было. diff --git a/docs/qa/program-solutions-member-removal-20261025/README.md b/docs/qa/program-solutions-member-removal-20261025/README.md new file mode 100644 index 000000000..0d863b538 --- /dev/null +++ b/docs/qa/program-solutions-member-removal-20261025/README.md @@ -0,0 +1,15 @@ + + +# BE08B/FE API: исключение участника и типы истории + +25.10.2026. Frontend product32fe953667f09ed4d15e9c132bd8a8ae2415ccee, backend product1e037afe301792fb168879f3c32c546585610431. C001.18.0:136 схем/53 операции/250 независимых сценариев; три shared files byte-identical. [Backend scope и evidence](https://github.com/PROCOLLAB-github/api/blob/1e037afe301792fb168879f3c32c546585610431/docs/product/program-solutions-be08-member-actions.md). + +Generated DTO используют discriminated union change_case/remove_member. Старые case payloads сохранены. Port/repository/adapter и два use case передают выбранного пользователя и точный signed proof, возвращают member/effects/controlled errors. Безопасные program/solution/user IDs проверяются до запроса, ошибки не запускают automatic retry. История читает исключенного Member, draft null snapshots и смешанный список case/member. Strict types требуют token и отделяют SolutionRef от Project. + +74 case/member/history wire checks PASS, включая22 новых member и3 history. Unknown-query400 проверяется backend; action client не принимает произвольный query. Invalid user блокируется frontend до сети, missing-token400 отправляется намеренно как invalid request. Полный frontend2887 tests/482 files +strict types, development build/scoped ESLint/locked Prettier3.8.3 по2023 files PASS до отчета. + +Настоящий backend HTTP:57 responses/35 request bodies по C00; КЧ38/решение53 и хакатон39/решение54 после сдачи сохраняют материалы/0/false/оценки/назначения/регистрацию/accepted invite. Исключенный теряет private доступ, исходный состав остается в истории. Старые строки23 моделей, включая case revisions1/2, совпали; новых Project=0. Backend371 native solution +152 legacy tests/OK; локальная0036 сохраняет историю, Project handlers не менялись. + +Это API integration; новый organizer layout/routes/SCSS не добавлялся, D02 approval уже запрошен. Снятие/архив заявки, удаление регистрации/retention, остальные screens и полный QA01/REL01 остаются в первом выпуске. Backend/миграция0036 предшествуют frontend; без merge/deploy/реальных переключений/переноса Project. Файлы evidence совпадают с backend report;10 signed proof strings скрыты только в Git wire. + +Финальная locked format проверка с отчетом:2028 files, failures=[]; sanitized wire форматирован без изменения JSON-значений. diff --git a/docs/qa/program-solutions-member-removal-20261025/contract-evidence.json b/docs/qa/program-solutions-member-removal-20261025/contract-evidence.json new file mode 100644 index 000000000..bab158751 --- /dev/null +++ b/docs/qa/program-solutions-member-removal-20261025/contract-evidence.json @@ -0,0 +1,11 @@ +{ + "version": "1.18.0", + "schemas": 136, + "operations": 53, + "scenarios": 250, + "oldSchemasUnchanged": 130, + "oldScenariosUnchanged": 230, + "oldOperationsUnchanged": 51, + "sharedByteIdentical": 3, + "oldCaseSchemaPreserved": true +} diff --git a/docs/qa/program-solutions-member-removal-20261025/evidence.json b/docs/qa/program-solutions-member-removal-20261025/evidence.json new file mode 100644 index 000000000..eeaf368a4 --- /dev/null +++ b/docs/qa/program-solutions-member-removal-20261025/evidence.json @@ -0,0 +1,186 @@ +{ + "status": "PASS", + "scope": "BE08B member exclusion and retained revisions, not full QA01/REL01", + "backendProduct": "1e037afe301792fb168879f3c32c546585610431", + "frontendProduct": "32fe953667f09ed4d15e9c132bd8a8ae2415ccee", + "c00": { + "version": "1.18.0", + "schemas": 136, + "operations": 53, + "scenarios": 250, + "oldSchemasUnchanged": 130, + "oldScenariosUnchanged": 230, + "oldOperationsUnchanged": 51, + "sharedByteIdentical": 3, + "oldCaseSchemaPreserved": true + }, + "tests": { + "nativePostgresSolution": 371, + "newNative": 18, + "newObservedRaces": 3, + "legacyRegression": 152, + "frontend": 2887, + "frontendFiles": 482, + "wire": 74, + "newMemberWire": 22, + "newHistoryWire": 3, + "strictTypes": true, + "developmentBuild": true, + "scopedEslint": true, + "black": true, + "flake8": true, + "djangoCheck": true, + "migrationConsistency": true, + "lockedFormat": { + "prettier": "3.8.3", + "locked": "3.8.3", + "checked": 2023, + "failures": [], + "filesWithReport": 2028, + "finalFailures": [] + } + }, + "http": { + "status": "PASS", + "c00": "1.18.0", + "validation": { + "responses": 57, + "validRequests": 35 + }, + "responses": 57, + "programs": [ + { + "program": 38, + "solution": 53, + "change": 3, + "state": { + "accepted": true, + "submitted": true, + "evaluationSeeded": true, + "applied": true, + "complete": true + } + }, + { + "program": 39, + "solution": 54, + "change": 4, + "state": { + "accepted": true, + "submitted": true, + "evaluationSeeded": true, + "applied": true, + "complete": true + } + } + ], + "oldModelsUnchanged": 23, + "oldRowsHash": "ce0a2edae7fe3e68846a097634c0b39364512edb77e32ada8166e860c82bbd61", + "newProjects": 0, + "newSolutions": 2, + "newMemberRevisions": 2, + "originalCaseRevisionsPreserved": 2, + "noRealProgramsChanged": true + }, + "migration": { + "status": "PASS", + "migration": "0036_solution_member_admin_history", + "database": "local-procollab_ci", + "oldModelsUnchanged": 23, + "hashBefore": "ce0a2edae7fe3e68846a097634c0b39364512edb77e32ada8166e860c82bbd61", + "hashAfter": "ce0a2edae7fe3e68846a097634c0b39364512edb77e32ada8166e860c82bbd61", + "rowCounts": { + "partner_programs.PartnerProgram": 35, + "partner_programs.PartnerProgramField": 259, + "partner_programs.PartnerProgramUserProfile": 102, + "partner_programs.PartnerProgramProject": 3, + "partner_programs.PartnerProgramFieldValue": 2, + "partner_programs.ProgramSolution": 51, + "partner_programs.ProgramSolutionAnswer": 246, + "partner_programs.ProgramSolutionMember": 69, + "partner_programs.SolutionInvitation": 32, + "project_rates.SolutionScore": 28, + "project_rates.SolutionExpertAssignment": 16, + "project_rates.Criteria": 58, + "users.Expert": 7, + "files.UserFile": 8, + "projects.Project": 4, + "projects.Collaborator": 6, + "invites.Invite": 4, + "project_rates.ProjectScore": 3, + "project_rates.ProjectExpertAssignment": 1, + "industries.Industry": 1, + "users.Expert_programs": 21, + "partner_programs.PartnerProgram_managers": 39, + "partner_programs.SolutionAdministrativeChange": 2 + }, + "historyCaseRowsPreserved": 2, + "existingProjectsMigratedOrDeleted": 0, + "onlyLocalSchema": true + }, + "proofRedactions": 10, + "rawArtifacts": { + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\contract-evidence.json": "22e1e63322e4e358eb0523f0e53803d7ae510ac0a6fed355bd284fcf8cf3832a", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\http-evidence.json": "1456cc9a3914199e173326d4534635f6445cbcd01c3e6c7977a5f2dbe12410da", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\http-wire.json": "311015b3a4c6a2d333c607d419b102de8003557b3ab49bb6da164f16a9f7f283", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\migration-evidence.json": "586784a1f62bbe3777a9e9f2ece06c0d91ca830fd10001862e676c9aea82986c", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-black.log": "555e1c5b96b9a5560a570bc43c0342f9785f5038e526f64aa1848a84cb357132", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-build.log": "a7ed32725338e9e194e6dbe54c6e078a85ab7f7a4b52908379ae6767972eda79", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-check.log": "f87ceb15410bde28ff826ce26228e0576558258fa64d145847921a593a61ee63", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-eslint.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-flake8.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-focused.log": "01d12c90abaf62879655a0b323420c3533a3f0c1ff37b078e7ee142b4f7b7c0f", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-format.log": "e86b8d88d750e693566c0b612dcbd9b4bb504e559c34571b0e378768d1ad1b5d", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-frontend-suite.log": "b7c089de3ca4b5164662b87308e935a7fd128e698b8707a4e6418183a313ef33", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-http-resumed.log": "db3880acb0661ccce924d8d7f940b0eada9ff0dea2e7187f64dd072836d69b09", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-http.log": "3fb00bc2f1cd7857f7f24819cd59141d0ff82d563d62718602ec8737a5c01d55", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-legacy.log": "f89c4762ec22bab465df486e379ad329e377afbffeb260073c9de23ca857e31d", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-member-tests.log": "a983d4b862214d059f98f41f7eb78b264320f7b21a5697373f74b1e4c4f655e0", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-migrate.log": "8e22625ff9cdf489f3be02a91f12490d7bd82016c9f3dc34899bd383b8393b44", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-migration-check.log": "93e6e2d5be3c054add80c0f19101608803f7ca8a7df1fa41f76bbb9a1f570f4a", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-preceding-general-ci.log": "0fd9aca95f0626f120d52687d52e3c2864d79c0e5bad5f3d3cb0f1241eaa5c6f", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-solution-suite.log": "6424a5ac99535eb8426ef883af05fdc23cbb92b97a878fd8b89d04cfe66315ff", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-wire.log": "5ebc2483342ab76e2d00785b57a9335c6b0a5fe99324ffcc246bced3152d9c82", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-final-format-verified.log": "a0772a2489808c75967329d6f8ad737fb8d5f355300569bf121d307f16123f36", + "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-member-removal\\procollab-member-removal-final-contract.log": "d52e056d10a5aeab3cc964f1db6e6d4e0caf4335f84034873f0dcc6d60870cc8" + }, + "helpers": { + "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-member-removal-contract.py": "25bde53cdf012c028d5f8926bbf72bcd923fa3f065cacbe3bd750cb81ad5c9c1", + "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-member-removal-migrate.py": "0c18e8019ece0dda1c4f59f48ddebb2ad1edef8e398a00e0d5300c18988d9580", + "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-member-removal-http.py": "a2fbb149c9a737bc7aca31f60c3cd3839f6bb1a0b4499cd2dc1595c0d14ca62e", + "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-member-removal-package.py": "74293a724b1ce0f1013cfc683a5832a65ce8feba40bf4ab2a875fb1a03786941" + }, + "precedingCi": { + "head": "7656b472e263d5e28539a02ea44c2250944c6efb", + "general": { + "id": 40199193190, + "status": "completed/success", + "tests": 1730, + "skipped": 61 + }, + "postgres": { + "id": 40199197945, + "status": "in_progress" + }, + "notProofForNewProduct": true + }, + "limitations": [ + "Organizer UI requires already-requested D02 approval; no new UI/browser flow", + "Registration removal/detach/archive/retention still pending", + "D03 and competitive unsent publication eligibility pending", + "Full QA01/REL01 remain open, BE10 separate", + "No merge/deploy/real-program switching", + "First focused failures were test fixture registration/storage-mode/stale local instance assertions; corrected before full371 PASS", + "HTTP setup first attempt rolled back completely on helper role argument mismatch; no repeated submission or confirmed removal" + ], + "sourceHashes": { + "partner_programs/solution_models.py": "2f5b93bfab3fe506a80df0fdd0d1b6195d37cc03485eb1bdb1c6221e27cf6988", + "partner_programs/solution_history_api.py": "acf4d716f1777c38b68c4e18c13a2557352f833185e59d5468a0a66e1b34e3ac", + "partner_programs/solution_lifecycle_signals.py": "f2f47cefbbe479fa15d771befb3fdcf0e02b516d06282b7e3341882a402fea8e", + "partner_programs/solution_member_action_api.py": "37bffab4c21bf654dfaf69d6778e6c767cd72c3ce0d251ace3b37c6b3b6fec23", + "partner_programs/services/solution_member_actions.py": "e1fb60cb231404e6173ac1e1ed98c712e0170fd3cba5205cc6b8164db2c4501c", + "partner_programs/migrations/0036_solution_member_admin_history.py": "7334d04203350df3e61a28292555fe46be331b60feebb6427a5f0c8a72baa27b", + "partner_programs/tests/test_solution_member_actions.py": "e843e1ffa241425a6c07ae78fe05999045b61833c0aafd375f3a89ba1bcc89db", + "partner_programs/urls.py": "7eaed255ea776e6bc17ea01420f546430e9bbf8ebde884eed875b73f708912ba" + } +} diff --git a/docs/qa/program-solutions-member-removal-20261025/http-wire.json b/docs/qa/program-solutions-member-removal-20261025/http-wire.json new file mode 100644 index 000000000..c02228b83 --- /dev/null +++ b/docs/qa/program-solutions-member-removal-20261025/http-wire.json @@ -0,0 +1,4763 @@ +[ + { + "actor": "captain", + "method": "POST", + "path": "/programs/38/solutions/", + "status": 201, + "request": { + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "response": { + "id": 53, + "program_id": 38, + "version": 1, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:14.875029Z", + "updated_at": "2026-10-25T14:14:14.875029Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 38, + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T14:14:14.598410Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 271 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 272 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 273 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 274 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/38/solutions/53/", + "status": 200, + "request": null, + "response": { + "id": 53, + "program_id": 38, + "version": 1, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:14.875029Z", + "updated_at": "2026-10-25T14:14:14.875029Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 38, + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T14:14:14.598410Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 271 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 272 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 273 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 274 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/38/solutions/53/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 10, + "role": "Backend-разработчик" + }, + "response": { + "invitation": { + "id": 56, + "version": 1, + "program": { + "id": 38, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "solution": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T14:14:15.127265Z", + "updated_at": "2026-10-25T14:14:15.127265Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/56/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 56, + "version": 2, + "program": { + "id": 38, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "solution": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T14:14:15.127265Z", + "updated_at": "2026-10-25T14:14:15.272157Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/38/solutions/53/", + "status": 200, + "request": null, + "response": { + "id": 53, + "program_id": 38, + "version": 3, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:14.875029Z", + "updated_at": "2026-10-25T14:14:15.275159Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 38, + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T14:14:14.598410Z", + "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": 271 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 272 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 273 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 274 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/38/solutions/53/", + "status": 200, + "request": { + "expected_version": 3, + "expected_form_revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "answers": [ + { + "field_id": 271, + "value": "Город" + }, + { + "field_id": 272, + "value": "Материалы qa01mr-20261025-60d40caf" + }, + { + "field_id": 273, + "value": 0 + }, + { + "field_id": 274, + "value": false + } + ] + }, + "response": { + "id": 53, + "program_id": 38, + "version": 4, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:14.875029Z", + "updated_at": "2026-10-25T14:14:15.518344Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 38, + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 271, + "value": "Город" + }, + { + "field_id": 272, + "value": "Материалы qa01mr-20261025-60d40caf" + }, + { + "field_id": 273, + "value": 0 + }, + { + "field_id": 274, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T14:14:14.598410Z", + "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 + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/preview/", + "status": 200, + "request": { + "user_id": 10 + }, + "response": { + "application": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "member": { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false + }, + "solution_version": 4, + "is_submitted": false, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 0, + "errors": [], + "can_apply": true, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/38/solutions/53/submit/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99" + }, + "response": { + "id": 53, + "program_id": 38, + "version": 5, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:14.875029Z", + "updated_at": "2026-10-25T14:14:15.823471Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "program_id": 38, + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 271, + "value": "Город" + }, + { + "field_id": 272, + "value": "Материалы qa01mr-20261025-60d40caf" + }, + { + "field_id": 273, + "value": 0 + }, + { + "field_id": 274, + "value": false + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T14:14:14.598410Z", + "submitted_at": "2026-10-25T14:14:15.819475Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/apply/", + "status": 409, + "request": { + "user_id": 10, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Состав или данные решения изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/preview/", + "status": 403, + "request": { + "user_id": 10 + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "member", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/preview/", + "status": 403, + "request": { + "user_id": 10 + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "rated", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/preview/", + "status": 403, + "request": { + "user_id": 10 + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": null, + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/preview/", + "status": 401, + "request": { + "user_id": 10 + }, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "rated", + "method": "GET", + "path": "/programs/38/solutions/53/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 38, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "solution": { + "id": 53, + "program_id": 38, + "version": 5, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:14.875029Z", + "updated_at": "2026-10-25T14:14:15.823471Z", + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ], + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "program_id": 38, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 271 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 272 + }, + { + "value": 0, + "field_id": 273 + }, + { + "value": false, + "field_id": 274 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T14:14:15.925692Z", + "submitted_at": "2026-10-25T14:14:15.819475Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "137b0820da62b5af55ced3e0d4f27f99e983be14c9c16787a1e35ea8efd89c10", + "criteria": [ + { + "id": 68, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 69, + "name": "Проверка нуля 1", + "description": null, + "type": "int", + "min_value": 0.0, + "max_value": 10.0, + "value": "0" + }, + { + "id": 70, + "name": "Проверка false 2", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": false, + "rated_count": 0, + "max_rates": 1, + "required_evaluators": 2, + "deadline": "2026-10-27T14:14:14.598410Z" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/preview/", + "status": 200, + "request": { + "user_id": 9 + }, + "response": { + "application": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "member": { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true, + "specialization": null + }, + "solution_version": 5, + "is_submitted": true, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 2, + "errors": [ + { + "path": "user_id", + "code": "captain_cannot_be_removed", + "detail": "Капитана нельзя исключить из состава. Для него снимите заявку с участия.", + "field_id": null + } + ], + "can_apply": false, + "preview_token": null + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/preview/", + "status": 200, + "request": { + "user_id": 10 + }, + "response": { + "application": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "member": { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false, + "specialization": null + }, + "solution_version": 5, + "is_submitted": true, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 2, + "errors": [], + "can_apply": true, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/apply/", + "status": 200, + "request": { + "user_id": 10, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "solution_id": 53, + "solution_version": 6, + "updated_at": "2026-10-25T14:14:16.844983Z" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/38/management/solutions/53/members/remove/apply/", + "status": 409, + "request": { + "user_id": 10, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Состав или данные решения изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/38/solutions/53/", + "status": 200, + "request": null, + "response": { + "id": 53, + "program_id": 38, + "version": 6, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:14.875029Z", + "updated_at": "2026-10-25T14:14:16.844983Z", + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true, + "specialization": null + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ], + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "program_id": 38, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 271 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 272 + }, + { + "value": 0, + "field_id": 273 + }, + { + "value": false, + "field_id": 274 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T14:14:15.925692Z", + "submitted_at": "2026-10-25T14:14:15.819475Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/38/solutions/53/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/solution-invitations/56/", + "status": 200, + "request": null, + "response": { + "id": 56, + "version": 2, + "program": { + "id": 38, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "solution": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T14:14:15.127265Z", + "updated_at": "2026-10-25T14:14:15.272157Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/56/accept/", + "status": 409, + "request": { + "expected_version": 2 + }, + "response": { + "code": "invite_already_processed", + "detail": "Приглашение уже обработано.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/38/management/solutions/53/history/", + "status": 200, + "request": null, + "response": { + "count": 1, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 3, + "application": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "action": "remove_member", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T14:14:16.847006Z", + "before_version": 5, + "after_version": 6, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_member": { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false, + "specialization": null + }, + "revoked_invitation_count": 0 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/38/management/solutions/53/history/3/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 3, + "application": { + "kind": "solution", + "program_id": 38, + "solution_id": 53, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "action": "remove_member", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T14:14:16.847006Z", + "before_version": 5, + "after_version": 6, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_member": { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false, + "specialization": null + }, + "revoked_invitation_count": 0 + }, + "before_snapshot": { + "form": { + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ], + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "program_id": 38, + "is_snapshot": true, + "is_configured": true + }, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "answers": [ + { + "value": "Город", + "field_id": 271 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 272 + }, + { + "value": 0, + "field_id": 273 + }, + { + "value": false, + "field_id": 274 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:15.264093Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 9, + "submitted_at": "2026-10-25T14:14:15.819475Z" + }, + "after_snapshot": { + "form": { + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ], + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "program_id": 38, + "is_snapshot": true, + "is_configured": true + }, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "answers": [ + { + "value": "Город", + "field_id": 271 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 272 + }, + { + "value": 0, + "field_id": 273 + }, + { + "value": false, + "field_id": 274 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true, + "specialization": null + } + ], + "captain_id": 9, + "submitted_at": "2026-10-25T14:14:15.819475Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "rated", + "method": "GET", + "path": "/programs/38/solutions/53/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 38, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf" + }, + "solution": { + "id": 53, + "program_id": 38, + "version": 6, + "name": "Исключение участника case_championship qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:14.875029Z", + "updated_at": "2026-10-25T14:14:16.844983Z", + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:14.881393Z", + "is_captain": true, + "specialization": null + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 271, + "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": 272, + "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": 273, + "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": 274, + "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 + } + } + ], + "revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "template": "case_championship", + "program_id": 38, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 271 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 272 + }, + { + "value": 0, + "field_id": 273 + }, + { + "value": false, + "field_id": 274 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T14:14:15.925692Z", + "submitted_at": "2026-10-25T14:14:15.819475Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "137b0820da62b5af55ced3e0d4f27f99e983be14c9c16787a1e35ea8efd89c10", + "criteria": [ + { + "id": 68, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 69, + "name": "Проверка нуля 1", + "description": null, + "type": "int", + "min_value": 0.0, + "max_value": 10.0, + "value": "0" + }, + { + "id": 70, + "name": "Проверка false 2", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": false, + "rated_count": 0, + "max_rates": 1, + "required_evaluators": 2, + "deadline": "2026-10-27T14:14:14.598410Z" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/38/solutions/53/", + "status": 409, + "request": { + "expected_version": 6, + "expected_form_revision": "d28254a65cd5a20b50b5b5bd526d32a2567d7e6f19b992093b13d188904b7e99", + "name": "Недоступная правка" + }, + "response": { + "code": "team_frozen", + "detail": "Состав команды зафиксирован после сдачи.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/38/management/solutions/53/history/", + "status": 403, + "request": null, + "response": { + "code": "permission_denied", + "detail": "История доступна организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/39/solutions/", + "status": 201, + "request": { + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "response": { + "id": 54, + "program_id": 39, + "version": 1, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:17.988823Z", + "updated_at": "2026-10-25T14:14:17.988823Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 39, + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T14:14:14.598410Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 275 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 276 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 277 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 278 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 279 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/39/solutions/54/", + "status": 200, + "request": null, + "response": { + "id": 54, + "program_id": 39, + "version": 1, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:17.988823Z", + "updated_at": "2026-10-25T14:14:17.988823Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 39, + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T14:14:14.598410Z", + "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": "team", + "code": "team_min_size", + "detail": "В команде недостаточно принятых участников.", + "field_id": null + }, + { + "path": "answers", + "code": "case_required", + "detail": "Выберите кейс или задачу.", + "field_id": 275 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 276 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 277 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 278 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 279 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/39/solutions/54/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 10, + "role": "Backend-разработчик" + }, + "response": { + "invitation": { + "id": 57, + "version": 1, + "program": { + "id": 39, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "solution": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T14:14:18.311018Z", + "updated_at": "2026-10-25T14:14:18.311018Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/57/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 57, + "version": 2, + "program": { + "id": 39, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "solution": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T14:14:18.311018Z", + "updated_at": "2026-10-25T14:14:18.427523Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/39/solutions/54/", + "status": 200, + "request": null, + "response": { + "id": 54, + "program_id": 39, + "version": 3, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:17.988823Z", + "updated_at": "2026-10-25T14:14:18.431565Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 39, + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T14:14:14.598410Z", + "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": 275 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 276 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 277 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 278 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 279 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/39/solutions/54/", + "status": 200, + "request": { + "expected_version": 3, + "expected_form_revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "answers": [ + { + "field_id": 275, + "value": "Город" + }, + { + "field_id": 276, + "value": "Материалы qa01mr-20261025-60d40caf" + }, + { + "field_id": 277, + "value": 0 + }, + { + "field_id": 278, + "value": false + }, + { + "field_id": 279, + "value": "https://example.com/code/qa01mr-20261025-60d40caf" + } + ] + }, + "response": { + "id": 54, + "program_id": 39, + "version": 4, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:17.988823Z", + "updated_at": "2026-10-25T14:14:18.634118Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": false + }, + "form": { + "program_id": 39, + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 275, + "value": "Город" + }, + { + "field_id": 276, + "value": "Материалы qa01mr-20261025-60d40caf" + }, + { + "field_id": 277, + "value": 0 + }, + { + "field_id": 278, + "value": false + }, + { + "field_id": 279, + "value": "https://example.com/code/qa01mr-20261025-60d40caf" + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T14:14:14.598410Z", + "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 + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/preview/", + "status": 200, + "request": { + "user_id": 10 + }, + "response": { + "application": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "member": { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false + }, + "solution_version": 4, + "is_submitted": false, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 0, + "errors": [], + "can_apply": true, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/39/solutions/54/submit/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b" + }, + "response": { + "id": 54, + "program_id": 39, + "version": 5, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:17.988823Z", + "updated_at": "2026-10-25T14:14:18.917159Z", + "team": { + "members": [ + { + "user": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true + }, + { + "user": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "program_id": 39, + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 275, + "value": "Город" + }, + { + "field_id": 276, + "value": "Материалы qa01mr-20261025-60d40caf" + }, + { + "field_id": 277, + "value": 0 + }, + { + "field_id": 278, + "value": false + }, + { + "field_id": 279, + "value": "https://example.com/code/qa01mr-20261025-60d40caf" + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T14:14:14.598410Z", + "submitted_at": "2026-10-25T14:14:18.912690Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/apply/", + "status": 409, + "request": { + "user_id": 10, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Состав или данные решения изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/preview/", + "status": 403, + "request": { + "user_id": 10 + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "member", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/preview/", + "status": 403, + "request": { + "user_id": 10 + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "rated", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/preview/", + "status": 403, + "request": { + "user_id": 10 + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": null, + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/preview/", + "status": 401, + "request": { + "user_id": 10 + }, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "rated", + "method": "GET", + "path": "/programs/39/solutions/54/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 39, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "solution": { + "id": 54, + "program_id": 39, + "version": 5, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:17.988823Z", + "updated_at": "2026-10-25T14:14:18.917159Z", + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false, + "specialization": null + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "program_id": 39, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 275 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 276 + }, + { + "value": 0, + "field_id": 277 + }, + { + "value": false, + "field_id": 278 + }, + { + "value": "https://example.com/code/qa01mr-20261025-60d40caf", + "field_id": 279 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T14:14:19.007608Z", + "submitted_at": "2026-10-25T14:14:18.912690Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "f98a11a2db7f5de736fd8f188480f615ee65a65cce0549963672829792a9fbcd", + "criteria": [ + { + "id": 71, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 72, + "name": "Проверка нуля 3", + "description": null, + "type": "int", + "min_value": 0.0, + "max_value": 10.0, + "value": "0" + }, + { + "id": 73, + "name": "Проверка false 4", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": false, + "rated_count": 0, + "max_rates": 1, + "required_evaluators": 2, + "deadline": "2026-10-27T14:14:14.598410Z" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/preview/", + "status": 200, + "request": { + "user_id": 9 + }, + "response": { + "application": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "member": { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true, + "specialization": null + }, + "solution_version": 5, + "is_submitted": true, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 2, + "errors": [ + { + "path": "user_id", + "code": "captain_cannot_be_removed", + "detail": "Капитана нельзя исключить из состава. Для него снимите заявку с участия.", + "field_id": null + } + ], + "can_apply": false, + "preview_token": null + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/preview/", + "status": 200, + "request": { + "user_id": 10 + }, + "response": { + "application": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "member": { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false, + "specialization": null + }, + "solution_version": 5, + "is_submitted": true, + "remaining_member_count": 1, + "revoked_invitation_count": 0, + "score_count": 2, + "errors": [], + "can_apply": true, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/apply/", + "status": 200, + "request": { + "user_id": 10, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "solution_id": 54, + "solution_version": 6, + "updated_at": "2026-10-25T14:14:19.926401Z" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/39/management/solutions/54/members/remove/apply/", + "status": 409, + "request": { + "user_id": 10, + "preview_token": "[REDACTED_SIGNED_PREVIEW]" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Состав или данные решения изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/39/solutions/54/", + "status": 200, + "request": null, + "response": { + "id": 54, + "program_id": 39, + "version": 6, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:17.988823Z", + "updated_at": "2026-10-25T14:14:19.926401Z", + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true, + "specialization": null + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "program_id": 39, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 275 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 276 + }, + { + "value": 0, + "field_id": 277 + }, + { + "value": false, + "field_id": 278 + }, + { + "value": "https://example.com/code/qa01mr-20261025-60d40caf", + "field_id": 279 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T14:14:19.007608Z", + "submitted_at": "2026-10-25T14:14:18.912690Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/39/solutions/54/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/solution-invitations/57/", + "status": 200, + "request": null, + "response": { + "id": 57, + "version": 2, + "program": { + "id": 39, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "solution": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "captain": { + "id": 9, + "first_name": "Илья", + "last_name": "Сквозной", + "avatar_url": null + }, + "recipient": { + "id": 10, + "first_name": "Мария", + "last_name": "Сквозная", + "avatar_url": null + }, + "role": "Backend-разработчик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T14:14:18.311018Z", + "updated_at": "2026-10-25T14:14:18.427523Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/57/accept/", + "status": 409, + "request": { + "expected_version": 2 + }, + "response": { + "code": "invite_already_processed", + "detail": "Приглашение уже обработано.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/39/management/solutions/54/history/", + "status": 200, + "request": null, + "response": { + "count": 1, + "offset": 0, + "limit": 20, + "results": [ + { + "id": 4, + "application": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "action": "remove_member", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T14:14:19.927302Z", + "before_version": 5, + "after_version": 6, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_member": { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false, + "specialization": null + }, + "revoked_invitation_count": 0 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/39/management/solutions/54/history/4/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 4, + "application": { + "kind": "solution", + "program_id": 39, + "solution_id": 54, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "action": "remove_member", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T14:14:19.927302Z", + "before_version": 5, + "after_version": 6, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_member": { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false, + "specialization": null + }, + "revoked_invitation_count": 0 + }, + "before_snapshot": { + "form": { + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "program_id": 39, + "is_snapshot": true, + "is_configured": true + }, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "answers": [ + { + "value": "Город", + "field_id": 275 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 276 + }, + { + "value": 0, + "field_id": 277 + }, + { + "value": false, + "field_id": 278 + }, + { + "value": "https://example.com/code/qa01mr-20261025-60d40caf", + "field_id": 279 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Backend-разработчик", + "user": { + "id": 10, + "last_name": "Сквозная", + "avatar_url": null, + "first_name": "Мария" + }, + "joined_at": "2026-10-25T14:14:18.421437Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 9, + "submitted_at": "2026-10-25T14:14:18.912690Z" + }, + "after_snapshot": { + "form": { + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "program_id": 39, + "is_snapshot": true, + "is_configured": true + }, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "answers": [ + { + "value": "Город", + "field_id": 275 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 276 + }, + { + "value": 0, + "field_id": 277 + }, + { + "value": false, + "field_id": 278 + }, + { + "value": "https://example.com/code/qa01mr-20261025-60d40caf", + "field_id": 279 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true, + "specialization": null + } + ], + "captain_id": 9, + "submitted_at": "2026-10-25T14:14:18.912690Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "rated", + "method": "GET", + "path": "/programs/39/solutions/54/evaluation/", + "status": 200, + "request": null, + "response": { + "program": { + "id": 39, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf" + }, + "solution": { + "id": 54, + "program_id": 39, + "version": 6, + "name": "Исключение участника hackathon qa01mr-20261025-60d40caf", + "captain_id": 9, + "created_at": "2026-10-25T14:14:17.988823Z", + "updated_at": "2026-10-25T14:14:19.926401Z", + "team": { + "members": [ + { + "role": null, + "user": { + "id": 9, + "last_name": "Сквозной", + "avatar_url": null, + "first_name": "Илья" + }, + "joined_at": "2026-10-25T14:14:17.992981Z", + "is_captain": true, + "specialization": null + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 3, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 275, + "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": 276, + "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": 277, + "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": 278, + "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": 279, + "type": "url", + "label": "Репозиторий", + "options": [], + "purpose": "repository", + "position": 4, + "required": true, + "help_text": "", + "validation": { + "maximum": null, + "minimum": null, + "max_length": null, + "min_length": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ], + "revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "template": "hackathon", + "program_id": 39, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 275 + }, + { + "value": "Материалы qa01mr-20261025-60d40caf", + "field_id": 276 + }, + { + "value": 0, + "field_id": 277 + }, + { + "value": false, + "field_id": 278 + }, + { + "value": "https://example.com/code/qa01mr-20261025-60d40caf", + "field_id": 279 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T14:14:19.007608Z", + "submitted_at": "2026-10-25T14:14:18.912690Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "criteria_revision": "f98a11a2db7f5de736fd8f188480f615ee65a65cce0549963672829792a9fbcd", + "criteria": [ + { + "id": 71, + "name": "Комментарий", + "description": "Доп. поле для впечатлений о проекте", + "type": "str", + "min_value": null, + "max_value": null, + "value": null + }, + { + "id": 72, + "name": "Проверка нуля 3", + "description": null, + "type": "int", + "min_value": 0.0, + "max_value": 10.0, + "value": "0" + }, + { + "id": 73, + "name": "Проверка false 4", + "description": null, + "type": "bool", + "min_value": null, + "max_value": null, + "value": "False" + } + ], + "managed_evaluation": true, + "can_rate": true, + "blocked_reason": null, + "is_completed": false, + "rated_count": 0, + "max_rates": 1, + "required_evaluators": 2, + "deadline": "2026-10-27T14:14:14.598410Z" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/39/solutions/54/", + "status": 409, + "request": { + "expected_version": 6, + "expected_form_revision": "f3ff0a58557e6667d16c308067b4562d96cbf7a18368465cd5d439fd6ce94e8b", + "name": "Недоступная правка" + }, + "response": { + "code": "team_frozen", + "detail": "Состав команды зафиксирован после сдачи.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/39/management/solutions/54/history/", + "status": 403, + "request": null, + "response": { + "code": "permission_denied", + "detail": "История доступна организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/34/solution-context/", + "status": 200, + "request": null, + "response": { + "program_id": 34, + "storage_mode": "legacy_project", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-26T11:17:58.692009Z", + "can_create": false, + "blocked_reason": "already_has_application", + "current_application": { + "kind": "legacy_project", + "program_id": 34, + "project_id": 4, + "program_link_id": 3, + "name": "Заявка проекта qa01l-20261025-c6c0e219 сохранен" + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/34/management/solutions/53/members/remove/preview/", + "status": 409, + "request": { + "user_id": 10 + }, + "response": { + "code": "solution_mode_unavailable", + "detail": "Программа использует заявки на основе проектов.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/38/management/solutions/54/history/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + } +] diff --git a/docs/qa/program-solutions-member-removal-20261025/migration-evidence.json b/docs/qa/program-solutions-member-removal-20261025/migration-evidence.json new file mode 100644 index 000000000..487b998d6 --- /dev/null +++ b/docs/qa/program-solutions-member-removal-20261025/migration-evidence.json @@ -0,0 +1,36 @@ +{ + "status": "PASS", + "migration": "0036_solution_member_admin_history", + "database": "local-procollab_ci", + "oldModelsUnchanged": 23, + "hashBefore": "ce0a2edae7fe3e68846a097634c0b39364512edb77e32ada8166e860c82bbd61", + "hashAfter": "ce0a2edae7fe3e68846a097634c0b39364512edb77e32ada8166e860c82bbd61", + "rowCounts": { + "partner_programs.PartnerProgram": 35, + "partner_programs.PartnerProgramField": 259, + "partner_programs.PartnerProgramUserProfile": 102, + "partner_programs.PartnerProgramProject": 3, + "partner_programs.PartnerProgramFieldValue": 2, + "partner_programs.ProgramSolution": 51, + "partner_programs.ProgramSolutionAnswer": 246, + "partner_programs.ProgramSolutionMember": 69, + "partner_programs.SolutionInvitation": 32, + "project_rates.SolutionScore": 28, + "project_rates.SolutionExpertAssignment": 16, + "project_rates.Criteria": 58, + "users.Expert": 7, + "files.UserFile": 8, + "projects.Project": 4, + "projects.Collaborator": 6, + "invites.Invite": 4, + "project_rates.ProjectScore": 3, + "project_rates.ProjectExpertAssignment": 1, + "industries.Industry": 1, + "users.Expert_programs": 21, + "partner_programs.PartnerProgram_managers": 39, + "partner_programs.SolutionAdministrativeChange": 2 + }, + "historyCaseRowsPreserved": 2, + "existingProjectsMigratedOrDeleted": 0, + "onlyLocalSchema": true +} diff --git a/docs/qa/program-solutions-negative-20261025/README.md b/docs/qa/program-solutions-negative-20261025/README.md new file mode 100644 index 000000000..4411415d9 --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/README.md @@ -0,0 +1,65 @@ + + +# QA01 NEG01–NEG08: сдача, сеть и обновление формы + +Scope этого checkpoint подтвержден; **общие QA01/REL01 и новая сущность еще не завершены**. [Каноническое ТЗ](https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175), исходные AC07/09/10/13/14/16/17/24/25. Это не замена полной матрице всех 26 AC. + +## Ошибка и исправление + +При потере ответа POST после реального commit сервер уже возвращал submitted snapshot по GET, но footer сохранял сообщение «Не удалось загрузить решение. Попробуйте еще раз.». Это воспроизведено на КЧ и хакатоне и подтверждено красным regression test: 1 failed / 16 passed. [Исходный browser результат FAIL](negative-browser-before-fix.json) сохранен с обоими findings. + +Теперь принятый hydrate результата GET со status=submitted снимает старую командную ошибку, показывает существующее «Решение сдано.» и убирает причины неполноты/attempted. Код не отправляет повторный POST, не меняет API или данные решения и не вызывает hydrate(saved=true): локальный ввод сохраняется прежним механизмом. Если GET вернул draft или тоже завершился ошибкой, успех не показывается. Более старый ответ не принимается через существующий version guard. + +3 новых component tests и усиленный существующий interrupted-POST test проверяют: подтвержденную сдачу, draft после неизвестного результата, ошибку уточняющего GET и сохранение более поздней локальной правки в отдельном блоке копирования. Финальный focused прогон component/facade/form-rebase — 31 PASS. Полный test:ci — 2763 tests/478 files и strict C00 types PASS; development build/scoped ESLint/locked Prettier 3.8.3 по 1984 files PASS. Перед этим полный baseline был 2760/478. Дизайн D00 v3, controls/layout/SCSS, C00 1.13.0 и permissions не менялись. + +## Среда и разделение evidence + +Backend runtime 2ad4743; 6db9050 меняет только лимит CI. Frontend до исправления 55332ef2, после — текущий source hash в [manifest](local-artifacts.json) и [build provenance](build-provenance-after-fix.json). PostgreSQL 18/main procollab_ci, настоящий Django/JWT/Angular, locmem mail и local storage; внешняя сеть блокировалась. Тестовые пароли/JWT/credential manifests в Git не включены. + +До исправления: программы 11/12, сданные контрольные решения 12/13 и проверяемые решения 14/15. После исправления: свежие программы 13/14, контрольные 16/17 и проверяемые 18/19. Все сданные fixtures сохранены. Ни один реальный пользователь/программа/dev/production не переключался, Project не создавался. Дедлайн менялся только guarded helper выбранных synthetic программ и всегда восстанавливался в finally. + +Первый negative browser проход продолжался по завершенным фазам после исправления ожидания harness: действующий UI сохраняет кнопку сдачи disabled при закрытом сроке, а не скрывает ее. Два raw phase attempts сохранены. Файл before-fix содержит события последней invocation; более ранние empty/team/material фазы подтверждаются state и [raw attempts](local-artifacts.json). Не заявляется один непрерывный успешный проход всего negative сценария. Product finding — отдельная stale network error после confirmed submit; ее исправление проверено заново на обеих формах свежими fixtures. + +## Матрица checkpoint + +| Gate | Проверка и наблюдаемый результат | Evidence | Verdict | +| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------- | +| NEG01 | Пустая заявка с одним капитаном и null кейсом сохраняется PATCH 200. Readiness переводит на Team и фокусирует team-title; submit POST не отправляется. Ссылка ошибки кейса возвращает к select с тем же ID. | negative-state + raw phase attempts; source prepareSubmission/focusIssue | PASS | +| NEG02 | Pending не закрывает минимум; после настоящего accept вторым аккаунтом ошибка из Team ведет к required material: PDF button КЧ / repository input хакатона. | negative state/events/screenshots | PASS | +| NEG03 | Срок закрывается после PATCH/подтверждения: POST 409/submission_closed, статус остается draft, submit disabled; сохранение draft после срока дает 200. Дата восстановлена. | negative browser/deadline helper + domain | PASS | +| NEG04 | PATCH прерывается до отправки: локальный textarea остается, серверные ответы прежние. Только явный retry сохраняет текст. | negative-browser faults + events | PASS | +| NEG05 | Manager PUT меняет labels/order и добавляет required при локальных правках. PATCH со старой form revision дает 409; GET/rebase сохраняет локальные name/text/0/False/case, отличающиеся от прежних server 13/True/«Город». Повтор сохраняет новые значения. | negative browser + manager HTTP + domain | PASS | +| NEG06 | Новый required блокирует только новую работу и фокусирует новый control из Team. Сданный контрольный snapshot сохраняет прежние definitions/answers/team; manager field filters not_applicable/present/missing различают две сдачи. | negative-domain: 24 HTTP/C00 checks | PASS | +| NEG07 | JSON submit реально коммитится, ответ намеренно теряется. После fix GET показывает submitted/readonly и success notice, stale network alert отсутствует; ровно один UI POST, 0 automatic retries, 0/False сохранены. | unknown-browser-after-fix: обе формы, 2 injected lost responses, JS errors=[] | PASS после fix | +| NEG08 | DB/HTTP verifier подтверждает snapshots/права/повторную сдачу/597-byte PDF/MIME/owner/bytes; прежняя выборка совпадает с baseline SHA-256. Сам verifier не меняет домен. | negative-domain 24 + unknown-domain 18 = 42 HTTP/C00 checks | PASS | + +## Файлы и воспроизведение + +- [Negative before-fix browser](negative-browser-before-fix.json), [state](negative-state.json), [domain](negative-domain.json), [seed](negative-seed.json), [baseline](negative-baseline.json). +- [Post-fix browser](unknown-browser-after-fix.json), [state](unknown-state.json), [domain](unknown-domain.json), [seed](unknown-seed.json), [baseline](unknown-baseline.json). +- [Manifest исходных артефактов](local-artifacts.json): raw SHA-256/semantic SHA каждого JSON, хеши семи harness sources в Temp и 17 локальных screenshots. Копии JSON приведены к repo Prettier; их значения, включая FAIL before-fix, не менялись. +- Локальные изображения: Z:/Документы/PROCOLLAB-dev/design-artifacts/program-solutions-20261024/qa01-negative и qa01-unknown-submit. Sources/логи: C:/Users/evsee/AppData/Local/Temp/procollab-qa01-\*. Не перезапускать seed/harness вслепую: submitted fixtures неизменяемы, state/phases имеют значение. +- Логи: procollab-qa01-unknown-submit-red.log, procollab-qa01-unknown-submit-focused.log, procollab-qa01-negative-fe-tests.log, procollab-qa01-negative-fe-build.log, procollab-qa01-negative-fe-eslint.log и procollab-qa01-negative-fe-format.log. + +Основной путь выполнен на 390 px; final readonly состояния проверены при 320/390/768/1440, отдельно снят desktop. Визуально просмотрены post-fix КЧ mobile и хакатон desktop; Mont загружен. Полный desktop путь, screen reader/zoom/contrast, production CDN и рассылки не заявляются; default remote avatars отсутствуют из-за network guard. Upload идет native multipart прямо в local API, JSON response-loss injection отдельно записан; успех API не подменяется mock body. Seed/browser намеренно меняют только новые synthetic данные; это не zero writes. + +## Source audit конкурентности + +Прочитаны конкретные assertions backend 2ad4743, а не только имена файлов. Эти tests входили в финальный локальный PostgreSQL прогон 261 PASS; frontend fix backend не меняет. + +| Исходный AC | Что доказывают существующие assertions | Остаток | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| AC02 | SolutionCreationConcurrencyTests: два PostgreSQL потока/Barrier возвращают один ID, created=[False,True], одна Solution/Member и ни одного Project. | Не заменяет общий пользовательский QA всех входов. | +| AC07 | validate_submission_team/min использует только accepted; maximum включает unique pending/reserved. API test максимума проверяет team_max_size и прием собственного резерва; browser NEG02 подтверждает минимум. | Отдельная гонка последнего места с разными recipients еще требует прямого evidence; duplicate invite same-version race ее не подменяет. | +| AC08 | Два accepted invitations в разные команды дают success/already_in_program_team, у получателя ровно одно членство. | Остальные форматы/legacy coexistence сверяются отдельно. | +| AC15 | submit×PATCH/remove/accept/form/file delete проверяют допустимые ordered outcomes и точные snapshot/name/member/file invariants. При проигравшем submit остается draft без snapshot; при победившем замороженные данные не меняются. | Полный итог QA01 и остальные edges не закрываются числом tests. | + +## CI и оставшаяся работа + +PostgreSQL run 40164049519 для 2ad4743 отменен по аннотации GitHub «The job has exceeded the maximum execution time of 30m0s»: 39 locking tests прошли, полный набор нашел 1669 tests, но итоговый Ran/OK не получен. Это не PASS. В 6db9050 timeout поднят до 60 минут; YAML diff проверен: triggers/permissions/jobs/полный набор tests сохранены. Новые runs: PostgreSQL 40166190670 и General 40166187673; их terminal verdict проверяется отдельно, старые результаты его не заменяют. + +Обновление 25.10.2026: PostgreSQL 40166190670 для текущего 6db9050 завершен SUCCESS; лог подтверждает 39 locking/vacancy tests и полный набор 1669 tests, оба Ran/OK. General 40166187673 также SUCCESS, Tests/Lint, лог Ran 1669/OK (skipped=54) проверен. Прямой last-slot race с разными получателями и invitation UI edges проверены следующим [checkpoint](../program-solutions-invitations-20261025/README.md); это закрывает названный выше частный пробел evidence, сохраняя общий QA01 открытым. + +[General CI 2ad4743](https://github.com/PROCOLLAB-github/api/actions/runs/40164045725) SUCCESS: Ran 1669 tests, OK (skipped=54), Tests/Lint, лог проверен. Этот non-PostgreSQL прогон не подменяет locking/full PostgreSQL gate; runtime 6db9050 совпадает с 2ad4743, изменен только CI timeout. + +Впереди полные organizer/expert/public/legacy/noncompetitive пути, полный desktop, invitation role bounds/edit/unavailable, прямое evidence last-slot race и оставшиеся исходные AC. D02/D03 и административная политика сданных заявок/конкурсная eligibility ожидают уже заданных решений. Эта проверка их не согласует и не реализует. PR остается draft, merge/deploy не выполнялись. diff --git a/docs/qa/program-solutions-negative-20261025/build-provenance-after-fix.json b/docs/qa/program-solutions-negative-20261025/build-provenance-after-fix.json new file mode 100644 index 000000000..8158367c9 --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/build-provenance-after-fix.json @@ -0,0 +1,28 @@ +{ + "source": "Z:\\Документы\\PROCOLLAB-dev\\procollab-dev-program-solutions-fe01-20261024\\dist\\social_platform", + "target": "C:/Users/evsee/AppData/Local/Temp/procollab-unknown-angular", + "frontendBase": "55332ef295658d5473c9d500958d8567c161ab1c", + "frontendState": "working tree with QA01 confirmed-submit reconciliation and C00 1.13; source hashes identify runtime input", + "changes": "only API/WebSocket origins in copied generated JS", + "changed": [ + { + "file": "chunk-CHX6T2XY.js", + "before": "4707c125db5485fa8b5eeb9e30e23c6f90759d9f2daf86b56a5a0b332da287dd", + "after": "9952a60a44e2cf169099556f3b81c6866212b5aa80784d253b7b1a627defd7bd" + } + ], + "sources": [ + { + "file": "projects/social_platform/src/app/api/solution/solution-navigation.ts", + "sha256": "f08e39b9feb74fbec0fb40056b1a680841f2fc382cf71d7b77b65fafe3a1195e" + }, + { + "file": "projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts", + "sha256": "0db6ba8a18789cff01aebd66f55e320df4757d5885590d8ca9c53434c5161651" + }, + { + "file": "projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts", + "sha256": "08828f85f0a61eeb57b8aff4a510b70a45b2268edb146cc2053cb6db967ac739" + } + ] +} diff --git a/docs/qa/program-solutions-negative-20261025/local-artifacts.json b/docs/qa/program-solutions-negative-20261025/local-artifacts.json new file mode 100644 index 000000000..a67cee500 --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/local-artifacts.json @@ -0,0 +1,205 @@ +{ + "backendRuntime": "2ad47438e666963819665222253d9b0672df074f", + "backendWithCIChange": "6db9050c2d1eb7313006a285566501a104dbc415", + "frontendBefore": "55332ef295658d5473c9d500958d8567c161ab1c", + "frontendAfter": "working tree with confirmed-submit reconciliation; component source hash below", + "componentSourceSha256": "0db6ba8a18789cff01aebd66f55e320df4757d5885590d8ca9c53434c5161651", + "artifacts": [ + { + "file": "negative-baseline.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative\\baseline-evidence.json", + "sourceSha256": "fb14189188916f80720e0236b8b72df97f2d204ba28d4b0e016c8427ce060c78", + "semanticSha256": "3a3f5e21588822f8f1645fa29e4b482e668f6888370fda98e18c0152b565c901" + }, + { + "file": "negative-seed.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative\\seed-evidence.json", + "sourceSha256": "869689bbefa26acf09c3b2e09a915788bccd8f07c8f2a64328e3ccab796bdce8", + "semanticSha256": "d15a7ab6949ddd0a56b8ea767f08ca933863ce5600a7ee231c3dbcb5a5263dd8" + }, + { + "file": "negative-browser-before-fix.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative\\browser-evidence.json", + "sourceSha256": "392ed202717be8e8207d07739bedc4a2fda7aefa97694cd7f1b7115228fbcc05", + "semanticSha256": "2d5417e95203c55d3489902d440d907e193c52fc769720acee0f299beb3322ab" + }, + { + "file": "negative-domain.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative\\domain-evidence.json", + "sourceSha256": "26fafed76ceb3af3a075344b6fd7bd80160592220b227ecab57b751e0094f8ad", + "semanticSha256": "56abc21da02687e80e3163fca5da2081ab1c13a26bf7a59e7a0fda862c8015ac" + }, + { + "file": "negative-state.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative\\run-state.json", + "sourceSha256": "ebd871d6d44f64af1eb7ce4df205c2d52480611d118f9fbd79c0bf33387575fe", + "semanticSha256": "af8cbea709226ad1138ad8db2a34b70571a2c70914b84d0953e912584fa9c0b9" + }, + { + "file": "negative-attempt-1792905580688.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative\\attempt-1792905580688.json", + "sourceSha256": "9f8de2161bf29c761c5ac62ef378cdfaefbffb43c0474470bf1909d6d25d74db", + "semanticSha256": "7b380bd2d0038fd4537090ee0b2e3d3fa5f1865824117883e43a5134caab35aa" + }, + { + "file": "negative-attempt-1792905674286.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative\\attempt-1792905674286.json", + "sourceSha256": "cad17b0928c38f42e1c44cdc264ff81c07d27dbf411b06c3e7a7ee76d257f6b1", + "semanticSha256": "c26198d4893dd215af656f6b6078a288c2d08abddaa1b234bf481553377e2216" + }, + { + "file": "unknown-baseline.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit\\baseline-evidence.json", + "sourceSha256": "51b945f2660febe09614fcb2acf2ef2981f80c98c1c83d716b1b682fc557edd6", + "semanticSha256": "9128de236677c4035367fa709c7b13ea001f17d9259985e48e8c10bf68a162ea" + }, + { + "file": "unknown-seed.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit\\seed-evidence.json", + "sourceSha256": "03ef0063e641d5dd04ad57c0df5dc93a7a70d15c19cfb92e61cae423ebac9d4f", + "semanticSha256": "72feabe89c3d4b370f0a817e8800cfe89c6e21ee61ee0472db4bd6d7341feff5" + }, + { + "file": "unknown-browser-after-fix.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit\\browser-evidence.json", + "sourceSha256": "14a60232c053dd4a7302d58d4625508fef78261067cf719b9b93dbc879838148", + "semanticSha256": "70222f890e4cb8735b8fc6c8ed857848d404ce227428566342a2d31c488b5fbd" + }, + { + "file": "unknown-domain.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit\\domain-evidence.json", + "sourceSha256": "1e45bb48c23cb3b243dccf177dd8e32d7d00f75e478b4d63ba5316e5df4106f8", + "semanticSha256": "54a4d19c88d2c7b74696411f34924161ffa37b01b6c8267b4d4aa9594ea73a8b" + }, + { + "file": "unknown-state.json", + "source": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit\\run-state.json", + "sourceSha256": "519fc280cea53165678098763478f7ae04cd9a07979210bccd9ff1c5e596a3d2", + "semanticSha256": "4798682264130a10abeae13c70c42f3d9c4a3b7dba0ea126e7397d9e02cc3270" + }, + { + "file": "build-provenance-after-fix.json", + "source": "C:\\Users\\evsee\\AppData\\Local\\Temp\\procollab-unknown-angular\\qa-build-provenance.json", + "sourceSha256": "629c89f6c94bc6fa3375f451c6bf9728a3f1d4948da547cc84c26a2d8d705ace", + "semanticSha256": "683d27be55467ce7051638d95ea380c3a681087a5a2f4ad800a085ea139362f3" + } + ], + "helpers": [ + { + "file": "procollab-qa01-negative-seed.py", + "sha256": "12fcce6c2e279eed973e1a9f9ee931c99cb454359dd30dbc06c8e9534a5a0bf3" + }, + { + "file": "procollab-qa01-negative-deadline.py", + "sha256": "7eb36d7eb66a507c5a6e9e50626abed9f65f6bb44f1747aadf4511b9756d5aec" + }, + { + "file": "procollab-qa01-negative-browser.cjs", + "sha256": "d367ba3b99d6b503bd9156c5d03a346227f9b35c762dd956bb670ff322624ab9" + }, + { + "file": "procollab-qa01-negative-verify.py", + "sha256": "de960234984d7f6c1101477129492b4c908b7de3c57a5b3679cf3b71482dce60" + }, + { + "file": "procollab-qa01-unknown-seed.py", + "sha256": "8d9fa4796af8095960d140491108d9a1b081f3af01c474c46006376bd504af63" + }, + { + "file": "procollab-qa01-unknown-browser.cjs", + "sha256": "d11fefc264bfd19a44fd6306116347655a2a97c64170057261955d9896a838f2" + }, + { + "file": "procollab-qa01-unknown-build.cjs", + "sha256": "12e80c561944b85ea91cabb854af3cb54c8a15a620d46a08a3778e03301228ff" + } + ], + "screenshots": [ + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "browser-error.png", + "sha256": "c487ca07eb847aba20301a6f30955790a721ba7f0f1684737239677419a28073" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "case_championship-case-error-mobile.png", + "sha256": "c3d7c01e44e5c9396c82fda2ca864e4f8ec7895cacf5dbf37e90dfb8bdfb8a00" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "case_championship-closed-draft-mobile.png", + "sha256": "70831ae6ea52a13185a0a53d71edc14da5cd21deca0903ed22e65454bb6566d2" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "case_championship-material-error-mobile.png", + "sha256": "98dde1ecc79fdc97a43a9f6a398a629cabebef331ae5c55e724440d0a4c9f1c9" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "case_championship-new-required-mobile.png", + "sha256": "9161a5d91d23e81f2124457ed30e8f143449a5b3dd11ef31ccf6f38746cea315" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "case_championship-unknown-submit-desktop.png", + "sha256": "f459c6a921db134af748ba4f1776a6561255f0d6a0a7df3521e48d5cc4cd22d8" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "case_championship-unknown-submit-mobile.png", + "sha256": "14d844d042c2e5246f84f9ace6718abac8fd81c21ac821ce2e4543b8027d73fa" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "hackathon-case-error-mobile.png", + "sha256": "c5afebadb0e50ddab1d45a3af234eb1710af39348c9b420408249226dff91219" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "hackathon-closed-draft-mobile.png", + "sha256": "c896378125cd34007bdcbe853f636f1a2bc5760cb559cdb54686a6c7d8f858da" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "hackathon-material-error-mobile.png", + "sha256": "e9146cc3586c16d2481fbd68d969f7c0251cf8de287844ada402c653b954681c" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "hackathon-new-required-mobile.png", + "sha256": "ab1a24535242aa2ad5500409b76cdb62841c48bfedfcc4cff02c2fa40cc3c23a" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "hackathon-unknown-submit-desktop.png", + "sha256": "dcaae09935ba9af6dbbd7657703b33a366c64d4cbe6a524ba9477e6219cc09e6" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-negative", + "file": "hackathon-unknown-submit-mobile.png", + "sha256": "3c7647df38d8ed5b9268c071fdab27171a7dcd5fa64d8cdf1f75546d6b39b354" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit", + "file": "case_championship-confirmed-desktop.png", + "sha256": "5276782ef275b821bfc42b82a93a00b253fef6677d4ffedea141247c2b0e2b12" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit", + "file": "case_championship-confirmed-mobile.png", + "sha256": "057d31ec72197ed08d1d8f69e7d670e9d7c36e394cfcd1c0e67edfb2d49e8787" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit", + "file": "hackathon-confirmed-desktop.png", + "sha256": "8f7a6d2093f156bc0c67be17d8ebab121dac292bc366c1edb512dc4388bc002c" + }, + { + "directory": "Z:\\Документы\\PROCOLLAB-dev\\design-artifacts\\program-solutions-20261024\\qa01-unknown-submit", + "file": "hackathon-confirmed-mobile.png", + "sha256": "5a89e8b7ab8c4f33fcf60473ed3a18e79f2120808a9644b1a6136d690f06b497" + } + ], + "representation": "Copies may use repository Prettier formatting; source raw SHA and semantic SHA preserve original records, including FAIL findings" +} diff --git a/docs/qa/program-solutions-negative-20261025/negative-attempt-1792905580688.json b/docs/qa/program-solutions-negative-20261025/negative-attempt-1792905580688.json new file mode 100644 index 000000000..b34f20890 --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/negative-attempt-1792905580688.json @@ -0,0 +1,292 @@ +{ + "status": "FAIL", + "checkedAt": "2026-10-25T05:19:40.688Z", + "message": "AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:\n\n1 !== 0\n", + "events": [ + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solution-context/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/11/solutions/", + "status": 201 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/11/solutions/14/invitations/", + "status": 201 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/11/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/11/news/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/11/certificates/me/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/11/analytics-widget/", + "status": 200 + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/11/acknowledge-welcome/", + "status": 200 + }, + { + "role": "pending_a", + "method": "POST", + "path": "/solution-invitations/28/accept/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/files/", + "status": 201 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/11/solutions/14/submit/", + "status": 409 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + } + ], + "checks": [ + { + "program": 11, + "check": "local deadline fixture close", + "receipt": { + "program_id": 11, + "action": "close", + "submission_open": false, + "mode": "solution" + } + }, + { + "program": 11, + "check": "local deadline fixture restore", + "receipt": { + "program_id": 11, + "action": "restore", + "submission_open": true, + "mode": "solution" + } + } + ], + "faults": [], + "findings": [], + "state": { + "prefix": "qa01n-20261025-bdb96b29", + "programs": { + "11": { + "solutionId": 14, + "checks": [ + "empty draft saves, team focus, explicit missing case focus; no submit POST", + "pending does not satisfy team minimum; accepted member permits material/file focus from team tab" + ], + "emptyDone": true, + "basicsDone": true + } + } + }, + "javascriptErrors": [] +} diff --git a/docs/qa/program-solutions-negative-20261025/negative-attempt-1792905674286.json b/docs/qa/program-solutions-negative-20261025/negative-attempt-1792905674286.json new file mode 100644 index 000000000..f248a7316 --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/negative-attempt-1792905674286.json @@ -0,0 +1,130 @@ +{ + "status": "FAIL", + "checkedAt": "2026-10-25T05:21:14.286Z", + "message": "TimeoutError: locator.waitFor: Timeout 20000ms exceeded.\nCall log:\n\u001b[2m - waiting for getByRole('button', { name: 'Сдать решение', exact: true }) to be hidden\u001b[22m\n\u001b[2m 43 × locator resolved to visible \u001b[22m\n", + "events": [ + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solution-context/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/11/solutions/14/submit/", + "status": 409 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + } + ], + "checks": [ + { + "program": 11, + "check": "local deadline fixture close", + "receipt": { + "program_id": 11, + "action": "close", + "submission_open": false, + "mode": "solution" + } + }, + { + "program": 11, + "check": "local deadline fixture restore", + "receipt": { + "program_id": 11, + "action": "restore", + "submission_open": true, + "mode": "solution" + } + } + ], + "faults": [], + "findings": [], + "state": { + "prefix": "qa01n-20261025-bdb96b29", + "programs": { + "11": { + "solutionId": 14, + "checks": [ + "empty draft saves, team focus, explicit missing case focus; no submit POST", + "pending does not satisfy team minimum; accepted member permits material/file focus from team tab" + ], + "emptyDone": true, + "basicsDone": true + } + } + }, + "javascriptErrors": [] +} diff --git a/docs/qa/program-solutions-negative-20261025/negative-baseline.json b/docs/qa/program-solutions-negative-20261025/negative-baseline.json new file mode 100644 index 000000000..f24f46dcf --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/negative-baseline.json @@ -0,0 +1,15 @@ +{ + "counts": { + "partner_programs.PartnerProgram": 10, + "partner_programs.PartnerProgramField": 56, + "partner_programs.ProgramSolution": 10, + "partner_programs.ProgramSolutionAnswer": 35, + "partner_programs.ProgramSolutionMember": 16, + "partner_programs.SolutionInvitation": 9, + "project_rates.SolutionScore": 12, + "project_rates.SolutionExpertAssignment": 4, + "files.UserFile": 6, + "projects.Project": 2 + }, + "sha256": "42a279014e03858fa9266dc24ddc7a44d4ffca2d8c0600e4d934f2279d3a6aad" +} diff --git a/docs/qa/program-solutions-negative-20261025/negative-browser-before-fix.json b/docs/qa/program-solutions-negative-20261025/negative-browser-before-fix.json new file mode 100644 index 000000000..68d595fce --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/negative-browser-before-fix.json @@ -0,0 +1,649 @@ +{ + "status": "FAIL", + "checkedAt": "2026-10-25T05:23:12.507Z", + "prefix": "qa01n-20261025-bdb96b29", + "programs": { + "11": { + "solutionId": 14, + "checks": [ + "empty draft saves, team focus, explicit missing case focus; no submit POST", + "pending does not satisfy team minimum; accepted member permits material/file focus from team tab", + "deadline closes after confirmation PATCH: submit 409, remains draft; later draft save still works", + "save network failure preserves local input and does not persist until explicit retry", + "form conflict preserves unsaved name/text/0/false/case; new required blocks only draft; anchor snapshot unchanged", + "unknown submit result reconciled by GET without automatic POST retry" + ], + "emptyDone": true, + "basicsDone": true, + "deadlineDone": true, + "networkDone": true, + "addedFieldId": 82, + "previousRevision": "1642e7f74cb7810d2b06358b86cdbe314a5a88a41e982349d50e9747da5dd588", + "updatedRevision": "d399ec97f8f832812145b9cccb13d577134e95119267a494c17c7dc62c11da3f", + "rebaseDone": true, + "committedUnknownOutcome": true, + "complete": true, + "staleNetworkAlertAfterConfirmedSubmit": true + }, + "12": { + "solutionId": 15, + "checks": [ + "empty draft saves, team focus, explicit missing case focus; no submit POST", + "pending does not satisfy team minimum; accepted member permits material/file focus from team tab", + "deadline closes after confirmation PATCH: submit 409, remains draft; later draft save still works", + "save network failure preserves local input and does not persist until explicit retry", + "form conflict preserves unsaved name/text/0/false/case; new required blocks only draft; anchor snapshot unchanged", + "unknown submit result reconciled by GET without automatic POST retry" + ], + "emptyDone": true, + "basicsDone": true, + "deadlineDone": true, + "networkDone": true, + "addedFieldId": 83, + "previousRevision": "946c1dae1f6f53d603098d44da51c7504fe6110e3fd978b002840352d1cbb1d4", + "updatedRevision": "3e2981472138fcaa60afe45a8826a0a665debd1cff9eb7f78aabd5a2db5e0682", + "rebaseDone": true, + "committedUnknownOutcome": true, + "complete": true, + "staleNetworkAlertAfterConfirmedSubmit": true + } + }, + "checks": [ + { + "program": 11, + "check": "local deadline fixture close", + "receipt": { + "program_id": 11, + "action": "close", + "submission_open": false, + "mode": "solution" + } + }, + { + "program": 11, + "check": "local deadline fixture restore", + "receipt": { + "program_id": 11, + "action": "restore", + "submission_open": true, + "mode": "solution" + } + }, + { + "program": 11, + "solutionId": 14, + "checks": [ + "empty draft saves, team focus, explicit missing case focus; no submit POST", + "pending does not satisfy team minimum; accepted member permits material/file focus from team tab", + "deadline closes after confirmation PATCH: submit 409, remains draft; later draft save still works", + "save network failure preserves local input and does not persist until explicit retry", + "form conflict preserves unsaved name/text/0/false/case; new required blocks only draft; anchor snapshot unchanged", + "unknown submit result reconciled by GET without automatic POST retry" + ] + }, + { + "program": 12, + "check": "local deadline fixture close", + "receipt": { + "program_id": 12, + "action": "close", + "submission_open": false, + "mode": "solution" + } + }, + { + "program": 12, + "check": "local deadline fixture restore", + "receipt": { + "program_id": 12, + "action": "restore", + "submission_open": true, + "mode": "solution" + } + }, + { + "program": 12, + "solutionId": 15, + "checks": [ + "empty draft saves, team focus, explicit missing case focus; no submit POST", + "pending does not satisfy team minimum; accepted member permits material/file focus from team tab", + "deadline closes after confirmation PATCH: submit 409, remains draft; later draft save still works", + "save network failure preserves local input and does not persist until explicit retry", + "form conflict preserves unsaved name/text/0/false/case; new required blocks only draft; anchor snapshot unchanged", + "unknown submit result reconciled by GET without automatic POST retry" + ] + } + ], + "events": [ + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solution-context/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/11/solutions/14/submit/", + "status": 409 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "manager-http", + "method": "GET", + "path": "/programs/11/management/solution-form/", + "status": 200 + }, + { + "role": "manager-http", + "method": "PUT", + "path": "/programs/11/management/solution-form/", + "status": 200 + }, + { + "role": "manager-http", + "method": "GET", + "path": "/programs/11/management/solution-form/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 409 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/11/solutions/14/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solution-context/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/12/solutions/", + "status": 201 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/12/solutions/15/invitations/", + "status": 201 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/12/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/12/news/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/12/certificates/me/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/12/analytics-widget/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/12/acknowledge-welcome/", + "status": 200 + }, + { + "role": "pending_a", + "method": "POST", + "path": "/solution-invitations/29/accept/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/12/solutions/15/submit/", + "status": 409 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "manager-http", + "method": "GET", + "path": "/programs/12/management/solution-form/", + "status": 200 + }, + { + "role": "manager-http", + "method": "PUT", + "path": "/programs/12/management/solution-form/", + "status": 200 + }, + { + "role": "manager-http", + "method": "GET", + "path": "/programs/12/management/solution-form/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 409 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/12/solutions/15/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/12/solutions/15/", + "status": 200 + } + ], + "faults": [ + { + "program": 11, + "kind": "PATCH aborted before network", + "serverDataUnchanged": true + }, + { + "program": 11, + "kind": "submit response lost after real server commit", + "status": 200, + "solutionId": 14, + "version": 16 + }, + { + "program": 12, + "kind": "PATCH aborted before network", + "serverDataUnchanged": true + }, + { + "program": 12, + "kind": "submit response lost after real server commit", + "status": 200, + "solutionId": 15, + "version": 14 + } + ], + "findings": [ + { + "program": 11, + "solutionId": 14, + "issue": "Successful authoritative GET confirms submitted, but stale network error still claims failure" + }, + { + "program": 12, + "solutionId": 15, + "issue": "Successful authoritative GET confirms submitted, but stale network error still claims failure" + } + ], + "javascriptErrors": [], + "browser": "Headless Edge / Playwright", + "origin": "http://127.0.0.1:8875", + "api": "http://127.0.0.1:8871", + "widths": [320, 390, 768, 1440], + "limitations": [ + "synthetic fixtures intentionally changed; not zero writes", + "deadline controlled only in guarded local fixtures, restored after each phase", + "PATCH-before-send and lost-JSON-response fault injection explicitly recorded; API otherwise real", + "primary journey 390; final readonly states at other widths; full desktop/a11y/zoom not claimed", + "external avatars and injected external scripts blocked" + ] +} diff --git a/docs/qa/program-solutions-negative-20261025/negative-domain.json b/docs/qa/program-solutions-negative-20261025/negative-domain.json new file mode 100644 index 000000000..f46785dd2 --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/negative-domain.json @@ -0,0 +1,200 @@ +{ + "status": "PASS", + "scope": "domain and HTTP checks only; browser outcome remains in separate evidence", + "programs": [ + { + "program_id": 11, + "template": "case_championship", + "solution_id": 14, + "anchor_id": 12, + "anchorSnapshotUnchanged": true, + "zeroFalseCaseAndFileBytesRetained": true, + "deadlineRestored": true, + "newRequiredAppliesOnlyToNewSubmission": true + }, + { + "program_id": 12, + "template": "hackathon", + "solution_id": 15, + "anchor_id": 13, + "anchorSnapshotUnchanged": true, + "zeroFalseCaseAndFileBytesRetained": true, + "deadlineRestored": true, + "newRequiredAppliesOnlyToNewSubmission": true + } + ], + "httpChecks": [ + { + "path": "/programs/11/solutions/14/", + "role": "pending_b", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/11/solutions/14/", + "role": "pending_a", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/11/solutions/12/", + "role": "captain", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/11/management/solution-analytics/solutions/?field_id=82&answer_state=not_applicable", + "role": "manager", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/11/management/solution-analytics/solutions/?field_id=82&answer_state=present", + "role": "manager", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/11/management/solution-analytics/solutions/?field_id=82&answer_state=missing", + "role": "manager", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/11/solutions/14/", + "role": "anonymous", + "method": "GET", + "status": 401, + "code": "authentication_required" + }, + { + "path": "/programs/11/solutions/14/", + "role": "member", + "method": "GET", + "status": 404, + "code": "not_found" + }, + { + "path": "/programs/11/solutions/", + "role": "pending_b", + "method": "POST", + "status": 200, + "code": null + }, + { + "path": "/programs/11/solutions/14/", + "role": "pending_a", + "method": "PATCH", + "status": 403, + "code": "not_solution_captain" + }, + { + "path": "/programs/11/solutions/14/", + "role": "pending_b", + "method": "PATCH", + "status": 409, + "code": "team_frozen" + }, + { + "path": "/programs/11/solutions/14/submit/", + "role": "pending_b", + "method": "POST", + "status": 409, + "code": "already_submitted" + }, + { + "path": "/programs/12/solutions/15/", + "role": "pending_b", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/12/solutions/15/", + "role": "pending_a", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/12/solutions/13/", + "role": "captain", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/12/management/solution-analytics/solutions/?field_id=83&answer_state=not_applicable", + "role": "manager", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/12/management/solution-analytics/solutions/?field_id=83&answer_state=present", + "role": "manager", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/12/management/solution-analytics/solutions/?field_id=83&answer_state=missing", + "role": "manager", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/12/solutions/15/", + "role": "anonymous", + "method": "GET", + "status": 401, + "code": "authentication_required" + }, + { + "path": "/programs/12/solutions/15/", + "role": "member", + "method": "GET", + "status": 404, + "code": "not_found" + }, + { + "path": "/programs/12/solutions/", + "role": "pending_b", + "method": "POST", + "status": 200, + "code": null + }, + { + "path": "/programs/12/solutions/15/", + "role": "pending_a", + "method": "PATCH", + "status": 403, + "code": "not_solution_captain" + }, + { + "path": "/programs/12/solutions/15/", + "role": "pending_b", + "method": "PATCH", + "status": 409, + "code": "team_frozen" + }, + { + "path": "/programs/12/solutions/15/submit/", + "role": "pending_b", + "method": "POST", + "status": 409, + "code": "already_submitted" + } + ], + "verificationDomainMutations": 0, + "preexistingDataSha256": "42a279014e03858fa9266dc24ddc7a44d4ffca2d8c0600e4d934f2279d3a6aad", + "preexistingDataUnchanged": true, + "projectCountUnchanged": true +} diff --git a/docs/qa/program-solutions-negative-20261025/negative-seed.json b/docs/qa/program-solutions-negative-20261025/negative-seed.json new file mode 100644 index 000000000..ed6337a31 --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/negative-seed.json @@ -0,0 +1,104 @@ +{ + "status": "PASS", + "programs": [ + { + "id": 11, + "template": "case_championship", + "anchorId": 12 + }, + { + "id": 12, + "template": "hackathon", + "anchorId": 13 + } + ], + "events": [ + { + "role": "captain", + "method": "POST", + "path": "/programs/11/solutions/", + "status": 201 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/11/solutions/12/invitations/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/11/solutions/12/invitations/", + "status": 201 + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/26/accept/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/11/solutions/12/", + "status": 200 + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/11/solutions/12/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/11/solutions/12/submit/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/12/solutions/", + "status": 201 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/12/solutions/13/invitations/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/12/solutions/13/invitations/", + "status": 201 + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/27/accept/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/12/solutions/13/", + "status": 200 + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/12/solutions/13/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/12/solutions/13/submit/", + "status": 200 + } + ], + "usersCreated": 0, + "projectsCreated": 0, + "anchorSnapshotsRealHTTP": true +} diff --git a/docs/qa/program-solutions-negative-20261025/negative-state.json b/docs/qa/program-solutions-negative-20261025/negative-state.json new file mode 100644 index 000000000..469e41d4f --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/negative-state.json @@ -0,0 +1,49 @@ +{ + "prefix": "qa01n-20261025-bdb96b29", + "programs": { + "11": { + "solutionId": 14, + "checks": [ + "empty draft saves, team focus, explicit missing case focus; no submit POST", + "pending does not satisfy team minimum; accepted member permits material/file focus from team tab", + "deadline closes after confirmation PATCH: submit 409, remains draft; later draft save still works", + "save network failure preserves local input and does not persist until explicit retry", + "form conflict preserves unsaved name/text/0/false/case; new required blocks only draft; anchor snapshot unchanged", + "unknown submit result reconciled by GET without automatic POST retry" + ], + "emptyDone": true, + "basicsDone": true, + "deadlineDone": true, + "networkDone": true, + "addedFieldId": 82, + "previousRevision": "1642e7f74cb7810d2b06358b86cdbe314a5a88a41e982349d50e9747da5dd588", + "updatedRevision": "d399ec97f8f832812145b9cccb13d577134e95119267a494c17c7dc62c11da3f", + "rebaseDone": true, + "committedUnknownOutcome": true, + "complete": true, + "staleNetworkAlertAfterConfirmedSubmit": true + }, + "12": { + "solutionId": 15, + "checks": [ + "empty draft saves, team focus, explicit missing case focus; no submit POST", + "pending does not satisfy team minimum; accepted member permits material/file focus from team tab", + "deadline closes after confirmation PATCH: submit 409, remains draft; later draft save still works", + "save network failure preserves local input and does not persist until explicit retry", + "form conflict preserves unsaved name/text/0/false/case; new required blocks only draft; anchor snapshot unchanged", + "unknown submit result reconciled by GET without automatic POST retry" + ], + "emptyDone": true, + "basicsDone": true, + "deadlineDone": true, + "networkDone": true, + "addedFieldId": 83, + "previousRevision": "946c1dae1f6f53d603098d44da51c7504fe6110e3fd978b002840352d1cbb1d4", + "updatedRevision": "3e2981472138fcaa60afe45a8826a0a665debd1cff9eb7f78aabd5a2db5e0682", + "rebaseDone": true, + "committedUnknownOutcome": true, + "complete": true, + "staleNetworkAlertAfterConfirmedSubmit": true + } + } +} diff --git a/docs/qa/program-solutions-negative-20261025/unknown-baseline.json b/docs/qa/program-solutions-negative-20261025/unknown-baseline.json new file mode 100644 index 000000000..4c4ed17c5 --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/unknown-baseline.json @@ -0,0 +1,15 @@ +{ + "counts": { + "partner_programs.PartnerProgram": 12, + "partner_programs.PartnerProgramField": 76, + "partner_programs.ProgramSolution": 14, + "partner_programs.ProgramSolutionAnswer": 61, + "partner_programs.ProgramSolutionMember": 24, + "partner_programs.SolutionInvitation": 13, + "project_rates.SolutionScore": 12, + "project_rates.SolutionExpertAssignment": 4, + "files.UserFile": 7, + "projects.Project": 2 + }, + "sha256": "fe1ac880882cec6f8c3210028faa90d8a803c088d4027a283c34a9d8202df9c6" +} diff --git a/docs/qa/program-solutions-negative-20261025/unknown-browser-after-fix.json b/docs/qa/program-solutions-negative-20261025/unknown-browser-after-fix.json new file mode 100644 index 000000000..7a757e53e --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/unknown-browser-after-fix.json @@ -0,0 +1,661 @@ +{ + "status": "PASS", + "checkedAt": "2026-10-25T05:30:21.682Z", + "prefix": "qa01u-20261025-c5cbadd8", + "programs": { + "13": { + "solutionId": 18, + "confirmedVersion": 4, + "serverCommittedVersion": 5, + "serverCommitted": true, + "complete": true, + "networkErrorCleared": true, + "successNotice": true, + "submitPostCount": 1 + }, + "14": { + "solutionId": 19, + "confirmedVersion": 4, + "serverCommittedVersion": 5, + "serverCommitted": true, + "complete": true, + "networkErrorCleared": true, + "successNotice": true, + "submitPostCount": 1 + } + }, + "checks": [ + { + "program": 13, + "solutionId": 18, + "unknownOutcomeResolvedByGet": true, + "networkErrorCleared": true, + "readonly": true, + "zeroFalseRetained": true, + "automaticSubmitRetries": 0 + }, + { + "program": 14, + "solutionId": 19, + "unknownOutcomeResolvedByGet": true, + "networkErrorCleared": true, + "readonly": true, + "zeroFalseRetained": true, + "automaticSubmitRetries": 0 + } + ], + "events": [ + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/13/solution-context/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/13/solutions/", + "status": 201 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/13/solutions/18/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/13/solutions/18/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/13/solutions/18/invitations/", + "status": 201 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/13/solutions/18/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/13/solutions/18/invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/13/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/13/news/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/13/certificates/me/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/13/analytics-widget/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/13/acknowledge-welcome/", + "status": 200 + }, + { + "role": "pending_a", + "method": "POST", + "path": "/solution-invitations/32/accept/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/13/solutions/18/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/13/solutions/18/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/13/solutions/18/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/files/", + "status": 201 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/13/solutions/18/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/13/solutions/18/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/14/solution-context/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/14/solutions/", + "status": 201 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/14/solutions/19/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/14/solutions/19/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "POST", + "path": "/programs/14/solutions/19/invitations/", + "status": 201 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/14/solutions/19/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/14/solutions/19/invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/14/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/14/news/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/14/certificates/me/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/14/analytics-widget/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "POST", + "path": "/programs/14/acknowledge-welcome/", + "status": 200 + }, + { + "role": "pending_a", + "method": "POST", + "path": "/solution-invitations/33/accept/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_a", + "method": "GET", + "path": "/programs/14/solutions/19/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/solution-invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/14/solutions/19/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/14/solutions/19/invitations/", + "status": 200 + }, + { + "role": "pending_b", + "method": "PATCH", + "path": "/programs/14/solutions/19/", + "status": 200 + }, + { + "role": "pending_b", + "method": "GET", + "path": "/programs/14/solutions/19/", + "status": 200 + } + ], + "requests": [ + { + "method": "GET", + "path": "/auth/users/current/" + }, + { + "method": "GET", + "path": "/auth/users/types/" + }, + { + "method": "GET", + "path": "/auth/users/roles/" + }, + { + "method": "GET", + "path": "/industries/" + }, + { + "method": "GET", + "path": "/invites/" + }, + { + "method": "GET", + "path": "/chats/has-unreads/" + }, + { + "method": "GET", + "path": "/programs/" + }, + { + "method": "GET", + "path": "/core/notifications/" + }, + { + "method": "GET", + "path": "/programs/13/solution-context/" + }, + { + "method": "GET", + "path": "/solution-invitations/" + }, + { + "method": "POST", + "path": "/programs/13/solutions/" + }, + { + "method": "GET", + "path": "/programs/13/solutions/18/" + }, + { + "method": "GET", + "path": "/programs/13/solutions/18/invitations/" + }, + { + "method": "GET", + "path": "/auth/public-users/" + }, + { + "method": "POST", + "path": "/programs/13/solutions/18/invitations/" + }, + { + "method": "GET", + "path": "/programs/13/solutions/18/" + }, + { + "method": "GET", + "path": "/programs/13/solutions/18/invitations/" + }, + { + "method": "GET", + "path": "/auth/users/current/" + }, + { + "method": "GET", + "path": "/auth/users/types/" + }, + { + "method": "GET", + "path": "/auth/users/roles/" + }, + { + "method": "GET", + "path": "/industries/" + }, + { + "method": "GET", + "path": "/invites/" + }, + { + "method": "GET", + "path": "/chats/has-unreads/" + }, + { + "method": "GET", + "path": "/programs/" + }, + { + "method": "GET", + "path": "/core/notifications/" + }, + { + "method": "GET", + "path": "/programs/13/solutions/18/" + }, + { + "method": "GET", + "path": "/solution-invitations/" + }, + { + "method": "GET", + "path": "/programs/13/solutions/18/invitations/" + }, + { + "method": "POST", + "path": "/files/" + }, + { + "method": "PATCH", + "path": "/programs/13/solutions/18/" + }, + { + "method": "POST", + "path": "/programs/13/solutions/18/submit/" + }, + { + "method": "GET", + "path": "/programs/13/solutions/18/" + }, + { + "method": "GET", + "path": "/auth/users/current/" + }, + { + "method": "GET", + "path": "/auth/users/types/" + }, + { + "method": "GET", + "path": "/auth/users/roles/" + }, + { + "method": "GET", + "path": "/industries/" + }, + { + "method": "GET", + "path": "/invites/" + }, + { + "method": "GET", + "path": "/chats/has-unreads/" + }, + { + "method": "GET", + "path": "/programs/" + }, + { + "method": "GET", + "path": "/core/notifications/" + }, + { + "method": "GET", + "path": "/programs/14/solution-context/" + }, + { + "method": "GET", + "path": "/solution-invitations/" + }, + { + "method": "POST", + "path": "/programs/14/solutions/" + }, + { + "method": "GET", + "path": "/programs/14/solutions/19/" + }, + { + "method": "GET", + "path": "/programs/14/solutions/19/invitations/" + }, + { + "method": "GET", + "path": "/auth/public-users/" + }, + { + "method": "POST", + "path": "/programs/14/solutions/19/invitations/" + }, + { + "method": "GET", + "path": "/programs/14/solutions/19/" + }, + { + "method": "GET", + "path": "/programs/14/solutions/19/invitations/" + }, + { + "method": "GET", + "path": "/auth/users/current/" + }, + { + "method": "GET", + "path": "/auth/users/types/" + }, + { + "method": "GET", + "path": "/auth/users/roles/" + }, + { + "method": "GET", + "path": "/industries/" + }, + { + "method": "GET", + "path": "/invites/" + }, + { + "method": "GET", + "path": "/chats/has-unreads/" + }, + { + "method": "GET", + "path": "/programs/" + }, + { + "method": "GET", + "path": "/core/notifications/" + }, + { + "method": "GET", + "path": "/programs/14/solutions/19/" + }, + { + "method": "GET", + "path": "/solution-invitations/" + }, + { + "method": "GET", + "path": "/programs/14/solutions/19/invitations/" + }, + { + "method": "PATCH", + "path": "/programs/14/solutions/19/" + }, + { + "method": "POST", + "path": "/programs/14/solutions/19/submit/" + }, + { + "method": "GET", + "path": "/programs/14/solutions/19/" + } + ], + "faults": [ + { + "program": 13, + "solutionId": 18, + "kind": "real POST committed, response intentionally aborted", + "status": 200, + "version": 5 + }, + { + "program": 14, + "solutionId": 19, + "kind": "real POST committed, response intentionally aborted", + "status": 200, + "version": 5 + } + ], + "javascriptErrors": [], + "browser": "Headless Edge / Playwright", + "origin": "http://127.0.0.1:8876", + "api": "http://127.0.0.1:8871", + "widths": [320, 390, 768, 1440], + "buildProvenance": "C:/Users/evsee/AppData/Local/Temp/procollab-unknown-angular/qa-build-provenance.json", + "limitations": [ + "synthetic data intentionally created and submitted", + "only JSON submit response fault injected after real commit; no mocked success body", + "primary flow mobile 390; final states checked at other widths; full desktop/a11y/zoom not claimed", + "external avatars/scripts blocked; local file storage, no production CDN/mail/deploy" + ] +} diff --git a/docs/qa/program-solutions-negative-20261025/unknown-domain.json b/docs/qa/program-solutions-negative-20261025/unknown-domain.json new file mode 100644 index 000000000..9c418159a --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/unknown-domain.json @@ -0,0 +1,158 @@ +{ + "status": "PASS", + "scope": "domain and HTTP checks only; browser outcome remains in separate evidence", + "programs": [ + { + "program_id": 13, + "template": "case_championship", + "solution_id": 18, + "anchor_id": 16, + "anchorSnapshotUnchanged": true, + "zeroFalseCaseAndFileBytesRetained": true, + "deadlineRestored": true, + "newRequiredAppliesOnlyToNewSubmission": false + }, + { + "program_id": 14, + "template": "hackathon", + "solution_id": 19, + "anchor_id": 17, + "anchorSnapshotUnchanged": true, + "zeroFalseCaseAndFileBytesRetained": true, + "deadlineRestored": true, + "newRequiredAppliesOnlyToNewSubmission": false + } + ], + "httpChecks": [ + { + "path": "/programs/13/solutions/18/", + "role": "pending_b", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/13/solutions/18/", + "role": "pending_a", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/13/solutions/16/", + "role": "captain", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/13/solutions/18/", + "role": "anonymous", + "method": "GET", + "status": 401, + "code": "authentication_required" + }, + { + "path": "/programs/13/solutions/18/", + "role": "member", + "method": "GET", + "status": 404, + "code": "not_found" + }, + { + "path": "/programs/13/solutions/", + "role": "pending_b", + "method": "POST", + "status": 200, + "code": null + }, + { + "path": "/programs/13/solutions/18/", + "role": "pending_a", + "method": "PATCH", + "status": 403, + "code": "not_solution_captain" + }, + { + "path": "/programs/13/solutions/18/", + "role": "pending_b", + "method": "PATCH", + "status": 409, + "code": "team_frozen" + }, + { + "path": "/programs/13/solutions/18/submit/", + "role": "pending_b", + "method": "POST", + "status": 409, + "code": "already_submitted" + }, + { + "path": "/programs/14/solutions/19/", + "role": "pending_b", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/14/solutions/19/", + "role": "pending_a", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/14/solutions/17/", + "role": "captain", + "method": "GET", + "status": 200, + "code": null + }, + { + "path": "/programs/14/solutions/19/", + "role": "anonymous", + "method": "GET", + "status": 401, + "code": "authentication_required" + }, + { + "path": "/programs/14/solutions/19/", + "role": "member", + "method": "GET", + "status": 404, + "code": "not_found" + }, + { + "path": "/programs/14/solutions/", + "role": "pending_b", + "method": "POST", + "status": 200, + "code": null + }, + { + "path": "/programs/14/solutions/19/", + "role": "pending_a", + "method": "PATCH", + "status": 403, + "code": "not_solution_captain" + }, + { + "path": "/programs/14/solutions/19/", + "role": "pending_b", + "method": "PATCH", + "status": 409, + "code": "team_frozen" + }, + { + "path": "/programs/14/solutions/19/submit/", + "role": "pending_b", + "method": "POST", + "status": 409, + "code": "already_submitted" + } + ], + "verificationDomainMutations": 0, + "preexistingDataSha256": "fe1ac880882cec6f8c3210028faa90d8a803c088d4027a283c34a9d8202df9c6", + "preexistingDataUnchanged": true, + "projectCountUnchanged": true +} diff --git a/docs/qa/program-solutions-negative-20261025/unknown-seed.json b/docs/qa/program-solutions-negative-20261025/unknown-seed.json new file mode 100644 index 000000000..a872f4b5a --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/unknown-seed.json @@ -0,0 +1,104 @@ +{ + "status": "PASS", + "programs": [ + { + "id": 13, + "template": "case_championship", + "anchorId": 16 + }, + { + "id": 14, + "template": "hackathon", + "anchorId": 17 + } + ], + "events": [ + { + "role": "captain", + "method": "POST", + "path": "/programs/13/solutions/", + "status": 201 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/13/solutions/16/invitations/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/13/solutions/16/invitations/", + "status": 201 + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/30/accept/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/13/solutions/16/", + "status": 200 + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/13/solutions/16/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/13/solutions/16/submit/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/14/solutions/", + "status": 201 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/14/solutions/17/invitations/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/14/solutions/17/invitations/", + "status": 201 + }, + { + "role": "member", + "method": "POST", + "path": "/solution-invitations/31/accept/", + "status": 200 + }, + { + "role": "captain", + "method": "GET", + "path": "/programs/14/solutions/17/", + "status": 200 + }, + { + "role": "captain", + "method": "PATCH", + "path": "/programs/14/solutions/17/", + "status": 200 + }, + { + "role": "captain", + "method": "POST", + "path": "/programs/14/solutions/17/submit/", + "status": 200 + } + ], + "usersCreated": 0, + "projectsCreated": 0, + "anchorSnapshotsRealHTTP": true +} diff --git a/docs/qa/program-solutions-negative-20261025/unknown-state.json b/docs/qa/program-solutions-negative-20261025/unknown-state.json new file mode 100644 index 000000000..2d690c28c --- /dev/null +++ b/docs/qa/program-solutions-negative-20261025/unknown-state.json @@ -0,0 +1,25 @@ +{ + "prefix": "qa01u-20261025-c5cbadd8", + "programs": { + "13": { + "solutionId": 18, + "confirmedVersion": 4, + "serverCommittedVersion": 5, + "serverCommitted": true, + "complete": true, + "networkErrorCleared": true, + "successNotice": true, + "submitPostCount": 1 + }, + "14": { + "solutionId": 19, + "confirmedVersion": 4, + "serverCommittedVersion": 5, + "serverCommitted": true, + "complete": true, + "networkErrorCleared": true, + "successNotice": true, + "submitPostCount": 1 + } + } +} diff --git a/docs/qa/program-solutions-registration-removal-20261025/README.md b/docs/qa/program-solutions-registration-removal-20261025/README.md new file mode 100644 index 000000000..393a09b3f --- /dev/null +++ b/docs/qa/program-solutions-registration-removal-20261025/README.md @@ -0,0 +1,26 @@ + + +# QA01: удаление регистрации с сохранением решения + +25 октября 2026 года. FE API BE08C2: port/adapter/repository и четыре use cases без нового organizer UI. Это checkpoint первого выпуска; весь BE01–BE09/FE01–FE08/QA01/REL01 еще не завершен. + +Product refs: [backend abfb7c7e](https://github.com/PROCOLLAB-github/api/commit/abfb7c7e8584365cfe5846618cc03af719cc6fad), [frontend 7e5bf118](https://github.com/PROCOLLAB-github/procollab/commit/7e5bf1183bb15f3d99b030854df8b6bca11c4cb7). [C00 1.20.0](../../contracts/program-solutions-v1/README.md):156 схем/59 операций/297 независимых сценариев. Все55 прежних операций и272 сценария сохранены; две схемы истории расширены совместимо. Три shared файла byte-identical. [Scope BE08C2](https://github.com/PROCOLLAB-github/api/blob/abfb7c7e8584365cfe5846618cc03af719cc6fad/docs/product/program-solutions-be08-registration-removal.md). + +## Проверенный объем + +- Native PostgreSQL:417 solution tests/OK без skips, включая26 новых (23 функциональных,2 наблюдаемых гонки,1 historical migration). Отдельно152 legacy tests/OK по прежним12 labels Project/team/invites/management/case lifecycle. Эти числа не складываются с предыдущими22/416 прогоном. +- Frontend:2948 tests/484 files, strict types, development build и scoped ESLint PASS. В том числе135 case/member/archive/history/registration wire tests (33/22/27/22/31). Locked Prettier3.8.3 до report проверил2037 файлов; финальная проверка с report:2042 файлов PASS. +- Actual HTTP:74 schema-valid responses и44 valid requests, включая401/403/404/409, подпись, replay, score drift, display-name drift, pending-only, submitted member, captain bulk archive и регистрацию без решения. Набор включает первые68 ответов; завершенные mutations не повторялись. Финальные6 ответов сняты после restart API с финальным fingerprint. +- Local0038:23 модели,58 решений,4 архива,8 прежних case/member/archive events,4 Project и8 UserFile сохранены. SHA до/после `717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d`. Миграция не удаляла и не архивировала старые записи. + +Новые программы43/44/45: КЧ, хакатон, неконкурсная. Работы60/63 сняты после удаления регистраций их капитанов;63 остается draft без submit. В60 до архива отдельно отозван pending и исключен участник после сдачи: исходный/новый состав сохранен в истории, ответы и оценки string `0`/`False` остались. Назначения сохранены при исключении участника и сняты при архиве. Принятое приглашение сохранено, но доступа к материалам исключенному участнику не дает. Работы61/62 остаются draft с материалами и капитаном после удаления регистрации участника. Семь audit headers включают удаления без решения, manager/expert registration и сохраненные роли; шесть новых per-solution events. Все новые creation flags=False. Исходные23 модели совпадают с baseline и после HTTP; новый Project не создавался. Fixture IDs не относятся к реальным программам. + +Архивные составы и снимки не меняются при последующем удалении регистрации. Смешанные legacy links блокируют всю новую команду. Auth User, manager/expert, другие программы и Project handlers сохраняются. Original26 AC strings неизменны;23 PASS: AC01–17/20/22–26, AC18/19 OPEN, AC21 отдельно BE10. Дополнительные admin tests не подменяют исходные критерии. + +## Ограничения и воспроизводимость + +Сохранены ранний focused log (31 FE tests PASS), промежуточный416 и финальный417, первые68 и финальные74 HTTP-ответа. Ошибка расположения negative TypeScript directive была видна в terminal output и исправлена до финальных проверок. Первые31 FE focused tests прошли; type assertion исправлена до полного2948/types/build. Итоговые assertions не ослаблялись, skips на PostgreSQL отсутствуют. Raw logs/helpers находятся в `Z:/Документы/PROCOLLAB-dev/design-artifacts/program-solutions-20261024/qa01-solution-registration-removal`; SHA перечислены в [evidence.json](evidence.json). [HTTP wire](http-wire.json) проверен до удаления signed proof strings; 25 значений скрыто только в Git-копии, Authorization/JWT/password не записывались. Private baseline/manifest остаются в TEMP; migration helper повторно не запускать. + +Owned API8877 остановлен; прежние8769/8871/8876 не перезапускались и не доказывают текущий product. Binary/CDN operations проверяются native regression с mock, новая production CDN/browser приемка этим checkpoint не заявляется. Новый organizer UI/confirmation/history и навигация архива следуют D02; D03 и конкурсные несданные черновики уже запрошены и остаются открыты. Manager/reminder/actions/results/public screens и полный QA01/REL01 остаются в первом выпуске. BE10 отдельно. + +Preceding C1 Linux PG40208633691 terminal success:39 locking +1783 full/OK; General40208630839 еще in progress на момент упаковки. Это результат предыдущего bf5fc5a, не текущего C2. Новый HEAD CI фиксируется после push отдельно.0038 → backend → frontend; после C2 события сохранять readers/guards C00 1.20 и аудит, schema-down недопустим. Merge/deploy/реальных переключений/автоматического переноса или удаления Project не было. diff --git a/docs/qa/program-solutions-registration-removal-20261025/contract-evidence.json b/docs/qa/program-solutions-registration-removal-20261025/contract-evidence.json new file mode 100644 index 000000000..1abb07afa --- /dev/null +++ b/docs/qa/program-solutions-registration-removal-20261025/contract-evidence.json @@ -0,0 +1,18 @@ +{ + "status": "PASS", + "contract": "1.20.0", + "schemas": 156, + "operations": 59, + "scenarios": 297, + "oldSchemasUnchanged": 143, + "oldSchemasCompatiblyExtended": ["SolutionAdminChange", "SolutionArchiveAdminChange"], + "oldOperationsUnchanged": 55, + "oldScenariosUnchanged": 272, + "newScenarios": 25, + "sharedFileHashes": { + "openapi.json": "1f1350d730b66891e0c019bff92b97c0129f62ae6f3d261bab2e27a7953b7191", + "examples.json": "69cdb43626d6d89d8f7716d2d6f5777ed6f147496c72046dd364503fbf18a93f", + "README.md": "d6505ba0c6035e0486f66e2911032ff5ec430ea6dab2d8283abfc5284a2ce1ab" + }, + "originalAcceptanceRowsUnchanged": 26 +} diff --git a/docs/qa/program-solutions-registration-removal-20261025/evidence.json b/docs/qa/program-solutions-registration-removal-20261025/evidence.json new file mode 100644 index 000000000..2fb654d9a --- /dev/null +++ b/docs/qa/program-solutions-registration-removal-20261025/evidence.json @@ -0,0 +1,218 @@ +{ + "status": "PASS", + "classification": "BE08C2 backend + FE API checkpoint; first release incomplete", + "backendProduct": "abfb7c7e8584365cfe5846618cc03af719cc6fad", + "frontendProduct": "7e5bf1183bb15f3d99b030854df8b6bca11c4cb7", + "contract": { + "status": "PASS", + "contract": "1.20.0", + "schemas": 156, + "operations": 59, + "scenarios": 297, + "oldSchemasUnchanged": 143, + "oldSchemasCompatiblyExtended": ["SolutionAdminChange", "SolutionArchiveAdminChange"], + "oldOperationsUnchanged": 55, + "oldScenariosUnchanged": 272, + "newScenarios": 25, + "sharedFileHashes": { + "openapi.json": "1f1350d730b66891e0c019bff92b97c0129f62ae6f3d261bab2e27a7953b7191", + "examples.json": "69cdb43626d6d89d8f7716d2d6f5777ed6f147496c72046dd364503fbf18a93f", + "README.md": "d6505ba0c6035e0486f66e2911032ff5ec430ea6dab2d8283abfc5284a2ce1ab" + }, + "originalAcceptanceRowsUnchanged": 26 + }, + "http": { + "status": "PASS", + "contract": "1.20.0", + "responses": 74, + "validRequests": 44, + "programs": [ + { + "id": 43, + "key": "case", + "solutions": [60, 63], + "audits": [1, 2, 3, 4] + }, + { + "id": 44, + "key": "hack", + "solutions": [61], + "audits": [5, 7] + }, + { + "id": 45, + "key": "regular", + "solutions": [62], + "audits": [6] + } + ], + "oldModelsUnchanged": 23, + "hashBefore": "717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d", + "hashAfter": "717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d", + "existingProjectsPreserved": 4, + "existingArchivesPreserved": 4, + "existingHistoryPreserved": 8, + "newProjects": 0, + "localOnly": true, + "creationFlagsFalse": true + }, + "migration": { + "status": "PASS", + "migration": "0038_solution_registration_removal_audit", + "database": "local-procollab_ci", + "oldModelsUnchanged": 23, + "hashBefore": "717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d", + "hashAfter": "717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d", + "rowCounts": { + "partner_programs.PartnerProgram": 40, + "partner_programs.PartnerProgramField": 281, + "partner_programs.PartnerProgramUserProfile": 116, + "partner_programs.PartnerProgramProject": 3, + "partner_programs.PartnerProgramFieldValue": 2, + "partner_programs.ProgramSolution": 58, + "partner_programs.ProgramSolutionAnswer": 273, + "partner_programs.ProgramSolutionMember": 80, + "partner_programs.SolutionInvitation": 38, + "project_rates.SolutionScore": 34, + "project_rates.SolutionExpertAssignment": 20, + "project_rates.Criteria": 69, + "users.Expert": 7, + "files.UserFile": 8, + "projects.Project": 4, + "projects.Collaborator": 6, + "invites.Invite": 4, + "project_rates.ProjectScore": 3, + "project_rates.ProjectExpertAssignment": 1, + "industries.Industry": 1, + "users.Expert_programs": 27, + "partner_programs.PartnerProgram_managers": 44, + "partner_programs.SolutionAdministrativeChange": 8 + }, + "existingAdminEventsPreserved": 8, + "existingArchivedSolutionsPreserved": 4, + "existingRowsDeletedOrArchived": 0, + "existingProjectsMigratedOrDeleted": 0, + "onlyLocalSchema": true + }, + "tests": { + "backendSolutions": 417, + "newRegistrationTests": 26, + "newFunctionalTests": 23, + "newObservedPGRaces": 2, + "newHistoricalMigrationTests": 1, + "legacyRegression": 152, + "frontend": 2948, + "frontendFiles": 484, + "strictTypes": "PASS", + "developmentBuild": "PASS", + "administrativeWireTests": 135, + "wireBreakdown": { + "case": 33, + "member": 22, + "archive": 27, + "history": 22, + "registration": 31 + }, + "lockedPrettierBeforeReports": { + "version": "3.8.3", + "files": 2037, + "status": "PASS" + }, + "lockedPrettierWithReports": { + "version": "3.8.3", + "files": 2042, + "status": "PASS" + } + }, + "sourceHashesNormalizedLF": { + "backend": { + "docs/contracts/program-solutions-v1/README.md": "d6505ba0c6035e0486f66e2911032ff5ec430ea6dab2d8283abfc5284a2ce1ab", + "docs/contracts/program-solutions-v1/examples.json": "d2c5b8a5d81d6376150523dd8fd2e4da70801820e92ec3b1c815ec720ae41b9f", + "docs/contracts/program-solutions-v1/openapi.json": "5d417fac5d9dcc56a96c26176bfdb68c3e8c30f9cfb85084d285be0fdc70a397", + "docs/product/program-solutions-be08-admin-exceptions.md": "530041af9c96734cc6f88c57ec3800df448900263c6c5f19ef1d108b7b503c27", + "docs/product/program-solutions-be08-registration-removal.md": "59a9b188cf28d77b3e49d1bda94c02c988dd71c328c483e198af383789da6a1a", + "docs/product/program-solutions-qa01.md": "f6f6aa876478b0e6d7c1637ef8a69043e8ace402e13130e13b9325cbef3681be", + "docs/product/program-solutions-rel01.md": "1a9b3a7a132fa575484a529ecf03a8251ae63560864152763fe68363bf008cf7", + "partner_programs/migrations/0038_solution_registration_removal_audit.py": "a55958f8662cfb5b3d3fb55cfe5ee76d0ee6ff5d83595e206dcc84ac77a2017e", + "partner_programs/services/solution_archive_actions.py": "7571e0dff1b6bbcabccc69336c23e9de377ff6d31e9ed094542499991bebd7e5", + "partner_programs/services/solution_member_actions.py": "573679c07be01575d355f2556c1880ab3ebc0289c757b1b4910070cfed3ac1b3", + "partner_programs/services/solution_registration_actions.py": "799756778ac9b50c20ad106eb3863a3d455ed372b42e35be7f5bc667ba1fddbd", + "partner_programs/solution_admin.py": "e0bcacb374c18a4e495f5b4425e471483ec593f24844470f590bd4d5d00cf093", + "partner_programs/solution_history_api.py": "7417e9ede5889cfbac2ddc7be0167445b522d56a1fdba290286f624d5014c95f", + "partner_programs/solution_lifecycle_signals.py": "ab586c9344d83296679fabb36b7cd67ad048eecb8d8b6e6ba7abd8a548a6ce34", + "partner_programs/solution_models.py": "af30b98eb2bda77bea9ffbc5c4e33e2f2479d9c2bc9e6c301013416efc762803", + "partner_programs/solution_registration_action_api.py": "809d706802f2b090a4e871de284ea5fb813af0dfee9ab9c5faa07650efcf9e10", + "partner_programs/tests/test_solution_registration_actions.py": "d3c4acab25d915e2c5fa873ae7c4d7f1f9e528e96ef471efc9dadac88057099c", + "partner_programs/urls.py": "a5ea50390d1de3a76f56a456547cbb17504408eb2477f35af327b622f8dd851d" + }, + "frontend": { + "docs/contracts/program-solutions-v1/README.md": "d6505ba0c6035e0486f66e2911032ff5ec430ea6dab2d8283abfc5284a2ce1ab", + "docs/contracts/program-solutions-v1/examples.json": "d2c5b8a5d81d6376150523dd8fd2e4da70801820e92ec3b1c815ec720ae41b9f", + "docs/contracts/program-solutions-v1/openapi.json": "5d417fac5d9dcc56a96c26176bfdb68c3e8c30f9cfb85084d285be0fdc70a397", + "projects/social_platform/src/app/api/solution/solution-registration-removal-contract.spec.ts": "ac8391f523917d264db8e2132d536091c71fb31ab53e2299468aa45d1a097780", + "projects/social_platform/src/app/api/solution/use-cases/solution-registration-removal.use-cases.ts": "a74bc7bc6d772bd6911e4fd3d621f7815bed0c3faa08ec4c0befe8692336026b", + "projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts": "1149ef61a7b7df36399ac0dc8e5234543e76d8154dfd470342a733f6ede01ba2", + "projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts": "0e2fa788fa51b2b4dce80100ce7d85c4c8696f3281b4ef67e040f030b0b1d728", + "projects/social_platform/src/app/domain/solution/solution-contract.model.ts": "15f682687c0a437c2700da3dda35978bdca5b48f8c45eb94b42766e6d2cce801", + "projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts": "83e34263a224e20ffdcff393df3e85da85aa39633119d1c3e073617e1a8bb742", + "projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts": "698dfaddddb8e40adf862e9083b9bfc54e13648d9bd81bd4f7ffba781dfc61bf", + "tools/verify-solution-contract-types.ts": "0dfa81db6386ff479fad5c42351cd0f9d4a1f5345b449840c098bfc9027667d1" + } + }, + "rawArtifacts": { + "contract-evidence.json": "afc0dd26e3e6b6ca06975c451a056b74b103623c4d65708da7ec4695ebbeaee8", + "http-evidence.json": "dd2d4624589c2b601c20d699e82f89ca29999ecd61cfab26ef7e5b7c349c8678", + "http-wire.json": "c08bb97c9e77823ccf5a9d4f46fff4893a07f2c1d3c1295a669b48c587cb8f83", + "migration-evidence.json": "baa84cade150fb0ec2a635aa133ddacfe5d67de9ec9090eb57321a1e98571179", + "procollab-registration-contract-evidence.py": "4b982019e2c48cf3e079f7b0f61ef6dd1cb257c03782a43f38751b6e429d304b", + "procollab-registration-contract.py": "f80a5c3baa380fc31f4f7c9254fe716170160e4cbdea4a53b7f7cec82b339df3", + "procollab-registration-fe.py": "2cab10b1e7ec8f028d4a207f381f9607b164c91b1a91402ceec4702ccf92e32a", + "procollab-registration-migration-order.py": "b823b89c361b8c978bd8d68c8ec5ea66c6df3898265baff077e263e6813c2284", + "procollab-registration-product-docs.py": "ee50d34a8a067073e1218fd4b5364033e3717298b658412bb69857fce53ed264", + "procollab-registration-refactor.py": "25a038886d297dfa36c6615a761436979155799f2c4de00d7ff07ab0f0638c57", + "procollab-solution-registration-fe-build.log": "1044af952d79b8eeedbbfea14dc214844e935ba5dc7d01184ebb08f5c3d39e58", + "procollab-solution-registration-fe-focused.log": "94bab194d822f8e2b3023c62168236733f79fa264ee02d7652eb63ee9ad21d12", + "procollab-solution-registration-fe-suite.log": "4c39cd4c6b166675923dee94ff891e916497aa3ee626c00d5320b42ccf91b7c3", + "procollab-solution-registration-focused.log": "6bbcccaa7eb9fa6940805292ece4c8ae1e79dbd18bc75e19c3e826ddd701b453", + "procollab-solution-registration-http-final.log": "3e7c93f2d26256e4c0734717be2bf7f6851e3898a6ce6a66bbaabfa2986dc366", + "procollab-solution-registration-http.log": "ce990abb70cb4ee201b634ef702ccddf5d097c94a44a3717540f7511e6800e04", + "procollab-solution-registration-http.py": "02ee0f6fd48ec257ce3fdc2120a08bf64cbb2b1ae3ee166283e88c5d9ba2dacb", + "procollab-solution-registration-legacy.log": "e7dd155a56641e2bf7af21983729db35195b690aecdfef8c05be9009b9d73abb", + "procollab-solution-registration-migrate.py": "a243dea3bfa4f5dc436a68b7e096cd609c03558afadb3502f71746fb21f61d67", + "procollab-solution-registration-preceding-pg-ci.log": "b825f20caaa349eab8cde15cbae34bb23bf792c471d8f538f07d07ddceb96691", + "procollab-solution-registration-server.py": "2b2edeffed8c144f469726a59db01561f309ddb0c792927cc44a4948a0aa1e27", + "procollab-solution-registration-suite-final.log": "c00acfd82f3f812a89a2cf4b7e70267920073a5d7e15effdbc5ee91237f3918e", + "procollab-solution-registration-suite.log": "87b83b32a56774d3856415476dfb035fd7bb0d1305ab98288c75835208e7ac01", + "procollab-solution-registration-final-format.json": "7b9282c7f10eef7761182843bc43761099da5080beb561f7299ab1fa3954b9b7" + }, + "retainedAttempts": [ + "First native focused 22/OK; later final new test group26 is included in417.", + "First FE focused31 passed; negative TS assertion directive moved after Prettier before final full2948/types/build.", + "Initial full416 passed; final417 repeated after binding display identity in no-solution preview.", + "Initial68 HTTP responses retained; final74 includes an actual stale identity confirmation on the final restarted API." + ], + "publication": "Program-only storage remains unchanged; competitive unsent drafts eligibility pending.", + "qa": { + "originalAcceptanceRows": 26, + "passCount": 23, + "passAC": "01-17,20,22-26", + "openAC": ["18", "19"], + "separateBE10": "21" + }, + "limitations": [ + "No new organizer/public UI/browser/mobile acceptance in this checkpoint.", + "D02/D03 approval, manager/reminder/actions/results/public screens, archive navigation, competitive draft eligibility and full QA01/REL01 remain in first release.", + "No merge/deploy/real-program switch/Project migration or deletion.", + "Current HEAD Linux CI is not yet claimed; preceding C1 PG run40208633691:39 locking +1783 full/OK. General40208630839 still in progress at packaging." + ], + "helpers": { + "ownedApiStopped": true, + "releasedPort": 8877, + "oldServersUnchanged": { + "8769": 20920, + "8871": 19092, + "8876": 33332 + } + }, + "redactedSignedProofOccurrences": 25 +} diff --git a/docs/qa/program-solutions-registration-removal-20261025/http-wire.json b/docs/qa/program-solutions-registration-removal-20261025/http-wire.json new file mode 100644 index 000000000..64938619a --- /dev/null +++ b/docs/qa/program-solutions-registration-removal-20261025/http-wire.json @@ -0,0 +1,6339 @@ +[ + { + "actor": "captain", + "method": "POST", + "path": "/programs/43/solutions/", + "status": 201, + "request": { + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "response": { + "id": 60, + "program_id": 43, + "version": 1, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:51.346683Z", + "updated_at": "2026-10-25T16:20:51.346683Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 43, + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "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": 293 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 294 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 295 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 296 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/43/solutions/60/", + "status": 200, + "request": null, + "response": { + "id": 60, + "program_id": 43, + "version": 1, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:51.346683Z", + "updated_at": "2026-10-25T16:20:51.346683Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 43, + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "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": 293 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 294 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 295 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 296 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/43/solutions/60/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 20, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 64, + "version": 1, + "program": { + "id": 43, + "name": "Удаление регистраций case qa01reg-20261025-c45626d0" + }, + "solution": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "captain": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T16:20:51.746859Z", + "updated_at": "2026-10-25T16:20:51.746859Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/64/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 64, + "version": 2, + "program": { + "id": 43, + "name": "Удаление регистраций case qa01reg-20261025-c45626d0" + }, + "solution": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "captain": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T16:20:51.746859Z", + "updated_at": "2026-10-25T16:20:51.929766Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/43/solutions/60/", + "status": 200, + "request": null, + "response": { + "id": 60, + "program_id": 43, + "version": 3, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:51.346683Z", + "updated_at": "2026-10-25T16:20:51.940304Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true + }, + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 43, + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "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": 293 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 294 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 295 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 296 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/43/solutions/60/invitations/", + "status": 201, + "request": { + "expected_solution_version": 3, + "recipient_user_id": 21, + "role": "Дизайнер" + }, + "response": { + "invitation": { + "id": 65, + "version": 1, + "program": { + "id": 43, + "name": "Удаление регистраций case qa01reg-20261025-c45626d0" + }, + "solution": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "captain": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 21, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Дизайнер", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T16:20:52.312212Z", + "updated_at": "2026-10-25T16:20:52.312212Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 4 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/43/solutions/60/", + "status": 200, + "request": null, + "response": { + "id": 60, + "program_id": 43, + "version": 4, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:51.346683Z", + "updated_at": "2026-10-25T16:20:52.319311Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true + }, + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 43, + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "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": 293 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 294 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 295 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 296 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/43/solutions/60/", + "status": 200, + "request": { + "expected_version": 4, + "expected_form_revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "answers": [ + { + "field_id": 293, + "value": "Город" + }, + { + "field_id": 294, + "value": "Материалы qa01reg-20261025-c45626d0" + }, + { + "field_id": 295, + "value": 0 + }, + { + "field_id": 296, + "value": false + } + ] + }, + "response": { + "id": 60, + "program_id": 43, + "version": 5, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:51.346683Z", + "updated_at": "2026-10-25T16:20:52.718844Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true + }, + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 43, + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 293, + "value": "Город" + }, + { + "field_id": 294, + "value": "Материалы qa01reg-20261025-c45626d0" + }, + { + "field_id": 295, + "value": 0 + }, + { + "field_id": 296, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "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 + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/43/solutions/60/submit/", + "status": 200, + "request": { + "expected_version": 5, + "expected_form_revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992" + }, + "response": { + "id": 60, + "program_id": 43, + "version": 6, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:51.346683Z", + "updated_at": "2026-10-25T16:20:53.019840Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true + }, + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 43, + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 293, + "value": "Город" + }, + { + "field_id": 294, + "value": "Материалы qa01reg-20261025-c45626d0" + }, + { + "field_id": 295, + "value": 0 + }, + { + "field_id": 296, + "value": false + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "submitted_at": "2026-10-25T16:20:53.016408Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/44/solutions/", + "status": 201, + "request": { + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "response": { + "id": 61, + "program_id": 44, + "version": 1, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:53.173180Z", + "updated_at": "2026-10-25T16:20:53.173180Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:53.178841Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 44, + "revision": "0f42c860c99957bf2cf0f5d0f71bb387e1e332e6cc40031acf11786e7e91f3d4", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 297, + "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": 298, + "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": 299, + "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": 300, + "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": 301, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T16:20:51.000466Z", + "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": 297 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 298 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 299 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 300 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 301 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/44/solutions/61/", + "status": 200, + "request": null, + "response": { + "id": 61, + "program_id": 44, + "version": 1, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:53.173180Z", + "updated_at": "2026-10-25T16:20:53.173180Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:53.178841Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 44, + "revision": "0f42c860c99957bf2cf0f5d0f71bb387e1e332e6cc40031acf11786e7e91f3d4", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 297, + "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": 298, + "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": 299, + "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": 300, + "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": 301, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T16:20:51.000466Z", + "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": 297 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 298 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 299 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 300 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 301 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/44/solutions/61/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 20, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 66, + "version": 1, + "program": { + "id": 44, + "name": "Удаление регистраций hack qa01reg-20261025-c45626d0" + }, + "solution": { + "kind": "solution", + "program_id": 44, + "solution_id": 61, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "captain": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T16:20:53.589134Z", + "updated_at": "2026-10-25T16:20:53.589134Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/66/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 66, + "version": 2, + "program": { + "id": 44, + "name": "Удаление регистраций hack qa01reg-20261025-c45626d0" + }, + "solution": { + "kind": "solution", + "program_id": 44, + "solution_id": 61, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "captain": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T16:20:53.589134Z", + "updated_at": "2026-10-25T16:20:53.733409Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 44, + "solution_id": 61, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/44/solutions/61/", + "status": 200, + "request": null, + "response": { + "id": 61, + "program_id": 44, + "version": 3, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:53.173180Z", + "updated_at": "2026-10-25T16:20:53.741105Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:53.178841Z", + "is_captain": true + }, + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:53.725029Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 44, + "revision": "0f42c860c99957bf2cf0f5d0f71bb387e1e332e6cc40031acf11786e7e91f3d4", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 297, + "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": 298, + "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": 299, + "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": 300, + "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": 301, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "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-26T16:20:51.000466Z", + "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": 297 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 298 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 299 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 300 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 301 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/44/solutions/61/", + "status": 200, + "request": { + "expected_version": 3, + "expected_form_revision": "0f42c860c99957bf2cf0f5d0f71bb387e1e332e6cc40031acf11786e7e91f3d4", + "answers": [ + { + "field_id": 297, + "value": "Город" + }, + { + "field_id": 298, + "value": "Материалы qa01reg-20261025-c45626d0" + }, + { + "field_id": 299, + "value": 0 + }, + { + "field_id": 300, + "value": false + }, + { + "field_id": 301, + "value": "https://example.com/registration-code/qa01reg-20261025-c45626d0" + } + ] + }, + "response": { + "id": 61, + "program_id": 44, + "version": 4, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:53.173180Z", + "updated_at": "2026-10-25T16:20:54.066386Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:53.178841Z", + "is_captain": true + }, + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:53.725029Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 44, + "revision": "0f42c860c99957bf2cf0f5d0f71bb387e1e332e6cc40031acf11786e7e91f3d4", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 297, + "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": 298, + "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": 299, + "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": 300, + "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": 301, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 297, + "value": "Город" + }, + { + "field_id": 298, + "value": "Материалы qa01reg-20261025-c45626d0" + }, + { + "field_id": 299, + "value": 0 + }, + { + "field_id": 300, + "value": false + }, + { + "field_id": 301, + "value": "https://example.com/registration-code/qa01reg-20261025-c45626d0" + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "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 + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/45/solutions/", + "status": 201, + "request": { + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "response": { + "id": 62, + "program_id": 45, + "version": 1, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:54.245085Z", + "updated_at": "2026-10-25T16:20:54.245085Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:54.252779Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 45, + "revision": "c89c925124ce46e44f6f2e9809dcbeb92a75003cccb7362e5ab04ffd112ec14f", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 302, + "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": 303, + "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": 304, + "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": 305, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/45/solutions/62/", + "status": 200, + "request": null, + "response": { + "id": 62, + "program_id": 45, + "version": 1, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:54.245085Z", + "updated_at": "2026-10-25T16:20:54.245085Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:54.252779Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 45, + "revision": "c89c925124ce46e44f6f2e9809dcbeb92a75003cccb7362e5ab04ffd112ec14f", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 302, + "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": 303, + "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": 304, + "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": 305, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "POST", + "path": "/programs/45/solutions/62/invitations/", + "status": 201, + "request": { + "expected_solution_version": 1, + "recipient_user_id": 20, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 67, + "version": 1, + "program": { + "id": 45, + "name": "Удаление регистраций regular qa01reg-20261025-c45626d0" + }, + "solution": { + "kind": "solution", + "program_id": 45, + "solution_id": 62, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "captain": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "pending", + "created_at": "2026-10-25T16:20:54.531130Z", + "updated_at": "2026-10-25T16:20:54.531130Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 2 + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "POST", + "path": "/solution-invitations/67/accept/", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 67, + "version": 2, + "program": { + "id": 45, + "name": "Удаление регистраций regular qa01reg-20261025-c45626d0" + }, + "solution": { + "kind": "solution", + "program_id": 45, + "solution_id": 62, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "captain": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T16:20:54.531130Z", + "updated_at": "2026-10-25T16:20:54.708310Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 45, + "solution_id": 62, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 3 + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/45/solutions/62/", + "status": 200, + "request": null, + "response": { + "id": 62, + "program_id": 45, + "version": 3, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:54.245085Z", + "updated_at": "2026-10-25T16:20:54.716052Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:54.252779Z", + "is_captain": true + }, + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:54.696345Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 45, + "revision": "c89c925124ce46e44f6f2e9809dcbeb92a75003cccb7362e5ab04ffd112ec14f", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 302, + "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": 303, + "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": 304, + "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": 305, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "captain", + "method": "PATCH", + "path": "/programs/45/solutions/62/", + "status": 200, + "request": { + "expected_version": 3, + "expected_form_revision": "c89c925124ce46e44f6f2e9809dcbeb92a75003cccb7362e5ab04ffd112ec14f", + "answers": [ + { + "field_id": 302, + "value": "Город" + }, + { + "field_id": 303, + "value": "Материалы qa01reg-20261025-c45626d0" + }, + { + "field_id": 304, + "value": 0 + }, + { + "field_id": 305, + "value": false + } + ] + }, + "response": { + "id": 62, + "program_id": 45, + "version": 4, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:54.245085Z", + "updated_at": "2026-10-25T16:20:55.061769Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:54.252779Z", + "is_captain": true + }, + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:54.696345Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 0, + "reserved_count": 2, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 45, + "revision": "c89c925124ce46e44f6f2e9809dcbeb92a75003cccb7362e5ab04ffd112ec14f", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 302, + "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": 303, + "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": 304, + "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": 305, + "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 + } + } + ] + }, + "answers": [ + { + "field_id": 302, + "value": "Город" + }, + { + "field_id": 303, + "value": "Материалы qa01reg-20261025-c45626d0" + }, + { + "field_id": 304, + "value": 0 + }, + { + "field_id": 305, + "value": false + } + ], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false, + "can_leave": false + }, + "readiness": null + }, + "cacheControl": null + }, + { + "actor": "extra", + "method": "POST", + "path": "/programs/43/solutions/", + "status": 201, + "request": { + "name": "Работа extra qa01reg-20261025-c45626d0" + }, + "response": { + "id": 63, + "program_id": 43, + "version": 1, + "name": "Работа extra qa01reg-20261025-c45626d0", + "captain_id": 22, + "created_at": "2026-10-25T16:20:55.355057Z", + "updated_at": "2026-10-25T16:20:55.355057Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 22, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:55.362746Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 43, + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "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": 293 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 294 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 295 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 296 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "extra", + "method": "GET", + "path": "/programs/43/solutions/63/", + "status": 200, + "request": null, + "response": { + "id": 63, + "program_id": 43, + "version": 1, + "name": "Работа extra qa01reg-20261025-c45626d0", + "captain_id": 22, + "created_at": "2026-10-25T16:20:55.355057Z", + "updated_at": "2026-10-25T16:20:55.355057Z", + "participation": { + "status": "active", + "archived_at": null, + "reason": null + }, + "team": { + "members": [ + { + "user": { + "id": 22, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-25T16:20:55.362746Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 43, + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-26T16:20:51.000466Z", + "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": 293 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 294 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 295 + }, + { + "path": "answers", + "code": "required", + "detail": "Заполните обязательное поле.", + "field_id": 296 + } + ] + } + }, + "cacheControl": null + }, + { + "actor": null, + "method": "POST", + "path": "/programs/43/management/registrations/remove/preview/", + "status": 401, + "request": { + "registration_ids": [124] + }, + "response": { + "code": "authentication_required", + "detail": "Authentication credentials were not provided.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "member", + "method": "POST", + "path": "/programs/43/management/registrations/remove/preview/", + "status": 403, + "request": { + "registration_ids": [124] + }, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/preview/", + "status": 404, + "request": { + "registration_ids": [131] + }, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/34/management/registrations/remove/preview/", + "status": 409, + "request": { + "registration_ids": [124] + }, + "response": { + "code": "solution_mode_unavailable", + "detail": "Программа использует заявки на основе проектов.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [124] + }, + "response": { + "registrations": [ + { + "registration_id": 124, + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 6, + "is_submitted": true, + "effect": "remove_members", + "removed_members": [ + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false, + "specialization": null + } + ], + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 2 + } + ], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 1, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 2, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [124], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [125] + }, + "response": { + "registrations": [ + { + "registration_id": 125, + "user": { + "id": 21, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 6, + "is_submitted": true, + "effect": "revoke_invitations", + "removed_members": [], + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "score_count": 2 + } + ], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 1, + "score_count": 2, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 200, + "request": { + "registration_ids": [125], + "preview_token": "" + }, + "response": { + "audit_id": 1, + "deleted_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 7, + "change_id": 9, + "action": "remove_registrations" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [125], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/registrations/removals/1/", + "status": 200, + "request": null, + "response": { + "audit": { + "id": 1, + "program_id": 43, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:57.086574Z", + "registrations": [ + { + "user": { + "id": 21, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 125 + } + ], + "deleted_count": 1 + }, + "changes": [ + { + "id": 9, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:57.101452Z", + "before_version": 6, + "after_version": 7, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_members": [], + "registration_removal_id": 1, + "revoked_invitation_count": 1 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/solutions/60/history/9/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 9, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:57.101452Z", + "before_version": 6, + "after_version": 7, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_members": [], + "registration_removal_id": 1, + "revoked_invitation_count": 1 + }, + "before_snapshot": { + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain qa01reg-20261025-c45626d0", + "answers": [ + { + "value": "Город", + "field_id": 293 + }, + { + "value": "Материалы qa01reg-20261025-c45626d0", + "field_id": 294 + }, + { + "value": 0, + "field_id": 295 + }, + { + "value": false, + "field_id": 296 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 19, + "submitted_at": "2026-10-25T16:20:53.016408Z" + }, + "after_snapshot": { + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain qa01reg-20261025-c45626d0", + "answers": [ + { + "value": "Город", + "field_id": 293 + }, + { + "value": "Материалы qa01reg-20261025-c45626d0", + "field_id": 294 + }, + { + "value": 0, + "field_id": 295 + }, + { + "value": false, + "field_id": 296 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 19, + "submitted_at": "2026-10-25T16:20:53.016408Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [124] + }, + "response": { + "registrations": [ + { + "registration_id": 124, + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 7, + "is_submitted": true, + "effect": "remove_members", + "removed_members": [ + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false, + "specialization": null + } + ], + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 2 + } + ], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 1, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 2, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 200, + "request": { + "registration_ids": [124], + "preview_token": "" + }, + "response": { + "audit_id": 2, + "deleted_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 8, + "change_id": 10, + "action": "remove_registrations" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [124], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/registrations/removals/2/", + "status": 200, + "request": null, + "response": { + "audit": { + "id": 2, + "program_id": 43, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:57.967511Z", + "registrations": [ + { + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 124 + } + ], + "deleted_count": 1 + }, + "changes": [ + { + "id": 10, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:57.982484Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_members": [ + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false, + "specialization": null + } + ], + "registration_removal_id": 2, + "revoked_invitation_count": 0 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/solutions/60/history/10/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 10, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:57.982484Z", + "before_version": 7, + "after_version": 8, + "removed_assignments_count": 0, + "score_count": 2, + "was_submitted": true, + "removed_members": [ + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false, + "specialization": null + } + ], + "registration_removal_id": 2, + "revoked_invitation_count": 0 + }, + "before_snapshot": { + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain qa01reg-20261025-c45626d0", + "answers": [ + { + "value": "Город", + "field_id": 293 + }, + { + "value": "Материалы qa01reg-20261025-c45626d0", + "field_id": 294 + }, + { + "value": 0, + "field_id": 295 + }, + { + "value": false, + "field_id": 296 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true, + "specialization": null + }, + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.918099Z", + "is_captain": false, + "specialization": null + } + ], + "captain_id": 19, + "submitted_at": "2026-10-25T16:20:53.016408Z" + }, + "after_snapshot": { + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain qa01reg-20261025-c45626d0", + "answers": [ + { + "value": "Город", + "field_id": 293 + }, + { + "value": "Материалы qa01reg-20261025-c45626d0", + "field_id": 294 + }, + { + "value": 0, + "field_id": 295 + }, + { + "value": false, + "field_id": 296 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true, + "specialization": null + } + ], + "captain_id": 19, + "submitted_at": "2026-10-25T16:20:53.016408Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/43/solutions/60/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": null + }, + { + "actor": "member", + "method": "GET", + "path": "/solution-invitations/64/", + "status": 200, + "request": null, + "response": { + "id": 64, + "version": 2, + "program": { + "id": 43, + "name": "Удаление регистраций case qa01reg-20261025-c45626d0" + }, + "solution": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "captain": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "recipient": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "motivational_letter": null, + "status": "accepted", + "created_at": "2026-10-25T16:20:51.746859Z", + "updated_at": "2026-10-25T16:20:51.929766Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [123, 126] + }, + "response": { + "registrations": [ + { + "registration_id": 123, + "user": { + "id": 19, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + }, + { + "registration_id": 126, + "user": { + "id": 22, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 8, + "is_submitted": true, + "effect": "archive", + "removed_members": [], + "removed_assignments_count": 2, + "revoked_invitation_count": 0, + "score_count": 2 + }, + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 63, + "name": "Работа extra qa01reg-20261025-c45626d0" + }, + "solution_version": 1, + "is_submitted": false, + "effect": "archive", + "removed_members": [], + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0 + } + ], + "deleted_count": 2, + "archived_count": 2, + "removed_member_count": 0, + "removed_assignments_count": 2, + "revoked_invitation_count": 0, + "score_count": 2, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 200, + "request": { + "registration_ids": [126, 123], + "preview_token": "" + }, + "response": { + "audit_id": 3, + "deleted_count": 2, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 9, + "change_id": 11, + "action": "archive" + }, + { + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 63, + "name": "Работа extra qa01reg-20261025-c45626d0" + }, + "solution_version": 2, + "change_id": 12, + "action": "archive" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [126, 123], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/registrations/removals/3/", + "status": 200, + "request": null, + "response": { + "audit": { + "id": 3, + "program_id": 43, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:59.221440Z", + "registrations": [ + { + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 123 + }, + { + "user": { + "id": 22, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 126 + } + ], + "deleted_count": 2 + }, + "changes": [ + { + "id": 11, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:59.243734Z", + "before_version": 8, + "after_version": 9, + "removed_assignments_count": 2, + "score_count": 2, + "was_submitted": true, + "archived_at": "2026-10-25T16:20:59.222947Z", + "reason": "registration_removed", + "revoked_invitation_count": 0, + "registration_removal_id": 3 + }, + { + "id": 12, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 63, + "name": "Работа extra qa01reg-20261025-c45626d0" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:59.262521Z", + "before_version": 1, + "after_version": 2, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T16:20:59.222947Z", + "reason": "registration_removed", + "revoked_invitation_count": 0, + "registration_removal_id": 3 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/solutions/60/history/11/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 11, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 60, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:59.243734Z", + "before_version": 8, + "after_version": 9, + "removed_assignments_count": 2, + "score_count": 2, + "was_submitted": true, + "archived_at": "2026-10-25T16:20:59.222947Z", + "reason": "registration_removed", + "revoked_invitation_count": 0, + "registration_removal_id": 3 + }, + "before_snapshot": { + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain qa01reg-20261025-c45626d0", + "answers": [ + { + "value": "Город", + "field_id": 293 + }, + { + "value": "Материалы qa01reg-20261025-c45626d0", + "field_id": 294 + }, + { + "value": 0, + "field_id": 295 + }, + { + "value": false, + "field_id": 296 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true, + "specialization": null + } + ], + "captain_id": 19, + "submitted_at": "2026-10-25T16:20:53.016408Z" + }, + "after_snapshot": { + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain qa01reg-20261025-c45626d0", + "answers": [ + { + "value": "Город", + "field_id": 293 + }, + { + "value": "Материалы qa01reg-20261025-c45626d0", + "field_id": 294 + }, + { + "value": 0, + "field_id": 295 + }, + { + "value": false, + "field_id": 296 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true, + "specialization": null + } + ], + "captain_id": 19, + "submitted_at": "2026-10-25T16:20:53.016408Z" + }, + "archive_snapshot": { + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа captain qa01reg-20261025-c45626d0", + "answers": [ + { + "value": "Город", + "field_id": 293 + }, + { + "value": "Материалы qa01reg-20261025-c45626d0", + "field_id": 294 + }, + { + "value": 0, + "field_id": 295 + }, + { + "value": false, + "field_id": 296 + } + ], + "members": [ + { + "role": null, + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true, + "specialization": null + } + ], + "captain_id": 19, + "captured_at": "2026-10-25T16:20:59.222947Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/solutions/63/history/12/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 12, + "application": { + "kind": "solution", + "program_id": 43, + "solution_id": 63, + "name": "Работа extra qa01reg-20261025-c45626d0" + }, + "action": "archive", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:59.262521Z", + "before_version": 1, + "after_version": 2, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "archived_at": "2026-10-25T16:20:59.222947Z", + "reason": "registration_removed", + "revoked_invitation_count": 0, + "registration_removal_id": 3 + }, + "before_snapshot": null, + "after_snapshot": null, + "archive_snapshot": { + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "name": "Работа extra qa01reg-20261025-c45626d0", + "answers": [], + "members": [ + { + "role": null, + "user": { + "id": 22, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:55.362746Z", + "is_captain": true, + "specialization": null + } + ], + "captain_id": 22, + "captured_at": "2026-10-25T16:20:59.222947Z" + } + }, + "cacheControl": "private, no-store" + }, + { + "actor": "captain", + "method": "GET", + "path": "/programs/43/solutions/60/", + "status": 200, + "request": null, + "response": { + "id": 60, + "program_id": 43, + "version": 9, + "name": "Работа captain qa01reg-20261025-c45626d0", + "captain_id": 19, + "created_at": "2026-10-25T16:20:51.346683Z", + "updated_at": "2026-10-25T16:20:59.222947Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T16:20:59.222947Z", + "reason": "registration_removed" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:51.354991Z", + "is_captain": true, + "specialization": null + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "answers": [ + { + "value": "Город", + "field_id": 293 + }, + { + "value": "Материалы qa01reg-20261025-c45626d0", + "field_id": 294 + }, + { + "value": 0, + "field_id": 295 + }, + { + "value": false, + "field_id": 296 + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": false, + "deadline": "2026-10-24T16:20:58.625346Z", + "submitted_at": "2026-10-25T16:20:53.016408Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "extra", + "method": "GET", + "path": "/programs/43/solutions/63/", + "status": 200, + "request": null, + "response": { + "id": 63, + "program_id": 43, + "version": 2, + "name": "Работа extra qa01reg-20261025-c45626d0", + "captain_id": 22, + "created_at": "2026-10-25T16:20:55.355057Z", + "updated_at": "2026-10-25T16:20:59.222947Z", + "participation": { + "status": "archived", + "archived_at": "2026-10-25T16:20:59.222947Z", + "reason": "registration_removed" + }, + "team": { + "members": [ + { + "role": null, + "user": { + "id": 22, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:55.362746Z", + "is_captain": true, + "specialization": null + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 1, + "max_size": 4, + "is_frozen": true + }, + "form": { + "fields": [ + { + "id": 293, + "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": 294, + "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": 295, + "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": 296, + "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 + } + } + ], + "revision": "123ee68a8411c1610004b187c7025f3755cc61a62e74654bfba428f6ce72c992", + "template": "case_championship", + "program_id": 43, + "is_snapshot": true, + "is_configured": true + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": false, + "deadline": "2026-10-24T16:20:58.625346Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false, + "can_leave": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "participation", + "code": "solution_archived", + "detail": "Решение снято с участия.", + "field_id": null + } + ] + } + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/solutions/", + "status": 200, + "request": null, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [] + }, + "cacheControl": null + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [127, 128, 129] + }, + "response": { + "registrations": [ + { + "registration_id": 127, + "user": { + "id": 23, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + }, + { + "registration_id": 128, + "user": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + } + }, + { + "registration_id": 129, + "user": { + "id": 15, + "first_name": "Роман", + "last_name": "Эксперт", + "avatar_url": null + } + } + ], + "effects": [], + "deleted_count": 3, + "archived_count": 0, + "removed_member_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 200, + "request": { + "registration_ids": [129, 128, 127], + "preview_token": "" + }, + "response": { + "audit_id": 4, + "deleted_count": 3, + "items": [] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/43/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [129, 128, 127], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/registrations/removals/4/", + "status": 200, + "request": null, + "response": { + "audit": { + "id": 4, + "program_id": 43, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:21:01.222833Z", + "registrations": [ + { + "user": { + "id": 23, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 127 + }, + { + "user": { + "id": 8, + "last_name": "Проверки", + "avatar_url": null, + "first_name": "Олег" + }, + "registration_id": 128 + }, + { + "user": { + "id": 15, + "last_name": "Эксперт", + "avatar_url": null, + "first_name": "Роман" + }, + "registration_id": 129 + } + ], + "deleted_count": 3 + }, + "changes": [] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/44/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [131] + }, + "response": { + "registrations": [ + { + "registration_id": 131, + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 44, + "solution_id": 61, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 4, + "is_submitted": false, + "effect": "remove_members", + "removed_members": [ + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:53.725029Z", + "is_captain": false + } + ], + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0 + } + ], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 1, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/44/management/registrations/remove/apply/", + "status": 200, + "request": { + "registration_ids": [131], + "preview_token": "" + }, + "response": { + "audit_id": 5, + "deleted_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 44, + "solution_id": 61, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 5, + "change_id": 13, + "action": "remove_registrations" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/44/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [131], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/44/management/registrations/removals/5/", + "status": 200, + "request": null, + "response": { + "audit": { + "id": 5, + "program_id": 44, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:21:02.207028Z", + "registrations": [ + { + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 131 + } + ], + "deleted_count": 1 + }, + "changes": [ + { + "id": 13, + "application": { + "kind": "solution", + "program_id": 44, + "solution_id": 61, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:21:02.222881Z", + "before_version": 4, + "after_version": 5, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "removed_members": [ + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:53.725029Z", + "is_captain": false, + "specialization": null + } + ], + "registration_removal_id": 5, + "revoked_invitation_count": 0 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/44/management/solutions/61/history/13/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 13, + "application": { + "kind": "solution", + "program_id": 44, + "solution_id": 61, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:21:02.222881Z", + "before_version": 4, + "after_version": 5, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "removed_members": [ + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:53.725029Z", + "is_captain": false, + "specialization": null + } + ], + "registration_removal_id": 5, + "revoked_invitation_count": 0 + }, + "before_snapshot": null, + "after_snapshot": null + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/45/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [134] + }, + "response": { + "registrations": [ + { + "registration_id": 134, + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + } + ], + "effects": [ + { + "application": { + "kind": "solution", + "program_id": 45, + "solution_id": 62, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 4, + "is_submitted": false, + "effect": "remove_members", + "removed_members": [ + { + "user": { + "id": 20, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": null, + "joined_at": "2026-10-25T16:20:54.696345Z", + "is_captain": false + } + ], + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0 + } + ], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 1, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/45/management/registrations/remove/apply/", + "status": 200, + "request": { + "registration_ids": [134], + "preview_token": "" + }, + "response": { + "audit_id": 6, + "deleted_count": 1, + "items": [ + { + "application": { + "kind": "solution", + "program_id": 45, + "solution_id": 62, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "solution_version": 5, + "change_id": 14, + "action": "remove_registrations" + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/45/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [134], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/45/management/registrations/removals/6/", + "status": 200, + "request": null, + "response": { + "audit": { + "id": 6, + "program_id": 45, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:21:03.227002Z", + "registrations": [ + { + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 134 + } + ], + "deleted_count": 1 + }, + "changes": [ + { + "id": 14, + "application": { + "kind": "solution", + "program_id": 45, + "solution_id": 62, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:21:03.242113Z", + "before_version": 4, + "after_version": 5, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "removed_members": [ + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:54.696345Z", + "is_captain": false, + "specialization": null + } + ], + "registration_removal_id": 6, + "revoked_invitation_count": 0 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/45/management/solutions/62/history/14/", + "status": 200, + "request": null, + "response": { + "change": { + "id": 14, + "application": { + "kind": "solution", + "program_id": 45, + "solution_id": 62, + "name": "Работа captain qa01reg-20261025-c45626d0" + }, + "action": "remove_registrations", + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:21:03.242113Z", + "before_version": 4, + "after_version": 5, + "removed_assignments_count": 0, + "score_count": 0, + "was_submitted": false, + "removed_members": [ + { + "role": "Аналитик", + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "joined_at": "2026-10-25T16:20:54.696345Z", + "is_captain": false, + "specialization": null + } + ], + "registration_removal_id": 6, + "revoked_invitation_count": 0 + }, + "before_snapshot": null, + "after_snapshot": null + }, + "cacheControl": "private, no-store" + }, + { + "actor": "member", + "method": "GET", + "path": "/programs/43/management/registrations/removals/", + "status": 403, + "request": null, + "response": { + "code": "permission_denied", + "detail": "Действие доступно организатору программы.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/44/management/registrations/removals/2/", + "status": 404, + "request": null, + "response": { + "code": "not_found", + "detail": "Решение или программа не найдены.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/registrations/removals/?offset=1&limit=2", + "status": 200, + "request": null, + "response": { + "count": 4, + "offset": 1, + "limit": 2, + "results": [ + { + "id": 3, + "program_id": 43, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:59.221440Z", + "registrations": [ + { + "user": { + "id": 19, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 123 + }, + { + "user": { + "id": 22, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 126 + } + ], + "deleted_count": 2 + }, + { + "id": 2, + "program_id": 43, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:20:57.967511Z", + "registrations": [ + { + "user": { + "id": 20, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 124 + } + ], + "deleted_count": 1 + } + ] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/43/management/registrations/removals/?all=true", + "status": 400, + "request": null, + "response": { + "code": "validation_error", + "detail": "Проверьте данные запроса.", + "errors": [ + { + "path": "all", + "code": "invalid", + "detail": "Неизвестное поле.", + "field_id": null + } + ], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/44/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [132] + }, + "response": { + "registrations": [ + { + "registration_id": 132, + "user": { + "id": 21, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + } + ], + "effects": [], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/44/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [132], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/44/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [132] + }, + "response": { + "registrations": [ + { + "registration_id": 132, + "user": { + "id": 21, + "first_name": "Program", + "last_name": "User", + "avatar_url": null + } + } + ], + "effects": [], + "deleted_count": 1, + "archived_count": 0, + "removed_member_count": 0, + "removed_assignments_count": 0, + "revoked_invitation_count": 0, + "score_count": 0, + "can_apply": true, + "errors": [], + "preview_token": "" + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/44/management/registrations/remove/apply/", + "status": 200, + "request": { + "registration_ids": [132], + "preview_token": "" + }, + "response": { + "audit_id": 7, + "deleted_count": 1, + "items": [] + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "POST", + "path": "/programs/44/management/registrations/remove/apply/", + "status": 409, + "request": { + "registration_ids": [132], + "preview_token": "" + }, + "response": { + "code": "solution_action_conflict", + "detail": "Данные решений изменились. Проверьте их и подтвердите действие заново.", + "errors": [], + "meta": {} + }, + "cacheControl": "private, no-store" + }, + { + "actor": "manager", + "method": "GET", + "path": "/programs/44/management/registrations/removals/7/", + "status": 200, + "request": null, + "response": { + "audit": { + "id": 7, + "program_id": 44, + "actor": { + "id": 8, + "first_name": "Олег", + "last_name": "Проверки", + "avatar_url": null + }, + "actor_name": "Олег Проверки", + "created_at": "2026-10-25T16:23:50.358524Z", + "registrations": [ + { + "user": { + "id": 21, + "last_name": "User", + "avatar_url": null, + "first_name": "Program" + }, + "registration_id": 132 + } + ], + "deleted_count": 1 + }, + "changes": [] + }, + "cacheControl": "private, no-store" + } +] diff --git a/docs/qa/program-solutions-registration-removal-20261025/migration-evidence.json b/docs/qa/program-solutions-registration-removal-20261025/migration-evidence.json new file mode 100644 index 000000000..b85527aef --- /dev/null +++ b/docs/qa/program-solutions-registration-removal-20261025/migration-evidence.json @@ -0,0 +1,38 @@ +{ + "status": "PASS", + "migration": "0038_solution_registration_removal_audit", + "database": "local-procollab_ci", + "oldModelsUnchanged": 23, + "hashBefore": "717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d", + "hashAfter": "717c9a664170764394fd1f9a256fca7c78fa42d929abe0e85a4557be1f684e3d", + "rowCounts": { + "partner_programs.PartnerProgram": 40, + "partner_programs.PartnerProgramField": 281, + "partner_programs.PartnerProgramUserProfile": 116, + "partner_programs.PartnerProgramProject": 3, + "partner_programs.PartnerProgramFieldValue": 2, + "partner_programs.ProgramSolution": 58, + "partner_programs.ProgramSolutionAnswer": 273, + "partner_programs.ProgramSolutionMember": 80, + "partner_programs.SolutionInvitation": 38, + "project_rates.SolutionScore": 34, + "project_rates.SolutionExpertAssignment": 20, + "project_rates.Criteria": 69, + "users.Expert": 7, + "files.UserFile": 8, + "projects.Project": 4, + "projects.Collaborator": 6, + "invites.Invite": 4, + "project_rates.ProjectScore": 3, + "project_rates.ProjectExpertAssignment": 1, + "industries.Industry": 1, + "users.Expert_programs": 27, + "partner_programs.PartnerProgram_managers": 44, + "partner_programs.SolutionAdministrativeChange": 8 + }, + "existingAdminEventsPreserved": 8, + "existingArchivedSolutionsPreserved": 4, + "existingRowsDeletedOrArchived": 0, + "existingProjectsMigratedOrDeleted": 0, + "onlyLocalSchema": true +} diff --git a/docs/qa/program-solutions-release-acceptance-20261026/README.md b/docs/qa/program-solutions-release-acceptance-20261026/README.md new file mode 100644 index 000000000..3d3f1169b --- /dev/null +++ b/docs/qa/program-solutions-release-acceptance-20261026/README.md @@ -0,0 +1,90 @@ + + +# Первый выпуск ProgramSolution: интеграция и приемка + +QA01: **25 обязательных AC PASS**, AC21 вынесен в отдельный BE10 по исходному плану. REL01: **локальная репетиция включения и отключения нового создания PASS**. Код первого выпуска готов к интеграции в dev; merge, развертывание и включение реальных программ в этот отчет не входят. + +Требования: [каноническое ТЗ](https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175), исходный неизменный блок26 AC `baeab2fb-c1e1-46c0-9e95-2f5c4b9bfc41`. Согласованные финальные поправки26.10 реализованы без нового design gate. Этот итог заменяет открытые статусы AC18/19 в датированных промежуточных отчетах, сохраняя их исторические evidence. + +## Итоговое поведение + +- ProgramSolution хранит заявку, материалы и команду внутри нее. Project для новой заявки не создается. «Решение» и «Команда» остаются вкладками одной заявки; действующие поиск участника, свободная роль и переходы приглашений сохраняются. +- Организатор получает реальные списки, сводки, кейсы, покрытие полей, назначения, результаты и Excel из API решений. Solution IDs никогда не передаются в Project API. Из существующего expert workspace используются только roster/revision, а не проектные счетчики. +- Результаты используют существующее представление, формулу и детализацию критериев. Для решений ссылка открывает решение этой программы. Нулевой балл и False остаются оценками, готовность задает сервер. Неконкурсная программа сохраняет материалы без требования сдачи и оценивания. +- Напоминание отправляется после серверного preview только по его токену. Изменение выборки, поиск и смена программы отменяют старые ответы; повторное нажатие и закрытие во время записи блокируются. +- Административная смена кейса, исключение участника, удаление регистрации и архив доступны по согласованным исключениям после сдачи. Материалы, оценки и цепочка before/after snapshots остаются в истории. Архив исключается из active/expert/public readers без физического удаления. +- Публичные решения находятся в самой программе, в прежнем месте кнопки «проекты-участники». После окончания публикуются сданные конкурсные и сохраненные неконкурсные работы при включенной публикации. Конкурсный черновик закрыт. Регистрационные ответы, оценки и pending invitations не попадают в публичный DTO. +- Семь разделов управления и старые Project branches сохранены. Прежние программы не переключаются автоматически; BE10 не запускался. + +## Source и API + +FE baseline: `cff3195052415d6af4ef8bfe9a2236af89da49b1`; observed dev: `08414b794dd5b34de5471dddaa19cd0af7f4a167`. BE runtime: `fdec3dcbd6693ddb487816114301d0df01798f93`; observed BE dev: `587a9155b9f53d9e50981a9939a7007c19fd6ecc`. [Source manifest](source-manifest.json) идентифицирует26 затронутых product/test файлов по raw/LF SHA256 и Git blob. Переносы legacy analytics template изменены formatter при добавлении mode branch; legacy expression/handlers сохранены. + +Контракт C00 **1.21.0 /156 schemas /59 operations /305 scenarios не менялся**. Новые UI consumers вызывают существующие `solution-analytics`, `solution-results`, `solution-evaluation`, signed registration/member/case/archive endpoints. Старые list/results/removal calls выполняются только в legacy mode. Авторитетные object permissions, revisions, deadlines, snapshots и eligibility остаются на backend. + +## Проверки + +**Checked by source:** mode routing, пути и типы ID, сохраненные legacy branches, cancellation/busy guards, ревизии и токены, отсутствие новых migration/model/API изменений. На новой таблице заявок нет колонки презентации; ссылка находится в native RowActions. Смена кейса остается одним экраном. Невыполненных BLOCKER/MAJOR по итоговому review нет. + +**Checked by tests:** `npm run test:ci` — **3003 tests /493 files PASS**, затем strict C00 typecheck PASS. После финального исправления восстановления назначения по stale revision —7 targeted tests PASS; полный3003 относится к предшествующему feature scope, точный prospective full suite фиксируется отдельно в PR. Три baseline исключения Vitest не входят в число493: `projects.component.spec.ts`, `program.component.spec.ts`, `editor-submit-button.directive.spec.ts`. Они не скрываются этим отчетом. Changed/new TS ESLint, новый SCSS Stylelint, Prettier и `git diff --check` PASS. Backend runtime не менялся; terminal current-head General CI40233747308 и PostgreSQL CI40233750723 success. Старые1812/1846 относятся к своим точным refs и не выдаются за counts текущего CI run. + +**Checked by build:** Angular development `npm run build:pr` PASS. В последнем UI review исправлен перенос заголовков таблицы; это только SCSS, tests не менялись. Проверка точного prospective merge source оформляется отдельно в PR, без суммирования counts с feature suite. + +**Checked in browser:** настоящий Angular → локальный Django8877/settings_ci → PostgreSQL55439, JWT только вымышленных пользователей. Owned headless Edge; неизменные dist bytes отдавались через Playwright route.fulfill для8884, URL dev API перенаправлялись только в localhost. Это не remote dev deployment и не слушающий HTTP static server. + +| Scope | Evidence | Результат | +| ---------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| Организатор → эксперт → результаты, КЧ и хакатон | [browser journey](browser-journey.json) | 24 checks,0 page errors; реальные manual/auto allocation apply, reminders и rating POST; score0/False, place1,2 настоящих XLSX | +| Публикация и административные исключения после сдачи | [browser administration](browser-administration.json) | 28 checks,0 page errors; anonymous allowlist, public draft404/200 по применимости, case/member/registration apply, история и сохраненный score | +| Архив | [browser archive](browser-archive.json) | 7 checks,0 page errors; из active/public исключен, архив и история доступны | +| Финальная проверка поверхностей | [browser final](browser-final.json) | 59 checks,0 page errors; все3 формата, legacy55, native menus, protected assignment, чистое закрытие без лишнего discard,320/390/768/1440 | +| Сверка БД, фильтров и rollout/rollback | [domain and REL01](domain-and-rel01.json) | 27 HTTP checks;26 прежних моделей: values unchanged; Project5→5 после setup; official creation command dry-run/apply/repeat | + +Скриншоты и исходные XLSX сохранены в локальных `design-artifacts/program-solutions-20261024/release-integration-browser-*`. В Git включены обезличенные receipts, а не credentials/private fixture. В wire receipt текстовые request поля могут содержать replacement characters из декодирования Playwright postDataJSON; серверный Unicode readback и материалы проверены отдельно, numeric/boolean/ID assertions от этого не зависят. + +## AC audit + +Для23 ранее закрытых AC использованы точные датированные MR/FORM/ACCESS/TEAM/PART/INV/NEG/DRAFT scopes из [предыдущего QA01](https://github.com/PROCOLLAB-github/api/blob/fdec3dcbd6693ddb487816114301d0df01798f93/docs/product/program-solutions-qa01.md) и связанных в нем отчетов. Новая интеграция не переписывает эти writers. Их регрессии проходят на текущем runtime/FE, а новые AC18/19 закрыты самостоятельными receipts выше. Предпосылки и ограничения старых concurrency/PDF evidence сохраняются. + +| AC | Исходное требование без изменения | Verdict | Evidence | +| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| AC01 | Создание решения новой программы не создает Project или самостоятельную Team | PASS | FORM/MR; свежая приемка: Project 5→5 после setup, 26 прежних моделей сохранены. | +| AC02 | Повторное создание капитаном открывает существующую заявку; конкурентные запросы не создают дубль | PASS | MR01–MR03; существующие concurrency tests и повторный POST при отключенном создании возвращает прежний ID. | +| AC03 | Черновик с одним капитаном сохраняется без кейса и обязательных при сдаче материалов | PASS | FORM02; реальные черновики73/75/77 сохранены, включая PATCH после выключения создания. | +| AC04 | Состав команды и приглашения доступны внутри заявки; отдельный выбор команды не требуется | PASS | TEAM/PART/INV; команда второй вкладкой в expert/manager browser, без самостоятельной Team. | +| AC05 | Принятие, отклонение, отзыв и изменение приглашений повторяют действующие права и переходы | PASS | TEAM/PART/INV/NEG; текущие регрессии приглашений и свежие create/accept через HTTP. | +| AC06 | Посторонний, обычный участник и эксперт не могут редактировать ответы или сдавать за капитана | PASS | ACCESS; свежие member PATCH/submit403 и outsider404, current backend CI. | +| AC07 | При максимуме команды учитываются ожидающие приглашения; при минимуме только принятые участники и капитан | PASS | TEAM/PART/INV/NEG; accepted/pending учитываются отдельно, итоговые accepted counters1/2/3. | +| AC08 | Два одновременных вступления в разные команды активной программы дают не более одного успеха | PASS | MR04–MR07 PostgreSQL, прежний неизменный writer и current CI. | +| AC09 | После сдачи нельзя изменить ответы или состав; отклонение и отзыв ожидающего приглашения доступны | PASS | TEAM/PART/INV/NEG; обычные операции заморожены. Согласованные административные исключения сохраняют before/after history. | +| AC10 | Кейс не выбирается автоматически; недопустимый или отсутствующий обязательный выбор блокирует сдачу | PASS | DRAFT/FORM; выбор и validation writer не менялись, current CI. | +| AC11 | Организатор настраивает поля, обязательность, порядок и ограничения; чужой организатор доступа не получает | PASS | FORM и tests настройки формы; C00 не менялся. | +| AC12 | Шаблоны КЧ и хакатона дают разные формы, а последующее изменение шаблона не меняет существующую программу | PASS | FORM; разные КЧ/hackathon формы, presentation отсутствует у hackathon. | +| AC13 | Пустое необязательное поле, число 0 и ответ false корректно различаются | PASS | DRAFT и fresh public/list/XLSX:0/false не пустые. | +| AC14 | Сдача неполной заявки или после дедлайна не меняет статус; ошибки указывают причину | PASS | DRAFT; submission/permissions/deadlines regression tests. | +| AC15 | Одновременная сдача и изменение состава или ответов не меняют уже сданные данные | PASS | DRAFT PostgreSQL races; тот же BE writer, current CI. | +| AC16 | Устаревшая форма обновляется без потери локального ввода; новое обязательное поле требуется только несданным работам | PASS | DRAFT stale schema UI tests; компонент ввода не менялся. | +| AC17 | Смена подписей и порядка в настройках не меняет представление ранее сданного решения | PASS | DRAFT snapshot/XLSX evidence; полная цепь before/after snapshots сохранена. | +| AC18 | Эксперт оценивает нужное решение; критерии, назначения, расчет и публикация результатов сохраняют текущие правила | PASS | Новые browser-journey/admin + domain-and-rel01: manual/auto assignment→reminder→expert score0/False→results/place1→XLSX→publication; protected scores retained. | +| AC19 | Аналитика и выгрузки считают заявки, принятый состав и применимость материалов без дублей Project | PASS | Новые browser/domain: active1/2/2, accepted1/2/3, public0/1/2; filtered list/XLSX IDs []/[74]/[76]; нет Project дублей. | +| AC20 | Несколько программ одного пользователя не смешивают ответы, приглашения, права и оценки | PASS | ACCESS/MR и новые52/53/54: один пользователь, разные решения, оценки и program-scoped URL. | +| AC21 | Перенос сохраняет связанные данные и ссылки; повторный запуск не создает дубли | BE10 отдельно | Отдельная поставка BE10 после первого выпуска; перенос не запускался. | +| AC22 | Обычные проекты и старые программы без переноса сохраняют прежнее поведение | PASS | Реальная legacy программа55 и неизменность прежних26 моделей; старые ветки API/UI сохранены. | +| AC23 | Неконкурсная программа сохраняется без искусственного требования сдачи | PASS | Новая программа54: draft без submit, publication сохраненных работ; UI явно сообщает отсутствие оценивания. | +| AC24 | Форму, приглашения и сдачу можно завершить на мобильном экране | PASS | TEAM/PART mobile evidence; новые поверхности320/390/768/1440 без document overflow. | +| AC25 | Заявка имеет вкладки «Решение» и «Команда»; переходы сохраняют ввод и выбранный кейс, сдача проверяет обе вкладки и открывает вкладку с ошибкой | PASS | TEAM/PART/DRAFT; actual экспертная/организаторская команда второй вкладкой, participant редактор сохранен. | +| AC26 | «Пригласить участника» открывает модальный поиск по имени/фамилии в программе; недоступные кандидаты не выбираются, выбранный ID и роль 1–128 символов обязательны, подсказки допускают свободный ввод; pending блокирует повтор, ошибка сохраняет выбор и роль, успех добавляет только ожидающее приглашение; изменение роли и отзыв доступны по текущим правам | PASS | INV/NEG/TEAM; прежний modal search/role writer и tests неизменны, current full suite PASS. | + +## REL01 и сохранность + +На свежих synthetic программах52/53/54 сначала проверено выключенное создание409, затем включение и полный путь. После приемки official `set_solution_creation` проверен dry-run → apply → repeat: changed false/true/false. Новое создание409, повторный вход в существующую заявку200, PATCH прежнего черновика200. Все три creation flags оставлены false. Реальные программы и их flags не менялись. + +После административных изменений active counts1/2/2, уникальный принятый состав1/2/3, public0/1/2. Filtered list и XLSX совпали по ID: []/[74]/[76]. Отсутствующее purpose presentation в хакатоне не считается пропущенным материалом. Все4 записи оценки каждого конкурсного решения и полная цепочка snapshots сохранены. Позднее выключение публикации/смена даты не отзывают установленный marker. + +Прежние26 моделей перечитаны по сохраненным ID: исходный и итоговый SHA256 совпали. Fixture setup отдельно создал один legacy Project; после setup ни создание/сдача решений, ни публикация/архив не добавляли Project. Legacy55 остался legacy_project. + +Первая версия finish helper остановилась после корректного отключения первого flag из-за ошибочного ожидания top-level `status` вместо C00 `submission.status`. Частичное состояние проверено read-only; отдельная recovery phase использовала guard конкретных flags/IDs, свежую папку evidence и завершила проверки. Прежний failed run не объявляется PASS. Аналогично первоначальный duplicate-call test и Sass selector failure исправлены до итогового3003/build/browser PASS. + +## Граница готовности + +C00/D00, BE01–BE09, FE01–FE08, QA01 и локальная REL01 репетиция закрыты для первого выпуска новых программ. **BE10/AC21 — отдельный перенос по утвержденному списку**. Merge/deploy/включение реальных программ выполняются отдельно человеком; production и CDN file bytes не проверялись. После разрешенного rollout порядок остается: совместимый полный graph до0039 → backend/workers C001.21 → frontend → отдельная новая программа → явное включение создания. Schema-down не используется. diff --git a/docs/qa/program-solutions-release-acceptance-20261026/browser-administration.json b/docs/qa/program-solutions-release-acceptance-20261026/browser-administration.json new file mode 100644 index 000000000..33d1b0b0b --- /dev/null +++ b/docs/qa/program-solutions-release-acceptance-20261026/browser-administration.json @@ -0,0 +1,368 @@ +{ + "checks": [ + "public-case_championship-competitive:320", + "public-case_championship-competitive:390", + "public-case_championship-competitive:768", + "public-case_championship-competitive:1440", + "published-allowlist-and-applicability-case_championship-competitive", + "public-hackathon-competitive:320", + "public-hackathon-competitive:390", + "public-hackathon-competitive:768", + "public-hackathon-competitive:1440", + "published-allowlist-and-applicability-hackathon-competitive", + "public-hackathon-noncompetitive:320", + "public-hackathon-noncompetitive:390", + "public-hackathon-noncompetitive:768", + "public-hackathon-noncompetitive:1440", + "published-allowlist-and-applicability-hackathon-noncompetitive", + "case-admin:320", + "case-admin:390", + "case-admin:768", + "case-admin:1440", + "actual-admin-case-after-submit", + "actual-admin-member-after-submit", + "admin-preserves-score-and-updates-current-readers", + "registration-removal-admin:320", + "registration-removal-admin:390", + "registration-removal-admin:768", + "registration-removal-admin:1440", + "actual-registration-removal-after-submit", + "registration-removal-retained-history" + ], + "wire": [ + { + "role": "anonymous", + "method": "GET", + "path": "/programs/52/", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/52/published-solutions/?search=&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/53/", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/53/published-solutions/?search=&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/54/", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/54/published-solutions/?search=&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?submission=all&case_scope=all&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-form/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/solutions/72/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/52/management/solutions/72/case/preview/", + "status": 200, + "request": { + "case_name": "Образование" + } + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/52/management/solutions/72/case/apply/", + "status": 200, + "request": { + "case_name": "Образование", + "preview_token": "[redacted]" + } + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?submission=all&case_scope=all&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/solutions/72/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/solutions/72/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/52/management/solutions/72/members/remove/preview/", + "status": 200, + "request": { + "user_id": 40 + } + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/52/management/solutions/72/members/remove/apply/", + "status": 200, + "request": { + "user_id": 40, + "preview_token": "[redacted]" + } + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?submission=all&case_scope=all&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/participants/?search=qa01release-20261026-9c3c54b4-member-3@example.com&limit=20&offset=0®ion_scope=all&activity=all&order=activity_desc", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/53/management/registrations/remove/preview/", + "status": 200, + "request": { + "registration_ids": [159] + } + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/53/management/registrations/remove/apply/", + "status": 200, + "request": { + "registration_ids": [159], + "preview_token": "[redacted]" + } + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/participants/?search=qa01release-20261026-9c3c54b4-member-3@example.com&limit=20&offset=0®ion_scope=all&activity=all&order=activity_desc", + "status": 200, + "request": null + } + ], + "pageErrors": [], + "deniedExternal": [ + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif" + ], + "completed": true +} diff --git a/docs/qa/program-solutions-release-acceptance-20261026/browser-archive.json b/docs/qa/program-solutions-release-acceptance-20261026/browser-archive.json new file mode 100644 index 000000000..6344877b7 --- /dev/null +++ b/docs/qa/program-solutions-release-acceptance-20261026/browser-archive.json @@ -0,0 +1,238 @@ +{ + "checks": [ + "archived-solution-excluded-from-active", + "archived-solution-excluded-from-public", + "archived-list:320", + "archived-list:390", + "archived-list:768", + "archived-list:1440", + "archived-history-remains-readable" + ], + "wire": [ + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-form/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?submission=all&case_scope=all&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/solutions/72/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/52/management/solutions/archive/preview/", + "status": 200, + "request": { + "ids": [72] + } + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/52/management/solutions/archive/apply/", + "status": 200, + "request": { + "ids": [72], + "preview_token": "[redacted]" + } + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?submission=all&case_scope=all&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/solutions/?participation=archived&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-form/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/solutions/72/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solutions/72/history/?limit=100", + "status": 200, + "request": null + } + ], + "pageErrors": [], + "deniedExternal": [ + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif" + ], + "completed": true +} diff --git a/docs/qa/program-solutions-release-acceptance-20261026/browser-final.json b/docs/qa/program-solutions-release-acceptance-20261026/browser-final.json new file mode 100644 index 000000000..5f1bf9a88 --- /dev/null +++ b/docs/qa/program-solutions-release-acceptance-20261026/browser-final.json @@ -0,0 +1,966 @@ +{ + "checks": [ + "final-analytics-case_championship-competitive:320", + "final-analytics-case_championship-competitive:390", + "final-analytics-case_championship-competitive:768", + "final-analytics-case_championship-competitive:1440", + "native-export-menu-case_championship-competitive", + "final-experts-case_championship-competitive:320", + "final-experts-case_championship-competitive:390", + "final-experts-case_championship-competitive:768", + "final-experts-case_championship-competitive:1440", + "final-results-case_championship-competitive:320", + "final-results-case_championship-competitive:390", + "final-results-case_championship-competitive:768", + "final-results-case_championship-competitive:1440", + "final-results-case_championship-competitive", + "final-analytics-hackathon-competitive:320", + "final-analytics-hackathon-competitive:390", + "final-analytics-hackathon-competitive:768", + "final-analytics-hackathon-competitive:1440", + "native-export-menu-hackathon-competitive", + "final-experts-hackathon-competitive:320", + "final-experts-hackathon-competitive:390", + "final-experts-hackathon-competitive:768", + "final-experts-hackathon-competitive:1440", + "protected-assignment:320", + "protected-assignment:390", + "protected-assignment:768", + "protected-assignment:1440", + "protected-assignment-and-clean-close", + "final-results-hackathon-competitive:320", + "final-results-hackathon-competitive:390", + "final-results-hackathon-competitive:768", + "final-results-hackathon-competitive:1440", + "final-results-hackathon-competitive", + "final-analytics-hackathon-noncompetitive:320", + "final-analytics-hackathon-noncompetitive:390", + "final-analytics-hackathon-noncompetitive:768", + "final-analytics-hackathon-noncompetitive:1440", + "native-export-menu-hackathon-noncompetitive", + "final-experts-hackathon-noncompetitive:320", + "final-experts-hackathon-noncompetitive:390", + "final-experts-hackathon-noncompetitive:768", + "final-experts-hackathon-noncompetitive:1440", + "final-results-hackathon-noncompetitive:320", + "final-results-hackathon-noncompetitive:390", + "final-results-hackathon-noncompetitive:768", + "final-results-hackathon-noncompetitive:1440", + "final-results-hackathon-noncompetitive", + "final-legacy-project-mode", + "final-public-marker-and-archive-case_championship-competitive", + "final-public-hackathon-competitive:320", + "final-public-hackathon-competitive:390", + "final-public-hackathon-competitive:768", + "final-public-hackathon-competitive:1440", + "final-public-marker-and-archive-hackathon-competitive", + "final-public-hackathon-noncompetitive:320", + "final-public-hackathon-noncompetitive:390", + "final-public-hackathon-noncompetitive:768", + "final-public-hackathon-noncompetitive:1440", + "final-public-marker-and-archive-hackathon-noncompetitive" + ], + "wire": [ + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?submission=submitted&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-results/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/solutions/?submission=submitted&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-evaluation/experts/13/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-results/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/54/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/54/management/solution-analytics/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/54/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/54/management/solution-analytics/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/54/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/54/management/solution-analytics/solutions/?submission=submitted&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/54/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/54/management/solution-results/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/55/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/55/management/projects/?search=&limit=20&offset=0&submission=all&case_scope=all&evaluation=all", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/53/", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/53/published-solutions/?search=&offset=0&limit=20", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/54/", + "status": 200, + "request": null + }, + { + "role": "anonymous", + "method": "GET", + "path": "/programs/54/published-solutions/?search=&offset=0&limit=20", + "status": 200, + "request": null + } + ], + "pageErrors": [], + "deniedExternal": [ + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif" + ], + "completed": true, + "readOnlyDomain": true +} diff --git a/docs/qa/program-solutions-release-acceptance-20261026/browser-journey.json b/docs/qa/program-solutions-release-acceptance-20261026/browser-journey.json new file mode 100644 index 000000000..0e18e45cc --- /dev/null +++ b/docs/qa/program-solutions-release-acceptance-20261026/browser-journey.json @@ -0,0 +1,913 @@ +{ + "checks": [ + "actual-manager-assignment-case_championship-competitive", + "actual-expert-reminder-case_championship-competitive", + "actual-expert-score-case_championship-competitive", + "expert-case_championship-competitive:320", + "expert-case_championship-competitive:390", + "expert-case_championship-competitive:768", + "expert-case_championship-competitive:1440", + "results-case_championship-competitive:320", + "results-case_championship-competitive:390", + "results-case_championship-competitive:768", + "results-case_championship-competitive:1440", + "expert-to-organizer-results-case_championship-competitive", + "actual-manager-assignment-hackathon-competitive", + "actual-expert-reminder-hackathon-competitive", + "actual-expert-score-hackathon-competitive", + "expert-hackathon-competitive:320", + "expert-hackathon-competitive:390", + "expert-hackathon-competitive:768", + "expert-hackathon-competitive:1440", + "results-hackathon-competitive:320", + "results-hackathon-competitive:390", + "results-hackathon-competitive:768", + "results-hackathon-competitive:1440", + "expert-to-organizer-results-hackathon-competitive" + ], + "wire": [ + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?submission=submitted&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-evaluation/experts/13/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "PUT", + "path": "/programs/52/management/solution-evaluation/experts/13/assignments/", + "status": 200, + "request": { + "revision": "f6cbd03c36307a3351a5755edc649c9ea937ac28bd58829dd9d5d7f444a5e13a", + "solution_ids": [72] + } + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?submission=submitted&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-analytics/solutions/?scope=awaiting_evaluation&search=&limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/52/management/solution-analytics/attention/solutions-awaiting-evaluation/reminder-preview/", + "status": 200, + "request": { + "search": "", + "selection": { + "mode": "all", + "ids": [], + "excluded_ids": [] + } + } + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/52/management/solution-analytics/attention/solutions-awaiting-evaluation/reminders/", + "status": 200, + "request": { + "token": "[redacted]" + } + }, + { + "role": "expert", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/52/solutions/72/evaluation/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "POST", + "path": "/auth/users/current/acknowledge-verification-notice/", + "status": 200, + "request": {} + }, + { + "role": "expert", + "method": "POST", + "path": "/programs/52/solutions/72/evaluation/", + "status": 200, + "request": { + "expected_criteria_revision": "4d1aadf56089f600861ed74e0fabc7be271e115cea7282c4cbf21ee1385fa658", + "scores": [ + { + "criterion_id": 102, + "value": "Проверка решения 52" + }, + { + "criterion_id": 103, + "value": "0" + }, + { + "criterion_id": 104, + "value": "False" + }, + { + "criterion_id": 105, + "value": "" + } + ] + } + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-results/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/52/management/solution-results/export/?search=&case_scope=all&order=desc&scope=current&status=all", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/solutions/?submission=submitted&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/53/management/solution-evaluation/allocation/", + "status": 200, + "request": { + "revision": "3d1efde1520983e9f29685656fdb662b92fcc466d299865e9849e6abf26176b6", + "expert_ids": [13], + "solution_ids": [74] + } + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/53/management/solution-evaluation/allocation/apply/", + "status": 200, + "request": { + "plan_token": "[redacted]" + } + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/assignments/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/experts/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/solutions/?submission=submitted&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-analytics/solutions/?scope=awaiting_evaluation&search=&limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/53/management/solution-analytics/attention/solutions-awaiting-evaluation/reminder-preview/", + "status": 200, + "request": { + "search": "", + "selection": { + "mode": "all", + "ids": [], + "excluded_ids": [] + } + } + }, + { + "role": "manager", + "method": "POST", + "path": "/programs/53/management/solution-analytics/attention/solutions-awaiting-evaluation/reminders/", + "status": 200, + "request": { + "token": "[redacted]" + } + }, + { + "role": "expert", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "GET", + "path": "/programs/53/solutions/74/evaluation/", + "status": 200, + "request": null + }, + { + "role": "expert", + "method": "POST", + "path": "/programs/53/solutions/74/evaluation/", + "status": 200, + "request": { + "expected_criteria_revision": "1a24ae348ae3c7146e40edeefceda0d05a98b29dbeb192499eda9586d3914ad1", + "scores": [ + { + "criterion_id": 106, + "value": "Проверка решения 53" + }, + { + "criterion_id": 107, + "value": "0" + }, + { + "criterion_id": 108, + "value": "False" + }, + { + "criterion_id": 109, + "value": "" + } + ] + } + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/types/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/roles/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/auth/users/current/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/invites/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/chats/has-unreads/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/core/notifications/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/industries/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/solution-invitations/?status=pending&limit=100&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/?limit=20&offset=0", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-results/", + "status": 200, + "request": null + }, + { + "role": "manager", + "method": "GET", + "path": "/programs/53/management/solution-results/export/?search=&case_scope=all&order=desc&scope=current&status=all", + "status": 200, + "request": null + } + ], + "pageErrors": [], + "deniedExternal": [ + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://fonts.googleapis.com/icon", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif", + "https://hwchamber.co.uk/wp-content/uploads/2022/04/avatar-placeholder.gif" + ], + "completed": true +} diff --git a/docs/qa/program-solutions-release-acceptance-20261026/domain-and-rel01.json b/docs/qa/program-solutions-release-acceptance-20261026/domain-and-rel01.json new file mode 100644 index 000000000..8e07a83b2 --- /dev/null +++ b/docs/qa/program-solutions-release-acceptance-20261026/domain-and-rel01.json @@ -0,0 +1,169 @@ +{ + "completed": true, + "programs": [ + { + "programId": 52, + "activeSolutions": 1, + "acceptedInTeams": 1, + "publicSolutions": 0, + "scoresRetained": 4, + "history": ["change_case", "remove_member", "archive"], + "originalSnapshotRetained": true + }, + { + "programId": 53, + "activeSolutions": 2, + "acceptedInTeams": 2, + "publicSolutions": 1, + "scoresRetained": 4, + "history": ["remove_registrations"], + "originalSnapshotRetained": true + }, + { + "programId": 54, + "activeSolutions": 2, + "acceptedInTeams": 3, + "publicSolutions": 2, + "scoresRetained": 0, + "history": [], + "originalSnapshotRetained": true + } + ], + "creationOnlyRollback": [ + { + "programId": 52, + "receipts": [ + { + "program_id": 52, + "storage_mode": "solution", + "previous_enabled": true, + "requested_enabled": false, + "applied": false, + "changed": false + }, + { + "program_id": 52, + "storage_mode": "solution", + "previous_enabled": true, + "requested_enabled": false, + "applied": true, + "changed": true + }, + { + "program_id": 52, + "storage_mode": "solution", + "previous_enabled": false, + "requested_enabled": false, + "applied": true, + "changed": false + } + ], + "newCreationStatus": 409, + "existingDraftReadableAndWritable": true, + "existingApplicationReopened": false + }, + { + "programId": 53, + "receipts": [ + { + "program_id": 53, + "storage_mode": "solution", + "previous_enabled": true, + "requested_enabled": false, + "applied": false, + "changed": false + }, + { + "program_id": 53, + "storage_mode": "solution", + "previous_enabled": true, + "requested_enabled": false, + "applied": true, + "changed": true + }, + { + "program_id": 53, + "storage_mode": "solution", + "previous_enabled": false, + "requested_enabled": false, + "applied": true, + "changed": false + } + ], + "newCreationStatus": 409, + "existingDraftReadableAndWritable": true, + "existingApplicationReopened": true + }, + { + "programId": 54, + "receipts": [ + { + "program_id": 54, + "storage_mode": "solution", + "previous_enabled": true, + "requested_enabled": false, + "applied": false, + "changed": false + }, + { + "program_id": 54, + "storage_mode": "solution", + "previous_enabled": true, + "requested_enabled": false, + "applied": true, + "changed": true + }, + { + "program_id": 54, + "storage_mode": "solution", + "previous_enabled": false, + "requested_enabled": false, + "applied": true, + "changed": false + } + ], + "newCreationStatus": 409, + "existingDraftReadableAndWritable": true, + "existingApplicationReopened": true + } + ], + "exports": [ + { + "programId": 52, + "count": 0, + "listIds": [], + "xlsxIds": [], + "sha256": "1d74bb7684e9d8aaed2c0f9eb68ed25eaacefbbcbd340782485fc452ae426228", + "sheets": ["Решения", "Команды", "Поля формы", "Ответы решения"] + }, + { + "programId": 53, + "count": 1, + "listIds": [74], + "xlsxIds": [74], + "sha256": "63e075dffeb5f407976fc3a949b2974a3f76e7f3b92c44a28d9c2b5b01e25ebc", + "sheets": ["Решения", "Команды", "Поля формы", "Ответы решения"] + }, + { + "programId": 54, + "count": 1, + "listIds": [76], + "xlsxIds": [76], + "sha256": "7420ddab38bda539ac44a9c7afcad8b59b619d8dc368279f686ea4ef01bbaead", + "sheets": ["Решения", "Команды", "Поля формы", "Ответы решения"] + } + ], + "oldModelsUnchanged": 26, + "baselineHash": "25307a71850fa695332d83047ec32d94954ffe310d9d2562f97fa07ac3443030", + "finalHash": "25307a71850fa695332d83047ec32d94954ffe310d9d2562f97fa07ac3443030", + "projectCount": 5, + "newProjectsDuringJourney": 0, + "legacyModeUnchanged": true, + "publicMarkersSurviveLaterOptOut": true, + "httpChecks": 27, + "notChecked": [ + "production deployment", + "BE10 migration of old programs", + "CDN file bytes; fixture metadata only" + ] +} diff --git a/docs/qa/program-solutions-release-acceptance-20261026/source-manifest.json b/docs/qa/program-solutions-release-acceptance-20261026/source-manifest.json new file mode 100644 index 000000000..1e8f76da3 --- /dev/null +++ b/docs/qa/program-solutions-release-acceptance-20261026/source-manifest.json @@ -0,0 +1,198 @@ +{ + "baseHead": "cff3195052415d6af4ef8bfe9a2236af89da49b1", + "observedDev": "08414b794dd5b34de5471dddaa19cd0af7f4a167", + "backend": "fdec3dcbd6693ddb487816114301d0df01798f93", + "contractVersion": "1.21.0", + "tests": 3003, + "testFiles": 493, + "browser": { + "journey": { + "artifact": "release-integration-browser-journey-2", + "checks": 24, + "pageErrors": 0, + "requests": 112, + "sha256": "6638ff96a39edcea3dda220a9f8de96b1e3884d13f4354f23360c71953a63299" + }, + "administration": { + "artifact": "release-integration-browser-admin-1", + "checks": 28, + "pageErrors": 0, + "requests": 42, + "sha256": "55a2318520f115a7b8a07fafc31f77d9a974ed47a19a46e12dae00fce8cb3d17" + }, + "archive": { + "artifact": "release-integration-browser-archive-1", + "checks": 7, + "pageErrors": 0, + "requests": 30, + "sha256": "d4d6aa33c8d2e375c95849c249001edd5a121875d2039f3225538d28416367d9" + }, + "final": { + "artifact": "release-integration-browser-final-2", + "checks": 59, + "pageErrors": 0, + "requests": 121, + "sha256": "1c64e71ac14a59e9a411d777696917e0b0e1172af0b6d4d5439a7dec9fc14906" + } + }, + "files": [ + { + "path": "projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.spec.ts", + "sha256Raw": "b4a91c59946fa0f0d5ae6b154db07e3098aaa659c64c7ef0187ef0f46981269f", + "sha256LF": "b4a91c59946fa0f0d5ae6b154db07e3098aaa659c64c7ef0187ef0f46981269f", + "gitBlob": "9711dbe9a6cd8eac071871158e18b176f3341450" + }, + { + "path": "projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.ts", + "sha256Raw": "e7b72fb8d80f42435bcbf5f5f25df5ace4c308306dc8ce1e6ff74ec68aaa594c", + "sha256LF": "e7b72fb8d80f42435bcbf5f5f25df5ace4c308306dc8ce1e6ff74ec68aaa594c", + "gitBlob": "67304e75dac499eae6817d79abe45bbc188e1f08" + }, + { + "path": "projects/social_platform/src/app/domain/program/program-results.model.ts", + "sha256Raw": "9bd52e1946030593eada90fc1504218ec7d4a93fb286bbc05527cc7dbcef34ee", + "sha256LF": "9bd52e1946030593eada90fc1504218ec7d4a93fb286bbc05527cc7dbcef34ee", + "gitBlob": "7ec528248c41ed2ad7929fb466f359818132cb64" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/activity-chart/activity-chart.component.html", + "sha256Raw": "aab714940e8ffd1040a03c95fe938a5ed6be78320be55b39ee4bfd0db54c5339", + "sha256LF": "aab714940e8ffd1040a03c95fe938a5ed6be78320be55b39ee4bfd0db54c5339", + "gitBlob": "501702773a0372c980e4b95bc6cc2d65fbdb7a1e" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/activity-chart/activity-chart.component.spec.ts", + "sha256Raw": "a32c501465543fd2557f8daaa6dec7a05189fc89f1a8670aa6326214277403d7", + "sha256LF": "a32c501465543fd2557f8daaa6dec7a05189fc89f1a8670aa6326214277403d7", + "gitBlob": "2f1ac17b5e946cec7a7098080f28faa1952ec91d" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/activity-chart/activity-chart.component.ts", + "sha256Raw": "b37ff0bd4e59a716f2e03ca2952414b39b30818eb59d578c0b5ad6baf0ce56b1", + "sha256LF": "b37ff0bd4e59a716f2e03ca2952414b39b30818eb59d578c0b5ad6baf0ce56b1", + "gitBlob": "b7da6e9b357539a40b584625cbff7b2cc06b70db" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.html", + "sha256Raw": "0dfa1df3cf370388c64a4621f352419d84eb8b748ffc2f9ae8300276b0f926a4", + "sha256LF": "0dfa1df3cf370388c64a4621f352419d84eb8b748ffc2f9ae8300276b0f926a4", + "gitBlob": "cb2dd017f15e18987f2b7336e7157077cd22f0ad" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.spec.ts", + "sha256Raw": "2f0da103d29335da33d252cef68f1c148ca8c305336a246cfd01fdc26da3a306", + "sha256LF": "2f0da103d29335da33d252cef68f1c148ca8c305336a246cfd01fdc26da3a306", + "gitBlob": "04200d6126fea088656ff34ae1eb89c98a6479f9" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/analytics.component.ts", + "sha256Raw": "4d9d891a2506d46d3c4c905fa2682589368ebca896612f0225671eed6b4c8b19", + "sha256LF": "4d9d891a2506d46d3c4c905fa2682589368ebca896612f0225671eed6b4c8b19", + "gitBlob": "2f0448093eee7ac798e8fd82ebc0c059b7b277be" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.html", + "sha256Raw": "e4f48299e9936068045db4ef2b4e222a38828258fef122ee684486495900b038", + "sha256LF": "e4f48299e9936068045db4ef2b4e222a38828258fef122ee684486495900b038", + "gitBlob": "c0ccc1f3da51ca68d44811f86dc17e42de57651b" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.scss", + "sha256Raw": "71f96044ab88493d7eb4bb5003ebd2412c48a5bf5eed13d634038a152c8786b9", + "sha256LF": "71f96044ab88493d7eb4bb5003ebd2412c48a5bf5eed13d634038a152c8786b9", + "gitBlob": "0b077ef50890d47a7861cd4f756da126657ca931" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.spec.ts", + "sha256Raw": "754bff2a6634d1f2c3fb04b9c641c3155dff96798b36bd97e03e6b7ef0978e30", + "sha256LF": "754bff2a6634d1f2c3fb04b9c641c3155dff96798b36bd97e03e6b7ef0978e30", + "gitBlob": "e7ac715903688db3b85c82f1b7e10e0f9d87ec7b" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/detail/analytics/solution-analytics.component.ts", + "sha256Raw": "da4f98ea82c38c26f5743cd66c089a3a56b9080fb3d7e79812d7e114ace9faa0", + "sha256LF": "da4f98ea82c38c26f5743cd66c089a3a56b9080fb3d7e79812d7e114ace9faa0", + "gitBlob": "818775d4ce8657c0c1ed6cc6dd7c26786a042d15" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.html", + "sha256Raw": "4a5a22994324fdbb4ef8c27c0b8302a2aaf202c277604b57ad613c04705bb33e", + "sha256LF": "4a5a22994324fdbb4ef8c27c0b8302a2aaf202c277604b57ad613c04705bb33e", + "gitBlob": "9ddc821fc059928adb5970b934a9cb2b7dafcd73" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.spec.ts", + "sha256Raw": "cb1cf94ce82d179050a2dd3605fcafd9b26e8e4eb20e3d453e5d5f62badbfa87", + "sha256LF": "cb1cf94ce82d179050a2dd3605fcafd9b26e8e4eb20e3d453e5d5f62badbfa87", + "gitBlob": "be93d0c46a39f760dbf647d99ec349921793b1d1" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/experts/solution-experts.component.ts", + "sha256Raw": "dff447535f62f350f352978b15db7fa60add4569e29ce58b0f3c4668eb2d668a", + "sha256LF": "dff447535f62f350f352978b15db7fa60add4569e29ce58b0f3c4668eb2d668a", + "gitBlob": "0364194919d80cfebfa884dd6e0e5dc784e19918" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/management.component.html", + "sha256Raw": "cb8cc257cf04b57606243b24eea62de49235e503d458d5d8af1b80bfbf34de01", + "sha256LF": "cb8cc257cf04b57606243b24eea62de49235e503d458d5d8af1b80bfbf34de01", + "gitBlob": "dddd51fae6969820dec906f47b6af77e71ee2d94" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/management.component.ts", + "sha256Raw": "596073be98b943b86d02bff734e50fff2a11650e726311994949d978d25bae21", + "sha256LF": "596073be98b943b86d02bff734e50fff2a11650e726311994949d978d25bae21", + "gitBlob": "38d9d422a4bcffaf5d4e291e60f4eaab6d7a982a" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.html", + "sha256Raw": "d7dbabf06adca08bae6fb1630ad0ecbd9faa22aad68556d030b9a0e7ea66e8df", + "sha256LF": "d7dbabf06adca08bae6fb1630ad0ecbd9faa22aad68556d030b9a0e7ea66e8df", + "gitBlob": "59fea219d242aad376e83b1f0b76a8d6e2538a4a" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/results/result-project-scores.component.ts", + "sha256Raw": "dfca69d690e358d3536e3bdd97e3622fcdfc0c9e3c8b5ca34948bf7d24ec8558", + "sha256LF": "dfca69d690e358d3536e3bdd97e3622fcdfc0c9e3c8b5ca34948bf7d24ec8558", + "gitBlob": "d12aefa000f2e0fad5e7d6ccd2979e04cb627b80" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/results/results.component.html", + "sha256Raw": "6712bb6c0811d6601c656d629286df3e9a9cc1a8930e9975672b6de26e90ddea", + "sha256LF": "6712bb6c0811d6601c656d629286df3e9a9cc1a8930e9975672b6de26e90ddea", + "gitBlob": "1f19902c8d6383bd4ebaa170625b9f8f85655373" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/results/results.component.ts", + "sha256Raw": "b6c77fe3138c97591cae0f88ce98323cb7e4a6c48b7ec4f8896539c086608465", + "sha256LF": "b6c77fe3138c97591cae0f88ce98323cb7e4a6c48b7ec4f8896539c086608465", + "gitBlob": "75c69a6374be37ca6038a5dc69b77eb240b604a1" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/results/solution-results.component.spec.ts", + "sha256Raw": "ecda712ddda8bb8edb011235a7fcac12cf753331a61c50362e0ab8e0a9342a98", + "sha256LF": "ecda712ddda8bb8edb011235a7fcac12cf753331a61c50362e0ab8e0a9342a98", + "gitBlob": "e6b834f07eb6a59ae90737270c99ee6f644d60db" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.ts", + "sha256Raw": "ba9cbd1473e6b2a2858234f6666aeb09de2c8ef6a13b554b99f21e92bda3cd9f", + "sha256LF": "ba9cbd1473e6b2a2858234f6666aeb09de2c8ef6a13b554b99f21e92bda3cd9f", + "gitBlob": "dfe85e392048d942fddd6846bcf913db7635983f" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-registration-removal.component.spec.ts", + "sha256Raw": "8ab5535a33fbd1f7c12031b44b3b9b8642e4b1b20d0f585656271dc2fb8ea182", + "sha256LF": "8ab5535a33fbd1f7c12031b44b3b9b8642e4b1b20d0f585656271dc2fb8ea182", + "gitBlob": "0ea1236c2955024871e76b6f3dd07c20461475ba" + }, + { + "path": "projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-registration-removal.component.ts", + "sha256Raw": "fe8823b49668c89d616e77bc592ced78764422a12142d6f754ab150476c75763", + "sha256LF": "fe8823b49668c89d616e77bc592ced78764422a12142d6f754ab150476c75763", + "gitBlob": "58514f66bdfd5585d08395e1106af279130c6995" + } + ], + "testScope": "Feature3003/493 before final stale-assignment retry fix; post-review7 targeted tests PASS. Exact prospective full suite is reported separately in PR.", + "postReviewTargetedTests": 7 +} 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/paths/app-routes.ts b/projects/social_platform/src/app/api/paths/app-routes.ts index 6f954a2a0..857e99042 100644 --- a/projects/social_platform/src/app/api/paths/app-routes.ts +++ b/projects/social_platform/src/app/api/paths/app-routes.ts @@ -42,6 +42,10 @@ export const AppRoutes = { }, program: { + publishedSolutions: (programId: number | string) => + `/office/program/${programId}/published-solutions`, + publishedSolution: (programId: number | string, solutionId: number | string) => + `/office/program/${programId}/published-solutions/${solutionId}`, solutionCreate: (programId: number | string) => `/office/program/${programId}/solutions/new`, solution: (programId: number | string, solutionId: number | string) => `/office/program/${programId}/solutions/${solutionId}`, @@ -54,6 +58,8 @@ export const AppRoutes = { detail: (programId: number | string) => `/office/program/${programId}`, projects: (programId: number | string) => `/office/program/${programId}/projects`, projectsRating: (programId: number | string) => `/office/program/${programId}/projects-rating`, + rating: (programId: number | string, storageMode?: "legacy_project" | "solution") => + `/office/program/${programId}/${storageMode === "solution" ? "solutions-rating" : "projects-rating"}`, analytics: (programId: number | string) => `/office/program/${programId}/analytics`, members: (programId: number | string) => `/office/program/${programId}/members`, register: (programId: number | string) => `/office/program/${programId}/register`, diff --git a/projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.spec.ts b/projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.spec.ts index 1d37320f0..9711dbe9a 100644 --- a/projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.spec.ts +++ b/projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.spec.ts @@ -88,6 +88,19 @@ describe("ProgramAnalyticsInfoService", () => { ]); expect(getOverview.execute).toHaveBeenCalledExactlyOnceWith(12); }); + it("solution mode delegates to the solution UI without reading Project aggregates", () => { + programUI.program.set({ + ...Program.default(), + id: 12, + isUserManager: true, + solutionStorageMode: "solution", + }); + service.initialize(); + expect(service.solutionMode()).toBe(true); + expect(service.programId()).toBe(12); + expect(service.data()).toBeNull(); + expect(getOverview.execute).not.toHaveBeenCalled(); + }); it("не запрашивает аналитику для пользователя без manager-признака", () => { programUI.program.set({ ...Program.default(), id: 12, isUserManager: false }); diff --git a/projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.ts b/projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.ts index c36a57475..67304e75d 100644 --- a/projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.ts +++ b/projects/social_platform/src/app/api/program/facades/detail/program-analytics-info.service.ts @@ -35,6 +35,9 @@ export class ProgramAnalyticsInfoService { private routeProgramId: number | null = null; private loadedProgram: Program | undefined; readonly programId = signal(null); + readonly solutionMode = computed( + () => this.programUI.program()?.solutionStorageMode === "solution", + ); readonly programName = computed(() => this.programUI.program()?.name ?? "program"); readonly scheduleDates = computed(() => { const program = this.programUI.program(); @@ -112,6 +115,10 @@ export class ProgramAnalyticsInfoService { this.state.set(loading()); this.programId.set(programId); + if (program.solutionStorageMode === "solution") { + this.state.set(initial()); + return; + } this.getOverview .execute(programId) .pipe(takeUntil(this.cancel), takeUntilDestroyed(this.destroyRef)) diff --git a/projects/social_platform/src/app/api/program/facades/detail/program-management.service.spec.ts b/projects/social_platform/src/app/api/program/facades/detail/program-management.service.spec.ts index 9e904afe8..45671cec7 100644 --- a/projects/social_platform/src/app/api/program/facades/detail/program-management.service.spec.ts +++ b/projects/social_platform/src/app/api/program/facades/detail/program-management.service.spec.ts @@ -54,6 +54,16 @@ describe("ProgramManagementService", () => { service.select(second); expect(service.selected()?.id).toBe(second.id); }); + it("в режиме решений не обращается к старому списку проектов или оценке Project", () => { + const { service, api } = setup(); + api.get.mockReturnValue(of({ ...program, solutionStorageMode: "solution" })); + service.load(context()); + expect(service.program()?.solutionStorageMode).toBe("solution"); + expect(service.projects()).toBeNull(); + expect(api.projects).not.toHaveBeenCalled(); + service.load({ ...context(), section: "evaluation", linkId: 11 }); + expect(api.evaluation).not.toHaveBeenCalled(); + }); it("очищает выбор, если проект исчез после фильтрации", () => { const { service, api } = setup(); service.load(context()); diff --git a/projects/social_platform/src/app/api/program/facades/detail/program-management.service.ts b/projects/social_platform/src/app/api/program/facades/detail/program-management.service.ts index 13dfc861d..4c3984f21 100644 --- a/projects/social_platform/src/app/api/program/facades/detail/program-management.service.ts +++ b/projects/social_platform/src/app/api/program/facades/detail/program-management.service.ts @@ -64,43 +64,52 @@ export class ProgramManagementService { this.closeTeam(); }), switchMap(ctx => { - const content = - ctx.section === "participants" - ? this.api - .participants(ctx.programId, ctx.query) - .pipe(map(participants => ({ participants }) as Content)) - : ctx.section === "projects" - ? this.api - .projects(ctx.programId, ctx.query) - .pipe(map(projects => ({ projects }) as Content)) - : ctx.section === "evaluation" && ctx.linkId - ? this.api - .evaluation(ctx.programId, ctx.linkId) - .pipe(map(evaluation => ({ evaluation }) as Content)) - : of({} as Content); - return forkJoin({ program: this.api.get(ctx.programId), content }).pipe( - tap(({ program, content }) => { - this.program.set(program); - this.participants.set(content.participants ?? null); - this.projects.set(content.projects ?? null); - this.evaluation.set(content.evaluation ?? null); - const selectedId = ctx.selectedId ?? this.selected()?.id; - this.selected.set(content.projects?.results.find(p => p.id === selectedId) ?? null); - this.programUI.program.set( - Object.assign(new Program(), Program.default(), program, { isUserManager: true }), - ); - }), - catchError((error: HttpErrorResponse) => { - this.error.set(error); - this.program.set(null); - this.participants.set(null); - this.projects.set(null); - this.evaluation.set(null); - this.clearSelection(); - return of(null); - }), - tap(() => this.pending.set(false)), - ); + return this.api + .get(ctx.programId) + .pipe( + switchMap(program => { + const content = + ctx.section === "participants" + ? this.api + .participants(ctx.programId, ctx.query) + .pipe(map(participants => ({ participants }) as Content)) + : ctx.section === "projects" && program.solutionStorageMode !== "solution" + ? this.api + .projects(ctx.programId, ctx.query) + .pipe(map(projects => ({ projects }) as Content)) + : ctx.section === "evaluation" && + ctx.linkId && + program.solutionStorageMode !== "solution" + ? this.api + .evaluation(ctx.programId, ctx.linkId) + .pipe(map(evaluation => ({ evaluation }) as Content)) + : of({} as Content); + return forkJoin({ program: of(program), content }); + }), + ) + .pipe( + tap(({ program, content }) => { + this.program.set(program); + this.participants.set(content.participants ?? null); + this.projects.set(content.projects ?? null); + this.evaluation.set(content.evaluation ?? null); + const selectedId = ctx.selectedId ?? this.selected()?.id; + this.selected.set(content.projects?.results.find(p => p.id === selectedId) ?? null); + this.programUI.program.set( + Object.assign(new Program(), Program.default(), program, { isUserManager: true }), + ); + }), + catchError((error: HttpErrorResponse) => { + this.error.set(error); + this.program.set(null); + this.participants.set(null); + this.projects.set(null); + this.evaluation.set(null); + this.clearSelection(); + return of(null); + }), + tap(() => this.pending.set(false)), + ); }), takeUntilDestroyed(this.destroyRef), ) diff --git a/projects/social_platform/src/app/api/program/program-widget-presentation.ts b/projects/social_platform/src/app/api/program/program-widget-presentation.ts index d0eb4851f..261c632a8 100644 --- a/projects/social_platform/src/app/api/program/program-widget-presentation.ts +++ b/projects/social_platform/src/app/api/program/program-widget-presentation.ts @@ -60,6 +60,7 @@ export function expertWidgetPresentation( expert: ExpertWidget, competitive: boolean, now: number, + entity: "project" | "solution" = "project", ): ExpertWidgetStatus { const deadline = expert.evaluationEnds ? Date.parse(expert.evaluationEnds) : NaN; const tooltip = Number.isFinite(deadline) @@ -91,8 +92,13 @@ export function expertWidgetPresentation( if (!competitive && expert.mode === "distributed") return status("Без обязательной сдачи", "neutral"); if (expert.mode === "distributed") { - if (expert.assigned === 0) return status("Нет назначенных проектов", "neutral"); - if (expert.remaining === 0) return status("Все проекты оценены", "green"); + if (expert.assigned === 0) + return status( + entity === "solution" ? "Нет назначенных решений" : "Нет назначенных проектов", + "neutral", + ); + if (expert.remaining === 0) + return status(entity === "solution" ? "Все решения оценены" : "Все проекты оценены", "green"); } if (!Number.isFinite(deadline)) return status("Срок не установлен", "neutral"); const remaining = deadline - now; diff --git a/projects/social_platform/src/app/api/program/use-cases/program-role-widget-contract.spec.ts b/projects/social_platform/src/app/api/program/use-cases/program-role-widget-contract.spec.ts new file mode 100644 index 000000000..066c98d92 --- /dev/null +++ b/projects/social_platform/src/app/api/program/use-cases/program-role-widget-contract.spec.ts @@ -0,0 +1,58 @@ +/** @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 { ProgramRepositoryPort } from "@domain/program/ports/program.repository.port"; +import { ProgramRepository } from "@infrastructure/repository/program/program.repository"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { GetProgramRoleWidgetUseCase } from "./get-program-role-widget.use-case"; + +describe("C00 role widget through the actual HTTP adapter", () => { + 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: ProgramRepositoryPort, useExisting: ProgramRepository }, + ], + }), + ); + afterEach(() => TestBed.inject(HttpTestingController).verify()); + + it.each([ + "solution_widget_participant_snapshot", + "solution_widget_organizer_noncompetitive", + "solution_widget_expert_zero_complete", + "legacy_widget_participant", + "legacy_widget_organizer", + "legacy_widget_expert", + ])("keeps real references, zero/null and the %s payload", async name => { + const expected = solutionContractScenarios.find(row => row.name === name)!.response; + const result = firstValueFrom(TestBed.inject(GetProgramRoleWidgetUseCase).execute(12)); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/analytics-widget/", + ); + expect(request.request.method).toBe("GET"); + request.flush(snakecaseKeys(expected as object, { deep: true })); + expect(await result).toEqual({ ok: true, value: expected }); + }); + it.each([ + [401, "unauthorized"], + [409, "integrity"], + ])("preserves status %s without displaying the raw DRF body", async (status, kind) => { + const result = firstValueFrom(TestBed.inject(GetProgramRoleWidgetUseCase).execute(12)); + TestBed.inject(HttpTestingController) + .expectOne("https://local.test/programs/12/analytics-widget/") + .flush({ detail: "private raw error" }, { status: Number(status), statusText: "Error" }); + expect(await result).toEqual({ ok: false, error: kind }); + }); +}); 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..3945461c7 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.spec.ts @@ -0,0 +1,173 @@ +/** @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, + SubmitSolutionUseCase, +} 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() }, + 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); + 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); + }); + 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 new file mode 100644 index 000000000..41078985f --- /dev/null +++ b/projects/social_platform/src/app/api/solution/facades/solution-draft.facade.ts @@ -0,0 +1,341 @@ +/** @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, + SolutionIssue, +} 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, + SubmitSolutionUseCase, +} 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 submission = inject(SubmitSolutionUseCase); + 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 submitting = signal(false); + readonly uploading = signal(null); + readonly busy = computed(() => this.saving() || this.submitting() || 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( + incoming.submission.status === "submitted" ? "Решение сдано." : "Черновик сохранен.", + ); + } + 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); + } + 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); + 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/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..fa0c832c8 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/facades/solution-invitation-inbox.service.spec.ts @@ -0,0 +1,132 @@ +/** @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-archive-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-archive-contract.spec.ts new file mode 100644 index 000000000..04b269f6e --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-archive-contract.spec.ts @@ -0,0 +1,126 @@ +/** @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 { SolutionArchiveApplyRequest } from "@domain/solution/solution-contract.model"; +import { + PreviewSolutionArchiveUseCase, + ApplySolutionArchiveUseCase, +} from "./use-cases/solution-archive.use-cases"; +import { GetSolutionUseCase } from "./use-cases/solution.use-cases"; + +describe("C00 retained solution archive", () => { + 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.each( + solutionContractScenarios.filter( + s => + ["previewSolutionArchive", "applySolutionArchive"].includes(s.operationId) && + !["archive_invalid_empty_selection", "archive_invalid_duplicate_selection"].includes( + s.name, + ), + ), + )("preserves bulk selection, proof, effects and errors: $name", async scenario => { + const preview = scenario.operationId === "previewSolutionArchive"; + const command = (scenario.invalidRequest ?? scenario.request) as SolutionArchiveApplyRequest; + const pending = firstValueFrom( + preview + ? TestBed.inject(PreviewSolutionArchiveUseCase).execute(12, command) + : TestBed.inject(ApplySolutionArchiveUseCase).execute(12, command), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + `https://local.test/programs/12/management/solutions/archive/${preview ? "preview" : "apply"}/`, + ); + expect(req.request.method).toBe("POST"); + expect(req.request.params.keys()).toEqual([]); + expect(req.request.body).toEqual(snakecaseKeys(command, { deep: true })); + req.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + TestBed.inject(HttpTestingController).expectNone(req => + req.url.startsWith("https://local.test"), + ); + }); + + it.each([ + [], + [301, 301], + [0], + [-1], + [1.5], + [NaN], + [Infinity], + [Number.MAX_SAFE_INTEGER + 1], + Array.from({ length: 10001 }, (_, i) => i + 1), + ])("rejects invalid or duplicate selection before HTTP %#", async ids => { + expect( + (await firstValueFrom(TestBed.inject(PreviewSolutionArchiveUseCase).execute(12, { ids }))).ok, + ).toBe(false); + expect( + ( + await firstValueFrom( + TestBed.inject(ApplySolutionArchiveUseCase).execute(12, { ids, previewToken: "opaque" }), + ) + ).ok, + ).toBe(false); + TestBed.inject(HttpTestingController).expectNone(req => + req.url.startsWith("https://local.test"), + ); + }); + + it.each(["active", "archived", "all"] as const)( + "sends explicit participation filter %s", + async participation => { + const pending = firstValueFrom( + TestBed.inject(SolutionRepositoryPort).list(12, { participation, offset: 20, limit: 10 }), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + r => r.url === "https://local.test/programs/12/solutions/", + ); + expect(req.request.params.get("participation")).toBe(participation); + expect(req.request.params.get("offset")).toBe("20"); + req.flush({ count: 0, next: null, previous: null, results: [] }); + expect(await pending).toEqual({ count: 0, next: null, previous: null, results: [] }); + }, + ); + + it.each( + solutionContractScenarios.filter(s => + ["archive_read_submitted_owner", "archive_read_draft_owner"].includes(s.name), + ), + )("retains archived materials without changing submission state: $name", async scenario => { + const pending = firstValueFrom(TestBed.inject(GetSolutionUseCase).execute(12, 301)); + const req = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/301/", + ); + req.flush(snakecaseKeys(scenario.response as object, { deep: true })); + expect(await pending).toEqual({ ok: true, value: scenario.response }); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/solution-case-actions-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-case-actions-contract.spec.ts new file mode 100644 index 000000000..8e74c6912 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-case-actions-contract.spec.ts @@ -0,0 +1,142 @@ +/** @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 { + SolutionCaseChangePreviewRequest, + SolutionCaseChangeApplyRequest, +} from "@domain/solution/solution-contract.model"; +import { + PreviewSolutionCaseChangeUseCase, + ApplySolutionCaseChangeUseCase, +} from "./use-cases/solution-case-actions.use-cases"; + +describe("C00 confirmed solution case change", () => { + 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.each( + solutionContractScenarios.filter(s => + ["previewSolutionCaseChange", "applySolutionCaseChange"].includes(s.operationId), + ), + )("preserves exact cases, token/null, version and errors: $name", async scenario => { + const preview = scenario.operationId === "previewSolutionCaseChange"; + const response = scenario.response as { + application?: { programId: number; solutionId: number }; + }; + const programId = response.application?.programId ?? 12, + solutionId = response.application?.solutionId ?? 301; + const command = (scenario.request as SolutionCaseChangePreviewRequest | undefined) ?? { + caseName: "Образование | наука", + }; + const confirmation = (scenario.request as SolutionCaseChangeApplyRequest | undefined) ?? { + caseName: command.caseName, + previewToken: "opaque-case-preview-token", + }; + const pending = firstValueFrom( + preview + ? TestBed.inject(PreviewSolutionCaseChangeUseCase).execute(programId, solutionId, command) + : TestBed.inject(ApplySolutionCaseChangeUseCase).execute( + programId, + solutionId, + confirmation, + ), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + `https://local.test/programs/${programId}/management/solutions/${solutionId}/case/${preview ? "preview" : "apply"}/`, + ); + expect(req.request.method).toBe("POST"); + expect(req.request.params.keys()).toEqual([]); + expect(req.request.body).toEqual( + snakecaseKeys(preview ? command : confirmation, { deep: true }), + ); + req.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + }); + it.each([0, -1, Number.MAX_SAFE_INTEGER + 1, 1.5, NaN, Infinity])( + "rejects unsafe IDs %s before HTTP", + async id => { + for (const [programId, solutionId] of [ + [id, 301], + [12, id], + ]) { + expect( + ( + await firstValueFrom( + TestBed.inject(PreviewSolutionCaseChangeUseCase).execute(programId, solutionId, { + caseName: "A", + }), + ) + ).ok, + ).toBe(false); + expect( + ( + await firstValueFrom( + TestBed.inject(ApplySolutionCaseChangeUseCase).execute(programId, solutionId, { + caseName: "A", + previewToken: "opaque", + }), + ) + ).ok, + ).toBe(false); + } + TestBed.inject(HttpTestingController).expectNone(req => + req.url.startsWith("https://local.test"), + ); + }, + ); + it("does not normalize spaces, pipe, ID boundary or signed token", async () => { + const caseName = " Город | Образование ", + previewToken = " opaque =./подпись "; + const pending = firstValueFrom( + TestBed.inject(ApplySolutionCaseChangeUseCase).execute( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + { caseName, previewToken }, + ), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + `https://local.test/programs/${Number.MAX_SAFE_INTEGER}/management/solutions/${Number.MAX_SAFE_INTEGER}/case/apply/`, + ); + expect(req.request.body).toEqual({ case_name: caseName, preview_token: previewToken }); + req.flush({ + solution_id: Number.MAX_SAFE_INTEGER, + solution_version: 8, + updated_at: "2026-10-25T10:00:00Z", + }); + expect(await pending).toEqual({ + ok: true, + value: { + solutionId: Number.MAX_SAFE_INTEGER, + solutionVersion: 8, + updatedAt: "2026-10-25T10:00:00Z", + }, + }); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/solution-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-contract.spec.ts index e134ed031..1321eee07 100644 --- a/projects/social_platform/src/app/api/solution/solution-contract.spec.ts +++ b/projects/social_platform/src/app/api/solution/solution-contract.spec.ts @@ -15,7 +15,7 @@ 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 { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; -import { ProgramSolution } from "@domain/solution/solution-contract.model"; +import { ProgramSolution, SolutionContext } from "@domain/solution/solution-contract.model"; import { SolutionRepository } from "@infrastructure/repository/solution/solution.repository"; import { SolutionHttpAdapter } from "@infrastructure/adapters/solution/solution-http.adapter"; import { @@ -94,6 +94,44 @@ describe("C00 solution API boundary", () => { request.flush({ count: 0, next: null, previous: null, results: [] }); }); + it("preserves creation-disabled context through the real interceptor", async () => { + const expected = solutionContractScenarios.find(s => s.name === "context_creation_disabled")! + .response as SolutionContext; + const result = firstValueFrom( + TestBed.inject(GetSolutionContextUseCase).execute(expected.programId), + ); + TestBed.inject(HttpTestingController) + .expectOne(`https://local.test/programs/${expected.programId}/solution-context/`) + .flush(snakecaseKeys(expected, { deep: true })); + expect(await result).toEqual({ ok: true, value: expected }); + }); + + it("returns the authoritative disabled-creation error without retrying", async () => { + const expected = solutionContractScenarios.find( + s => s.name === "create_creation_disabled", + )!.response; + const result = firstValueFrom(TestBed.inject(CreateSolutionUseCase).execute(12)); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/", + ); + expect(request.request.body).toEqual({}); + request.flush(expected, { status: 409, statusText: "Conflict" }); + expect(await result).toEqual({ ok: false, error: expected }); + }); + + it("returns the existing solution during paused creation without altering IDs", async () => { + const expected = solutionContractScenarios.find( + s => s.name === "create_existing_creation_disabled", + )!.response as ProgramSolution; + const result = firstValueFrom( + TestBed.inject(CreateSolutionUseCase).execute(expected.programId), + ); + TestBed.inject(HttpTestingController) + .expectOne(`https://local.test/programs/${expected.programId}/solutions/`) + .flush(snakecaseKeys(expected, { deep: true }), { status: 200, statusText: "OK" }); + expect(await result).toEqual({ ok: true, value: expected }); + }); + it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, NaN])( "rejects invalid ID %s before HTTP", async id => { 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..05fad1464 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-draft-contract.spec.ts @@ -0,0 +1,109 @@ +/** @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, + SubmitSolutionUseCase, +} 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("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, + 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/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..ca01b1f51 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-evaluation-contract.spec.ts @@ -0,0 +1,269 @@ +/** @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 { SolutionCabinetApplications } from "@domain/solution/solution-contract.model"; +import { + GetSolutionRatingsUseCase, + GetSolutionRatingUseCase, + RateSolutionUseCase, + GetSolutionExpertAssignmentsUseCase, + SaveSolutionExpertAssignmentsUseCase, + PreviewSolutionAllocationUseCase, + ApplySolutionAllocationUseCase, + PreviewSolutionQuotaUseCase, + SaveSolutionQuotaUseCase, + GetSolutionResultsUseCase, + GetCabinetApplicationsUseCase, +} 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("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, { + 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/solution-expert-reminder-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-expert-reminder-contract.spec.ts new file mode 100644 index 000000000..58f2f6c35 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-expert-reminder-contract.spec.ts @@ -0,0 +1,129 @@ +/** @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 { + PreviewSolutionExpertReminderUseCase, + ConfirmSolutionExpertReminderUseCase, +} from "./use-cases/solution-manager-analytics.use-cases"; + +describe("C00 solution expert reminder wire contract", () => { + const root = + "https://local.test/programs/12/management/solution-analytics/attention/solutions-awaiting-evaluation/"; + + 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.each( + solutionContractScenarios.filter(row => + ["previewSolutionExpertReminder", "confirmSolutionExpertReminder"].includes(row.operationId), + ), + )("preserves counts, null token and C00 errors for $name", async scenario => { + const preview = scenario.operationId === "previewSolutionExpertReminder"; + const pending = firstValueFrom( + preview + ? TestBed.inject(PreviewSolutionExpertReminderUseCase).execute(12, { + selection: { mode: "all" }, + }) + : TestBed.inject(ConfirmSolutionExpertReminderUseCase).execute(12, "opaque.token"), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + root + (preview ? "reminder-preview/" : "reminders/"), + ); + expect(request.request.method).toBe("POST"); + expect(request.request.params.keys()).toEqual([]); + request.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + }); + + it("sends all-found exclusions and actual solution IDs through the interceptor", async () => { + const pending = firstValueFrom( + TestBed.inject(PreviewSolutionExpertReminderUseCase).execute(12, { + search: "Кейс", + selection: { mode: "all", excludedIds: [301, 304] }, + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne(root + "reminder-preview/"); + expect(request.request.body).toEqual({ + search: "Кейс", + selection: { mode: "all", excluded_ids: [301, 304] }, + }); + request.flush( + { code: "validation_error", detail: "Проверка", errors: [], meta: {} }, + { + status: 400, + statusText: "Error", + }, + ); + expect((await pending).ok).toBe(false); + }); + + it("sends selected solution IDs and leaves confirmation token opaque", async () => { + const preview = firstValueFrom( + TestBed.inject(PreviewSolutionExpertReminderUseCase).execute(12, { + selection: { mode: "selected", ids: [301, 302] }, + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne(root + "reminder-preview/"); + expect(request.request.body).toEqual({ selection: { mode: "selected", ids: [301, 302] } }); + request.flush({ + token: "opaque.signed-token:_", + selected_count: 2, + recipient_count: 1, + without_assignments_count: 0, + unavailable_count: 0, + preview_text: "Текст", + recipients: [{ user_id: 27, full_name: "Анна Петрова" }], + expires_in: 900, + }); + const result = await preview; + expect(result.ok).toBe(true); + if (!result.ok || result.value.token === null) throw new Error("Expected token"); + const confirmation = firstValueFrom( + TestBed.inject(ConfirmSolutionExpertReminderUseCase).execute(12, result.value.token), + ); + const confirmRequest = TestBed.inject(HttpTestingController).expectOne(root + "reminders/"); + expect(confirmRequest.request.body).toEqual({ token: "opaque.signed-token:_" }); + confirmRequest.flush({ created_count: 0, skipped_count: 1 }); + expect(await confirmation).toEqual({ ok: true, value: { createdCount: 0, skippedCount: 1 } }); + }); + + it.each([0, -1, NaN, Number.MAX_SAFE_INTEGER + 1])( + "rejects unsafe program ID %s before creating HTTP request", + async programId => { + const result = await firstValueFrom( + TestBed.inject(PreviewSolutionExpertReminderUseCase).execute(programId, { + selection: { mode: "all" }, + }), + ); + expect(result.ok).toBe(false); + }, + ); +}); diff --git a/projects/social_platform/src/app/api/solution/solution-field-filter-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-field-filter-contract.spec.ts new file mode 100644 index 000000000..1ef690011 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-field-filter-contract.spec.ts @@ -0,0 +1,156 @@ +/** @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 { + GetSolutionManagerSolutionsUseCase, + ExportSolutionAnalyticsUseCase, +} from "./use-cases/solution-manager-analytics.use-cases"; + +describe("C00 configurable solution field filters and matching XLSX", () => { + 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()); + const root = "https://local.test/programs/12/management/solution-analytics/"; + const filters = { + search: "Кейс", + caseScope: "selected", + caseName: "Задача 1", + submission: "submitted", + scope: "awaiting_evaluation", + fieldId: 31, + } as const; + const params = (request: { params: { keys(): string[]; get(key: string): string | null } }) => + Object.fromEntries(request.params.keys().map(key => [key, request.params.get(key)])); + + it.each(["all", "present", "missing", "not_applicable"] as const)( + "passes field and %s through actual interceptor with existing filters before pagination", + async answerState => { + const pending = firstValueFrom( + TestBed.inject(GetSolutionManagerSolutionsUseCase).execute(12, { + ...filters, + answerState, + limit: 5, + offset: 20, + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + r => r.url === root + "solutions/", + ); + expect(params(request.request)).toEqual({ + search: "Кейс", + case_scope: "selected", + case_name: "Задача 1", + submission: "submitted", + scope: "awaiting_evaluation", + field_id: "31", + answer_state: answerState, + limit: "5", + offset: "20", + }); + const page = solutionContractScenarios.find( + row => row.name === "manager_solution_list_awaiting", + )!.response as object; + request.flush(snakecaseKeys(page, { deep: true })); + expect(await pending).toEqual({ ok: true, value: page }); + }, + ); + for (const kind of ["solutions", "submitted"] as const) { + it.each(["present", "missing", "not_applicable"] as const)( + `exports all matching ${kind} with %s without pagination`, + async answerState => { + const pending = firstValueFrom( + TestBed.inject(ExportSolutionAnalyticsUseCase).execute(12, { + kind, + ...filters, + answerState, + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + r => r.url === root + "export/", + ); + expect(request.request.responseType).toBe("blob"); + expect(params(request.request)).toEqual({ + kind, + search: "Кейс", + case_scope: "selected", + case_name: "Задача 1", + submission: "submitted", + scope: "awaiting_evaluation", + field_id: "31", + answer_state: answerState, + }); + const book = new Blob(["PK filtered fixture"], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + request.flush(book); + expect(await pending).toEqual({ ok: true, value: book }); + }, + ); + } + it.each( + solutionContractScenarios.filter( + row => row.name.startsWith("manager_export_") && row.status === 400, + ), + )("decodes C00 JSON from filtered export Blob for $name", async scenario => { + const pending = firstValueFrom( + TestBed.inject(ExportSolutionAnalyticsUseCase).execute(12, { + kind: "solutions", + fieldId: 31, + answerState: "present", + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + r => r.url === root + "export/", + ); + request.flush( + new Blob([JSON.stringify(snakecaseKeys(scenario.response as object, { deep: true }))], { + type: "application/json", + }), + { status: 400, statusText: "Bad Request" }, + ); + expect(await pending).toEqual({ ok: false, error: scenario.response }); + }); + it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, NaN, Infinity])( + "rejects unsafe field ID %s before list or file HTTP", + async fieldId => { + for (const call of [ + () => + TestBed.inject(GetSolutionManagerSolutionsUseCase).execute(12, { + fieldId, + answerState: "present", + }), + () => + TestBed.inject(ExportSolutionAnalyticsUseCase).execute(12, { + kind: "solutions", + fieldId, + answerState: "present", + }), + ]) + expect(await firstValueFrom(call())).toMatchObject({ + ok: false, + error: { code: "invalid_id" }, + }); + TestBed.inject(HttpTestingController).expectNone(() => true); + }, + ); +}); 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/solution-history-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-history-contract.spec.ts new file mode 100644 index 000000000..e6020b54b --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-history-contract.spec.ts @@ -0,0 +1,93 @@ +/** @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 { + SolutionHistoryQuery, + 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 { + ListSolutionAdminChangesUseCase, + ReadSolutionAdminChangeUseCase, +} from "./use-cases/solution-history.use-cases"; + +describe("C00 administrative solution history", () => { + 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.each( + solutionContractScenarios.filter( + scenario => + ["listSolutionAdminChanges", "readSolutionAdminChange"].includes(scenario.operationId) && + scenario.name !== "admin_history_unknown_detail_query", + ), + )("preserves private revisions, nulls and controlled errors: $name", async scenario => { + const list = scenario.operationId === "listSolutionAdminChanges"; + const query = (scenario.query ?? {}) as SolutionHistoryQuery; + const pending = firstValueFrom( + list + ? TestBed.inject(ListSolutionAdminChangesUseCase).execute(12, 301, query) + : TestBed.inject(ReadSolutionAdminChangeUseCase).execute(12, 301, 71), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + request => + request.url === + `https://local.test/programs/12/management/solutions/301/history/${list ? "" : "71/"}`, + ); + expect(req.request.method).toBe("GET"); + expect(req.request.body).toBeNull(); + expect(req.request.params.keys().sort()).toEqual(Object.keys(query).sort()); + for (const [key, value] of Object.entries(query)) { + expect(req.request.params.get(key)).toBe(String(value)); + } + req.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + TestBed.inject(HttpTestingController).expectNone(request => + request.url.startsWith("https://local.test"), + ); + }); + + it.each([0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1])( + "rejects invalid program/solution/change IDs before HTTP: %s", + async id => { + const list = TestBed.inject(ListSolutionAdminChangesUseCase); + const detail = TestBed.inject(ReadSolutionAdminChangeUseCase); + for (const result of [ + await firstValueFrom(list.execute(id, 301)), + await firstValueFrom(list.execute(12, id)), + await firstValueFrom(detail.execute(12, 301, id)), + ]) { + expect(result.ok).toBe(false); + } + TestBed.inject(HttpTestingController).expectNone(request => + request.url.startsWith("https://local.test"), + ); + }, + ); +}); diff --git a/projects/social_platform/src/app/api/solution/solution-manager-analytics-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-manager-analytics-contract.spec.ts new file mode 100644 index 000000000..03423c6da --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-manager-analytics-contract.spec.ts @@ -0,0 +1,172 @@ +/** @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, Observable } 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 { + GetSolutionManagerAnalyticsUseCase, + GetSolutionManagerAssignmentsUseCase, + GetSolutionManagerAssignmentUseCase, + GetSolutionManagerSolutionsUseCase, + ExportSolutionAnalyticsUseCase, +} from "./use-cases/solution-manager-analytics.use-cases"; + +describe("C00 manager solution analytics 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()); + const root = "https://local.test/programs/12/management/solution-analytics/"; + const paths: Record = { + getSolutionManagerAnalytics: "", + getSolutionManagerAssignments: "assignments/", + getSolutionManagerAssignment: "assignments/801/", + getSolutionManagerSolutions: "solutions/", + exportSolutionProgramAnalytics: "export/", + }; + function execute(operationId: string, programId = 12): Observable { + switch (operationId) { + case "getSolutionManagerAnalytics": + return TestBed.inject(GetSolutionManagerAnalyticsUseCase).execute(programId); + case "getSolutionManagerAssignments": + return TestBed.inject(GetSolutionManagerAssignmentsUseCase).execute(programId); + case "getSolutionManagerAssignment": + return TestBed.inject(GetSolutionManagerAssignmentUseCase).execute(programId, 801); + case "getSolutionManagerSolutions": + return TestBed.inject(GetSolutionManagerSolutionsUseCase).execute(programId); + case "exportSolutionProgramAnalytics": + return TestBed.inject(ExportSolutionAnalyticsUseCase).execute(programId); + default: + throw new Error("Unexpected operation " + operationId); + } + } + it.each(solutionContractScenarios.filter(row => row.operationId in paths))( + "preserves actual references, 0/null and C00 errors for $name", + async scenario => { + const pending = firstValueFrom(execute(scenario.operationId)); + const request = TestBed.inject(HttpTestingController).expectOne( + root + paths[scenario.operationId], + ); + expect(request.request.method).toBe("GET"); + expect(request.request.params.keys()).toEqual([]); + const body = snakecaseKeys(scenario.response as object, { deep: true }); + request.flush( + scenario.operationId === "exportSolutionProgramAnalytics" + ? new Blob([JSON.stringify(body)], { type: "application/json" }) + : body, + { status: scenario.status, statusText: scenario.status === 200 ? "OK" : "Error" }, + ); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + }, + ); + it("uses snake_case solution filters and preserves limit/offset and source scope", async () => { + const pending = firstValueFrom( + TestBed.inject(GetSolutionManagerSolutionsUseCase).execute(12, { + limit: 25, + offset: 50, + search: "Кейс", + caseScope: "selected", + caseName: "Задача 1", + submission: "submitted", + scope: "awaiting_evaluation", + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + r => r.url === root + "solutions/", + ); + expect( + Object.fromEntries( + request.request.params.keys().map(key => [key, request.request.params.get(key)]), + ), + ).toEqual({ + limit: "25", + offset: "50", + search: "Кейс", + case_scope: "selected", + case_name: "Задача 1", + submission: "submitted", + scope: "awaiting_evaluation", + }); + request.flush({ + count: 0, + next: null, + previous: null, + mode: "distributed", + submission_applicable: true, + results: [], + }); + expect(await pending).toMatchObject({ ok: true, value: { count: 0, results: [] } }); + }); + it("keeps assignment completed scope on the real endpoint", async () => { + const pending = firstValueFrom( + TestBed.inject(GetSolutionManagerAssignmentsUseCase).execute(12, { scope: "completed" }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + r => r.url === root + "assignments/", + ); + expect(request.request.params.get("scope")).toBe("completed"); + request.flush({ applicable: true, mode: "distributed", results: [] }); + expect(await pending).toMatchObject({ ok: true }); + }); + it.each(["program", "solutions", "submitted", "scores"] as const)( + "returns original XLSX Blob for %s", + async kind => { + const pending = firstValueFrom( + TestBed.inject(ExportSolutionAnalyticsUseCase).execute(12, { kind }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + r => r.url === root + "export/", + ); + expect(request.request.responseType).toBe("blob"); + expect(request.request.params.keys()).toEqual(["kind"]); + expect(request.request.params.get("kind")).toBe(kind); + const blob = new Blob(["PK fixture"], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + request.flush(blob); + expect(await pending).toEqual({ ok: true, value: blob }); + }, + ); + it.each([0, -1, Number.MAX_SAFE_INTEGER + 1, NaN])( + "rejects unsafe program ID %s before every HTTP request", + async id => { + for (const operation of Object.keys(paths)) { + expect(await firstValueFrom(execute(operation, id))).toMatchObject({ + ok: false, + error: { code: "invalid_id" }, + }); + } + TestBed.inject(HttpTestingController).expectNone(() => true); + }, + ); + it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1])( + "rejects unsafe assignment ID %s before HTTP", + async id => { + expect( + await firstValueFrom(TestBed.inject(GetSolutionManagerAssignmentUseCase).execute(12, id)), + ).toMatchObject({ ok: false, error: { code: "invalid_id" } }); + TestBed.inject(HttpTestingController).expectNone(() => true); + }, + ); +}); diff --git a/projects/social_platform/src/app/api/solution/solution-member-actions-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-member-actions-contract.spec.ts new file mode 100644 index 000000000..a750db95d --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-member-actions-contract.spec.ts @@ -0,0 +1,126 @@ +/** @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 { + SolutionMemberRemovalPreviewRequest, + SolutionMemberRemovalApplyRequest, +} from "@domain/solution/solution-contract.model"; +import { + PreviewSolutionMemberRemovalUseCase, + ApplySolutionMemberRemovalUseCase, +} from "./use-cases/solution-member-actions.use-cases"; + +describe("C00 confirmed organizer member removal", () => { + 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.each( + solutionContractScenarios.filter( + s => + ["previewSolutionMemberRemoval", "applySolutionMemberRemoval"].includes(s.operationId) && + !["member_removal_invalid_user", "member_removal_unknown_query"].includes(s.name), + ), + )("preserves selected member, proof, effects and errors: $name", async scenario => { + const preview = scenario.operationId === "previewSolutionMemberRemoval"; + const command = (scenario.invalidRequest ?? + scenario.request) as SolutionMemberRemovalPreviewRequest & SolutionMemberRemovalApplyRequest; + const pending = firstValueFrom( + preview + ? TestBed.inject(PreviewSolutionMemberRemovalUseCase).execute(12, 301, command) + : TestBed.inject(ApplySolutionMemberRemovalUseCase).execute(12, 301, command), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + `https://local.test/programs/12/management/solutions/301/members/remove/${preview ? "preview" : "apply"}/`, + ); + expect(req.request.method).toBe("POST"); + expect(req.request.params.keys()).toEqual([]); + expect(req.request.body).toEqual(snakecaseKeys(command, { deep: true })); + req.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + TestBed.inject(HttpTestingController).expectNone(req => + req.url.startsWith("https://local.test"), + ); + }); + + it.each([0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1])( + "rejects invalid program/solution/user IDs before HTTP: %s", + async id => { + for (const [programId, solutionId, userId] of [ + [id, 301, 10], + [12, id, 10], + [12, 301, id], + ]) { + expect( + ( + await firstValueFrom( + TestBed.inject(PreviewSolutionMemberRemovalUseCase).execute(programId, solutionId, { + userId, + }), + ) + ).ok, + ).toBe(false); + expect( + ( + await firstValueFrom( + TestBed.inject(ApplySolutionMemberRemovalUseCase).execute(programId, solutionId, { + userId, + previewToken: "opaque", + }), + ) + ).ok, + ).toBe(false); + } + TestBed.inject(HttpTestingController).expectNone(req => + req.url.startsWith("https://local.test"), + ); + }, + ); + + it("preserves maximum safe IDs and exact signed token", async () => { + const id = Number.MAX_SAFE_INTEGER, + previewToken = " opaque =./подпись "; + const pending = firstValueFrom( + TestBed.inject(ApplySolutionMemberRemovalUseCase).execute(id, id, { + userId: id, + previewToken, + }), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + `https://local.test/programs/${id}/management/solutions/${id}/members/remove/apply/`, + ); + expect(req.request.body).toEqual({ user_id: id, preview_token: previewToken }); + req.flush({ solution_id: id, solution_version: 8, updated_at: "2026-10-25T15:00:00Z" }); + expect(await pending).toEqual({ + ok: true, + value: { solutionId: id, solutionVersion: 8, updatedAt: "2026-10-25T15:00:00Z" }, + }); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/solution-navigation.ts b/projects/social_platform/src/app/api/solution/solution-navigation.ts index f357846ed..9ce43dc05 100644 --- a/projects/social_platform/src/app/api/solution/solution-navigation.ts +++ b/projects/social_platform/src/app/api/solution/solution-navigation.ts @@ -19,3 +19,9 @@ export function solutionApplicationRoute(application: SolutionApplicationRef) { }, }; } + +export function solutionCreationBlockedMessage(reason: string | null): string { + return reason === "solution_creation_disabled" + ? "Создание новых решений временно приостановлено. Ранее созданные заявки доступны." + : "Создание заявки сейчас недоступно. Проверьте регистрацию и сроки программы."; +} diff --git a/projects/social_platform/src/app/api/solution/solution-publication-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-publication-contract.spec.ts new file mode 100644 index 000000000..05a230c66 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-publication-contract.spec.ts @@ -0,0 +1,96 @@ +/** @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 { + GetPublishedSolutionUseCase, + GetPublishedSolutionsUseCase, +} from "./use-cases/solution-publication.use-cases"; + +describe("C00 published solution wire contract", () => { + const root = "https://local.test/programs/14/published-solutions/"; + 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.each( + solutionContractScenarios.filter(row => + ["getPublishedSolutions", "getPublishedSolution"].includes(row.operationId), + ), + )("preserves public allowlist, 0/False/null and errors for $name", async scenario => { + const list = scenario.operationId === "getPublishedSolutions"; + const pending = firstValueFrom( + list + ? TestBed.inject(GetPublishedSolutionsUseCase).execute(14) + : TestBed.inject(GetPublishedSolutionUseCase).execute(14, 501), + ); + const request = TestBed.inject(HttpTestingController).expectOne(root + (list ? "" : "501/")); + expect(request.request.method).toBe("GET"); + expect(request.request.body).toBeNull(); + expect(request.request.headers.has("Authorization")).toBe(false); + request.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + }); + + it("uses program-scoped list, search and offset without private filters", async () => { + const pending = firstValueFrom( + TestBed.inject(GetPublishedSolutionsUseCase).execute(14, { + search: "Прототип", + limit: 25, + offset: 50, + }), + ); + const request = TestBed.inject(HttpTestingController).expectOne(req => req.url === root); + expect(request.request.params.keys().sort()).toEqual(["limit", "offset", "search"]); + expect(request.request.params.get("offset")).toBe("50"); + expect(request.request.params.get("search")).toBe("Прототип"); + request.flush({ program_id: 14, count: 0, next: null, previous: null, results: [] }); + expect(await pending).toEqual({ + ok: true, + value: { programId: 14, count: 0, next: null, previous: null, results: [] }, + }); + }); + + it.each([0, -1, NaN, Number.MAX_SAFE_INTEGER + 1])( + "rejects unsafe program %s before HTTP", + async id => { + expect( + (await firstValueFrom(TestBed.inject(GetPublishedSolutionsUseCase).execute(id))).ok, + ).toBe(false); + }, + ); + it.each([0, -1, NaN, Number.MAX_SAFE_INTEGER + 1])( + "rejects unsafe solution %s before HTTP", + async id => { + expect( + (await firstValueFrom(TestBed.inject(GetPublishedSolutionUseCase).execute(14, id))).ok, + ).toBe(false); + }, + ); +}); diff --git a/projects/social_platform/src/app/api/solution/solution-registration-removal-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-registration-removal-contract.spec.ts new file mode 100644 index 000000000..a826ae94e --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-registration-removal-contract.spec.ts @@ -0,0 +1,153 @@ +/** @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 { SolutionRegistrationRemovalApplyRequest } from "@domain/solution/solution-contract.model"; +import { + PreviewSolutionRegistrationRemovalUseCase, + ApplySolutionRegistrationRemovalUseCase, + ListSolutionRegistrationRemovalsUseCase, + ReadSolutionRegistrationRemovalUseCase, +} from "./use-cases/solution-registration-removal.use-cases"; +import { ReadSolutionAdminChangeUseCase } from "./use-cases/solution-history.use-cases"; + +describe("C00 registration removal with retained program work", () => { + 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.each( + solutionContractScenarios.filter( + s => s.name.startsWith("registration_removal_") && !s.invalidRequest, + ), + )("uses registration IDs, signed proof and typed audit: $name", async scenario => { + const command = scenario.request as SolutionRegistrationRemovalApplyRequest; + let path: string; + let pending: Promise; + let method = "POST"; + switch (scenario.operationId) { + case "previewSolutionRegistrationRemoval": + path = "/registrations/remove/preview/"; + pending = firstValueFrom( + TestBed.inject(PreviewSolutionRegistrationRemovalUseCase).execute(12, command), + ); + break; + case "applySolutionRegistrationRemoval": + path = "/registrations/remove/apply/"; + pending = firstValueFrom( + TestBed.inject(ApplySolutionRegistrationRemovalUseCase).execute(12, command), + ); + break; + case "listSolutionRegistrationRemovals": + method = "GET"; + path = "/registrations/removals/"; + pending = firstValueFrom( + TestBed.inject(ListSolutionRegistrationRemovalsUseCase).execute(12), + ); + break; + case "readSolutionRegistrationRemoval": + method = "GET"; + path = "/registrations/removals/801/"; + pending = firstValueFrom( + TestBed.inject(ReadSolutionRegistrationRemovalUseCase).execute(12, 801), + ); + break; + case "readSolutionAdminChange": + method = "GET"; + path = "/solutions/301/history/901/"; + pending = firstValueFrom( + TestBed.inject(ReadSolutionAdminChangeUseCase).execute(12, 301, 901), + ); + break; + default: + throw new Error(`Unexpected operation ${scenario.operationId}`); + } + const req = TestBed.inject(HttpTestingController).expectOne( + `https://local.test/programs/12/management${path}`, + ); + expect(req.request.method).toBe(method); + expect(req.request.params.keys()).toEqual([]); + expect(req.request.body).toEqual( + method === "POST" ? snakecaseKeys(command, { deep: true }) : null, + ); + req.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + TestBed.inject(HttpTestingController).expectNone(r => r.url.includes("/projects/")); + }); + + it.each([ + [], + [601, 601], + [0], + [-1], + [1.5], + [NaN], + [Infinity], + [Number.MAX_SAFE_INTEGER + 1], + Array.from({ length: 10001 }, (_, i) => i + 1), + ])("rejects invalid registration selection before HTTP %#", async registrationIds => { + expect( + ( + await firstValueFrom( + TestBed.inject(PreviewSolutionRegistrationRemovalUseCase).execute(12, { + registrationIds, + }), + ) + ).ok, + ).toBe(false); + expect( + ( + await firstValueFrom( + TestBed.inject(ApplySolutionRegistrationRemovalUseCase).execute(12, { + registrationIds, + previewToken: "opaque", + }), + ) + ).ok, + ).toBe(false); + TestBed.inject(HttpTestingController).expectNone(r => r.url.startsWith("https://local.test")); + }); + it("sends audit pagination in the query", async () => { + const pending = firstValueFrom( + TestBed.inject(ListSolutionRegistrationRemovalsUseCase).execute(12, { + offset: 20, + limit: 10, + }), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + r => r.url === "https://local.test/programs/12/management/registrations/removals/", + ); + expect(req.request.params.get("offset")).toBe("20"); + expect(req.request.params.get("limit")).toBe("10"); + req.flush({ count: 0, offset: 20, limit: 10, results: [] }); + expect(await pending).toEqual({ + ok: true, + value: { count: 0, offset: 20, limit: 10, results: [] }, + }); + }); +}); 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/solution-submission-actions-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-submission-actions-contract.spec.ts new file mode 100644 index 000000000..366ea08bd --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-submission-actions-contract.spec.ts @@ -0,0 +1,122 @@ +/** @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 { + SolutionActionPreviewRequest, + SolutionActionApplyRequest, +} from "@domain/solution/solution-contract.model"; +import { + PreviewSolutionSubmitActionUseCase, + ApplySolutionSubmitActionUseCase, +} from "./use-cases/solution-submission-actions.use-cases"; + +describe("C00 organizer first solution submission", () => { + const root = "https://local.test/programs/12/management/solutions/actions/"; + 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.each( + solutionContractScenarios.filter(row => + ["previewSolutionSubmitAction", "applySolutionSubmitAction"].includes(row.operationId), + ), + )("preserves C00 counts, nullable token and errors: $name", async scenario => { + const preview = scenario.operationId === "previewSolutionSubmitAction"; + const selection = (scenario.request as SolutionActionPreviewRequest | undefined) ?? { + action: "submit", + ids: [301], + }; + const confirmation = (scenario.request as SolutionActionApplyRequest | undefined) ?? { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }; + const pending = firstValueFrom( + preview + ? TestBed.inject(PreviewSolutionSubmitActionUseCase).execute(12, selection) + : TestBed.inject(ApplySolutionSubmitActionUseCase).execute(12, confirmation), + ); + const req = TestBed.inject(HttpTestingController).expectOne( + root + (preview ? "preview/" : "apply/"), + ); + expect(req.request.method).toBe("POST"); + expect(req.request.params.keys()).toEqual([]); + expect(req.request.body).toEqual( + snakecaseKeys(preview ? selection : confirmation, { deep: true }), + ); + req.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + }); + + it.each([0, -1, Number.MAX_SAFE_INTEGER + 1, 1.5, NaN])( + "rejects unsafe program or solution ID %s before HTTP", + async id => { + const preview = TestBed.inject(PreviewSolutionSubmitActionUseCase), + apply = TestBed.inject(ApplySolutionSubmitActionUseCase); + for (const [programId, ids] of [ + [id, [301]], + [12, [id]], + ] as const) { + expect( + (await firstValueFrom(preview.execute(programId, { action: "submit", ids: [...ids] }))) + .ok, + ).toBe(false); + expect( + ( + await firstValueFrom( + apply.execute(programId, { action: "submit", ids: [...ids], previewToken: "opaque" }), + ) + ).ok, + ).toBe(false); + } + TestBed.inject(HttpTestingController).expectNone(request => + request.url.startsWith("https://local.test"), + ); + }, + ); + + it("rejects empty/oversized selection before HTTP without silently changing IDs", async () => { + for (const ids of [[], Array.from({ length: 10001 }, () => 301)]) { + expect( + ( + await firstValueFrom( + TestBed.inject(PreviewSolutionSubmitActionUseCase).execute(12, { + action: "submit", + ids, + }), + ) + ).ok, + ).toBe(false); + } + TestBed.inject(HttpTestingController).expectNone(request => + request.url.startsWith("https://local.test"), + ); + }); +}); 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..e90b5b6ce --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-team-contract.spec.ts @@ -0,0 +1,235 @@ +/** @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, + GetSolutionInvitationUseCase, + GetSolutionInvitationsUseCase, + LeaveSolutionUseCase, + RemoveSolutionMemberUseCase, + RevokeSolutionInvitationUseCase, + TransferSolutionCaptainUseCase, + UpdateSolutionInvitationUseCase, +} from "./use-cases/solution-team.use-cases"; +import { SolutionCreateInvitation } from "@domain/solution/solution-contract.model"; + +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.each(["invitation_role_one_character", "invitation_role_maximum"])( + "%s preserves the exact required role on the wire", + async name => { + const scenario = solutionContractScenarios.find(item => item.name === name)!; + const command = scenario.request as SolutionCreateInvitation; + const result = firstValueFrom( + TestBed.inject(CreateSolutionInvitationUseCase).execute(12, 301, command), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/301/invitations/", + ); + expect(request.request.body).toEqual(snakecaseKeys(command, { deep: true })); + expect(request.request.body.role.length).toBe(name.endsWith("maximum") ? 128 : 1); + request.flush(snakecaseKeys(scenario.response as object, { deep: true })); + expect(await result).toEqual({ ok: true, value: scenario.response }); + }, + ); + + it.each([ + ["invitation_missing_role", undefined], + ["invitation_null_role", null], + ["invitation_blank_role", " "], + ])("%s surfaces server validation without retrying", async (name, role) => { + const command = { + expectedSolutionVersion: 8, + recipientUserId: 9, + ...(role === undefined ? {} : { role }), + } as unknown as SolutionCreateInvitation; + const result = firstValueFrom( + TestBed.inject(CreateSolutionInvitationUseCase).execute(12, 301, command), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/301/invitations/", + ); + const scenario = solutionContractScenarios.find(item => item.name === name)!; + request.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: 400, + statusText: "Bad Request", + }); + expect(await result).toEqual({ ok: false, error: scenario.response }); + TestBed.inject(HttpTestingController).expectNone(() => true); + }); + + it("reads a historical invitation with null role", async () => { + const result = firstValueFrom(TestBed.inject(GetSolutionInvitationUseCase).execute(701)); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/solution-invitations/701/", + ); + const scenario = solutionContractScenarios.find( + item => item.name === "invitation_legacy_null_role", + )!; + request.flush(snakecaseKeys(scenario.response as object, { deep: true })); + expect(await result).toEqual({ ok: true, value: scenario.response }); + }); + + 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: "Аналитик", + }), + ); + 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: "Аналитик", + }); + 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-analytics.use-cases.spec.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-analytics.use-cases.spec.ts new file mode 100644 index 000000000..135a8377a --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-analytics.use-cases.spec.ts @@ -0,0 +1,60 @@ +/** @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 { GetSolutionCaseAnalyticsUseCase } from "./solution-analytics.use-cases"; + +describe("Solution case analytics through actual HTTP adapter", () => { + 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.each(solutionContractScenarios.filter(row => row.operationId === "getSolutionCaseAnalytics"))( + "preserves scoped aggregates, 0/null and C00 errors for $name", + async scenario => { + const pending = firstValueFrom(TestBed.inject(GetSolutionCaseAnalyticsUseCase).execute(12)); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/management/solution-cases/", + ); + expect(request.request.method).toBe("GET"); + expect(request.request.params.keys()).toEqual([]); + request.flush(snakecaseKeys(scenario.response as object, { deep: true }), { + status: scenario.status, + statusText: scenario.status === 200 ? "OK" : "Error", + }); + expect(await pending).toEqual( + scenario.status === 200 + ? { ok: true, value: scenario.response } + : { ok: false, error: scenario.response }, + ); + }, + ); + it.each([0, -1, Number.MAX_SAFE_INTEGER + 1, NaN])( + "rejects unsafe program ID %s before HTTP", + async programId => { + expect( + await firstValueFrom(TestBed.inject(GetSolutionCaseAnalyticsUseCase).execute(programId)), + ).toMatchObject({ ok: false, error: { code: "invalid_id" } }); + TestBed.inject(HttpTestingController).expectNone(() => true); + }, + ); +}); diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-analytics.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-analytics.use-cases.ts new file mode 100644 index 000000000..5245fa73b --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-analytics.use-cases.ts @@ -0,0 +1,13 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class GetSolutionCaseAnalyticsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number) { + return solutionRequestResult(() => this.repository.caseAnalytics(programId)); + } +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-archive.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-archive.use-cases.ts new file mode 100644 index 000000000..d01f401de --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-archive.use-cases.ts @@ -0,0 +1,25 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { + SolutionArchivePreviewRequest, + SolutionArchiveApplyRequest, +} from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class PreviewSolutionArchiveUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionArchivePreviewRequest) { + return solutionRequestResult(() => this.repository.previewArchive(programId, command)); + } +} + +@Injectable({ providedIn: "root" }) +export class ApplySolutionArchiveUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionArchiveApplyRequest) { + return solutionRequestResult(() => this.repository.applyArchive(programId, command)); + } +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-case-actions.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-case-actions.use-cases.ts new file mode 100644 index 000000000..968f908cf --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-case-actions.use-cases.ts @@ -0,0 +1,29 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { + SolutionCaseChangePreviewRequest, + SolutionCaseChangeApplyRequest, +} from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class PreviewSolutionCaseChangeUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: SolutionCaseChangePreviewRequest) { + return solutionRequestResult(() => + this.repository.previewCaseChange(programId, solutionId, command), + ); + } +} + +@Injectable({ providedIn: "root" }) +export class ApplySolutionCaseChangeUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: SolutionCaseChangeApplyRequest) { + return solutionRequestResult(() => + this.repository.applyCaseChange(programId, solutionId, command), + ); + } +} 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..ec7fecf1e --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-draft.use-cases.ts @@ -0,0 +1,32 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +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); + 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/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..ab6f8ea77 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-evaluation.use-cases.ts @@ -0,0 +1,102 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { + SolutionRepositoryPort, + SolutionRatingQuery, + SolutionResultsQuery, + SolutionCabinetQuery, +} 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 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); + 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/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/api/solution/use-cases/solution-history.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-history.use-cases.ts new file mode 100644 index 000000000..f0957f6de --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-history.use-cases.ts @@ -0,0 +1,28 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { + SolutionHistoryQuery, + SolutionRepositoryPort, +} from "@domain/solution/ports/solution.repository.port"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class ListSolutionAdminChangesUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, query?: SolutionHistoryQuery) { + return solutionRequestResult(() => + this.repository.listAdministrativeChanges(programId, solutionId, query), + ); + } +} + +@Injectable({ providedIn: "root" }) +export class ReadSolutionAdminChangeUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, changeId: number) { + return solutionRequestResult(() => + this.repository.readAdministrativeChange(programId, solutionId, changeId), + ); + } +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-manager-analytics.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-manager-analytics.use-cases.ts new file mode 100644 index 000000000..5943a7882 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-manager-analytics.use-cases.ts @@ -0,0 +1,63 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { + SolutionRepositoryPort, + SolutionManagerAssignmentQuery, + SolutionManagerSolutionsQuery, + SolutionAnalyticsExportQuery, +} from "@domain/solution/ports/solution.repository.port"; +import { solutionRequestResult } from "./solution.use-cases"; +import { solutionFileRequestResult } from "./solution-results-export.use-case"; +import { SolutionReminderPreviewRequest } from "@domain/solution/solution-contract.model"; + +@Injectable({ providedIn: "root" }) +export class PreviewSolutionExpertReminderUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionReminderPreviewRequest) { + return solutionRequestResult(() => this.repository.previewExpertReminder(programId, command)); + } +} +@Injectable({ providedIn: "root" }) +export class ConfirmSolutionExpertReminderUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, token: string) { + return solutionRequestResult(() => this.repository.confirmExpertReminder(programId, token)); + } +} + +@Injectable({ providedIn: "root" }) +export class GetSolutionManagerAnalyticsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number) { + return solutionRequestResult(() => this.repository.managerAnalytics(programId)); + } +} +@Injectable({ providedIn: "root" }) +export class GetSolutionManagerAssignmentsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionManagerAssignmentQuery) { + return solutionRequestResult(() => this.repository.managerAssignments(programId, query)); + } +} +@Injectable({ providedIn: "root" }) +export class GetSolutionManagerAssignmentUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, assignmentId: number) { + return solutionRequestResult(() => this.repository.managerAssignment(programId, assignmentId)); + } +} +@Injectable({ providedIn: "root" }) +export class GetSolutionManagerSolutionsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionManagerSolutionsQuery) { + return solutionRequestResult(() => this.repository.managerSolutions(programId, query)); + } +} +@Injectable({ providedIn: "root" }) +export class ExportSolutionAnalyticsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionAnalyticsExportQuery) { + return solutionFileRequestResult(() => this.repository.exportAnalytics(programId, query)); + } +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-member-actions.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-member-actions.use-cases.ts new file mode 100644 index 000000000..43da79dff --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-member-actions.use-cases.ts @@ -0,0 +1,29 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { + SolutionMemberRemovalPreviewRequest, + SolutionMemberRemovalApplyRequest, +} from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class PreviewSolutionMemberRemovalUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: SolutionMemberRemovalPreviewRequest) { + return solutionRequestResult(() => + this.repository.previewMemberRemoval(programId, solutionId, command), + ); + } +} + +@Injectable({ providedIn: "root" }) +export class ApplySolutionMemberRemovalUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number, command: SolutionMemberRemovalApplyRequest) { + return solutionRequestResult(() => + this.repository.applyMemberRemoval(programId, solutionId, command), + ); + } +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-publication.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-publication.use-cases.ts new file mode 100644 index 000000000..b16141733 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-publication.use-cases.ts @@ -0,0 +1,24 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { + SolutionPublishedQuery, + SolutionRepositoryPort, +} from "@domain/solution/ports/solution.repository.port"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class GetPublishedSolutionsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionPublishedQuery) { + return solutionRequestResult(() => this.repository.publishedSolutions(programId, query)); + } +} + +@Injectable({ providedIn: "root" }) +export class GetPublishedSolutionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number) { + return solutionRequestResult(() => this.repository.publishedSolution(programId, solutionId)); + } +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-registration-removal.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-registration-removal.use-cases.ts new file mode 100644 index 000000000..d4fa1f8b1 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-registration-removal.use-cases.ts @@ -0,0 +1,44 @@ +/** @format */ +import { inject, Injectable } from "@angular/core"; +import { + SolutionRepositoryPort, + SolutionHistoryQuery, +} from "@domain/solution/ports/solution.repository.port"; +import { + SolutionRegistrationRemovalPreviewRequest, + SolutionRegistrationRemovalApplyRequest, +} from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class PreviewSolutionRegistrationRemovalUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionRegistrationRemovalPreviewRequest) { + return solutionRequestResult(() => + this.repository.previewRegistrationRemoval(programId, command), + ); + } +} +@Injectable({ providedIn: "root" }) +export class ApplySolutionRegistrationRemovalUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionRegistrationRemovalApplyRequest) { + return solutionRequestResult(() => + this.repository.applyRegistrationRemoval(programId, command), + ); + } +} +@Injectable({ providedIn: "root" }) +export class ListSolutionRegistrationRemovalsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionHistoryQuery) { + return solutionRequestResult(() => this.repository.listRegistrationRemovals(programId, query)); + } +} +@Injectable({ providedIn: "root" }) +export class ReadSolutionRegistrationRemovalUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, auditId: number) { + return solutionRequestResult(() => this.repository.readRegistrationRemoval(programId, auditId)); + } +} 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..4f8c0da83 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-results-export.use-case.ts @@ -0,0 +1,49 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { HttpErrorResponse } from "@angular/common/http"; +import { catchError, defer, map, Observable, 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"; + +export function solutionFileRequestResult(request: () => Observable) { + return defer(request).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())), + ); + }), + ); +} + +@Injectable({ providedIn: "root" }) +export class ExportSolutionResultsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionResultsExportQuery) { + return solutionFileRequestResult(() => this.repository.exportResults(programId, query)); + } +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution-submission-actions.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution-submission-actions.use-cases.ts new file mode 100644 index 000000000..5d093e84e --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution-submission-actions.use-cases.ts @@ -0,0 +1,25 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { + SolutionActionPreviewRequest, + SolutionActionApplyRequest, +} from "@domain/solution/solution-contract.model"; +import { solutionRequestResult } from "./solution.use-cases"; + +@Injectable({ providedIn: "root" }) +export class PreviewSolutionSubmitActionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionActionPreviewRequest) { + return solutionRequestResult(() => this.repository.previewSubmitAction(programId, command)); + } +} + +@Injectable({ providedIn: "root" }) +export class ApplySolutionSubmitActionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: SolutionActionApplyRequest) { + return solutionRequestResult(() => this.repository.applySubmitAction(programId, command)); + } +} 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..e5377e4d4 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 @@ -2,12 +2,15 @@ import { inject, Injectable } from "@angular/core"; import { catchError, defer, map, Observable, of } from "rxjs"; -import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { + SolutionListQuery, + SolutionRepositoryPort, +} from "@domain/solution/ports/solution.repository.port"; 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 +21,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 +29,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 +37,14 @@ 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)); + } +} + +@Injectable({ providedIn: "root" }) +export class ListSolutionsUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, query?: SolutionListQuery) { + return solutionRequestResult(() => this.repository.list(programId, query)); } } diff --git a/projects/social_platform/src/app/app.routes.ts b/projects/social_platform/src/app/app.routes.ts index 80498cf11..0ad9c45e3 100644 --- a/projects/social_platform/src/app/app.routes.ts +++ b/projects/social_platform/src/app/app.routes.ts @@ -3,6 +3,7 @@ import { Routes } from "@angular/router"; import { AppComponent } from "./app.component"; import { AuthRequiredGuard } from "@core/lib/guards/auth/auth-required.guard"; +import { PUBLISHED_SOLUTION_ROUTES } from "./ui/routes/program/published-solutions.routes"; /** * Основные маршруты приложения @@ -15,6 +16,7 @@ import { AuthRequiredGuard } from "@core/lib/guards/auth/auth-required.guard"; */ export const APP_ROUTES: Routes = [ + ...PUBLISHED_SOLUTION_ROUTES, { path: "program-registration/:id", loadComponent: () => 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/program/program-results.model.ts b/projects/social_platform/src/app/domain/program/program-results.model.ts index db399470e..7ec528248 100644 --- a/projects/social_platform/src/app/domain/program/program-results.model.ts +++ b/projects/social_platform/src/app/domain/program/program-results.model.ts @@ -1,5 +1,6 @@ /** @format */ import { ManagementCriterion } from "./program-criteria.model"; +import { SolutionRef } from "@domain/solution/solution-contract.model"; export interface ResultExpert { userId: number; @@ -26,8 +27,11 @@ export interface ResultProject { } export interface ResultsGroup { caseName: string | null; - projects: ResultProject[]; + projects: ResultEntry[]; } +/** Shared results presentation. Solution entries never contain a Project foreign key. */ +export type ResultSolution = Omit & { application: SolutionRef }; +export type ResultEntry = ResultProject | ResultSolution; export interface ProgramResults { programId: number; programName: string; diff --git a/projects/social_platform/src/app/domain/program/program-role-widget.model.ts b/projects/social_platform/src/app/domain/program/program-role-widget.model.ts index c59feb313..03ef5eb34 100644 --- a/projects/social_platform/src/app/domain/program/program-role-widget.model.ts +++ b/projects/social_platform/src/app/domain/program/program-role-widget.model.ts @@ -1,46 +1,23 @@ /** - * Read-only данные Angular-проектов; legacy currentApplication не используется. + * Read-only ролевой контракт: проект и решение различаются по storageMode. * * @format */ -export type ProgramWidgetRole = "organizer" | "expert" | "participant"; -export type ParticipantStage = - | "none" - | "not_submitted" - | "submitted" - | "review" - | "evaluated" - | "not_applicable"; -export interface ParticipantWidget { - participantProject: { id: number; name: string; programLinkId: number } | null; - caseProvided: boolean; - caseName: string | null; - stage: ParticipantStage; - submissionOpen: boolean; -} -export interface OrganizerWidget { - participants: number; - projects: number; - submittedSolutions: number | null; - participantsWithoutProject: number; -} -export interface ExpertWidget { - mode: "open" | "distributed"; - assigned: number | null; - remaining: number | null; - evaluationEnds: string | null; -} -interface WidgetContext { - programId: number; - isCompetitive: boolean; -} -export type ProgramRoleWidget = WidgetContext & - ( - | { role: "participant"; participant: ParticipantWidget } - | { role: "organizer"; organizer: OrganizerWidget } - | { role: "expert"; expert: ExpertWidget } - ); +import { + SolutionRoleWidget, + SolutionWidgetStage, + SolutionWidgetLegacyParticipant, + SolutionWidgetLegacyOrganizer, + SolutionWidgetExpert, +} from "@domain/solution/solution-contract.model"; + +export type ProgramRoleWidget = SolutionRoleWidget; +export type ProgramWidgetRole = ProgramRoleWidget["role"]; +export type ParticipantStage = SolutionWidgetStage; +export type ParticipantWidget = SolutionWidgetLegacyParticipant; +export type OrganizerWidget = SolutionWidgetLegacyOrganizer; +export type ExpertWidget = SolutionWidgetExpert; export type ProgramWidgetError = | "network" | "forbidden" diff --git a/projects/social_platform/src/app/domain/program/program.model.ts b/projects/social_platform/src/app/domain/program/program.model.ts index d032496b6..7c0b25469 100644 --- a/projects/social_platform/src/app/domain/program/program.model.ts +++ b/projects/social_platform/src/app/domain/program/program.model.ts @@ -37,6 +37,7 @@ export class Program { isUserLiked!: boolean; isUserManager!: boolean; isUserExpert?: boolean; + solutionStorageMode?: "legacy_project" | "solution"; pendingExpertInvitationId?: number | null; isUserMember!: boolean; currentApplication!: ProgramCurrentApplication | null; 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..0fe10fb5e 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 @@ -2,26 +2,340 @@ import { Observable } from "rxjs"; import { + SolutionRegistrationRemovalPreviewRequest, + SolutionRegistrationRemovalApplyRequest, + SolutionRegistrationRemovalPreview, + SolutionRegistrationRemovalReceipt, + SolutionRegistrationRemovalAuditPage, + SolutionRegistrationRemovalAuditDetail, + SolutionArchivePreviewRequest, + SolutionArchiveApplyRequest, + SolutionArchivePreview, + SolutionArchiveReceipt, + SolutionMemberRemovalPreviewRequest, + SolutionMemberRemovalApplyRequest, + SolutionMemberRemovalPreview, + SolutionCaseChangePreviewRequest, + SolutionCaseChangeApplyRequest, + SolutionCaseChangePreview, + SolutionAdminChangePage, + SolutionAdminChangeDetail, + SolutionActionPreviewRequest, + SolutionActionApplyRequest, + SolutionActionPreview, + SolutionActionResult, CreateSolutionCommand, ProgramSolution, SolutionContext, SolutionPage, + SolutionCaptainCommand, + SolutionTeamCommand, + SolutionMutationReceipt, + SolutionInvitation, + SolutionInvitationPage, + SolutionCreateInvitation, + SolutionUpdateInvitation, + SolutionInvitationCreated, + SolutionInvitationActionResult, + SolutionForm, + SolutionFormWrite, + SolutionFormManagement, + SolutionTemplates, + UpdateSolutionCommand, + SolutionUploadResponse, + SubmitSolutionCommand, + RateSolutionCommand, + SolutionRatingPage, + SolutionRatingFrame, + SolutionAssignmentOptions, + SolutionAssignmentsWrite, + SolutionAllocationWrite, + SolutionAllocationPlan, + SolutionAssignmentReceipt, + SolutionQuota, + SolutionQuotaWrite, + SolutionResults, + SolutionCabinetApplications, + SolutionCaseSolutionAnalytics, + SolutionManagerAnalytics, + SolutionManagerAssignments, + SolutionManagerAssignmentDetail, + SolutionManagerSolutions, + SolutionReminderPreviewRequest, + SolutionReminderPreview, + SolutionReminderReceipt, + SolutionPublishedSolution, + SolutionPublishedSolutionPage, } from "../solution-contract.model"; export abstract class SolutionRepositoryPort { + abstract previewRegistrationRemoval( + programId: number, + command: SolutionRegistrationRemovalPreviewRequest, + ): Observable; + abstract applyRegistrationRemoval( + programId: number, + command: SolutionRegistrationRemovalApplyRequest, + ): Observable; + abstract listRegistrationRemovals( + programId: number, + query?: SolutionHistoryQuery, + ): Observable; + abstract readRegistrationRemoval( + programId: number, + auditId: number, + ): Observable; + abstract previewArchive( + programId: number, + command: SolutionArchivePreviewRequest, + ): Observable; + abstract applyArchive( + programId: number, + command: SolutionArchiveApplyRequest, + ): Observable; + abstract previewMemberRemoval( + programId: number, + solutionId: number, + command: SolutionMemberRemovalPreviewRequest, + ): Observable; + abstract applyMemberRemoval( + programId: number, + solutionId: number, + command: SolutionMemberRemovalApplyRequest, + ): Observable; + abstract listAdministrativeChanges( + programId: number, + solutionId: number, + query?: SolutionHistoryQuery, + ): Observable; + abstract readAdministrativeChange( + programId: number, + solutionId: number, + changeId: number, + ): Observable; + abstract previewCaseChange( + programId: number, + solutionId: number, + command: SolutionCaseChangePreviewRequest, + ): Observable; + abstract applyCaseChange( + programId: number, + solutionId: number, + command: SolutionCaseChangeApplyRequest, + ): Observable; + abstract previewSubmitAction( + programId: number, + command: SolutionActionPreviewRequest, + ): Observable; + abstract applySubmitAction( + programId: number, + command: SolutionActionApplyRequest, + ): Observable; + abstract publishedSolutions( + programId: number, + query?: SolutionPublishedQuery, + ): Observable; + abstract publishedSolution( + programId: number, + solutionId: number, + ): Observable; + abstract previewExpertReminder( + programId: number, + command: SolutionReminderPreviewRequest, + ): Observable; + abstract confirmExpertReminder( + programId: number, + token: string, + ): Observable; + abstract managerAnalytics(programId: number): Observable; + abstract managerAssignments( + programId: number, + query?: SolutionManagerAssignmentQuery, + ): Observable; + abstract managerAssignment( + programId: number, + assignmentId: number, + ): Observable; + abstract managerSolutions( + programId: number, + query?: SolutionManagerSolutionsQuery, + ): Observable; + abstract exportAnalytics( + programId: number, + query?: SolutionAnalyticsExportQuery, + ): Observable; + abstract caseAnalytics(programId: number): Observable; + abstract cabinetApplications( + query?: SolutionCabinetQuery, + ): Observable; + 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 exportResults(programId: number, query?: SolutionResultsExportQuery): 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, + command: SubmitSolutionCommand, + ): Observable; + 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( + 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; 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 SolutionHistoryQuery = Partial>; + +export type SolutionInvitationQuery = { + status?: "pending" | "accepted" | "declined"; + limit?: number; + offset?: number; +}; + export type SolutionListQuery = { + participation?: "active" | "archived" | "all"; search?: string; submission?: "all" | "submitted" | "not_submitted"; limit?: number; offset?: number; }; +export type SolutionPublishedQuery = Pick; + +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 type SolutionResultsExportQuery = SolutionResultsQuery & { + scope?: "current" | "all"; +}; + +export type SolutionCabinetQuery = { + status?: "pending" | "completed"; + q?: string; + programId?: number; + limit?: number; + offset?: number; +}; + +export type SolutionManagerAssignmentQuery = { + scope?: "all" | "completed" | "pending"; +}; + +export type SolutionManagerSolutionFilters = { + search?: string; + scope?: "all" | "not_submitted" | "awaiting_evaluation"; + submission?: "all" | "submitted" | "not_submitted"; + caseScope?: "all" | "selected" | "without_case"; + caseName?: string; + fieldId?: number; + answerState?: "all" | "present" | "missing" | "not_applicable"; +}; + +export type SolutionManagerSolutionsQuery = SolutionManagerSolutionFilters & { + limit?: number; + offset?: number; +}; + +export type SolutionAnalyticsExportQuery = { limit?: never; offset?: never } & ( + | ({ kind?: "program" | "solutions" | "submitted" | "scores" } & { + [Key in keyof SolutionManagerSolutionFilters]?: never; + }) + | ({ kind: "solutions" | "submitted" } & SolutionManagerSolutionFilters) +); + 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 4b1a90f52..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 @@ -6,6 +6,8 @@ export type SolutionContractScenario = { operationId: string; status: number; request?: unknown; + invalidRequest?: unknown; + query?: unknown; response?: unknown; }; export const solutionContractScenarios: SolutionContractScenario[] = [ @@ -227,6 +229,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: false, @@ -422,6 +425,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: true, @@ -610,6 +614,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: false, canInvite: false, canSubmit: false, + canLeave: false, }, readiness: { isReady: true, @@ -792,6 +797,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: false, + canLeave: false, }, readiness: null, }, @@ -971,6 +977,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: false, + canLeave: false, }, readiness: { isReady: false, @@ -1132,6 +1139,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: false, @@ -1340,6 +1348,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: false, @@ -1555,6 +1564,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: true, @@ -1747,6 +1757,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: false, @@ -1946,6 +1957,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: false, canInvite: false, canSubmit: false, + canLeave: false, }, readiness: { isReady: true, @@ -2114,6 +2126,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ request: { expectedSolutionVersion: 8, recipientUserId: 9, + role: "Аналитик", }, response: { code: "duplicate_pending_invite", @@ -2129,6 +2142,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ request: { expectedSolutionVersion: 8, recipientUserId: 9, + role: "Аналитик", }, response: { code: "team_max_size", @@ -3149,6 +3163,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: true, canInvite: true, canSubmit: true, + canLeave: false, }, readiness: { isReady: true, @@ -3544,6 +3559,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ request: { expectedSolutionVersion: 10, recipientUserId: 9, + role: "Аналитик", }, response: { code: "team_frozen", @@ -3727,6 +3743,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: false, canInvite: false, canSubmit: false, + canLeave: true, }, readiness: { isReady: false, @@ -3922,6 +3939,7 @@ export const solutionContractScenarios: SolutionContractScenario[] = [ canManageTeam: false, canInvite: false, canSubmit: false, + canLeave: false, }, readiness: null, }, @@ -3942,4 +3960,8404 @@ 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, + }, + ], + }, + }, + { + 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, + }, + ], + }, + }, + { + 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: {}, + }, + }, + { + 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: ["Цифровой сервис"], + }, + }, + { + name: "solution_widget_participant_snapshot", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "participant", + isCompetitive: true, + storageMode: "solution", + participant: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + caseProvided: true, + caseName: "Цифровой сервис", + stage: "evaluated", + submissionOpen: false, + }, + }, + }, + { + name: "solution_widget_participant_draft_without_case", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "participant", + isCompetitive: true, + storageMode: "solution", + participant: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + caseProvided: true, + caseName: null, + stage: "not_submitted", + submissionOpen: true, + }, + }, + }, + { + name: "solution_widget_participant_noncompetitive", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "participant", + isCompetitive: false, + storageMode: "solution", + participant: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + caseProvided: true, + caseName: "Цифровой сервис", + stage: "not_applicable", + submissionOpen: false, + }, + }, + }, + { + name: "solution_widget_organizer", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "organizer", + isCompetitive: true, + storageMode: "solution", + organizer: { + participants: 5, + solutions: 2, + submittedSolutions: 1, + participantsWithoutSolution: 2, + }, + }, + }, + { + name: "solution_widget_organizer_noncompetitive", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "organizer", + isCompetitive: false, + storageMode: "solution", + organizer: { + participants: 5, + solutions: 2, + submittedSolutions: null, + participantsWithoutSolution: 2, + }, + }, + }, + { + name: "solution_widget_expert_zero_complete", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "expert", + isCompetitive: true, + storageMode: "solution", + expert: { + mode: "distributed", + assigned: 1, + remaining: 0, + evaluationEnds: "2026-10-24T18:00:00Z", + }, + }, + }, + { + name: "solution_widget_expert_open", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "expert", + isCompetitive: true, + storageMode: "solution", + expert: { + mode: "open", + assigned: null, + remaining: null, + evaluationEnds: "2026-10-24T18:00:00Z", + }, + }, + }, + { + name: "solution_widget_expert_empty", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "expert", + isCompetitive: true, + storageMode: "solution", + expert: { + mode: "distributed", + assigned: 0, + remaining: 0, + evaluationEnds: "2026-10-24T18:00:00Z", + }, + }, + }, + { + name: "legacy_widget_participant", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "participant", + isCompetitive: true, + participant: { + caseProvided: true, + caseName: "Цифровой сервис", + stage: "evaluated", + submissionOpen: false, + participantProject: { + id: 55, + name: "Исторический проект", + programLinkId: 65, + }, + }, + }, + }, + { + name: "legacy_widget_organizer", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "organizer", + isCompetitive: true, + organizer: { + participants: 5, + projects: 2, + submittedSolutions: 1, + participantsWithoutProject: 2, + }, + }, + }, + { + name: "legacy_widget_expert", + operationId: "getProgramRoleWidget", + status: 200, + response: { + programId: 12, + role: "expert", + isCompetitive: true, + expert: { + mode: "distributed", + assigned: 1, + remaining: 0, + evaluationEnds: "2026-10-24T18:00:00Z", + }, + }, + }, + { + name: "widget_forbidden", + operationId: "getProgramRoleWidget", + status: 403, + response: { + detail: "Аналитика доступна только участникам этой программы.", + }, + }, + { + name: "widget_ambiguous_membership", + operationId: "getProgramRoleWidget", + status: 409, + response: { + detail: "Связи заявки требуют проверки организатором.", + }, + }, + { + name: "solution_case_analytics_competitive", + operationId: "getSolutionCaseAnalytics", + status: 200, + response: { + programId: 12, + configured: true, + submissionApplicable: true, + items: [ + { + name: "Экология", + participantsTotal: 3, + solutionsTotal: 2, + notSubmitted: 1, + submitted: 1, + }, + { + name: "Технологии", + participantsTotal: 0, + solutionsTotal: 0, + notSubmitted: 0, + submitted: 0, + }, + ], + withoutCase: { + participantsTotal: 1, + solutionsTotal: 1, + notSubmitted: 1, + submitted: 0, + }, + }, + }, + { + name: "solution_case_analytics_noncompetitive", + operationId: "getSolutionCaseAnalytics", + status: 200, + response: { + programId: 12, + configured: true, + submissionApplicable: false, + items: [ + { + name: "Экология", + participantsTotal: 3, + solutionsTotal: 2, + notSubmitted: null, + submitted: null, + }, + { + name: "Технологии", + participantsTotal: 0, + solutionsTotal: 0, + notSubmitted: null, + submitted: null, + }, + ], + withoutCase: { + participantsTotal: 1, + solutionsTotal: 1, + notSubmitted: null, + submitted: null, + }, + }, + }, + { + name: "solution_case_analytics_unconfigured", + operationId: "getSolutionCaseAnalytics", + status: 200, + response: { + programId: 12, + configured: false, + submissionApplicable: true, + items: [], + withoutCase: { + participantsTotal: 0, + solutionsTotal: 0, + notSubmitted: 0, + submitted: 0, + }, + }, + }, + { + name: "solution_case_analytics_forbidden", + operationId: "getSolutionCaseAnalytics", + status: 403, + response: { + code: "permission_denied", + detail: "Аналитика доступна организатору программы.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_case_analytics_anonymous", + operationId: "getSolutionCaseAnalytics", + status: 401, + response: { + code: "authentication_required", + detail: "Требуется авторизация.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_case_analytics_missing", + operationId: "getSolutionCaseAnalytics", + status: 404, + response: { + code: "not_found", + detail: "Решение или программа не найдены.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_case_analytics_legacy", + operationId: "getSolutionCaseAnalytics", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Программа использует заявки на основе проектов.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_case_analytics_unexpected_query", + operationId: "getSolutionCaseAnalytics", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные запроса.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_solution_analytics_distributed", + operationId: "getSolutionManagerAnalytics", + status: 200, + response: { + programId: 12, + summary: { + participants: { + total: 3, + }, + solutions: { + total: 1, + }, + experts: { + total: 1, + }, + participantRegions: { + total: 1, + items: [ + { + name: "Москва", + count: 3, + }, + ], + }, + }, + participantFunnel: { + registrations: 3, + uniqueParticipants: 3, + visitedParticipants: 2, + onboardedParticipants: 2, + withTeam: 2, + captains: 1, + submittedCaptains: 1, + }, + solutionFunnel: { + created: 1, + submissionApplicable: true, + notSubmitted: 0, + submitted: 1, + evaluated: 0, + }, + evaluationStatus: { + applicable: true, + mode: "distributed", + requiredEvaluators: 2, + maxEvaluationsPerSolution: 3, + assignments: { + total: 1, + pending: 1, + evaluated: 0, + }, + solutions: { + submitted: 1, + awaitingEvaluation: 1, + partiallyEvaluated: 0, + evaluated: 0, + }, + }, + attention: { + participantsWithoutVisit: 1, + participantsWithoutOnboarding: 1, + participantsWithoutTeam: 1, + solutionsAwaitingEvaluation: 1, + solutionsNotSubmitted: 0, + delayedExperts: { + total: 0, + items: [], + }, + }, + activity: [ + { + date: "2026-10-24", + registrations: 3, + submittedSolutions: 1, + }, + ], + cases: { + programId: 12, + configured: true, + submissionApplicable: true, + items: [ + { + name: "Экология", + participantsTotal: 2, + solutionsTotal: 1, + notSubmitted: 0, + submitted: 1, + }, + ], + withoutCase: { + participantsTotal: 0, + solutionsTotal: 0, + notSubmitted: 0, + submitted: 0, + }, + }, + fieldCoverage: [ + { + fieldId: 9, + label: "Презентация", + type: "file", + purpose: "presentation", + applicable: 1, + answered: 0, + requiredApplicable: 0, + requiredUnanswered: 0, + }, + ], + }, + }, + { + name: "manager_solution_analytics_open", + operationId: "getSolutionManagerAnalytics", + status: 200, + response: { + programId: 12, + summary: { + participants: { + total: 3, + }, + solutions: { + total: 1, + }, + experts: { + total: 1, + }, + participantRegions: { + total: 1, + items: [ + { + name: "Москва", + count: 3, + }, + ], + }, + }, + participantFunnel: { + registrations: 3, + uniqueParticipants: 3, + visitedParticipants: 2, + onboardedParticipants: 2, + withTeam: 2, + captains: 1, + submittedCaptains: 1, + }, + solutionFunnel: { + created: 1, + submissionApplicable: true, + notSubmitted: 0, + submitted: 1, + evaluated: 0, + }, + evaluationStatus: { + applicable: true, + mode: "open", + requiredEvaluators: 2, + maxEvaluationsPerSolution: 3, + assignments: null, + solutions: { + submitted: 1, + awaitingEvaluation: 1, + partiallyEvaluated: 0, + evaluated: 0, + }, + }, + attention: { + participantsWithoutVisit: 1, + participantsWithoutOnboarding: 1, + participantsWithoutTeam: 1, + solutionsAwaitingEvaluation: 1, + solutionsNotSubmitted: 0, + delayedExperts: { + total: 0, + items: [], + }, + }, + activity: [ + { + date: "2026-10-24", + registrations: 3, + submittedSolutions: 1, + }, + ], + cases: { + programId: 12, + configured: true, + submissionApplicable: true, + items: [ + { + name: "Экология", + participantsTotal: 2, + solutionsTotal: 1, + notSubmitted: 0, + submitted: 1, + }, + ], + withoutCase: { + participantsTotal: 0, + solutionsTotal: 0, + notSubmitted: 0, + submitted: 0, + }, + }, + fieldCoverage: [ + { + fieldId: 9, + label: "Презентация", + type: "file", + purpose: "presentation", + applicable: 1, + answered: 0, + requiredApplicable: 0, + requiredUnanswered: 0, + }, + ], + }, + }, + { + name: "manager_solution_analytics_noncompetitive", + operationId: "getSolutionManagerAnalytics", + status: 200, + response: { + programId: 12, + summary: { + participants: { + total: 3, + }, + solutions: { + total: 1, + }, + experts: { + total: 1, + }, + participantRegions: { + total: 1, + items: [ + { + name: "Москва", + count: 3, + }, + ], + }, + }, + participantFunnel: { + registrations: 3, + uniqueParticipants: 3, + visitedParticipants: 2, + onboardedParticipants: 2, + withTeam: 2, + captains: 1, + submittedCaptains: null, + }, + solutionFunnel: { + created: 1, + submissionApplicable: false, + notSubmitted: null, + submitted: null, + evaluated: null, + }, + evaluationStatus: { + applicable: false, + mode: "distributed", + requiredEvaluators: 2, + maxEvaluationsPerSolution: 3, + assignments: null, + solutions: null, + }, + attention: { + participantsWithoutVisit: 1, + participantsWithoutOnboarding: 1, + participantsWithoutTeam: 1, + solutionsAwaitingEvaluation: null, + solutionsNotSubmitted: null, + delayedExperts: { + total: 0, + items: [], + }, + }, + activity: [ + { + date: "2026-10-24", + registrations: 3, + submittedSolutions: null, + }, + ], + cases: { + programId: 12, + configured: true, + submissionApplicable: false, + items: [ + { + name: "Экология", + participantsTotal: 2, + solutionsTotal: 1, + notSubmitted: null, + submitted: null, + }, + ], + withoutCase: { + participantsTotal: 0, + solutionsTotal: 0, + notSubmitted: null, + submitted: null, + }, + }, + fieldCoverage: [ + { + fieldId: 9, + label: "Презентация", + type: "file", + purpose: "presentation", + applicable: 1, + answered: 0, + requiredApplicable: 0, + requiredUnanswered: 0, + }, + ], + }, + }, + { + name: "manager_solution_assignments_pending", + operationId: "getSolutionManagerAssignments", + status: 200, + response: { + applicable: true, + mode: "distributed", + results: [ + { + assignmentId: 801, + expert: { + expertId: 40, + userId: 71, + firstName: "Иван", + lastName: "Тест", + fullName: "Иван Тест", + avatar: null, + }, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + status: "pending", + assignedAt: "2026-10-25T00:00:00Z", + submitted: true, + submittedAt: "2026-10-24T00:00:00Z", + waitingSince: "2026-10-25T00:00:00Z", + waitingSeconds: 3600, + }, + ], + }, + }, + { + name: "manager_solution_assignments_completed", + operationId: "getSolutionManagerAssignments", + status: 200, + response: { + applicable: true, + mode: "distributed", + results: [ + { + assignmentId: 801, + expert: { + expertId: 40, + userId: 71, + firstName: "Иван", + lastName: "Тест", + fullName: "Иван Тест", + avatar: null, + }, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + status: "completed", + assignedAt: "2026-10-25T00:00:00Z", + submitted: true, + submittedAt: "2026-10-24T00:00:00Z", + waitingSince: null, + waitingSeconds: null, + }, + ], + }, + }, + { + name: "manager_solution_assignment_zero_false_comment", + operationId: "getSolutionManagerAssignment", + status: 200, + response: { + assignmentId: 801, + expert: { + expertId: 40, + userId: 71, + firstName: "Иван", + lastName: "Тест", + fullName: "Иван Тест", + avatar: null, + }, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + status: "completed", + assignedAt: "2026-10-25T00:00:00Z", + submitted: true, + submittedAt: "2026-10-24T00:00:00Z", + waitingSince: null, + waitingSeconds: null, + scores: [ + { + criterionId: 7, + name: "Оценка", + description: null, + type: "int", + minValue: 0, + maxValue: 10, + value: "0", + isScored: true, + }, + { + criterionId: 8, + name: "Условие", + description: null, + type: "bool", + minValue: null, + maxValue: null, + value: "False", + isScored: true, + }, + { + criterionId: 9, + name: "Комментарий", + description: null, + type: "str", + minValue: null, + maxValue: null, + value: "", + isScored: true, + }, + ], + }, + }, + { + name: "manager_solution_list_awaiting", + operationId: "getSolutionManagerSolutions", + status: 200, + response: { + count: 1, + next: null, + previous: null, + submissionApplicable: true, + mode: "distributed", + results: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + captain: { + userId: 5, + fullName: "Капитан Тест", + avatar: null, + }, + caseName: "Экология", + submissionStatus: "submitted", + evaluationStatus: "awaiting_evaluation", + createdAt: "2026-10-23T00:00:00Z", + submittedAt: "2026-10-24T00:00:00Z", + assignmentsTotal: 1, + assignmentsCompleted: 0, + waitingReason: "no_completed_evaluations", + }, + ], + }, + }, + { + name: "manager_solution_list_empty", + operationId: "getSolutionManagerSolutions", + status: 200, + response: { + count: 0, + next: null, + previous: null, + submissionApplicable: false, + mode: "open", + results: [], + }, + }, + { + name: "getSolutionManagerAnalytics_forbidden", + operationId: "getSolutionManagerAnalytics", + status: 403, + response: { + code: "permission_denied", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAnalytics_anonymous", + operationId: "getSolutionManagerAnalytics", + status: 401, + response: { + code: "authentication_required", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAnalytics_missing", + operationId: "getSolutionManagerAnalytics", + status: 404, + response: { + code: "not_found", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAnalytics_legacy", + operationId: "getSolutionManagerAnalytics", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAnalytics_invalid", + operationId: "getSolutionManagerAnalytics", + status: 400, + response: { + code: "validation_error", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignments_forbidden", + operationId: "getSolutionManagerAssignments", + status: 403, + response: { + code: "permission_denied", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignments_anonymous", + operationId: "getSolutionManagerAssignments", + status: 401, + response: { + code: "authentication_required", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignments_missing", + operationId: "getSolutionManagerAssignments", + status: 404, + response: { + code: "not_found", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignments_legacy", + operationId: "getSolutionManagerAssignments", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignments_invalid", + operationId: "getSolutionManagerAssignments", + status: 400, + response: { + code: "validation_error", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignment_forbidden", + operationId: "getSolutionManagerAssignment", + status: 403, + response: { + code: "permission_denied", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignment_anonymous", + operationId: "getSolutionManagerAssignment", + status: 401, + response: { + code: "authentication_required", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignment_missing", + operationId: "getSolutionManagerAssignment", + status: 404, + response: { + code: "not_found", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignment_legacy", + operationId: "getSolutionManagerAssignment", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerAssignment_invalid", + operationId: "getSolutionManagerAssignment", + status: 400, + response: { + code: "validation_error", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerSolutions_forbidden", + operationId: "getSolutionManagerSolutions", + status: 403, + response: { + code: "permission_denied", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerSolutions_anonymous", + operationId: "getSolutionManagerSolutions", + status: 401, + response: { + code: "authentication_required", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerSolutions_missing", + operationId: "getSolutionManagerSolutions", + status: 404, + response: { + code: "not_found", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerSolutions_legacy", + operationId: "getSolutionManagerSolutions", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "getSolutionManagerSolutions_invalid", + operationId: "getSolutionManagerSolutions", + status: 400, + response: { + code: "validation_error", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "exportSolutionProgramAnalytics_forbidden", + operationId: "exportSolutionProgramAnalytics", + status: 403, + response: { + code: "permission_denied", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "exportSolutionProgramAnalytics_anonymous", + operationId: "exportSolutionProgramAnalytics", + status: 401, + response: { + code: "authentication_required", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "exportSolutionProgramAnalytics_missing", + operationId: "exportSolutionProgramAnalytics", + status: 404, + response: { + code: "not_found", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "exportSolutionProgramAnalytics_legacy", + operationId: "exportSolutionProgramAnalytics", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "exportSolutionProgramAnalytics_invalid", + operationId: "exportSolutionProgramAnalytics", + status: 400, + response: { + code: "validation_error", + detail: "Запрос недоступен.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_reminder_preview_selected", + operationId: "previewSolutionExpertReminder", + status: 200, + response: { + token: "opaque-signed-example-token", + selectedCount: 3, + recipientCount: 1, + withoutAssignmentsCount: 1, + unavailableCount: 0, + previewText: "Эксперт, здравствуйте! В программе остались незавершенные оценки решений.", + recipients: [ + { + userId: 27, + fullName: "Анна Петрова", + }, + ], + expiresIn: 900, + }, + request: { + selection: { + mode: "selected", + ids: [301, 302, 303], + }, + }, + }, + { + name: "solution_reminder_preview_all_search_excluded", + operationId: "previewSolutionExpertReminder", + status: 200, + response: { + token: "opaque-signed-example-token", + selectedCount: 2, + recipientCount: 1, + withoutAssignmentsCount: 0, + unavailableCount: 0, + previewText: "Эксперт, здравствуйте! В программе остались незавершенные оценки решений.", + recipients: [ + { + userId: 27, + fullName: "Анна Петрова", + }, + ], + expiresIn: 900, + }, + request: { + search: "Сервис", + selection: { + mode: "all", + excludedIds: [304], + }, + }, + }, + { + name: "solution_reminder_preview_no_assignments", + operationId: "previewSolutionExpertReminder", + status: 200, + response: { + token: null, + selectedCount: 1, + recipientCount: 0, + withoutAssignmentsCount: 1, + unavailableCount: 0, + previewText: "", + recipients: [], + expiresIn: 900, + }, + request: { + selection: { + mode: "selected", + ids: [303], + }, + }, + }, + { + name: "solution_reminder_preview_missing_email", + operationId: "previewSolutionExpertReminder", + status: 200, + response: { + token: null, + selectedCount: 1, + recipientCount: 0, + withoutAssignmentsCount: 0, + unavailableCount: 1, + previewText: "", + recipients: [], + expiresIn: 900, + }, + request: { + selection: { + mode: "selected", + ids: [302], + }, + }, + }, + { + name: "solution_reminder_preview_no_auth", + operationId: "previewSolutionExpertReminder", + status: 401, + response: { + code: "authentication_required", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_reminder_preview_forbidden", + operationId: "previewSolutionExpertReminder", + status: 403, + response: { + code: "permission_denied", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_reminder_preview_not_found", + operationId: "previewSolutionExpertReminder", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_reminder_preview_legacy", + operationId: "previewSolutionExpertReminder", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_reminder_preview_not_applicable", + operationId: "previewSolutionExpertReminder", + status: 409, + response: { + code: "reminders_not_applicable", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_reminder_preview_invalid_selection", + operationId: "previewSolutionExpertReminder", + status: 400, + response: { + code: "validation_error", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_reminder_confirm", + operationId: "confirmSolutionExpertReminder", + status: 200, + response: { + createdCount: 1, + skippedCount: 0, + }, + request: { + token: "opaque-signed-example-token", + }, + }, + { + name: "solution_reminder_confirm_idempotent", + operationId: "confirmSolutionExpertReminder", + status: 200, + response: { + createdCount: 1, + skippedCount: 0, + }, + request: { + token: "opaque-signed-example-token", + }, + }, + { + name: "solution_reminder_confirm_completed_before_confirm", + operationId: "confirmSolutionExpertReminder", + status: 200, + response: { + createdCount: 0, + skippedCount: 1, + }, + request: { + token: "opaque-signed-example-token", + }, + }, + { + name: "solution_reminder_confirm_bad_token", + operationId: "confirmSolutionExpertReminder", + status: 400, + response: { + code: "validation_error", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + request: { + token: "bad-token", + }, + }, + { + name: "solution_reminder_confirm_cross_actor", + operationId: "confirmSolutionExpertReminder", + status: 400, + response: { + code: "validation_error", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + request: { + token: "another-actor-token", + }, + }, + { + name: "published_solutions_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_solutions_empty", + operationId: "getPublishedSolutions", + status: 200, + response: { + programId: 14, + count: 0, + next: null, + previous: null, + results: [], + }, + }, + { + name: "published_solutions_search_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_solution_zero_false_file", + 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_solution_saved_partial_without_submit", + 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: [], + team: { + members: [ + { + user: { + id: 11, + firstName: "Иван", + lastName: "Токарев", + avatarUrl: null, + }, + role: "Капитан", + specialization: null, + isCaptain: true, + }, + ], + acceptedCount: 1, + }, + }, + }, + { + name: "published_solution_empty_form_captain_only", + operationId: "getPublishedSolution", + status: 200, + response: { + application: { + kind: "solution", + programId: 14, + solutionId: 501, + name: "Прототип сервиса", + }, + publishedAt: "2026-10-25T06:00:00Z", + fields: [], + answers: [], + team: { + members: [ + { + user: { + id: 11, + firstName: "Иван", + lastName: "Токарев", + avatarUrl: null, + }, + role: "Капитан", + specialization: null, + isCaptain: true, + }, + ], + acceptedCount: 1, + }, + }, + }, + { + name: "published_solution_from_immutable_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_solution_list_legacy", + operationId: "getPublishedSolutions", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_solution_list_competitive_pending_rule", + operationId: "getPublishedSolutions", + status: 200, + response: { + programId: 14, + count: 0, + next: null, + previous: null, + results: [], + }, + }, + { + name: "published_solution_list_missing_program", + operationId: "getPublishedSolutions", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_solution_list_invalid_query", + operationId: "getPublishedSolutions", + status: 400, + response: { + code: "validation_error", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_solution_detail_unpublished", + operationId: "getPublishedSolution", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_solution_detail_other_program", + operationId: "getPublishedSolution", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_solution_detail_unsafe_id", + operationId: "getPublishedSolution", + status: 404, + response: { + code: "not_found", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_solution_detail_bad_authorization", + operationId: "getPublishedSolution", + status: 401, + response: { + code: "authentication_required", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "published_solution_list_bad_authorization", + operationId: "getPublishedSolutions", + status: 401, + response: { + code: "authentication_required", + detail: "Контролируемая ошибка.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_solutions_field_present_zero", + operationId: "getSolutionManagerSolutions", + status: 200, + response: { + count: 1, + next: null, + previous: null, + submissionApplicable: true, + mode: "distributed", + results: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + captain: { + userId: 5, + fullName: "Капитан Тест", + avatar: null, + }, + caseName: "Экология", + submissionStatus: "submitted", + evaluationStatus: "awaiting_evaluation", + createdAt: "2026-10-23T00:00:00Z", + submittedAt: "2026-10-24T00:00:00Z", + assignmentsTotal: 1, + assignmentsCompleted: 0, + waitingReason: "no_completed_evaluations", + }, + ], + }, + }, + { + name: "manager_solutions_field_present_false", + operationId: "getSolutionManagerSolutions", + status: 200, + response: { + count: 1, + next: null, + previous: null, + submissionApplicable: true, + mode: "distributed", + results: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + captain: { + userId: 5, + fullName: "Капитан Тест", + avatar: null, + }, + caseName: "Экология", + submissionStatus: "submitted", + evaluationStatus: "awaiting_evaluation", + createdAt: "2026-10-23T00:00:00Z", + submittedAt: "2026-10-24T00:00:00Z", + assignmentsTotal: 1, + assignmentsCompleted: 0, + waitingReason: "no_completed_evaluations", + }, + ], + }, + }, + { + name: "manager_solutions_field_missing", + operationId: "getSolutionManagerSolutions", + status: 200, + response: { + count: 0, + next: null, + previous: null, + submissionApplicable: false, + mode: "open", + results: [], + }, + }, + { + name: "manager_solutions_field_not_applicable_snapshot", + operationId: "getSolutionManagerSolutions", + status: 200, + response: { + count: 1, + next: null, + previous: null, + submissionApplicable: true, + mode: "distributed", + results: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + captain: { + userId: 5, + fullName: "Капитан Тест", + avatar: null, + }, + caseName: "Экология", + submissionStatus: "submitted", + evaluationStatus: "awaiting_evaluation", + createdAt: "2026-10-23T00:00:00Z", + submittedAt: "2026-10-24T00:00:00Z", + assignmentsTotal: 1, + assignmentsCompleted: 0, + waitingReason: "no_completed_evaluations", + }, + ], + }, + }, + { + name: "manager_solutions_field_historical_snapshot", + operationId: "getSolutionManagerSolutions", + status: 200, + response: { + count: 1, + next: null, + previous: null, + submissionApplicable: true, + mode: "distributed", + results: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + captain: { + userId: 5, + fullName: "Капитан Тест", + avatar: null, + }, + caseName: "Экология", + submissionStatus: "submitted", + evaluationStatus: "awaiting_evaluation", + createdAt: "2026-10-23T00:00:00Z", + submittedAt: "2026-10-24T00:00:00Z", + assignmentsTotal: 1, + assignmentsCompleted: 0, + waitingReason: "no_completed_evaluations", + }, + ], + }, + }, + { + name: "manager_solutions_field_noncompetitive_empty", + operationId: "getSolutionManagerSolutions", + status: 200, + response: { + count: 0, + next: null, + previous: null, + submissionApplicable: false, + mode: "open", + results: [], + }, + }, + { + name: "manager_solutions_field_required", + operationId: "getSolutionManagerSolutions", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры фильтра.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_solutions_field_foreign_or_unknown", + operationId: "getSolutionManagerSolutions", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры фильтра.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_solutions_field_unsafe", + operationId: "getSolutionManagerSolutions", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры фильтра.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_solutions_answer_state_invalid", + operationId: "getSolutionManagerSolutions", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры фильтра.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_export_field_required", + operationId: "exportSolutionProgramAnalytics", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры фильтра.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_export_field_foreign_or_unknown", + operationId: "exportSolutionProgramAnalytics", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры фильтра.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_export_filters_unsupported_kind", + operationId: "exportSolutionProgramAnalytics", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры фильтра.", + errors: [], + meta: {}, + }, + }, + { + name: "manager_export_answer_state_invalid", + operationId: "exportSolutionProgramAnalytics", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры фильтра.", + errors: [], + meta: {}, + }, + }, + { + name: "context_creation_disabled", + operationId: "getSolutionContext", + status: 200, + response: { + programId: 12, + storageMode: "solution", + isRegistered: true, + isCompetitive: true, + submissionOpen: true, + submissionDeadline: "2026-10-30T18:00:00Z", + canCreate: false, + blockedReason: "solution_creation_disabled", + currentApplication: null, + }, + }, + { + name: "create_creation_disabled", + operationId: "createSolution", + status: 409, + request: {}, + response: { + code: "solution_creation_disabled", + detail: "Создание новых решений временно приостановлено. Ранее созданные заявки доступны.", + errors: [], + meta: {}, + }, + }, + { + name: "create_existing_creation_disabled", + operationId: "createSolution", + status: 200, + request: {}, + response: { + id: 301, + programId: 12, + version: 8, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00: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: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + 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, + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: true, + canLeave: false, + }, + readiness: { + isReady: false, + issues: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + }, + }, + }, + { + name: "solution_action_preview_ready", + operationId: "previewSolutionSubmitAction", + status: 200, + response: { + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Готовое решение", + }, + errors: [], + }, + ], + readyCount: 1, + alreadySubmittedCount: 0, + canApply: true, + previewToken: "opaque-preview-token", + }, + request: { + action: "submit", + ids: [301], + }, + }, + { + name: "solution_action_preview_incomplete", + operationId: "previewSolutionSubmitAction", + status: 200, + response: { + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Готовое решение", + }, + errors: [ + { + path: "answers", + code: "required", + fieldId: 1001, + detail: "Заполните обязательное поле.", + }, + ], + }, + ], + readyCount: 0, + alreadySubmittedCount: 0, + canApply: false, + previewToken: null, + }, + request: { + action: "submit", + ids: [301], + }, + }, + { + name: "solution_action_preview_already", + operationId: "previewSolutionSubmitAction", + status: 200, + response: { + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Готовое решение", + }, + errors: [], + }, + ], + readyCount: 0, + alreadySubmittedCount: 1, + canApply: false, + previewToken: null, + }, + request: { + action: "submit", + ids: [301], + }, + }, + { + name: "solution_action_preview_mixed", + operationId: "previewSolutionSubmitAction", + status: 200, + response: { + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Готовое решение", + }, + errors: [], + }, + { + application: { + kind: "solution", + programId: 12, + solutionId: 302, + name: "Готовое решение", + }, + errors: [], + }, + ], + readyCount: 1, + alreadySubmittedCount: 1, + canApply: true, + previewToken: "opaque-preview-token", + }, + request: { + action: "submit", + ids: [301, 302], + }, + }, + { + name: "solution_action_preview_noncompetitive", + operationId: "previewSolutionSubmitAction", + status: 409, + response: { + code: "not_competitive", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + }, + }, + { + name: "solution_action_preview_forbidden", + operationId: "previewSolutionSubmitAction", + status: 403, + response: { + code: "permission_denied", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + }, + }, + { + name: "solution_action_preview_anonymous", + operationId: "previewSolutionSubmitAction", + status: 401, + response: { + code: "authentication_required", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + }, + }, + { + name: "solution_action_preview_foreign", + operationId: "previewSolutionSubmitAction", + status: 404, + response: { + code: "not_found", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + }, + }, + { + name: "solution_action_preview_invalid", + operationId: "previewSolutionSubmitAction", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_action_preview_legacy", + operationId: "previewSolutionSubmitAction", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + }, + }, + { + name: "solution_action_apply_ready", + operationId: "applySolutionSubmitAction", + status: 200, + response: { + changedCount: 1, + alreadySubmittedCount: 0, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_mixed", + operationId: "applySolutionSubmitAction", + status: 200, + response: { + changedCount: 1, + alreadySubmittedCount: 1, + }, + request: { + action: "submit", + ids: [301, 302], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_replay", + operationId: "applySolutionSubmitAction", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_bad_signature", + operationId: "applySolutionSubmitAction", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_expired", + operationId: "applySolutionSubmitAction", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_changed_state", + operationId: "applySolutionSubmitAction", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_other_manager", + operationId: "applySolutionSubmitAction", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_forbidden", + operationId: "applySolutionSubmitAction", + status: 403, + response: { + code: "permission_denied", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_foreign", + operationId: "applySolutionSubmitAction", + status: 404, + response: { + code: "not_found", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_noncompetitive", + operationId: "applySolutionSubmitAction", + status: 409, + response: { + code: "not_competitive", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_legacy", + operationId: "applySolutionSubmitAction", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_invalid", + operationId: "applySolutionSubmitAction", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + }, + { + name: "solution_action_apply_file_invalid", + operationId: "applySolutionSubmitAction", + status: 400, + response: { + code: "invalid_answers", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "solution_action_apply_anonymous", + operationId: "applySolutionSubmitAction", + status: 401, + response: { + code: "authentication_required", + detail: "Проверьте действие.", + errors: [], + meta: {}, + }, + request: { + action: "submit", + ids: [301], + previewToken: "opaque-preview-token", + }, + }, + { + name: "case_action_preview_ready", + operationId: "previewSolutionCaseChange", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + caseFieldId: 401, + currentCase: "Город", + caseName: "Образование | наука", + solutionVersion: 7, + errors: [], + canApply: true, + previewToken: "opaque-case-preview-token", + isSubmitted: false, + removedAssignmentsCount: 0, + scoreCount: 0, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_empty_current", + operationId: "previewSolutionCaseChange", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + caseFieldId: 401, + currentCase: null, + caseName: "Образование | наука", + solutionVersion: 7, + errors: [], + canApply: true, + previewToken: "opaque-case-preview-token", + isSubmitted: false, + removedAssignmentsCount: 0, + scoreCount: 0, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_noncompetitive", + operationId: "previewSolutionCaseChange", + status: 200, + response: { + application: { + kind: "solution", + programId: 13, + solutionId: 302, + name: "Решение команды", + }, + caseFieldId: 401, + currentCase: "Город", + caseName: "Образование | наука", + solutionVersion: 7, + errors: [], + canApply: true, + previewToken: "opaque-case-preview-token", + isSubmitted: false, + removedAssignmentsCount: 0, + scoreCount: 0, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_same", + operationId: "previewSolutionCaseChange", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + caseFieldId: 401, + currentCase: "Образование | наука", + caseName: "Образование | наука", + solutionVersion: 7, + errors: [ + { + path: "case_name", + code: "case_unchanged", + detail: "Выберите другой кейс.", + fieldId: 401, + }, + ], + canApply: false, + previewToken: null, + isSubmitted: false, + removedAssignmentsCount: 0, + scoreCount: 0, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_submitted", + operationId: "previewSolutionCaseChange", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + caseFieldId: 401, + currentCase: "Город", + caseName: "Образование | наука", + solutionVersion: 7, + errors: [], + canApply: true, + previewToken: "opaque-submitted-case-preview-token", + isSubmitted: true, + removedAssignmentsCount: 0, + scoreCount: 0, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_evaluation", + operationId: "previewSolutionCaseChange", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + caseFieldId: 401, + currentCase: "Город", + caseName: "Образование | наука", + solutionVersion: 7, + errors: [ + { + path: "case_name", + code: "evaluation_started", + detail: "У решения уже есть данные оценивания.", + fieldId: 401, + }, + ], + canApply: false, + previewToken: null, + isSubmitted: false, + removedAssignmentsCount: 0, + scoreCount: 0, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_bad_case", + operationId: "previewSolutionCaseChange", + status: 400, + response: { + code: "invalid_answers", + detail: "Выберите актуальный кейс.", + errors: [ + { + path: "case_name", + code: "case_unavailable", + detail: "Вариант недоступен.", + fieldId: 401, + }, + ], + meta: {}, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_anonymous", + operationId: "previewSolutionCaseChange", + status: 401, + response: { + code: "authentication_required", + detail: "Войдите в аккаунт.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_foreign_manager", + operationId: "previewSolutionCaseChange", + status: 403, + response: { + code: "permission_denied", + detail: "Действие доступно организатору программы.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_foreign_solution", + operationId: "previewSolutionCaseChange", + status: 404, + response: { + code: "not_found", + detail: "Решение не найдено.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_legacy", + operationId: "previewSolutionCaseChange", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Программа использует проектную заявку.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_preview_unconfigured", + operationId: "previewSolutionCaseChange", + status: 409, + response: { + code: "form_not_configured", + detail: "Настройте форму решения.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_apply_changed", + operationId: "applySolutionCaseChange", + status: 200, + response: { + solutionId: 301, + solutionVersion: 8, + updatedAt: "2026-10-25T10:00:00Z", + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_stale", + operationId: "applySolutionCaseChange", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Данные решения изменились. Повторите предпросмотр.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_replay", + operationId: "applySolutionCaseChange", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Данные решения изменились. Повторите предпросмотр.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_expired", + operationId: "applySolutionCaseChange", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Данные решения изменились. Повторите предпросмотр.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_cross_actor", + operationId: "applySolutionCaseChange", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Данные решения изменились. Повторите предпросмотр.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_cross_solution", + operationId: "applySolutionCaseChange", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Данные решения изменились. Повторите предпросмотр.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_cross_case", + operationId: "applySolutionCaseChange", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Данные решения изменились. Повторите предпросмотр.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_submitted", + operationId: "applySolutionCaseChange", + status: 200, + response: { + solutionId: 301, + solutionVersion: 8, + updatedAt: "2026-10-25T12:00:00Z", + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_bad_case", + operationId: "applySolutionCaseChange", + status: 400, + response: { + code: "invalid_answers", + detail: "Выберите актуальный кейс.", + errors: [ + { + path: "case_name", + code: "case_unavailable", + detail: "Вариант недоступен.", + fieldId: 401, + }, + ], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_foreign_manager", + operationId: "applySolutionCaseChange", + status: 403, + response: { + code: "permission_denied", + detail: "Действие доступно организатору программы.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "case_action_apply_invalid_payload", + operationId: "applySolutionCaseChange", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные запроса.", + errors: [], + meta: {}, + }, + }, + { + name: "case_action_apply_unknown_query", + operationId: "applySolutionCaseChange", + status: 400, + response: { + code: "validation_error", + detail: "Неизвестный параметр.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "opaque-case-preview-token", + }, + }, + { + name: "invitation_role_one_character", + operationId: "createSolutionInvitation", + status: 201, + request: { + expectedSolutionVersion: 8, + recipientUserId: 9, + role: "Р", + }, + response: { + invitation: { + id: 701, + version: 1, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "Р", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "pending", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: true, + canRevoke: true, + canAccept: false, + canDecline: false, + }, + }, + solutionVersion: 9, + }, + }, + { + name: "invitation_role_maximum", + operationId: "createSolutionInvitation", + status: 201, + request: { + expectedSolutionVersion: 8, + recipientUserId: 9, + role: "РРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРР", + }, + response: { + invitation: { + id: 701, + version: 1, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "РРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРРР", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "pending", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: true, + canRevoke: true, + canAccept: false, + canDecline: false, + }, + }, + solutionVersion: 9, + }, + }, + { + name: "invitation_missing_role", + operationId: "createSolutionInvitation", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте поля приглашения.", + errors: [ + { + path: "role", + code: "invalid", + detail: "Укажите роль от 1 до 128 символов.", + fieldId: null, + }, + ], + meta: {}, + }, + }, + { + name: "invitation_blank_role", + operationId: "createSolutionInvitation", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте поля приглашения.", + errors: [ + { + path: "role", + code: "invalid", + detail: "Укажите роль от 1 до 128 символов.", + fieldId: null, + }, + ], + meta: {}, + }, + }, + { + name: "invitation_null_role", + operationId: "createSolutionInvitation", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте поля приглашения.", + errors: [ + { + path: "role", + code: "invalid", + detail: "Укажите роль от 1 до 128 символов.", + fieldId: null, + }, + ], + meta: {}, + }, + }, + { + name: "invitation_update_null_role", + operationId: "updateSolutionInvitation", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте поля приглашения.", + errors: [ + { + path: "role", + code: "invalid", + detail: "Укажите роль от 1 до 128 символов.", + fieldId: null, + }, + ], + meta: {}, + }, + }, + { + name: "invitation_legacy_null_role", + operationId: "getSolutionInvitation", + status: 200, + response: { + id: 701, + version: 1, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: null, + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "pending", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: true, + canRevoke: true, + canAccept: false, + canDecline: false, + }, + }, + }, + { + name: "invitation_legacy_null_role_accept", + operationId: "acceptSolutionInvitation", + status: 200, + request: { + expectedVersion: 1, + }, + response: { + invitation: { + id: 701, + version: 2, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: null, + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "accepted", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: false, + canRevoke: false, + canAccept: false, + canDecline: false, + }, + }, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + solutionVersion: 9, + }, + }, + { + name: "case_action_preview_submitted_assignment_effects", + operationId: "previewSolutionCaseChange", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + caseFieldId: 401, + currentCase: "Город", + caseName: "Образование | наука", + solutionVersion: 7, + errors: [], + canApply: true, + previewToken: "opaque-submitted-case-preview-token", + isSubmitted: true, + removedAssignmentsCount: 1, + scoreCount: 2, + }, + request: { + caseName: "Образование | наука", + }, + }, + { + name: "case_action_apply_submitted_replay", + operationId: "applySolutionCaseChange", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Данные решения изменились. Подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + caseName: "Образование | наука", + previewToken: "used-case-preview-token", + }, + }, + { + name: "admin_history_list", + operationId: "listSolutionAdminChanges", + status: 200, + response: { + count: 1, + offset: 0, + limit: 20, + results: [ + { + id: 71, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + action: "change_case", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + caseFieldId: 1001, + previousCase: "Цифровой сервис", + caseName: "Образование | наука", + removedAssignmentsCount: 1, + scoreCount: 2, + wasSubmitted: true, + }, + ], + }, + query: { + offset: 0, + limit: 20, + }, + }, + { + name: "admin_history_detail", + operationId: "readSolutionAdminChange", + status: 200, + response: { + change: { + id: 71, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + action: "change_case", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + caseFieldId: 1001, + previousCase: "Цифровой сервис", + caseName: "Образование | наука", + removedAssignmentsCount: 1, + scoreCount: 2, + wasSubmitted: true, + }, + beforeSnapshot: { + name: "Сервис для студентов", + captainId: 7, + 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, + }, + ], + 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", + }, + }, + ], + submittedAt: "2026-10-23T10:05:00Z", + }, + afterSnapshot: { + name: "Сервис для студентов", + captainId: 7, + 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, + }, + ], + 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", + }, + }, + ], + submittedAt: "2026-10-23T10:05:00Z", + }, + }, + }, + { + name: "admin_history_draft_detail", + operationId: "readSolutionAdminChange", + status: 200, + response: { + change: { + id: 72, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + action: "change_case", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + caseFieldId: 1001, + previousCase: "Цифровой сервис", + caseName: "Образование | наука", + removedAssignmentsCount: 0, + scoreCount: 0, + wasSubmitted: false, + }, + beforeSnapshot: null, + afterSnapshot: null, + }, + }, + { + name: "admin_history_empty", + operationId: "listSolutionAdminChanges", + status: 200, + response: { + count: 0, + offset: 100, + limit: 20, + results: [], + }, + query: { + offset: 100, + limit: 20, + }, + }, + { + name: "admin_history_anonymous", + operationId: "listSolutionAdminChanges", + status: 401, + response: { + code: "authentication_required", + detail: "Требуется авторизация.", + errors: [], + meta: {}, + }, + }, + { + name: "admin_history_foreign_manager", + operationId: "listSolutionAdminChanges", + status: 403, + response: { + code: "permission_denied", + detail: "История доступна организатору программы.", + errors: [], + meta: {}, + }, + }, + { + name: "admin_history_foreign_solution", + operationId: "listSolutionAdminChanges", + status: 404, + response: { + code: "not_found", + detail: "Решение не найдено.", + errors: [], + meta: {}, + }, + }, + { + name: "admin_history_foreign_change", + operationId: "readSolutionAdminChange", + status: 404, + response: { + code: "not_found", + detail: "Изменение не найдено.", + errors: [], + meta: {}, + }, + }, + { + name: "admin_history_invalid_pagination", + operationId: "listSolutionAdminChanges", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте параметры запроса.", + errors: [], + meta: {}, + }, + query: { + limit: 0, + }, + }, + { + name: "admin_history_unknown_detail_query", + operationId: "readSolutionAdminChange", + status: 400, + response: { + code: "validation_error", + detail: "Неизвестный параметр запроса.", + errors: [], + meta: {}, + }, + query: { + all: true, + }, + }, + { + name: "admin_history_legacy", + operationId: "listSolutionAdminChanges", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Программа использует заявки на основе проектов.", + errors: [], + meta: {}, + }, + }, + { + name: "member_removal_preview_submitted", + operationId: "previewSolutionMemberRemoval", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + member: { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + solutionVersion: 7, + isSubmitted: true, + remainingMemberCount: 1, + revokedInvitationCount: 0, + scoreCount: 2, + errors: [], + canApply: true, + previewToken: "opaque-member-removal-preview-token", + }, + request: { + userId: 10, + }, + }, + { + name: "member_removal_preview_draft_pending", + operationId: "previewSolutionMemberRemoval", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + member: { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + solutionVersion: 7, + isSubmitted: false, + remainingMemberCount: 1, + revokedInvitationCount: 1, + scoreCount: 0, + errors: [], + canApply: true, + previewToken: "opaque-member-removal-preview-token", + }, + request: { + userId: 10, + }, + }, + { + name: "member_removal_preview_captain", + operationId: "previewSolutionMemberRemoval", + status: 200, + response: { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + member: { + user: { + id: 9, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: true, + }, + solutionVersion: 7, + isSubmitted: true, + remainingMemberCount: 1, + revokedInvitationCount: 0, + scoreCount: 2, + errors: [ + { + path: "user_id", + code: "captain_cannot_be_removed", + detail: "Капитана нельзя исключить из состава. Для него снимите заявку с участия.", + fieldId: null, + }, + ], + canApply: false, + previewToken: null, + }, + request: { + userId: 9, + }, + }, + { + name: "member_removal_apply_submitted", + operationId: "applySolutionMemberRemoval", + status: 200, + response: { + solutionId: 301, + solutionVersion: 8, + updatedAt: "2026-10-25T15:00:00Z", + }, + request: { + userId: 10, + previewToken: "opaque-member-removal-preview-token", + }, + }, + { + name: "member_removal_anonymous", + operationId: "previewSolutionMemberRemoval", + status: 401, + response: { + code: "authentication_required", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + }, + }, + { + name: "member_removal_captain_denied", + operationId: "previewSolutionMemberRemoval", + status: 403, + response: { + code: "permission_denied", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + }, + }, + { + name: "member_removal_foreign_manager", + operationId: "previewSolutionMemberRemoval", + status: 403, + response: { + code: "permission_denied", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + }, + }, + { + name: "member_removal_foreign_solution", + operationId: "previewSolutionMemberRemoval", + status: 404, + response: { + code: "not_found", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + }, + }, + { + name: "member_removal_missing_member", + operationId: "previewSolutionMemberRemoval", + status: 404, + response: { + code: "not_found", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + }, + }, + { + name: "member_removal_legacy", + operationId: "previewSolutionMemberRemoval", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + }, + }, + { + name: "member_removal_replay", + operationId: "applySolutionMemberRemoval", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + previewToken: "opaque-member-removal-preview-token", + }, + }, + { + name: "member_removal_stale", + operationId: "applySolutionMemberRemoval", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + previewToken: "opaque-member-removal-preview-token", + }, + }, + { + name: "member_removal_expired", + operationId: "applySolutionMemberRemoval", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + previewToken: "opaque-member-removal-preview-token", + }, + }, + { + name: "member_removal_changed_target", + operationId: "applySolutionMemberRemoval", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + previewToken: "opaque-member-removal-preview-token", + }, + }, + { + name: "member_removal_missing_token", + operationId: "applySolutionMemberRemoval", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + invalidRequest: { + userId: 10, + }, + }, + { + name: "member_removal_invalid_user", + operationId: "previewSolutionMemberRemoval", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + invalidRequest: { + userId: 0, + }, + }, + { + name: "member_removal_unknown_query", + operationId: "previewSolutionMemberRemoval", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + userId: 10, + }, + query: { + all: true, + }, + }, + { + name: "member_removal_history_detail", + operationId: "readSolutionAdminChange", + status: 200, + response: { + change: { + id: 73, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + action: "remove_member", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + removedAssignmentsCount: 0, + scoreCount: 2, + wasSubmitted: true, + removedMember: { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + revokedInvitationCount: 0, + }, + beforeSnapshot: { + name: "Сервис для студентов", + captainId: 7, + 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, + }, + { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + ], + 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", + }, + }, + ], + submittedAt: "2026-10-23T10:05:00Z", + }, + afterSnapshot: { + name: "Сервис для студентов", + captainId: 7, + 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, + }, + ], + 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", + }, + }, + ], + submittedAt: "2026-10-23T10:05:00Z", + }, + }, + }, + { + name: "member_removal_history_draft", + operationId: "readSolutionAdminChange", + status: 200, + response: { + change: { + id: 73, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + action: "remove_member", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + removedAssignmentsCount: 0, + scoreCount: 0, + wasSubmitted: false, + removedMember: { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + revokedInvitationCount: 1, + }, + beforeSnapshot: null, + afterSnapshot: null, + }, + }, + { + name: "member_removal_history_mixed_list", + operationId: "listSolutionAdminChanges", + status: 200, + response: { + count: 2, + offset: 0, + limit: 20, + results: [ + { + id: 73, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + action: "remove_member", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + removedAssignmentsCount: 0, + scoreCount: 2, + wasSubmitted: true, + removedMember: { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + revokedInvitationCount: 0, + }, + { + id: 71, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + action: "change_case", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + caseFieldId: 1001, + previousCase: "Цифровой сервис", + caseName: "Образование | наука", + removedAssignmentsCount: 1, + scoreCount: 2, + wasSubmitted: true, + }, + ], + }, + }, + { + name: "archive_preview_submitted", + operationId: "previewSolutionArchive", + status: 200, + response: { + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 7, + isSubmitted: true, + isArchived: false, + scoreCount: 2, + removedAssignmentsCount: 2, + revokedInvitationCount: 1, + errors: [], + }, + ], + scoreCount: 2, + removedAssignmentsCount: 2, + revokedInvitationCount: 1, + canApply: true, + previewToken: "opaque-archive-preview-token", + }, + request: { + ids: [301], + }, + }, + { + name: "archive_preview_draft", + operationId: "previewSolutionArchive", + status: 200, + response: { + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 7, + isSubmitted: false, + isArchived: false, + scoreCount: 0, + removedAssignmentsCount: 0, + revokedInvitationCount: 1, + errors: [], + }, + ], + scoreCount: 0, + removedAssignmentsCount: 0, + revokedInvitationCount: 1, + canApply: true, + previewToken: "opaque-archive-preview-token", + }, + request: { + ids: [301], + }, + }, + { + name: "archive_preview_already_archived", + operationId: "previewSolutionArchive", + status: 200, + response: { + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 7, + isSubmitted: true, + isArchived: true, + scoreCount: 2, + removedAssignmentsCount: 0, + revokedInvitationCount: 0, + errors: [ + { + path: "ids", + code: "solution_archived", + fieldId: null, + detail: "Решение уже снято с участия.", + }, + ], + }, + ], + scoreCount: 2, + removedAssignmentsCount: 0, + revokedInvitationCount: 0, + canApply: false, + previewToken: null, + }, + request: { + ids: [301], + }, + }, + { + name: "archive_apply", + operationId: "applySolutionArchive", + status: 200, + response: { + changedCount: 1, + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 8, + archivedAt: "2026-10-25T15:00:00Z", + }, + ], + }, + request: { + ids: [301], + previewToken: "opaque-archive-preview-token", + }, + }, + { + name: "archive_anonymous", + operationId: "previewSolutionArchive", + status: 401, + response: { + code: "authentication_required", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + ids: [301], + }, + }, + { + name: "archive_captain_denied", + operationId: "previewSolutionArchive", + status: 403, + response: { + code: "permission_denied", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + ids: [301], + }, + }, + { + name: "archive_foreign_manager", + operationId: "previewSolutionArchive", + status: 403, + response: { + code: "permission_denied", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + ids: [301], + }, + }, + { + name: "archive_foreign_solution", + operationId: "previewSolutionArchive", + status: 404, + response: { + code: "not_found", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + ids: [301], + }, + }, + { + name: "archive_legacy", + operationId: "previewSolutionArchive", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + ids: [301], + }, + }, + { + name: "archive_replay", + operationId: "applySolutionArchive", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + ids: [301], + previewToken: "opaque-archive-preview-token", + }, + }, + { + name: "archive_stale", + operationId: "applySolutionArchive", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + ids: [301], + previewToken: "opaque-archive-preview-token", + }, + }, + { + name: "archive_expired", + operationId: "applySolutionArchive", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + request: { + ids: [301], + previewToken: "opaque-archive-preview-token", + }, + }, + { + name: "archive_invalid_empty_selection", + operationId: "previewSolutionArchive", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + invalidRequest: { + ids: [], + }, + }, + { + name: "archive_invalid_duplicate_selection", + operationId: "previewSolutionArchive", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + invalidRequest: { + ids: [301, 301], + }, + }, + { + name: "archive_missing_token", + operationId: "applySolutionArchive", + status: 400, + response: { + code: "validation_error", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + invalidRequest: { + ids: [301], + }, + }, + { + name: "archive_read_submitted_owner", + operationId: "getSolution", + status: 200, + response: { + id: 301, + programId: 12, + version: 11, + 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: 0, + reservedCount: 2, + 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: false, + issues: [ + { + path: "participation", + code: "solution_archived", + fieldId: null, + detail: "Решение снято с участия.", + }, + ], + }, + participation: { + status: "archived", + archivedAt: "2026-10-25T15:00:00Z", + reason: "detached", + }, + }, + }, + { + name: "archive_read_draft_owner", + operationId: "getSolution", + status: 200, + response: { + id: 301, + programId: 12, + version: 11, + 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: 0, + reservedCount: 2, + 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: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: false, + canManageTeam: false, + canInvite: false, + canSubmit: false, + canLeave: false, + }, + readiness: { + isReady: false, + issues: [ + { + path: "participation", + code: "solution_archived", + fieldId: null, + detail: "Решение снято с участия.", + }, + ], + }, + participation: { + status: "archived", + archivedAt: "2026-10-25T15:00:00Z", + reason: "detached", + }, + }, + }, + { + name: "archive_list_owned", + operationId: "listSolutions", + status: 200, + response: { + count: 1, + next: null, + previous: null, + results: [ + { + id: 301, + programId: 12, + name: "Сервис для студентов", + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + acceptedCount: 2, + caseValue: "Цифровой сервис", + status: "draft", + submittedAt: null, + participation: { + status: "archived", + archivedAt: "2026-10-25T15:00:00Z", + reason: "detached", + }, + }, + ], + }, + query: { + participation: "archived", + }, + }, + { + name: "archive_default_active_empty", + operationId: "listSolutions", + status: 200, + response: { + count: 0, + next: null, + previous: null, + results: [], + }, + }, + { + name: "archive_owner_patch_frozen", + operationId: "updateSolution", + status: 409, + response: { + code: "solution_archived", + detail: "Проверьте данные и подтвердите действие заново.", + errors: [], + meta: {}, + }, + }, + { + name: "archive_history_detail", + operationId: "readSolutionAdminChange", + status: 200, + response: { + change: { + id: 71, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + action: "archive", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T15:00:00Z", + beforeVersion: 7, + afterVersion: 8, + removedAssignmentsCount: 2, + scoreCount: 2, + wasSubmitted: true, + archivedAt: "2026-10-25T15:00:00Z", + reason: "detached", + revokedInvitationCount: 1, + }, + beforeSnapshot: { + name: "Сервис для студентов", + captainId: 7, + 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, + }, + ], + 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", + }, + }, + ], + submittedAt: "2026-10-23T10:05:00Z", + }, + afterSnapshot: { + name: "Сервис для студентов", + captainId: 7, + 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, + }, + ], + 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", + }, + }, + ], + submittedAt: "2026-10-23T10:05:00Z", + }, + archiveSnapshot: { + name: "Сервис для студентов", + captainId: 7, + 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, + }, + ], + 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", + }, + }, + ], + capturedAt: "2026-10-25T15:00:00Z", + }, + }, + }, + { + name: "archive_history_draft", + operationId: "readSolutionAdminChange", + status: 200, + response: { + change: { + id: 71, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + action: "archive", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T15:00:00Z", + beforeVersion: 7, + afterVersion: 8, + removedAssignmentsCount: 0, + scoreCount: 0, + wasSubmitted: false, + archivedAt: "2026-10-25T15:00:00Z", + reason: "detached", + revokedInvitationCount: 1, + }, + beforeSnapshot: null, + afterSnapshot: null, + archiveSnapshot: { + name: "Сервис для студентов", + captainId: 7, + 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, + }, + ], + 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", + }, + }, + ], + capturedAt: "2026-10-25T15:00:00Z", + }, + }, + }, + { + name: "registration_removal_preview_member_submitted", + operationId: "previewSolutionRegistrationRemoval", + status: 200, + response: { + registrations: [ + { + registrationId: 601, + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + }, + ], + effects: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 7, + isSubmitted: true, + effect: "remove_members", + removedMembers: [ + { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + ], + removedAssignmentsCount: 0, + revokedInvitationCount: 1, + scoreCount: 2, + }, + ], + deletedCount: 1, + archivedCount: 0, + removedMemberCount: 1, + removedAssignmentsCount: 0, + revokedInvitationCount: 1, + scoreCount: 2, + canApply: true, + errors: [], + previewToken: "opaque-registration-removal-proof", + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_preview_pending_only", + operationId: "previewSolutionRegistrationRemoval", + status: 200, + response: { + registrations: [ + { + registrationId: 601, + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + }, + ], + effects: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 7, + isSubmitted: true, + effect: "revoke_invitations", + removedMembers: [], + removedAssignmentsCount: 0, + revokedInvitationCount: 1, + scoreCount: 2, + }, + ], + deletedCount: 1, + archivedCount: 0, + removedMemberCount: 0, + removedAssignmentsCount: 0, + revokedInvitationCount: 1, + scoreCount: 2, + canApply: true, + errors: [], + previewToken: "opaque-registration-removal-proof", + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_preview_captain_submitted", + operationId: "previewSolutionRegistrationRemoval", + status: 200, + response: { + registrations: [ + { + registrationId: 601, + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + }, + ], + effects: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 7, + isSubmitted: true, + effect: "archive", + removedMembers: [], + removedAssignmentsCount: 2, + revokedInvitationCount: 1, + scoreCount: 2, + }, + ], + deletedCount: 1, + archivedCount: 1, + removedMemberCount: 0, + removedAssignmentsCount: 2, + revokedInvitationCount: 1, + scoreCount: 2, + canApply: true, + errors: [], + previewToken: "opaque-registration-removal-proof", + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_preview_member_draft", + operationId: "previewSolutionRegistrationRemoval", + status: 200, + response: { + registrations: [ + { + registrationId: 601, + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + }, + ], + effects: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 7, + isSubmitted: false, + effect: "remove_members", + removedMembers: [ + { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + ], + removedAssignmentsCount: 0, + revokedInvitationCount: 1, + scoreCount: 0, + }, + ], + deletedCount: 1, + archivedCount: 0, + removedMemberCount: 1, + removedAssignmentsCount: 0, + revokedInvitationCount: 1, + scoreCount: 0, + canApply: true, + errors: [], + previewToken: "opaque-registration-removal-proof", + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_preview_without_solution", + operationId: "previewSolutionRegistrationRemoval", + status: 200, + response: { + registrations: [ + { + registrationId: 601, + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + }, + ], + effects: [], + deletedCount: 1, + archivedCount: 0, + removedMemberCount: 0, + removedAssignmentsCount: 0, + revokedInvitationCount: 0, + scoreCount: 0, + canApply: true, + errors: [], + previewToken: "opaque-registration-removal-proof", + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_apply_member", + operationId: "applySolutionRegistrationRemoval", + status: 200, + response: { + auditId: 801, + deletedCount: 1, + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 8, + changeId: 901, + action: "remove_registrations", + }, + ], + }, + request: { + registrationIds: [601], + previewToken: "opaque-registration-removal-proof", + }, + }, + { + name: "registration_removal_apply_captain", + operationId: "applySolutionRegistrationRemoval", + status: 200, + response: { + auditId: 801, + deletedCount: 1, + items: [ + { + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + solutionVersion: 8, + changeId: 901, + action: "archive", + }, + ], + }, + request: { + registrationIds: [601], + previewToken: "opaque-registration-removal-proof", + }, + }, + { + name: "registration_removal_apply_without_solution", + operationId: "applySolutionRegistrationRemoval", + status: 200, + response: { + auditId: 801, + deletedCount: 1, + items: [], + }, + request: { + registrationIds: [601], + previewToken: "opaque-registration-removal-proof", + }, + }, + { + name: "registration_removal_audit_list", + operationId: "listSolutionRegistrationRemovals", + status: 200, + response: { + count: 1, + offset: 0, + limit: 20, + results: [ + { + id: 801, + programId: 12, + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + registrations: [ + { + registrationId: 601, + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + }, + ], + deletedCount: 1, + }, + ], + }, + }, + { + name: "registration_removal_audit_detail", + operationId: "readSolutionRegistrationRemoval", + status: 200, + response: { + audit: { + id: 801, + programId: 12, + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + registrations: [ + { + registrationId: 601, + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + }, + ], + deletedCount: 1, + }, + changes: [ + { + id: 73, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + action: "remove_registrations", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + removedAssignmentsCount: 0, + scoreCount: 0, + wasSubmitted: false, + revokedInvitationCount: 1, + removedMembers: [ + { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + ], + registrationRemovalId: 801, + }, + ], + }, + }, + { + name: "registration_removal_audit_no_solution", + operationId: "readSolutionRegistrationRemoval", + status: 200, + response: { + audit: { + id: 801, + programId: 12, + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + registrations: [ + { + registrationId: 601, + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + }, + ], + deletedCount: 1, + }, + changes: [], + }, + }, + { + name: "registration_removal_solution_history", + operationId: "readSolutionAdminChange", + status: 200, + response: { + change: { + id: 73, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение команды", + }, + action: "remove_registrations", + actor: { + id: 8, + firstName: "Организатор", + lastName: "Программы", + avatarUrl: null, + }, + actorName: "Организатор Программы", + createdAt: "2026-10-25T12:00:00Z", + beforeVersion: 7, + afterVersion: 8, + removedAssignmentsCount: 0, + scoreCount: 0, + wasSubmitted: false, + revokedInvitationCount: 1, + removedMembers: [ + { + user: { + id: 10, + firstName: "Участник", + lastName: "Команды", + avatarUrl: null, + }, + role: "Backend-разработчик", + specialization: null, + joinedAt: "2026-10-24T10:00:00Z", + isCaptain: false, + }, + ], + registrationRemovalId: 801, + }, + beforeSnapshot: null, + afterSnapshot: null, + }, + }, + { + name: "registration_removal_stale", + operationId: "applySolutionRegistrationRemoval", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Срок сдачи завершен.", + errors: [], + meta: {}, + }, + request: { + registrationIds: [601], + previewToken: "opaque-registration-removal-proof", + }, + }, + { + name: "registration_removal_replay", + operationId: "applySolutionRegistrationRemoval", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Срок сдачи завершен.", + errors: [], + meta: {}, + }, + request: { + registrationIds: [601], + previewToken: "opaque-registration-removal-proof", + }, + }, + { + name: "registration_removal_expired", + operationId: "applySolutionRegistrationRemoval", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Срок сдачи завершен.", + errors: [], + meta: {}, + }, + request: { + registrationIds: [601], + previewToken: "opaque-registration-removal-proof", + }, + }, + { + name: "registration_removal_other_actor", + operationId: "applySolutionRegistrationRemoval", + status: 409, + response: { + code: "solution_action_conflict", + detail: "Срок сдачи завершен.", + errors: [], + meta: {}, + }, + request: { + registrationIds: [601], + previewToken: "opaque-registration-removal-proof", + }, + }, + { + name: "registration_removal_legacy_mode", + operationId: "previewSolutionRegistrationRemoval", + status: 409, + response: { + code: "solution_mode_unavailable", + detail: "Срок сдачи завершен.", + errors: [], + meta: {}, + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_mixed_legacy", + operationId: "previewSolutionRegistrationRemoval", + status: 409, + response: { + code: "legacy_application_conflict", + detail: "Срок сдачи завершен.", + errors: [], + meta: {}, + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_foreign_registration", + operationId: "previewSolutionRegistrationRemoval", + status: 404, + response: { + code: "not_found", + detail: "Решение или программа не найдены.", + errors: [], + meta: {}, + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_participant", + operationId: "previewSolutionRegistrationRemoval", + status: 403, + response: { + code: "permission_denied", + detail: "Сдать решение может только капитан.", + errors: [], + meta: {}, + }, + request: { + registrationIds: [601], + }, + }, + { + name: "registration_removal_foreign_audit", + operationId: "readSolutionRegistrationRemoval", + status: 404, + response: { + code: "not_found", + detail: "Решение или программа не найдены.", + errors: [], + meta: {}, + }, + }, + { + name: "registration_removal_invalid_empty", + operationId: "previewSolutionRegistrationRemoval", + status: 400, + response: { + code: "required_program_fields", + detail: "Заполните обязательные поля.", + errors: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + meta: {}, + }, + invalidRequest: { + registrationIds: [], + }, + }, + { + name: "registration_removal_invalid_duplicate", + operationId: "previewSolutionRegistrationRemoval", + status: 400, + response: { + code: "required_program_fields", + detail: "Заполните обязательные поля.", + errors: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + meta: {}, + }, + invalidRequest: { + registrationIds: [601, 601], + }, + }, + { + name: "registration_removal_invalid_user_id_instead", + operationId: "previewSolutionRegistrationRemoval", + status: 400, + response: { + code: "required_program_fields", + detail: "Заполните обязательные поля.", + errors: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + meta: {}, + }, + invalidRequest: { + userId: 10, + }, + }, + { + name: "registration_removal_invalid_unknown", + operationId: "previewSolutionRegistrationRemoval", + status: 400, + response: { + code: "required_program_fields", + detail: "Заполните обязательные поля.", + errors: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + meta: {}, + }, + invalidRequest: { + registrationIds: [601], + projectId: 1, + }, + }, + { + name: "rate_archived_solution_open", + 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: "solution_archived", + detail: "Решение снято с участия. Материалы доступны для просмотра.", + errors: [], + meta: {}, + }, + }, + { + name: "rate_archived_solution_distributed", + 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: "solution_archived", + detail: "Решение снято с участия. Материалы доступны для просмотра.", + errors: [], + 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 946f7e56e..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.0.0. +// Сгенерировано tools/generate-solution-contract.py из C00 1.21.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 = { @@ -191,6 +192,7 @@ export type SolutionPermissions = { canManageTeam: boolean; canInvite: boolean; canSubmit: boolean; + canLeave?: boolean; }; export type SolutionSubmission = { @@ -220,6 +222,7 @@ export type ProgramSolution = { submission: SolutionSubmission; permissions: SolutionPermissions; readiness: SolutionReadiness | null; + participation?: SolutionParticipation; }; export type SolutionSummary = { @@ -231,13 +234,26 @@ export type SolutionSummary = { caseValue: string | null; status: "draft" | "submitted"; submittedAt: string | null; + participation?: SolutionParticipation; }; 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; @@ -279,12 +295,33 @@ export type SolutionInvitation = { export type SolutionCreateInvitation = { expectedSolutionVersion: number; recipientUserId: number; - role?: string | null; + role: string; specialization?: string | null; motivationalLetter?: string | null; }; -export type SolutionUpdateInvitation = unknown | unknown | unknown; +export type SolutionUpdateInvitation = + | { + expectedVersion: number; + expectedSolutionVersion: number; + role: string; + specialization?: string | null; + motivationalLetter?: string | null; + } + | { + expectedVersion: number; + expectedSolutionVersion: number; + role?: string; + specialization: string | null; + motivationalLetter?: string | null; + } + | { + expectedVersion: number; + expectedSolutionVersion: number; + role?: string; + specialization?: string | null; + motivationalLetter: string | null; + }; export type SolutionInvitationAction = { expectedVersion: number; @@ -333,3 +370,934 @@ 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; + evaluationCounts?: { + pending: number; + completed: number; + }; + caseOptions?: 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; +}; + +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; +}; + +export type SolutionWidgetProjectRef = { + id: number; + name: string; + programLinkId: number; +}; + +export type SolutionWidgetStage = + | "none" + | "not_submitted" + | "submitted" + | "review" + | "evaluated" + | "not_applicable"; + +export type SolutionWidgetLegacyParticipant = { + participantProject: SolutionWidgetProjectRef | null; + caseProvided: boolean; + caseName: string | null; + stage: SolutionWidgetStage; + submissionOpen: boolean; +}; + +export type SolutionWidgetParticipant = { + application: SolutionRef | null; + caseProvided: boolean; + caseName: string | null; + stage: SolutionWidgetStage; + submissionOpen: boolean; +}; + +export type SolutionWidgetLegacyOrganizer = { + participants: number; + projects: number; + submittedSolutions: number | null; + participantsWithoutProject: number; +}; + +export type SolutionWidgetOrganizer = { + participants: number; + solutions: number; + submittedSolutions: number | null; + participantsWithoutSolution: number; +}; + +export type SolutionWidgetExpert = { + mode: "open" | "distributed"; + assigned: number | null; + remaining: number | null; + evaluationEnds: string | null; +}; + +export type SolutionWidget = + | { + programId: number; + role: "participant"; + isCompetitive: boolean; + storageMode: "solution"; + participant: SolutionWidgetParticipant; + } + | { + programId: number; + role: "organizer"; + isCompetitive: boolean; + storageMode: "solution"; + organizer: SolutionWidgetOrganizer; + } + | { + programId: number; + role: "expert"; + isCompetitive: boolean; + storageMode: "solution"; + expert: SolutionWidgetExpert; + }; + +export type SolutionLegacyWidget = + | { + programId: number; + role: "participant"; + isCompetitive: boolean; + storageMode?: "legacy_project"; + participant: SolutionWidgetLegacyParticipant; + } + | { + programId: number; + role: "organizer"; + isCompetitive: boolean; + storageMode?: "legacy_project"; + organizer: SolutionWidgetLegacyOrganizer; + } + | { + programId: number; + role: "expert"; + isCompetitive: boolean; + storageMode?: "legacy_project"; + expert: SolutionWidgetExpert; + }; + +export type SolutionRoleWidget = SolutionWidget | SolutionLegacyWidget; + +export type SolutionWidgetError = { + detail: string; +}; + +export type SolutionCaseSolutionMetrics = { + participantsTotal: number; + solutionsTotal: number; + notSubmitted: number | null; + submitted: number | null; +}; + +export type SolutionCaseSolutionItem = { + name: string; + participantsTotal: number; + solutionsTotal: number; + notSubmitted: number | null; + submitted: number | null; +}; + +export type SolutionCaseSolutionAnalytics = { + programId: number; + configured: boolean; + submissionApplicable: boolean; + items: Array; + withoutCase: SolutionCaseSolutionMetrics; +}; + +export type SolutionManagerTotal = { + total: number; +}; + +export type SolutionManagerRegion = { + name: string; + count: number; +}; + +export type SolutionManagerRegions = { + total: number; + items: Array; +}; + +export type SolutionManagerSummary = { + participants: SolutionManagerTotal; + solutions: SolutionManagerTotal; + experts: SolutionManagerTotal; + participantRegions: SolutionManagerRegions; +}; + +export type SolutionManagerParticipantFunnel = { + registrations: number; + uniqueParticipants: number; + visitedParticipants: number; + onboardedParticipants: number; + withTeam: number; + captains: number; + submittedCaptains: number | null; +}; + +export type SolutionManagerSolutionFunnel = { + created: number; + submissionApplicable: boolean; + notSubmitted: number | null; + submitted: number | null; + evaluated: number | null; +}; + +export type SolutionManagerEvaluationCounters = { + total: number; + pending: number; + evaluated: number; +}; + +export type SolutionManagerEvaluationSolutions = { + submitted: number; + awaitingEvaluation: number; + partiallyEvaluated: number; + evaluated: number; +}; + +export type SolutionManagerEvaluationStatus = { + applicable: boolean; + mode: "open" | "distributed"; + requiredEvaluators: number | null; + maxEvaluationsPerSolution: number | null; + assignments: SolutionManagerEvaluationCounters | null; + solutions: SolutionManagerEvaluationSolutions | null; +}; + +export type SolutionManagerExpert = { + expertId: number; + userId: number; + firstName: string; + lastName: string; + fullName: string; + avatar: string | null; +}; + +export type SolutionManagerAssignment = { + assignmentId: number; + expert: SolutionManagerExpert; + application: SolutionRef; + status: "not_ready" | "pending" | "in_progress" | "completed"; + assignedAt: string; + submitted: boolean; + submittedAt: string | null; + waitingSince: string | null; + waitingSeconds: number | null; +}; + +export type SolutionManagerAssignmentCriterion = { + name: string; + description: string | null; + type: "int" | "float" | "bool" | "str"; + minValue: number | null; + maxValue: number | null; + value: string | null; + criterionId: number; + isScored: boolean; +}; + +export type SolutionManagerAssignmentDetail = { + assignmentId: number; + expert: SolutionManagerExpert; + application: SolutionRef; + status: "not_ready" | "pending" | "in_progress" | "completed"; + assignedAt: string; + submitted: boolean; + submittedAt: string | null; + waitingSince: string | null; + waitingSeconds: number | null; + scores: Array; +}; + +export type SolutionManagerAssignments = { + applicable: boolean; + mode: "open" | "distributed"; + results: Array; +}; + +export type SolutionManagerDelayedExpert = { + expertId: number; + userId: number; + firstName: string; + lastName: string; + fullName: string; + avatar: string | null; + assignmentsTotal: number; + completed: number; + pending: number; + overdue_24h: number; + overdue_48h: number; + oldestWaitingSince: string | null; + oldestWaitingSeconds: number | null; + severity: "critical" | "warning"; +}; + +export type SolutionManagerDelayedExperts = { + total: number; + items: Array; +}; + +export type SolutionManagerAttention = { + participantsWithoutVisit: number; + participantsWithoutOnboarding: number; + participantsWithoutTeam: number; + solutionsAwaitingEvaluation: number | null; + solutionsNotSubmitted: number | null; + delayedExperts: SolutionManagerDelayedExperts; +}; + +export type SolutionManagerActivity = { + date: string; + registrations: number; + submittedSolutions: number | null; +}; + +export type SolutionManagerFieldCoverage = { + fieldId: number; + label: string; + type: SolutionFieldType; + purpose: SolutionPurpose; + applicable: number; + answered: number; + requiredApplicable: number; + requiredUnanswered: number; +}; + +export type SolutionManagerAnalytics = { + programId: number; + summary: SolutionManagerSummary; + participantFunnel: SolutionManagerParticipantFunnel; + solutionFunnel: SolutionManagerSolutionFunnel; + evaluationStatus: SolutionManagerEvaluationStatus; + attention: SolutionManagerAttention; + activity: Array; + cases: SolutionCaseSolutionAnalytics; + fieldCoverage: Array; +}; + +export type SolutionManagerSolutionRow = { + application: SolutionRef; + captain: { + userId: number; + fullName: string; + avatar: string | null; + }; + caseName: string | null; + submissionStatus: "draft" | "submitted"; + evaluationStatus: + | "not_applicable" + | "not_submitted" + | "awaiting_evaluation" + | "partially_evaluated" + | "evaluated"; + createdAt: string; + submittedAt: string | null; + assignmentsTotal: number | null; + assignmentsCompleted: number | null; + waitingReason: + | null + | "no_assignments" + | "no_completed_evaluations" + | "partially_evaluated" + | "awaiting_first_evaluation"; +}; + +export type SolutionManagerSolutions = { + count: number; + next: string | null; + previous: string | null; + submissionApplicable: boolean; + mode: "open" | "distributed"; + results: Array; +}; + +export type SolutionReminderSelection = + | { + mode: "selected"; + ids: Array; + excludedIds?: Array; + } + | { + mode: "all"; + ids?: Array; + excludedIds?: Array; + }; + +export type SolutionReminderPreviewRequest = { + search?: string; + selection: SolutionReminderSelection; +}; + +export type SolutionReminderRecipient = { + userId: number; + fullName: string; +}; + +export type SolutionReminderPreview = { + token: string | null; + selectedCount: number; + recipientCount: number; + withoutAssignmentsCount: number; + unavailableCount: number; + previewText: string; + recipients: Array; + expiresIn: 900; +}; + +export type SolutionReminderConfirm = { + token: string; +}; + +export type SolutionReminderReceipt = { + createdCount: number; + skippedCount: number; +}; + +export type SolutionPublishedSolutionMember = { + user: SolutionUser; + role: string | null; + specialization: string | null; + isCaptain: boolean; +}; + +export type SolutionPublishedSolution = { + application: SolutionRef; + publishedAt: string; + fields: Array; + answers: Array; + team: { + members: Array; + acceptedCount: number; + }; +}; + +export type SolutionPublishedSolutionCard = { + application: SolutionRef; + publishedAt: string; + captain: SolutionUser; + acceptedCount: number; +}; + +export type SolutionPublishedSolutionPage = { + programId: number; + count: number; + next: string | null; + previous: string | null; + results: Array; +}; + +export type SolutionActionPreviewRequest = { + action: "submit"; + ids: Array; +}; + +export type SolutionActionApplyRequest = { + action: "submit"; + ids: Array; + previewToken: string; +}; + +export type SolutionActionItem = { + application: SolutionRef; + errors: Array; +}; + +export type SolutionActionPreview = { + items: Array; + readyCount: number; + alreadySubmittedCount: number; + canApply: boolean; + previewToken: string | null; +}; + +export type SolutionActionResult = { + changedCount: number; + alreadySubmittedCount: number; +}; + +export type SolutionCaseChangePreviewRequest = { + caseName: string; +}; + +export type SolutionCaseChangeApplyRequest = { + caseName: string; + previewToken: string; +}; + +export type SolutionCaseChangePreview = { + application: SolutionRef; + caseFieldId: number; + currentCase: string | null; + caseName: string; + solutionVersion: number; + errors: Array; + canApply: boolean; + previewToken: string | null; + isSubmitted: boolean; + removedAssignmentsCount: number; + scoreCount: number; +}; + +export type SolutionAdminChange = + | SolutionCaseAdminChange + | SolutionMemberAdminChange + | SolutionArchiveAdminChange + | SolutionRegistrationAdminChange; + +export type SolutionAdminChangePage = { + count: number; + offset: number; + limit: number; + results: Array; +}; + +export type SolutionAdminChangeDetail = { + change: SolutionAdminChange; + beforeSnapshot: SolutionSubmissionSnapshot | null; + afterSnapshot: SolutionSubmissionSnapshot | null; + archiveSnapshot?: SolutionArchiveSnapshot; +}; + +export type SolutionMemberRemovalPreviewRequest = { + userId: number; +}; + +export type SolutionMemberRemovalApplyRequest = { + userId: number; + previewToken: string; +}; + +export type SolutionMemberRemovalPreview = { + application: SolutionRef; + member: SolutionMember; + solutionVersion: number; + isSubmitted: boolean; + remainingMemberCount: number; + revokedInvitationCount: number; + scoreCount: number; + errors: Array; + canApply: boolean; + previewToken: string | null; +}; + +export type SolutionCaseAdminChange = { + id: number; + application: SolutionRef; + action: "change_case"; + actor: SolutionUser | null; + actorName: string; + createdAt: string; + beforeVersion: number; + afterVersion: number; + caseFieldId: number; + previousCase: string | null; + caseName: string; + removedAssignmentsCount: number; + scoreCount: number; + wasSubmitted: boolean; +}; + +export type SolutionMemberAdminChange = { + id: number; + application: SolutionRef; + action: "remove_member"; + actor: SolutionUser | null; + actorName: string; + createdAt: string; + beforeVersion: number; + afterVersion: number; + removedAssignmentsCount: 0; + scoreCount: number; + wasSubmitted: boolean; + removedMember: SolutionMember; + revokedInvitationCount: number; +}; + +export type SolutionParticipation = { + status: "active" | "archived"; + archivedAt: string | null; + reason: "detached" | "registration_removed" | null; +}; + +export type SolutionArchiveSnapshot = { + name: string; + captainId: number; + members: Array; + form: SolutionForm; + answers: Array; + capturedAt: string; +}; + +export type SolutionArchivePreviewRequest = { + ids: Array; +}; + +export type SolutionArchiveApplyRequest = { + ids: Array; + previewToken: string; +}; + +export type SolutionArchivePreviewItem = { + application: SolutionRef; + solutionVersion: number; + isSubmitted: boolean; + isArchived: boolean; + scoreCount: number; + removedAssignmentsCount: number; + revokedInvitationCount: number; + errors: Array; +}; + +export type SolutionArchivePreview = { + items: Array; + scoreCount: number; + removedAssignmentsCount: number; + revokedInvitationCount: number; + canApply: boolean; + previewToken: string | null; +}; + +export type SolutionArchiveResult = { + application: SolutionRef; + solutionVersion: number; + archivedAt: string; +}; + +export type SolutionArchiveReceipt = { + changedCount: number; + items: Array; +}; + +export type SolutionArchiveAdminChange = { + id: number; + application: SolutionRef; + action: "archive"; + actor: SolutionUser | null; + actorName: string; + createdAt: string; + beforeVersion: number; + afterVersion: number; + removedAssignmentsCount: number; + scoreCount: number; + wasSubmitted: boolean; + archivedAt: string; + reason: "detached" | "registration_removed"; + revokedInvitationCount: number; + registrationRemovalId?: number; +}; + +export type SolutionRegistrationRemovalPreviewRequest = { + registrationIds: Array; +}; + +export type SolutionRegistrationRemovalApplyRequest = { + registrationIds: Array; + previewToken: string; +}; + +export type SolutionRemovedRegistration = { + registrationId: number; + user: SolutionUser; +}; + +export type SolutionRegistrationRemovalEffect = { + application: SolutionRef; + solutionVersion: number; + isSubmitted: boolean; + effect: "archive" | "remove_members" | "revoke_invitations"; + removedMembers: Array; + removedAssignmentsCount: number; + revokedInvitationCount: number; + scoreCount: number; +}; + +export type SolutionRegistrationRemovalPreview = { + registrations: Array; + effects: Array; + deletedCount: number; + archivedCount: number; + removedMemberCount: number; + removedAssignmentsCount: number; + revokedInvitationCount: number; + scoreCount: number; + canApply: boolean; + errors: Array; + previewToken: string | null; +}; + +export type SolutionRegistrationRemovalResult = { + application: SolutionRef; + solutionVersion: number; + changeId: number; + action: "archive" | "remove_registrations"; +}; + +export type SolutionRegistrationRemovalReceipt = { + auditId: number; + deletedCount: number; + items: Array; +}; + +export type SolutionRegistrationRemovalAudit = { + id: number; + programId: number; + actor: SolutionUser | null; + actorName: string; + createdAt: string; + registrations: Array; + deletedCount: number; +}; + +export type SolutionRegistrationRemovalAuditDetail = { + audit: SolutionRegistrationRemovalAudit; + changes: Array; +}; + +export type SolutionRegistrationRemovalAuditPage = { + count: number; + offset: number; + limit: number; + results: Array; +}; + +export type SolutionRegistrationAdminChange = { + id: number; + application: SolutionRef; + action: "remove_registrations"; + actor: SolutionUser | null; + actorName: string; + createdAt: string; + beforeVersion: number; + afterVersion: number; + removedAssignmentsCount: 0; + scoreCount: number; + wasSubmitted: boolean; + revokedInvitationCount: number; + removedMembers: Array; + registrationRemovalId: number; +}; 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 3efb0303d..3c4fec3b9 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 @@ -4,25 +4,452 @@ import { inject, Injectable } from "@angular/core"; import { HttpParams } from "@angular/common/http"; import { ApiService } from "@corelib"; import { + SolutionRegistrationRemovalPreviewRequest, + SolutionRegistrationRemovalApplyRequest, + SolutionRegistrationRemovalPreview, + SolutionRegistrationRemovalReceipt, + SolutionRegistrationRemovalAuditPage, + SolutionRegistrationRemovalAuditDetail, + SolutionArchivePreviewRequest, + SolutionArchiveApplyRequest, + SolutionArchivePreview, + SolutionArchiveReceipt, + SolutionMemberRemovalPreviewRequest, + SolutionMemberRemovalApplyRequest, + SolutionMemberRemovalPreview, + SolutionCaseChangePreviewRequest, + SolutionCaseChangeApplyRequest, + SolutionCaseChangePreview, + SolutionAdminChangePage, + SolutionAdminChangeDetail, + SolutionActionPreviewRequest, + SolutionActionApplyRequest, + SolutionActionPreview, + SolutionActionResult, CreateSolutionCommand, ProgramSolution, SolutionContext, SolutionPage, + SolutionCaptainCommand, + SolutionTeamCommand, + SolutionMutationReceipt, + SolutionInvitation, + SolutionInvitationPage, + SolutionCreateInvitation, + SolutionUpdateInvitation, + SolutionInvitationCreated, + SolutionInvitationActionResult, + SolutionForm, + SolutionFormWrite, + SolutionFormManagement, + SolutionTemplates, + UpdateSolutionCommand, + SolutionUploadResponse, + SubmitSolutionCommand, + RateSolutionCommand, + SolutionRatingPage, + SolutionRatingFrame, + SolutionAssignmentOptions, + SolutionAssignmentsWrite, + SolutionAllocationWrite, + SolutionAllocationPlan, + SolutionAssignmentReceipt, + SolutionQuota, + SolutionQuotaWrite, + SolutionResults, + SolutionCabinetApplications, + SolutionCaseSolutionAnalytics, + SolutionManagerAnalytics, + SolutionManagerAssignments, + SolutionManagerAssignmentDetail, + SolutionManagerSolutions, + SolutionReminderPreviewRequest, + SolutionReminderPreview, + SolutionReminderReceipt, + SolutionPublishedSolution, + SolutionPublishedSolutionPage, } from "@domain/solution/solution-contract.model"; import { SolutionListQuery, + SolutionInvitationQuery, + SolutionRatingQuery, + SolutionResultsQuery, + SolutionResultsExportQuery, + SolutionCabinetQuery, + SolutionManagerAssignmentQuery, + SolutionManagerSolutionsQuery, + SolutionAnalyticsExportQuery, + SolutionPublishedQuery, + SolutionHistoryQuery, validSolutionId, } from "@domain/solution/ports/solution.repository.port"; @Injectable({ providedIn: "root" }) export class SolutionHttpAdapter { + private registrationRemovalPath(programId: number, ids: number[]) { + this.path(programId); + if ( + !Array.isArray(ids) || + !ids.length || + ids.length > 10000 || + !ids.every(validSolutionId) || + new Set(ids).size !== ids.length + ) + throw new RangeError("Выберите регистрации без повторов."); + return `/programs/${programId}/management/registrations/remove/`; + } + previewRegistrationRemoval( + programId: number, + command: SolutionRegistrationRemovalPreviewRequest, + ) { + return this.api.post( + `${this.registrationRemovalPath(programId, command.registrationIds)}preview/`, + command, + ); + } + applyRegistrationRemoval(programId: number, command: SolutionRegistrationRemovalApplyRequest) { + return this.api.post( + `${this.registrationRemovalPath(programId, command.registrationIds)}apply/`, + command, + ); + } + listRegistrationRemovals(programId: number, query: SolutionHistoryQuery = {}) { + this.path(programId); + return this.api.get( + `/programs/${programId}/management/registrations/removals/`, + this.evaluationQuery(query), + ); + } + readRegistrationRemoval(programId: number, auditId: number) { + this.path(programId); + if (!validSolutionId(auditId)) throw new RangeError("Invalid audit ID"); + return this.api.get( + `/programs/${programId}/management/registrations/removals/${auditId}/`, + ); + } + private archivePath(programId: number, ids: number[]) { + this.path(programId); + if ( + !Array.isArray(ids) || + ids.length === 0 || + ids.length > 10000 || + !ids.every(validSolutionId) || + new Set(ids).size !== ids.length + ) + throw new RangeError("Выберите корректные решения без повторов."); + return `/programs/${programId}/management/solutions/archive/`; + } + previewArchive(programId: number, command: SolutionArchivePreviewRequest) { + return this.api.post( + `${this.archivePath(programId, command.ids)}preview/`, + command, + ); + } + applyArchive(programId: number, command: SolutionArchiveApplyRequest) { + return this.api.post( + `${this.archivePath(programId, command.ids)}apply/`, + command, + ); + } + previewMemberRemoval( + programId: number, + solutionId: number, + command: SolutionMemberRemovalPreviewRequest, + ) { + if (!validSolutionId(command.userId)) throw new RangeError("Invalid user ID"); + return this.api.post( + `${this.managementPath(programId, solutionId)}/members/remove/preview/`, + command, + ); + } + applyMemberRemoval( + programId: number, + solutionId: number, + command: SolutionMemberRemovalApplyRequest, + ) { + if (!validSolutionId(command.userId)) throw new RangeError("Invalid user ID"); + return this.api.post( + `${this.managementPath(programId, solutionId)}/members/remove/apply/`, + command, + ); + } + private managementPath(programId: number, solutionId: number) { + this.path(programId); + if (!validSolutionId(solutionId)) throw new RangeError("Invalid solution ID"); + return `/programs/${programId}/management/solutions/${solutionId}`; + } + private caseActionPath(programId: number, solutionId: number) { + return `${this.managementPath(programId, solutionId)}/case/`; + } + listAdministrativeChanges( + programId: number, + solutionId: number, + query: SolutionHistoryQuery = {}, + ) { + return this.api.get( + `${this.managementPath(programId, solutionId)}/history/`, + this.evaluationQuery(query), + ); + } + readAdministrativeChange(programId: number, solutionId: number, changeId: number) { + if (!validSolutionId(changeId)) throw new RangeError("Invalid change ID"); + return this.api.get( + `${this.managementPath(programId, solutionId)}/history/${changeId}/`, + ); + } + previewCaseChange( + programId: number, + solutionId: number, + command: SolutionCaseChangePreviewRequest, + ) { + return this.api.post( + `${this.caseActionPath(programId, solutionId)}preview/`, + command, + ); + } + applyCaseChange(programId: number, solutionId: number, command: SolutionCaseChangeApplyRequest) { + return this.api.post( + `${this.caseActionPath(programId, solutionId)}apply/`, + command, + ); + } + private submissionActionPath(programId: number, ids: number[]) { + this.path(programId); + if ( + !Array.isArray(ids) || + ids.length === 0 || + ids.length > 10000 || + !ids.every(validSolutionId) + ) { + throw new RangeError("Выберите корректные решения."); + } + return `/programs/${programId}/management/solutions/actions/`; + } + previewSubmitAction(programId: number, command: SolutionActionPreviewRequest) { + return this.api.post( + `${this.submissionActionPath(programId, command.ids)}preview/`, + command, + ); + } + applySubmitAction(programId: number, command: SolutionActionApplyRequest) { + return this.api.post( + `${this.submissionActionPath(programId, command.ids)}apply/`, + command, + ); + } + private publishedPath(programId: number) { + this.path(programId); + return `/programs/${programId}/published-solutions/`; + } + publishedSolutions(programId: number, query: SolutionPublishedQuery = {}) { + return this.api.get( + this.publishedPath(programId), + this.evaluationQuery(query), + ); + } + publishedSolution(programId: number, solutionId: number) { + if (!validSolutionId(solutionId)) throw new RangeError("Некорректный ID решения."); + return this.api.get( + `${this.publishedPath(programId)}${solutionId}/`, + ); + } + private expertReminderPath(programId: number) { + return `${this.analyticsPath(programId)}attention/solutions-awaiting-evaluation/`; + } + previewExpertReminder(programId: number, command: SolutionReminderPreviewRequest) { + return this.api.post( + `${this.expertReminderPath(programId)}reminder-preview/`, + command, + ); + } + confirmExpertReminder(programId: number, token: string) { + return this.api.post( + `${this.expertReminderPath(programId)}reminders/`, + { token }, + ); + } + private analyticsPath(programId: number) { + this.path(programId); + return `/programs/${programId}/management/solution-analytics/`; + } + managerAnalytics(programId: number) { + return this.api.get(this.analyticsPath(programId)); + } + managerAssignments(programId: number, query: SolutionManagerAssignmentQuery = {}) { + return this.api.get( + `${this.analyticsPath(programId)}assignments/`, + this.evaluationQuery(query), + ); + } + managerAssignment(programId: number, assignmentId: number) { + if (!validSolutionId(assignmentId)) throw new RangeError("Invalid assignment ID"); + return this.api.get( + `${this.analyticsPath(programId)}assignments/${assignmentId}/`, + ); + } + managerSolutions(programId: number, query: SolutionManagerSolutionsQuery = {}) { + return this.api.get( + `${this.analyticsPath(programId)}solutions/`, + this.managerQuery(query), + ); + } + exportAnalytics(programId: number, query: SolutionAnalyticsExportQuery = {}) { + return this.api.getFile(`${this.analyticsPath(programId)}export/`, this.managerQuery(query)); + } + private managerQuery(query: SolutionManagerSolutionsQuery | SolutionAnalyticsExportQuery) { + if (query.fieldId !== undefined && !validSolutionId(query.fieldId)) { + throw new RangeError("Invalid field ID"); + } + return this.evaluationQuery(query); + } + caseAnalytics(programId: number) { + this.path(programId); + return this.api.get( + `/programs/${programId}/management/solution-cases/`, + ); + } + 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/`; + } + 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), + ); + } + 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 + | SolutionManagerAssignmentQuery + | SolutionManagerSolutionsQuery + | SolutionAnalyticsExportQuery + | SolutionHistoryQuery, + ) { + let params = new HttpParams(); + const wireKeys: Record = { + caseScope: "case_scope", + caseName: "case_name", + assignedOnly: "assigned_only", + programId: "program_id", + fieldId: "field_id", + answerState: "answer_state", + }; + 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/`, + command, + ); + } 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"); 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/`); @@ -45,4 +472,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..63dcc67ff 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,13 +4,195 @@ import { inject, Injectable } from "@angular/core"; import { SolutionRepositoryPort, SolutionListQuery, + SolutionInvitationQuery, + SolutionRatingQuery, + SolutionResultsQuery, + SolutionResultsExportQuery, + SolutionCabinetQuery, + SolutionManagerAssignmentQuery, + SolutionManagerSolutionsQuery, + SolutionAnalyticsExportQuery, + SolutionPublishedQuery, + SolutionHistoryQuery, } from "@domain/solution/ports/solution.repository.port"; -import { CreateSolutionCommand } from "@domain/solution/solution-contract.model"; +import { + SolutionCaseChangePreviewRequest, + SolutionRegistrationRemovalPreviewRequest, + SolutionRegistrationRemovalApplyRequest, + SolutionRegistrationRemovalPreview, + SolutionRegistrationRemovalReceipt, + SolutionRegistrationRemovalAuditPage, + SolutionRegistrationRemovalAuditDetail, + SolutionArchivePreviewRequest, + SolutionArchiveApplyRequest, + SolutionMemberRemovalPreviewRequest, + SolutionMemberRemovalApplyRequest, + SolutionCaseChangeApplyRequest, + SolutionActionPreviewRequest, + SolutionActionApplyRequest, + CreateSolutionCommand, + SolutionTeamCommand, + SolutionCaptainCommand, + SolutionCreateInvitation, + SolutionUpdateInvitation, + SolutionFormWrite, + UpdateSolutionCommand, + SubmitSolutionCommand, + RateSolutionCommand, + SolutionAssignmentsWrite, + SolutionAllocationWrite, + SolutionQuotaWrite, + SolutionReminderPreviewRequest, +} from "@domain/solution/solution-contract.model"; import { SolutionHttpAdapter } from "../../adapters/solution/solution-http.adapter"; @Injectable({ providedIn: "root" }) export class SolutionRepository implements SolutionRepositoryPort { + previewRegistrationRemoval( + programId: number, + command: SolutionRegistrationRemovalPreviewRequest, + ) { + return this.http.previewRegistrationRemoval(programId, command); + } + applyRegistrationRemoval(programId: number, command: SolutionRegistrationRemovalApplyRequest) { + return this.http.applyRegistrationRemoval(programId, command); + } + listRegistrationRemovals(programId: number, query?: SolutionHistoryQuery) { + return this.http.listRegistrationRemovals(programId, query); + } + readRegistrationRemoval(programId: number, auditId: number) { + return this.http.readRegistrationRemoval(programId, auditId); + } + previewArchive(programId: number, command: SolutionArchivePreviewRequest) { + return this.http.previewArchive(programId, command); + } + applyArchive(programId: number, command: SolutionArchiveApplyRequest) { + return this.http.applyArchive(programId, command); + } + previewMemberRemoval( + programId: number, + solutionId: number, + command: SolutionMemberRemovalPreviewRequest, + ) { + return this.http.previewMemberRemoval(programId, solutionId, command); + } + applyMemberRemoval( + programId: number, + solutionId: number, + command: SolutionMemberRemovalApplyRequest, + ) { + return this.http.applyMemberRemoval(programId, solutionId, command); + } + listAdministrativeChanges(programId: number, solutionId: number, query?: SolutionHistoryQuery) { + return this.http.listAdministrativeChanges(programId, solutionId, query); + } + readAdministrativeChange(programId: number, solutionId: number, changeId: number) { + return this.http.readAdministrativeChange(programId, solutionId, changeId); + } + previewCaseChange( + programId: number, + solutionId: number, + command: SolutionCaseChangePreviewRequest, + ) { + return this.http.previewCaseChange(programId, solutionId, command); + } + applyCaseChange(programId: number, solutionId: number, command: SolutionCaseChangeApplyRequest) { + return this.http.applyCaseChange(programId, solutionId, command); + } + previewSubmitAction(programId: number, command: SolutionActionPreviewRequest) { + return this.http.previewSubmitAction(programId, command); + } + applySubmitAction(programId: number, command: SolutionActionApplyRequest) { + return this.http.applySubmitAction(programId, command); + } + publishedSolutions(programId: number, query?: SolutionPublishedQuery) { + return this.http.publishedSolutions(programId, query); + } + publishedSolution(programId: number, solutionId: number) { + return this.http.publishedSolution(programId, solutionId); + } + previewExpertReminder(programId: number, command: SolutionReminderPreviewRequest) { + return this.http.previewExpertReminder(programId, command); + } + confirmExpertReminder(programId: number, token: string) { + return this.http.confirmExpertReminder(programId, token); + } + managerAnalytics(programId: number) { + return this.http.managerAnalytics(programId); + } + managerAssignments(programId: number, query?: SolutionManagerAssignmentQuery) { + return this.http.managerAssignments(programId, query); + } + managerAssignment(programId: number, assignmentId: number) { + return this.http.managerAssignment(programId, assignmentId); + } + managerSolutions(programId: number, query?: SolutionManagerSolutionsQuery) { + return this.http.managerSolutions(programId, query); + } + exportAnalytics(programId: number, query?: SolutionAnalyticsExportQuery) { + return this.http.exportAnalytics(programId, query); + } + caseAnalytics(programId: number) { + return this.http.caseAnalytics(programId); + } + cabinetApplications(query?: SolutionCabinetQuery) { + return this.http.cabinetApplications(query); + } + 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); + } + exportResults(programId: number, query?: SolutionResultsExportQuery) { + return this.http.exportResults(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); + } 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); + } + 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); } @@ -23,4 +205,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/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 @@

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

-

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

+

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

- @@ -53,16 +57,37 @@

{{ requestError() }}

/> } @if (section() === "results") { - + } @if (section() === "experts") { - + } @else { + + } + } + @if (section() === "projects" && program.solutionStorageMode === "solution") { + } - @if (section() === "participants" || section() === "projects") { + @if ( + section() === "participants" || + (section() === "projects" && program.solutionStorageMode !== "solution") + ) {
@@ -643,13 +668,21 @@

{{ evaluation.name }}

> } @if (settingsTab() === "project-form") { - + @if (program.solutionStorageMode === "solution") { + + } @else { + + } } @else if (settingsTab() === "criteria") { Изменятся команды заявок
} +@if (solutionRemovalRequest()) { + +} ("participants"); + readonly solutionActionBusy = signal(false); readonly settingsTab = signal("basic"); readonly search = this.fb.control(""); readonly caseFilter = this.fb.control("all"); @@ -201,6 +210,7 @@ export class ProgramManagementComponent implements OnInit { preview: ParticipantRemovalPreview; } | null>(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 = ""; @@ -240,6 +250,7 @@ export class ProgramManagementComponent implements OnInit { readonly formError = signal(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 }); @@ -247,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); @@ -415,6 +427,7 @@ export class ProgramManagementComponent implements OnInit { this.saving() || this.uploading() || this.projectActionBusy() || + this.solutionActionBusy() || this.projectFormStatus().busy || this.criteriaStatus().busy || this.expertsStatus().busy || @@ -477,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; @@ -862,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); @@ -913,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(); @@ -1096,6 +1123,7 @@ export class ProgramManagementComponent implements OnInit { this.saving() || this.removingParticipants() || this.projectActionBusy() || + this.solutionActionBusy() || this.uploading() || this.projectFormStatus().busy || this.criteriaStatus().busy || @@ -1118,10 +1146,12 @@ 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 }); 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.spec.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.spec.ts new file mode 100644 index 000000000..890a395f1 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.spec.ts @@ -0,0 +1,104 @@ +/** @format */ +import { TestBed } from "@angular/core/testing"; +import { of, Subject } from "rxjs"; +import { ok, fail } from "@domain/shared/result.type"; +import { EvaluationUseCase } from "@api/program/use-cases/evaluation.use-case"; +import { + PreviewSolutionAllocationUseCase, + ApplySolutionAllocationUseCase, +} from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { SolutionAllocationPlan } from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { SolutionAllocationComponent } from "./solution-allocation.component"; + +const plan = solutionContractScenarios.find(s => s.name === "solution_allocation_preview")! + .response as SolutionAllocationPlan; +describe("Назначения из выбранных заявок решения", () => { + const roster = { workspace: vi.fn() }, + preview = { execute: vi.fn() }, + apply = { execute: vi.fn() }; + beforeEach(() => { + roster.workspace.mockReset().mockReturnValue( + of({ + revision: "solution-revision", + experts: [ + { id: 11, status: "joined", name: "Эксперт", email: "expert@example.test" }, + { id: "invite-12", status: "pending" }, + ], + projects: [{ id: 999 }], + }), + ); + preview.execute.mockReset().mockReturnValue(of(ok(structuredClone(plan)))); + apply.execute.mockReset().mockReturnValue(of(ok({}))); + TestBed.configureTestingModule({ + providers: [ + { provide: EvaluationUseCase, useValue: roster }, + { provide: PreviewSolutionAllocationUseCase, useValue: preview }, + { provide: ApplySolutionAllocationUseCase, useValue: apply }, + ], + }); + }); + async function setup() { + const fixture = TestBed.createComponent(SolutionAllocationComponent); + fixture.componentRef.setInput("request", { programId: 12, ids: [301, 302] }); + fixture.detectChanges(); + await fixture.whenStable(); + return fixture.componentInstance; + } + it("использует выбранные solution IDs и принятых экспертов, игнорирует Project IDs в общем roster", async () => { + const component = await setup(); + expect(component.experts().map(e => e.id)).toEqual([11]); + component.toggle(11); + component.preview(); + expect(preview.execute).toHaveBeenCalledExactlyOnceWith(12, { + revision: "solution-revision", + expertIds: [11], + solutionIds: [301, 302], + }); + expect(apply.execute).not.toHaveBeenCalled(); + }); + it("изменение выбора отменяет старый preview и исключает применение его токена", async () => { + const component = await setup(); + const old = new Subject>>(); + preview.execute.mockReturnValue(old); + component.toggle(11); + component.preview(); + component.toggle(11); + old.next(ok(plan)); + old.complete(); + component.confirm(); + expect(component.plan()).toBeNull(); + expect(apply.execute).not.toHaveBeenCalled(); + }); + it("запись применяет только server plan token и блокирует повтор/закрытие", async () => { + const component = await setup(), + pending = new Subject>(); + apply.execute.mockReturnValue(pending); + const closed = vi.fn(); + component.closed.subscribe(closed); + component.toggle(11); + component.preview(); + component.confirm(); + component.confirm(); + component.close(); + expect(apply.execute).toHaveBeenCalledExactlyOnceWith(12, plan.planToken); + expect(closed).not.toHaveBeenCalled(); + pending.next(ok({})); + pending.complete(); + expect(component.busy()).toBe(false); + }); + it("ошибка записи сбрасывает план и сохраняет выбранных экспертов без автоматической записи", async () => { + const component = await setup(); + apply.execute.mockReturnValue( + of(fail({ code: "stale_plan", detail: "План устарел", errors: [], meta: {} })), + ); + component.toggle(11); + component.preview(); + component.confirm(); + component.confirm(); + expect(component.plan()).toBeNull(); + expect([...component.selected()]).toEqual([11]); + expect(component.error()).toBe("План устарел"); + expect(apply.execute).toHaveBeenCalledTimes(1); + }); +}); 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 new file mode 100644 index 000000000..dfe85e392 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-allocation.component.ts @@ -0,0 +1,225 @@ +/** @format */ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + effect, + inject, + input, + output, + signal, + untracked, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Subject, takeUntil } from "rxjs"; +import { EvaluationUseCase } from "@api/program/use-cases/evaluation.use-case"; +import { + PreviewSolutionAllocationUseCase, + ApplySolutionAllocationUseCase, +} from "@api/solution/use-cases/solution-evaluation.use-cases"; +import { solutionError } from "@api/solution/solution-error"; +import { ExpertRow } from "@domain/program/evaluation-workspace.model"; +import { SolutionAllocationPlan } from "@domain/solution/solution-contract.model"; +import { ButtonDirective, ChoiceDirective } from "@uilib"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; + +@Component({ + selector: "app-solution-allocation", + imports: [ButtonDirective, ChoiceDirective, ModalComponent], + template: `
+
+

Назначить экспертов

+ +
+

+ Выбрано заявок: {{ request()?.ids?.length }}. Назначения проверяются по кейсам, командам и + сохраненным оценкам. +

+ @if (loading()) { +

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

+ } + @for (expert of experts(); track expert.id) { + + } + @if (!loading() && !experts().length && !error()) { +

В программе пока нет принявших приглашение экспертов.

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

{{ error() }}

+ + } + @if (plan(); as checked) { +

Будет добавлено назначений: {{ checked.assignments.length }}

+ @for (shortage of checked.shortages; track shortage.solutionId) { +

{{ shortage.solutionName }}: не хватает экспертов — {{ shortage.missing }}.

+ } + @for (row of checked.load; track row.expertId) { +

{{ row.name }}: {{ row.before }} → {{ row.after }} работ.

+ } + } +
+ + @if (plan()) { + + } @else { + + } +
+
`, + styleUrl: "./solution-applications.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionAllocationComponent { + readonly request = input<{ programId: number; ids: number[] } | null>(null); + readonly closed = output(); + readonly completed = output(); + readonly busyChange = output(); + readonly experts = signal([]); + readonly selected = signal(new Set()); + readonly revision = signal(""); + readonly loading = signal(false); + readonly busy = signal(false); + readonly error = signal(null); + readonly plan = signal(null); + private readonly roster = inject(EvaluationUseCase); + private readonly getPlan = inject(PreviewSolutionAllocationUseCase); + private readonly apply = inject(ApplySolutionAllocationUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly cancelled = new Subject(); + constructor() { + effect(() => { + const request = this.request(); + untracked(() => { + this.cancelled.next(); + this.experts.set([]); + this.selected.set(new Set()); + this.revision.set(""); + this.plan.set(null); + this.error.set(null); + if (request) this.load(); + }); + }); + } + load(): void { + const request = this.request(); + if (!request || this.busy()) return; + this.cancelled.next(); + this.loading.set(true); + this.plan.set(null); + this.revision.set(""); + this.error.set(null); + // Existing workspace supplies only the expert roster and revision here; Project IDs and counters are never consumed. + this.roster + .workspace(request.programId) + .pipe(takeUntil(this.cancelled), takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: data => { + this.experts.set( + data.experts.filter(e => e.status === "joined" && typeof e.id === "number"), + ); + this.revision.set(data.revision); + this.loading.set(false); + this.selected.update( + ids => new Set([...ids].filter(id => this.experts().some(e => e.id === id))), + ); + }, + error: error => { + this.loading.set(false); + this.error.set(solutionError(error).detail); + }, + }); + } + toggle(id: number): void { + if (this.busy()) return; + this.cancelled.next(); + this.loading.set(false); + this.plan.set(null); + this.selected.update(ids => { + const next = new Set(ids); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); + } + preview(): void { + const request = this.request(); + if (!request || !this.selected().size || !this.revision() || this.busy()) return; + this.cancelled.next(); + this.loading.set(true); + this.error.set(null); + this.plan.set(null); + this.getPlan + .execute(request.programId, { + revision: this.revision(), + expertIds: [...this.selected()], + solutionIds: request.ids, + }) + .pipe(takeUntil(this.cancelled), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.loading.set(false); + if (result.ok) this.plan.set(result.value); + else this.error.set(result.error.detail); + }); + } + confirm(): void { + const request = this.request(), + plan = this.plan(); + if (!request || !plan?.planToken || plan.shortages.length || this.busy()) return; + this.busy.set(true); + this.busyChange.emit(true); + this.apply + .execute(request.programId, plan.planToken) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.busy.set(false); + this.busyChange.emit(false); + this.plan.set(null); + if (this.request() !== request) return; + if (result.ok) this.completed.emit(); + else this.error.set(result.error.detail); + }); + } + close(): void { + if (this.busy()) return; + this.cancelled.next(); + this.closed.emit(); + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.html b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.html new file mode 100644 index 000000000..a104a6767 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.html @@ -0,0 +1,447 @@ + +
+ @if (formError()) { +

Не удалось загрузить настройки полей: {{ formError() }}

+ } +
+
+

Заявки

+

+ {{ archived() ? "В архиве" : "Всего заявок по фильтрам" }}: {{ count() }} +

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

Загружаем заявки…

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

{{ error() }}

+ + } @else if (!rows().length) { +

+ {{ + search.value.trim() + ? "Ничего не найдено. Измените запрос или фильтры." + : archived() + ? "В архиве пока нет заявок." + : "В программе пока нет заявок по выбранным условиям." + }} +

+ } @else { + @if (!archived()) { +
+ +
+ } + + + + + + + + + + + + + + + + + + + + + + + + + + @for (row of rows(); track row.application.solutionId; let index = $index) { + + + + + + + + + + + } + +
+ Заявки программы +
+ + №РешениеРуководительСдача решенияОцениваниеКейсДействия
+ + {{ offset() + index + 1 }} + + @if (program().isCompetitive && row.submissionStatus === "draft") { + Черновик заявки + } + {{ row.captain.fullName }} + {{ + program().isCompetitive + ? row.submissionStatus === "submitted" + ? "Сдано" + : "Не сдано" + : "Не применяется" + }} + {{ evaluation(row) }}{{ row.caseName || "Не выбран" }} + +
+ {{ offset() + 1 }}–{{ offset() + rows().length }} из {{ count() }} + } + @if (message()) { +

{{ message() }}

+ } + @if (selected().size && !archived()) { +
+ Выбрано заявок: {{ selected().size }} +
+ } +
+
+
+

Фильтры заявок

+ +
+ + @if (participation.value === "active") { + + @if (program().isCompetitive) { + + } + + @if (fieldFilter.value) { + + } + } +
+ +
+
+
+
+

{{ detail()?.name || "Заявка" }}

+ +
+ @if (detailLoading()) { +

Загружаем заявку…

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

{{ detailError() }}

+ } + @if (detail(); as item) { + + @if (historyOpen()) { +

История изменений

+ @if (historyLoading()) { +

Загружаем историю…

+ } + @if (history(); as records) { + @for (record of records.results; track record.id) { +

+ {{ record.createdAt | date: "dd.MM.yyyy HH:mm" : "+0300" }} · {{ record.actorName }} · + {{ historyLabel(record.action) }} +

+ } @empty { +

Изменений пока нет.

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

Принятых участников: {{ item.team.acceptedCount }}

+
    + @for (member of item.team.members; track member.user.id) { +
  • + {{ member.user.firstName }} {{ member.user.lastName }}{{ member.role || member.specialization || "Участник" + }}{{ member.isCaptain ? " · Капитан" : "" }} + @if (!member.isCaptain && item.participation?.status !== "archived") { + + } +
  • + } +
+ @if (item.team.pendingCount) { +

+ Ожидают подтверждения: {{ item.team.pendingCount }}. Приглашенные войдут в состав после + принятия приглашения. +

+ } + } @else { + + } +
+ +
+ } +
+ + +
+
+

{{ actionTitle() }}

+ +
+ @if (actionRequest()?.kind === "archive") { +

+ Материалы, состав и оценки сохранятся в архиве. Решение исчезнет из активных списков, + оценивания и публичной выдачи. Возврат из архива не предусмотрен. +

+ } @else if (actionRequest()?.kind === "member") { +

{{ actionRequest()?.kind === "member" ? memberName() : "" }}

+

+ Прежний состав останется в истории. Регистрация в программе, материалы и оценки сохранятся. +

+ } @else { +

+ После подтверждения материалы и принятый состав будут зафиксированы. Проверим обязательные + поля и команду. Ожидающие приглашения в состав не входят. +

+ } + @if (actionLoading()) { +

Проверяем изменения…

+ } + @if (actionPreview(); as checked) { + @if (actionRequest()?.kind !== "submit") { +

+ Сохранится оценок: {{ checked.scores }}. Будет снято назначений: + {{ checked.removedAssignments }}. Будет отозвано приглашений: {{ checked.invitations }}. +

+ } + @for (issue of checked.issues; track $index) { +

{{ issue }}

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

{{ actionError() }}

+ + } +
+ +
+
diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.scss b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.scss new file mode 100644 index 000000000..0c4a62056 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.scss @@ -0,0 +1,125 @@ +/** @format */ +@use "styles/ui-foundation"; +@use "styles/program-management-controls"; + +.management__solution-link { + padding: 0; + font: inherit; + color: var(--accent); + text-align: left; + overflow-wrap: anywhere; + cursor: pointer; + background: transparent; + border: 0; + + &:hover { + text-decoration: underline; + } + + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; + } +} + +.ui-field { + @include ui-foundation.control; + + font-size: 12px; +} + +.ui-button.ui-button { + @include ui-foundation.button; + @include program-management-controls.compact-action; +} + +.ui-choice { + @include ui-foundation.choice; +} + +:host { + display: block; + min-width: 0; +} + +.solution-dialog { + width: 100%; + min-width: 0; + max-height: 78dvh; + padding: 4px; + overflow: auto; + font: + 400 12px/1.5 Mont, + sans-serif; + + header, + footer { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + } + + footer { + flex-wrap: wrap; + justify-content: flex-end; + margin-top: 24px; + } + + h2 { + font-size: 20px; + line-height: 28px; + overflow-wrap: anywhere; + } + + p { + overflow-wrap: anywhere; + } + + label { + display: grid; + gap: 8px; + margin: 16px 0; + } + + [role="alert"] { + color: var(--red); + } + + &__tabs { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin: 20px 0; + } + + &__team { + padding: 0; + list-style: none; + + li { + display: grid; + gap: 8px; + padding: 16px 0; + border-bottom: 1px solid var(--medium-grey-for-outline); + } + } +} + +.management__card { + font: + 400 12px/1.5 Mont, + sans-serif; + + h2 { + margin: 0; + font-size: 16px; + line-height: 24px; + } +} + +.solution-selection { + left: 50%; + width: min(863px, calc(100% - 32px)); + transform: translateX(-50%); +} diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.spec.ts new file mode 100644 index 000000000..9e3b93965 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.spec.ts @@ -0,0 +1,197 @@ +/** @format */ +import { TestBed } from "@angular/core/testing"; +import { provideRouter, Router } from "@angular/router"; +import { of, Subject } from "rxjs"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { ProgramManagement } from "@domain/program/program-management.model"; +import { + ProgramSolution, + SolutionFormManagement, + SolutionManagerSolutions, +} from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { SolutionApplicationsComponent } from "./solution-applications.component"; +import { EvaluationUseCase } from "@api/program/use-cases/evaluation.use-case"; +vi.mock("file-saver", () => ({ saveAs: vi.fn() })); + +const page = solutionContractScenarios.find(s => s.name === "manager_solutions_field_present_zero")! + .response as SolutionManagerSolutions; +const form = solutionContractScenarios.find(s => s.name === "form_management")! + .response as SolutionFormManagement; +const detail = solutionContractScenarios.find(s => s.name === "created_solo")! + .response as ProgramSolution; +const program = { + id: 12, + name: "Программа", + solutionStorageMode: "solution", + isCompetitive: true, +} as ProgramManagement; +describe("Таблица заявок решения", () => { + function setupApi() { + const api = { + managerSolutions: vi.fn(() => of(structuredClone(page))), + managedForm: vi.fn(() => of(structuredClone(form))), + list: vi.fn(() => of({ count: 0, next: null, previous: null, results: [] })), + get: vi.fn(() => of({ ...structuredClone(detail), id: 301, programId: 12 })), + exportAnalytics: vi.fn(() => of(new Blob(["xlsx"]))), + previewCaseChange: vi.fn(), + applyCaseChange: vi.fn(), + previewSubmitAction: vi.fn(), + applySubmitAction: vi.fn(), + previewArchive: vi.fn(), + applyArchive: vi.fn(), + previewMemberRemoval: vi.fn(), + applyMemberRemoval: vi.fn(), + listAdministrativeChanges: vi.fn(), + }; + TestBed.configureTestingModule({ + providers: [ + provideRouter([]), + { provide: SolutionRepositoryPort, useValue: api }, + { provide: EvaluationUseCase, useValue: { workspace: vi.fn() } }, + ], + }); + return api; + } + async function setup() { + const api = setupApi(), + fixture = TestBed.createComponent(SolutionApplicationsComponent); + fixture.componentRef.setInput("program", program); + fixture.detectChanges(); + await fixture.whenStable(); + return { api, fixture, component: fixture.componentInstance }; + } + it("сохраняет таблицу из восьми колонок, а материал находится в меню", async () => { + const { api, component, fixture } = await setup(); + const headers = Array.from( + fixture.nativeElement.querySelectorAll("thead th") as NodeListOf, + ).map(el => el.textContent?.trim()); + expect(headers).toEqual([ + "", + "№", + "Решение", + "Руководитель", + "Сдача решения", + "Оценивание", + "Кейс", + "Действия", + ]); + const field = form.form.fields.find(f => f.purpose === "presentation")!; + api.get.mockReturnValue( + of({ + ...detail, + id: 301, + programId: 12, + form: form.form, + answers: [{ fieldId: field.id, value: { url: "https://files.test/presentation.pdf" } }], + }), + ); + component.loadMaterial(page.results[0]); + expect(component.actions(page.results[0]).find(a => a.id === "presentation")?.href).toBe( + "https://files.test/presentation.pdf", + ); + expect(fixture.nativeElement.querySelector('td[data-label="Презентация"]')).toBeNull(); + }); + it("использует те же серверные фильтры для списка и XLSX, включая нулевой ответ", async () => { + const { api, fixture, component } = await setup(); + await TestBed.inject(Router).navigateByUrl( + "/?search=Текст&field_id=701&answer_state=present&case_scope=selected&case_name=Экология&submission=submitted", + ); + fixture.detectChanges(); + const filters = { + search: "Текст", + submission: "submitted", + caseScope: "selected", + caseName: "Экология", + fieldId: 701, + answerState: "present", + }; + expect(api.managerSolutions).toHaveBeenLastCalledWith(12, { ...filters, limit: 20, offset: 0 }); + component.export(); + expect(api.exportAnalytics).toHaveBeenCalledExactlyOnceWith(12, { + kind: "solutions", + ...filters, + }); + }); + it("не создает ссылку меню из неподходящего протокола материала", async () => { + const { api, component } = await setup(); + const field = form.form.fields.find(f => f.purpose === "presentation")!; + api.get.mockReturnValue( + of({ + ...detail, + id: 301, + programId: 12, + form: form.form, + answers: [{ fieldId: field.id, value: { url: "javascript:alert(1)" } }], + }), + ); + component.loadMaterial(page.results[0]); + expect(component.actions(page.results[0]).find(a => a.id === "presentation")).toBeUndefined(); + }); + it("выбирает все серверные страницы и очищает выбор при смене фильтра", async () => { + const { api, fixture, component } = await setup(); + api.managerSolutions.mockImplementation((_program?: number, query?: { offset: number }) => + of({ + ...page, + count: 2, + next: query?.offset === 0 ? "next" : null, + results: [ + { + ...page.results[0], + application: { + ...page.results[0].application, + solutionId: query?.offset === 0 ? 301 : 302, + }, + }, + ], + }), + ); + component.selectAll(); + expect([...component.selected()]).toEqual([301, 302]); + await TestBed.inject(Router).navigateByUrl("/?search=Другой"); + fixture.detectChanges(); + expect(component.selected().size).toBe(0); + }); + it("читает архив отдельным API, исключает writers и не выгружает архив", async () => { + const { api, component } = await setup(); + await TestBed.inject(Router).navigateByUrl( + "/?participation=archived&field_id=701&answer_state=present", + ); + expect(api.list).toHaveBeenLastCalledWith(12, { + participation: "archived", + search: undefined, + offset: 0, + limit: 20, + }); + expect(component.actions(page.results[0]).map(a => a.id)).toEqual(["team", "history"]); + component.export(); + expect(api.exportAnalytics).not.toHaveBeenCalled(); + }); + it("смена программы очищает приватную заявку и административный токен", async () => { + const { api, fixture, component } = await setup(); + const slow = new Subject(); + api.get.mockReturnValue(slow); + component.open(page.results[0], true); + component.caseRequest.set({ programId: 12, solutionId: 301, name: "Старая" }); + fixture.componentRef.setInput("program", { ...program, id: 13 }); + fixture.detectChanges(); + await fixture.whenStable(); + slow.next(detail); + slow.complete(); + expect(component.detail()).toBeNull(); + expect(component.detailOpen()).toBe(false); + expect(component.caseRequest()).toBeNull(); + }); + it("запоздалая выдача прошлого фильтра не заменяет текущий список", async () => { + const { api, component } = await setup(); + const slow = new Subject(); + api.managerSolutions.mockReturnValueOnce(slow); + component.load(); + api.managerSolutions.mockReturnValue(of({ ...page, count: 0, results: [] })); + await TestBed.inject(Router).navigateByUrl("/?search=Новый"); + slow.next(page); + slow.complete(); + expect(component.rows()).toEqual([]); + expect(component.count()).toBe(0); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.ts new file mode 100644 index 000000000..5625aa215 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-applications.component.ts @@ -0,0 +1,763 @@ +/** @format */ +import { DatePipe } from "@angular/common"; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + ElementRef, + computed, + effect, + inject, + input, + output, + signal, + untracked, +} 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 { debounceTime, distinctUntilChanged, map, Observable, Subject, takeUntil } from "rxjs"; +import { Result } from "@domain/shared/result.type"; +import { saveAs } from "file-saver"; +import { AppRoutes } from "@api/paths/app-routes"; +import { presentationHref } from "@utils/program-case-analytics"; +import { + GetSolutionUseCase, + ListSolutionsUseCase, +} from "@api/solution/use-cases/solution.use-cases"; +import { GetManagedSolutionFormUseCase } from "@api/solution/use-cases/solution-form.use-cases"; +import { + ExportSolutionAnalyticsUseCase, + GetSolutionManagerSolutionsUseCase, +} from "@api/solution/use-cases/solution-manager-analytics.use-cases"; +import { + PreviewSolutionSubmitActionUseCase, + ApplySolutionSubmitActionUseCase, +} from "@api/solution/use-cases/solution-submission-actions.use-cases"; +import { + PreviewSolutionArchiveUseCase, + ApplySolutionArchiveUseCase, +} from "@api/solution/use-cases/solution-archive.use-cases"; +import { + PreviewSolutionMemberRemovalUseCase, + ApplySolutionMemberRemovalUseCase, +} from "@api/solution/use-cases/solution-member-actions.use-cases"; +import { ListSolutionAdminChangesUseCase } from "@api/solution/use-cases/solution-history.use-cases"; +import { SolutionManagerSolutionFilters } from "@domain/solution/ports/solution.repository.port"; +import { + ProgramSolution, + SolutionApiError, + SolutionAdminChangePage, + SolutionField, + SolutionManagerSolutionRow, + SolutionMember, +} from "@domain/solution/solution-contract.model"; +import { ProgramManagement } from "@domain/program/program-management.model"; +import { + ButtonDirective, + ChoiceDirective, + FieldDirective, + IconComponent, + PaginationComponent, + TableDirective, +} from "@uilib"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; +import { + RowAction, + RowActionEvent, + RowActionsComponent, +} from "@ui/primitives/row-actions/row-actions.component"; +import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fields.component"; +import { SolutionCaseActionComponent, SolutionCaseRequest } from "./solution-case-action.component"; +import { SolutionAllocationComponent } from "./solution-allocation.component"; + +type ActionRequest = + | { kind: "submit"; ids: number[] } + | { kind: "archive"; ids: number[] } + | { kind: "member"; ids: number[]; member: SolutionMember }; +type ActionPreview = { + token: string | null; + canApply: boolean; + issues: string[]; + scores: number; + removedAssignments: number; + invitations: number; +}; + +@Component({ + selector: "app-solution-applications", + imports: [ + DatePipe, + ReactiveFormsModule, + RouterLink, + ButtonDirective, + ChoiceDirective, + FieldDirective, + IconComponent, + PaginationComponent, + TableDirective, + ModalComponent, + RowActionsComponent, + SolutionFieldsComponent, + SolutionCaseActionComponent, + SolutionAllocationComponent, + ], + templateUrl: "./solution-applications.component.html", + styleUrls: ["../management.component.scss", "./solution-applications.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionApplicationsComponent { + readonly program = input.required(); + readonly busyChange = output(); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + private readonly element = inject>(ElementRef); + private readonly getList = inject(GetSolutionManagerSolutionsUseCase); + private readonly getArchive = inject(ListSolutionsUseCase); + private readonly get = inject(GetSolutionUseCase); + private readonly form = inject(GetManagedSolutionFormUseCase); + private readonly exportApi = inject(ExportSolutionAnalyticsUseCase); + private readonly historyApi = inject(ListSolutionAdminChangesUseCase); + private readonly submitPreview = inject(PreviewSolutionSubmitActionUseCase); + private readonly submitApply = inject(ApplySolutionSubmitActionUseCase); + private readonly archivePreview = inject(PreviewSolutionArchiveUseCase); + private readonly archiveApply = inject(ApplySolutionArchiveUseCase); + private readonly memberPreview = inject(PreviewSolutionMemberRemovalUseCase); + private readonly memberApply = inject(ApplySolutionMemberRemovalUseCase); + private readonly cancelList = new Subject(); + private readonly cancelDetail = new Subject(); + private readonly cancelAction = new Subject(); + private readonly cancelForm = new Subject(); + private readonly cancelMaterial = new Subject(); + private trigger: HTMLElement | null = null; + private activeProgram = 0; + private selectionKey = ""; + readonly paths = AppRoutes; + readonly rows = signal([]); + readonly presentationLinks = signal(new Map()); + readonly materialLoading = signal(null); + readonly fields = signal([]); + readonly formError = signal(null); + readonly count = signal(0); + readonly offset = signal(0); + readonly loading = signal(false); + readonly error = signal(null); + readonly message = signal(null); + readonly selected = signal(new Set()); + readonly selecting = signal(false); + readonly filtersOpen = signal(false); + readonly archived = signal(false); + readonly search = new FormControl("", { nonNullable: true }); + readonly caseFilter = new FormControl("all", { nonNullable: true }); + readonly submissionFilter = new FormControl("all", { nonNullable: true }); + readonly fieldFilter = new FormControl("", { nonNullable: true }); + readonly answerFilter = new FormControl("all", { nonNullable: true }); + readonly participation = new FormControl("active", { nonNullable: true }); + readonly exporting = signal(false); + readonly detailOpen = signal(false); + readonly detailLoading = signal(false); + readonly detailError = signal(null); + readonly detail = signal(null); + readonly materials = signal(new FormGroup({})); + readonly teamTab = signal(false); + readonly history = signal(null); + readonly historyOpen = signal(false); + readonly historyLoading = signal(false); + readonly caseRequest = signal(null); + readonly allocationRequest = signal<{ programId: number; ids: number[] } | null>(null); + readonly actionRequest = signal(null); + readonly actionPreview = signal(null); + readonly actionLoading = signal(false); + readonly actionError = signal(null); + readonly applying = signal(false); + readonly limit = 20; + readonly Array = Array; + readonly caseOptions = computed( + () => this.fields().find(f => f.purpose === "case")?.options ?? [], + ); + readonly allPicked = computed( + () => + !!this.rows().length && this.rows().every(r => this.selected().has(r.application.solutionId)), + ); + readonly actionTitle = computed( + () => + ({ + submit: "Сдать решение за команду", + archive: "Открепить от программы", + member: "Исключить участника", + })[this.actionRequest()?.kind ?? "submit"], + ); + + constructor() { + effect(() => { + const program = this.program(); + untracked(() => { + if (program.id === this.activeProgram) return; + this.cancelForm.next(); + this.cancelDetail.next(); + this.cancelAction.next(); + this.detailOpen.set(false); + this.detail.set(null); + this.caseRequest.set(null); + this.allocationRequest.set(null); + this.actionRequest.set(null); + this.actionPreview.set(null); + this.filtersOpen.set(false); + this.message.set(null); + this.formError.set(null); + this.activeProgram = program.id; + this.fields.set([]); + this.form + .execute(program.id) + .pipe(takeUntil(this.cancelForm), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + if (program.id !== this.activeProgram) return; + if (result.ok) this.fields.set(result.value.form.fields); + else this.formError.set(result.error.detail); + }); + this.load(); + }); + }); + this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe(() => { + if (this.activeProgram) this.load(); + }); + this.search.valueChanges + .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed()) + .subscribe(search => this.navigate({ search: search.trim() || null, offset: null })); + } + filters(): SolutionManagerSolutionFilters { + const q = this.route.snapshot.queryParamMap; + const submission = q.get("submission"), + caseScope = q.get("case_scope"), + answerState = q.get("answer_state"), + fieldId = Number(q.get("field_id")); + return { + search: q.get("search")?.trim() || undefined, + submission: submission === "submitted" || submission === "not_submitted" ? submission : "all", + caseScope: caseScope === "selected" || caseScope === "without_case" ? caseScope : "all", + ...(caseScope === "selected" ? { caseName: q.get("case_name") ?? "" } : {}), + ...(Number.isSafeInteger(fieldId) && fieldId > 0 + ? { + fieldId, + answerState: + answerState === "present" || + answerState === "missing" || + answerState === "not_applicable" + ? answerState + : "all", + } + : {}), + }; + } + load(): void { + const programId = this.activeProgram; + if (!programId) return; + this.cancelList.next(); + this.cancelMaterial.next(); + this.presentationLinks.set(new Map()); + this.materialLoading.set(null); + this.selecting.set(false); + const q = this.route.snapshot.queryParamMap, + offset = Number(q.get("offset")); + this.offset.set(Number.isSafeInteger(offset) && offset >= 0 ? offset : 0); + this.archived.set(q.get("participation") === "archived"); + const filters = this.filters(); + const key = JSON.stringify([programId, filters, this.archived()]); + if (this.selectionKey !== key) this.selected.set(new Set()); + this.selectionKey = key; + this.search.setValue(filters.search ?? "", { emitEvent: false }); + this.rows.set([]); + this.count.set(0); + this.error.set(null); + this.loading.set(true); + const request: Observable< + Result<{ count: number; results: SolutionManagerSolutionRow[] }, SolutionApiError> + > = this.archived() + ? this.getArchive + .execute(programId, { + participation: "archived", + search: filters.search, + offset: this.offset(), + limit: this.limit, + }) + .pipe( + map(result => + result.ok + ? { + ok: true as const, + value: { + count: result.value.count, + results: result.value.results.map(item => ({ + application: { + kind: "solution" as const, + programId, + solutionId: item.id, + name: item.name, + }, + captain: { + userId: item.captain.id, + fullName: `${item.captain.firstName} ${item.captain.lastName}`, + avatar: item.captain.avatarUrl, + }, + caseName: item.caseValue, + submissionStatus: item.status, + evaluationStatus: "not_applicable" as const, + createdAt: "", + submittedAt: item.submittedAt, + assignmentsTotal: null, + assignmentsCompleted: null, + waitingReason: null, + })), + }, + } + : result, + ), + ) + : this.getList.execute(programId, { ...filters, offset: this.offset(), limit: this.limit }); + request + .pipe(takeUntil(this.cancelList), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.loading.set(false); + if (result.ok) { + this.rows.set(result.value.results); + this.count.set(result.value.count); + } else this.error.set(result.error.detail); + }); + } + navigate(queryParams: Record): void { + void this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: "merge", + }); + } + page(direction: number): void { + this.navigate({ offset: Math.max(0, this.offset() + direction * this.limit) || null }); + } + toggle(id: number): void { + this.selected.update(ids => { + const next = new Set(ids); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); + } + clear(): void { + this.selected.set(new Set()); + } + selectAll(): void { + if (this.selecting() || this.archived()) return; + if (this.allPicked()) { + this.clear(); + return; + } + this.selecting.set(true); + const filters = this.filters(), + programId = this.activeProgram, + ids = new Set(); + const read = (offset: number) => + this.getList + .execute(programId, { ...filters, offset, limit: 100 }) + .pipe(takeUntil(this.cancelList), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + if (!result.ok) { + this.error.set(result.error.detail); + this.selecting.set(false); + return; + } + result.value.results.forEach(row => ids.add(row.application.solutionId)); + const next = offset + result.value.results.length; + if (result.value.next && next < result.value.count && next > offset) read(next); + else { + this.selected.set(ids); + this.selecting.set(false); + } + }); + read(0); + } + openFilters(trigger: HTMLElement): void { + this.trigger = trigger; + const filters = this.filters(); + this.participation.setValue(this.archived() ? "archived" : "active"); + this.caseFilter.setValue( + filters.caseScope === "selected" ? `case:${filters.caseName}` : (filters.caseScope ?? "all"), + ); + this.submissionFilter.setValue(filters.submission ?? "all"); + this.fieldFilter.setValue(filters.fieldId ? String(filters.fieldId) : ""); + this.answerFilter.setValue(filters.answerState ?? "all"); + this.filtersOpen.set(true); + } + applyFilters(): void { + const caseValue = this.caseFilter.value, + archived = this.participation.value === "archived"; + this.navigate({ + participation: archived ? "archived" : null, + submission: + !archived && this.submissionFilter.value !== "all" ? this.submissionFilter.value : null, + case_scope: + !archived && caseValue !== "all" + ? caseValue.startsWith("case:") + ? "selected" + : "without_case" + : null, + case_name: !archived && caseValue.startsWith("case:") ? caseValue.slice(5) : null, + field_id: !archived ? this.fieldFilter.value || null : null, + answer_state: !archived && this.fieldFilter.value ? this.answerFilter.value : null, + offset: null, + }); + this.filtersOpen.set(false); + this.restoreFocus(); + } + export(): void { + if (this.exporting() || this.archived()) return; + const programId = this.activeProgram; + this.exporting.set(true); + this.exportApi + .execute(programId, { kind: "solutions", ...this.filters() }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.exporting.set(false); + if (result.ok) saveAs(result.value, `program-${programId}-solutions.xlsx`); + else this.error.set(result.error.detail); + }); + } + evaluation(row: SolutionManagerSolutionRow): string { + return ( + { + not_applicable: "Не применяется", + not_submitted: "—", + awaiting_evaluation: "Не оценено", + partially_evaluated: "Частично", + evaluated: "Оценено", + }[row.evaluationStatus] + + (row.assignmentsTotal ? ` · ${row.assignmentsCompleted ?? 0}/${row.assignmentsTotal}` : "") + ); + } + actions(row: SolutionManagerSolutionRow): RowAction[] { + if (this.archived()) + return [ + { id: "team", label: "Посмотреть команду" }, + { id: "history", label: "История изменений" }, + ]; + return [ + { id: "team", label: "Посмотреть команду" }, + { + id: "evaluation", + label: "Перейти к оценке", + routerLink: `${AppRoutes.program.solution(this.activeProgram, row.application.solutionId)}/evaluation`, + queryParams: { returnUrl: this.router.url }, + disabled: !this.program().isCompetitive || row.submissionStatus !== "submitted", + reason: !this.program().isCompetitive + ? "В программе нет оценивания" + : row.submissionStatus !== "submitted" + ? "Решение еще не сдано" + : undefined, + }, + ...(this.presentationLinks().has(row.application.solutionId) + ? [ + { + id: "presentation", + label: "Открыть презентацию", + href: this.presentationLinks().get(row.application.solutionId)!, + }, + ] + : this.materialLoading() === row.application.solutionId && + this.fields().some(f => f.purpose === "presentation") + ? [ + { + id: "presentation", + label: "Открыть презентацию", + disabled: true, + reason: "Загружаем материал…", + }, + ] + : []), + { + id: "case", + label: "Сменить кейс", + disabled: !this.caseOptions().length, + reason: !this.caseOptions().length ? "В программе нет кейсов" : undefined, + }, + { + id: "submit", + label: "Сдать решение", + disabled: !this.program().isCompetitive || row.submissionStatus === "submitted", + reason: !this.program().isCompetitive + ? "В программе нет сдачи" + : row.submissionStatus === "submitted" + ? "Решение уже сдано" + : undefined, + }, + { id: "archive", label: "Открепить от программы", destructive: true }, + ]; + } + action(row: SolutionManagerSolutionRow, event: RowActionEvent): void { + this.trigger = event.trigger; + if (event.id === "team" || event.id === "materials" || event.id === "history") + this.open(row, event.id === "team", event.id === "history"); + else if (event.id === "case") + this.caseRequest.set({ + programId: this.activeProgram, + solutionId: row.application.solutionId, + name: row.application.name, + }); + else if (event.id === "submit" || event.id === "archive") + this.beginAction({ kind: event.id, ids: [row.application.solutionId] }); + } + loadMaterial(row: SolutionManagerSolutionRow): void { + if (this.archived()) return; + this.cancelMaterial.next(); + const programId = this.activeProgram, + solutionId = row.application.solutionId; + this.materialLoading.set(solutionId); + this.presentationLinks.update(links => { + const next = new Map(links); + next.delete(solutionId); + return next; + }); + this.get + .execute(programId, solutionId) + .pipe(takeUntil(this.cancelMaterial), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.materialLoading.set(null); + if (!result.ok || programId !== this.activeProgram) return; + const field = result.value.form.fields.find( + f => f.purpose === "presentation" && (f.type === "file" || f.type === "url"), + ); + const value = result.value.answers.find(a => a.fieldId === field?.id)?.value; + const href = presentationHref( + typeof value === "string" + ? value + : value && typeof value === "object" && "url" in value + ? value.url + : null, + ); + if (href) this.presentationLinks.update(links => new Map(links).set(solutionId, href)); + }); + } + open(row: SolutionManagerSolutionRow, team = false, history = false): void { + this.cancelDetail.next(); + this.detail.set(null); + this.detailError.set(null); + this.teamTab.set(team); + this.historyOpen.set(false); + this.history.set(null); + this.detailOpen.set(true); + this.detailLoading.set(true); + this.get + .execute(this.activeProgram, row.application.solutionId) + .pipe(takeUntil(this.cancelDetail), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.detailLoading.set(false); + if (result.ok) { + this.detail.set(result.value); + this.materials.set( + new FormGroup( + Object.fromEntries( + result.value.answers.map(a => [String(a.fieldId), new FormControl(a.value)]), + ), + ), + ); + if (history) this.openHistory(); + } else this.detailError.set(result.error.detail); + }); + } + closeDetail(): void { + if (this.applying()) return; + this.cancelDetail.next(); + this.detailOpen.set(false); + this.restoreFocus(); + } + openHistory(): void { + const detail = this.detail(); + if (!detail) return; + this.historyOpen.set(true); + this.historyLoading.set(true); + this.detailError.set(null); + this.historyApi + .execute(this.activeProgram, detail.id, { limit: 100 }) + .pipe(takeUntil(this.cancelDetail), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.historyLoading.set(false); + if (result.ok) this.history.set(result.value); + else this.detailError.set(result.error.detail); + }); + } + beginAction(request: ActionRequest): void { + if (this.applying()) return; + this.detailOpen.set(false); + this.cancelAction.next(); + this.actionRequest.set(request); + this.actionPreview.set(null); + this.actionError.set(null); + this.actionLoading.set(true); + const programId = this.activeProgram; + const stream = + request.kind === "submit" + ? this.submitPreview.execute(programId, { action: "submit", ids: request.ids }).pipe( + map(result => + result.ok + ? { + ok: true as const, + value: { + token: result.value.previewToken, + canApply: result.value.canApply, + issues: result.value.items.flatMap(item => + item.errors.map(e => `${item.application.name}: ${e.detail}`), + ), + scores: 0, + removedAssignments: 0, + invitations: 0, + }, + } + : result, + ), + ) + : request.kind === "archive" + ? this.archivePreview.execute(programId, { ids: request.ids }).pipe( + map(result => + result.ok + ? { + ok: true as const, + value: { + token: result.value.previewToken, + canApply: result.value.canApply, + issues: result.value.items.flatMap(item => + item.errors.map(e => `${item.application.name}: ${e.detail}`), + ), + scores: result.value.scoreCount, + removedAssignments: result.value.removedAssignmentsCount, + invitations: result.value.revokedInvitationCount, + }, + } + : result, + ), + ) + : this.memberPreview + .execute(programId, request.ids[0], { userId: request.member.user.id }) + .pipe( + map(result => + result.ok + ? { + ok: true as const, + value: { + token: result.value.previewToken, + canApply: result.value.canApply, + issues: result.value.errors.map(e => e.detail), + scores: result.value.scoreCount, + removedAssignments: 0, + invitations: result.value.revokedInvitationCount, + }, + } + : result, + ), + ); + stream + .pipe(takeUntil(this.cancelAction), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.actionLoading.set(false); + if (result.ok) this.actionPreview.set(result.value); + else this.actionError.set(result.error.detail); + }); + } + confirmAction(): void { + const request = this.actionRequest(), + preview = this.actionPreview(); + if (!request || !preview?.canApply || !preview.token || this.applying()) return; + this.applying.set(true); + const programId = this.activeProgram; + this.busyChange.emit(true); + this.actionError.set(null); + const stream = + request.kind === "submit" + ? this.submitApply + .execute(this.activeProgram, { + action: "submit", + ids: request.ids, + previewToken: preview.token, + }) + .pipe(map(result => (result.ok ? { ok: true as const } : result))) + : request.kind === "archive" + ? this.archiveApply + .execute(this.activeProgram, { ids: request.ids, previewToken: preview.token }) + .pipe(map(result => (result.ok ? { ok: true as const } : result))) + : this.memberApply + .execute(this.activeProgram, request.ids[0], { + userId: request.member.user.id, + previewToken: preview.token, + }) + .pipe(map(result => (result.ok ? { ok: true as const } : result))); + stream.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(result => { + this.applying.set(false); + this.busyChange.emit(false); + this.actionPreview.set(null); + if (this.activeProgram !== programId || this.actionRequest() !== request) return; + if (result.ok) { + this.actionRequest.set(null); + this.detailOpen.set(false); + this.selected.set(new Set()); + this.message.set("Изменения сохранены."); + this.load(); + this.restoreFocus(); + } else this.actionError.set(result.error.detail); + }); + } + closeAction(): void { + if (!this.applying()) { + this.cancelAction.next(); + this.actionRequest.set(null); + this.restoreFocus(); + } + } + retryAction(): void { + const request = this.actionRequest(); + if (request) this.beginAction(request); + } + memberName(): string { + const request = this.actionRequest(); + return request?.kind === "member" + ? `${request.member.user.firstName} ${request.member.user.lastName}` + : ""; + } + historyLabel(action: string): string { + return ( + ( + { + change_case: "Смена кейса", + remove_member: "Исключение участника", + archive: "Заявка перемещена в архив", + remove_registrations: "Удаление регистрации", + } as Record + )[action] ?? action + ); + } + caseChanged(): void { + this.caseRequest.set(null); + this.detailOpen.set(false); + this.message.set("Кейс изменен. Материалы, команда и оценки сохранены."); + this.load(); + this.restoreFocus(); + } + openAllocation(trigger: HTMLElement): void { + if (!this.selected().size || !this.program().isCompetitive) return; + this.trigger = trigger; + this.allocationRequest.set({ programId: this.activeProgram, ids: [...this.selected()] }); + } + allocationCompleted(): void { + this.allocationRequest.set(null); + this.message.set("Назначения экспертов сохранены."); + this.clear(); + this.load(); + this.restoreFocus(); + } + restoreFocus(): void { + const trigger = this.trigger; + setTimeout(() => { + if (trigger?.isConnected) trigger.focus(); + else + this.element.nativeElement + .querySelector("#solution-management-search") + ?.focus(); + }); + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.html b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.html new file mode 100644 index 000000000..97ae60906 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.html @@ -0,0 +1,74 @@ + + + @if (request(); as item) { +
+
+

Сменить кейс

+ +
+

{{ item.name }}

+ +

+ Материалы, команда и оценки сохранятся. Изменение кейса и снимка сдачи попадет в историю. +

+ @if (loading()) { +

Проверяем изменение кейса…

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

Текущий кейс: {{ checked.currentCase || "Не выбран" }}.

+

+ Будет снято назначений без оценок, не подходящих новому кейсу: + {{ checked.removedAssignmentsCount }}. Сохранится оценок по критериям: + {{ checked.scoreCount }}. +

+
+ @for (issue of checked.errors; track $index) { +

{{ issue.detail }}

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

{{ error() }}

+ + } +
+ +
+
+ } +
diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.spec.ts new file mode 100644 index 000000000..0cc47ea37 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.spec.ts @@ -0,0 +1,150 @@ +/** @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 { SolutionCaseChangePreview } from "@domain/solution/solution-contract.model"; +import { + PreviewSolutionCaseChangeUseCase, + ApplySolutionCaseChangeUseCase, +} from "@api/solution/use-cases/solution-case-actions.use-cases"; +import { SolutionCaseActionComponent } from "./solution-case-action.component"; + +const checked = solutionContractScenarios.find(s => s.name === "case_action_preview_ready")! + .response as SolutionCaseChangePreview; +const problem = { + code: "stale_preview", + detail: "Данные изменились. Обновите данные.", + errors: [], + meta: {}, +}; +describe("Смена кейса решения на одном экране", () => { + const preview = { execute: vi.fn() }, + apply = { execute: vi.fn() }; + beforeEach(() => { + preview.execute.mockReset().mockReturnValue(of(ok(structuredClone(checked)))); + apply.execute.mockReset().mockReturnValue(of(ok({}))); + TestBed.configureTestingModule({ + providers: [ + { provide: PreviewSolutionCaseChangeUseCase, useValue: preview }, + { provide: ApplySolutionCaseChangeUseCase, useValue: apply }, + ], + }); + }); + async function setup() { + const fixture = TestBed.createComponent(SolutionCaseActionComponent); + fixture.componentRef.setInput("request", { + programId: 12, + solutionId: 301, + name: "Решение команды", + }); + fixture.componentRef.setInput("caseOptions", [checked.caseName, "Другой кейс"]); + fixture.detectChanges(); + await fixture.whenStable(); + return fixture; + } + it("показывает важные последствия вместе с выбором кейса, без второго шага", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + component.caseControl.setValue(checked.caseName); + fixture.detectChanges(); + await fixture.whenStable(); + await vi.waitFor(() => + expect(document.querySelector('[aria-labelledby="solution-case-title"]')).not.toBeNull(), + ); + fixture.detectChanges(); + const dialog = document.querySelector('[aria-labelledby="solution-case-title"]')!; + expect(dialog.querySelector("select")).not.toBeNull(); + expect(dialog.textContent).toContain("Материалы, команда и оценки сохранятся"); + expect(dialog.textContent).toContain("не подходящих новому кейсу"); + expect(dialog.textContent).not.toContain("Посмотреть последствия"); + expect(dialog.querySelector('[role="tablist"]')).toBeNull(); + expect(component.canApply()).toBe(true); + expect(apply.execute).not.toHaveBeenCalled(); + }); + it("отменяет старый preview и не применяет поздние последствия прошлого выбора", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + const old = new Subject>>(); + preview.execute + .mockReturnValueOnce(old) + .mockReturnValueOnce( + of(ok({ ...checked, caseName: "Другой кейс", previewToken: "new-token" })), + ); + component.caseControl.setValue(checked.caseName); + component.caseControl.setValue("Другой кейс"); + old.next(ok(checked)); + old.complete(); + expect(component.preview()?.previewToken).toBe("new-token"); + component.confirm(); + expect(apply.execute).toHaveBeenCalledExactlyOnceWith(12, 301, { + caseName: "Другой кейс", + previewToken: "new-token", + }); + }); + it("при ошибке apply сохраняет выбор, сбрасывает токен и требует обновления", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + component.caseControl.setValue(checked.caseName); + apply.execute.mockReturnValue(of(fail(problem))); + component.confirm(); + component.confirm(); + expect(apply.execute).toHaveBeenCalledTimes(1); + expect(component.caseControl.value).toBe(checked.caseName); + expect(component.preview()).toBeNull(); + expect(component.canApply()).toBe(false); + expect(component.error()).toBe(problem.detail); + expect(preview.execute).toHaveBeenCalledTimes(1); + component.refresh(); + expect(preview.execute).toHaveBeenCalledTimes(2); + }); + it("блокирует повторное применение и закрытие во время записи", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + const writing = new Subject>(); + apply.execute.mockReturnValue(writing); + component.caseControl.setValue(checked.caseName); + const closed = vi.fn(), + busy = vi.fn(); + component.closed.subscribe(closed); + component.busyChange.subscribe(busy); + component.confirm(); + component.confirm(); + component.close(); + expect(apply.execute).toHaveBeenCalledTimes(1); + expect(closed).not.toHaveBeenCalled(); + expect(busy).toHaveBeenLastCalledWith(true); + writing.next(ok({})); + writing.complete(); + expect(busy).toHaveBeenLastCalledWith(false); + }); + it("смена программы очищает выбор и исключает применение чужого токена", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + const old = new Subject>>(); + preview.execute.mockReturnValue(old); + component.caseControl.setValue(checked.caseName); + fixture.componentRef.setInput("request", { + programId: 13, + solutionId: 302, + name: "Другая команда", + }); + fixture.detectChanges(); + await fixture.whenStable(); + old.next(ok(checked)); + component.confirm(); + expect(component.caseControl.value).toBe(""); + expect(component.preview()).toBeNull(); + expect(apply.execute).not.toHaveBeenCalled(); + }); + it("не применяет ответ с несовпадающей ссылкой на заявку", async () => { + const fixture = await setup(), + component = fixture.componentInstance; + preview.execute.mockReturnValue( + of(ok({ ...checked, application: { ...checked.application, solutionId: 999 } })), + ); + component.caseControl.setValue(checked.caseName); + component.confirm(); + expect(apply.execute).not.toHaveBeenCalled(); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.ts b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.ts new file mode 100644 index 000000000..92c4b21a2 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-applications/solution-case-action.component.ts @@ -0,0 +1,142 @@ +/** @format */ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + computed, + effect, + inject, + input, + output, + signal, + untracked, +} from "@angular/core"; +import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Subject, takeUntil } from "rxjs"; +import { + PreviewSolutionCaseChangeUseCase, + ApplySolutionCaseChangeUseCase, +} from "@api/solution/use-cases/solution-case-actions.use-cases"; +import { SolutionCaseChangePreview } from "@domain/solution/solution-contract.model"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; +import { ButtonDirective, FieldDirective } from "@uilib"; + +export interface SolutionCaseRequest { + programId: number; + solutionId: number; + name: string; +} + +@Component({ + selector: "app-solution-case-action", + imports: [ReactiveFormsModule, ModalComponent, ButtonDirective, FieldDirective], + templateUrl: "./solution-case-action.component.html", + styleUrl: "./solution-applications.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SolutionCaseActionComponent { + readonly request = input(null); + readonly caseOptions = input([]); + readonly closed = output(); + readonly completed = output(); + readonly busyChange = output(); + readonly caseControl = new FormControl("", { nonNullable: true }); + readonly preview = signal(null); + readonly loading = signal(false); + readonly applying = signal(false); + readonly error = signal(null); + readonly canApply = computed(() => { + const checked = this.preview(), + request = this.request(); + return ( + !!request && + !!checked?.canApply && + !!checked.previewToken && + checked.application.programId === request.programId && + checked.application.solutionId === request.solutionId && + !this.loading() && + !this.applying() + ); + }); + private readonly getPreview = inject(PreviewSolutionCaseChangeUseCase); + private readonly apply = inject(ApplySolutionCaseChangeUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly cancelled = new Subject(); + private sequence = 0; + + constructor() { + effect(() => { + this.request(); + untracked(() => { + this.sequence++; + this.cancelled.next(); + this.caseControl.reset("", { emitEvent: false }); + this.preview.set(null); + this.error.set(null); + this.loading.set(false); + }); + }); + this.caseControl.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => this.refresh()); + } + refresh(): void { + const request = this.request(), + caseName = this.caseControl.value; + const sequence = ++this.sequence; + this.cancelled.next(); + this.preview.set(null); + this.error.set(null); + this.loading.set(false); + if (!request || !caseName || this.applying()) return; + this.loading.set(true); + this.getPreview + .execute(request.programId, request.solutionId, { caseName }) + .pipe(takeUntil(this.cancelled), takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + if ( + sequence !== this.sequence || + this.request() !== request || + this.caseControl.value !== caseName + ) + return; + this.loading.set(false); + if (result.ok) this.preview.set(result.value); + else this.error.set(result.error.detail); + }); + } + confirm(): void { + const request = this.request(), + checked = this.preview(); + if ( + !request || + !checked || + !this.canApply() || + checked.caseName !== this.caseControl.value || + checked.application.solutionId !== request.solutionId || + checked.application.programId !== request.programId + ) + return; + this.applying.set(true); + this.busyChange.emit(true); + this.apply + .execute(request.programId, request.solutionId, { + caseName: checked.caseName, + previewToken: checked.previewToken!, + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.applying.set(false); + this.busyChange.emit(false); + this.preview.set(null); + if (this.request() !== request) return; + if (result.ok) this.completed.emit(); + else this.error.set(result.error.detail); + }); + } + close(): void { + if (this.applying()) return; + this.sequence++; + this.cancelled.next(); + this.closed.emit(); + } +} 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(); + } +} 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..40f8e7fb8 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.spec.ts @@ -0,0 +1,206 @@ +/** @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.each(["case_championship", "hackathon"] as const)( + "starts an unconfigured form clean and only confirms discarding real edits: %s", + async template => { + read.execute.mockReturnValue( + of( + ok({ + ...structuredClone(base), + form: { + ...structuredClone(base.form), + template: null, + isConfigured: false, + fields: [], + }, + hasAnswers: false, + hasSolutions: false, + affectedDraftCount: 0, + fieldsUsage: [], + }), + ), + ); + const fixture = await setup(), + component = fixture.componentInstance, + confirmDiscard = vi.fn(() => Promise.resolve(false)); + fixture.componentRef.setInput("confirmDiscard", confirmDiscard); + fixture.detectChanges(); + expect(component.dirty()).toBe(false); + expect(component.template()).toBeNull(); + component.templateChoice.setValue(template); + expect(component.dirty()).toBe(false); + await component.applyTemplate(); + expect(confirmDiscard).not.toHaveBeenCalled(); + expect(component.template()).toBe(template); + expect(component.fields().length).toBeGreaterThan(0); + expect(component.dirty()).toBe(true); + expect(save.execute).not.toHaveBeenCalled(); + const edits = component.fields(); + await component.applyTemplate(); + expect(confirmDiscard).toHaveBeenCalledTimes(1); + expect(component.fields()).toBe(edits); + expect(save.execute).not.toHaveBeenCalled(); + }, + ); + 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..5998fe4b6 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.ts @@ -0,0 +1,521 @@ +/** @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(), template: this.template() }) !== + 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", "permission_denied", "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/pages/program/published-solutions/public-program-solutions.component.ts b/projects/social_platform/src/app/ui/pages/program/published-solutions/public-program-solutions.component.ts new file mode 100644 index 000000000..66ce51f1e --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/public-program-solutions.component.ts @@ -0,0 +1,44 @@ +/** @format */ +import { ChangeDetectionStrategy, Component, inject } from "@angular/core"; +import { RouterLink } from "@angular/router"; +import { DeatilComponent } from "@ui/widgets/detail/detail.component"; +import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/program-detail-main-ui-info.service"; + +/** Public publications reuse the program header without the authenticated office shell. */ +@Component({ + selector: "app-public-program-solutions", + imports: [RouterLink, DeatilComponent], + template: `
+ +

{{ program()?.name }}

+ +
`, + styles: ` + .public-program { + max-width: 1200px; + margin: 24px auto; + padding: 0 16px; + font-family: Mont, sans-serif; + min-width: 0; + } + .public-program__logo { + width: 140px; + margin-bottom: 24px; + } + .public-program__title { + font-size: 24px; + overflow-wrap: anywhere; + } + @media (max-width: 999px) { + .public-program__title { + display: none; + } + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PublicProgramSolutionsComponent { + readonly program = inject(ProgramDetailMainUIInfoService).program; +} diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.html b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.html new file mode 100644 index 000000000..4a4d097a1 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.html @@ -0,0 +1,111 @@ + +
+ @if (solutionId() !== null) { + К решениям программы + } +

{{ solution()?.application?.name || "Решения программы" }}

+ @if (solutionId() === null) { + + } + @if (loading()) { +

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

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

+ {{ + problem.code === "not_found" + ? "Решение недоступно или еще не опубликовано." + : problem.detail + }} +

+ + } @else if (solution(); as item) { +

Опубликовано {{ item.publishedAt | date: "dd.MM.yyyy" }}

+ + @if (teamTab()) { +

Команда · {{ item.team.acceptedCount }}

+
    + @for (member of item.team.members; track member.user.id) { +
  • + {{ member.user.firstName }} {{ member.user.lastName }}{{ member.role || member.specialization || "Участник" + }}{{ member.isCaptain ? " · Капитан" : "" }} +
  • + } +
+ } @else { + + @if (!item.fields.length) { +

Материалы не добавлены.

+ } + } + } @else if (data(); as list) { +

Опубликовано решений: {{ list.count }}

+ @if (!list.results.length) { +

+ {{ + search.value.trim() + ? "По вашему запросу решений не найдено." + : "В программе пока нет опубликованных решений." + }} +

+ @if (search.value.trim()) { + + } + } @else { +
    + @for (card of list.results; track card.application.solutionId) { +
  • + {{ card.application.name }} +

    {{ card.captain.firstName }} {{ card.captain.lastName }}

    + В команде: {{ card.acceptedCount }} · + {{ card.publishedAt | date: "dd.MM.yyyy" }} +
  • + } +
+ {{ offset() + 1 }}–{{ offset() + list.results.length }} из {{ list.count }} + } + } +
diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.scss b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.scss new file mode 100644 index 000000000..926569334 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.scss @@ -0,0 +1,87 @@ +/** @format */ +@use "styles/ui-foundation"; + +.ui-field { + @include ui-foundation.control; + + font-size: 12px; +} + +.ui-button.ui-button { + @include ui-foundation.button; +} + +:host { + display: block; + min-width: 0; +} + +.published { + padding: 24px; + font-family: Mont, sans-serif; + background: var(--white); + border-radius: 15px; + + h2 { + margin: 16px 0; + font-size: 20px; + line-height: 28px; + overflow-wrap: anywhere; + } + + h3 { + font-size: 16px; + } + + a { + color: var(--accent); + overflow-wrap: anywhere; + } + + &__muted, + small { + color: var(--grey-for-text); + } + + &__tabs { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin: 20px 0; + } + + &__search { + display: grid; + gap: 8px; + margin: 20px 0; + } + + &__list, + &__team { + display: grid; + gap: 16px; + padding: 0; + list-style: none; + } + + &__list { + grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); + } + + &__list li { + padding: 20px; + border: 1px solid var(--medium-grey-for-outline); + border-radius: 15px; + } + + &__team li { + display: grid; + gap: 4px; + padding: 16px 0; + border-bottom: 1px solid var(--medium-grey-for-outline); + } + + @media (max-width: 600px) { + padding: 16px; + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.spec.ts new file mode 100644 index 000000000..60fb6d80d --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.spec.ts @@ -0,0 +1,151 @@ +/** @format */ +import { TestBed } from "@angular/core/testing"; +import { provideRouter, Router } from "@angular/router"; +import { RouterTestingHarness } from "@angular/router/testing"; +import { of, Subject } from "rxjs"; +import { ok, fail } from "@domain/shared/result.type"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { + SolutionPublishedSolution, + SolutionPublishedSolutionPage, +} from "@domain/solution/solution-contract.model"; +import { + GetPublishedSolutionUseCase, + GetPublishedSolutionsUseCase, +} from "@api/solution/use-cases/solution-publication.use-cases"; +import { PublishedSolutionsComponent } from "./published-solutions.component"; + +const list = solutionContractScenarios.find(s => s.name === "published_solutions_list")! + .response as SolutionPublishedSolutionPage; +const detail = solutionContractScenarios.find(s => s.name === "published_solution_zero_false_file")! + .response as SolutionPublishedSolution; +describe("Публичные решения внутри программы", () => { + const getList = { execute: vi.fn() }, + getDetail = { execute: vi.fn() }; + beforeEach(() => { + getList.execute.mockReset().mockReturnValue(of(ok(structuredClone(list)))); + getDetail.execute.mockReset().mockReturnValue(of(ok(structuredClone(detail)))); + TestBed.configureTestingModule({ + providers: [ + provideRouter([ + { + path: "office/program/:programId", + children: [ + { path: "published-solutions", component: PublishedSolutionsComponent }, + { path: "published-solutions/:solutionId", component: PublishedSolutionsComponent }, + ], + }, + ]), + { provide: GetPublishedSolutionsUseCase, useValue: getList }, + { provide: GetPublishedSolutionUseCase, useValue: getDetail }, + ], + }); + }); + it("строит только публичные ссылки решения и переносит поиск/страницу в карточку", async () => { + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl( + "/office/program/14/published-solutions?q=Прототип&offset=20", + PublishedSolutionsComponent, + ); + expect(getList.execute).toHaveBeenCalledExactlyOnceWith(14, { + search: "Прототип", + offset: 20, + limit: 20, + }); + const links = Array.from( + harness.routeNativeElement!.querySelectorAll(".published__list a"), + ); + expect(links.length).toBe(list.results.length); + expect(links.every(a => a.getAttribute("href")?.includes("/published-solutions/"))).toBe(true); + expect(links[0].getAttribute("href")).toContain("offset=20"); + expect(harness.routeNativeElement!.querySelector('a[href*="/projects/"]')).toBeNull(); + expect(getDetail.execute).not.toHaveBeenCalled(); + }); + it("показывает 0/Нет/файл, а Команда остается второй вкладкой без повторного GET", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/14/published-solutions/501?q=Прототип&offset=20", + PublishedSolutionsComponent, + ); + const values = Array.from(harness.routeNativeElement!.querySelectorAll(".answer")).map(el => + el.textContent?.trim(), + ); + expect(values).toContain("0"); + expect(values).toContain("Нет"); + expect(harness.routeNativeElement!.querySelector('a[target="_blank"]')).not.toBeNull(); + const tabs = Array.from(harness.routeNativeElement!.querySelectorAll("nav a")); + expect(tabs.map(a => a.textContent?.trim())).toEqual(["Решение", "Команда"]); + await harness.navigateByUrl( + "/office/program/14/published-solutions/501?q=Прототип&offset=20&tab=team", + PublishedSolutionsComponent, + ); + expect(component.teamTab()).toBe(true); + expect(getDetail.execute).toHaveBeenCalledTimes(1); + expect(harness.routeNativeElement!.querySelectorAll(".published__team li").length).toBe( + detail.team.acceptedCount, + ); + const back = harness.routeNativeElement!.querySelector("section > a")!; + expect(back.getAttribute("href")).toContain("published-solutions?"); + expect(back.getAttribute("href")).toContain("offset=20"); + }); + it("очищает прошлую публикацию при смене программы и игнорирует запоздалый ответ", async () => { + const old = new Subject>>(); + getDetail.execute.mockReturnValueOnce(old); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/14/published-solutions/501", + PublishedSolutionsComponent, + ); + await harness.navigateByUrl( + "/office/program/15/published-solutions/502", + PublishedSolutionsComponent, + ); + old.next(ok({ ...detail, application: { ...detail.application, name: "Старая публикация" } })); + old.complete(); + expect(component.programId()).toBe(15); + expect(harness.routeNativeElement!.textContent).not.toContain("Старая публикация"); + expect(getDetail.execute).toHaveBeenLastCalledWith(15, 502); + }); + it("не использует приватный редактор как fallback, если публикация недоступна", async () => { + getDetail.execute.mockReturnValue( + of(fail({ code: "not_found", detail: "Не найдено", errors: [], meta: {} })), + ); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/14/published-solutions/501", + PublishedSolutionsComponent, + ); + expect(component.solution()).toBeNull(); + expect(harness.routeNativeElement!.textContent).toContain("еще не опубликовано"); + expect(getList.execute).not.toHaveBeenCalled(); + expect(TestBed.inject(Router).url).toBe("/office/program/14/published-solutions/501"); + }); + it("оставляет поле поиска на месте при загрузке и позволяет повторить ошибочный запрос", async () => { + const loading = new Subject>>(); + getList.execute.mockReturnValueOnce(loading); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/14/published-solutions?q=Текст", + PublishedSolutionsComponent, + ); + const input = + harness.routeNativeElement!.querySelector('input[type="search"]'); + expect(input?.value).toBe("Текст"); + loading.next(ok(list)); + loading.complete(); + harness.detectChanges(); + expect(harness.routeNativeElement!.querySelector('input[type="search"]')).toBe(input); + component.reload(); + expect(getList.execute).toHaveBeenCalledTimes(2); + }); + it("не отправляет некорректные ID в API", async () => { + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl( + "/office/program/14/published-solutions/NaN", + PublishedSolutionsComponent, + ); + expect(getDetail.execute).not.toHaveBeenCalled(); + expect(getList.execute).not.toHaveBeenCalled(); + expect(harness.routeNativeElement!.textContent).toContain("недоступно"); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.ts b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.ts new file mode 100644 index 000000000..9620ff39c --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.ts @@ -0,0 +1,162 @@ +/** @format */ + +import { DatePipe } from "@angular/common"; +import { ChangeDetectionStrategy, Component, inject, signal } 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, + debounceTime, + distinctUntilChanged, + map, + startWith, + Subject, + switchMap, +} from "rxjs"; +import { + GetPublishedSolutionUseCase, + GetPublishedSolutionsUseCase, +} from "@api/solution/use-cases/solution-publication.use-cases"; +import { AppRoutes } from "@api/paths/app-routes"; +import { + SolutionApiError, + SolutionPublishedSolution, + SolutionPublishedSolutionPage, +} from "@domain/solution/solution-contract.model"; +import { validSolutionId } from "@domain/solution/ports/solution.repository.port"; +import { ButtonDirective, FieldDirective, PaginationComponent } from "@uilib"; +import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fields.component"; + +@Component({ + selector: "app-published-solutions", + imports: [ + DatePipe, + RouterLink, + ReactiveFormsModule, + ButtonDirective, + FieldDirective, + PaginationComponent, + SolutionFieldsComponent, + ], + templateUrl: "./published-solutions.component.html", + styleUrl: "./published-solutions.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PublishedSolutionsComponent { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly list = inject(GetPublishedSolutionsUseCase); + private readonly read = inject(GetPublishedSolutionUseCase); + private readonly retry = new Subject(); + readonly paths = AppRoutes; + readonly programId = signal(0); + readonly solutionId = signal(null); + readonly data = signal(null); + readonly solution = signal(null); + readonly materials = signal(new FormGroup({})); + readonly loading = signal(true); + readonly error = signal(null); + readonly search = new FormControl("", { nonNullable: true }); + readonly offset = signal(0); + readonly teamTab = signal(false); + readonly limit = 20; + + constructor() { + this.search.valueChanges + .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed()) + .subscribe(search => this.navigate({ q: search.trim() || null, offset: null })); + const context = combineLatest([ + combineLatest(this.route.pathFromRoot.map(r => r.paramMap)), + this.route.queryParamMap, + ]).pipe( + map(([path, query]) => { + const programId = Number(path.find(p => p.has("programId"))?.get("programId")); + const raw = path.find(p => p.has("solutionId"))?.get("solutionId"); + const solutionId = raw === undefined || raw === null ? null : Number(raw); + const offset = Number(query.get("offset")); + this.teamTab.set(query.get("tab") === "team"); + this.search.setValue(query.get("q") ?? "", { emitEvent: false }); + return { + programId, + solutionId, + search: this.search.value.trim(), + offset: Number.isSafeInteger(offset) && offset >= 0 ? offset : 0, + }; + }), + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), + ); + combineLatest([context, this.retry.pipe(startWith(undefined))]) + .pipe( + switchMap(([ctx]) => { + this.programId.set(ctx.programId); + this.solutionId.set(ctx.solutionId); + this.offset.set(ctx.offset); + this.data.set(null); + this.solution.set(null); + this.error.set(null); + this.loading.set(true); + if ( + !validSolutionId(ctx.programId) || + (ctx.solutionId !== null && !validSolutionId(ctx.solutionId)) + ) { + this.loading.set(false); + this.error.set({ + code: "not_found", + detail: "Решение не найдено.", + errors: [], + meta: {}, + }); + return []; + } + return ctx.solutionId === null + ? this.list + .execute(ctx.programId, { + search: ctx.search, + offset: ctx.offset, + limit: this.limit, + }) + .pipe(map(result => ({ kind: "list" as const, result }))) + : this.read + .execute(ctx.programId, ctx.solutionId) + .pipe(map(result => ({ kind: "detail" as const, result }))); + }), + takeUntilDestroyed(), + ) + .subscribe(response => { + this.loading.set(false); + if (response.kind === "list") { + if (response.result.ok) this.data.set(response.result.value); + else this.error.set(response.result.error); + } else if (response.result.ok) { + this.solution.set(response.result.value); + this.materials.set( + new FormGroup( + Object.fromEntries( + response.result.value.answers.map(a => [ + String(a.fieldId), + new FormControl(a.value), + ]), + ), + ), + ); + } else this.error.set(response.result.error); + }); + } + navigate(queryParams: Record): void { + void this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: "merge", + }); + } + reload(): void { + this.retry.next(); + } + page(direction: number): void { + this.navigate({ offset: Math.max(0, this.offset() + direction * this.limit) || null }); + } + listQuery(): Record { + return { q: this.search.value.trim() || null, offset: this.offset() || null }; + } +} 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..5f6fed1db --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.spec.ts @@ -0,0 +1,240 @@ +/** @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.each(["not_found", "not_program_expert", "solution_archived"])( + "shows saved/closed results read-only and clears a revoked evaluation frame: %s", + async code => { + 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(code))) as never); + page.prepare(); + page.confirm(); + fixture.detectChanges(); + expect(page.frame()).toBeNull(); + expect(page.form.value).toEqual({}); + expect(page.previous()).toEqual([]); + expect(page.renderer()).toBeUndefined(); + expect(fixture.nativeElement.textContent).toContain("Решение недоступно"); + expect(fixture.nativeElement.querySelector("app-solution-fields")).toBeNull(); + expect(get.execute).toHaveBeenCalledTimes(1); + 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..3ffd5f974 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.ts @@ -0,0 +1,328 @@ +/** @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", + "solution_archived", + "forbidden", + "not_assigned", + "not_program_expert", + "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/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..0ebba3b96 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.html @@ -0,0 +1,225 @@ + +
+ @if (updating()) { +

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

+ } +
+
+

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

+

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

+
+ @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..a51d545e6 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution-team.component.ts @@ -0,0 +1,277 @@ +/** @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 blocked = 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 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] }); + 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..73ae38321 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" }}

@@ -35,40 +45,205 @@

{{ 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 { -
+

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

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

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

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

+ {{ + item.submission.required + ? "Заполняйте решение постепенно. Обязательные поля понадобятся при сдаче." + : "Заполняйте решение постепенно и сохраняйте материалы по мере готовности." + }} +

+
+ + + @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 (submissionIssues().length) { +
    + @for (issue of submissionIssues(); track $index) { +
  • + +
  • + } +
+ } + @if (draft.notice()) { +

{{ draft.notice() }}

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

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

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

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

+ } +
+ @if (item.submission.required && item.submission.status === "draft" && draft.canEdit()) { + + } + @if (draft.canEdit()) { + + } + @if (draft.dirty()) { + + } +
+
}

+@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 9ea57f006..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 @@ -51,58 +51,95 @@ 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); } -dl { - margin: 0; +nav a:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; } -dl > div { - padding: 16px 0; - border-bottom: 1px solid var(--medium-grey-for-outline); +.solution-name { + display: grid; + gap: 8px; + margin: 24px 0; } -dt { - margin-bottom: 8px; +.solution-name .ui-field { + @include ui-foundation.control; + + width: 100%; } -dd { - margin: 0; - color: var(--grey-for-text); +.error { + color: var(--error, var(--ui-error-text)); +} + +.local-answers { + padding: 20px 0; + border-top: 1px solid var(--medium-grey-for-outline); +} + +.local-answers p { overflow-wrap: anywhere; white-space: pre-wrap; } -.members { - padding: 0; - list-style: none; +.draft-actions { + padding-top: 20px; + margin-top: 24px; + border-top: 1px solid var(--medium-grey-for-outline); } -.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__buttons { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; } -.name small { - display: block; - margin-top: 6px; +.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 { @@ -114,8 +151,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 76cf0e167..6018b8e6b 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"; @@ -13,6 +16,13 @@ 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, + SubmitSolutionUseCase, +} from "@api/solution/use-cases/solution-draft.use-cases"; +import { canLeaveSolution } from "@ui/routes/program/solution-leave.guard"; +import { EventBus } from "@domain/shared/event-bus"; +import { loggedOut } from "@domain/auth/events/logged-out.event"; const solution = solutionContractScenarios.find(s => s.name === "created_solo")! .response as ProgramSolution; @@ -32,10 +42,16 @@ describe("Solution route foundation", () => { const get = { execute: vi.fn() }; const create = { execute: vi.fn() }; const getContext = { execute: vi.fn() }; + const update = { execute: vi.fn() }; + const submit = { execute: vi.fn() }; + const profile = signal<{ id: number } | null>({ id: 7 }); beforeEach(() => { + profile.set({ id: 7 }); 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([ @@ -43,18 +59,197 @@ 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: SubmitSolutionUseCase, useValue: submit }, + { provide: ProfileInfoService, useValue: { profile } }, + { + provide: SolutionRepositoryPort, + useValue: { + invitations: vi + .fn() + .mockReturnValue(of({ count: 0, next: null, previous: null, results: [] })), + }, + }, ], }); }); + it("clears private draft, team and submission confirmation immediately on logout", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88/edit?tab=team", + ProgramSolutionComponent, + ); + component.draft.name.setValue("Личное несохраненное название"); + component.draft.group().get("1002")!.setValue("Личный ответ"); + component.confirmation.set(solution); + const confirmation = vi.spyOn(window, "confirm").mockReturnValue(false); + + TestBed.inject(EventBus).emit(loggedOut()); + expect(component.solution()).toBeNull(); + expect(component.draft.source()).toBeNull(); + expect(component.draft.name.value).toBe(""); + expect(component.draft.group().controls).toEqual({}); + expect(component.confirmation()).toBeNull(); + expect(component.canLeave()).toBe(true); + expect(confirmation).not.toHaveBeenCalled(); + harness.detectChanges(); + expect(harness.routeNativeElement?.querySelector("app-solution-team")).toBeNull(); + expect(harness.routeNativeElement?.textContent).not.toContain(solution.name); + expect(harness.routeNativeElement?.textContent).not.toContain("Личный ответ"); + confirmation.mockRestore(); + }); + + it("cancels a pending read on logout and cannot reload with the previous profile", async () => { + const response = new Subject>>(); + get.execute.mockReturnValue(response); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + expect(response.observed).toBe(true); + TestBed.inject(EventBus).emit(loggedOut()); + expect(response.observed).toBe(false); + component.reload(); + expect(get.execute).toHaveBeenCalledTimes(1); + response.next(ok(solution)); + expect(component.solution()).toBeNull(); + expect(component.error()?.code).toBe("unauthorized"); + }); + + it("cancels a pending creation context on logout without creating from its late response", async () => { + const response = new Subject>>(); + getContext.execute.mockReturnValue(response); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/new", + ProgramSolutionComponent, + ); + TestBed.inject(EventBus).emit(loggedOut()); + expect(response.observed).toBe(false); + response.next(ok(context)); + expect(create.execute).not.toHaveBeenCalled(); + expect(component.solution()).toBeNull(); + }); + + it.each(["save", "submit"] as const)( + "cancels a pending %s on logout without restoring private data from a late response", + async action => { + const response = new Subject>>(); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + if (action === "save") { + component.draft.name.setValue("Несохраненное название"); + update.execute.mockReturnValue(response); + component.saveDraft(); + } else { + component.confirmation.set(solution); + submit.execute.mockReturnValue(response); + component.confirmSubmission(); + } + expect(response.observed).toBe(true); + TestBed.inject(EventBus).emit(loggedOut()); + expect(response.observed).toBe(false); + response.next(ok({ ...solution, version: 2 })); + expect(component.solution()).toBeNull(); + expect(component.draft.source()).toBeNull(); + expect(component.draft.busy()).toBe(false); + expect(component.confirmation()).toBeNull(); + expect(get.execute).toHaveBeenCalledTimes(1); + }, + ); + + it.each([null, { id: 8 }])("clears an open draft when the profile becomes %j", async next => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88/edit", + ProgramSolutionComponent, + ); + component.draft.name.setValue("Правка прежнего аккаунта"); + profile.set(next); + await harness.fixture.whenStable(); + expect(component.solution()).toBeNull(); + expect(component.draft.name.value).toBe(""); + expect(component.canLeave()).toBe(true); + expect(update.execute).not.toHaveBeenCalled(); + expect(create.execute).not.toHaveBeenCalled(); + expect(get.execute).toHaveBeenCalledTimes(1); + }); + + it("a new account retries a fresh permission check without the previous local edits", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88/edit", + ProgramSolutionComponent, + ); + component.draft.name.setValue("Правка капитана"); + TestBed.inject(EventBus).emit(loggedOut()); + profile.set({ id: 8 }); + await harness.fixture.whenStable(); + const memberView: ProgramSolution = { + ...solution, + permissions: { canEdit: false, canSubmit: false, canManageTeam: false, canInvite: false }, + }; + get.execute.mockReturnValue(of(ok(memberView))); + component.reload(); + harness.detectChanges(); + expect(get.execute).toHaveBeenCalledTimes(2); + expect(component.draft.name.value).toBe(solution.name); + expect(component.draft.canEdit()).toBe(false); + expect(harness.routeNativeElement?.querySelector("#solution-name")).toBeNull(); + expect(update.execute).not.toHaveBeenCalled(); + }); + + it("a refreshed profile for the same user preserves unsaved edits", async () => { + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88/edit", + ProgramSolutionComponent, + ); + component.draft.name.setValue("Сохранить локальную правку"); + profile.set({ id: 7 }); + await harness.fixture.whenStable(); + expect(component.solution()).toEqual(solution); + expect(component.draft.name.value).toBe("Сохранить локальную правку"); + expect(component.draft.dirty()).toBe(true); + expect(get.execute).toHaveBeenCalledTimes(1); + }); + + it("initial profile loading does not discard a solution opened in the same session", async () => { + profile.set(null); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88/edit", + ProgramSolutionComponent, + ); + component.draft.name.setValue("Правка во время загрузки профиля"); + profile.set({ id: 7 }); + await harness.fixture.whenStable(); + expect(component.solution()).toEqual(solution); + expect(component.draft.name.value).toBe("Правка во время загрузки профиля"); + expect(get.execute).toHaveBeenCalledTimes(1); + }); + it("loads the exact program/solution and switches tabs without a second request", async () => { const harness = await RouterTestingHarness.create(); await harness.navigateByUrl("/office/program/12/solutions/88/edit", ProgramSolutionComponent); @@ -91,6 +286,32 @@ describe("Solution route foundation", () => { expect(create.execute).not.toHaveBeenCalled(); }); + it("shows paused creation at a direct new route without creating or falling back", async () => { + getContext.execute.mockReturnValue( + of(ok({ ...context, canCreate: false, blockedReason: "solution_creation_disabled" })), + ); + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl("/office/program/12/solutions/new", ProgramSolutionComponent); + expect(harness.routeNativeElement?.querySelector('[role="alert"]')?.textContent).toContain( + "Создание новых решений временно приостановлено. Ранее созданные заявки доступны.", + ); + expect(create.execute).not.toHaveBeenCalled(); + expect(get.execute).not.toHaveBeenCalled(); + expect(TestBed.inject(Router).url).toBe("/office/program/12/solutions/new"); + }); + + it("reads an existing solution during paused creation without consulting creation context", async () => { + getContext.execute.mockReturnValue( + of(ok({ ...context, canCreate: false, blockedReason: "solution_creation_disabled" })), + ); + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl("/office/program/12/solutions/88", ProgramSolutionComponent); + expect(get.execute).toHaveBeenCalledExactlyOnceWith(12, 88); + expect(getContext.execute).not.toHaveBeenCalled(); + expect(harness.routeNativeElement?.textContent).toContain(solution.name); + expect(create.execute).not.toHaveBeenCalled(); + }); + it("error state supports retry and does not render stale solution data", async () => { get.execute.mockReturnValueOnce( of(fail({ code: "not_found", detail: "Заявка не найдена.", errors: [], meta: {} })), @@ -133,4 +354,336 @@ 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(); + }); + + 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, + canManageTeam: false, + canInvite: false, + }, + form: { ...saved.form, isSnapshot: true }, + }), + ), + ); + component.confirmSubmission(); + expect(submit.execute).toHaveBeenCalledTimes(1); + expect(component.solution()?.submission.status).toBe("submitted"); + expect(component.draft.canEdit()).toBe(false); + expect(component.draft.error()).toBeNull(); + expect(component.draft.notice()).toBe("Решение сдано."); + harness.detectChanges(); + expect(harness.routeNativeElement?.textContent).not.toContain("Соединение прервано"); + }); + + it("keeps the uncertain submit error when the authoritative GET still returns a draft", 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))); + component.confirmSubmission(); + expect(get.execute).toHaveBeenCalledTimes(2); + expect(submit.execute).toHaveBeenCalledTimes(1); + expect(component.solution()?.submission.status).toBe("draft"); + expect(component.draft.error()?.code).toBe("network"); + expect(component.draft.notice()).not.toBe("Решение сдано."); + expect(component.draft.canEdit()).toBe(true); + expect(component.draft.name.value).toBe(saved.name); + }); + + it("confirms the submitted snapshot without discarding a later local edit", 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); + component.draft.name.setValue("Локальная правка после сохранения"); + 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 }, + form: { ...saved.form, isSnapshot: true }, + }), + ), + ); + component.confirmSubmission(); + harness.detectChanges(); + expect(submit.execute).toHaveBeenCalledTimes(1); + expect(component.solution()?.name).toBe(saved.name); + expect(component.draft.name.value).toBe("Локальная правка после сохранения"); + expect(component.draft.dirty()).toBe(true); + expect(component.draft.error()).toBeNull(); + expect(component.draft.notice()).toBe("Решение сдано."); + expect(harness.routeNativeElement?.querySelector(".local-answers")?.textContent).toContain( + "Локальная правка после сохранения", + ); + }); + + it("does not claim submission success when the verification GET also fails", 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(fail({ code: "network", detail: "Не удалось уточнить состояние", errors: [], meta: {} })), + ); + component.confirmSubmission(); + expect(get.execute).toHaveBeenCalledTimes(2); + expect(submit.execute).toHaveBeenCalledTimes(1); + expect(component.solution()?.submission.status).toBe("draft"); + expect(component.draft.error()?.code).toBe("network"); + expect(component.teamRefreshError()).toBe("Не удалось уточнить состояние"); + expect(component.draft.notice()).not.toBe("Решение сдано."); + harness.detectChanges(); + expect( + harness.routeNativeElement + ?.querySelector(".draft-actions__buttons button") + ?.hasAttribute("disabled"), + ).toBe(true); + }); + + 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?.textContent).toContain( + "Заполняйте решение постепенно и сохраняйте материалы по мере готовности.", + ); + expect(harness.routeNativeElement?.textContent).not.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 661989be0..927d03dda 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,26 +1,71 @@ /** @format */ -import { ChangeDetectionStrategy, Component, 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"; -import { combineLatest, EMPTY, map, startWith, Subject, switchMap } from "rxjs"; +import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop"; +import { + combineLatest, + distinctUntilChanged, + EMPTY, + finalize, + map, + pairwise, + startWith, + Subject, + switchMap, + takeUntil, +} from "rxjs"; +import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; +import { EventBus } from "@domain/shared/event-bus"; +import { User } from "@domain/auth/user.model"; import { GetSolutionContextUseCase, CreateSolutionUseCase, GetSolutionUseCase, } from "@api/solution/use-cases/solution.use-cases"; -import { solutionApplicationRoute } from "@api/solution/solution-navigation"; +import { + solutionApplicationRoute, + solutionCreationBlockedMessage, +} 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 } 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"; +import { ProjectInviteDialogComponent } from "@ui/widgets/project-invite/project-invite-dialog.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, + FieldDirective, + ReactiveFormsModule, + DatePipe, + SolutionTeamComponent, + SolutionFieldsComponent, + ProjectInviteDialogComponent, + ], + providers: [SolutionDraftFacade], templateUrl: "./solution.component.html", styleUrl: "./solution.component.scss", changeDetection: ChangeDetectionStrategy.OnPush, @@ -31,19 +76,318 @@ 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); + private readonly profile = inject(ProfileInfoService).profile; + private readonly events = inject(EventBus); + readonly draft = inject(SolutionDraftFacade); readonly solution = signal(null); readonly loading = signal(true); readonly error = signal(null); readonly teamTab = signal(false); readonly programId = signal(0); + 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 readonly accountChanged = new Subject(); + private loggedOutProfile: User | null | undefined; + private identityRequired = false; + private reconcileDraft = false; + + canLeave(): boolean { + return ( + !(this.draft.dirty() || this.commandBusy()) || + confirm("В заявке есть несохраненные изменения. Покинуть страницу?") + ); + } + + @HostListener("window:beforeunload", ["$event"]) + onBeforeUnload(event: BeforeUnloadEvent): void { + if (this.draft.dirty() || this.commandBusy()) { + event.preventDefault(); + event.returnValue = ""; + } + } + + saveDraft(): void { + if (this.commandBusy() || this.refreshingTeam() || this.teamRefreshError()) return; + this.draft.save().subscribe(result => { + if (result.ok) this.solution.set(result.value); + 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 ( + [ + "network", + "server_error", + "unknown", + "already_submitted", + "submission_closed", + "not_competitive", + ].includes(result.error.code) + ) + this.refreshTeam(); + } + }); + } + + 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.commandBusy() && confirm("Отменить несохраненные изменения решения?")) + this.draft.discard(); + } 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())]); + } + + private clearApplication(): void { + this.teamRefresh.next(null); + this.refreshingTeam.set(false); + this.teamRefreshError.set(""); + this.reconcileDraft = false; + this.confirmation.set(null); + this.submissionTrigger = null; + this.submissionIssues.set([]); + this.submissionAttempted.set(false); + this.draft.clear(); + this.solution.set(null); + this.error.set(null); + } + + private showAccountError(): void { + this.showError({ + code: "unauthorized", + detail: "Аккаунт изменился. Повторите вход и откройте заявку заново.", + errors: [], + meta: {}, + }); + } + + private invalidateAccount(): void { + this.identityRequired = true; + this.accountChanged.next(); + this.clearApplication(); + this.showAccountError(); + } + constructor() { + this.events + .on("LoggedOut") + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this.loggedOutProfile = this.profile(); + this.invalidateAccount(); + }); + toObservable(this.profile) + .pipe( + map(profile => profile?.id ?? null), + distinctUntilChanged(), + pairwise(), + takeUntilDestroyed(), + ) + .subscribe(([previous, current]) => { + if (previous !== null && previous !== current) this.invalidateAccount(); + }); + 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) { + const hydrated = this.draft.hydrate(result.value); + if (hydrated) this.solution.set(result.value); + if (this.reconcileDraft) { + this.reconcileDraft = false; + this.draft.reconciled(); + } + // GET подтверждает сдачу даже после потери ответа POST. Локальные + // несохраненные правки остаются в hydrate и отдельном блоке копирования. + if (hydrated && result.value.submission.status === "submitted") { + this.draft.error.set(null); + this.draft.notice.set("Решение сдано."); + this.submissionIssues.set([]); + this.submissionAttempted.set(false); + } + return; + } + 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); + }); this.route.queryParamMap .pipe(takeUntilDestroyed()) .subscribe(params => this.teamTab.set(params.get("tab") === "team")); @@ -57,10 +401,16 @@ export class ProgramSolutionComponent { solutionId: params.find(p => p.has("solutionId"))?.get("solutionId"), })), switchMap(({ programId, solutionId }) => { - this.solution.set(null); - this.error.set(null); + this.clearApplication(); this.loading.set(true); this.programId.set(programId); + if ( + (this.identityRequired && !this.profile()?.id) || + this.profile() === this.loggedOutProfile + ) { + this.showAccountError(); + return EMPTY; + } if ( !validSolutionId(programId) || (solutionId !== undefined && !validSolutionId(Number(solutionId))) @@ -73,7 +423,10 @@ export class ProgramSolutionComponent { }); return EMPTY; } - if (solutionId !== undefined) return this.get.execute(programId, Number(solutionId)); + if (solutionId !== undefined) + return this.get + .execute(programId, Number(solutionId)) + .pipe(takeUntil(this.accountChanged)); return this.context.execute(programId).pipe( switchMap(result => { if (!result.ok) { @@ -96,14 +449,14 @@ export class ProgramSolutionComponent { } else { this.showError({ code: context.blockedReason ?? "forbidden", - detail: - "Создание заявки сейчас недоступно. Проверьте регистрацию и сроки программы.", + detail: solutionCreationBlockedMessage(context.blockedReason), errors: [], meta: {}, }); } return EMPTY; }), + takeUntil(this.accountChanged), ); }), takeUntilDestroyed(), @@ -115,6 +468,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)], @@ -128,12 +482,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/pages/projects/edit/edit.component.html b/projects/social_platform/src/app/ui/pages/projects/edit/edit.component.html index 5238a4cc1..4bc9c08fa 100644 --- a/projects/social_platform/src/app/ui/pages/projects/edit/edit.component.html +++ b/projects/social_platform/src/app/ui/pages/projects/edit/edit.component.html @@ -75,7 +75,7 @@

редактировать проект

} } @else if (editingStep() === "additional") { - @defer { + @defer (on immediate) { { let component: ProjectEditComponent; @@ -80,6 +81,7 @@ describe("ProjectEditComponent", () => { await TestBed.configureTestingModule({ imports: [ReactiveFormsModule, ProjectEditComponent], providers: [provideNgxMask(), provideRouter([])], + deferBlockBehavior: DeferBlockBehavior.Playthrough, }) .overrideComponent(ProjectEditComponent, { remove: { @@ -129,7 +131,10 @@ describe("ProjectEditComponent", () => { { provide: ProjectVacancyService, useValue: emptySpy }, { provide: ProjectTeamService, useValue: emptySpy }, { provide: ProjectTeamUIService, useValue: emptySpy }, - { provide: ProjectAdditionalService, useValue: emptySpy }, + { + provide: ProjectAdditionalService, + useValue: { getAdditionalForm: () => projectsEditInfoServiceSpy.additionalForm }, + }, { provide: ProjectAchievementsService, useValue: emptySpy }, { provide: ProjectPartnerService, useValue: emptySpy }, { provide: ProjectResourceService, useValue: emptySpy }, @@ -138,6 +143,12 @@ describe("ProjectEditComponent", () => { ], }, }) + .overrideComponent(ProjectAdditionalStepComponent, { + set: { + template: '

Дополнительные сведения

', + imports: [], + }, + }) .compileComponents(); }); @@ -151,6 +162,29 @@ describe("ProjectEditComponent", () => { expect(component).toBeTruthy(); }); + it("opens the selected contest fields while the browser has no idle time", async () => { + vi.stubGlobal( + "requestIdleCallback", + vi.fn(() => 1), + ); + vi.stubGlobal("cancelIdleCallback", vi.fn()); + try { + fixture.debugElement.injector.get(ProjectStepService).currentStep.set("additional"); + fixture.detectChanges(); + await expect + .poll( + () => { + fixture.detectChanges(); + return fixture.nativeElement.querySelector('[data-testid="contest-fields"]'); + }, + { timeout: 1000 }, + ) + .not.toBeNull(); + } finally { + vi.unstubAllGlobals(); + } + }); + it("на любом шаге блокирует сохранение старой обложки во время reset", () => { fixture.debugElement.injector.get(ProjectCoverResetService).pending.set(true); fixture.detectChanges(); diff --git a/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.html b/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.html index d6cd23376..1056e4135 100644 --- a/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.html +++ b/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.html @@ -8,6 +8,7 @@ [disabled]="disabled()" [cdkMenuTriggerFor]="menu" [cdkMenuPosition]="positions" + (cdkMenuOpened)="opened.emit()" (click)="$event.stopPropagation()" > diff --git a/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.spec.ts b/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.spec.ts index 6e8c58b6d..307ded674 100644 --- a/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.spec.ts +++ b/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.spec.ts @@ -40,6 +40,15 @@ describe("Меню действий строки", () => { disabled.click(); expect(chosen).not.toHaveBeenCalled(); }); + it("сообщает об открытии меню для загрузки материала, не выбирая действие", () => { + const { fixture, trigger, chosen } = setup(); + const opened = vi.fn(); + fixture.componentInstance.opened.subscribe(opened); + trigger.click(); + fixture.detectChanges(); + expect(opened).toHaveBeenCalledTimes(1); + expect(chosen).not.toHaveBeenCalled(); + }); it("закрывается по Escape и возвращает фокус кнопке своей строки", () => { const { fixture, trigger, overlay } = setup(); trigger.focus(); diff --git a/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.ts b/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.ts index f89b90662..96611d91e 100644 --- a/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.ts +++ b/projects/social_platform/src/app/ui/primitives/row-actions/row-actions.component.ts @@ -40,6 +40,7 @@ export class RowActionsComponent { readonly actions = input.required(); readonly disabled = input(false); readonly action = output(); + readonly opened = output(); readonly trigger = viewChild.required>("trigger"); readonly positions: ConnectedPosition[] = [ { originX: "end", originY: "bottom", overlayX: "end", overlayY: "top", offsetY: 4 }, 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 c15f66332..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 @@ -11,10 +11,29 @@ 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"; import { RatingWorkspaceComponent } from "@ui/pages/program/rating/rating-workspace.component"; /** Маршруты детальной страницы программы: информация, проекты и участники. */ 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, @@ -25,16 +44,19 @@ 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/published-solutions.routes.spec.ts b/projects/social_platform/src/app/ui/routes/program/published-solutions.routes.spec.ts new file mode 100644 index 000000000..8d31eda3f --- /dev/null +++ b/projects/social_platform/src/app/ui/routes/program/published-solutions.routes.spec.ts @@ -0,0 +1,76 @@ +/** @format */ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { provideRouter, Router, RouterOutlet } from "@angular/router"; +import { RouterTestingHarness } from "@angular/router/testing"; +import { of } from "rxjs"; +import { AuthRequiredGuard } from "@core/lib/guards/auth/auth-required.guard"; +import { TokenService } from "@core/lib/services/tokens/token.service"; +import { GetProgramUseCase } from "@api/program/use-cases/get-program.use-case"; +import { + GetPublishedSolutionsUseCase, + GetPublishedSolutionUseCase, +} from "@api/solution/use-cases/solution-publication.use-cases"; +import { ok } from "@domain/shared/result.type"; +import { Program } from "@domain/program/program.model"; +import { PublicProgramSolutionsComponent } from "@ui/pages/program/published-solutions/public-program-solutions.component"; +import { PUBLISHED_SOLUTION_ROUTES } from "./published-solutions.routes"; + +@Component({ template: "private", standalone: true }) +class PrivatePage {} +@Component({ template: "login", standalone: true }) +class LoginPage {} +describe("Доступ внешнего посетителя к опубликованным решениям", () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideRouter([ + ...PUBLISHED_SOLUTION_ROUTES, + { + path: "office", + canActivate: [AuthRequiredGuard], + children: [{ path: "**", component: PrivatePage }], + }, + { path: "auth/login", component: LoginPage }, + ]), + { provide: TokenService, useValue: { getTokens: () => null } }, + { + provide: GetProgramUseCase, + useValue: { + execute: () => + of(ok(Object.assign(Program.default(), { id: 14, solutionStorageMode: "solution" }))), + }, + }, + { + provide: GetPublishedSolutionsUseCase, + useValue: { + execute: () => + of(ok({ programId: 14, count: 0, next: null, previous: null, results: [] })), + }, + }, + { provide: GetPublishedSolutionUseCase, useValue: { execute: vi.fn() } }, + ], + }); + TestBed.overrideComponent(PublicProgramSolutionsComponent, { + set: { template: "", imports: [RouterOutlet] }, + }); + }); + it("открывает точный публичный маршрут без аккаунта", async () => { + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl("/office/program/14/published-solutions"); + expect(TestBed.inject(Router).url).toBe("/office/program/14/published-solutions"); + expect(harness.routeNativeElement!.textContent).toContain("Решения программы"); + }); + it.each([ + "/office/program/14", + "/office/program/14/projects", + "/office/program/14/solutions/501", + "/office/program/14/solutions/501/edit", + "/office/program/14/management/projects", + ])("сохраняет прежний auth guard для %s", async path => { + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl(path); + expect(TestBed.inject(Router).url).toMatch(/^\/auth\/login\?/); + expect(harness.routeNativeElement!.textContent).toBe("login"); + }); +}); diff --git a/projects/social_platform/src/app/ui/routes/program/published-solutions.routes.ts b/projects/social_platform/src/app/ui/routes/program/published-solutions.routes.ts new file mode 100644 index 000000000..f60ced3e2 --- /dev/null +++ b/projects/social_platform/src/app/ui/routes/program/published-solutions.routes.ts @@ -0,0 +1,27 @@ +/** @format */ +import { Routes } from "@angular/router"; +import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/program-detail-main-ui-info.service"; +import { ProgramDetailResolver } from "@ui/pages/program/detail/detail.resolver"; +import { PublicProgramSolutionsComponent } from "@ui/pages/program/published-solutions/public-program-solutions.component"; + +/** These exact read-only paths are public; the remaining office routes keep AuthRequiredGuard. */ +export const PUBLISHED_SOLUTION_ROUTES: Routes = [ + "office/program/:programId/published-solutions", + "office/program/:programId/published-solutions/:solutionId", +].map(path => ({ + path, + component: PublicProgramSolutionsComponent, + providers: [ProgramDetailMainUIInfoService], + resolve: { data: ProgramDetailResolver }, + runGuardsAndResolvers: "pathParamsChange", + data: { listType: "program" }, + children: [ + { + path: "", + loadComponent: () => + import("@ui/pages/program/published-solutions/published-solutions.component").then( + m => m.PublishedSolutionsComponent, + ), + }, + ], +})); 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/detail/detail.component.html b/projects/social_platform/src/app/ui/widgets/detail/detail.component.html index 3b29d7979..01bc5050b 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/detail.component.html +++ b/projects/social_platform/src/app/ui/widgets/detail/detail.component.html @@ -92,7 +92,7 @@

{{ info().name }}

} - @if (userType() !== undefined) { + @if (userType() !== undefined || publicationsOnly()) { @if ( (isProjectsPage() || isProjectsRatingPage() || @@ -170,12 +170,16 @@

{{ info().name }}

{{ applicationLabel() }} @@ -195,7 +199,9 @@

{{ info().name }}

> } @if (isUserExpert()) { - + {{ info().name }} [size]="appWidth >= 1000 ? 'medium' : 'big'" customTypographyClass="text-body-12" > - оценка проектов + {{ + info().solutionStorageMode === "solution" + ? "оценка решений" + : "оценка проектов" + }} } @@ -261,7 +271,21 @@

{{ info().name }}

} } - @if (!isUserManager() && isUserExpert() && appWidth >= 1000) { + @if (info().solutionStorageMode === "solution") { + + решения + + } @else if (!isUserManager() && isUserExpert() && appWidth >= 1000) { {{ info().name }} class="cancel__cross" >

- произошла ошибка при редактировании! + {{ + info().solutionStorageMode === "solution" + ? "не удалось открыть решение" + : "произошла ошибка при редактировании!" + }}

- {{ assignProjectToProgramModalMessage() }}. + {{ assignProjectToProgramModalMessage() }}

diff --git a/projects/social_platform/src/app/ui/widgets/detail/detail.component.ts b/projects/social_platform/src/app/ui/widgets/detail/detail.component.ts index c295ba391..b46dc0cda 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/detail.component.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/detail.component.ts @@ -6,6 +6,7 @@ import { Component, HostListener, inject, + input, OnDestroy, OnInit, } from "@angular/core"; @@ -58,6 +59,7 @@ import { PROGRAM_DEFAULT_COVER, PROGRAM_LOGO_SIZES } from "@utils/program-brandi changeDetection: ChangeDetectionStrategy.OnPush, }) export class DeatilComponent implements OnInit, OnDestroy { + readonly publicationsOnly = input(false); protected readonly defaultCover = PROGRAM_DEFAULT_COVER; protected get programCover(): string { const program = this.info(); diff --git a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts index f759564da..78f268506 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts @@ -105,6 +105,54 @@ describe("DetailProgramInfoService", () => { expect(TestBed.inject(ApplyProjectToProgramUseCase).execute).not.toHaveBeenCalled(); }); + it("opens the owned solution after the deadline without trusting a stale legacy reference", () => { + programUI.program.set( + program({ + id: 12, + isUserMember: true, + solutionStorageMode: "solution", + datetimeProjectSubmissionEnds: "2000-01-01T00:00:00Z", + currentApplication: { projectId: 55, programLinkId: 700, submitted: true }, + }), + ); + vi.mocked(TestBed.inject(GetSolutionContextUseCase).execute).mockReturnValue( + of( + ok({ + ...legacyContext, + storageMode: "solution", + submissionOpen: false, + canCreate: false, + currentApplication: { kind: "solution", programId: 12, solutionId: 88, name: "Решение" }, + }), + ), + ); + expect(service.applicationLabel()).toBe("мое решение"); + service.addNewProject(12); + expect(router.navigate).toHaveBeenCalledExactlyOnceWith(["/office/program/12/solutions/88"], { + queryParams: {}, + }); + expect(TestBed.inject(ApplyProjectToProgramUseCase).execute).not.toHaveBeenCalled(); + }); + + it("does not create a solution when the closed context has no owned application", () => { + programUI.program.set(program({ id: 12, isUserMember: true, solutionStorageMode: "solution" })); + vi.mocked(TestBed.inject(GetSolutionContextUseCase).execute).mockReturnValue( + of( + ok({ + ...legacyContext, + storageMode: "solution", + submissionOpen: false, + canCreate: false, + blockedReason: "submission_closed", + }), + ), + ); + service.addNewProject(12); + expect(router.navigate).not.toHaveBeenCalled(); + expect(TestBed.inject(ApplyProjectToProgramUseCase).execute).not.toHaveBeenCalled(); + expect(service.isAssignProjectToProgramModalOpen()).toBe(true); + }); + it("context failure does not silently fall back to legacy creation", () => { vi.mocked(TestBed.inject(GetSolutionContextUseCase).execute).mockReturnValue( of(fail({ code: "network", detail: "Нет сети", errors: [], meta: {} })), @@ -117,6 +165,27 @@ describe("DetailProgramInfoService", () => { expect(router.navigate).not.toHaveBeenCalled(); }); + it("explains paused creation at the program entry without navigation or legacy apply", () => { + programUI.program.set(program({ id: 12, isUserMember: true, solutionStorageMode: "solution" })); + vi.mocked(TestBed.inject(GetSolutionContextUseCase).execute).mockReturnValue( + of( + ok({ + ...legacyContext, + storageMode: "solution", + canCreate: false, + blockedReason: "solution_creation_disabled", + }), + ), + ); + service.addNewProject(12); + expect(service.assignProjectToProgramModalMessage()).toBe( + "Создание новых решений временно приостановлено. Ранее созданные заявки доступны.", + ); + expect(service.isAssignProjectToProgramModalOpen()).toBe(true); + expect(router.navigate).not.toHaveBeenCalled(); + expect(TestBed.inject(ApplyProjectToProgramUseCase).execute).not.toHaveBeenCalled(); + }); + it("ignores stale program context and guards double clicks", () => { const response = new Subject>(); const getContext = vi diff --git a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts index 472d7c496..c9f7bb201 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts @@ -18,7 +18,10 @@ import { AppRoutes } from "@api/paths/app-routes"; import { PROGRAM_CASE_FIELD_NAME } from "@domain/program/program-case-field.const"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { GetSolutionContextUseCase } from "@api/solution/use-cases/solution.use-cases"; -import { solutionApplicationRoute } from "@api/solution/solution-navigation"; +import { + solutionApplicationRoute, + solutionCreationBlockedMessage, +} from "@api/solution/solution-navigation"; @Injectable() export class DetailProgramInfoService { @@ -56,17 +59,22 @@ export class DetailProgramInfoService { ); readonly applicationPending = signal(false); readonly applicationLabel = computed(() => - this.application()?.submitted - ? "вы подали проект" - : this.application() - ? "перейти в заявку" - : "создать заявку", + this.programDetailMainUIInfoService.program()?.solutionStorageMode === "solution" + ? "мое решение" + : this.application()?.submitted + ? "вы подали проект" + : this.application() + ? "перейти в заявку" + : "создать заявку", ); addNewProject(programId: number): void { - if (this.applicationPending() || this.application()?.submitted) return; + if (this.applicationPending()) return; + const solutionMode = + this.programDetailMainUIInfoService.program()?.solutionStorageMode === "solution"; + if (!solutionMode && this.application()?.submitted) return; // Existing legacy references already carry the exact project and program-link IDs. - if (this.application()) { + if (!solutionMode && this.application()) { this.openApplication(); return; } @@ -92,7 +100,7 @@ export class DetailProgramInfoService { void this.router.navigate([AppRoutes.program.solutionCreate(programId)]); } else { this.assignProjectToProgramModalMessage.set( - "Создание заявки сейчас недоступно. Проверьте регистрацию и сроки программы.", + solutionCreationBlockedMessage(context.blockedReason), ); this.isAssignProjectToProgramModalOpen.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-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); 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-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..804c03394 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.html @@ -0,0 +1,141 @@ + + +
+ @for (field of fields(); track field.id) { +
+ + @if (readonly()) { + @if (fileLink(field); as link) { + {{ + field.type === "url" ? "Открыть ссылку" : "Открыть файл" + }} + } @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() && 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 new file mode 100644 index 000000000..74eb5ec18 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss @@ -0,0 +1,95 @@ +/** @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, +.file-current { + 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.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 new file mode 100644 index 000000000..fa733b7dd --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.ts @@ -0,0 +1,67 @@ +/** @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 requireAnswers = 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); + 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(link); + 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/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/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/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; +} 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/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(); diff --git a/tools/generate-solution-contract.py b/tools/generate-solution-contract.py index fdb7a7f6e..a88825b25 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) @@ -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): @@ -39,13 +54,15 @@ 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}" 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) @@ -63,7 +80,7 @@ def convert(obj): fixtures = APP / "domain/solution/solution-contract.fixture.ts" fixtures.parent.mkdir(parents=True, exist_ok=True) fixtures.write_text("// Независимые сценарии C00; не последовательность запросов и не production mock provider.\n" - "export type SolutionContractScenario = { name: string; operationId: string; status: number; request?: unknown; response?: unknown };\n" + "export type SolutionContractScenario = { name: string; operationId: string; status: number; request?: unknown; invalidRequest?: unknown; query?: unknown; response?: unknown };\n" "export const solutionContractScenarios: SolutionContractScenario[] = " + json.dumps(convert(examples["scenarios"]), ensure_ascii=False, indent=2) + ";\n", encoding="utf-8") print(f"Generated {len(spec['components']['schemas'])} types and {len(examples['scenarios'])} scenarios") diff --git a/tools/verify-solution-contract-types.ts b/tools/verify-solution-contract-types.ts new file mode 100644 index 000000000..ee65e1429 --- /dev/null +++ b/tools/verify-solution-contract-types.ts @@ -0,0 +1,375 @@ +/** @format */ +import type { + SolutionRegistrationRemovalPreviewRequest, + SolutionRegistrationRemovalApplyRequest, + SolutionRegistrationAdminChange, +} from "../projects/social_platform/src/app/domain/solution/solution-contract.model"; + +import type { + SolutionArchiveApplyRequest, + SolutionArchivePreviewRequest, + SolutionArchiveAdminChange, + SolutionArchiveSnapshot, + SolutionMemberRemovalApplyRequest, + SolutionMemberRemovalPreviewRequest, + SolutionMemberAdminChange, + SolutionCaseChangePreviewRequest, + SolutionCaseChangeApplyRequest, + SolutionCaseChangePreview, + SolutionAdminChange, + SolutionAdminChangeDetail, + SolutionSubmissionSnapshot, + SolutionActionPreviewRequest, + SolutionActionApplyRequest, + SolutionUpdateInvitation, + SolutionCreateInvitation, + UpdateSolutionCommand, + RateSolutionCommand, + SolutionAllocationWrite, + SolutionQuotaWrite, + SolutionWidgetParticipant, + SolutionWidgetOrganizer, + SolutionManagerAssignment, + SolutionManagerSolutionFunnel, + SolutionReminderPreviewRequest, + SolutionPublishedSolutionMember, +} from "../projects/social_platform/src/app/domain/solution/solution-contract.model"; + +const validSubmitPreview: SolutionActionPreviewRequest = { action: "submit", ids: [301] }; +const validCasePreview: SolutionCaseChangePreviewRequest = { caseName: "Кейс A" }; +const validCaseApply: SolutionCaseChangeApplyRequest = { + ...validCasePreview, + previewToken: "opaque", +}; +// @ts-expect-error Case confirmation requires its own signed token. +const missingCaseToken: SolutionCaseChangeApplyRequest = { caseName: "Кейс A" }; +// @ts-expect-error A project reference cannot be used as a case-change command. +const projectCaseCommand: SolutionCaseChangePreviewRequest = { caseName: "Кейс A", projectId: 301 }; +const validSubmitApply: SolutionActionApplyRequest = { + ...validSubmitPreview, + previewToken: "opaque", +}; +// @ts-expect-error Confirmation requires the exact signed preview token. +const missingSubmitToken: SolutionActionApplyRequest = { action: "submit", ids: [301] }; +// @ts-expect-error Post-submission administrative exceptions are not part of this contract. +const destructiveSolutionAction: SolutionActionPreviewRequest = { action: "detach", ids: [301] }; +const wrongSubmissionReference: SolutionActionPreviewRequest = { + action: "submit", + // @ts-expect-error Project link IDs cannot replace the required solution selection. + projectIds: [301], +}; +void [ + validCasePreview, + validCaseApply, + missingCaseToken, + projectCaseCommand, + validSubmitPreview, + validSubmitApply, + missingSubmitToken, + destructiveSolutionAction, + wrongSubmissionReference, +]; + +const reminderSelection = { + selection: { mode: "selected", ids: [301] }, +} satisfies SolutionReminderPreviewRequest; +// @ts-expect-error Для selected нужны настоящие IDs решений. +const reminderWithoutIds: SolutionReminderPreviewRequest = { selection: { mode: "selected" } }; +void reminderSelection; +void reminderWithoutIds; +import type { + SolutionResultsExportQuery, + SolutionAnalyticsExportQuery, + SolutionPublishedQuery, +} from "../projects/social_platform/src/app/domain/solution/ports/solution.repository.port"; + +const publicQuery = { search: "Прототип", limit: 25, offset: 0 } satisfies SolutionPublishedQuery; +// @ts-expect-error Публичный список не принимает закрытый фильтр сдачи. +const privatePublicQuery: SolutionPublishedQuery = { submission: "submitted" }; +function publishedMemberPrivacy(member: SolutionPublishedSolutionMember) { + // @ts-expect-error В публичном участнике нет email. + return member.user.email; +} +void publicQuery; +void privatePublicQuery; +void publishedMemberPrivacy; + +const validResultsExport = { + scope: "all", + caseScope: "selected", + caseName: "Задача 1", +} satisfies SolutionResultsExportQuery; +// @ts-expect-error Выгрузка принимает только current/all. +const invalidResultsExport: SolutionResultsExportQuery = { scope: "everything" }; + +// Независимые примеры C00: у обеих команд обязательны ревизии и хотя бы одно изменение. +const role = { + expectedVersion: 1, + expectedSolutionVersion: 2, + role: "Аналитик", +} satisfies SolutionUpdateInvitation; +const cleared: SolutionUpdateInvitation = { + expectedVersion: 1, + expectedSolutionVersion: 2, + // @ts-expect-error Переданную роль нельзя очистить в null. + role: null, +}; +// @ts-expect-error Новое приглашение требует роль. +const missingInvitationRole: SolutionCreateInvitation = { + expectedSolutionVersion: 1, + recipientUserId: 9, +}; +void missingInvitationRole; +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: "Решение" }; + +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 }; + +const solutionWidgetParticipant: SolutionWidgetParticipant = { + application: { kind: "solution", programId: 12, solutionId: 301, name: "Решение" }, + caseProvided: false, + caseName: null, + stage: "not_submitted", + submissionOpen: true, +}; +const wrongWidgetReference: SolutionWidgetParticipant = { + ...solutionWidgetParticipant, + // @ts-expect-error Решение не принимает projectId вместо solutionId. + application: { kind: "solution", programId: 12, projectId: 55, name: "Решение" }, +}; +const wrongWidgetProjection: SolutionWidgetParticipant = { + ...solutionWidgetParticipant, + // @ts-expect-error У решения нет фиктивного participantProject. + participantProject: { id: 55, name: "Проект", programLinkId: 65 }, +}; +const solutionOrganizer: SolutionWidgetOrganizer = { + participants: 1, + solutions: 0, + submittedSolutions: null, + participantsWithoutSolution: 1, +}; + +const noncompetitiveFunnel: SolutionManagerSolutionFunnel = { + created: 1, + submissionApplicable: false, + submitted: null, + notSubmitted: null, + evaluated: null, +}; +const assignmentReference: SolutionManagerAssignment["application"] = { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение", +}; +const wrongAssignmentReference: SolutionManagerAssignment["application"] = { + kind: "solution", + programId: 12, + name: "Решение", + // @ts-expect-error Назначение решения не принимает projectId. + projectId: 301, +}; +// @ts-expect-error Выгрузка решения не принимает legacy kind проектов. +const wrongAnalyticsExport: SolutionAnalyticsExportQuery = { kind: "projects" }; + +const filteredExport: SolutionAnalyticsExportQuery = { + kind: "solutions", + fieldId: 123, + answerState: "not_applicable", + caseScope: "selected", + caseName: "Кейс", + scope: "awaiting_evaluation", +}; +// @ts-expect-error Глобальная сводка не принимает фильтры списка решений. +const filteredProgramExport: SolutionAnalyticsExportQuery = { kind: "program", fieldId: 123 }; +// @ts-expect-error Выгрузка оценок сохраняет полный scope. +const filteredScoresExport: SolutionAnalyticsExportQuery = { kind: "scores", search: "Кейс" }; +// @ts-expect-error XLSX включает все найденные решения, без pagination. +const paginatedSolutionsExport: SolutionAnalyticsExportQuery = { kind: "solutions", limit: 20 }; +const invalidAnswerState: SolutionAnalyticsExportQuery = { + kind: "solutions", + // @ts-expect-error Состояние ответа совпадает с C00, нового answered enum нет. + answerState: "answered", +}; + +void [ + filteredExport, + filteredProgramExport, + filteredScoresExport, + paginatedSolutionsExport, + invalidAnswerState, + noncompetitiveFunnel, + assignmentReference, + wrongAssignmentReference, + wrongAnalyticsExport, + validResultsExport, + invalidResultsExport, + role, + cleared, + text, + name, + answers, + emptyInvite, + missingVersion, + invalidRole, + emptySolution, + missingRevision, + validRating, + missingCriteriaRevision, + wrongRatingValue, + validPlan, + wrongPlan, + missingQuotaRevision, + solutionWidgetParticipant, + wrongWidgetReference, + wrongWidgetProjection, + solutionOrganizer, +]; + +declare const administrativeHistory: SolutionAdminChangeDetail; +declare const casePreview: SolutionCaseChangePreview; +const preservedSubmission: SolutionSubmissionSnapshot | null = administrativeHistory.beforeSnapshot; +const confirmedEffects: { + isSubmitted: boolean; + removedAssignmentsCount: number; + scoreCount: number; +} = casePreview; +const wrongHistoryReference: SolutionAdminChange["application"] = { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Решение", + // @ts-expect-error История решения не принимает идентификатор Project. + projectId: 301, +}; +void [preservedSubmission, confirmedEffects, wrongHistoryReference]; + +const memberRemoval: SolutionMemberRemovalPreviewRequest = { userId: 10 }; +const confirmedMemberRemoval: SolutionMemberRemovalApplyRequest = { + userId: 10, + previewToken: "opaque", +}; +// @ts-expect-error Подтверждение исключения требует подписанный preview token. +const unsignedMemberRemoval: SolutionMemberRemovalApplyRequest = { userId: 10 }; +// @ts-expect-error Команда решения не принимает Project ID. +const projectMemberRemoval: SolutionMemberRemovalPreviewRequest = { userId: 10, projectId: 301 }; +declare const historyEvent: SolutionAdminChange; +if (historyEvent.action === "remove_member") { + const removed: SolutionMemberAdminChange = historyEvent; + const affectedUser: number = removed.removedMember.user.id; + // @ts-expect-error Поля смены кейса не относятся к исключению участника. + const unrelatedCase: string = removed.caseName; + void [affectedUser, unrelatedCase]; +} +void [memberRemoval, confirmedMemberRemoval, unsignedMemberRemoval, projectMemberRemoval]; + +const archiveSelection: SolutionArchivePreviewRequest = { ids: [301] }; +const archiveConfirmation: SolutionArchiveApplyRequest = { ids: [301], previewToken: "opaque" }; +// @ts-expect-error Снятие заявки требует подтвержденный token. +const unsignedArchive: SolutionArchiveApplyRequest = { ids: [301] }; +// @ts-expect-error Archive API принимает IDs решений без Project ID. +const projectArchive: SolutionArchivePreviewRequest = { ids: [301], projectId: 301 }; +declare const archiveEvent: SolutionAdminChange; +if (archiveEvent.action === "archive") { + const archive: SolutionArchiveAdminChange = archiveEvent; + const stamp: string = archive.archivedAt; + // @ts-expect-error Архивирование не является сменой кейса. + const caseName: string = archive.caseName; + void [stamp, caseName]; +} +declare const draftArchive: SolutionArchiveSnapshot; +const capturedAt: string = draftArchive.capturedAt; +// @ts-expect-error Архивный снимок не означает искусственную сдачу. +const artificialSubmission: string = draftArchive.submittedAt; +void [ + archiveSelection, + archiveConfirmation, + unsignedArchive, + projectArchive, + capturedAt, + artificialSubmission, +]; + +const registrationSelection: SolutionRegistrationRemovalPreviewRequest = { registrationIds: [601] }; +const registrationConfirmation: SolutionRegistrationRemovalApplyRequest = { + registrationIds: [601], + previewToken: "opaque", +}; +// @ts-expect-error Подтверждение удаления регистраций требует signed proof. +const unsignedRegistrationRemoval: SolutionRegistrationRemovalApplyRequest = { + registrationIds: [601], +}; +// @ts-expect-error Команда принимает ID регистрации, а не поле userId. +const userRemoval: SolutionRegistrationRemovalPreviewRequest = { userId: 10 }; +const projectRegistrationRemoval: SolutionRegistrationRemovalPreviewRequest = { + registrationIds: [601], + // @ts-expect-error Команда не принимает Project ID. + projectId: 301, +}; +declare const registrationEvent: SolutionAdminChange; +if (registrationEvent.action === "remove_registrations") { + const typed: SolutionRegistrationAdminChange = registrationEvent; + const auditId: number = typed.registrationRemovalId; + // @ts-expect-error Смена кейса относится к другой ветке истории. + const unrelatedCase: string = typed.caseName; + void [auditId, unrelatedCase]; +} +void [ + registrationSelection, + registrationConfirmation, + unsignedRegistrationRemoval, + userRemoval, + projectRegistrationRemoval, +];