# Инвентарь поведения — ct / oid / kps → ct-forms-portal Спецификация для нового объединённого проекта. Источники (РЕФЕРЕНС, не трогаем): - `ct-create-form` (ctCreateForm) — закупка по позициям из заявок TNSU - `oid-create-form` (oidCreateForm) — ручной ввод работ (ОИД) - `kps-ct-form` (kpsCtForm) — эталон организации (decomposed, common/) Правило: поведение КАЖДОЙ формы переносим точь-в-точь. Общее → shared; различия → per-form. Заполняется по областям по мере чтения. --- ## Область 1. Машина состояний мастера (навигация по шагам) **Вывод: логика ОДИНАКОВА во всех трёх → общий хук `useWizard` (в common/), различия — конфигом.** ### Общее (переносим как shared `useWizard`) Состояние: `currentStep` (1..N), `completedSteps: number[]`, `highestStep`, `formData`. - `goToStep(step)` → `currentStep = step`; `highestStep = max(highestStep, step)`. - `goNext()` → `currentStep = min(LAST, s+1)`; `highestStep = max(highestStep, next)`. - `goPrev()` → `currentStep = max(1, s-1)`. **highestStep НЕ меняет.** - `markStepComplete(step)` → добавить в completedSteps, если ещё нет (без дублей). - `ensureHighestStep(step)` → `highestStep = max(highestStep, step)`. - `updateFormData(partial)` → merge в formData. - **Кликабельность шага в степпере: `step <= highestStep`** (защита от прыжка вперёд) — уже реализовано в `@tnsu/ui-kit` Stepper. Это ЧИСТАЯ логика — обязательно покрыть юнит-тестами (clamp 1..LAST, max для highestStep, отсутствие дублей в completed). ### Сохранение состояния (sessionStorage) — общее, ключ per-form - Персистим `{ formData, currentStep, completedSteps, highestStep }` в sessionStorage. - Ключ: ct — `ct_form_{ctId}`; kps — свой `sessionKey(ctId)`. → параметр `sessionKeyPrefix` в конфиге формы. - Пишем, когда известен id сущности (ct.id / ctId). Читаем при инициализации (ленивый init useState). ### Вход по URL + ссылка из уведомления — общее поведение, детали per-form - id сущности из URL: ct — `ctId`|`ctid`; oid/kps — свои. Признак «ссылка из уведомления» = наличие ПАРАМЕТРА в нижнем регистре (ct: `ctid`). → параметры `urlIdParam` / `notificationParam` в конфиге. - Логика приземления при входе по URL: - **ссылка из уведомления** (согласующий): загрузить из SP → `goToStep(3)` (Шаг 3 главный) + пометить шаги 1..3 завершёнными (⇒ highestStep=4, доступны все). [ct: markComplete 3 и 4; kps: 1,2,3 — СВЕРИТЬ при переносе, привести к единому]. - **обычный вход по URL** (редактирование): загрузить → приземлиться на «самый дальний заполненный» шаг (`furthestFilledStep`), пометить завершёнными до него; если статус approved/rejected → пометить и шаг 4. - `furthestFilledStep(data)` (kps, эталон): approvers>0 или статус on_approval/approved/rejected → 4; offers>0 → 3; suppliers>0 → 2; иначе → 1. ### Различия per-form (СОХРАНИТЬ) - **ct:** хук `useCtForm`; SP-загрузка — большим useEffect в контейнере (~130 строк): восстанавливает offers, currencyRates, uploadedKpFiles (по сессии + дочитка из SP), `step3DeliveryKm`, suppliers из офферов. `getCurrentUserProfile → initiatorDepartment`; `getCurrentUserCompany → isRestrictedView` (company === 'astel-k'). `hidePrices = isRestrictedView || (approvalSent && !userInApprovalChain)`. Статусы CTStatus: draft/kp_loading/kp_loaded/formed/on_approval/approved/rejected. - **oid:** хук `useOidForm(context)` (грузит внутри). Шаг 1 — РУЧНОЙ ВВОД работ (Step1WorksInput, ComboBox) — принципиально другой. `isReadOnly = formData.approvalSent`. Профиль → department+company. Статусы OIDStatus: draft/formed/on_approval/approved/rejected (нет kp_*). CurrencyRates — inline в Step2. - **kps (эталон):** хук `useKpsCtForm(ctId)` + отдельный `useSharePointLoad`. Методы `next`/`prev` (не goNext/goPrev). `replaceFormData`. `deriveCtStatus` — пока заглушка (=formData.status), TODO реальная логика. `CT_STATUS_META[status] = {label, kind}` (kind → Pill/Badge). `READ_ONLY_STATUSES`. Единый контракт `StepProps` для всех шагов. Самый чистый — берём за каркас. ### Как ляжет в новый проект - shared `useWizard(config)` — состояние + переходы + сессия (config: lastStep, sessionKeyPrefix, urlIdParam, notificationParam). - per-form: свои шаги, статусы (STATUS_META), SP-загрузка (furthestFilled + восстановление данных), Step1 (ct/kps — выбор позиций; oid — ручной ввод). - Степпер — `@tnsu/ui-kit` Stepper; шапка — `@tnsu/ui-kit` FormHeader (kps так уже и делает). --- ## Область 2. Оболочка + реестр (config-driven) — ИСТОЧНИК: ct-forms-list / myDocuments Готовый каркас для портала. myDocuments — единый дашборд по всем 3 типам, управляется массивом конфигов DocumentTypeConfig (по одному на CT/OID/KPS в config/*DocumentConfig.ts). Умеет ПРОСМОТР (реестр+очереди) и СОЗДАНИЕ (кнопка «Создать» → открывает форму-мастер). ### Что уже есть (берём за основу каркаса) - Сайдбар: пункты из documentTypes (typeLabel + icon), активный тип, бейдж-счётчик «на согласовании у меня» на пункте. → заменить на @tnsu/ui-kit Sidebar. - Контент по активному типу: заголовок, тулбар (поиск + фильтр статуса + кнопка «Создать» по createNewUrl) и 3 вкладки-очереди: «На согласовании у меня» / «Инициализированные мной» / «Согласованные мной» (Fluent Pivot). - DocumentTypeConfig (per-type): typeKey, typeLabel, icon, myDocsListName, buildMyDocsFilter, myDocsSelect, pendingApproval{...}, approvedByMe{...}, buildLinkUrl(row) (ctId для черновика / ctid иначе), createNewUrl/Label, mapItem(raw→DashboardRow), columns[]. - useDocumentDashboard(context, documentTypes) — грузит myDocs/approvedDocs/pendingDocs. - StatusBadge (variant neutral/info/success/error) → @tnsu/ui-kit Badge; маппинг статусов — в конфиге типа. - DocumentTable → @tnsu/ui-kit DataTable (колонки уже описаны в конфиге). ### Fluent → ui-kit при переносе Pivot → НУЖЕН Tabs (в ките пока НЕТ); SearchBox → TextInput; Dropdown → Select; Spinner/MessageBar → Alert; Default/PrimaryButton → Button; свой сайдбар → Sidebar; DocumentTable → DataTable. ### Как это меняет каркас портала Портал = config-driven: массив конфигов типов (CT/OID/KPS), каждый описывает и РЕЕСТР (списки/очереди из myDocuments), и СОЗДАНИЕ/РЕДАКТИРОВАНИЕ (мастер из ct/oid/kps). Оболочка (Sidebar + вкладки) — из myDocuments. Мастера — из kps-каркаса. Эталоны: shell/реестр = myDocuments, мастер = kps. ### Пробел ui-kit Tabs (вкладки-очереди, замена Pivot) — добавить в @tnsu/ui-kit (был в списке: из request-portal CategoryTabs).