8.6 KiB
Инвентарь поведения — ct / oid / kps → ct-forms-portal
Спецификация для нового объединённого проекта. Источники (РЕФЕРЕНС, не трогаем):
ct-create-form(ctCreateForm) — закупка по позициям из заявок TNSUoid-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-kitStepper.
Это ЧИСТАЯ логика — обязательно покрыть юнит-тестами (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.
- ссылка из уведомления (согласующий): загрузить из SP →
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-kitStepper; шапка —@tnsu/ui-kitFormHeader (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).