diff --git a/TODOS.md b/TODOS.md
index 4bf32ed..29a0db9 100644
--- a/TODOS.md
+++ b/TODOS.md
@@ -17,40 +17,14 @@ record rows reuse ReviewDrawer with status-gated footer.
---
-## 2. Interaction state pass — reviews page polish
+## 2. Interaction state pass — reviews page polish ⏳ MR !189 pending
-**What.** One MR covering five small fixes:
-
-- Records empty state: add description + primary action ("Queue is clear, see
- /audit for past decisions"). Current ``
- is just a heading.
-- Reject reason inline validation: today `handleReject` does
- `if (!comment.trim()) return` silently. Button click → nothing happens. Show
- inline error or set the TextArea into error state.
-- Drawer empty state (`workflow.schemaDraft.empty`): distinguish "not loaded",
- "not found" (deleted), "no permission". Today the same blank message covers
- all three.
-- Approve action: drawer closes immediately on success. Add a 500ms success
- state (or toast) so the reviewer gets visual confirmation. Currently feels
- like the click might not have registered.
-- Post-bulk-result polish: the `bulkResult` Alert lists business keys but the
- failed list shows raw `reason` codes (`self_approve_forbidden`,
- `draft_not_pending`). Map through `extractReviewError` already in the file.
-
-**Why.** Pass 2 rated state coverage 5/10. These are the visible gaps. Each
-is 10-30 LOC; the value is interaction confidence.
-
-**Pros.** All in one place — `reviews.tsx`, `SchemaDraftDrawer.tsx`. Low risk.
-
-**Cons.** Five different surfaces in one MR means review surface is wider than
-a single-purpose change. Still worth grouping because they're all "state
-polish" semantically.
-
-**Context.** Pass 2 finding. None of these block functionality; they're trust
-builders. Reject silent fail is the most user-visible — a reviewer clicks the
-button and nothing happens, then notices the TextArea is empty after a beat.
-
-**Depends on / blocked by.** Nothing.
+Closed in [MR !189](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/merge_requests/189) — all 5 fixes in one MR:
+1. Records empty state — icon + description + audit link
+2. Reject reason inline validation — aria-invalid + role=alert
+3. SchemaDraftDrawer empty state — QueryErrorState для errors + dashed block для no-draft
+4. Approve/reject success flash — 600ms green/pink band before close
+5. Bulk failed reasons humanized — extracted `humanizeBulkReason()` lib + 7 unit tests
---
diff --git a/ordinis-admin-ui/src/components/workflow/SchemaDraftDrawer.tsx b/ordinis-admin-ui/src/components/workflow/SchemaDraftDrawer.tsx
index 1a3d533..29a21f8 100644
--- a/ordinis-admin-ui/src/components/workflow/SchemaDraftDrawer.tsx
+++ b/ordinis-admin-ui/src/components/workflow/SchemaDraftDrawer.tsx
@@ -206,13 +206,24 @@ export function SchemaDraftDrawer({
>
{isLoading && }
+ {/* QueryErrorState уже сам обрабатывает 403/404 с human messages —
+ больше не надо guessить здесь. Раньше один blank empty покрывал
+ "not loaded / not found / forbidden" одинаково. */}
{error && refetch()} />}
- {!isLoading && !draft && (
-
+ {t('workflow.schemaDraft.empty', {
+ defaultValue: 'Активного черновика на этом словаре нет.',
+ })}
+
+
+ {t('workflow.schemaDraft.emptyHint', {
+ defaultValue:
+ 'Создайте новый черновик через editor словаря — кнопка «Создать черновик схемы» появится если ваши изменения требуют ревью.',
+ })}
+
+
)}
{draft && (
diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts
index 2f8acb2..c9e3f8c 100644
--- a/ordinis-admin-ui/src/i18n.ts
+++ b/ordinis-admin-ui/src/i18n.ts
@@ -581,6 +581,14 @@ i18n
'reviews.tab.records': 'Записи',
'reviews.tab.schemas': 'Схемы',
'reviews.tab.mine': 'Мои',
+ // Interaction state polish (TODO 2)
+ 'reviews.emptyDescription':
+ 'Очередь чистая. Прошлые решения можно посмотреть в журнале аудита.',
+ 'reviews.emptyAuditLink': 'Открыть /audit',
+ 'reviews.drawer.rejectReasonRequired':
+ 'Укажите причину отклонения — comment обязателен для reject (compliance).',
+ 'workflow.schemaDraft.emptyHint':
+ 'Создайте новый черновик через editor словаря — кнопка «Создать черновик схемы» появится если ваши изменения требуют ревью.',
'reviews.schema.empty': 'Очередь пуста',
'reviews.schema.emptyDescription': 'Схемы на ревью появятся здесь.',
// === /reviews → "Мои" tab (maker self-tracking) ===
@@ -1315,6 +1323,14 @@ i18n
'reviews.tab.records': 'Records',
'reviews.tab.schemas': 'Schemas',
'reviews.tab.mine': 'Mine',
+ // Interaction state polish (TODO 2)
+ 'reviews.emptyDescription':
+ 'Queue is clear. Past decisions live in the audit log.',
+ 'reviews.emptyAuditLink': 'Open /audit',
+ 'reviews.drawer.rejectReasonRequired':
+ 'Reject requires a comment (compliance) — fill the comment field first.',
+ 'workflow.schemaDraft.emptyHint':
+ 'Create a new draft via the dictionary editor — the "Create schema draft" button appears when your changes need review.',
'reviews.schema.empty': 'Queue is empty',
'reviews.schema.emptyDescription': 'Schemas under review will appear here.',
// === /reviews → "Mine" tab (maker self-tracking) ===
diff --git a/ordinis-admin-ui/src/lib/bulkReason.test.ts b/ordinis-admin-ui/src/lib/bulkReason.test.ts
new file mode 100644
index 0000000..f626247
--- /dev/null
+++ b/ordinis-admin-ui/src/lib/bulkReason.test.ts
@@ -0,0 +1,44 @@
+import { describe, expect, it } from 'vitest'
+import { humanizeBulkReason } from './bulkReason'
+
+describe('humanizeBulkReason', () => {
+ it('maps self_approve_forbidden to russian title', () => {
+ expect(humanizeBulkReason('self_approve_forbidden')).toBe(
+ 'Нельзя одобрить свой draft (maker-checker)',
+ )
+ })
+
+ it('maps draft_not_pending to russian title', () => {
+ expect(humanizeBulkReason('draft_not_pending')).toBe(
+ 'Draft уже не pending — обнови очередь',
+ )
+ })
+
+ it('maps forbidden_role to russian title', () => {
+ expect(humanizeBulkReason('forbidden_role')).toBe(
+ 'Недостаточно прав (нужна reviewer role)',
+ )
+ })
+
+ it('maps reject_reason_required to russian title', () => {
+ expect(humanizeBulkReason('reject_reason_required')).toBe(
+ 'Reject требует comment (compliance)',
+ )
+ })
+
+ it('maps self_reject_forbidden to russian title', () => {
+ expect(humanizeBulkReason('self_reject_forbidden')).toBe(
+ 'Нельзя отклонить свой draft',
+ )
+ })
+
+ it('returns unknown reason as-is (backend message fallback)', () => {
+ expect(humanizeBulkReason('Some raw backend message')).toBe(
+ 'Some raw backend message',
+ )
+ })
+
+ it('returns empty string for empty input', () => {
+ expect(humanizeBulkReason('')).toBe('')
+ })
+})
diff --git a/ordinis-admin-ui/src/lib/bulkReason.ts b/ordinis-admin-ui/src/lib/bulkReason.ts
new file mode 100644
index 0000000..5416680
--- /dev/null
+++ b/ordinis-admin-ui/src/lib/bulkReason.ts
@@ -0,0 +1,27 @@
+/**
+ * Bulk-action failure reason → human-readable label.
+ *
+ *
Lives in `src/lib` (а не в `routes/reviews.tsx`) потому что bulk runner
+ * captures axios errors как plain string `reason` (либо `response.data.code`,
+ * либо `error.message`) и хранит в `BulkResult.failed[].reason`. Раньше
+ * reviewer видел сырой code типа `self_approve_forbidden` в bulk result
+ * alert'е — теперь маппит на russain title из {@code extractReviewError}
+ * mapping (single source of truth).
+ *
+ *
Если reason нет в known codes — возвращает as-is (backend message
+ * вероятно уже human-readable, либо unknown server error который оставляем
+ * сырым чтобы reviewer мог скопировать в bug report).
+ *
+ *
Surfaced during TODO 2 (`/plan-design-review` interaction state pass
+ * 2026-05-14). Pure function — unit tested через `bulkReason.test.ts`.
+ */
+export function humanizeBulkReason(reason: string): string {
+ const known: Record = {
+ self_approve_forbidden: 'Нельзя одобрить свой draft (maker-checker)',
+ self_reject_forbidden: 'Нельзя отклонить свой draft',
+ draft_not_pending: 'Draft уже не pending — обнови очередь',
+ forbidden_role: 'Недостаточно прав (нужна reviewer role)',
+ reject_reason_required: 'Reject требует comment (compliance)',
+ }
+ return known[reason] ?? reason
+}
diff --git a/ordinis-admin-ui/src/routes/reviews.tsx b/ordinis-admin-ui/src/routes/reviews.tsx
index e2d52aa..cb6f55b 100644
--- a/ordinis-admin-ui/src/routes/reviews.tsx
+++ b/ordinis-admin-ui/src/routes/reviews.tsx
@@ -38,6 +38,7 @@ import { useApproveDraft, useRejectDraft, useWithdrawDraft } from '@/api/mutatio
import type { DraftOperation, DraftResponse, DraftStatus } from '@/api/client'
import { UserCell } from '@/lib/useUserDisplay'
import { shallowDiffSummary, isEmptyDiffSummary } from '@/lib/diff'
+import { humanizeBulkReason } from '@/lib/bulkReason'
import {
MyDraftsPanel,
classifyRecordStatus,
@@ -368,7 +369,7 @@ function ReviewsPage() {