# Стартовый промт для нового 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)`. - Производные данные считай на лету (`useMemo`), не дублируй в отдельный `useState`. - `currentStep`, `completedSteps` (пройденные), `highestStep` (максимально доступный) — тоже здесь. ### 5.2. Контракт props для шага (единый на все шаги) Каждый `Step*` получает один и тот же набор: ```ts interface StepProps { service: ⟨Feature⟩Service; formData: FormData; onSave: (partial: Partial) => 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`.