From b868e8012d5f1da21dc35e1b6da0b21f6ccdc234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Thu, 15 Oct 2026 16:42:37 +0500 Subject: [PATCH] feat: add StroyBot app shell with URL routing One web part replaces the separate stroy-bot-* web parts. The section and its parameters live in the page query string (?page=objects&projectId=5), so links can be copied, opened in a new tab and the browser Back button works. - shared config: site/Dataverse/API addresses, list and table names - HTTP layer behind IHttpClient so repositories can be unit-tested - ui-kit AppLayout + Sidebar, placeholder for sections not ported yet - migration checklist with every feature of the old web parts - design tokens mirrored from ui-kit instead of 8 copies Co-Authored-By: Claude Opus 5.5 (1M context) --- config/config.json | 10 +- docs/MIGRATION-CHECKLIST.md | 213 ++++++++++++++++++ package-lock.json | 2 + package.json | 2 + src/styles/_design-tokens.scss | 165 ++++++++++++++ .../stroyBot/StroyBotWebPart.manifest.json | 25 ++ src/webparts/stroyBot/StroyBotWebPart.ts | 58 +++++ src/webparts/stroyBot/app/App.tsx | 22 ++ .../stroyBot/app/AppServicesContext.ts | 14 ++ .../stroyBot/app/AppShell.module.scss | 7 + src/webparts/stroyBot/app/AppShell.tsx | 42 ++++ src/webparts/stroyBot/app/PageOutlet.tsx | 13 ++ .../stroyBot/app/SectionPlaceholder.tsx | 8 + src/webparts/stroyBot/app/navigation.test.ts | 34 +++ src/webparts/stroyBot/app/navigation.ts | 39 ++++ .../stroyBot/app/routing/RouteLink.tsx | 43 ++++ .../stroyBot/app/routing/RouterContext.tsx | 49 ++++ .../stroyBot/app/routing/routes.test.ts | 20 ++ src/webparts/stroyBot/app/routing/routes.ts | 70 ++++++ src/webparts/stroyBot/app/texts.ts | 18 ++ src/webparts/stroyBot/config/constants.ts | 23 ++ src/webparts/stroyBot/config/dataverse.ts | 15 ++ src/webparts/stroyBot/config/lists.ts | 23 ++ src/webparts/stroyBot/services/AppServices.ts | 70 ++++++ .../stroyBot/services/http/IHttpClient.ts | 51 +++++ .../stroyBot/services/http/spfxClients.ts | 43 ++++ .../shared/components/EmptyState.module.scss | 39 ++++ .../stroyBot/shared/components/EmptyState.tsx | 23 ++ .../StroyBotMonorepoWebPart.manifest.json | 28 --- .../StroyBotMonorepoWebPart.ts | 121 ---------- .../stroyBotMonorepo/assets/welcome-dark.png | Bin 12545 -> 0 bytes .../stroyBotMonorepo/assets/welcome-light.png | Bin 12816 -> 0 bytes .../components/IStroyBotMonorepoProps.ts | 7 - .../components/StroyBotMonorepo.module.scss | 34 --- .../components/StroyBotMonorepo.tsx | 43 ---- src/webparts/stroyBotMonorepo/loc/en-us.js | 16 -- .../stroyBotMonorepo/loc/mystrings.d.ts | 19 -- 37 files changed, 1135 insertions(+), 274 deletions(-) create mode 100644 docs/MIGRATION-CHECKLIST.md create mode 100644 src/styles/_design-tokens.scss create mode 100644 src/webparts/stroyBot/StroyBotWebPart.manifest.json create mode 100644 src/webparts/stroyBot/StroyBotWebPart.ts create mode 100644 src/webparts/stroyBot/app/App.tsx create mode 100644 src/webparts/stroyBot/app/AppServicesContext.ts create mode 100644 src/webparts/stroyBot/app/AppShell.module.scss create mode 100644 src/webparts/stroyBot/app/AppShell.tsx create mode 100644 src/webparts/stroyBot/app/PageOutlet.tsx create mode 100644 src/webparts/stroyBot/app/SectionPlaceholder.tsx create mode 100644 src/webparts/stroyBot/app/navigation.test.ts create mode 100644 src/webparts/stroyBot/app/navigation.ts create mode 100644 src/webparts/stroyBot/app/routing/RouteLink.tsx create mode 100644 src/webparts/stroyBot/app/routing/RouterContext.tsx create mode 100644 src/webparts/stroyBot/app/routing/routes.test.ts create mode 100644 src/webparts/stroyBot/app/routing/routes.ts create mode 100644 src/webparts/stroyBot/app/texts.ts create mode 100644 src/webparts/stroyBot/config/constants.ts create mode 100644 src/webparts/stroyBot/config/dataverse.ts create mode 100644 src/webparts/stroyBot/config/lists.ts create mode 100644 src/webparts/stroyBot/services/AppServices.ts create mode 100644 src/webparts/stroyBot/services/http/IHttpClient.ts create mode 100644 src/webparts/stroyBot/services/http/spfxClients.ts create mode 100644 src/webparts/stroyBot/shared/components/EmptyState.module.scss create mode 100644 src/webparts/stroyBot/shared/components/EmptyState.tsx delete mode 100644 src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.manifest.json delete mode 100644 src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.ts delete mode 100644 src/webparts/stroyBotMonorepo/assets/welcome-dark.png delete mode 100644 src/webparts/stroyBotMonorepo/assets/welcome-light.png delete mode 100644 src/webparts/stroyBotMonorepo/components/IStroyBotMonorepoProps.ts delete mode 100644 src/webparts/stroyBotMonorepo/components/StroyBotMonorepo.module.scss delete mode 100644 src/webparts/stroyBotMonorepo/components/StroyBotMonorepo.tsx delete mode 100644 src/webparts/stroyBotMonorepo/loc/en-us.js delete mode 100644 src/webparts/stroyBotMonorepo/loc/mystrings.d.ts diff --git a/config/config.json b/config/config.json index ea22522..6f30b2f 100644 --- a/config/config.json +++ b/config/config.json @@ -2,17 +2,15 @@ "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", "version": "2.0", "bundles": { - "stroy-bot-monorepo-web-part": { + "stroy-bot-web-part": { "components": [ { - "entrypoint": "./lib/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.js", - "manifest": "./src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.manifest.json" + "entrypoint": "./lib/webparts/stroyBot/StroyBotWebPart.js", + "manifest": "./src/webparts/stroyBot/StroyBotWebPart.manifest.json" } ] } }, "externals": {}, - "localizedResources": { - "StroyBotMonorepoWebPartStrings": "lib/webparts/stroyBotMonorepo/loc/{locale}.js" - } + "localizedResources": {} } diff --git a/docs/MIGRATION-CHECKLIST.md b/docs/MIGRATION-CHECKLIST.md new file mode 100644 index 0000000..ddac953 --- /dev/null +++ b/docs/MIGRATION-CHECKLIST.md @@ -0,0 +1,213 @@ +# Чек-лист миграции 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}» / «Ошибка при загрузке отчёта» + +🐞 Лишний запрос к «Доступы» без фильтра по пользователю. + +--- + +## 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` + +🐞 Перетаскивание меняет только родителя, порядок не сохраняется. 🐞 Удаление узла не удаляет детей. + +--- + +## 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`) не выводится. 🐞 Нет проверки «начало ≤ окончание». + +--- + +## 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. + +--- + +## 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. + +--- + +## 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). + +--- + +## 7. Ресурсы проекта (stroy-bot-project-assigner) — `?page=resources&projectId=` + +- [ ] Нет projectId — «Проект не найден. Откройте страницу через список проектов.» +- [ ] Вкладки «Профессии (N)» и «Компании (N)» +- [ ] Слева «Доступные»: поиск, очистка ✕, «уже добавлено», «+», «Ничего не найдено» / «Нет элементов» +- [ ] Справа «Привязаны к проекту»: «−», «Ничего не добавлено» +- [ ] Оптимистичное добавление / удаление с откатом и ошибкой +- [ ] На узких экранах колонки друг под другом + +--- + +## 8. Привязка к справочнику (stroy-bot-projects-binder) — `?page=binder&projectId=` + +- [ ] Карточка проекта: название, Описание, Адрес, Статус (цвет), Начало, Окончание, Координаты +- [ ] Бейдж «Привязан» / «Не привязан», `UniqueName · UniqueId0`, «Отвязать» +- [ ] Поиск по справочнику (debounce 400 мс, пустой — все), пагинация ← / →, «{page} / {total} · всего: N» +- [ ] Карточки результатов: name, Код, Описание, Адрес, Координаты; выбор / снятие выбора; «Связать» +- [ ] Сообщения успеха и ошибок +- [ ] Запись `UniqueId0` и `UniqueName` в «Проекты» + +--- + +## Открытые вопросы + +- Схема списка **«Доступы»**: в репозиториях встречаются и поля `projectId`/`userId`, и lookup `user`/`project`. Проверить на сайте. +- Схема списка **«Объекты»**: родитель читается то как `parentId`, то как `parent/Id`. Проверить на сайте. diff --git a/package-lock.json b/package-lock.json index d4daea8..2f6ee55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "@fluentui/react": "^8.106.4", "@microsoft/sp-component-base": "1.22.1", "@microsoft/sp-core-library": "1.22.1", + "@microsoft/sp-http": "1.22.1", + "@microsoft/sp-loader": "1.22.1", "@microsoft/sp-lodash-subset": "1.22.1", "@microsoft/sp-office-ui-fabric-core": "1.22.1", "@microsoft/sp-property-pane": "1.22.1", diff --git a/package.json b/package.json index 592a5dd..c033360 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@fluentui/react": "^8.106.4", "@microsoft/sp-component-base": "1.22.1", "@microsoft/sp-core-library": "1.22.1", + "@microsoft/sp-http": "1.22.1", + "@microsoft/sp-loader": "1.22.1", "@microsoft/sp-lodash-subset": "1.22.1", "@microsoft/sp-office-ui-fabric-core": "1.22.1", "@microsoft/sp-property-pane": "1.22.1", diff --git a/src/styles/_design-tokens.scss b/src/styles/_design-tokens.scss new file mode 100644 index 0000000..20dde5c --- /dev/null +++ b/src/styles/_design-tokens.scss @@ -0,0 +1,165 @@ +// ============================================ +// ЗЕРКАЛО токенов пакета @tnsu/ui-kit (src/tokens/_tokens.scss). +// Источник правды — ui-kit. Локальная копия нужна потому, что heft не резолвит +// scss из node_modules через ~. При изменении токенов в ui-kit — синхронизировать. +// Заменяет 8 копий design-tokens-v5.scss из старых репозиториев stroy-bot-*. +// ============================================ + +// ---- Navy (Primary) ---- +$navy-950: #0A1B30; +$navy-900: #0F2440; +$navy-800: #1B3A5C; +$navy-700: #244B73; +$navy-600: #2D5C8A; +$navy-100: #E8EEF4; +$navy-50: #F4F7FA; + +// ---- Gold (Accent) ---- +$gold-600: #B8881A; +$gold-500: #D4A01E; +$gold-400: #E5B635; +$gold-300: #F0CC5E; +$gold-100: #FDF5E0; +$gold-50: #FEFBF0; + +// ---- Gray (Neutral) ---- +$gray-900: #111827; +$gray-700: #374151; +$gray-500: #6B7280; +$gray-400: #9CA3AF; +$gray-300: #D1D5DB; +$gray-200: #E5E7EB; +$gray-100: #F3F4F6; +$gray-50: #F9FAFB; + +// ---- Semantic ---- +$red-500: #DC2626; +$red-50: #FEF2F2; +$green-600: #15803D; +$green-50: #F0FDF4; +$blue-600: #2563EB; +$blue-50: #EFF6FF; +$amber-700: #854D0E; +$amber-50: #FBF1D9; // светлый янтарный фон предупреждений (пара к $amber-700) + +$white: #fff; + +// ---- Typography ---- +// Заголовки страниц и секций — Lora (serif). Остальной текст — Inter (sans). +$font-serif: 'Lora', Georgia, serif; +$font-sans: 'Inter', -apple-system, 'Segoe UI', sans-serif; + +$fs-page-title: 24px; // Lora 600 +$fs-form-title: 22px; // Lora 600 +$fs-section-title: 15px; // Lora 600 +$fs-body: 14px; // Inter 400 +$fs-sm: 13px; +$fs-label: 12px; // Inter 500 +$fs-caption: 11px; +$fs-overline: 10.5px; // uppercase, letter-spacing 1.4-1.8px + +// ---- Radius ---- +$r-sm: 6px; +$r-md: 10px; +$r-lg: 14px; +$r-full: 100px; + +// ---- Shadows ---- +$sh-1: 0 1px 2px rgba(15,36,64,.04), 0 1px 3px rgba(15,36,64,.06); +$sh-2: 0 4px 16px rgba(15,36,64,.07); +$sh-3: 0 12px 32px rgba(15,36,64,.10); +$sh-gold: 0 4px 20px rgba(212,160,30,.18); +$sh-modal: 0 24px 70px rgba(10,27,48,.25); + +// ---- Gradients (фирменные) ---- +$grad-header: linear-gradient(135deg, $navy-950 0%, $navy-800 85%); +$grad-gold: linear-gradient(135deg, $gold-400, $gold-500); +$grad-gold-v: linear-gradient(180deg, $gold-400, $gold-600); +$grad-hairline: linear-gradient(90deg, $gold-400, transparent 70%); + +// ---- Spacing ---- +$sp-1: 4px; $sp-2: 8px; $sp-3: 12px; +$sp-4: 16px; $sp-5: 20px; $sp-6: 24px; +$sp-7: 28px; $sp-8: 32px; + +// ---- Готовые миксины ---- +@mixin page-header { + background: $grad-header; + border-radius: $r-lg; + padding: 26px 30px; + position: relative; + overflow: hidden; + &::before { + content: ''; + position: absolute; + left: 0; right: 0; bottom: 0; + height: 2px; + background: linear-gradient(90deg, $gold-500, transparent 60%); + } +} + +@mixin card-hover-gold { + transition: .22s; + &:hover { + box-shadow: $sh-3; + transform: translateY(-2px); + border-color: $gold-300; + } +} + +@mixin btn-accent { + background: $grad-gold; + color: $navy-950; + font-weight: 600; + border: 1px solid $gold-500; + &:hover { box-shadow: $sh-gold; transform: translateY(-1px); } +} + +@mixin input-focus-gold { + &:focus { + outline: 0; + border-color: $gold-500; + box-shadow: 0 0 0 3px rgba(212,160,30,.12); + } +} + +// Вид поля «только чтение» (режим согласующего / отправленная заявка): +// приглушённый фон, серый текст, без золотого фокуса — визуально «неактивно». +// $sel — псевдокласс состояния поля (:read-only для input/textarea, :disabled для select). +@mixin readonly-field($sel) { + &#{$sel}, + &#{$sel}:focus { + background: $gray-50; + color: $gray-500; + border-color: $gray-200; + box-shadow: none; + cursor: default; + } +} + +@mixin hairline { + height: 1px; + background: $grad-hairline; + border: 0; +} + +// ---- Брейкпоинты (адаптивность) ---- +// Единые точки перелома для ВСЕХ компонентов ui-kit. Используй миксин below(): +// @include below($bp-md) { ... правила для экранов уже 768px ... } +$bp-xs: 350px; // очень маленькая мобилка (страховочный минимум) +$bp-sm: 640px; // крупный телефон +$bp-md: 768px; // планшет / граница «мобилка ↔ десктоп» +$bp-lg: 1024px; // небольшой ноутбук +$bp-xl: 1280px; // широкий монитор + +@mixin below($bp) { + @media (max-width: $bp) { + @content; + } +} + +@mixin above($bp) { + @media (min-width: ($bp + 1)) { + @content; + } +} diff --git a/src/webparts/stroyBot/StroyBotWebPart.manifest.json b/src/webparts/stroyBot/StroyBotWebPart.manifest.json new file mode 100644 index 0000000..3517a4e --- /dev/null +++ b/src/webparts/stroyBot/StroyBotWebPart.manifest.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", + "id": "619a5eba-d596-47e5-a95a-9b9682ed7ec3", + "alias": "StroyBotWebPart", + "componentType": "WebPart", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + "requiresCustomScript": false, + "supportedHosts": ["SharePointWebPart", "SharePointFullPage"], + "supportsThemeVariants": true, + + "preconfiguredEntries": [ + { + "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced + "group": { "default": "Advanced" }, + "title": { "default": "StroyBot" }, + "description": { "default": "StroyBot: проекты, объекты, работы и отчёты" }, + "officeFabricIconFontName": "ConstructionCone", + "properties": {} + } + ] +} diff --git a/src/webparts/stroyBot/StroyBotWebPart.ts b/src/webparts/stroyBot/StroyBotWebPart.ts new file mode 100644 index 0000000..cb09cf6 --- /dev/null +++ b/src/webparts/stroyBot/StroyBotWebPart.ts @@ -0,0 +1,58 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; +import { Version } from '@microsoft/sp-core-library'; +import { IPropertyPaneConfiguration, PropertyPaneTextField } from '@microsoft/sp-property-pane'; +import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; +import { SPComponentLoader } from '@microsoft/sp-loader'; +import { App } from './app/App'; +import { createAppServices, IConnectionSettings } from './services/AppServices'; +import { FONTS_CSS_URL } from './config/constants'; +import { PROPERTY_PANE_TEXTS } from './app/texts'; + +export interface IStroyBotWebPartProps extends IConnectionSettings {} + +export default class StroyBotWebPart extends BaseClientSideWebPart { + protected async onInit(): Promise { + // Inter и Lora — шрифты дизайн-системы. Раньше их грузили report-list и assigner, + // остальные web parts полагались на шрифты, установленные у пользователя. + SPComponentLoader.loadCss(FONTS_CSS_URL); + await super.onInit(); + } + + public render(): void { + const services = createAppServices(this.context, this.properties); + ReactDom.render(React.createElement(App, { services }), this.domElement); + } + + protected onDispose(): void { + ReactDom.unmountComponentAtNode(this.domElement); + } + + protected get dataVersion(): Version { + return Version.parse('1.0'); + } + + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + const texts = PROPERTY_PANE_TEXTS; + return { + pages: [ + { + header: { description: texts.header }, + groups: [ + { + groupName: texts.connectionGroup, + groupFields: [ + PropertyPaneTextField('siteUrl', { label: texts.siteUrl, placeholder: texts.siteUrlPlaceholder }), + PropertyPaneTextField('dataverseUrl', { + label: texts.dataverseUrl, + placeholder: texts.dataverseUrlPlaceholder + }), + PropertyPaneTextField('apiBaseUrl', { label: texts.apiBaseUrl, placeholder: texts.apiBaseUrlPlaceholder }) + ] + } + ] + } + ] + }; + } +} diff --git a/src/webparts/stroyBot/app/App.tsx b/src/webparts/stroyBot/app/App.tsx new file mode 100644 index 0000000..4364ecc --- /dev/null +++ b/src/webparts/stroyBot/app/App.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import '@tnsu/ui-kit/styles.css'; +import { ConfirmHost, IconSprite } from '@tnsu/ui-kit'; +import { IAppServices } from '../services/AppServices'; +import { AppServicesContext } from './AppServicesContext'; +import { RouterProvider } from './routing/RouterContext'; +import { AppShell } from './AppShell'; + +interface Props { + services: IAppServices; +} + +/** Корень приложения: стили и общие хосты ui-kit (спрайт иконок, модалка подтверждения) монтируются один раз. */ +export const App: React.FC = ({ services }) => ( + + + + + + + +); diff --git a/src/webparts/stroyBot/app/AppServicesContext.ts b/src/webparts/stroyBot/app/AppServicesContext.ts new file mode 100644 index 0000000..de62d69 --- /dev/null +++ b/src/webparts/stroyBot/app/AppServicesContext.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { IAppServices } from '../services/AppServices'; + +// Клиенты к SharePoint / Dataverse / API нужны почти каждой странице — +// поэтому они в контексте, а не передаются пропсами через всё дерево. +export const AppServicesContext = React.createContext(undefined); + +export function useAppServices(): IAppServices { + const services = React.useContext(AppServicesContext); + if (!services) { + throw new Error('useAppServices: компонент отрисован вне AppServicesContext.Provider'); + } + return services; +} diff --git a/src/webparts/stroyBot/app/AppShell.module.scss b/src/webparts/stroyBot/app/AppShell.module.scss new file mode 100644 index 0000000..ba9076e --- /dev/null +++ b/src/webparts/stroyBot/app/AppShell.module.scss @@ -0,0 +1,7 @@ +@import '../../../styles/design-tokens'; + +.content { + width: 100%; + font-family: $font-sans; + color: $gray-900; +} diff --git a/src/webparts/stroyBot/app/AppShell.tsx b/src/webparts/stroyBot/app/AppShell.tsx new file mode 100644 index 0000000..745e9c8 --- /dev/null +++ b/src/webparts/stroyBot/app/AppShell.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { AppLayout, Sidebar, SidebarToggle } from '@tnsu/ui-kit'; +import { useRouter } from './routing/RouterContext'; +import { PageId } from './routing/routes'; +import { buildSidebarItems, navPageFor } from './navigation'; +import { PageOutlet } from './PageOutlet'; +import { APP_TEXTS } from './texts'; +import styles from './AppShell.module.scss'; + +/** Каркас приложения: меню разделов слева, текущий раздел справа. */ +export const AppShell: React.FC = () => { + const { route, navigate } = useRouter(); + const [isCollapsed, setIsCollapsed] = React.useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); + + const openSection = (id: string): void => { + setIsMobileMenuOpen(false); + navigate({ page: id as PageId, projectId: route.projectId }); + }; + + return ( + setIsCollapsed((prev) => !prev)} + mobileOpen={isMobileMenuOpen} + onMobileClose={() => setIsMobileMenuOpen(false)} + /> + } + mobileToggle={ setIsMobileMenuOpen(true)} />} + > +
+ +
+
+ ); +}; diff --git a/src/webparts/stroyBot/app/PageOutlet.tsx b/src/webparts/stroyBot/app/PageOutlet.tsx new file mode 100644 index 0000000..49d6e49 --- /dev/null +++ b/src/webparts/stroyBot/app/PageOutlet.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { useRouter } from './routing/RouterContext'; +import { SectionPlaceholder } from './SectionPlaceholder'; + +/** Показывает страницу текущего раздела. Разделы подключаются сюда по мере переноса. */ +export const PageOutlet: React.FC = () => { + const { route } = useRouter(); + + switch (route.page) { + default: + return ; + } +}; diff --git a/src/webparts/stroyBot/app/SectionPlaceholder.tsx b/src/webparts/stroyBot/app/SectionPlaceholder.tsx new file mode 100644 index 0000000..d2cbe7e --- /dev/null +++ b/src/webparts/stroyBot/app/SectionPlaceholder.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { EmptyState } from '../shared/components/EmptyState'; +import { APP_TEXTS } from './texts'; + +/** Временная заглушка для разделов, которые ещё не перенесены. Удаляется вместе с последним разделом. */ +export const SectionPlaceholder: React.FC = () => ( + +); diff --git a/src/webparts/stroyBot/app/navigation.test.ts b/src/webparts/stroyBot/app/navigation.test.ts new file mode 100644 index 0000000..14f4177 --- /dev/null +++ b/src/webparts/stroyBot/app/navigation.test.ts @@ -0,0 +1,34 @@ +import { buildSidebarItems, navPageFor } from './navigation'; + +describe('buildSidebarItems', () => { + it('без проекта в меню только главная', () => { + expect(buildSidebarItems(undefined).map((item) => item.id)).toEqual(['hub']); + }); + + it('с проектом — все разделы', () => { + expect(buildSidebarItems('5').map((item) => item.id)).toEqual([ + 'hub', + 'objects', + 'reports', + 'report', + 'import', + 'resources', + 'binder' + ]); + }); + + it('недоступные разделы скрываются, главная остаётся всегда', () => { + const onlyReports = (page: string): boolean => page === 'reports'; + expect(buildSidebarItems('5', onlyReports).map((item) => item.id)).toEqual(['hub', 'reports']); + }); +}); + +describe('navPageFor', () => { + it('работы подсвечивают пункт «Объекты»', () => { + expect(navPageFor('works')).toEqual('objects'); + }); + + it('остальные разделы подсвечивают сами себя', () => { + expect(navPageFor('reports')).toEqual('reports'); + }); +}); diff --git a/src/webparts/stroyBot/app/navigation.ts b/src/webparts/stroyBot/app/navigation.ts new file mode 100644 index 0000000..24dcca5 --- /dev/null +++ b/src/webparts/stroyBot/app/navigation.ts @@ -0,0 +1,39 @@ +import { IconName, ISidebarItem } from '@tnsu/ui-kit'; +import { PageId } from './routing/routes'; + +export interface INavItem { + page: PageId; + title: string; + icon: IconName; + /** Раздел работает только внутри проекта (нужен projectId). */ + needsProject: boolean; +} + +// Названия разделов — как на карточках stroy-bot-main. +export const NAV_ITEMS: INavItem[] = [ + { page: 'hub', title: 'Проект', icon: 'building', needsProject: false }, + { page: 'objects', title: 'Объекты', icon: 'folder', needsProject: true }, + { page: 'reports', title: 'Отчёты по проекту', icon: 'clipboard', needsProject: true }, + { page: 'report', title: 'Отправить отчёт', icon: 'send', needsProject: true }, + { page: 'import', title: 'Загрузить работы из сметы', icon: 'download', needsProject: true }, + { page: 'resources', title: 'Ресурсы проекта', icon: 'people', needsProject: true }, + { page: 'binder', title: 'Привязка к справочнику', icon: 'swap', needsProject: true } +]; + +/** Пункт меню, который подсвечивается для раздела. Работы — подраздел объектов, форма отчёта — свой пункт. */ +export function navPageFor(page: PageId): PageId { + return page === 'works' ? 'objects' : page; +} + +/** + * Пункты меню для текущего состояния. Без проекта — только главная. + * isAvailable — правила доступа к разделу (статус проекта, права); по умолчанию всё доступно. + */ +export function buildSidebarItems( + projectId: string | undefined, + isAvailable: (page: PageId) => boolean = () => true +): ISidebarItem[] { + return NAV_ITEMS.filter((item) => !item.needsProject || (projectId !== undefined && isAvailable(item.page))).map( + (item) => ({ id: item.page, title: item.title, icon: item.icon }) + ); +} diff --git a/src/webparts/stroyBot/app/routing/RouteLink.tsx b/src/webparts/stroyBot/app/routing/RouteLink.tsx new file mode 100644 index 0000000..ccfedf3 --- /dev/null +++ b/src/webparts/stroyBot/app/routing/RouteLink.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { IRoute } from './routes'; +import { useRouter } from './RouterContext'; + +interface Props { + to: IRoute; + className?: string; + /** Неактивная ссылка: рендерится без href и не реагирует на клик. */ + disabled?: boolean; + children: React.ReactNode; +} + +/** + * Ссылка на раздел приложения. Это настоящий , поэтому работают Ctrl+клик, + * средняя кнопка и «открыть в новой вкладке» (как было в stroy-bot-main), + * а обычный клик переключает раздел без перезагрузки страницы. + */ +export const RouteLink: React.FC = ({ to, className, disabled, children }) => { + const { navigate, buildHref } = useRouter(); + + if (disabled) { + return ( + + {children} + + ); + } + + const onClick = (event: React.MouseEvent): void => { + const opensNewTab = event.ctrlKey || event.metaKey || event.shiftKey || event.button !== 0; + if (opensNewTab) { + return; + } + event.preventDefault(); + navigate(to); + }; + + return ( + + {children} + + ); +}; diff --git a/src/webparts/stroyBot/app/routing/RouterContext.tsx b/src/webparts/stroyBot/app/routing/RouterContext.tsx new file mode 100644 index 0000000..8c2e395 --- /dev/null +++ b/src/webparts/stroyBot/app/routing/RouterContext.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { buildSearch, IRoute, parseRoute } from './routes'; + +interface IRouterValue { + route: IRoute; + /** Перейти в раздел: меняет адрес (history.pushState) и перерисовывает приложение. */ + navigate: (route: IRoute) => void; + /** Полный адрес раздела — для настоящих ссылок (Ctrl+клик, «открыть в новой вкладке»). */ + buildHref: (route: IRoute) => string; +} + +const RouterContext = React.createContext(undefined); + +function readCurrentRoute(): IRoute { + return parseRoute(window.location.search); +} + +function buildHref(route: IRoute): string { + return `${window.location.pathname}${buildSearch(route, window.location.search)}`; +} + +export const RouterProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [route, setRoute] = React.useState(readCurrentRoute); + + // Кнопки «назад/вперёд» браузера меняют адрес без нашего участия — перечитываем маршрут. + React.useEffect(() => { + const onPopState = (): void => setRoute(readCurrentRoute()); + window.addEventListener('popstate', onPopState); + return () => window.removeEventListener('popstate', onPopState); + }, []); + + const navigate = React.useCallback((next: IRoute): void => { + window.history.pushState(undefined, '', buildHref(next)); + setRoute(next); + window.scrollTo(0, 0); + }, []); + + const value = React.useMemo(() => ({ route, navigate, buildHref }), [route, navigate]); + + return {children}; +}; + +export function useRouter(): IRouterValue { + const router = React.useContext(RouterContext); + if (!router) { + throw new Error('useRouter: компонент отрисован вне RouterProvider'); + } + return router; +} diff --git a/src/webparts/stroyBot/app/routing/routes.test.ts b/src/webparts/stroyBot/app/routing/routes.test.ts new file mode 100644 index 0000000..f278183 --- /dev/null +++ b/src/webparts/stroyBot/app/routing/routes.test.ts @@ -0,0 +1,20 @@ +import { parseRoute, buildSearch } from './routes'; + +describe('parseRoute', () => { + it('пустой адрес - главная', () => { + expect(parseRoute('')).toEqual({ page: 'hub' }); + }); + it('адрес с разделом works, проектом 3 и объектом 41', () => { + expect(parseRoute('?page=works&projectId=3&objectId=41')).toEqual({ + page: 'works', + projectId: '3', + objectId: '41' + }); + }); + it('опечатка в разделе (objectz) — должна быть главная, а projectId сохраниться', () => { + expect(parseRoute('?page=objectz&projectId=3')).toEqual({ page: 'hub', projectId: '3' }); + }); + it('адрес только с projectId=7', () => { + expect(parseRoute('?projectId=7')).toEqual({ page: 'hub', projectId: '7' }); + }); +}); diff --git a/src/webparts/stroyBot/app/routing/routes.ts b/src/webparts/stroyBot/app/routing/routes.ts new file mode 100644 index 0000000..c38694a --- /dev/null +++ b/src/webparts/stroyBot/app/routing/routes.ts @@ -0,0 +1,70 @@ +// Маршруты приложения. Раздел и его параметры живут в query-строке страницы +// SharePoint (?page=objects&projectId=5), поэтому ссылку на любой раздел можно +// скопировать, открыть в новой вкладке, а «назад» в браузере работает как раньше. + +export const PAGE_IDS = ['hub', 'objects', 'works', 'reports', 'report', 'import', 'resources', 'binder'] as const; + +export type PageId = (typeof PAGE_IDS)[number]; + +/** Параметры маршрута. Все id — строки как в URL; разбор в числа делают страницы. */ +export interface IRoute { + page: PageId; + projectId?: string; + /** Работы: id элемента списка «Объекты». */ + objectId?: string; + /** Форма отчёта: открыть существующий отчёт (GUID Dataverse). */ + reportId?: string; + /** Форма отчёта: создать на основе существующего (GUID Dataverse). */ + copyFromId?: string; +} + +export const ROUTE_PARAMS = { + page: 'page', + projectId: 'projectId', + objectId: 'objectId', + reportId: 'reportId', + copyFromId: 'copyFromId' +} as const; + +const ROUTE_VALUE_KEYS: Array> = ['projectId', 'objectId', 'reportId', 'copyFromId']; + +function isPageId(value: string | undefined): value is PageId { + return value !== undefined && (PAGE_IDS as readonly string[]).indexOf(value) !== -1; +} + +export function parseRoute(search: string): IRoute { + const params = new URLSearchParams(search); + const page = params.get(ROUTE_PARAMS.page) ?? undefined; + const route: IRoute = { page: isPageId(page) ? page : 'hub' }; + + ROUTE_VALUE_KEYS.forEach((key) => { + const value = params.get(ROUTE_PARAMS[key]); + if (value) { + route[key] = value; + } + }); + return route; +} + +/** + * Собирает query-строку для маршрута. Посторонние параметры страницы SharePoint + * (например, режим редактирования) сохраняются, параметры прошлого маршрута — нет. + */ +export function buildSearch(route: IRoute, currentSearch: string): string { + const params = new URLSearchParams(currentSearch); + params.delete(ROUTE_PARAMS.page); + ROUTE_VALUE_KEYS.forEach((key) => params.delete(ROUTE_PARAMS[key])); + + if (route.page !== 'hub') { + params.set(ROUTE_PARAMS.page, route.page); + } + ROUTE_VALUE_KEYS.forEach((key) => { + const value = route[key]; + if (value) { + params.set(ROUTE_PARAMS[key], value); + } + }); + + const query = params.toString(); + return query ? `?${query}` : ''; +} diff --git a/src/webparts/stroyBot/app/texts.ts b/src/webparts/stroyBot/app/texts.ts new file mode 100644 index 0000000..731a598 --- /dev/null +++ b/src/webparts/stroyBot/app/texts.ts @@ -0,0 +1,18 @@ +// Тексты оболочки приложения. Тексты разделов лежат рядом с разделами (pages/*/texts.ts). + +export const APP_TEXTS = { + brand: 'StroyBot', + sectionInProgress: 'Раздел переносится из старого web part', + sectionInProgressHint: 'Пока пользуйтесь старой страницей раздела.' +} as const; + +export const PROPERTY_PANE_TEXTS = { + header: 'Настройки StroyBot', + connectionGroup: 'Подключение', + siteUrl: 'URL сайта SharePoint со списками (пусто — по умолчанию)', + siteUrlPlaceholder: 'https://tnsukz.sharepoint.com/sites/stroy', + dataverseUrl: 'URL Dataverse (пусто — по умолчанию)', + dataverseUrlPlaceholder: 'https://tansu.crm5.dynamics.com', + apiBaseUrl: 'Базовый URL API (пусто — по умолчанию)', + apiBaseUrlPlaceholder: 'https://api.tnsu.kz' +} as const; diff --git a/src/webparts/stroyBot/config/constants.ts b/src/webparts/stroyBot/config/constants.ts new file mode 100644 index 0000000..c770266 --- /dev/null +++ b/src/webparts/stroyBot/config/constants.ts @@ -0,0 +1,23 @@ +// Адреса окружения StroyBot. Раньше каждое значение было захардкожено +// в нескольких репозиториях stroy-bot-* — теперь единственное место. + +/** Сайт SharePoint со списками StroyBot (Проекты, Объекты, Доступы и т.д.). */ +export const SITE_URL = 'https://tnsukz.sharepoint.com/sites/stroy'; + +/** Окружение Dataverse (таблицы cree9_*). */ +export const DATAVERSE_URL = 'https://tansu.crm5.dynamics.com'; + +/** Корпоративный API (справочник проектов, сметы, Excel-выгрузки). */ +export const TNSU_API_BASE_URL = 'https://api.tnsu.kz'; + +/** AAD-приложение корпоративного API — ресурс для AadHttpClient. */ +export const TNSU_API_RESOURCE_ID = '9232d9e2-9807-4753-808f-07250e5474b0'; + +/** Шрифты дизайн-системы (Inter — текст, Lora — заголовки). Тот же URL, что в report-list и assigner. */ +export const FONTS_CSS_URL = + 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:wght@500;600;700&display=swap'; + +/** Ключи sessionStorage. Значение совпадает со stroy-bot-main — сохранённый выбор не теряется. */ +export const SESSION_KEYS = { + selectedProject: 'stroybot_selected_project' +} as const; diff --git a/src/webparts/stroyBot/config/dataverse.ts b/src/webparts/stroyBot/config/dataverse.ts new file mode 100644 index 0000000..c522eea --- /dev/null +++ b/src/webparts/stroyBot/config/dataverse.ts @@ -0,0 +1,15 @@ +// Таблицы Dataverse StroyBot (Web API, множественное число — как в URL). + +export const DATAVERSE_API_PATH = '/api/data/v9.2/'; + +export const DV_TABLES = { + /** Работы (иерархия, привязка к объекту SharePoint через cree9_objectid). */ + works: 'cree9_tables', + /** Отчёты. */ + reports: 'cree9_reports', + /** Строки отчётов: работы, персонал, техника. */ + reportData: 'cree9_report_datas' +} as const; + +/** Логическое имя (единственное число) таблицы работ — для EntityDefinitions. */ +export const DV_WORKS_LOGICAL_NAME = 'cree9_table'; diff --git a/src/webparts/stroyBot/config/lists.ts b/src/webparts/stroyBot/config/lists.ts new file mode 100644 index 0000000..02707e4 --- /dev/null +++ b/src/webparts/stroyBot/config/lists.ts @@ -0,0 +1,23 @@ +// Названия списков и библиотек SharePoint (getbytitle). +// Внутренние имена полей описываются рядом с репозиторием, который с ними работает. + +export const LISTS = { + projects: 'Проекты', + objects: 'Объекты', + access: 'Доступы', + companies: 'Компании', + companiesByProject: 'Компании по проекту', + professions: 'Профессии', + professionsByProject: 'Профессии по проекту', + shifts: 'Смены', + technics: 'Техника', + units: 'Единицы измерения' +} as const; + +/** Библиотека с фотографиями отчётов: папка `{projectId}_{reportNumber}`. */ +export const REPORT_PHOTO_LIBRARY = 'DocLib'; + +/** Библиотека с архивом проектов: папка `Архив/{название проекта}`. */ +export const ARCHIVE_FOLDER_PATH = 'Shared Documents/Архив'; + +export type ListTitle = (typeof LISTS)[keyof typeof LISTS]; diff --git a/src/webparts/stroyBot/services/AppServices.ts b/src/webparts/stroyBot/services/AppServices.ts new file mode 100644 index 0000000..b67176c --- /dev/null +++ b/src/webparts/stroyBot/services/AppServices.ts @@ -0,0 +1,70 @@ +import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { IHttpClient } from './http/IHttpClient'; +import { createAadClient, createSharePointClient } from './http/spfxClients'; +import { DATAVERSE_URL, SITE_URL, TNSU_API_BASE_URL, TNSU_API_RESOURCE_ID } from '../config/constants'; + +/** Настройки подключения из property pane (пустое значение — берётся значение по умолчанию). */ +export interface IConnectionSettings { + siteUrl?: string; + dataverseUrl?: string; + apiBaseUrl?: string; +} + +export interface IResolvedSettings { + siteUrl: string; + dataverseUrl: string; + apiBaseUrl: string; +} + +/** Сведения о текущей странице и пользователе из контекста SPFx. */ +export interface IPageInfo { + /** Абсолютный URL сайта, на котором стоит web part. */ + webAbsoluteUrl: string; + /** Серверный относительный URL сайта (`/sites/stroy`) — нужен для путей к папкам. */ + webServerRelativeUrl: string; + /** Id пользователя в SharePoint этого сайта (legacyPageContext.userId). */ + userId: number | undefined; + userDisplayName: string; +} + +/** Всё, что нужно страницам для работы с данными. Создаётся в web part. */ +export interface IAppServices { + sharePoint: IHttpClient; + dataverse: IHttpClient; + tnsuApi: IHttpClient; + settings: IResolvedSettings; + page: IPageInfo; +} + +function withoutTrailingSlash(url: string): string { + return url.replace(/\/+$/, ''); +} + +export function resolveSettings(settings: IConnectionSettings): IResolvedSettings { + return { + siteUrl: withoutTrailingSlash(settings.siteUrl || SITE_URL), + dataverseUrl: withoutTrailingSlash(settings.dataverseUrl || DATAVERSE_URL), + apiBaseUrl: withoutTrailingSlash(settings.apiBaseUrl || TNSU_API_BASE_URL) + }; +} + +function readPageInfo(context: WebPartContext): IPageInfo { + const legacyUserId: unknown = context.pageContext.legacyPageContext?.userId; + return { + webAbsoluteUrl: context.pageContext.web.absoluteUrl, + webServerRelativeUrl: context.pageContext.web.serverRelativeUrl, + userId: typeof legacyUserId === 'number' ? legacyUserId : undefined, + userDisplayName: context.pageContext.user.displayName + }; +} + +export function createAppServices(context: WebPartContext, settings: IConnectionSettings): IAppServices { + const resolved = resolveSettings(settings); + return { + sharePoint: createSharePointClient(context), + dataverse: createAadClient(context, resolved.dataverseUrl), + tnsuApi: createAadClient(context, TNSU_API_RESOURCE_ID), + settings: resolved, + page: readPageInfo(context) + }; +} diff --git a/src/webparts/stroyBot/services/http/IHttpClient.ts b/src/webparts/stroyBot/services/http/IHttpClient.ts new file mode 100644 index 0000000..bbf745e --- /dev/null +++ b/src/webparts/stroyBot/services/http/IHttpClient.ts @@ -0,0 +1,51 @@ +// Минимальный контракт HTTP-клиента. Репозитории зависят только от него, +// а не от SPHttpClient/AadHttpClient напрямую — поэтому в тестах их можно +// проверить заглушкой и убедиться, что URL, фильтры и тела запросов 1:1 +// совпадают со старыми web parts. + +export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'; + +export interface IHttpRequest { + method?: HttpMethod; + headers?: Record; + body?: string | ArrayBuffer; +} + +export interface IHttpResponse { + ok: boolean; + status: number; + statusText: string; + json(): Promise; + text(): Promise; + blob(): Promise; +} + +export interface IHttpClient { + fetch(url: string, request?: IHttpRequest): Promise; +} + +/** Ошибка HTTP-ответа: сохраняет статус, чтобы страницы показывали те же тексты, что и раньше. */ +export class HttpError extends Error { + public readonly status: number; + public readonly statusText: string; + + constructor(status: number, statusText: string, details?: string) { + super(details ? `${status} ${statusText}: ${details}` : `${status} ${statusText}`); + this.status = status; + this.statusText = statusText; + } +} + +/** Бросает HttpError, если ответ не успешный. Текст ответа добавляется в сообщение. */ +export async function ensureOk(response: IHttpResponse): Promise { + if (response.ok) { + return response; + } + let details = ''; + try { + details = await response.text(); + } catch { + // Тело ошибки может быть недоступно — статуса достаточно. + } + throw new HttpError(response.status, response.statusText, details); +} diff --git a/src/webparts/stroyBot/services/http/spfxClients.ts b/src/webparts/stroyBot/services/http/spfxClients.ts new file mode 100644 index 0000000..18e3355 --- /dev/null +++ b/src/webparts/stroyBot/services/http/spfxClients.ts @@ -0,0 +1,43 @@ +import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { SPHttpClient, AadHttpClient } from '@microsoft/sp-http'; +import { IHttpClient, IHttpRequest, IHttpResponse } from './IHttpClient'; + +// Адаптеры SPFx-клиентов к IHttpClient. Вся работа с авторизацией — здесь, +// остальной код о SPFx не знает. + +export function createSharePointClient(context: WebPartContext): IHttpClient { + return { + fetch: (url: string, request: IHttpRequest = {}): Promise => + context.spHttpClient.fetch(url, SPHttpClient.configurations.v1, { + method: request.method ?? 'GET', + headers: request.headers, + body: request.body + }) + }; +} + +/** + * Клиент к ресурсу под AAD (Dataverse или корпоративный API). + * AadHttpClient создаётся лениво и один раз — получение клиента запрашивает токен. + */ +export function createAadClient(context: WebPartContext, resource: string): IHttpClient { + let clientPromise: Promise | undefined; + + const getClient = (): Promise => { + if (!clientPromise) { + clientPromise = context.aadHttpClientFactory.getClient(resource); + } + return clientPromise; + }; + + return { + fetch: async (url: string, request: IHttpRequest = {}): Promise => { + const client = await getClient(); + return client.fetch(url, AadHttpClient.configurations.v1, { + method: request.method ?? 'GET', + headers: request.headers, + body: request.body + }); + } + }; +} diff --git a/src/webparts/stroyBot/shared/components/EmptyState.module.scss b/src/webparts/stroyBot/shared/components/EmptyState.module.scss new file mode 100644 index 0000000..1421aed --- /dev/null +++ b/src/webparts/stroyBot/shared/components/EmptyState.module.scss @@ -0,0 +1,39 @@ +@import '../../../../styles/design-tokens'; + +.emptyState { + display: flex; + flex-direction: column; + align-items: center; + gap: $sp-2; + padding: $sp-8 $sp-4; + text-align: center; + color: $gray-500; +} + +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: $r-full; + background: $navy-50; + color: $navy-700; +} + +.title { + margin: 0; + font-family: $font-sans; + font-size: $fs-body; + font-weight: 600; + color: $navy-900; +} + +.text { + margin: 0; + font-size: $fs-sm; +} + +.action { + margin-top: $sp-2; +} diff --git a/src/webparts/stroyBot/shared/components/EmptyState.tsx b/src/webparts/stroyBot/shared/components/EmptyState.tsx new file mode 100644 index 0000000..f4a0a73 --- /dev/null +++ b/src/webparts/stroyBot/shared/components/EmptyState.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Icon, IconName } from '@tnsu/ui-kit'; +import styles from './EmptyState.module.scss'; + +interface Props { + icon?: IconName; + title: string; + text?: string; + /** Кнопка действия под текстом (например, «+ Добавить объект»). */ + action?: React.ReactNode; +} + +// В ui-kit пока нет EmptyState — живёт здесь, позже переедет в кит. +export const EmptyState: React.FC = ({ icon = 'info', title, text, action }) => ( +
+ + + +

