# Чек-лист миграции StroyBot → монорепо Каждый раздел считается перенесённым только когда все пункты проверены в workbench. Пишущие операции (создание / правка / удаление / импорт) проверяем **только на тестовом проекте «ТЕСТ StroyBot»**. Правила переноса: - Запросы, поля, фильтры, права и тексты переносятся **1:1** из исходного репозитория. - Известные баги переносятся как есть (помечены 🐞) — исправляем позже отдельными коммитами. - Старые страницы и web parts не трогаем, поддержка старых URL не нужна. Легенда: `[ ]` — не проверено, `[x]` — проверено в workbench. --- ## 0. Общее (оболочка) - [ ] Одна web part, разделы переключаются через `?page=…`, проект — через `?projectId=…` - [ ] Кнопки «назад/вперёд» браузера переключают разделы - [ ] Ссылку на любой раздел можно скопировать и открыть в новой вкладке - [ ] Выбранный проект сохраняется между перезагрузками (`sessionStorage`, ключ `stroybot_selected_project`) - [ ] Шрифты Inter и Lora подгружаются (раньше грузили report-list и assigner) - [ ] `webApiPermissionRequests` в одном пакете: `api.tnsu.kz` (`9232d9e2-…`, `Reports.Read` и `user_impersonation`) + Dataverse `https://tansu.crm5.dynamics.com` (`user_impersonation`) **Сознательные изменения (не функционал, а настройка):** - Настройки property pane из 8 web parts сведены в одну группу «Подключение». Имена списков больше не настраиваются в property pane — они в константах. - `worksPageUrl` (objects → works) больше не нужен: переход внутри приложения. --- ## 1. Главная (stroy-bot-main) ### Выбор проекта - [ ] Поле «Проект:», плейсхолдер «Выберите проект», иконка поиска - [ ] Список грузится при первом фокусе, «Загрузка проектов…» - [ ] При фокусе текст выделяется - [ ] Фильтр по Title (без учёта регистра, подстрока), максимум **7** результатов - [ ] У каждого — бейдж статуса, «Без статуса» если пусто - [ ] Сообщения: «Нет доступных проектов», «Проекты не найдены», «Ошибка загрузки. Попробуйте ещё раз.» - [ ] Клик вне / Esc — закрыть и вернуть выбранное название (или очистить) - [ ] Бейдж статуса рядом с полем: текущий — зелёный, завершен/завершён — серый, архивный — золотой, иначе — нейтральный - [ ] Сортировка по алфавиту, затем до 7 недавно активных (из Dataverse `cree9_reports`) — наверх - [ ] Права из «Доступы»: `read | edit0 | aproove`, `canEdit` = OR по `edit0` - [ ] Подсказка «Выберите проект, чтобы активировать ссылки ниже» ### Разделы проекта - [ ] Объекты / «Объекты по проекту» - [ ] Отчёты по проекту / «Просмотр отчётности» - [ ] Отправить отчёт / «Отправка отчёта по проекту» - [ ] Загрузить работы из сметы / «Импорт работ в систему» - [ ] Архив проекта / «Библиотека документов» → `{SITE}/Shared Documents/Архив/{Title}` (внешняя ссылка, новая вкладка) - [ ] Ресурсы проекта / «Профессии и компании» - [ ] Без выбранного проекта карточки неактивны - [ ] Статус «архивный» — неактивно всё, кроме Архива; экспорт тоже неактивен - [ ] Статус «завершен/завершён» — неактивно «Отправить отчёт» - [ ] `canEdit = false` — неактивно «Отправить отчёт» ### Экспорт данных «Выгрузить за период» - [ ] Кнопка «?» — поповер с 3 строками (день / Пн–Вс / произвольный), закрытие по клику мимо - [ ] Поля «Начало» и «Конец» видны только при выбранном проекте - [ ] «Укажите период для активации ссылки», пока даты не заданы - [ ] Ошибка «Дата начала не может быть позже даты конца», кнопка неактивна - [ ] Бейдж: «Ежедневный отчёт» (`daily`) / «Еженедельный отчёт» (Пн–Вс, `weekly`) / «Отчёт за период» (`period`) - [ ] «Выгрузить» → спиннер «Формируется…» → файл `{prefix}_{from}_{to}.xlsx` - [ ] Запрос `GET https://api.tnsu.kz/Report/StroyBot?start=…&end=…&projectId=…` (unix UTC 00:00:00 / 23:59:59) - [ ] Ошибки: «Ошибка API: {status} {statusText}» / «Ошибка при загрузке отчёта» **Сознательные отличия от main:** - Лишний запрос к «Доступы» без фильтра по пользователю (его результат не использовался) — убран, на поведение не влияет. - Список проектов грузится сразу при открытии, а не при первом фокусе: от статуса и прав проекта теперь зависит и меню слева. - Карточки разделов открывают раздел в этой же вкладке; новая вкладка — Ctrl+клик или средняя кнопка (раньше всегда `_blank`). Архив — по-прежнему в новой вкладке. - Ошибки выгрузки показываются под кнопкой, а не через `alert()`. Тексты те же. - Поповер «?» всегда открывается вниз (раньше переворачивался вверх у края экрана). - Адрес выгрузки берётся из настройки «Базовый URL API» (по умолчанию тот же `https://api.tnsu.kz`). - Меню слева скрывает разделы по правилам карточек: архивный — только «Проект»; завершён или нет `edit0` — без «Отправить отчёт»; «Привязка к справочнику» — только с `edit0`. --- ## 2. Объекты (stroy-bot-objects) — `?page=objects&projectId=` - [ ] Шапка «Объекты проекта» + название проекта («Загрузка...» / «Ошибка») - [ ] «Добавить объект» (только `canEdit`), «Экспорт» → «Экспортируется...» → `objects-{projectId}.xlsx` (`GET https://api.tnsu.kz/Report/ProjectObjects?projectId=`) - [ ] Без projectId — «Вы не выбрали проект»; «Проект с ID={id} не найден»; «Ошибка: …» - [ ] Пусто — «Объекты не найдены» + «+ Добавить объект» (если `canEdit`) - [ ] Тулбар «{N} объектов» (рекурсивно), «Развернуть», «Свернуть» - [ ] Дерево: отступ, иконка папки/файла, счётчик детей, шеврон, клик по строке — выделить + раскрыть - [ ] Раскрытые и выделенный узлы сохраняются при перезагрузке дерева после операций (в памяти страницы, как в старом коде; при обновлении страницы браузера — сбрасываются) - [ ] Уровни: 0 — Объект-пятно, 1 — Раздел, 2 — Вид работ (не глубже 3 уровней) - [ ] Контекстное меню (⋮ и правый клик, только `canEdit`): Добавить объект (не на уровне 2), Редактировать, Работы, Удалить - [ ] Модалка: «Название *» (обязательно, Enter — сохранить), «Родительский объект» (select только при правке Раздела: узлы уровня 0, сортировка ru, без себя и потомков), «Проект» (read-only) - [ ] Заголовки модалки: «Добавить Объект-пятно / Раздел / Вид работ», «Редактировать …», «— (корневой)» - [ ] Удаление: подтверждение с числом дочерних, «Это действие нельзя отменить.» - [ ] Drag & drop (только `canEdit`): до / внутрь / после, запрет на себя и потомков, ошибка «Не удалось переместить: …» - [ ] «Работы» → раздел Работы с `objectId` - [ ] После любой операции дерево перезагружается - [ ] Список «Объекты»: `Title`, `project` (текст = название проекта), `parent` (self-lookup) - [ ] Права: «Доступы» `projectId`/`userId` → `edit0` 🐞 Перетаскивание меняет только родителя, порядок не сохраняется. 🐞 Удаление узла не удаляет детей. 🐞 Зона сброса «внутрь» нулевой высоты — на практике узел можно бросить только «до» / «после» (перенесено как есть). **Сознательные отличия от objects:** - `alert()` → сообщение `Alert` на странице (ошибки переноса, удаления, экспорта) или в модалке (ошибка сохранения). Тексты те же. - `confirm()` удаления → `confirmModal` с кнопкой «Удалить»; текст тот же, но переносы строк ui-kit показывает одним абзацем. - Адрес сайта — настройка «URL сайта SharePoint» (раньше `pageContext.web.absoluteUrl`), экспорт — через клиент корпоративного API и настройку «Базовый URL API» (по умолчанию тот же `https://api.tnsu.kz`). --- ## 3. Работы (stroy-bot-works) — `?page=works&projectId=&objectId=` - [ ] Шапка с названием проекта - [ ] «Добавить работу» (только `canEdit`) - [ ] Таблица-дерево: #, Наименование, Ед. изм., Проектный объём (ru-RU, 3 знака), Дата (`dd.mm.yy`, «start – finish» / одна / «—») - [ ] Родители свёрнуты по умолчанию, шеврон «Свернуть/Развернуть», нумерация по видимым строкам - [ ] «Загрузка данных...», «Работы не найдены» (+ кнопка), ошибки Dataverse - [ ] Меню строки ⋮ (`canEdit`): Добавить дочернюю, Редактировать, Удалить - [ ] Модалка: НАИМЕНОВАНИЕ *, ЕД. ИЗМ. (список «Единицы измерения», с пагинацией; текущее значение не из списка — добавить наверх), ПРОЕКТНЫЙ ОБЪЁМ (шаг 0.001), РОДИТЕЛЬСКАЯ РАБОТА (без себя и потомков), ОБЪЕКТ (read-only), ДАТА НАЧАЛА / ОКОНЧАНИЯ - [ ] «Сохранить и добавить еще» (в режиме добавления: модалка остаётся, родитель сохраняется) - [ ] Подтверждение удаления «Подтвердите удаление» - [ ] Dataverse `cree9_tables`, фильтр `cree9_objectid eq '{objectId}'`; без objectId — все работы - [ ] Навигационное свойство родителя определяется через `EntityDefinitions` (fallback `cree9_parentid`) - [ ] Проект находится подъёмом по родителям объекта до непустого `project` 🐞 Путь объекта (`#headerTitle`) не выводится. 🐞 Нет проверки «начало ≤ окончание». 🐞 Отступы в списке «Родительская работа» — обычные пробелы, браузер их схлопывает (как было). **Сознательные отличия от works:** - `alert()` → сообщение `Alert`: ошибка сохранения — в модалке, ошибки удаления — над таблицей. Тексты те же. - Своё окно подтверждения удаления → `confirmModal` из ui-kit с тем же заголовком, текстом и кнопками. - PATCH и DELETE идут через тот же AAD-клиент Dataverse, а не через `fetch` с токеном вручную; заголовки те же. - Адрес сайта — настройка «URL сайта SharePoint» (раньше `pageContext.web.absoluteUrl`), Dataverse — настройка «URL Dataverse»; имя таблицы — константа (раньше настройка `worksTable`). - Таблица — `DataTable` из ui-kit; меню ⋮ — общее меню из `pages/_tree`. --- ## 4. Отчёты по проекту (stroy-bot-report-list) — `?page=reports&projectId=` - [ ] Заголовок — название проекта - [ ] Нет доступа — «Нет доступа» / «У вас нет прав на просмотр отчётов данного проекта.» - [ ] Период по умолчанию — текущая неделя Пн–Вс; «С:», «По:», «Применить» - [ ] Поиск «Поиск по любому полю...» (дата, номер, автор, компания, путь объекта, комментарий) - [ ] Карточка «Отправить отчёт» (если `edit0 || aproove`) - [ ] Карточки: `#номер`, дата, «✓ Утверждено», Пользователь, Компания (или «ID: n»), Объект (путь), Комментарий - [ ] Копировать (`edit0 || aproove`) → форма отчёта с `copyFromId` - [ ] Удалить (`aproove`, или `edit0` и не утверждён) → «Удалить отчёт?» → мягкое удаление `cree9_deleted = true` - [ ] Клик / Enter / Space по карточке → форма отчёта с `reportId` - [ ] Сортировка: дата desc, номер desc - [ ] Пусто — «Отчёты не найдены за выбранный период.» - [ ] Права: «Доступы» `user/Id` + `project/Id` (lookup) 🐞 Нет пагинации Dataverse (`@odata.nextLink` не читается). 🐞 Неделя по умолчанию считается в UTC. **Сознательные отличия от report-list:** - Карточка отчёта и «Создать на основе» открывают раздел «Форма отчёта» этого приложения (`?page=report&projectId=&reportId=` / `©FromId=`) — по-прежнему в новой вкладке. «Создать на основе» теперь настоящая ссылка (работает Ctrl+клик / средняя кнопка). - «Отправить отчёт» — переход в раздел формы отчёта в этой же вкладке (раньше `SendReport.aspx?projectId=` тоже в этой вкладке). - Списки читаются с сайта из настройки «URL сайта SharePoint» (по умолчанию `…/sites/stroy`), а не с сайта, где стоит web part. Id пользователя — по-прежнему `legacyPageContext.userId`. - Тёмная тема (`onThemeChanged`) и загрузка шрифтов не переносились: шрифты грузит оболочка, оформление — ui-kit. - Модалка удаления — `Dialog` из ui-kit (тексты те же, номер отчёта жирным). --- ## 5. Форма отчёта (stroy-bot-report) — `?page=report&projectId=[&reportId=|©FromId=]` ### Режимы - [ ] Нет projectId — «Не указан projectId в URL» - [ ] `reportId`: NoAccess / Approve (`aproove && !approved`) / Edit (`edit0 && !approved && дней < editDays`, по умолчанию 5) / ReadOnly - [ ] `copyFromId`: Create, предзаполнены объект, компания, смена, комментарий, работы (с персоналом/техникой), персонал, техника; дата — сегодня, номер — 0, фото — пусто - [ ] Без параметров — Create, смена — первая из «Смены», дата — сегодня ### Поля - [ ] «← Вернуться назад» - [ ] Заголовок «Отчет №N» / «Новый отчет», «Проект: …», бейдж «Утверждён» / «На утверждении» - [ ] Дата отчета*, Смена*, Объект* (пикер с путями; смена объекта очищает работы), Компания* (с фильтром по «Компании по проекту», fallback — все) - [ ] Работы: «+ Выбрать работу» («Сначала выберите объект»), Значение, План-факт (П / Ф / +/-, цвет), Комментарий - [ ] Вложенные «Персонал на работе» / «Техника на работе»: Кол-во, Часы, «+ Добавить профессию / технику» - [ ] Персонал: Кол-во, Часы, Тип часов (работы / простоя), Комментарий (обязателен для простоя) - [ ] Техника: то же + «Часы ремонта» (комментарий обязателен для простоя и ремонта) - [ ] Общий комментарий (contentEditable), Enter не отправляет форму SharePoint - [ ] Фото: «Открыть папку с фото», «Добавить фото» (multiple), превью с удалением ### Пикер - [ ] Поиск, группы для техники («Фильтры», чипы, «Все»), drill-down для работ (крошки «Все работы › …», «Выбрать «X»», «Нет работ на этом уровне») - [ ] Создание новой техники и новой работы (Ед. изм., План, Родительская работа) ### Сохранение - [ ] Валидация (все сообщения из исходника) - [ ] Номер = количество неудалённых отчётов проекта + 1 - [ ] При правке — удалить все старые строки (сначала дочерние), создать заново - [ ] Фото → `DocLib/{projectId}_{number}` - [ ] «Отчет успешно сохранен!», кнопка меняется на «← Вернуться назад» - [ ] Утверждение: подтверждение → `cree9_aprooved = true` → «Отчет утвержден!» - [ ] Права: «Доступы» `projectId` + `userId*` (несколько вариантов имени поля) 🐞 Create без проверки NoAccess. 🐞 Номер может совпасть после мягкого удаления. 🐞 Дата «сегодня» — UTC. **Сознательные отличия от report:** - `confirm('Утвердить отчет? …')` → `confirmModal` из ui-kit, текст тот же. - Адрес сайта для списков и ссылки на папку фото — настройка «URL сайта SharePoint» (раньше `pageContext.web.absoluteUrl`); путь папки при создании — по-прежнему от сайта, где стоит страница (`web.serverRelativeUrl`). По умолчанию это один и тот же сайт `/sites/stroy`. - Если Dataverse ответит на создание записи `204` без тела, старый код брал id из заголовка `OData-EntityId`, а новый клиент заголовков не видит. С `Prefer: return=representation` Dataverse отвечает `201` с телом, поэтому на практике не встречается; при пустом id сохранение останавливается той же ошибкой `reportId is empty`. - Логика режимов, валидации, сборки строк и порядка записи / удаления сверена со старым кодом построчно и покрыта тестами (45 тестов). --- ## 6. Импорт работ (stroy-bot-import-works) — `?page=import&projectId=` - [ ] «ИМПОРТ РАБОТ» + название проекта - [ ] Нет projectId — «Не указан projectId в URL»; нет `UniqueId0` — «Проект не привязан к справочнику проектов. Обратитесь к администратору.» - [ ] Загрузка работ из API постранично по 200 с прогрессом «Загрузка работ... X из Y» - [ ] Выбор объекта (модалка с поиском, сброс ×) - [ ] Таблица: чекбокс, #, Наименование (+ бейдж объекта, «✓ импортировано», подзаголовок), Код СНБ, Ед. изм., Объём - [ ] Поиск с debounce 250 мс по всем полям; пагинация по 100 - [ ] «Выделить всё на странице» / «Снять все на странице» (фактически — все отфильтрованные) - [ ] Уже импортированные — неактивны, «Уже импортировано» - [ ] «Импортировать» (нужен объект, иначе тост) и «Импорт с созданием объекта» (поиск/создание объекта и раздела по названиям из сметы) - [ ] Подсказка «Объекты из сметы:» (3 строки + «ещё N») - [ ] Тосты «✓ Успешно импортировано: N» / «Импортировано: N, ошибок: M» - [ ] Защита от дублей по `cree9_comment` = id работы из сметы 🐞 Пагинация в модалке объектов не выводится (видны только первые 100). **Сознательные отличия от import-works:** - Адрес Dataverse берётся из настройки «URL Dataverse», адрес сайта со списками — из «URL сайта SharePoint», API — из «Базовый URL API» (значения по умолчанию прежние; раньше Dataverse был захардкожен). - Если объект выбрать до окончания загрузки работ, загрузка продолжается (раньше перерисовка показывала «Работы не найдены», пока загрузка не заканчивалась). - Клик по ячейке с флажком вне самого флажка строку не переключает (флажок ui-kit обёрнут в label — иначе было бы двойное переключение). Клик по остальной части строки работает как раньше. - Тосты при нескольких подряд показываются стопкой, а не друг поверх друга. - Окно «Выбор объекта» — `Dialog` из ui-kit (у крестика появилась подпись «Закрыть»); поле «Объект» доступно с клавиатуры. --- ## 7. Ресурсы проекта (stroy-bot-project-assigner) — `?page=resources&projectId=` - [ ] Нет projectId — «Проект не найден. Откройте страницу через список проектов.» - [ ] Вкладки «Профессии (N)» и «Компании (N)» - [ ] Слева «Доступные»: поиск, очистка ✕, «уже добавлено», «+», «Ничего не найдено» / «Нет элементов» - [ ] Справа «Привязаны к проекту»: «−», «Ничего не добавлено» - [ ] Оптимистичное добавление / удаление с откатом и ошибкой - [ ] На узких экранах колонки друг под другом 🐞 Удаление ещё не сохранённой записи (временный отрицательный id) уходит на сервер, падает и откатывается с ошибкой. 🐞 Если запись удалили до ответа на добавление, она пропадает из списка, хотя на сервере создана. 🐞 Два добавления в одну миллисекунду получают одинаковый временный id. **Сознательные отличия от assigner:** - PnPjs заменён на SharePoint REST с теми же `$select` / `$expand` / `$filter`. Постраничная загрузка PnP (`for await`) → проход по `@odata.nextLink` / `odata.nextLink` до конца (без `$top`, страницы по умолчанию — 100). - Добавление: `POST …/items` с тем же телом (`Title`, `projectIdId`, `professionIdId` / `companyIdId`), заголовки `Accept` и `Content-Type: application/json;odata=nometadata` + пустой `odata-version` (как в binder). Удаление: `POST …/items({id})` с `X-HTTP-Method: DELETE` и `IF-MATCH: *` — так удаляет сам PnP. - Текст после префикса ошибки («Ошибка загрузки данных: …» и т.п.) — сообщение `HttpError` (`{status} {statusText}: {ответ сервера}`), а не формат ошибки PnP. - Счётчик во вкладке — часть подписи («Профессии (0)»): `count` в Tabs ui-kit скрывает ноль. - Кнопки «+» / «−» — `Button` ui-kit (с подсказками «Добавить» / «Удалить»), сообщение об ошибке закрывается крестиком рядом с `Alert`. - 🔧 **Исправлено (найдено при проверке T-RES, 2026-10-16):** перед добавлением проверяется, нет ли уже такой привязки на сервере. Раньше при двух открытых вкладках (или двух пользователях) одна профессия / компания добавлялась дважды. Если привязка уже есть — новая запись не создаётся, показывается существующая. --- ## 8. Привязка к справочнику (stroy-bot-projects-binder) — `?page=binder&projectId=` - [ ] Карточка проекта: название, Описание, Адрес, Статус (цвет), Начало, Окончание, Координаты - [ ] Бейдж «Привязан» / «Не привязан», `UniqueName · UniqueId0`, «Отвязать» - [ ] Поиск по справочнику (debounce 400 мс, пустой — все), пагинация ← / →, «{page} / {total} · всего: N» - [ ] Карточки результатов: name, Код, Описание, Адрес, Координаты; выбор / снятие выбора; «Связать» - [ ] Сообщения успеха и ошибок - [ ] Запись `UniqueId0` и `UniqueName` в «Проекты» 🐞 Ответы поиска не сверяются с последним запросом — при быстром вводе может победить устаревший. 🐞 У результата без полей `latitude`/`longitude` выводится «Координаты: undefined, undefined» (проверка только на `null`). **Сознательные отличия от binder:** - Сайт списка «Проекты» — из общей настройки «URL сайта SharePoint» (было отдельное свойство `listSiteUrl`, значение по умолчанию то же), адрес API — из «Базовый URL API». - У поля поиска своя кнопка очистки ✕ («Очистить») вместо встроенной в Fluent `SearchBox`; очистка по Esc не переносилась. - Сообщения успеха / ошибки сохранения закрываются крестиком рядом с `Alert` (у `Alert` ui-kit нет закрытия). - Статус, «Привязан» / «Не привязан» — `Badge` ui-kit тех же цветов; кнопка «Связать» — `primary` ui-kit вместо принудительно синей Fluent. --- ## Открытые вопросы - ✅ Схема списка **«Доступы»** проверена на сайте (2026-10-16): `project` — Lookup на «Проекты», `user` — Person, права `read` / `edit0` / `aproove`. SharePoint отдаёт к ним `projectId` / `userId` — поэтому оба способа из старых репозиториев (`project/Id` + `$expand` и `projectId` / `userId`) обращаются к одним и тем же полям. Расхождения нет. - ✅ Схема списка **«Объекты»** проверена на сайте (2026-10-16): `parent` — Lookup («Объект»), SharePoint отдаёт к нему `parentId` — чтение через `parent/Id` и запись через `parentId` обращаются к одному полю. `project` — Text (название проекта), как и ожидает код. - ℹ️ В «Объектах» есть поля `level` (Number) и `skipLevelUpdate` (Boolean), которые не писал ни один старый репозиторий — вероятно, уровень проставляет поток Power Automate. Новое приложение их тоже не трогает. При проверке T-OBJ посмотреть, что у созданных объектов `level` проставился.