336 lines
28 KiB
Markdown
336 lines
28 KiB
Markdown
# Стартовый промт для нового 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`.
|