diff --git a/src/app/api/savings/route.ts b/src/app/api/savings/route.ts index 30cfce6..6be5839 100644 --- a/src/app/api/savings/route.ts +++ b/src/app/api/savings/route.ts @@ -19,36 +19,32 @@ async function getAccessToken(): Promise { export async function GET() { try { const token = await getAccessToken(); - - const res = await fetch(`${PULSE_API}/finance/transactions?limit=200&type=expense`, { + + const res = await fetch(`${PULSE_API}/savings/categories`, { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000), }); - const txs = await res.json(); - - // Группируем накопления по цели - const groups: Record = {}; - for (const t of txs) { - if (t.category_emoji === "💎") { - // Нормализуем название - const desc = t.description as string; - let key = "Другое"; - if (desc.toLowerCase().includes("квартир")) key = "🏠 Квартира"; - else if (desc.toLowerCase().includes("отпуск") || desc.toLowerCase().includes("путешеств")) key = "✈️ Отпуск"; - else if (desc.toLowerCase().includes("машин") || desc.toLowerCase().includes("авто")) key = "🚗 Машина"; - else key = desc; - groups[key] = (groups[key] || 0) + (t.amount as number); - } - } - - const goals = Object.entries(groups) - .map(([name, amount]) => ({ name, amount })) - .sort((a, b) => b.amount - a.amount); - - const total = goals.reduce((s, g) => s + g.amount, 0); - - return NextResponse.json({ goals, total }); + const categories = await res.json(); + + const items = (Array.isArray(categories) ? categories : []) + .filter((c: any) => !c.is_closed && c.current_amount > 0) + .map((c: any) => ({ + id: c.id, + name: c.name, + amount: c.current_amount as number, + isDeposit: c.is_deposit as boolean, + isAccount: c.is_account as boolean, + isMulti: c.is_multi as boolean, + interestRate: c.interest_rate as number, + depositEndDate: c.deposit_end_date as string | null, + targetAmount: c.final_amount > 0 ? c.final_amount : null, + })) + .sort((a: any, b: any) => b.amount - a.amount); + + const total = items.reduce((s: number, i: any) => s + i.amount, 0); + + return NextResponse.json({ items, total }); } catch (e) { - return NextResponse.json({ error: String(e), goals: [], total: 0 }, { status: 500 }); + return NextResponse.json({ error: String(e), items: [], total: 0 }, { status: 500 }); } } diff --git a/src/components/widgets/SavingsWidget.tsx b/src/components/widgets/SavingsWidget.tsx index 7cf8b34..986f807 100644 --- a/src/components/widgets/SavingsWidget.tsx +++ b/src/components/widgets/SavingsWidget.tsx @@ -1,27 +1,48 @@ "use client"; import { useEffect, useState, useCallback } from "react"; -import { RefreshCw, PiggyBank } from "lucide-react"; +import { RefreshCw, TrendingUp, Wallet, PiggyBank } from "lucide-react"; -interface Goal { +interface SavingsItem { + id: number; name: string; amount: number; + isDeposit: boolean; + isAccount: boolean; + isMulti: boolean; + interestRate: number; + depositEndDate: string | null; + targetAmount: number | null; } interface SavingsData { - goals: Goal[]; + items: SavingsItem[]; total: number; error?: string; } function formatMoney(n: number): string { + if (n >= 1_000_000) return (n / 1_000_000).toFixed(2).replace(/\.?0+$/, "") + " М ₽"; return n.toLocaleString("ru-RU") + " ₽"; } -// Целевые суммы (максимум для прогресс-бара) -const TARGETS: Record = { - "🏠 Квартира": 500000, - "✈️ Отпуск": 200000, -}; +function getIcon(item: SavingsItem) { + if (item.isDeposit) return "🏦"; + if (item.isAccount) return "💳"; + return "🐷"; +} + +function getColor(item: SavingsItem, index: number): string { + const colors = ["#6366f1", "#f59e0b", "#10b981", "#06b6d4", "#8b5cf6", "#f43f5e"]; + if (item.isDeposit) return "#f59e0b"; + if (item.isAccount) return "#06b6d4"; + return colors[index % colors.length]; +} + +function getBadge(item: SavingsItem): string | null { + if (item.isDeposit && item.interestRate > 0) return `${item.interestRate}%`; + if (item.isMulti) return "общая"; + return null; +} export function SavingsWidget() { const [data, setData] = useState(null); @@ -33,7 +54,7 @@ export function SavingsWidget() { const res = await fetch("/api/savings", { cache: "no-store" }); setData(await res.json()); } catch { - setData({ goals: [], total: 0, error: "Ошибка загрузки" }); + setData({ items: [], total: 0, error: "Ошибка загрузки" }); } finally { setLoading(false); } @@ -43,6 +64,7 @@ export function SavingsWidget() { return (
+ {/* Header */}
@@ -56,46 +78,64 @@ export function SavingsWidget() {
{loading ? ( -
- {[1, 2].map(i =>
)} +
+ {[1,2,3,4].map(i =>
)}
) : data?.error ? (
{data.error}
) : ( <> - {/* Total */} + {/* Итого */}
- Итого накоплено + Всего накоплено {formatMoney(data?.total || 0)}
- {/* Goals */} -
- {(data?.goals || []).map(goal => { - const target = TARGETS[goal.name]; - const pct = target ? Math.min(100, (goal.amount / target) * 100) : null; - const color = goal.name.includes("Квартир") ? "#6366f1" : - goal.name.includes("Отпуск") ? "#f59e0b" : "#10b981"; + {/* Категории */} +
+ {(data?.items || []).map((item, i) => { + const color = getColor(item, i); + const badge = getBadge(item); + const pct = item.targetAmount ? Math.min(100, (item.amount / item.targetAmount) * 100) : null; + return ( -
-
- {goal.name} -
- {formatMoney(goal.amount)} - {target && / {formatMoney(target)}} -
+
+ {/* Icon */} +
+ {getIcon(item)}
- {pct !== null && ( -
-
+ + {/* Name + badge */} +
+
+ {item.name} + {badge && ( + + {badge} + + )}
- )} - {target && ( -
{pct?.toFixed(0)}% от цели
- )} + {/* Прогресс-бар если есть цель */} + {pct !== null && ( +
+
+
+ )} + {/* Дата окончания вклада */} + {item.depositEndDate && ( +
+ до {new Date(item.depositEndDate).toLocaleDateString("ru-RU", { day: "numeric", month: "short", year: "numeric" })} +
+ )} +
+ + {/* Amount */} + + {formatMoney(item.amount)} +
); })}