Оценка burn только за текущий месяц; KPI совпадает с convertVpsMonthlyBurnToBase. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 (
|
||||
<Card className={className}>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: <TrendingUpIcon className="size-4" />,
|
||||
iconClassName: 'text-info',
|
||||
to: '/reports',
|
||||
|
||||
Reference in New Issue
Block a user