From 6eb56cffe4383d6d488fcd0a17f710b83b0a7585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=97=D0=B8=D0=BC=D0=B8=D0=BD?= Date: Wed, 13 May 2026 15:29:11 +0000 Subject: [PATCH] =?UTF-8?q?feat(records):=20auto-route=20create/edit=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20draft=20=D0=BA=D0=BE=D0=B3?= =?UTF-8?q?=D0=B4=D0=B0=20approvalRequired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/routes/dictionaries.$name.tsx | 74 ++++++++++++++++++- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx index a8c794f..b430fe6 100644 --- a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx +++ b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx @@ -37,7 +37,7 @@ import { useRecords, type RecordsFilter, } from '@/api/queries' -import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord, useFormIdempotencyKey } from '@/api/mutations' +import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord, useFormIdempotencyKey, useSubmitDraft } from '@/api/mutations' import { useCanMutate } from '@/auth/useCanMutate' import { cn } from '@/lib/utils' import type { BulkCloseResponse, CreateRecordRequest, DataScope, FlattenedRecord } from '@/api/client' @@ -185,6 +185,12 @@ function DictionaryDetail() { }, [pendingDraftsQuery.data]) const createMut = useCreateRecord(name) const updateMut = useUpdateRecord(name) + // Approval Workflow v2 (D2=A): когда dict отмечен approvalRequired=true, + // backend rejects direct CRUD с 409 draft_required. Используем submit-draft + // путь автоматически — UX same form, но POST в /records/drafts вместо + // /records. Maker подаёт proposal, reviewer approve'нёт → запись применится. + const submitDraftMut = useSubmitDraft(name) + const approvalRequired = detailQuery.data?.approvalRequired === true // Per-form idempotency key. Stable между ререндерами для одной сессии edit // modal'а: двойной submit (Enter + click) не создаст 2 record'а — backend // deduplicate'ит по тому же ключу. Reset на onSuccess. @@ -527,6 +533,29 @@ function DictionaryDetail() { setConflict({ payload: submitReq, businessKey: bk }) } if (edit.kind === 'create') { + if (approvalRequired) { + // Approval Workflow v2: POST /records/drafts вместо /records. Backend + // создаёт draft со status=PENDING, после approve reviewer'ом запись + // материализуется в live table. UX same form, разный endpoint. + submitDraftMut.mutate( + { + businessKey: submitReq.businessKey ?? '', + operation: 'CREATE', + data: submitReq.data, + geometryWkt: submitReq.geometryWkt ?? null, + validFrom: submitReq.validFrom ?? null, + validTo: submitReq.validTo ?? null, + }, + { + onSuccess: () => { + setEdit({ kind: 'closed' }) + resetRecordIdempotencyKey() + }, + onError: (err) => handleSaveError(err, submitReq.businessKey ?? ''), + }, + ) + return + } createMut.mutate( { payload: submitReq, idempotencyKey: recordIdempotencyKey }, { @@ -544,6 +573,27 @@ function DictionaryDetail() { return } if (edit.kind === 'edit') { + if (approvalRequired) { + // Approval Workflow v2: update тоже через draft (UPDATE proposal). + submitDraftMut.mutate( + { + businessKey: edit.record.businessKey, + operation: 'UPDATE', + data: submitReq.data, + geometryWkt: submitReq.geometryWkt ?? null, + validFrom: submitReq.validFrom ?? null, + validTo: submitReq.validTo ?? null, + }, + { + onSuccess: () => { + setEdit({ kind: 'closed' }) + resetRecordIdempotencyKey() + }, + onError: (err) => handleSaveError(err, edit.record.businessKey), + }, + ) + return + } updateMut.mutate( { businessKey: edit.record.businessKey, @@ -596,8 +646,14 @@ function DictionaryDetail() { ) } - const activeMutation = - edit.kind === 'create' ? createMut : edit.kind === 'edit' ? updateMut : null + // Approval Workflow v2: когда dict approval-required — мы делаем submit + // через submitDraftMut, поэтому activeMutation должен указывать на него, + // иначе spinner / error из createMut/updateMut выйдут несинхронно. + const activeMutation = (() => { + if (edit.kind === 'create') return approvalRequired ? submitDraftMut : createMut + if (edit.kind === 'edit') return approvalRequired ? submitDraftMut : updateMut + return null + })() const serverError = serverErrorMessage(activeMutation?.error) const totalRecords = recordsResult.data?.length ?? 0 @@ -1091,7 +1147,17 @@ function DictionaryDetail() { }} caption={ edit.kind === 'edit' - ? t('drawer.captionEdit', { dict: name }) + ? approvalRequired + ? t('drawer.captionEditDraft', { + dict: name, + defaultValue: `${name} — proposal на изменение (через ревью)`, + }) + : t('drawer.captionEdit', { dict: name }) + : approvalRequired + ? t('drawer.captionCreateDraft', { + dict: name, + defaultValue: `${name} — proposal на создание (через ревью)`, + }) : t('drawer.captionCreate', { dict: name }) } recordId={edit.kind === 'edit' ? edit.record.businessKey : undefined}