diff --git a/src/components/finance/FinanceAnalytics.jsx b/src/components/finance/FinanceAnalytics.jsx
index 1dc597d..3383c5f 100644
--- a/src/components/finance/FinanceAnalytics.jsx
+++ b/src/components/finance/FinanceAnalytics.jsx
@@ -1,32 +1,83 @@
import { useState, useEffect } from "react"
import {
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer,
- PieChart, Pie, Cell, LineChart, Line,
+ PieChart, Pie, Cell, LineChart, Line, LabelList,
} from "recharts"
import { financeApi } from "../../api/finance"
-const COLORS = [
- "#0D4F4F", "#F7B538", "#6366f1", "#22c55e", "#ef4444",
- "#8b5cf6", "#0ea5e9", "#f97316", "#ec4899", "#14b8a6",
- "#64748b", "#a855f7", "#78716c",
+const CATEGORY_COLORS = {
+ "Жильё": "#ef4444",
+ "Еда": "#f97316",
+ "Транспорт": "#6366f1",
+ "Одежда": "#8b5cf6",
+ "Здоровье": "#22c55e",
+ "Развлечения": "#ec4899",
+ "Связь / Подписки": "#0ea5e9",
+ "Путешествия": "#14b8a6",
+ "Подарки": "#a855f7",
+ "Бытовое": "#64748b",
+ "Маркетплейсы": "#F7B538",
+ "Накопления": "#0D4F4F",
+ "Другое": "#78716c",
+ "Красота": "#f472b6",
+}
+
+const COLORS_FALLBACK = [
+ "#6366f1", "#22c55e", "#ef4444", "#f97316", "#8b5cf6",
+ "#0ea5e9", "#ec4899", "#14b8a6", "#64748b", "#a855f7",
]
+const getColor = (name, i) => {
+ const clean = name.replace(/^[\p{Emoji}\s]+/u, "").trim()
+ return CATEGORY_COLORS[clean] || COLORS_FALLBACK[i % COLORS_FALLBACK.length]
+}
+
const fmt = (n) => Number(n).toLocaleString("ru-RU") + " ₽"
+const fmtShort = (n) => {
+ if (n >= 1000) return Math.round(n / 1000).toLocaleString("ru-RU") + "к"
+ return n.toLocaleString("ru-RU")
+}
const MONTH_NAMES = [
"", "Янв", "Фев", "Мар", "Апр", "Май", "Июн",
"Июл", "Авг", "Сен", "Окт", "Ноя", "Дек",
]
+const SAVINGS_NAMES = ["накопления"]
+
+const CustomTooltip = ({ active, payload, label }) => {
+ if (!active || !payload?.length) return null
+ return (
+
+ {payload.map((p, i) => (
+
+ {p.name || label}:
+ {fmt(p.value)}
+
+ ))}
+
+ )
+}
+
+const BarLabel = ({ x, y, width, height, value }) => {
+ if (!value) return null
+ return (
+
+ {fmtShort(value)}
+
+ )
+}
+
export default function FinanceAnalytics({ month, year }) {
const [analytics, setAnalytics] = useState(null)
const [summary, setSummary] = useState(null)
const [loading, setLoading] = useState(true)
+ const [showSavings, setShowSavings] = useState(false)
useEffect(() => {
setLoading(true)
Promise.all([
- financeApi.getAnalytics({ months: 6 }),
+ financeApi.getAnalytics({ months: 6, month, year }),
financeApi.getSummary({ month, year }),
])
.then(([a, s]) => {
@@ -51,15 +102,33 @@ export default function FinanceAnalytics({ month, year }) {
if (!analytics || !summary) return null
- const expenseCategories = (summary.by_category || []).filter(
+ const allExpenseCategories = (summary.by_category || []).filter(
(c) => c.type === "expense"
)
- const barData = expenseCategories.map((c) => ({
- name: c.category_emoji + " " + c.category_name,
- value: c.amount,
- }))
+ const isSavings = (c) =>
+ SAVINGS_NAMES.includes(c.category_name?.toLowerCase?.())
+
+ const expenseCategories = showSavings
+ ? allExpenseCategories
+ : allExpenseCategories.filter((c) => !isSavings(c))
+
+ const hasSavings = allExpenseCategories.some(isSavings)
+
+ const barData = expenseCategories
+ .sort((a, b) => b.amount - a.amount)
+ .map((c, i) => ({
+ name: (c.category_emoji || "") + " " + c.category_name,
+ shortName: c.category_name?.length > 10
+ ? c.category_name.slice(0, 10) + "…"
+ : c.category_name,
+ value: c.amount,
+ fill: getColor(c.category_name, i),
+ }))
+
const pieData = barData
+ const filteredTotal = expenseCategories.reduce((s, c) => s + c.amount, 0)
+
const monthlyData = (analytics.monthly_trend || []).map((m) => ({
month: MONTH_NAMES[parseInt(m.month.slice(5))] || m.month,
income: m.income,
@@ -67,8 +136,10 @@ export default function FinanceAnalytics({ month, year }) {
}))
const comp = analytics.comparison_prev_month
- const diffPct = Math.abs(Math.round(comp.diff_percent))
- const isUp = comp.diff_percent > 0
+ const diffPct = Math.abs(Math.round(comp?.diff_percent || 0))
+ const isUp = (comp?.diff_percent || 0) > 0
+
+ const barChartHeight = Math.max(400, barData.length * 44)
return (
@@ -98,30 +169,41 @@ export default function FinanceAnalytics({ month, year }) {
{barData.length > 0 && (
-
- Расходы по категориям
-
-
+
+
+ Расходы по категориям
+
+ {hasSavings && (
+
+ )}
+
+
-
+
v / 1000 + "к"}
+ tickFormatter={fmtShort}
/>
- fmt(v)} />
-
- {barData.map((_, i) => (
- |
+ } />
+
+ {barData.map((entry, i) => (
+ |
))}
+ } />
@@ -145,29 +227,28 @@ export default function FinanceAnalytics({ month, year }) {
dataKey="value"
stroke="none"
>
- {pieData.map((_, i) => (
- |
+ {pieData.map((entry, i) => (
+ |
))}
- fmt(v)} />
+ } />
-
+
{pieData.map((c, i) => (
-
+
{c.name}
-
+
{Math.round(
- (c.value / summary.total_expense) * 100
- )}
- %
+ (c.value / filteredTotal) * 100
+ )}%
))}
@@ -192,9 +273,9 @@ export default function FinanceAnalytics({ month, year }) {
v / 1000 + "к"}
+ tickFormatter={fmtShort}
/>
- fmt(v)} />
+ } />