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

28 KiB
Raw Permalink Blame History

Стартовый промт для нового 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* получает один и тот же набор:

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 в полноширинную секцию страницы:

"supportsFullBleed": true

После деплоя на странице добавь WebPart в полноширинную секцию (Full-width section — доступна только на страницах без баннера/на пустом шаблоне).

2. Обход контейнера канвы в корневом *.module.scss через :global — снимает max-width и у воркбенча (dev), и у живой канвы:

: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):

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):

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.