diff --git a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx index b430fe6..0dbd27d 100644 --- a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx +++ b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx @@ -456,8 +456,54 @@ function DictionaryDetail() { // handleBulkExport больше не нужен — bulk export через InfoPanel "Экспорт..." // → ExportModal автоматически pre-picks scope=selected когда selection непуст. - const handleBulkClose = () => { + const [bulkDraftRunning, setBulkDraftRunning] = useState(false) + const handleBulkClose = async () => { if (selection.size === 0) return + if (approvalRequired) { + // Approval Workflow v2: на approval-required dict bulk-close ходит + // не через POST /records/_bulk_close (вернёт 409 draft_required для + // каждого), а через N отдельных draft submissions (operation=CLOSE). + // Backend сейчас не имеет _bulk endpoint для drafts, делаем Promise.all + // на клиенте — N drafts создаются параллельно, результат собирается + // в тот же BulkCloseResponse shape. + setBulkDraftRunning(true) + const targets = Array.from(selection) + const result: BulkCloseResponse = { closed: [], skipped: [], errors: [] } + await Promise.all( + targets.map(async (bk) => { + try { + await submitDraftMut.mutateAsync({ + businessKey: bk, + operation: 'CLOSE', + comment: bulkReason || null, + }) + // 'closed' семантически здесь = 'submitted as CLOSE draft'. + // Frontend label показывает «отправлено на ревью» когда approvalRequired. + result.closed.push(bk) + } catch (e: unknown) { + const code = + (e as { response?: { data?: { code?: string } } })?.response + ?.data?.code ?? + (e as Error)?.message ?? + 'unknown' + // draft_already_pending — этот key уже на ревью, skip (не error). + if (code === 'draft_already_pending') { + result.skipped.push({ businessKey: bk, reason: code }) + } else { + result.errors.push({ businessKey: bk, reason: code }) + } + } + }), + ) + setBulkResult(result) + setSelection((prev) => { + const next = new Set(prev) + for (const k of result.closed) next.delete(k) + return next + }) + setBulkDraftRunning(false) + return + } bulkCloseMut.mutate( { businessKeys: Array.from(selection), @@ -614,6 +660,25 @@ function DictionaryDetail() { const handleClose = () => { if (edit.kind !== 'close-confirm') return const targetKey = edit.record.businessKey + if (approvalRequired) { + // Approval Workflow v2: close тоже через draft (operation=CLOSE). + // Maker подаёт CLOSE proposal, reviewer'у достаточно approve и запись + // закроется автоматически через recordService.applyApprovedClose. + submitDraftMut.mutate( + { + businessKey: targetKey, + operation: 'CLOSE', + comment: closeReason || null, + }, + { + onSuccess: () => { + setEdit({ kind: 'closed' }) + setCloseReason('') + }, + }, + ) + return + } closeMut.mutate( { businessKey: targetKey, reason: closeReason || undefined }, { @@ -1263,20 +1328,35 @@ function DictionaryDetail() {
{t('dict.confirmClose.body', { key: edit.record.businessKey })}
+ {approvalRequired && ( +{t('dict.bulk.confirmBody', { count })}
+ {approvalMode && ( +