План реализации таблицы корректировок (#206)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
31fb404b47
commit
59bbf484cf
338
docs/superpowers/plans/2026-10-28-corrections-table-ui.md
Normal file
338
docs/superpowers/plans/2026-10-28-corrections-table-ui.md
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
# Таблица корректировки (UI) — план реализации
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** вкладка «Таблица корректировки» страницы отчёта версии: таблица агрегатов ВГО со строками ОСВ, нераспределённые
|
||||||
|
строки, решения по строкам, окно корректировки агрегата, экспорт — по дизайну 23.10 и аналитике Лысенко.
|
||||||
|
|
||||||
|
**Architecture:** только фронт `web/`. Данные — по контракту `../docs/dev/api-contract-corrections.md` (+ `.openapi.yaml`);
|
||||||
|
пока бэкенда (AURORA-1540) нет, клиент `services/api/corrections.ts` при `REACT_APP_CORRECTIONS_MOCK=true` отвечает из фейка
|
||||||
|
в памяти. Вся логика (строки таблицы, фильтры, итоги, окно, черновик) — чистые функции с тестами vitest; компоненты тонкие и
|
||||||
|
проверяются в браузере против дизайна.
|
||||||
|
|
||||||
|
**Tech Stack:** React 18, TS strict, antd 6, react-router 7, vitest 3, biome (+ ESLint/Prettier `npm run lint`).
|
||||||
|
|
||||||
|
**Spec:** `openspec/changes/corrections-table-ui/` — `design.md` (главное), `proposal.md`, `specs/corrections-table-ui/spec.md`,
|
||||||
|
дельта `specs/intercompany-matrix-ui/spec.md`. Контракт — `/Users/andrey.epifancev/projects/awp-ca/.wt/corrections-ui/docs/dev/api-contract-corrections.md`.
|
||||||
|
Дизайн — `/Users/andrey.epifancev/projects/awp-ca/ДКБ_таблица_корректировок_23_10_26_1.html` (SPA; открывать в браузере).
|
||||||
|
|
||||||
|
**Ветка:** `feat/206-corrections-ui`, worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/corrections-ui/arm-core`, issue
|
||||||
|
RSHB/arm-core#206, Jira AURORA-1541.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Только `web/`; бэкенд, DBML, `api/` не трогать.
|
||||||
|
- Деньги — строки контракта в рублях; показ — `formatScaledAmount` (`utils/money.ts`) через `useNumberDisplay`; сложение —
|
||||||
|
только `sumAmounts`; никаких `Number(...)`/`parseFloat` над суммами.
|
||||||
|
- Фронт не считает разнесение, отклонения, порог и статусы — берёт из ответа (`exceeds_threshold`, `deviation_*`, `status`,
|
||||||
|
`candidate_rule_ids`). Исключение — живой контроль в окне (суммы несохранённых статей) и «Итого» видимых строк.
|
||||||
|
- Только `statement_type = 'balance'`.
|
||||||
|
- Тексты (дословно): вкладка «Таблица корректировки»; «Учитывать в КФО»; группы «Дебет», «Кредит», «Контроль»,
|
||||||
|
«Корректировка»; «Отклонение, %», «Отклонение, сумма»; «Корректировать»; «Корректировка операции»; «Исходные данные ОСВ»;
|
||||||
|
«Добавить строку ОСВ»; «Распределение корректировки»; «Добавить строку»; «Комментарий к корректировке»; «Отменить»;
|
||||||
|
«Сохранить корректировки»; «Отображение» — «Общий» / «По пресету»; «Правило»; «Тип записи» — «Все» / «Пары счетов» /
|
||||||
|
«Нераспределенные записи»; «Сбросить фильтры»; «Настройка таблицы»; «Экспорт»; «На весь экран»; «Итого»;
|
||||||
|
«Внутригрупповых остатков между компаниями периметра нет»; «Нет записей для выбранных фильтров»; «Состав изменился после
|
||||||
|
корректировки»; «черновик»; «Перенести в правило…»; «Отнести к правилу…»; «Убрать из правила»; «Вернуть к автоматике»;
|
||||||
|
«Отменить корректировку»; «Да» / «Частично» / «Нет»; «Восстановлены несохранённые правки от ЧЧ:ММ»; «Сбросить»;
|
||||||
|
«Корректировку изменили после ваших правок»; «Есть несохранённые изменения. Закрыть без сохранения?»; «Остаться»;
|
||||||
|
«Корректировку уже изменили, данные обновлены»; подсказки только чтения: `VERSION_NOT_EDITABLE` — «Версия закрыта для
|
||||||
|
изменений», `LOCK_REQUIRED` — «Возьмите консолидацию в работу», `LOCKED_BY_OTHER` — «Консолидацию редактирует другой
|
||||||
|
пользователь».
|
||||||
|
- Подпись правила: `Правило N: <статья 1> — <статья 2>`, составная статья `"1450/1550"` показывается `1450 / 1550`.
|
||||||
|
- Порог для красного в окне (живой контроль): процент к меньшей стороне `≥ 5` или одна сторона 0 при ненулевой другой.
|
||||||
|
- `localStorage`/`sessionStorage` — только в `try/catch`; недоступно — работа без черновика.
|
||||||
|
- Стиль: табы, одинарные кавычки, ширина 140 (biome); комментарии в коде — по-русски; файлы виджета — в
|
||||||
|
`web/src/widgets/CorrectionTable/`.
|
||||||
|
- Проверки перед коммитом задачи: из `web/` — `npm test`, `npx tsc --noEmit -p .`, `npx biome check src`; в Task 7 — ещё
|
||||||
|
`npm run build` и `make web-check` из корня репозитория.
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
- Ответ на устаревший запрос (аналитик быстро щёлкнул два флажка) перетирает свежую таблицу — `useCorrectionTable` применяет
|
||||||
|
только ответ последнего запроса (счётчик запросов, как `usePagedRows`); тест в Task 4 на функцию выбора ответа.
|
||||||
|
- Сумма статьи «1 000 000,5» / «1000000,567» / «-5» / пусто в окне: принимается только `parseAmountInput`-валидное ≥ 0 с ≤ 2
|
||||||
|
знаками, иначе поле с ошибкой и «Сохранить» неактивна — тест в Task 3 (`validateLines`).
|
||||||
|
- Черновик из `localStorage` от другой формы данных (старый формат, мусор) не ломает окно — `loadDraft` возвращает `null` —
|
||||||
|
тест в Task 3.
|
||||||
|
- Агрегат исчез из ответа после переноса строк, пока окно открыто (все строки убраны) — окно показывает «—» по сторонам и
|
||||||
|
не падает; `findAggregate` возвращает `null` — тест в Task 2.
|
||||||
|
- Фильтр «ЮЛ» с латиницей/регистром/кавычками («ооо "бск"») находит «ООО "БСК"» — сравнение без регистра — тест в Task 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Типы контракта, клиент API, флаг фейка
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/types/corrections.ts`
|
||||||
|
- Create: `web/src/services/api/corrections.ts`
|
||||||
|
- Modify: `web/src/conf/config.ts`
|
||||||
|
- Test: `web/src/services/api/corrections.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces (`types/corrections.ts`) — типы 1:1 с `components.schemas` OpenAPI: `StatementType`, `Side = 'D' | 'K'`,
|
||||||
|
`Ref`, `CorrectionTableOut`, `CorrectionRuleOut`, `CorrectionAggregateOut`, `AggregateSideOut`, `CorrectionEntryOut`,
|
||||||
|
`UnallocatedEntryOut`, `CorrectionOut`, `CorrectionLineOut`, `CorrectionBalanceOut`, `CorrectionTotalsOut`,
|
||||||
|
`CorrectionLineIn`, `CorrectionIn`, `CorrectionRevokeIn`, `AllocationAction = 'ASSIGN' | 'UNASSIGN' | 'EXCLUDE' | 'INCLUDE' | 'RESET'`,
|
||||||
|
`AllocationIn`, `ReadOnlyReason`, `AggregateKey = { rule_id: string; company_1_id: string; company_2_id: string }`.
|
||||||
|
- Produces (`services/api/corrections.ts`):
|
||||||
|
`getCorrectionTable(versionId: string): Promise<CorrectionTableOut>`;
|
||||||
|
`saveCorrection(versionId: string, key: AggregateKey, body: CorrectionIn): Promise<CorrectionAggregateOut>`;
|
||||||
|
`revokeCorrection(versionId: string, key: AggregateKey, body: CorrectionRevokeIn): Promise<CorrectionAggregateOut>`;
|
||||||
|
`allocate(versionId: string, body: AllocationIn): Promise<CorrectionTableOut>`;
|
||||||
|
`exportCorrectionTable(versionId: string): Promise<{ blob: Blob; filename: string | null }>` (форма ответа — как у
|
||||||
|
`apiFetchBlob`; если она другая — вернуть то, что даёт `apiFetchBlob`, и записать в Interfaces);
|
||||||
|
`correctionsPath(versionId: string, key?: AggregateKey): string`.
|
||||||
|
При `Config.CORRECTIONS_MOCK` все функции делегируют в `correctionsFake` (Task 2); флаг читается в рантайме, фейк
|
||||||
|
остаётся в бандле — это допустимо (данные фейка — небольшие, без персональных данных).
|
||||||
|
- Produces (`conf/config.ts`): `Config.CORRECTIONS_MOCK: boolean` = `environment.REACT_APP_CORRECTIONS_MOCK === 'true'`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: node_modules в worktree.** `ln -s /Users/andrey.epifancev/projects/awp-ca/arm-core/web/node_modules web/node_modules`
|
||||||
|
(тот же `package-lock.json`, что у `main`; в git не попадает). Проверка: `cd web && npx vitest --version` печатает `3.1.4`.
|
||||||
|
- [ ] **Step 2: Failing test** `corrections.test.ts`:
|
||||||
|
`correctionsPath('v1')` → `'/consolidations/versions/v1/corrections'`;
|
||||||
|
`correctionsPath('v1', {rule_id: 'r', company_1_id: 'a', company_2_id: 'b'})` → `'/consolidations/versions/v1/corrections/aggregates/r/a/b'`.
|
||||||
|
- [ ] **Step 3: Run** `cd web && npx vitest run src/services/api/corrections.test.ts` — FAIL (нет модуля).
|
||||||
|
- [ ] **Step 4: Implement** типы, `correctionsPath`, функции поверх `api.get/put/post` и `apiFetchBlob`
|
||||||
|
(`?statement_type=balance` в GET и export), флаг в `Config`. До Task 2 ветка фейка — `throw new Error('фейк в Task 2')`.
|
||||||
|
- [ ] **Step 5: Run** тест — PASS; `npx tsc --noEmit -p .` и `npx biome check src` — чисто.
|
||||||
|
- [ ] **Step 6: Commit** `feat(web): типы и клиент API таблицы корректировок (#206)`.
|
||||||
|
|
||||||
|
### Task 2: Фейк контракта и модель таблицы
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/mockData.ts` — данные из дизайна 23.10
|
||||||
|
- Create: `web/src/services/api/correctionsFake.ts`, Test: `web/src/services/api/correctionsFake.test.ts`
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/tableModel.ts`, Test: `web/src/widgets/CorrectionTable/tableModel.test.ts`
|
||||||
|
- Modify: `web/src/services/api/corrections.ts` (ветка фейка)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: типы Task 1.
|
||||||
|
- Produces (`correctionsFake.ts`): объект `correctionsFake` с теми же сигнатурами, что функции Task 1, состояние в памяти
|
||||||
|
модуля, `resetCorrectionsFake(): void` для тестов. Правила фейка: агрегат пересчитывается из своих строк (суммы сторон
|
||||||
|
`sumAmounts` по не-`excluded`, `deviation_amount`, `deviation_percent` к меньшей с двумя знаками, `exceeds_threshold` при
|
||||||
|
`≥ 5` или одна сторона 0), `composition_changed = true` при изменении состава агрегата с корректировкой; `ASSIGN` —
|
||||||
|
только в правило из `candidate_rule_ids`, иначе `ApiError` 422 `RULE_NOT_APPLICABLE`; пустой комментарий — 422
|
||||||
|
`COMMENT_REQUIRED`; `expected_version_no` не совпал — 409 `CORRECTION_VERSION_CONFLICT`; `revoke` без корректировки — 409
|
||||||
|
`CORRECTION_NOT_APPLIED`; `editable = true`. Деление для процента — BigInt-копейки с округлением половины вверх.
|
||||||
|
`mockData.ts`: 8 правил (как `api/seed/core/elimination_rules_v1.yaml`, статьи `1150`/`1450/1550`, `1170`/`1310/1350`,
|
||||||
|
`1170`/`1410`, `1240`/`1510`, `1230`/`1510`, `1230`/`1520`, `1230`/`1410`, `1230`/`1510`), агрегаты и нераспределённые строки
|
||||||
|
из дизайна (суммы в рублях: «ЭПТ — ИЛС» 1000000.00 / 202320.00 с корректировкой 1230 / 1520 и комментарием «Требуется
|
||||||
|
сверка с контрагентом»; «ООО "БЕКОВСКИЙ САХАРНЫЙ ЗАВОД" — БЕКОВСКИЙ САХАРНЫЙ КОМБИНАТ ООО» правила 6 — четыре строки с
|
||||||
|
каждой стороны; правило 3 — «Пензенский Мир Продуктов — Регион-Сахар» 29742550.00 / 29242550.00; нераспределённые — не
|
||||||
|
меньше пяти, включая кредитовую 76.05 «ООО "БСК"» → «ПМП ООО» 142878.00 с `candidate_rule_ids` правил 6 и 7).
|
||||||
|
- Produces (`tableModel.ts`):
|
||||||
|
```ts
|
||||||
|
type TableRow =
|
||||||
|
| { kind: 'aggregate'; key: string; aggregate: CorrectionAggregateOut; include: 'Да' | 'Частично' | 'Нет' }
|
||||||
|
| { kind: 'entry'; key: string; parentKey: string; side: 1 | 2; entry: CorrectionEntryOut }
|
||||||
|
| { kind: 'unallocated'; key: string; entry: UnallocatedEntryOut };
|
||||||
|
type DisplayMode = { mode: 'all' } | { mode: 'preset'; ruleId: string };
|
||||||
|
type RecordType = 'all' | 'posting' | 'unallocated';
|
||||||
|
type DeviationFilter = 'all' | 'over' | 'under';
|
||||||
|
type TableFilters = { display: DisplayMode; recordType: RecordType; entity: string; line: string | null; account: string; deviation: DeviationFilter };
|
||||||
|
const EMPTY_FILTERS: TableFilters;
|
||||||
|
aggregateKey(a: CorrectionAggregateOut): string // `${rule_id}|${company_1.id}|${company_2.id}`
|
||||||
|
toAggregateKey(a: CorrectionAggregateOut): AggregateKey
|
||||||
|
includeState(a: CorrectionAggregateOut): 'Да' | 'Частично' | 'Нет'
|
||||||
|
ruleLabel(r: CorrectionRuleOut): string // «Правило 6: 1230 — 1520», «Правило 1: 1150 — 1450 / 1550»
|
||||||
|
articleCodes(line: string): string[] // '1450/1550' → ['1450','1550']
|
||||||
|
lineOptions(table: CorrectionTableOut): string[] // отдельные коды статей агрегатов, по возрастанию
|
||||||
|
filterTable(table: CorrectionTableOut, f: TableFilters): { aggregates: CorrectionAggregateOut[]; unallocated: UnallocatedEntryOut[] }
|
||||||
|
buildRows(view: ReturnType<typeof filterTable>, expanded: ReadonlySet<string>): TableRow[]
|
||||||
|
visibleTotals(aggregates: CorrectionAggregateOut[]): { amount_1: string | null; amount_2: string | null; deviation_amount: string | null; correction_debit: string | null; correction_credit: string | null }
|
||||||
|
isFiltered(f: TableFilters): boolean
|
||||||
|
findAggregate(table: CorrectionTableOut, key: string): CorrectionAggregateOut | null
|
||||||
|
```
|
||||||
|
Правила `filterTable` — design.md «Панель»: пресет — агрегаты правила + нераспределённые с этим правилом в
|
||||||
|
`candidate_rule_ids`; `recordType`; `entity` — подстрока без регистра в именах обеих сторон (у нераспределённых — `owner`,
|
||||||
|
`counterparty`); `account` — начало кода любого счёта строки (`accounts` сторон / `account_code`); `line` и `deviation` —
|
||||||
|
только агрегаты (по `articleCodes` статей сторон и по `exceeds_threshold`), при любом из них нераспределённые скрываются.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing tests `tableModel.test.ts`** (на `mockData`):
|
||||||
|
`ruleLabel` правила 1 = `'Правило 1: 1150 — 1450 / 1550'`; `articleCodes('1310/1350')` = `['1310','1350']`;
|
||||||
|
`includeState` — все не исключены → `'Да'`, одна из восьми исключена → `'Частично'`, все → `'Нет'`;
|
||||||
|
пресет правила 3 → только агрегаты правила 3 и нераспределённые, где правило 3 в `candidate_rule_ids`;
|
||||||
|
`recordType: 'unallocated'` → `aggregates = []`;
|
||||||
|
`entity: 'ооо "бск"'` → агрегаты и нераспределённые «ООО "БСК"» (Review Focus);
|
||||||
|
`deviation: 'over'` → только `exceeds_threshold`, `unallocated = []`;
|
||||||
|
`buildRows` с раскрытым агрегатом правила 6 «Бековский завод — комбинат» → после агрегата 4 строки `side: 1`, затем 4 `side: 2`;
|
||||||
|
`visibleTotals` по агрегатам «ЭПТ — ИЛС» и правила 3 → `amount_1 = '30742550.00'`;
|
||||||
|
`findAggregate(table, 'нет')` → `null` (Review Focus).
|
||||||
|
- [ ] **Step 2: Failing tests `correctionsFake.test.ts`**: `getCorrectionTable` после `resetCorrectionsFake` отдаёт агрегат
|
||||||
|
«ЭПТ — ИЛС» с `deviation_percent = '394.27'`, `exceeds_threshold = true`; `allocate EXCLUDE` строки 62.03 агрегата правила 6
|
||||||
|
уменьшает `side_1.amount` на её сумму; `allocate ASSIGN` в правило не из `candidate_rule_ids` — `ApiError` с
|
||||||
|
`code = 'RULE_NOT_APPLICABLE'`; `saveCorrection` с `comment: ' '` — `COMMENT_REQUIRED`; повторный `saveCorrection` с
|
||||||
|
`expected_version_no: null`, когда корректировка есть, — `CORRECTION_VERSION_CONFLICT`; `revokeCorrection` → `correction = null`,
|
||||||
|
затем `saveCorrection` с `expected_version_no: null` проходит.
|
||||||
|
- [ ] **Step 3: Run** `cd web && npx vitest run src/widgets/CorrectionTable src/services/api/correctionsFake.test.ts` — FAIL.
|
||||||
|
- [ ] **Step 4: Implement** `mockData.ts`, `correctionsFake.ts`, `tableModel.ts`; в `corrections.ts` ветка флага вызывает
|
||||||
|
`correctionsFake`. `ApiError` создавать так же, как это делает `api/client.ts` (посмотреть конструктор).
|
||||||
|
- [ ] **Step 5: Run** тесты — PASS; tsc и biome — чисто.
|
||||||
|
- [ ] **Step 6: Commit** `feat(web): фейк контракта корректировок и модель таблицы (#206)`.
|
||||||
|
|
||||||
|
### Task 3: Модель окна корректировки и черновик
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/editorModel.ts`, Test: `web/src/widgets/CorrectionTable/editorModel.test.ts`
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/draftStorage.ts`, Test: `web/src/widgets/CorrectionTable/draftStorage.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: типы Task 1, `articleCodes`, `aggregateKey` (Task 2), `parseAmountInput`, `sumAmounts` (`utils/money.ts`).
|
||||||
|
- Produces (`editorModel.ts`):
|
||||||
|
```ts
|
||||||
|
type EditLine = { line_code: string; amount: string }; // amount — как ввёл пользователь
|
||||||
|
type EditorState = { debit: EditLine[]; credit: EditLine[]; comment: string };
|
||||||
|
initialEditorState(a: CorrectionAggregateOut): EditorState // есть корректировка — её статьи (суммы как в API);
|
||||||
|
// нет — по стороне одна статья: первый код articleCodes(article_line), сумма стороны
|
||||||
|
validateLines(s: EditorState): { ok: true; body: Pick<CorrectionIn, 'debit' | 'credit' | 'comment'> } | { ok: false; errors: Record<string, string> }
|
||||||
|
// ключ ошибки `debit.0.amount`; пустая сторона ок, обе пустые — ошибка «Добавьте статью»;
|
||||||
|
// код — 4 цифры; сумма — parseAmountInput, ≥ 0, ≤ 2 знаков
|
||||||
|
liveControl(a: CorrectionAggregateOut, s: EditorState): { sourceDiff: Diff; allocationDiff: Diff }
|
||||||
|
type Diff = { left: string | null; right: string | null; difference: string | null; percent: string | null; red: boolean }
|
||||||
|
// percent к меньшей стороне, 2 знака; red — percent ≥ 5 или одна сторона 0 при ненулевой другой
|
||||||
|
isDirty(initial: EditorState, current: EditorState): boolean
|
||||||
|
pickerCandidates(table: CorrectionTableOut, a: CorrectionAggregateOut, side: 1 | 2): UnallocatedEntryOut[]
|
||||||
|
// та же пара: side 1 — owner = company_1, counterparty = company_2, entry.side = rule.side_1;
|
||||||
|
// side 2 — наоборот и противоположная сторона; правило агрегата в candidate_rule_ids; не excluded
|
||||||
|
toCorrectionIn(body: …, expectedVersionNo: number | null): CorrectionIn // status 'APPLIED'
|
||||||
|
```
|
||||||
|
- Produces (`draftStorage.ts`):
|
||||||
|
```ts
|
||||||
|
type Draft = { state: EditorState; savedAt: string; baseVersionNo: number | null };
|
||||||
|
draftKey(userId: string, versionId: string, aggregateKey: string): string // 'armka:correction-draft:<user>:<version>:<aggregate>'
|
||||||
|
saveDraft(key: string, draft: Draft, storage?: Storage | null): void
|
||||||
|
loadDraft(key: string, storage?: Storage | null): Draft | null // мусор/старый формат/исключение — null
|
||||||
|
clearDraft(key: string, storage?: Storage | null): void
|
||||||
|
isStale(draft: Draft, a: CorrectionAggregateOut): boolean // baseVersionNo ≠ (a.correction?.version_no ?? null)
|
||||||
|
```
|
||||||
|
`storage` по умолчанию — `window.localStorage`, доступ в `try/catch`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing tests `editorModel.test.ts`**: `initialEditorState` агрегата правила 3 без корректировки →
|
||||||
|
`debit = [{line_code: '1170', amount: '29742550.00'}]`, `credit = [{line_code: '1410', amount: '29242550.00'}]`;
|
||||||
|
агрегат правила 1 без корректировки → кредит `line_code = '1450'`;
|
||||||
|
`validateLines` с суммами `'1 000 000,5'`, `'1000000,567'`, `'-5'`, `''` → ошибки у `amount` (Review Focus), с `'500000,5'` →
|
||||||
|
`'500000.50'` в `body`; `liveControl` агрегата «ЭПТ — ИЛС» с исходным состоянием → `sourceDiff.percent = '394.27'`,
|
||||||
|
`red = true`; при Дт 1000000.00 и Кт 1000000.00 распределения → `allocationDiff.difference = '0.00'`, `red = false`;
|
||||||
|
`pickerCandidates` для агрегата «Пензенский Мир Продуктов — Бековский сахарный комбинат» правила 6, сторона 2 → только
|
||||||
|
кредитовые нераспределённые строки комбината на ПМП с правилом 6.
|
||||||
|
- [ ] **Step 2: Failing tests `draftStorage.test.ts`** (`storage` — простая заглушка `Map` с `getItem/setItem/removeItem`):
|
||||||
|
`saveDraft` → `loadDraft` возвращает то же; `setItem` строки `'{"x":1}'` → `loadDraft` = `null` (Review Focus); заглушка,
|
||||||
|
бросающая на `getItem`, → `null` без исключения; `isStale` при `baseVersionNo = 2` и `correction.version_no = 3` → `true`.
|
||||||
|
- [ ] **Step 3: Run** `cd web && npx vitest run src/widgets/CorrectionTable` — FAIL.
|
||||||
|
- [ ] **Step 4: Implement** оба модуля.
|
||||||
|
- [ ] **Step 5: Run** — PASS; tsc и biome — чисто.
|
||||||
|
- [ ] **Step 6: Commit** `feat(web): модель окна корректировки и черновик (#206)`.
|
||||||
|
|
||||||
|
### Task 4: Вкладка и таблица только на чтение
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/useCorrectionTable.ts`, Test: `web/src/widgets/CorrectionTable/useCorrectionTable.test.ts`
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/CorrectionTable.tsx` (таблица), `CorrectionToolbar.tsx` (панель),
|
||||||
|
`CorrectionContent.tsx` (вкладка: загрузка, состояния, панель + таблица)
|
||||||
|
- Modify: `web/src/widgets/ConsolidatedOsv/OsvTabs.tsx` (у `correction` убрать `disabled`/`tooltip`)
|
||||||
|
- Modify: `web/src/layouts/ConsolidationReportPage.tsx` (`TAB_COMPONENTS.correction`, `isValidTab` с `correction`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: Task 1–2 (`getCorrectionTable`, `filterTable`, `buildRows`, `visibleTotals`, `ruleLabel`, `lineOptions`,
|
||||||
|
`isFiltered`, `EMPTY_FILTERS`), `useNumberDisplay`, `ColumnSettings`/`useVisibleColumns` (`components/GenericTable`),
|
||||||
|
полноэкранный режим — как `toggleFullscreen` в `ConsolidatedChessContent.tsx`.
|
||||||
|
- Produces:
|
||||||
|
`latestOnly(): { next(): number; isLatest(id: number): boolean }` (в `useCorrectionTable.ts`, чистая, тестируется);
|
||||||
|
`useCorrectionTable(versionId: string): { table: CorrectionTableOut | null; loading: boolean; error: string | null; reload(): void; replace(t: CorrectionTableOut): void; replaceAggregate(a: CorrectionAggregateOut): void }`
|
||||||
|
— применяет только ответ последнего запроса (Review Focus);
|
||||||
|
`CorrectionContent({ versionId }: { versionId: string })`;
|
||||||
|
`CorrectionTable` props: `{ rows: TableRow[]; totals; format; expanded; onToggle(key); rules; editable; readOnlyHint: string | null; actions: RowActions }`,
|
||||||
|
где `RowActions` — колбэки Task 5–6 (`onCorrect(a)`, `onRevoke(a)`, `onAllocate(body)`), в этой задаче — заглушки.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing test** `useCorrectionTable.test.ts`: `latestOnly` — после `next()` = 1 и `next()` = 2 `isLatest(1) = false`,
|
||||||
|
`isLatest(2) = true`.
|
||||||
|
- [ ] **Step 2: Run** — FAIL.
|
||||||
|
- [ ] **Step 3: Implement** хук, вкладку и таблицу: колонки и тексты — Global Constraints и spec «Колонки и строка агрегата»,
|
||||||
|
«Раскрытие», «Нераспределённые строки», «Панель и фильтры»; суммы — `format` из `useNumberDisplay` (по умолчанию «тыс. руб.»,
|
||||||
|
2 знака, как в дизайне); красное — при `exceeds_threshold`; `composition_changed` — фон строки и значок с подсказкой;
|
||||||
|
`DRAFT` — метка «черновик»; «Итого» — `visibleTotals`, процент «—»; флажки и меню пока неактивны. Экспорт — `exportCorrectionTable`
|
||||||
|
+ `saveBlob(blob, filename ?? xlsxFilename('Таблица_корректировки', …))`. Вкладка доступна, адрес `…/correction`.
|
||||||
|
- [ ] **Step 4: Run** тест — PASS; `npx tsc --noEmit -p .`, `npx biome check src` — чисто.
|
||||||
|
- [ ] **Step 5: Проверка в браузере** (фейк): `cd web && REACT_APP_CORRECTIONS_MOCK=true npm run start`, бэкенд ядра для
|
||||||
|
карточки версии — как в `../docs/dev/qa-guide.md`; открыть `…/versions/<id>/correction`; сверить с дизайном (открыть
|
||||||
|
`ДКБ_таблица_корректировок_23_10_26_1.html` рядом): колонки, раскрытие агрегата правила 6, нераспределённые строки, пресет
|
||||||
|
правила 3, фильтры, «Итого», единицы. Снимки — в `/private/tmp/claude-501/` (scratchpad сессии), в репозиторий не класть.
|
||||||
|
- [ ] **Step 6: Commit** `feat(web): вкладка «Таблица корректировки» — таблица и панель (#206)`.
|
||||||
|
|
||||||
|
### Task 5: Решения по строкам, отмена корректировки, только чтение
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/RowActions.tsx` (флажок «Учитывать в КФО», меню строки, выбор правила)
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/RevokeModal.tsx`
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/readOnly.ts`, Test: `web/src/widgets/CorrectionTable/readOnly.test.ts`
|
||||||
|
- Modify: `CorrectionTable.tsx`, `CorrectionContent.tsx`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `allocate`, `revokeCorrection` (Task 1), `replace`/`replaceAggregate`/`reload` (Task 4), `ruleLabel`.
|
||||||
|
- Produces: `readOnlyHint(reason: ReadOnlyReason | null): string | null` (тексты — Global Constraints);
|
||||||
|
`allocationMenu(row: TableRow, rules: CorrectionRuleOut[]): { key: AllocationAction | 'MOVE'; label: string; disabled: boolean }[]`
|
||||||
|
(тестируется: у строки агрегата с `decision = 'AUTO'` нет «Вернуть к автоматике»; пустой `candidate_rule_ids` — «Перенести в
|
||||||
|
правило…» неактивен; у нераспределённой `reason = 'UNASSIGNED'` есть «Вернуть к автоматике»).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing tests** `readOnly.test.ts`: `readOnlyHint('LOCK_REQUIRED')` = «Возьмите консолидацию в работу», `null` → `null`;
|
||||||
|
три случая `allocationMenu` выше.
|
||||||
|
- [ ] **Step 2: Run** — FAIL.
|
||||||
|
- [ ] **Step 3: Implement**: флажок строки → `allocate({statement_type: 'balance', entry_ids, action: checked ? 'INCLUDE' : 'EXCLUDE'})`,
|
||||||
|
ответ — `replace`; меню → `ASSIGN` (выбор правила из `candidate_rule_ids`, подписи `ruleLabel`), `UNASSIGN`, `RESET`;
|
||||||
|
«Отменить корректировку» в меню агрегата → `RevokeModal` (комментарий обязателен, пустой — «Сохранить» неактивна) →
|
||||||
|
`revokeCorrection` → `replaceAggregate`. Ошибки API — уведомление с `ApiError.message`; 409 правок версии — ещё `reload()`.
|
||||||
|
`editable = false` — всё неактивно с подсказкой `readOnlyHint`.
|
||||||
|
- [ ] **Step 4: Run** тесты — PASS; tsc, biome — чисто.
|
||||||
|
- [ ] **Step 5: Проверка в браузере** (фейк): исключить строку 62.03 → у агрегата «Частично», сумма стороны меньше;
|
||||||
|
«Отнести к правилу…» у кредитовой 76.05 «ООО "БСК"» → строка в агрегате; «Отменить корректировку» у «Сатурн — Архол».
|
||||||
|
Флаг фейка `editable = false` (временно в тесте руками) — подсказки.
|
||||||
|
- [ ] **Step 6: Commit** `feat(web): решения по строкам и отмена корректировки (#206)`.
|
||||||
|
|
||||||
|
### Task 6: Окно «Корректировка операции»
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/widgets/CorrectionTable/CorrectionEditorModal.tsx`
|
||||||
|
- Modify: `CorrectionContent.tsx` (открытие окна из «Корректировать»)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: Task 3 целиком, `allocate`, `saveCorrection`, `findAggregate`, `toAggregateKey`, `useCurrentUser().id`,
|
||||||
|
`replace`/`replaceAggregate`/`reload`.
|
||||||
|
- Produces: `CorrectionEditorModal({ versionId, table, aggregateKey, editable, onClose, onTable, onAggregate })`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement** по spec «Окно…», «Сохранение…», «Черновик окна…»: состояние — `initialEditorState` или черновик
|
||||||
|
(`loadDraft(draftKey(...))`, плашки «Восстановлены несохранённые правки от ЧЧ:ММ» / «Корректировку изменили после ваших
|
||||||
|
правок» по `isStale`, кнопка «Сбросить»); `saveDraft` при каждой правке; «×» исходной строки и «Добавить строку ОСВ»
|
||||||
|
(`pickerCandidates`) — `allocate` сразу, таблица — `onTable`, агрегат окна — `findAggregate` (нет — по сторонам «—», Review
|
||||||
|
Focus); контроль — `liveControl`; «Сохранить корректировки» — `validateLines` → `saveCorrection(toCorrectionIn(body,
|
||||||
|
aggregate.correction?.version_no ?? null))` → `clearDraft`, `onAggregate`, закрыть; ошибки: `COMMENT_REQUIRED` — у
|
||||||
|
комментария, `INVALID_LINE_CODE` — у статьи `detail.line_code`, `CORRECTION_VERSION_CONFLICT` — сообщение «Корректировку уже
|
||||||
|
изменили, данные обновлены» + `reload()`; «Отменить» при `isDirty` — `Modal.confirm` «Есть несохранённые изменения. Закрыть
|
||||||
|
без сохранения?» («Остаться» / закрыть с `clearDraft`). `editable = false` — всё только на просмотр, без «Сохранить
|
||||||
|
корректировки».
|
||||||
|
- [ ] **Step 2: Run** `npx tsc --noEmit -p .`, `npx biome check src`, `npm test` — чисто.
|
||||||
|
- [ ] **Step 3: Проверка в браузере** (фейк): окно «ЭПТ — ИЛС» (суммы, 394,27 %, красное); правило 3 без корректировки —
|
||||||
|
подстановка 1170 / 1410; добавить статью 1550 на стороне Кт, сохранить без комментария → ошибка у поля; с комментарием →
|
||||||
|
таблица показывает две статьи; правка суммы → «Назад» в браузере → вернуться → плашка восстановления; «Отменить» →
|
||||||
|
вопрос. Сверка вида окна с дизайном (`editor0.png`, `editor-unalloc.png` — снимки в
|
||||||
|
`/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/d22b6541-bb55-46fc-a4fe-8bba06387616/scratchpad/mockup-1023/`).
|
||||||
|
- [ ] **Step 4: Commit** `feat(web): окно «Корректировка операции» с черновиком (#206)`.
|
||||||
|
|
||||||
|
### Task 7: Сверка с дизайном, проверки, документация
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web/README.md` (раздел про флаг `REACT_APP_CORRECTIONS_MOCK`), `web/ARCHITECTURE.md` (виджет `CorrectionTable`)
|
||||||
|
- Modify: `openspec/changes/corrections-table-ui/tasks.md` (создать по факту выполненного, все пункты отмечены)
|
||||||
|
|
||||||
|
- [ ] **Step 1:** полный проход сценариев spec `corrections-table-ui` в браузере на фейке, сверка каждой части экрана с
|
||||||
|
дизайном в Playwright (оба окна рядом, одинаковая ширина 1920); расхождения — исправить или записать в отчёт с причиной.
|
||||||
|
- [ ] **Step 2:** из корня репозитория `make web-check` — PASS; из `web/` — `npm test`, `npm run lint` — PASS.
|
||||||
|
- [ ] **Step 3:** `openspec validate corrections-table-ui --strict` — valid.
|
||||||
|
- [ ] **Step 4: Commit** `docs(web): таблица корректировок — README, ARCHITECTURE, tasks (#206)`.
|
||||||
|
|
||||||
|
### Task 8 (после слияния AURORA-1540): подключение к API
|
||||||
|
|
||||||
|
Выполняется отдельно, когда эндпоинты 1540 в `main`. Влить `main` в ветку; запустить без флага на стенде
|
||||||
|
(`../docs/dev/qa-guide.md`) на реальной версии; пройти сценарии spec; расхождения ответа с контрактом — правка в фейке и
|
||||||
|
типах или замечание в PR 1540; `make check-full`; PR в `main` (до этого PR не открывать — фронт без API не сливаем, если
|
||||||
|
владелец не решит иначе).
|
||||||
Loading…
x
Reference in New Issue
Block a user