Compare commits
No commits in common. "b4b8c04693847e591633b74851273e86ca88912e" and "12fcea7560592855d4b4fe8efd05584fddc16255" have entirely different histories.
b4b8c04693
...
12fcea7560
@ -44,14 +44,6 @@ SPA кредитного аналитика АРМ КА: справочники
|
||||
- `src/store/` — Redux Toolkit: `store.ts`, слайсы `companyGroupsSlice` и `consolidations` (+ `thunks.ts`).
|
||||
- `src/layouts/` — страницы (маршруты в `src/App.tsx`, в т.ч. `/documents/:document_id` и `/groups/:group_id/versions/:version_id/osv`), `src/widgets/` — блоки экранов (`DocumentReview` — проверка ББ/ОФР/ОСВ со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer` — файлы ЮЛ (стол консолидации / все файлы ЮЛ, подпись консолидации, селектор `store/slices/consolidations/selectors.ts`)), `src/components/` — примитивы
|
||||
(`GenericTable`, `TableTag`, `PageTitle`), `src/types/` — DTO API.
|
||||
- Экран проверки формы (`src/widgets/DocumentReview/`): вся логика — чистые модули с тестами рядом, компоненты `.tsx` без юнит-тестов.
|
||||
`formLayout` — раскладка строк по разделам и статусам; `issuePlaces` — предупреждения по местам (ячейка / колонка / строка / поле шапки),
|
||||
список для панели и `collapseIssues`; `statementDraft` — весь черновик правки и сборка тела `PATCH` (суммы, подтверждения, коды, даты, шапка);
|
||||
`cellView` — цвета, подсказки, текст ячейки суммы и кода; `codeSuggestions` — подсказки кодов по справочнику; `lineMenu` — меню строки
|
||||
(«Это расшифровка», «Назначить код»); `editRules` — что можно править и какая кнопка подтверждения нужна ячейке/колонке;
|
||||
`confirmBlockers` — строки без кода, блокирующие подтверждение; `focusTarget` — к какой строке прокрутить по «Показать».
|
||||
`src/widgets/FilesDrawer/fileWarnings` и `documentRows` — предупреждения файла и документа в панели файлов; `src/utils/percent` —
|
||||
формат уверенности.
|
||||
|
||||
## Команды
|
||||
- `make install` — `npm install`.
|
||||
|
||||
@ -55,15 +55,6 @@ SPA кредитного аналитика поверх API `arm-core` (`/api/v
|
||||
Если на стенде раньше вручную сопоставляли контрагентов (K14), алиасы сохраняются между версиями, и
|
||||
«несопоставленных» будет меньше (на стенде прогона — 1349); остальные цифры не меняются.
|
||||
|
||||
## Экран проверки формы ББ/ОФР (контракт 2.0, ядро v5.19)
|
||||
|
||||
Что нового (#43): строки формы показываются по статусам (`ok` / нераспознанный код / без кода / расшифровка) на своих местах;
|
||||
предупреждения и нарушения стоят значками у ячеек, колонок, строк и полей шапки, а полный список свёрнут до трёх записей
|
||||
(«Показать все»); в режиме правки можно назначить код строке, пометить её расшифровкой и подтвердить значение, код или дату
|
||||
колонки — всё уходит одним `PATCH`; кнопка «Подтвердить данные» заблокирована, пока есть строки без кода (ссылка
|
||||
«Разберите строки без кода: N» ведёт к первой); панель файлов ЮЛ показывает счётчик предупреждений файла и документа.
|
||||
Карта модулей — в `CLAUDE.md`, раздел «Архитектура».
|
||||
|
||||
## Ограничения прототипа
|
||||
|
||||
- Нет блокировки версии («Взять в работу»), сопоставления контрагентов, экрана чек-листа, комментариев, удаления файлов,
|
||||
|
||||
@ -81,10 +81,5 @@
|
||||
| T10.D-4 | Значок — `<button>` под плашкой статуса; без `documentId` раздел «Предупреждения документа» не показывается (дата 2026-10-18) | правила a11y | нет |
|
||||
| T10.D-5 | Пустой список в разделе «Предупреждения документа» — «Нет» (дата 2026-10-18) | текст не из плана | только вид |
|
||||
| UI-1 | Тексты, которых нет в плане: «Дата подтверждена (до сохранения)», «Подтверждение будет сохранено — нажмите, чтобы убрать», «Строка не найдена в форме», «Наименование организации — не длиннее 1000 символов» (дата 2026-10-18) | нужны для состояний, плана для них не было | только формулировки |
|
||||
| T11.D-1 | Колонки значений 128px вместо 112 (поле даты из 10 символов и сумма с галочкой обрезались), `tableLayout='fixed'` (дата 2026-10-18) | замечание T11.1: колонка «прыгала» при появлении кнопки даты | только вид |
|
||||
| T11.D-2 | Кнопка подтверждения даты — короткая «Подтвердить» / «Подтверждено», длинный текст «Подтвердить дату» / «Дата подтверждена (до сохранения)» в подсказке и `aria-label` (дата 2026-10-18) | фиксированная ширина колонки; отклонение от формулировки «Подтвердить дату» в P6-11 | только подпись |
|
||||
| T11.D-3 | Ссылка «Разберите строки без кода: N» — `Button type=link` отдельной строкой над шапкой карточки, видна и в режиме правки (дата 2026-10-18) | клавиатурный доступ; строки нужно разбирать именно при правке | только вид |
|
||||
| T11.D-4 | Список предупреждений документа показывает первые 3 записи (ошибки первыми) и «Показать все (N)»; то же на экране ОСВ (дата 2026-10-18) | таблица уезжала ниже сгиба | только вид |
|
||||
| T11.D-5 | Значение со `string_value` без суммы показывается серым текстом, а не «-» (дата 2026-10-18) | аналитик должен видеть нераспознанный текст | только вид |
|
||||
|
||||
Решения контроллера и исполнителей поверх плана (`R-n`, `O-n`) дописываются сюда по ходу исполнения.
|
||||
|
||||
@ -51,18 +51,9 @@ export function ColumnHeader({ col, issues, editing, draftDate, error = false, o
|
||||
style={{ marginTop: 4, fontWeight: 400 }}
|
||||
/>
|
||||
{canConfirm && (
|
||||
// короткая подпись, чтобы кнопка помещалась в колонку 112 px и ширина не прыгала (T11.1); длинный текст — в подсказке
|
||||
<Tooltip title={pending ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}>
|
||||
<Button
|
||||
size='small'
|
||||
block
|
||||
style={{ marginTop: 4, paddingInline: 0, fontSize: 12 }}
|
||||
disabled={pending}
|
||||
aria-label={pending ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}
|
||||
onClick={() => onConfirmDate?.(col)}>
|
||||
{pending ? 'Подтверждено' : 'Подтвердить'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button size='small' style={{ marginTop: 4 }} disabled={pending} onClick={() => onConfirmDate?.(col)}>
|
||||
{pending ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { ExclamationCircleOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Alert, Button, Tag } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import type { Severity } from '../../types/api';
|
||||
import { severityLabel } from '../../utils/labels';
|
||||
import { type IssueListItem, collapseIssues } from './issuePlaces';
|
||||
|
||||
const COLLAPSED_LIMIT = 3; // сколько записей видно в свёрнутом списке (T11.11)
|
||||
import type { IssueListItem } from './issuePlaces';
|
||||
|
||||
/** Значок уровня (P6-15): error — красный, info — синий. */
|
||||
export function SeverityIcon({ severity, style }: { severity: Severity; style?: React.CSSProperties }) {
|
||||
@ -24,9 +21,7 @@ type Props = { items: IssueListItem[]; mode: 'statement' | 'document'; onShow?:
|
||||
*/
|
||||
export function IssuesPanel({ items, mode, onShow }: Props) {
|
||||
const plain = mode === 'document';
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
if (items.length === 0) return null;
|
||||
const { shown, hidden } = collapseIssues(items, expanded, COLLAPSED_LIMIT);
|
||||
return (
|
||||
<Alert
|
||||
type={items.some((i) => i.severity === 'error') ? 'error' : 'info'}
|
||||
@ -34,7 +29,7 @@ export function IssuesPanel({ items, mode, onShow }: Props) {
|
||||
title={`Предупреждения (${items.length})`}
|
||||
description={
|
||||
<div style={{ maxHeight: 120, overflowY: 'auto' }}>
|
||||
{shown.map((item) => (
|
||||
{items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<Tag color={item.severity === 'error' ? 'error' : 'processing'}>{severityLabel(item.severity)}</Tag>
|
||||
{item.message}
|
||||
@ -46,11 +41,6 @@ export function IssuesPanel({ items, mode, onShow }: Props) {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(hidden > 0 || expanded) && (
|
||||
<Button type='link' size='small' style={{ paddingInline: 0 }} onClick={() => setExpanded(!expanded)}>
|
||||
{expanded ? 'Свернуть' : `Показать все (${items.length})`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -45,12 +45,7 @@ export function StatementHeaderBlock({ header, documentType, editing, draft, onD
|
||||
const reportDate = formatReportDate(header.report_date.value);
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<table style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||
{/* фиксированная раскладка: ширина блока не зависит от содержимого и подписи кнопки (T11.6) */}
|
||||
<colgroup>
|
||||
<col style={{ width: 170 }} />
|
||||
<col />
|
||||
</colgroup>
|
||||
<table style={{ alignSelf: 'flex-end', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||
<tbody>
|
||||
{headerFields(header, byField).map((r) => (
|
||||
<tr key={r.field}>
|
||||
@ -59,6 +54,7 @@ export function StatementHeaderBlock({ header, documentType, editing, draft, onD
|
||||
style={{
|
||||
...cell,
|
||||
fontWeight: 600,
|
||||
minWidth: r.field === 'organization_name' ? 320 : 160,
|
||||
background: fieldBackground(r.modified, r.attention),
|
||||
}}>
|
||||
{r.issues.length > 0 && (
|
||||
@ -78,16 +74,9 @@ export function StatementHeaderBlock({ header, documentType, editing, draft, onD
|
||||
onChange={(e) => onDraftChange(r.field, e.target.value)}
|
||||
/>
|
||||
{r.canConfirm && onConfirmDate && (
|
||||
<Tooltip title={draft.confirm_report_date ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}>
|
||||
<Button
|
||||
size='small'
|
||||
style={{ marginTop: 4 }}
|
||||
disabled={draft.confirm_report_date === true}
|
||||
aria-label={draft.confirm_report_date ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}
|
||||
onClick={onConfirmDate}>
|
||||
{draft.confirm_report_date ? 'Подтверждено' : 'Подтвердить'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button size='small' style={{ marginTop: 4 }} disabled={draft.confirm_report_date === true} onClick={onConfirmDate}>
|
||||
{draft.confirm_report_date ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@ -208,9 +208,7 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
|
||||
confirmDescription = confirmFailures.map((f) => <div key={`${f.column_index}-${f.relation}`}>{failureText(f)}</div>);
|
||||
} else if (confirmLines.length > 0) {
|
||||
confirmDescription = (
|
||||
<Button type='link' style={{ paddingInline: 0 }} onClick={() => focusRow(confirmLines[0].line_id)}>
|
||||
{unresolvedHint(confirmLines.length)}
|
||||
</Button>
|
||||
<Typography.Link onClick={() => focusRow(confirmLines[0].line_id)}>{unresolvedHint(confirmLines.length)}</Typography.Link>
|
||||
);
|
||||
} else if (duplicateId) {
|
||||
confirmDescription = (
|
||||
@ -244,6 +242,9 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
|
||||
onClick={confirm}>
|
||||
Подтвердить данные
|
||||
</Button>
|
||||
{unresolved.length > 0 && (
|
||||
<Typography.Link onClick={() => focusRow(unresolved[0].key)}>{unresolvedHint(unresolved.length)}</Typography.Link>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
|
||||
@ -258,12 +259,6 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
|
||||
<div style={{ height: '100%', paddingLeft: 6 }}>
|
||||
<ReviewCard title='Результат распознавания' extra={cardActions}>
|
||||
<Space orientation='vertical' size={12} style={{ width: '100%' }}>
|
||||
{unresolved.length > 0 && (
|
||||
// настоящая кнопка: достижима Tab, срабатывает по Enter; отдельной строкой, чтобы не ломать заголовок (T11.7)
|
||||
<Button type='link' style={{ paddingInline: 0 }} onClick={() => focusRow(unresolved[0].key)}>
|
||||
{unresolvedHint(unresolved.length)}
|
||||
</Button>
|
||||
)}
|
||||
<StatementHeaderBlock
|
||||
header={statement.header}
|
||||
documentType={statement.document.document_type}
|
||||
|
||||
@ -40,7 +40,6 @@ type Props = {
|
||||
onColumnDateConfirm?: (col: ColumnOut) => void;
|
||||
};
|
||||
|
||||
const ICON_WIDTH = 18; // значок уровня с отступом: у ребёнка вычитается из отступа, чтобы текст не съезжал
|
||||
const CHILD_INDENT = 20; // отступ названия строки детализации под родителем
|
||||
|
||||
function lineWeight(r: LayoutRow): number | undefined {
|
||||
@ -90,7 +89,7 @@ export function StatementTable({
|
||||
const rowIssues = places.byRow.get(r.key) ?? [];
|
||||
const rowWorst = worstSeverity(rowIssues);
|
||||
const text = (
|
||||
<span style={{ fontWeight: lineWeight(r), display: 'inline-block' }}>
|
||||
<span style={{ fontWeight: lineWeight(r), paddingLeft: r.child ? CHILD_INDENT : undefined, display: 'inline-block' }}>
|
||||
{r.displayName || '—'}
|
||||
{r.secondaryName && (
|
||||
<Typography.Text type='secondary' style={{ display: 'block', fontWeight: 400, fontSize: 12 }}>
|
||||
@ -112,11 +111,9 @@ export function StatementTable({
|
||||
) : (
|
||||
namedText
|
||||
);
|
||||
// отступ ребёнка — на общей обёртке: значок уровня остаётся внутри отступа и не сдвигает текст (T11.8)
|
||||
const indent = r.child ? CHILD_INDENT : undefined;
|
||||
if (!rowWorst) return <div style={{ paddingLeft: indent }}>{named}</div>;
|
||||
if (!rowWorst) return named;
|
||||
return (
|
||||
<div style={{ paddingLeft: indent === undefined ? undefined : indent - ICON_WIDTH, display: 'flex', alignItems: 'center' }}>
|
||||
<>
|
||||
<Tooltip
|
||||
title={rowIssues.map((i) => (
|
||||
<div key={i.id}>{i.message}</div>
|
||||
@ -126,16 +123,16 @@ export function StatementTable({
|
||||
</span>
|
||||
</Tooltip>
|
||||
{named}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
title: 'Код',
|
||||
width: editable ? 160 : 84,
|
||||
width: editable ? 160 : 60,
|
||||
align: 'center' as const,
|
||||
onCell: (r) => (r.kind === 'section' ? sectionSpan(r, false) : { style: { background: labelBackground(r), whiteSpace: 'nowrap' } }),
|
||||
onCell: (r) => (r.kind === 'section' ? sectionSpan(r, false) : { style: { background: labelBackground(r) } }),
|
||||
render: (_: unknown, r: LayoutRow) => {
|
||||
if (r.kind !== 'line') return null;
|
||||
const lineId = r.row.lineId;
|
||||
@ -164,7 +161,7 @@ export function StatementTable({
|
||||
onConfirmDate={onColumnDateConfirm}
|
||||
/>
|
||||
),
|
||||
width: 128,
|
||||
width: 112,
|
||||
align: 'right' as const,
|
||||
onCell: (r: LayoutRow) =>
|
||||
r.kind === 'section'
|
||||
@ -201,7 +198,6 @@ export function StatementTable({
|
||||
return (
|
||||
<Table<LayoutRow>
|
||||
size='small'
|
||||
tableLayout='fixed'
|
||||
bordered
|
||||
rowKey='key'
|
||||
columns={tableColumns}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { CheckOutlined, UndoOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Tooltip, Typography } from 'antd';
|
||||
import { Button, Input, Tooltip } from 'antd';
|
||||
import type { IssueOut, ValueOut } from '../../types/api';
|
||||
import { parseAmountInput } from '../../utils/money';
|
||||
import { formatAmount, parseAmountInput } from '../../utils/money';
|
||||
import { SeverityIcon } from './IssuesPanel';
|
||||
import { cellTipLines, valueCellText } from './cellView';
|
||||
import { cellTipLines } from './cellView';
|
||||
import type { CellConfirmState } from './editRules';
|
||||
import { worstSeverity } from './issuePlaces';
|
||||
|
||||
@ -51,19 +51,17 @@ export function ValueCell({
|
||||
}: Props) {
|
||||
const lines = cellTipLines(value, issues);
|
||||
const worst = worstSeverity(issues);
|
||||
const shown = valueCellText(value);
|
||||
// значок уровня — внутри ячейки, слева от суммы, по центру по вертикали (T11.3)
|
||||
const corner = worst && <SeverityIcon severity={worst} style={{ fontSize: 11, flex: 'none' }} />;
|
||||
// значок — в левом углу ячейки; поверх ввода и текста, не двигает суммы
|
||||
const corner = worst && <SeverityIcon severity={worst} style={{ position: 'absolute', top: 1, left: 2, fontSize: 10 }} />;
|
||||
const tip = lines.length > 0 ? lines.map((line, i) => <div key={`${i}-${line}`}>{line}</div>) : undefined;
|
||||
if (!editable) {
|
||||
return (
|
||||
// подсказка на всей ячейке: блок на всю ширину колонки (T11.4)
|
||||
<Tooltip title={tip}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 4, minHeight: 22 }}>
|
||||
<span>
|
||||
{corner}
|
||||
{value.is_confirmed && <CheckOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />}
|
||||
{shown.muted ? <Typography.Text type='secondary'>{shown.text}</Typography.Text> : shown.text}
|
||||
</div>
|
||||
{value.is_confirmed && <CheckOutlined style={{ fontSize: 10, color: '#8c8c8c', marginRight: 4 }} />}
|
||||
{formatAmount(value.amount, { fraction: 'auto', empty: '-' })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@ -72,7 +70,7 @@ export function ValueCell({
|
||||
const pending = confirmState === 'pending-confirm' || confirmState === 'pending-unconfirm';
|
||||
return (
|
||||
<Tooltip title={invalid ? 'Неверный формат суммы' : tip}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, width: '100%' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 2, width: '100%' }}>
|
||||
{corner}
|
||||
<Input
|
||||
size='small'
|
||||
@ -80,7 +78,8 @@ export function ValueCell({
|
||||
prefix={value.is_confirmed ? <CheckOutlined style={{ fontSize: 10, color: '#8c8c8c' }} /> : undefined}
|
||||
value={text}
|
||||
onChange={(e) => onDraftChange(draftKey, e.target.value, value.amount)}
|
||||
style={{ textAlign: 'right', minWidth: 0, flex: 1 }}
|
||||
// значок записи лежит в левом углу поверх поля — освобождаем ему место, чтобы не налезал на галочку и текст
|
||||
style={{ textAlign: 'right', paddingLeft: worst ? 14 : undefined }}
|
||||
/>
|
||||
{confirmState && (
|
||||
<Tooltip title={CONFIRM_TITLE[confirmState]}>
|
||||
@ -94,7 +93,7 @@ export function ValueCell({
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { ValueOut } from '../../types/api';
|
||||
import { formatAmount } from '../../utils/money';
|
||||
import { COLORS, cellTipLines, codeCellView, rowBackground, valueBackground, valueCellText } from './cellView';
|
||||
import { COLORS, cellTipLines, codeCellView, rowBackground, valueBackground } from './cellView';
|
||||
import type { GridRow } from './statementGrid';
|
||||
|
||||
const val = (extra: Partial<ValueOut> = {}): ValueOut => ({
|
||||
@ -139,24 +138,3 @@ describe('фон', () => {
|
||||
expect(rowBackground(row())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('valueCellText (T11.5)', () => {
|
||||
it('сумма есть — обычный текст', () => {
|
||||
expect(valueCellText(val({ amount: '1500.00' }))).toEqual({
|
||||
text: formatAmount('1500.00', { fraction: 'auto', empty: '-' }),
|
||||
muted: false,
|
||||
});
|
||||
});
|
||||
it('суммы нет, есть string_value — он серым', () => {
|
||||
expect(valueCellText(val({ amount: null, string_value: '211(1 |' }))).toEqual({ text: '211(1 |', muted: true });
|
||||
});
|
||||
it('ничего нет — прочерк', () => {
|
||||
expect(valueCellText(val({ amount: null }))).toEqual({ text: '-', muted: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('cellTipLines и string_value (T11.5)', () => {
|
||||
it('без суммы показывает нераспознанный string_value', () => {
|
||||
expect(cellTipLines(val({ amount: null, string_value: 'abc' }))).toContain('Не распознано как сумма: «abc»');
|
||||
});
|
||||
});
|
||||
|
||||
@ -34,22 +34,11 @@ export function cellTipLines(value: ValueOut, issues: IssueOut[] = []): string[]
|
||||
if (value.text !== null && value.text.trim() !== '') lines.push(`Распознано: «${value.text}»`);
|
||||
const confidence = formatPercent(value.confidence);
|
||||
if (confidence !== null) lines.push(`Уверенность: ${confidence}`);
|
||||
if (value.amount === null && value.string_value !== null && value.string_value.trim() !== '') {
|
||||
lines.push(`Не распознано как сумма: «${value.string_value}»`);
|
||||
}
|
||||
if (value.is_confirmed) lines.push('Подтверждено аналитиком');
|
||||
for (const issue of issues) lines.push(issue.message);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** Текст ячейки суммы (T11.5): сумма; без неё нераспознанный string_value серым; иначе прочерк. */
|
||||
export function valueCellText(value: ValueOut): { text: string; muted: boolean } {
|
||||
if (value.amount === null && value.string_value !== null && value.string_value.trim() !== '') {
|
||||
return { text: value.string_value, muted: true };
|
||||
}
|
||||
return { text: formatAmount(value.amount, { fraction: 'auto', empty: '-' }), muted: false };
|
||||
}
|
||||
|
||||
/** Ячейка кода (P6-16): текст, серый ли он, подсказка. */
|
||||
export function codeCellView(row: GridRow): { text: string; muted: boolean; tip: string | undefined } {
|
||||
switch (row.lineStatus) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { IssueOut, StatementOut } from '../../types/api';
|
||||
import statementV2Json from './__fixtures__/statement-v2.json';
|
||||
import { collapseIssues, issueListItems, issuePlaces, worstSeverity } from './issuePlaces';
|
||||
import { issueListItems, issuePlaces, worstSeverity } from './issuePlaces';
|
||||
import { buildStatementGrid, cellKey } from './statementGrid';
|
||||
|
||||
const statement = statementV2Json as unknown as StatementOut;
|
||||
@ -82,14 +82,3 @@ describe('worstSeverity', () => {
|
||||
expect(worstSeverity([])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapseIssues (T11.11)', () => {
|
||||
const items = Array.from({ length: 6 }, (_, i) => ({ id: String(i) })) as never[];
|
||||
it('свёрнуто: первые 3, скрыто 3', () => {
|
||||
expect(collapseIssues(items, false, 3)).toEqual({ shown: items.slice(0, 3), hidden: 3 });
|
||||
});
|
||||
it('развёрнуто или короткий список: всё', () => {
|
||||
expect(collapseIssues(items, true, 3)).toEqual({ shown: items, hidden: 0 });
|
||||
expect(collapseIssues(items.slice(0, 3), false, 3)).toEqual({ shown: items.slice(0, 3), hidden: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@ -118,9 +118,3 @@ export function worstSeverity(issues: IssueOut[]): Severity | null {
|
||||
if (issues.length === 0) return null;
|
||||
return issues.some((i) => i.severity === 'error') ? 'error' : 'info';
|
||||
}
|
||||
|
||||
/** Свёрнутый список предупреждений (T11.11): первые `limit` (порядок уже «ошибки первыми») и сколько скрыто. */
|
||||
export function collapseIssues<T>(items: T[], expanded: boolean, limit: number): { shown: T[]; hidden: number } {
|
||||
if (expanded || items.length <= limit) return { shown: items, hidden: 0 };
|
||||
return { shown: items.slice(0, limit), hidden: items.length - limit };
|
||||
}
|
||||
|
||||
@ -173,10 +173,6 @@ describe('parent_code и пустые строки детализации', () =
|
||||
];
|
||||
expect([...emptySublineKeys(rows, {})]).toEqual(['11101', '11102']);
|
||||
});
|
||||
it('расшифровка (lineStatus detail) с parent_code не считается пустой подстрокой (T11.10)', () => {
|
||||
const detail = { ...gridRow('d1', '1110', { c1: cell({}) }), lineStatus: 'detail' as const };
|
||||
expect([...emptySublineKeys([detail], {})]).toEqual([]);
|
||||
});
|
||||
it('подстрока с правкой в черновике не считается пустой', () => {
|
||||
const rows = [gridRow('11101', '1110', { c1: cell({}) }), gridRow('11102', '1110', { c1: cell({}) })];
|
||||
expect([...emptySublineKeys(rows, { '11101:c1': '7' })]).toEqual(['11102']);
|
||||
|
||||
@ -88,7 +88,6 @@ export function emptySublineKeys(
|
||||
.filter(
|
||||
(row) =>
|
||||
row.parentCode !== null &&
|
||||
row.lineStatus !== 'detail' &&
|
||||
!edited.has(row.key) &&
|
||||
!(row.lineId !== null && row.lineId in lineActions) &&
|
||||
!Object.values(row.cells).some(hasValue),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user