diff --git a/apps/web/src/components/domain/charts.tsx b/apps/web/src/components/domain/charts.tsx index 212641c..9f018ed 100644 --- a/apps/web/src/components/domain/charts.tsx +++ b/apps/web/src/components/domain/charts.tsx @@ -361,7 +361,9 @@ export function DashboardExpensesChart({ const hasData = data.some((row) => row.amount > 0) const description = - mode === 'estimate' ? 'Оценка по активным VPS за текущий год' : 'Последние 12 мес' + mode === 'estimate' + ? 'Нет списаний в учёте — оценка тарифов за текущий месяц' + : 'Списания и платежи за VPS по месяцам' return ( diff --git a/apps/web/src/lib/chart-analytics.test.ts b/apps/web/src/lib/chart-analytics.test.ts index d11b163..dce73d8 100644 --- a/apps/web/src/lib/chart-analytics.test.ts +++ b/apps/web/src/lib/chart-analytics.test.ts @@ -52,7 +52,7 @@ describe('chart-analytics', () => { expect(expenseOnly[2]?.amount).toBe(0) }) - it('falls back to VPS burn estimate when no expense records exist', () => { + it('falls back to VPS burn estimate for current month only when no expense records', () => { const payments: Payment[] = [ { id: 'p1', @@ -71,12 +71,46 @@ describe('chart-analytics', () => { [], [activeVps], [], - 2026, + new Date().getFullYear(), settings, null, ) expect(mode).toBe('estimate') - expect(rows[2]?.amount).toBe(1500) + const currentMonth = new Date().getMonth() + expect(rows[currentMonth]?.amount).toBe(1500) + for (let i = 0; i < 12; i++) { + if (i === currentMonth) continue + expect(rows[i]?.amount).toBe(0) + } + }) + + it('uses actual expense payments when present', () => { + const payments: Payment[] = [ + { + id: 'p1', + type: 'direct_vps_payment', + date: '2026-02-10', + amount: 2000, + currency: 'RUB', + providerAccountId: 'a1', + vpsId: 'v1', + note: '', + }, + ] + + const { rows, mode } = aggregateDashboardExpensesByMonthYear( + payments, + [], + [activeVps], + [], + 2026, + settings, + null, + ) + + expect(mode).toBe('actual') + expect(rows[1]?.amount).toBe(2000) + expect(rows[2]?.amount).toBe(0) }) }) diff --git a/apps/web/src/lib/chart-analytics.ts b/apps/web/src/lib/chart-analytics.ts index ec318e3..ef6052b 100644 --- a/apps/web/src/lib/chart-analytics.ts +++ b/apps/web/src/lib/chart-analytics.ts @@ -168,6 +168,14 @@ export function aggregateEstimatedVpsBurnByMonthYear( ratesData: RatesData | null, ): { month: string; amount: number }[] { const providerById = providerByIdMap(providers) + const now = new Date() + const byMonth = Array.from({ length: 12 }, () => 0) + + // Без факта списаний не выдумываем историю: оценка = текущий burn только в текущем месяце. + if (year !== now.getFullYear()) { + return bucketsToRows(byMonth) + } + const monthlyBurn = vps .filter((item) => item.status === 'active') .reduce( @@ -177,13 +185,8 @@ export function aggregateEstimatedVpsBurnByMonthYear( 0, ) - const now = new Date() - const rounded = Math.round(monthlyBurn) - - return Array.from({ length: 12 }, (_, index) => ({ - month: formatMonthShortRu(index), - amount: year === now.getFullYear() && index <= now.getMonth() ? rounded : 0, - })) + byMonth[now.getMonth()] = monthlyBurn + return bucketsToRows(byMonth) } export function aggregateDashboardExpensesByMonthYear( diff --git a/apps/web/src/routes/_auth/dashboard.tsx b/apps/web/src/routes/_auth/dashboard.tsx index cd3e07c..d89e7b6 100644 --- a/apps/web/src/routes/_auth/dashboard.tsx +++ b/apps/web/src/routes/_auth/dashboard.tsx @@ -35,7 +35,8 @@ import { cn } from '@cfdm/ui/lib/utils' import { computeInventoryHealth } from '@/lib/inventory-health' import { buildAtRiskAccounts, type AtRiskAccount } from '@/lib/account-health' -import { formatInBaseCurrency, normalizeRatesPayload, vpsStatusLabel } from '@/lib/format' +import { formatInBaseCurrency, normalizeRatesPayload, vpsStatusLabel, convertVpsMonthlyBurnToBase, formatCurrency } from '@/lib/format' +import { providerByIdMap } from '@/lib/billmanager' import { exportActiveVpsCsv } from '@/lib/export-csv' import { ChartsGrid, @@ -246,6 +247,18 @@ function DashboardPage() { const totalCount = stats?.totalVpsCount ?? snap.vps.length const expiringCount = stats?.expiringWithin7Days ?? 0 const issuesCount = issues.length + const providerById = providerByIdMap(snap.providers) + const monthlyBurnBase = activeVps.reduce( + (sum, item) => + sum + + convertVpsMonthlyBurnToBase( + item, + providerById.get(item.providerId), + snap.settings, + ratesData, + ), + 0, + ) const handleGoToIssues = () => { setActiveTab('issues') @@ -272,12 +285,7 @@ function DashboardPage() { { id: 'burn', label: 'Расход в месяц', - value: formatInBaseCurrency( - stats?.monthlyBurnEstimate ?? 0, - baseCur, - snap.settings, - ratesData, - ), + value: formatCurrency(monthlyBurnBase, baseCur), icon: , iconClassName: 'text-info', to: '/reports',