Files
kps-ct-form/NEW-PROJECT-PROMPT.md
T
Тимур Городецкий 8a79c259b4 feat: working version
2026-07-27 17:50:31 +05:00

336 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Стартовый промт для нового SPFx-проекта (многошаговая форма)
> **Как использовать этот файл.**
> Положи его в корень нового репозитория (например, переименуй в `CLAUDE.md`, чтобы Claude Code подхватывал автоматически).
> В начале работы над новым проектом дай агенту команду: _«Прочитай `NEW-PROJECT-PROMPT.md` и следуй ему»_.
> Разделы, помеченные `⟨…⟩`, — это **плейсхолдеры**: замени их под свою предметную область до старта.
> Этот файл — обобщение боевого проекта `ct-create-form` (СТ — сравнительная таблица закупок). Новый проект «похожий»: тот же стек, та же архитектура, другая доменная модель.
---
## 1. Роль
Ты — опытный Senior Frontend Engineer. Специализация: React (TypeScript), корпоративные веб-приложения на **SharePoint Framework (SPFx)**, порталы и SPA.
Код пишешь так, будто он идёт в production и дальше его ведёт джун без твоего контекста: приоритет — **читаемость, стабильность, предсказуемость**.
---
## 2. Что за проект (замени под себя)
⟨Опиши в 3–5 предложениях, что делает приложение.⟩
Шаблон, от которого отталкиваемся: **SPFx WebPart — многошаговая форма (wizard) на корпоративном портале**. Пользователь проходит N шагов (выбор данных → загрузка → формирование → согласование), данные на каждом шаге сохраняются в SharePoint через REST, форму можно открыть повторно по ссылке (в т.ч. из уведомления Power Automate/Teams) и продолжить с любого шага.
- **Предметная область:** ⟨напр. «заявка на …», «карточка …», «согласование …»⟩
- **Шаги формы:** 4 шага (подробно — см. §12). Каркас потока: **сбор данных → загрузка/обогащение → формирование/сравнение → согласование.**
- **Роли пользователей:** ⟨автор/инициатор, согласующие, наблюдатели с ограниченным доступом⟩
- **Внешние системы:** ⟨SharePoint-списки, внешний API, Power Automate flows⟩
---
## 3. Технологический стек (фиксированный)
- **SPFx 1.22.x** (Heft-сборка: `heft start` / `heft test` / `heft package-solution`)
- **Node** `>=22.14.0 <23`
- **React 17** (function components + hooks, без классов)
- **TypeScript ~5.8**, строгая типизация, **без `any`**
- **Fluent UI React v8** (`@fluentui/react ^8`)
- **SCSS Modules** (`*.module.scss`)
- Работа с SharePoint — через `SPHttpClient` (из `@microsoft/sp-http`), REST `_api/web/lists/...`. **PnPjs не используем.**
- Внешние API — через `aadTokenProviderFactory` (Bearer-токен Azure AD) + нативный `fetch`.
> Не добавляй новые зависимости без явной необходимости и без согласования. Не выдумывай API/props/окружение — используй только то, что явно есть.
---
## 4. Эталонная структура проекта
```
src/webparts/⟨feature⟩/
⟨Feature⟩WebPart.ts — точка входа: пробрасывает context, userDisplayName, isDarkTheme в React
components/
I⟨Feature⟩Props.ts — props корневого компонента (context, userDisplayName, isDarkTheme)
⟨Feature⟩.tsx — ГЛАВНЫЙ контейнер (functional). Мемоизирует Service, держит стейт через хук, рендерит шаги
⟨Feature⟩.module.scss
types/domain.ts — ВСЕ доменные типы + union-типы статусов + константа шагов
hooks/
use⟨Feature⟩Form.ts — стейт формы: currentStep, formData, completedSteps, highestStep + persist в sessionStorage
useRequests.ts — загрузка данных с отменой на unmount
Breadcrumbs/ — индикатор прогресса шагов (completed / current / доступность)
Step1…/ Step2…/ Step3…/ Step4…/
— на каждый шаг папка: контейнер Step*.tsx + под-компоненты (таблицы, формы, диалоги)
utils/ — чистые функции (конвертации, форматирование, расчёты)
services/⟨Feature⟩Service.ts — ВСЕ вызовы SharePoint REST + внешние API + Power Automate в одном классе
loc/ — строки локализации
```
Правила декомпозиции: один компонент — один файл; компонент ≤ 300 строк; функция/хук ≤ ~50 строк; ≤ 3–4 уровня вложенности JSX; ≤ ~7 props (иначе группируй в объект). Повторил блок 3-й раз → выноси в общий компонент/функцию/хук.
---
## 5. Ключевые архитектурные паттерны
Эти паттерны — суть проекта. Соблюдай их, расширяя форму.
### 5.1. Единый стейт формы в одном хуке-родителе
Весь стейт многошаговой формы живёт в **одном** хуке (`use⟨Feature⟩Form`) внутри главного контейнера. Шаги **не** держат общий стейт сами — они получают срез данных и колбэки.
- `formData` — единый объект со всеми данными формы (см. `CTFormData` как образец).
- Обновление — иммутабельно и точечно: `updateFormData(partial: Partial<FormData>)`.
- Производные данные считай на лету (`useMemo`), не дублируй в отдельный `useState`.
- `currentStep`, `completedSteps` (пройденные), `highestStep` (максимально доступный) — тоже здесь.
### 5.2. Контракт props для шага (единый на все шаги)
Каждый `Step*` получает один и тот же набор:
```ts
interface StepProps {
service: ⟨Feature⟩Service;
formData: FormData;
onSave: (partial: Partial<FormData>) => void; // = updateFormData
onNext?: () => void;
onPrev?: () => void;
onStepComplete: (step: FormStep) => void;
isReadOnly?: boolean; // форма на согласовании/согласована/отклонена
isRestrictedView?: boolean; // урезанный доступ (напр. скрытие цен)
userDisplayName: string;
isNotificationLink?: boolean; // открыто по ссылке из уведомления
}
```
### 5.3. Сервисный слой
`⟨Feature⟩Service` — единственное место, где происходят сетевые вызовы. Компоненты не делают `fetch`/`SPHttpClient` напрямую.
- Конструктор принимает `WebPartContext`, вытаскивает `baseUrl = context.pageContext.web.absoluteUrl` и `spHttpClient`.
- Приватные хелперы для URL списков: `_li(listName)` и `_liById(listName, id)`.
- Мемоизируй сервис в контейнере: `const service = useMemo(() => new ⟨Feature⟩Service(context), [context])`.
### 5.4. Персист и восстановление сессии
- Стейт формы сериализуется в `sessionStorage` по ключу `⟨prefix⟩_${entityId}` при каждом изменении.
- `entityId` кладётся в URL (`?⟨entity⟩Id=…`) через `history.replaceState`, чтобы перезагрузка/шаринг ссылки продолжали ту же форму.
- Поддержи **ссылку из уведомления** (Power Automate/Teams) с другим регистром параметра (напр. `?⟨entity⟩id=…`): по ней всегда подгружай свежие данные из SharePoint и открывай нужный шаг.
- При восстановлении **не затирай** несохранённые правки из сессии данными из SharePoint без нужды: бери SP-данные только когда в сессии пусто **или** это ссылка из уведомления (см. слияние в `⟨Feature⟩.tsx`).
### 5.5. Доступность шагов и режимы только для чтения
- Breadcrumbs даёт кликать только по `completedSteps` и в пределах `highestStep`.
- `isReadOnly` включается по статусу сущности (`on_approval` / `approved` / `rejected`).
- Особые ограничения по компании/роли (напр. скрытие цен) — отдельным флагом, который **складывается** с общими правилами, а не заменяет их.
### 5.6. Статусы — только union-типы
Статусы сущности и участников согласования — `type X = 'a' | 'b' | ...`, чтобы опечатка ловилась компилятором. Никаких magic-strings по коду.
### 5.7. Верстка на всю ширину экрана (full-width)
По умолчанию SPFx-страница зажимает WebPart шириной канвы (`max-width` ~1204px). Чтобы форма занимала всю ширину экрана, нужны **две вещи в связке**:
**1. Флаг в манифесте WebPart** (`⟨Feature⟩WebPart.manifest.json`) — разрешает положить WebPart в полноширинную секцию страницы:
```json
"supportsFullBleed": true
```
После деплоя на странице добавь WebPart в **полноширинную секцию** (Full-width section — доступна только на страницах без баннера/на пустом шаблоне).
**2. Обход контейнера канвы в корневом `*.module.scss`** через `:global` — снимает `max-width` и у воркбенча (dev), и у живой канвы:
```scss
:global {
#workbenchPageContent,
.CanvasComponent.LCS .CanvasZone {
max-width: 100% !important;
}
}
.root {
min-height: 100vh; // тянем на всю высоту вьюпорта
*,
*::before,
*::after {
box-sizing: border-box;
}
}
```
`!important` тут оправдан: перебиваем инлайновые/встроенные стили канвы SharePoint, на которые повлиять иначе нельзя (это тот редкий случай, когда `!important` — не костыль). Селекторы `#workbenchPageContent` и `.CanvasComponent.LCS .CanvasZone` — внутренние классы SharePoint; если после обновления платформы ширина «вернётся», проверь актуальные имена в DevTools.
---
## 6. Playbook по SharePoint (важно — тут «грабли»)
Внутренние имена списков и полей — **только в константах** сервиса (`const LISTS = {...}`), не размазывай по коду. Помни: внутреннее имя SP-поля ≠ отображаемое (напр. поле «Статус» может иметь internal name `status`).
**Запись lookup-поля:** в теле запроса ключ — `{InternalName}Id` со **скалярным** значением (напр. `{ winSupplierId: 42 }`). Суффикс `LookupId` бывает только в GET-ответах, для записи он не нужен.
**Грабли с «защищёнными» списками (Retention / event receiver).**
На некоторых списках hard `DELETE` и `MERGE/PATCH` падают с `E_FAIL` (`-2147467259, SPException`). Признак — на списке стоит политика удержания или ресивер.
- ✅ Работают: `POST` (создание), `GET` (чтение), `POST /items(N)/recycle()` (мягкое удаление — то же, что делает кнопка «Удалить» в UI).
- ❌ Не работают: `DELETE` любым синтаксисом, `MERGE/PATCH`.
- **Приём:** для «обновления» такой записи — `recycle()` + повторный `POST`, а не `MERGE`.
- Если на новом списке REST `DELETE/MERGE` падает с `E_FAIL` — первым делом пробуй `recycle()`.
**Грабли с `$expand`/`$select`:** выборка Choice-поля через `$expand` по lookup иногда даёт `400`. Решение — грузить связанные данные отдельным запросом по id и мерджить на клиенте (см. загрузку курсов валют в `⟨Feature⟩.tsx`).
**Округление денег:** округляй монетарные значения до 2 знаков перед записью (`Math.round(v*100)/100`), чтобы не улетали float-хвосты.
---
## 7. Внешний API и Power Automate
**Azure AD Bearer-токен** (паттерн SPFx):
```ts
const tokenProvider = await context.aadTokenProviderFactory.getTokenProvider();
const token = await tokenProvider.getToken(⟨AAD_RESOURCE_ID⟩);
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
```
- Внутренний API TNSU: base `https://api.tnsu.kz`, AAD Resource ID `9232d9e2-9807-4753-808f-07250e5474b0`.
- Пример готовой реализации: `pmo-records/src/webparts/pmoRecords/services/TnsuApiService.ts`.
- Требуется прописать разрешение в `package-solution.json` (`webApiPermissionRequests`) и одобрить его в API access центра администрирования SharePoint.
**Power Automate:** триггеры вызываются POST-запросом на HTTP-trigger URL. Все URL флоу — **в константах сервиса**, не в компонентах. Обрабатывай ответ и ошибки явно; вызовы, которые не должны блокировать UX (уведомления, статусы), делай non-blocking: `service.doX(...).catch(() => undefined)`.
---
## 8. Конвенции кода (обязательны)
**React-практики**
- `useEffect` — только для сайд-эффектов; бизнес-логику не клади в JSX.
- `useMemo`/`useCallback` — только при реальной необходимости.
- Контролируемые компоненты предпочтительнее неконтролируемых. Props/state не мутируем.
- Загрузку данных отменяй на unmount (флаг `cancelled`/`AbortController`), чтобы не было setState после размонтирования.
**Состояние**
- Состояние живёт максимально низко; наверх поднимаем только реально общее.
- Prop-drilling глубже 2–3 уровней → React Context (для сквозного: текущая сущность, пользователь, справочники).
- Один источник истины; изменения иммутабельны (`setItems(prev => [...prev, x])`).
**Имена и стиль**
- Компоненты — `PascalCase`; хуки — `useSomething`; переменные — `camelCase`; CSS-классы — единообразно (`kebab-case` или `camelCase`).
- Имена отражают смысл, а не реализацию (`isLoading`, а не `flag`).
**Константы и строки**
- Никаких magic-strings в логике: статусы, имена списков/полей, роуты, id ресурсов, URL — в константах/конфиге.
- Пользовательские тексты — по возможности в одном месте (задел под локализацию).
- Числовые «магические» значения — именованные константы с комментарием-смыслом.
**Ошибки и состояния**
- Явно обрабатывай `loading` / `error` / `empty`. Ошибки не скрывай. UI корректен при отсутствии данных.
**Layout**
- Адаптив; `flex`/`grid`; контейнеры по умолчанию 100% ширины; консистентные `padding`/`margin`; без magic-numbers без комментария.
**Комментарии**
- Комментируй **ПОЧЕМУ**, а не ЧТО. Не дублируй очевидный код. Сложную бизнес-логику сопровождай коротким «почему».
- Не оставляй мёртвый/закомментированный код — удаляй.
---
## 9. Git и коммиты
- Формат: `тип: краткое описание в настоящем времени` (`feat:`, `fix:`, `refactor:`, `chore:`, `docs:`, `test:`).
- Один коммит — одно логическое изменение. Сообщение объясняет **что** и **зачем**, а не пересказывает диф.
- Не коммить отладочный код и секреты. По истории джун должен понять ход работы.
---
## 10. Чеклист старта нового проекта
1. `yo @microsoft/sharepoint` (SPFx 1.22.x) → WebPart с React-фреймворком. Проверь Node `22.x`.
2. Замени все `⟨плейсхолдеры⟩` в этом файле и переименуй его в `CLAUDE.md`.
3. Опиши доменную модель в `types/domain.ts`: сущности, `type Status = …`, `FORM_STEPS`.
4. Заведи `⟨Feature⟩Service` с `const LISTS = {...}` и хелперами `_li`/`_liById`.
5. Собери каркас: `WebPart.ts` → главный контейнер → `use⟨Feature⟩Form` → `Breadcrumbs` + пустые `Step1..N`.
6. Настрой persist в `sessionStorage` и чтение `entityId` из URL (+ вариант ссылки из уведомления).
7. Пропиши API-разрешения в `package-solution.json`, если нужен внешний API.
8. Для full-width: поставь `"supportsFullBleed": true` в манифесте + `:global`-обход канвы в корневом SCSS (§5.7), и клади WebPart в полноширинную секцию страницы.
9. Реализуй шаги по одному, соблюдая единый контракт props (§5.2).
## 11. TODO-заглушки (типовые места, которые обычно доделываются позже)
- `⟨Feature⟩Service`: заменить константы `LISTS` на реальные имена SP-списков.
- Загрузка файлов в библиотеку SharePoint + триггер обработки (Syntex/Flow), если есть шаг с документами.
- People picker (`NormalPeoplePicker`) для добавления согласующих.
- Вызовы Power Automate для уведомлений/сохранения статусов.
---
## 12. Шаги формы — эталонная модель (на примере СТ)
Ниже — реальный поток из проекта `ct-create-form`. Каркас (4 шага, роли, статусы, режимы) переноси как есть, **содержимое шагов адаптируй под свою предметку.** Все 4 шага рендерятся из главного контейнера, стейт — в `use⟨Feature⟩Form`, каждый шаг получает единый контракт props (§5.2).
Метки шагов (`FORM_STEPS` в `types/domain.ts`):
```ts
export const FORM_STEPS = [
{ step: 1, label: "Выбор позиций" },
{ step: 2, label: "Загрузка КП" },
{ step: 3, label: "Формирование СТ" },
{ step: 4, label: "Согласование" },
];
```
### Шаг 1 — «Выбор позиций» (сбор исходных данных)
**Задача:** пользователь выбирает исходные записи (в СТ — позиции потребностей), из которых собирается сущность.
- Грузит данные из **внешнего API** за последние N дней (по умолчанию 30), с поиском по номеру и пагинацией (~10/страница). Загрузка — через хук с отменой на unmount (`useRequests`).
- Таблица с раскрывающимися строками (родитель → дочерние записи) и чекбоксами выбора.
- **Бизнес-ограничение:** все выбранные записи — из одного проекта. На первом чекбоксе фиксируется `selectedProject`, остальные проекты блокируются (`lockedProjectId`).
- **По «Далее»:** создаёт сущность (СТ) и её позиции в SharePoint, кладёт `selectedPositions` / `selectedProject` в `formData`, помечает шаг завершённым, проставляет `?⟨entity⟩Id` в URL.
- **Read-only:** если сущность уже на согласовании/согласована/отклонена — показывает выбранное без правки.
### Шаг 2 — «Загрузка КП» (загрузка и обогащение данных)
**Задача:** загрузить документы (коммерческие предложения) и извлечь из них структурированные данные.
- **Drag&drop загрузка файлов** в библиотеку SharePoint (`FileUploadArea`).
- После загрузки — триггер обработки документа (**Syntex / Power Automate flow**) для извлечения полей (позиции, цены, поставщик). Идёт **поллинг результата** (интервал ~5с, лимит ~5 мин; статусы файла: `uploading → extracting → flowing → done/error`).
- **Курсы валют** (`CurrencyRatesForm`): подгрузка из SP-списка «Курс Валют» / API + ручной ввод. Валюта КП запоминается на поставщике, чтобы Шаг 3 показал верные колонки.
- Поставщики **без КП** фиксируются отдельно (список «Не получены КП») с комментарием.
- **Результат в `formData`:** `suppliers`, `offers`, `currencyRates`, `uploadedKpFiles`.
### Шаг 3 — «Формирование СТ» (сведение и сравнение)
**Задача:** свести всё в сравнительную матрицу и выбрать победителя.
- **Excel-подобная матрица:** строки = позиции, столбцы = поставщики; в ячейках — цена/сумма с НДС.
- **Подсветка минимума** по строке (зелёным) и **авто-выбор рекомендованного поставщика** (наименьшая итоговая сумма) → `recommendedSupplierId`.
- Редактируемо: количество по позиции (`step3PositionQtyOverrides`), доп-позиции и доп-поставщики (`step3Extra*`), строки доставки с км (`step3DeliveryKm`), критерии (оплата/доставка/срок/сертификаты) и кастомные критерии. Конвертация единиц измерения — `utils/unitConversion`.
- **AI-комментарий** по таблице (через Power Automate flow).
- **Скрытие цен** (`isRestrictedView`) для урезанного доступа.
- При открытии **по ссылке из уведомления** этот шаг — главный: тут же показывается цепочка согласования и доступны действия «Согласовать/Отклонить».
### Шаг 4 — «Согласование» (маршрут и решение)
**Задача:** собрать цепочку согласующих и провести сущность по маршруту.
- **Конструктор цепочки:** добавление согласующих через `NormalPeoplePicker` (поиск пользователей SP), порядок, роли, право делегирования (`canAddApprover`).
- Инициатор ставит **«Рекомендую»** → цепочка активируется, статус сущности → **«На согласовании»**.
- **Диалог «Согласовать/Отклонить»** с комментарием. Статусы участников: `pending | recommends | approved | rejected | skipped`.
- **Побочные эффекты (non-blocking):** уведомления через Power Automate; запись статуса сущности и выбранного поставщика (`winSupplier`, lookup) в SP-список. Статусы сущности: `draft | kp_loading | kp_loaded | formed | on_approval | approved | rejected`.
> **Как адаптировать под новый проект:** сохрани роли (автор/инициатор → согласующие → наблюдатель), 3 режима отображения (обычный / read-only по статусу / restricted), персист+ссылку из уведомления и деривацию статуса из цепочки (`deriveCtStatus` в контейнере). Меняй только _содержимое_: какие данные собираешь на Шаге 1, что грузишь/извлекаешь на Шаге 2, что и как сравниваешь на Шаге 3.
---
### Ссылки на образцы (в текущем окружении разработчика)
- Боевой проект-образец: `ct-create-form` (СТ). Смотри `CtService.ts`, `useCtForm.ts`, `CtCreateForm.tsx`.
- Родственный проект той же архитектуры: `oid-create-form`.
- Пример внешнего API с AAD-токеном: `pmo-records/.../TnsuApiService.ts`.