Финальное ревью I-1: у неотмеченной строки поле доли скрыто — effectiveRow отправляет сохранённую долю вместо неверного ввода; подсказка у «Сохранить» называет строки с ошибкой и предупреждает о скрытых фильтром. design.md goodwill-ui: R-4…R-6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
4.7 KiB
TypeScript
96 lines
4.7 KiB
TypeScript
/** Чистые функции вкладки «Гудвилл» (DOC-259025 §4–§6): ввод доли, предпросмотр P/G, фильтр знака, изменённые строки. */
|
||
import type { GoodwillEdit, GoodwillRow, GoodwillRowKeyIn, GoodwillSign } from '../types/goodwill/goodwill';
|
||
import { fromCents, toCents } from './money';
|
||
|
||
const SHARE = /^\d{1,3}(?:\.\d{1,2})?$/;
|
||
|
||
export function rowKey(row: Pick<GoodwillRow, 'head_legal_entity' | 'subsidiary_legal_entity'>): string {
|
||
return `${row.head_legal_entity.id}:${row.subsidiary_legal_entity.id}`;
|
||
}
|
||
|
||
/** Доля 0–100, не больше двух знаков, запятая или точка; пусто — null. Лишние знаки — ошибка, не обрезка. */
|
||
export function parseShare(input: string): { ok: true; value: string | null } | { ok: false; error: string } {
|
||
const value = input.trim().replace(',', '.');
|
||
if (value === '') return { ok: true, value: null };
|
||
if (!SHARE.test(value) || shareHundredths(value) > 10000n) {
|
||
return { ok: false, error: 'Число от 0 до 100, не больше двух знаков после запятой' };
|
||
}
|
||
return { ok: true, value };
|
||
}
|
||
|
||
/** Доля строкой («80.5») → сотые доли процента (8050n). */
|
||
function shareHundredths(value: string): bigint {
|
||
const [int, frac = ''] = value.split('.');
|
||
return BigInt(int) * 100n + BigInt(frac.padEnd(2, '0'));
|
||
}
|
||
|
||
/** Предварительные P = round(УК × d / 100, 2) и G = ФВ − P в копейках, половина вверх; как на бэке. */
|
||
export function previewAmounts(
|
||
row: GoodwillRow,
|
||
share: string | null,
|
||
include: boolean,
|
||
): { participation: string | null; goodwill: string | null } {
|
||
const fv = row.financial_investment_amount;
|
||
const capital = row.charter_capital_amount;
|
||
if (!row.has_investments || !include || share === null || fv === null || capital === null) {
|
||
return { participation: null, goodwill: null };
|
||
}
|
||
const parsed = parseShare(share);
|
||
if (!parsed.ok || parsed.value === null) return { participation: null, goodwill: null };
|
||
const product = toCents(capital) * shareHundredths(parsed.value);
|
||
const divisor = 10000n;
|
||
const negative = product < 0n;
|
||
const abs = negative ? -product : product;
|
||
const rounded = (abs + divisor / 2n) / divisor;
|
||
const p = negative ? -rounded : rounded;
|
||
return { participation: fromCents(p), goodwill: fromCents(toCents(fv) - p) };
|
||
}
|
||
|
||
/** Фильтр «Гудвилл»: нерассчитанный (null) — только во «Все значения». */
|
||
export function matchesSign(goodwill: string | null, sign: GoodwillSign): boolean {
|
||
if (sign === 'all') return true;
|
||
if (goodwill === null) return false;
|
||
const cents = toCents(goodwill);
|
||
if (sign === 'positive') return cents > 0n;
|
||
if (sign === 'negative') return cents < 0n;
|
||
return cents === 0n;
|
||
}
|
||
|
||
/** Значения строки с учётом правки: что уйдёт в пакет. */
|
||
export function effectiveRow(row: GoodwillRow, edit: GoodwillEdit | undefined): GoodwillRowKeyIn {
|
||
const include = edit?.include_in_kfo ?? row.include_in_kfo;
|
||
const share = edit && 'ownership_share' in edit ? (edit.ownership_share ?? null) : row.ownership_share;
|
||
const parsed = share === null ? null : parseShare(share);
|
||
const comment = edit && 'comment' in edit ? (edit.comment ?? null) : row.comment;
|
||
let ownership = parsed?.ok ? parsed.value : share;
|
||
// у неотмеченной строки поле доли скрыто: неверный ввод не исправить, поэтому уходит сохранённая доля
|
||
if (!include && parsed && !parsed.ok) ownership = row.ownership_share;
|
||
return {
|
||
head_legal_entity_id: row.head_legal_entity.id,
|
||
subsidiary_legal_entity_id: row.subsidiary_legal_entity.id,
|
||
ownership_share: ownership,
|
||
include_in_kfo: include,
|
||
comment: comment?.trim() ? comment : null,
|
||
};
|
||
}
|
||
|
||
function sameShare(a: string | null, b: string | null): boolean {
|
||
if (a === null || b === null) return a === b;
|
||
if (!SHARE.test(a) || !SHARE.test(b)) return a === b;
|
||
return shareHundredths(a) === shareHundredths(b);
|
||
}
|
||
|
||
/** Строки пакета — только те, где правка отличается от сохранённого. */
|
||
export function changedRows(rows: GoodwillRow[], edits: Record<string, GoodwillEdit>): GoodwillRowKeyIn[] {
|
||
return rows.flatMap((row) => {
|
||
const edit = edits[rowKey(row)];
|
||
if (!edit) return [];
|
||
const next = effectiveRow(row, edit);
|
||
const same =
|
||
next.include_in_kfo === row.include_in_kfo &&
|
||
sameShare(next.ownership_share, row.ownership_share) &&
|
||
(next.comment?.trim() ?? null) === (row.comment?.trim() || null);
|
||
return same ? [] : [next];
|
||
});
|
||
}
|