a709efdee4
Владелец 06.08: "кошелёк убогий и неинформативный". Отметил пять слабых мест из шести. Не отметил только прогноз "надолго ли хватит" - он пополняет по факту, а не по остатку. Что теперь на экране: - слева липкая колонка денег: свободно крупно, и под ней РАСШИФРОВКА заморозки - какая кампания, сколько заперто, когда вернётся. Раньше стояло голое "Заморожено 2 507 руб" без единого слова, под что; - период: сегодня / вчера / 7 дней / 30 дней / свои даты, живёт в адресе страницы; - столбики расхода по дням, цвет по каналу, своим CSS без библиотеки; - таблица "Куда ушли" - строка на кампанию с суммой и долей, клик отбирает ленту; - закладки по каналам сохранены, но теперь считаются за выбранный период; - лента разбита по дням с итогом за день и постраничной догрузкой. Сервер: две новые сборки RaskryitieZamorozki и OtchyotKoshelka, новая ручка /api/advertising/wallet/report, лента получила отбор по датам, каналу и кампании и листание по ключу вместо смещения. Расчёт списаний, заморозки и возврата по сроку НЕ тронут - правка только про показ. Найдено по дороге и закрыто: - чтение отчёта обёрнуто контекстом построчной защиты. Без этого на боевом запрос вернул бы НОЛЬ строк молча: местная база под суперпользователем защиту обходит, и мы бы увидели это только у клиента; - сторож на число запросов мог зеленеть БЕЗ самой ручки - на любой неизвестный путь портал отвечает страницей с кодом 200. Усилен и проверен вырезанием маршрута; - общий форматтер срезал хвостовой ноль: "833,50" превращалось в "833,5". На это независимо наступили три правки подряд, каждая завела свою копию. Сведено в formatExact. Границы суток и группировка по дням считаются ПО МОСКВЕ, а не по Гринвичу. Сторож взят с живого боевого: запись 69, 2026-08-05 21:00:24 UTC - для человека это 6 августа. Наивные версии всех трёх мест были написаны нарочно и увидены красными, прежде чем чинились. Старые сторожа не выброшены, а перенесены: закладки по каналам переехали в отчёт вместе с проверкой на 120 строк, отбор ленты по каналу - в проверку ленты. Каждый переезд назван поимённо. Проверено: 461 сторож сервера, 2135 сторожей экрана, контроль типов ноль, статанализ ноль, форматтер чисто, сборка фронта проходит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
60 lines
2.8 KiB
TypeScript
60 lines
2.8 KiB
TypeScript
/**
|
||
* Форматтеры биллинга — BillingView + TransactionsTable + InvoicesTable.
|
||
*
|
||
* Sprint 2 Plan C: status/format-функции (statusChipColor/statusLabel/
|
||
* formatLabel/formatIcon) удалены — real-API транзакции не имеют статуса
|
||
* (append-only ledger), счета — отдельный формат. txAmountClass
|
||
* перетипизирован под знак суммы.
|
||
*/
|
||
|
||
/** «5000» → «5 000 ₽» (без знака). */
|
||
export function formatPlain(cost: number): string {
|
||
return new Intl.NumberFormat('ru-RU').format(cost) + ' ₽';
|
||
}
|
||
|
||
/**
|
||
* «833.5» → «833,50 ₽» — ВСЕГДА две цифры после запятой.
|
||
*
|
||
* 🪤 `formatPlain` число знаков не фиксирует и срезает хвостовой ноль: «833.50» у него
|
||
* выходит «833,5 ₽», а «124.00» — «124 ₽». В крупных заголовочных суммах это незаметно,
|
||
* а в построчных деньгах читается как ошибка в копейках. 06.08.2026 на это независимо
|
||
* наступили три правки кошелька подряд, каждая завела свою копию форматтера — свели сюда.
|
||
*
|
||
* Заголовочные суммы (баланс/заперто/свободно) по-прежнему через `formatPlain`.
|
||
*/
|
||
export function formatExact(cost: number): string {
|
||
return new Intl.NumberFormat('ru-RU', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
}).format(cost) + ' ₽';
|
||
}
|
||
|
||
/** Знаковый формат: «+ 5 000 ₽» / «− 6 600 ₽» / «0 ₽». */
|
||
export function formatCost(cost: number): string {
|
||
const sign = cost > 0 ? '+ ' : cost < 0 ? '− ' : '';
|
||
return sign + new Intl.NumberFormat('ru-RU').format(Math.abs(cost)) + ' ₽';
|
||
}
|
||
|
||
/** CSS-класс суммы транзакции по знаку. */
|
||
export function txAmountClass(amount: number): string {
|
||
if (amount > 0) return 'tx-amount-up';
|
||
if (amount < 0) return 'tx-amount-down';
|
||
return 'tx-amount-neutral';
|
||
}
|
||
|
||
/** Человекочитаемые лейблы для feature-слагов tariff_plans.features. */
|
||
export const FEATURE_LABELS: Record<string, string> = {
|
||
webhook: 'Webhook',
|
||
kanban: 'Канбан',
|
||
basic_analytics: 'Базовая аналитика',
|
||
advanced_analytics: 'Расширенная аналитика',
|
||
api: 'API',
|
||
'2fa': 'Двухфакторная аутентификация',
|
||
custom_domain: 'Свой домен',
|
||
};
|
||
|
||
/** Лейбл feature-слага; неизвестный слаг возвращается как есть. */
|
||
export function featureLabel(slug: string): string {
|
||
return FEATURE_LABELS[slug] ?? slug;
|
||
}
|