diff --git a/apps/web/src/components/domain/charts.tsx b/apps/web/src/components/domain/charts.tsx index 232f94f..e8650dc 100644 --- a/apps/web/src/components/domain/charts.tsx +++ b/apps/web/src/components/domain/charts.tsx @@ -12,13 +12,24 @@ import { import { ChartContainer, ChartTooltipContent, + ChartLegend, + ChartLegendContent, type ChartConfig, } from '@cfdm/ui/components/chart' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cfdm/ui/components/card' import type { ReactNode } from 'react' +import { useMemo } from 'react' import type { Vps, Provider, Payment, Settings, RatesData } from '@/types/entities' -import { convertCurrency, convertVpsMonthlyBurnToBase, formatCurrency, monthKey, toIsoCurrency } from '@/lib/format' +import { + canonicalPaymentType, + convertCurrency, + convertVpsMonthlyBurnToBase, + formatCurrency, + monthKey, + paymentTypeLabel, + toIsoCurrency, +} from '@/lib/format' import { providerByIdMap } from '@/lib/billmanager' import { EmptyState } from '@/components/empty-state' @@ -92,10 +103,6 @@ export function MonthlyExpenseChart({ const PIE_COLORS = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'] -const PAYMENTS_CONFIG: ChartConfig = { - amount: { label: 'Платежи', color: 'var(--chart-2)' }, -} - export function PaymentsPieChart({ payments, settings, @@ -108,12 +115,30 @@ export function PaymentsPieChart({ className?: string }) { const baseCurrency = (settings[0]?.baseCurrency ?? 'RUB').toUpperCase() - const byType = new Map() - for (const p of payments) { - const converted = convertCurrency(Number(p.amount), toIsoCurrency(p.currency), baseCurrency, ratesData) - byType.set(p.type, (byType.get(p.type) ?? 0) + converted) - } - const data = Array.from(byType.entries()).map(([type, amount]) => ({ type, amount: Math.round(amount) })) + + const { data, chartConfig } = useMemo(() => { + const byType = new Map() + for (const p of payments) { + const type = canonicalPaymentType(p.type) + const converted = convertCurrency(Number(p.amount), toIsoCurrency(p.currency), baseCurrency, ratesData) + byType.set(type, (byType.get(type) ?? 0) + converted) + } + const entries = Array.from(byType.entries()) + .map(([type, amount]) => ({ type, amount: Math.round(amount) })) + .filter((row) => row.amount > 0) + .sort((a, b) => b.amount - a.amount) + + const config: ChartConfig = { + amount: { label: 'Платежи', color: 'var(--chart-2)' }, + } + entries.forEach((row, i) => { + config[row.type] = { + label: paymentTypeLabel(row.type), + color: PIE_COLORS[i % PIE_COLORS.length], + } + }) + return { data: entries, chartConfig: config } + }, [payments, baseCurrency, ratesData]) return ( @@ -125,12 +150,20 @@ export function PaymentsPieChart({ {data.length === 0 ? ( ) : ( - + - formatCurrency(Number(v), baseCurrency)} />} /> + formatCurrency(Number(v), baseCurrency)} + /> + } + /> + } /> - {data.map((_, i) => ( - + {data.map((row, i) => ( + ))} diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index 4650dd3..d050b0d 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -72,12 +72,23 @@ export function getCountryFlagEmojiByCode(code?: string): string { const PAYMENT_TYPE_LABELS: Record = { direct_vps_payment: 'Прямой платеж за VPS', provider_balance_topup: 'Пополнение баланса хостера', + topup: 'Пополнение баланса хостера', daily_debit: 'Ежедневное списание', monthly_debit: 'Ежемесячное списание', } +/** Синонимы типов из разных API → единый ключ для отчётов и агрегации. */ +const PAYMENT_TYPE_CANONICAL: Record = { + topup: 'provider_balance_topup', +} + +export function canonicalPaymentType(type: string): string { + const key = String(type ?? '').trim() + return PAYMENT_TYPE_CANONICAL[key] ?? key +} + export function paymentTypeLabel(type: string): string { - return PAYMENT_TYPE_LABELS[type] ?? type + return PAYMENT_TYPE_LABELS[type] ?? PAYMENT_TYPE_LABELS[canonicalPaymentType(type)] ?? type } const VPS_STATUS_LABELS: Record = {