feat: creats frontend for the project
This commit is contained in:
144
frontend/src/pages/AnalyticsPage.tsx
Normal file
144
frontend/src/pages/AnalyticsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user