Files
SimpleFinanceDash/frontend/src/components/AvailableBalance.tsx
Jean-Luc Makiola f141c4ff73 feat(04-02): add locale prop and custom currency tooltips to chart components
- Add locale?: string prop to ExpenseBreakdown and AvailableBalance
- Replace bare <Tooltip /> in ExpenseBreakdown with custom content renderer
- Add Tooltip import and custom content renderer to AvailableBalance
- Pass locale to formatCurrency in AvailableBalance center text
- Tooltip styled with shadcn design tokens (bg-background, border-border/50, shadow-xl)
2026-03-12 09:26:13 +01:00

79 lines
3.3 KiB
TypeScript

import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'
import type { BudgetDetail } from '@/lib/api'
import { formatCurrency } from '@/lib/format'
import { palette, headerGradient, type CategoryType } from '@/lib/palette'
import { cn } from '@/lib/utils'
interface Props {
budget: BudgetDetail
locale?: string
}
export function AvailableBalance({ budget, locale = 'en' }: Props) {
const { t } = useTranslation()
const { totals } = budget
const available = totals.available
const allData: Array<{ name: string; value: number; categoryType: CategoryType }> = [
{ name: t('dashboard.remaining'), value: Math.max(0, available), categoryType: 'carryover' },
{ name: t('dashboard.bills'), value: totals.bills_actual, categoryType: 'bill' },
{ name: t('dashboard.expenses'), value: totals.expenses_actual, categoryType: 'variable_expense' },
{ name: t('dashboard.debts'), value: totals.debts_actual, categoryType: 'debt' },
{ name: t('dashboard.savings'), value: totals.savings_actual, categoryType: 'saving' },
{ name: t('dashboard.investments'), value: totals.investments_actual, categoryType: 'investment' },
]
const data = allData.filter((d) => d.value > 0)
return (
<Card>
<CardHeader style={headerGradient('saving')} className="px-6 py-5">
<CardTitle className="text-2xl font-semibold">{t('dashboard.availableAmount')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center gap-4 pt-6">
<div className="relative size-48">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={55}
outerRadius={80}
paddingAngle={2}
dataKey="value"
>
{data.map((entry, index) => (
<Cell key={index} fill={palette[entry.categoryType]?.base ?? palette.carryover.base} />
))}
</Pie>
<Tooltip
content={({ active, payload }) => {
if (!active || !payload?.length) return null
const item = payload[0]
return (
<div className="rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl">
<p className="font-medium text-foreground">{item.name}</p>
<p className="font-mono tabular-nums text-muted-foreground">
{formatCurrency(Number(item.value), budget.currency, locale)}
</p>
</div>
)
}}
/>
</PieChart>
</ResponsiveContainer>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className={cn('text-3xl font-bold tabular-nums', available >= 0 ? 'text-success' : 'text-destructive')}>
{formatCurrency(available, budget.currency, locale)}
</span>
<span className="text-xs text-muted-foreground">{t('dashboard.available', 'Available')}</span>
</div>
</div>
</CardContent>
</Card>
)
}