{title}

+ {text &&

{text}

} + {action &&
{action}
} +
+); diff --git a/src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.manifest.json b/src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.manifest.json deleted file mode 100644 index 7e11be5..0000000 --- a/src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.manifest.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", - "id": "619a5eba-d596-47e5-a95a-9b9682ed7ec3", - "alias": "StroyBotMonorepoWebPart", - "componentType": "WebPart", - - // The "*" signifies that the version should be taken from the package.json - "version": "*", - "manifestVersion": 2, - - // If true, the component can only be installed on sites where Custom Script is allowed. - // Components that allow authors to embed arbitrary script code should set this to true. - // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f - "requiresCustomScript": false, - "supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"], - "supportsThemeVariants": true, - - "preconfiguredEntries": [{ - "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced - "group": { "default": "Advanced" }, - "title": { "default": "stroy-bot-monorepo" }, - "description": { "default": "stroy-bot-monorepo description" }, - "officeFabricIconFontName": "Page", - "properties": { - "description": "stroy-bot-monorepo" - } - }] -} diff --git a/src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.ts b/src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.ts deleted file mode 100644 index 6bae3e8..0000000 --- a/src/webparts/stroyBotMonorepo/StroyBotMonorepoWebPart.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as React from 'react'; -import * as ReactDom from 'react-dom'; -import { Version } from '@microsoft/sp-core-library'; -import { - type IPropertyPaneConfiguration, - PropertyPaneTextField -} from '@microsoft/sp-property-pane'; -import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; -import { IReadonlyTheme } from '@microsoft/sp-component-base'; - -import * as strings from 'StroyBotMonorepoWebPartStrings'; -import StroyBotMonorepo from './components/StroyBotMonorepo'; -import { IStroyBotMonorepoProps } from './components/IStroyBotMonorepoProps'; - -export interface IStroyBotMonorepoWebPartProps { - description: string; -} - -export default class StroyBotMonorepoWebPart extends BaseClientSideWebPart { - - private _isDarkTheme: boolean = false; - private _environmentMessage: string = ''; - - public render(): void { - const element: React.ReactElement = React.createElement( - StroyBotMonorepo, - { - description: this.properties.description, - isDarkTheme: this._isDarkTheme, - environmentMessage: this._environmentMessage, - hasTeamsContext: !!this.context.sdks.microsoftTeams, - userDisplayName: this.context.pageContext.user.displayName - } - ); - - ReactDom.render(element, this.domElement); - } - - protected onInit(): Promise { - return this._getEnvironmentMessage().then(message => { - this._environmentMessage = message; - }); - } - - - - private _getEnvironmentMessage(): Promise { - if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook - return this.context.sdks.microsoftTeams.teamsJs.app.getContext() - .then(context => { - let environmentMessage: string = ''; - switch (context.app.host.name) { - case 'Office': // running in Office - environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment; - break; - case 'Outlook': // running in Outlook - environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment; - break; - case 'Teams': // running in Teams - case 'TeamsModern': - environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment; - break; - default: - environmentMessage = strings.UnknownEnvironment; - } - - return environmentMessage; - }); - } - - return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment); - } - - protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void { - if (!currentTheme) { - return; - } - - this._isDarkTheme = !!currentTheme.isInverted; - const { - semanticColors - } = currentTheme; - - if (semanticColors) { - this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null); - this.domElement.style.setProperty('--link', semanticColors.link || null); - this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null); - } - - } - - protected onDispose(): void { - ReactDom.unmountComponentAtNode(this.domElement); - } - - protected get dataVersion(): Version { - return Version.parse('1.0'); - } - - protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { - return { - pages: [ - { - header: { - description: strings.PropertyPaneDescription - }, - groups: [ - { - groupName: strings.BasicGroupName, - groupFields: [ - PropertyPaneTextField('description', { - label: strings.DescriptionFieldLabel - }) - ] - } - ] - } - ] - }; - } -} diff --git a/src/webparts/stroyBotMonorepo/assets/welcome-dark.png b/src/webparts/stroyBotMonorepo/assets/welcome-dark.png deleted file mode 100644 index 42f0b8d24a9aa964a2be4885fe5700c1c54191a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12545 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�Gp~kbx)oU28ew4C2+WtKA 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 G`2PSf1J4)$ diff --git a/src/webparts/stroyBotMonorepo/assets/welcome-light.png b/src/webparts/stroyBotMonorepo/assets/welcome-light.png deleted file mode 100644 index 69eb3b48cd83031f106df4b4df127c749657e319..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12816 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 diff --git a/src/webparts/stroyBotMonorepo/components/IStroyBotMonorepoProps.ts b/src/webparts/stroyBotMonorepo/components/IStroyBotMonorepoProps.ts deleted file mode 100644 index 4031a75..0000000 --- a/src/webparts/stroyBotMonorepo/components/IStroyBotMonorepoProps.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface IStroyBotMonorepoProps { - description: string; - isDarkTheme: boolean; - environmentMessage: string; - hasTeamsContext: boolean; - userDisplayName: string; -} diff --git a/src/webparts/stroyBotMonorepo/components/StroyBotMonorepo.module.scss b/src/webparts/stroyBotMonorepo/components/StroyBotMonorepo.module.scss deleted file mode 100644 index 6ea803a..0000000 --- a/src/webparts/stroyBotMonorepo/components/StroyBotMonorepo.module.scss +++ /dev/null @@ -1,34 +0,0 @@ -@import '~@fluentui/react/dist/sass/References.scss'; - -.stroyBotMonorepo { - overflow: hidden; - padding: 1em; - color: "[theme:bodyText, default: #323130]"; - color: var(--bodyText); - &.teams { - font-family: $ms-font-family-fallbacks; - } -} - -.welcome { - text-align: center; -} - -.welcomeImage { - width: 100%; - max-width: 420px; -} - -.links { - a { - text-decoration: none; - color: "[theme:link, default:#03787c]"; - color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only - - &:hover { - text-decoration: underline; - color: "[theme:linkHovered, default: #014446]"; - color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only - } - } -} \ No newline at end of file diff --git a/src/webparts/stroyBotMonorepo/components/StroyBotMonorepo.tsx b/src/webparts/stroyBotMonorepo/components/StroyBotMonorepo.tsx deleted file mode 100644 index dce7147..0000000 --- a/src/webparts/stroyBotMonorepo/components/StroyBotMonorepo.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from 'react'; -import styles from './StroyBotMonorepo.module.scss'; -import type { IStroyBotMonorepoProps } from './IStroyBotMonorepoProps'; -import { escape } from '@microsoft/sp-lodash-subset'; - -export default class StroyBotMonorepo extends React.Component { - public render(): React.ReactElement { - const { - description, - isDarkTheme, - environmentMessage, - hasTeamsContext, - userDisplayName - } = this.props; - - return ( -
-
- -

Well done, {escape(userDisplayName)}!

-
{environmentMessage}
-
Web part property value: {escape(description)}
-
-
-

Welcome to SharePoint Framework!

-

- The SharePoint Framework (SPFx) is a extensibility model for Microsoft Viva, Microsoft Teams and SharePoint. It's the easiest way to extend Microsoft 365 with automatic Single Sign On, automatic hosting and industry standard tooling. -

-

Learn more about SPFx development:

- -
-
- ); - } -} diff --git a/src/webparts/stroyBotMonorepo/loc/en-us.js b/src/webparts/stroyBotMonorepo/loc/en-us.js deleted file mode 100644 index 3b25e74..0000000 --- a/src/webparts/stroyBotMonorepo/loc/en-us.js +++ /dev/null @@ -1,16 +0,0 @@ -define([], function() { - return { - "PropertyPaneDescription": "Description", - "BasicGroupName": "Group Name", - "DescriptionFieldLabel": "Description Field", - "AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part", - "AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app", - "AppLocalEnvironmentOffice": "The app is running on your local environment in office.com", - "AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook", - "AppSharePointEnvironment": "The app is running on SharePoint page", - "AppTeamsTabEnvironment": "The app is running in Microsoft Teams", - "AppOfficeEnvironment": "The app is running in office.com", - "AppOutlookEnvironment": "The app is running in Outlook", - "UnknownEnvironment": "The app is running in an unknown environment" - } -}); \ No newline at end of file diff --git a/src/webparts/stroyBotMonorepo/loc/mystrings.d.ts b/src/webparts/stroyBotMonorepo/loc/mystrings.d.ts deleted file mode 100644 index 449537e..0000000 --- a/src/webparts/stroyBotMonorepo/loc/mystrings.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare interface IStroyBotMonorepoWebPartStrings { - PropertyPaneDescription: string; - BasicGroupName: string; - DescriptionFieldLabel: string; - AppLocalEnvironmentSharePoint: string; - AppLocalEnvironmentTeams: string; - AppLocalEnvironmentOffice: string; - AppLocalEnvironmentOutlook: string; - AppSharePointEnvironment: string; - AppTeamsTabEnvironment: string; - AppOfficeEnvironment: string; - AppOutlookEnvironment: string; - UnknownEnvironment: string; -} - -declare module 'StroyBotMonorepoWebPartStrings' { - const strings: IStroyBotMonorepoWebPartStrings; - export = strings; -}