- Мобильная навигация: hamburger-меню и drawer вместо фиксированного sidebar - Модальные окна на весь экран при ширине < 480px - Адаптивные заголовки страниц и фильтры (touch-friendly) - Card view для таблицы операций при ширине < 600px - Горизонтальный скролл вкладок настроек - Увеличенные touch-targets (44px) для пагинации и кнопок - Уменьшенная высота графиков на мобильных - Поддержка safe-area-inset для устройств с вырезами - theme-color в index.html Made-with: Cursor
106 lines
2.8 KiB
TypeScript
106 lines
2.8 KiB
TypeScript
import {
|
||
PieChart,
|
||
Pie,
|
||
Cell,
|
||
Tooltip,
|
||
ResponsiveContainer,
|
||
} from 'recharts';
|
||
import type { ByCategoryItem } from '@family-budget/shared';
|
||
import { formatAmount } from '../utils/format';
|
||
import { useMediaQuery } from '../hooks/useMediaQuery';
|
||
|
||
interface Props {
|
||
data: ByCategoryItem[];
|
||
}
|
||
|
||
const COLORS = [
|
||
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
|
||
'#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1',
|
||
'#14b8a6', '#e11d48', '#0ea5e9', '#a855f7', '#22c55e',
|
||
];
|
||
|
||
const rubFormatter = new Intl.NumberFormat('ru-RU', {
|
||
style: 'currency',
|
||
currency: 'RUB',
|
||
maximumFractionDigits: 0,
|
||
});
|
||
|
||
export function CategoryChart({ data }: Props) {
|
||
const isMobile = useMediaQuery('(max-width: 600px)');
|
||
const chartHeight = isMobile ? 250 : 300;
|
||
|
||
if (data.length === 0) {
|
||
return <div className="chart-empty">Нет данных за период</div>;
|
||
}
|
||
|
||
const chartData = data.map((item) => ({
|
||
name: item.categoryName,
|
||
value: Math.abs(item.amount) / 100,
|
||
txCount: item.txCount,
|
||
share: item.share,
|
||
}));
|
||
|
||
return (
|
||
<div className="category-chart-wrapper">
|
||
<ResponsiveContainer width="100%" height={chartHeight}>
|
||
<PieChart>
|
||
<Pie
|
||
data={chartData}
|
||
cx="50%"
|
||
cy="50%"
|
||
outerRadius={100}
|
||
dataKey="value"
|
||
nameKey="name"
|
||
label={({ name, share }: { name: string; share: number }) =>
|
||
`${name} ${(share * 100).toFixed(0)}%`
|
||
}
|
||
labelLine
|
||
>
|
||
{chartData.map((_, idx) => (
|
||
<Cell
|
||
key={idx}
|
||
fill={COLORS[idx % COLORS.length]}
|
||
/>
|
||
))}
|
||
</Pie>
|
||
<Tooltip
|
||
formatter={(value: number) => rubFormatter.format(value)}
|
||
/>
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
|
||
<table className="category-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Категория</th>
|
||
<th>Сумма</th>
|
||
<th className="th-center">Операций</th>
|
||
<th className="th-center">Доля</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.map((item, idx) => (
|
||
<tr key={item.categoryId}>
|
||
<td>
|
||
<span
|
||
className="color-dot"
|
||
style={{
|
||
backgroundColor:
|
||
COLORS[idx % COLORS.length],
|
||
}}
|
||
/>
|
||
{item.categoryName}
|
||
</td>
|
||
<td>{formatAmount(item.amount)}</td>
|
||
<td className="td-center">{item.txCount}</td>
|
||
<td className="td-center">
|
||
{(item.share * 100).toFixed(1)}%
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
}
|