Files
family_budget/frontend/src/pages/AnalyticsPage.tsx
2026-03-02 00:33:09 +03:00

145 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
import type {
Account,
AnalyticsSummaryResponse,
ByCategoryItem,
TimeseriesItem,
Granularity,
} from '@family-budget/shared';
import { getAccounts } from '../api/accounts';
import { getSummary, getByCategory, getTimeseries } from '../api/analytics';
import {
PeriodSelector,
type PeriodState,
} from '../components/PeriodSelector';
import { SummaryCards } from '../components/SummaryCards';
import { TimeseriesChart } from '../components/TimeseriesChart';
import { CategoryChart } from '../components/CategoryChart';
import { toISODate } from '../utils/format';
function getDefaultPeriod(): PeriodState {
const now = new Date();
return {
mode: 'month',
from: toISODate(new Date(now.getFullYear(), now.getMonth(), 1)),
to: toISODate(now),
};
}
export function AnalyticsPage() {
const [period, setPeriod] = useState<PeriodState>(getDefaultPeriod);
const [accountId, setAccountId] = useState<string>('');
const [onlyConfirmed, setOnlyConfirmed] = useState(false);
const [accounts, setAccounts] = useState<Account[]>([]);
const [summary, setSummary] = useState<AnalyticsSummaryResponse | null>(
null,
);
const [byCategory, setByCategory] = useState<ByCategoryItem[]>([]);
const [timeseries, setTimeseries] = useState<TimeseriesItem[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
getAccounts().then(setAccounts).catch(() => {});
}, []);
const fetchAll = useCallback(async () => {
if (!period.from || !period.to) return;
setLoading(true);
let gran: Granularity;
switch (period.mode) {
case 'week':
gran = 'day';
break;
case 'month':
gran = 'week';
break;
default:
gran = 'month';
break;
}
const base = {
from: period.from,
to: period.to,
...(accountId ? { accountId: Number(accountId) } : {}),
...(onlyConfirmed ? { onlyConfirmed: true } : {}),
};
try {
const [s, bc, ts] = await Promise.all([
getSummary(base),
getByCategory(base),
getTimeseries({ ...base, granularity: gran }),
]);
setSummary(s);
setByCategory(bc);
setTimeseries(ts);
} catch {
// 401 handled by interceptor
} finally {
setLoading(false);
}
}, [period, accountId, onlyConfirmed]);
useEffect(() => {
fetchAll();
}, [fetchAll]);
return (
<div className="page">
<div className="page-header">
<h1>Аналитика</h1>
</div>
<div className="analytics-controls">
<PeriodSelector period={period} onChange={setPeriod} />
<div className="analytics-filters">
<div className="filter-group">
<label>Счёт</label>
<select
value={accountId}
onChange={(e) => setAccountId(e.target.value)}
>
<option value="">Все счета</option>
{accounts.map((a) => (
<option key={a.id} value={a.id}>
{a.alias || a.accountNumberMasked}
</option>
))}
</select>
</div>
<div className="filter-group filter-group-checkbox">
<label>
<input
type="checkbox"
checked={onlyConfirmed}
onChange={(e) => setOnlyConfirmed(e.target.checked)}
/>
Только подтверждённые
</label>
</div>
</div>
</div>
{loading ? (
<div className="section-loading">Загрузка данных...</div>
) : (
<>
{summary && <SummaryCards summary={summary} />}
<div className="analytics-charts">
<div className="chart-card">
<h3>Динамика</h3>
<TimeseriesChart data={timeseries} />
</div>
<div className="chart-card">
<h3>По категориям</h3>
<CategoryChart data={byCategory} />
</div>
</div>
</>
)}
</div>
);
}