feat: creats frontend for the project

This commit is contained in:
vakabunga
2026-03-02 00:33:09 +03:00
parent 4d67636633
commit cd56e2bf9d
37 changed files with 3762 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
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>
);
}