Оценка burn только за текущий месяц; KPI совпадает с convertVpsMonthlyBurnToBase. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -361,7 +361,9 @@ export function DashboardExpensesChart({
|
|||||||
|
|
||||||
const hasData = data.some((row) => row.amount > 0)
|
const hasData = data.some((row) => row.amount > 0)
|
||||||
const description =
|
const description =
|
||||||
mode === 'estimate' ? 'Оценка по активным VPS за текущий год' : 'Последние 12 мес'
|
mode === 'estimate'
|
||||||
|
? 'Нет списаний в учёте — оценка тарифов за текущий месяц'
|
||||||
|
: 'Списания и платежи за VPS по месяцам'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ describe('chart-analytics', () => {
|
|||||||
expect(expenseOnly[2]?.amount).toBe(0)
|
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[] = [
|
const payments: Payment[] = [
|
||||||
{
|
{
|
||||||
id: 'p1',
|
id: 'p1',
|
||||||
@@ -71,12 +71,46 @@ describe('chart-analytics', () => {
|
|||||||
[],
|
[],
|
||||||
[activeVps],
|
[activeVps],
|
||||||
[],
|
[],
|
||||||
2026,
|
new Date().getFullYear(),
|
||||||
settings,
|
settings,
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(mode).toBe('estimate')
|
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,
|
ratesData: RatesData | null,
|
||||||
): { month: string; amount: number }[] {
|
): { month: string; amount: number }[] {
|
||||||
const providerById = providerByIdMap(providers)
|
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
|
const monthlyBurn = vps
|
||||||
.filter((item) => item.status === 'active')
|
.filter((item) => item.status === 'active')
|
||||||
.reduce(
|
.reduce(
|
||||||
@@ -177,13 +185,8 @@ export function aggregateEstimatedVpsBurnByMonthYear(
|
|||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
const now = new Date()
|
byMonth[now.getMonth()] = monthlyBurn
|
||||||
const rounded = Math.round(monthlyBurn)
|
return bucketsToRows(byMonth)
|
||||||
|
|
||||||
return Array.from({ length: 12 }, (_, index) => ({
|
|
||||||
month: formatMonthShortRu(index),
|
|
||||||
amount: year === now.getFullYear() && index <= now.getMonth() ? rounded : 0,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aggregateDashboardExpensesByMonthYear(
|
export function aggregateDashboardExpensesByMonthYear(
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ import { cn } from '@cfdm/ui/lib/utils'
|
|||||||
|
|
||||||
import { computeInventoryHealth } from '@/lib/inventory-health'
|
import { computeInventoryHealth } from '@/lib/inventory-health'
|
||||||
import { buildAtRiskAccounts, type AtRiskAccount } from '@/lib/account-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 { exportActiveVpsCsv } from '@/lib/export-csv'
|
||||||
import {
|
import {
|
||||||
ChartsGrid,
|
ChartsGrid,
|
||||||
@@ -246,6 +247,18 @@ function DashboardPage() {
|
|||||||
const totalCount = stats?.totalVpsCount ?? snap.vps.length
|
const totalCount = stats?.totalVpsCount ?? snap.vps.length
|
||||||
const expiringCount = stats?.expiringWithin7Days ?? 0
|
const expiringCount = stats?.expiringWithin7Days ?? 0
|
||||||
const issuesCount = issues.length
|
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 = () => {
|
const handleGoToIssues = () => {
|
||||||
setActiveTab('issues')
|
setActiveTab('issues')
|
||||||
@@ -272,12 +285,7 @@ function DashboardPage() {
|
|||||||
{
|
{
|
||||||
id: 'burn',
|
id: 'burn',
|
||||||
label: 'Расход в месяц',
|
label: 'Расход в месяц',
|
||||||
value: formatInBaseCurrency(
|
value: formatCurrency(monthlyBurnBase, baseCur),
|
||||||
stats?.monthlyBurnEstimate ?? 0,
|
|
||||||
baseCur,
|
|
||||||
snap.settings,
|
|
||||||
ratesData,
|
|
||||||
),
|
|
||||||
icon: <TrendingUpIcon className="size-4" />,
|
icon: <TrendingUpIcon className="size-4" />,
|
||||||
iconClassName: 'text-info',
|
iconClassName: 'text-info',
|
||||||
to: '/reports',
|
to: '/reports',
|
||||||
|
|||||||
Reference in New Issue
Block a user