fix(web): заполнить график расходов на дашборде
Docker / build (push) Failing after 19s

Учитывать списания из журнала баланса и не-пополнения; при отсутствии истории показывать оценку по активным VPS. Год по умолчанию — текущий.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
DenozordecandCursor committed 2026-06-30 21:25:14 +07:00
1 parent 77099b9ab0
commit a11fbc6efd
4 files changed
+354 -34

No files matched your search

+100 -11
View File
@@ -30,11 +30,13 @@ import { useMemo, useState } from 'react'
import { SelectField } from '@/components/select-field'
import {
aggregatePaymentsByMonthYear,
aggregateDashboardExpensesByMonthYear,
availablePaymentYears,
availableExpenseYears,
type PaymentChartFilter,
} from '@/lib/chart-analytics'
import type { Vps, Provider, Payment, Settings, RatesData, ServerProject } from '@/types/entities'
import type { Vps, Provider, Payment, Settings, RatesData, ServerProject, BalanceLedgerRow } from '@/types/entities'
import {
canonicalPaymentType,
convertCurrency,
@@ -216,9 +218,10 @@ function DashboardMonthlyBarChart({
}) {
const baseCurrency = (settings[0]?.baseCurrency ?? 'RUB').toUpperCase()
const years = useMemo(() => availablePaymentYears(payments), [payments])
const [year, setYear] = useState(() => years[0] ?? new Date().getFullYear())
const currentYear = new Date().getFullYear()
const [year, setYear] = useState(currentYear)
const effectiveYear = years.includes(year) ? year : (years[0] ?? year)
const effectiveYear = years.includes(year) ? year : currentYear
const data = useMemo(
() => aggregatePaymentsByMonthYear(payments, effectiveYear, settings, ratesData, paymentFilter),
@@ -302,20 +305,106 @@ export function DashboardPaymentsChart(props: {
)
}
export function DashboardExpensesChart(props: {
export function DashboardExpensesChart({
payments,
balanceLedger,
vps,
providers,
settings,
ratesData,
className,
}: {
payments: Payment[]
balanceLedger: BalanceLedgerRow[]
vps: Vps[]
providers: Provider[]
settings: Settings[]
ratesData: RatesData | null
className?: string
}) {
const baseCurrency = (settings[0]?.baseCurrency ?? 'RUB').toUpperCase()
const years = useMemo(
() => availableExpenseYears(payments, balanceLedger),
[payments, balanceLedger],
)
const currentYear = new Date().getFullYear()
const [year, setYear] = useState(currentYear)
const effectiveYear = years.includes(year) ? year : currentYear
const { rows: data, mode } = useMemo(
() =>
aggregateDashboardExpensesByMonthYear(
payments,
balanceLedger,
vps,
providers,
effectiveYear,
settings,
ratesData,
),
[payments, balanceLedger, vps, providers, effectiveYear, settings, ratesData],
)
const chartConfig: ChartConfig = useMemo(
() => ({
amount: { label: 'Расходы', color: 'var(--chart-1)' },
}),
[],
)
const hasData = data.some((row) => row.amount > 0)
const description =
mode === 'estimate' ? 'Оценка по активным VPS за текущий год' : 'Последние 12 мес'
return (
<DashboardMonthlyBarChart
{...props}
title="Расходы"
chartColor="var(--chart-1)"
paymentFilter="expense"
ariaLabel="График расходов по месяцам"
/>
<Card className={className}>
<CardHeader>
<CardTitle>Расходы</CardTitle>
<CardDescription>{description}</CardDescription>
<CardAction>
<div className="flex flex-wrap items-center gap-2">
<SelectField
size="sm"
triggerClassName="w-[130px]"
aria-label="Группировка"
value="month"
options={[{ value: 'month', label: 'По месяцам' }]}
/>
<SelectField
size="sm"
triggerClassName="w-[100px]"
aria-label="Год"
value={String(effectiveYear)}
onValueChange={(v) => {
if (v) setYear(Number(v))
}}
options={years.map((y) => ({ value: String(y), label: String(y) }))}
/>
</div>
</CardAction>
</CardHeader>
<CardContent>
{!hasData ? (
<ChartEmpty message="Нет данных за выбранный период" />
) : (
<ChartContainer config={chartConfig} className="h-72 w-full" aria-label="График расходов по месяцам">
<BarChart data={data} margin={{ top: 8, right: 8, bottom: 8, left: 8 }}>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis tickLine={false} axisLine={false} width={48} />
<RechartsTooltip
cursor={false}
content={
<ChartTooltipContent formatter={(v) => formatCurrency(Number(v), baseCurrency)} />
}
/>
<Bar dataKey="amount" fill="var(--color-amount)" radius={4} />
</BarChart>
</ChartContainer>
)}
</CardContent>
</Card>
)
}