diff --git a/frontend/src/components/AvailableBalance.tsx b/frontend/src/components/AvailableBalance.tsx index 7772b3a..8219671 100644 --- a/frontend/src/components/AvailableBalance.tsx +++ b/frontend/src/components/AvailableBalance.tsx @@ -3,32 +3,32 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { PieChart, Pie, Cell, ResponsiveContainer } 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 } -const PASTEL_COLORS = ['#93c5fd', '#f9a8d4', '#fcd34d', '#a5b4fc', '#86efac', '#c4b5fd'] - export function AvailableBalance({ budget }: Props) { const { t } = useTranslation() const { totals } = budget const available = totals.available - const data = [ - { name: t('dashboard.remaining'), value: Math.max(0, available) }, - { name: t('dashboard.bills'), value: totals.bills_actual }, - { name: t('dashboard.expenses'), value: totals.expenses_actual }, - { name: t('dashboard.debts'), value: totals.debts_actual }, - { name: t('dashboard.savings'), value: totals.savings_actual }, - { name: t('dashboard.investments'), value: totals.investments_actual }, + const data: 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' }, ].filter((d) => d.value > 0) return ( - - {t('dashboard.availableAmount')} + + {t('dashboard.availableAmount')}
@@ -43,14 +43,17 @@ export function AvailableBalance({ budget }: Props) { paddingAngle={2} dataKey="value" > - {data.map((_, index) => ( - + {data.map((entry, index) => ( + ))}
- {formatCurrency(available, budget.currency)} + = 0 ? 'text-success' : 'text-destructive')}> + {formatCurrency(available, budget.currency)} + + {t('dashboard.available', 'Available')}
diff --git a/frontend/src/components/BillsTracker.tsx b/frontend/src/components/BillsTracker.tsx index 6aaeec5..058610d 100644 --- a/frontend/src/components/BillsTracker.tsx +++ b/frontend/src/components/BillsTracker.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import { Input } from '@/components/ui/input' import type { BudgetDetail } from '@/lib/api' import { formatCurrency } from '@/lib/format' +import { headerGradient, amountColorClass } from '@/lib/palette' +import { InlineEditCell } from '@/components/InlineEditCell' interface Props { budget: BudgetDetail @@ -17,7 +17,7 @@ export function BillsTracker({ budget, onUpdate }: Props) { return ( - + {t('dashboard.billsTracker')} @@ -31,16 +31,20 @@ export function BillsTracker({ budget, onUpdate }: Props) { {bills.map((item) => ( - onUpdate(item.id, { actual_amount: actual })} - /> + + {item.category_name} + + {formatCurrency(item.budgeted_amount, budget.currency)} + + onUpdate(item.id, { actual_amount: actual })} + className={amountColorClass({ type: 'bill', actual: item.actual_amount, budgeted: item.budgeted_amount })} + /> + ))} - + {t('dashboard.budget')} {formatCurrency(bills.reduce((s, i) => s + i.budgeted_amount, 0), budget.currency)} @@ -55,56 +59,3 @@ export function BillsTracker({ budget, onUpdate }: Props) { ) } - -function InlineEditRow({ - label, - budgeted, - actual, - currency, - onSave, -}: { - label: string - budgeted: number - actual: number - currency: string - onSave: (value: number) => Promise -}) { - const [editing, setEditing] = useState(false) - const [value, setValue] = useState(String(actual)) - - const handleBlur = async () => { - const num = parseFloat(value) - if (!isNaN(num) && num !== actual) { - await onSave(num) - } - setEditing(false) - } - - return ( - - {label} - {formatCurrency(budgeted, currency)} - - {editing ? ( - setValue(e.target.value)} - onBlur={handleBlur} - onKeyDown={(e) => e.key === 'Enter' && handleBlur()} - className="ml-auto w-28 text-right" - autoFocus - /> - ) : ( - { setValue(String(actual)); setEditing(true) }} - > - {formatCurrency(actual, currency)} - - )} - - - ) -} diff --git a/frontend/src/components/DebtTracker.tsx b/frontend/src/components/DebtTracker.tsx index d680f75..9b59b15 100644 --- a/frontend/src/components/DebtTracker.tsx +++ b/frontend/src/components/DebtTracker.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import { Input } from '@/components/ui/input' import type { BudgetDetail } from '@/lib/api' import { formatCurrency } from '@/lib/format' +import { headerGradient, amountColorClass } from '@/lib/palette' +import { InlineEditCell } from '@/components/InlineEditCell' interface Props { budget: BudgetDetail @@ -19,7 +19,7 @@ export function DebtTracker({ budget, onUpdate }: Props) { return ( - + {t('dashboard.debtTracker')} @@ -33,16 +33,20 @@ export function DebtTracker({ budget, onUpdate }: Props) { {debts.map((item) => ( - onUpdate(item.id, { actual_amount: actual })} - /> + + {item.category_name} + + {formatCurrency(item.budgeted_amount, budget.currency)} + + onUpdate(item.id, { actual_amount: actual })} + className={amountColorClass({ type: 'debt', actual: item.actual_amount, budgeted: item.budgeted_amount })} + /> + ))} - + {t('dashboard.budget')} {formatCurrency(debts.reduce((s, i) => s + i.budgeted_amount, 0), budget.currency)} @@ -57,56 +61,3 @@ export function DebtTracker({ budget, onUpdate }: Props) { ) } - -function InlineEditRow({ - label, - budgeted, - actual, - currency, - onSave, -}: { - label: string - budgeted: number - actual: number - currency: string - onSave: (value: number) => Promise -}) { - const [editing, setEditing] = useState(false) - const [value, setValue] = useState(String(actual)) - - const handleBlur = async () => { - const num = parseFloat(value) - if (!isNaN(num) && num !== actual) { - await onSave(num) - } - setEditing(false) - } - - return ( - - {label} - {formatCurrency(budgeted, currency)} - - {editing ? ( - setValue(e.target.value)} - onBlur={handleBlur} - onKeyDown={(e) => e.key === 'Enter' && handleBlur()} - className="ml-auto w-28 text-right" - autoFocus - /> - ) : ( - { setValue(String(actual)); setEditing(true) }} - > - {formatCurrency(actual, currency)} - - )} - - - ) -} diff --git a/frontend/src/components/ExpenseBreakdown.tsx b/frontend/src/components/ExpenseBreakdown.tsx index 5e6e99a..7ee7fc0 100644 --- a/frontend/src/components/ExpenseBreakdown.tsx +++ b/frontend/src/components/ExpenseBreakdown.tsx @@ -2,24 +2,27 @@ 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 { palette, headerGradient, type CategoryType } from '@/lib/palette' interface Props { budget: BudgetDetail } -const PASTEL_COLORS = ['#f9a8d4', '#fcd34d', '#93c5fd', '#a5b4fc', '#86efac', '#c4b5fd', '#fca5a5', '#fdba74'] - export function ExpenseBreakdown({ budget }: Props) { const { t } = useTranslation() const expenses = budget.items .filter((i) => i.category_type === 'variable_expense' && i.actual_amount > 0) - .map((i) => ({ name: i.category_name, value: i.actual_amount })) + .map((i) => ({ + name: i.category_name, + value: i.actual_amount, + categoryType: i.category_type as CategoryType, + })) if (expenses.length === 0) return null return ( - + {t('dashboard.expenseBreakdown')} @@ -34,8 +37,8 @@ export function ExpenseBreakdown({ budget }: Props) { dataKey="value" label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`} > - {expenses.map((_, index) => ( - + {expenses.map((entry, index) => ( + ))} diff --git a/frontend/src/components/FinancialOverview.tsx b/frontend/src/components/FinancialOverview.tsx index 4f55268..ae2a845 100644 --- a/frontend/src/components/FinancialOverview.tsx +++ b/frontend/src/components/FinancialOverview.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import type { BudgetDetail } from '@/lib/api' import { formatCurrency } from '@/lib/format' +import { palette, overviewHeaderGradient, amountColorClass, type CategoryType } from '@/lib/palette' interface Props { budget: BudgetDetail @@ -12,20 +13,20 @@ export function FinancialOverview({ budget }: Props) { const { t } = useTranslation() const { totals } = budget - const rows = [ - { label: t('dashboard.carryover'), budget: budget.carryover_amount, actual: budget.carryover_amount, color: 'bg-sky-50' }, - { label: t('dashboard.income'), budget: totals.income_budget, actual: totals.income_actual, color: 'bg-emerald-50' }, - { label: t('dashboard.bills'), budget: totals.bills_budget, actual: totals.bills_actual, color: 'bg-blue-50' }, - { label: t('dashboard.expenses'), budget: totals.expenses_budget, actual: totals.expenses_actual, color: 'bg-amber-50' }, - { label: t('dashboard.debts'), budget: totals.debts_budget, actual: totals.debts_actual, color: 'bg-red-50' }, - { label: t('dashboard.savings'), budget: totals.savings_budget, actual: totals.savings_actual, color: 'bg-violet-50' }, - { label: t('dashboard.investments'), budget: totals.investments_budget, actual: totals.investments_actual, color: 'bg-pink-50' }, + const rows: Array<{ label: string; budget: number; actual: number; categoryType: CategoryType; isIncome?: boolean }> = [ + { label: t('dashboard.carryover'), budget: budget.carryover_amount, actual: budget.carryover_amount, categoryType: 'carryover', isIncome: true }, + { label: t('dashboard.income'), budget: totals.income_budget, actual: totals.income_actual, categoryType: 'income', isIncome: true }, + { label: t('dashboard.bills'), budget: totals.bills_budget, actual: totals.bills_actual, categoryType: 'bill' }, + { label: t('dashboard.expenses'), budget: totals.expenses_budget, actual: totals.expenses_actual, categoryType: 'variable_expense' }, + { label: t('dashboard.debts'), budget: totals.debts_budget, actual: totals.debts_actual, categoryType: 'debt' }, + { label: t('dashboard.savings'), budget: totals.savings_budget, actual: totals.savings_actual, categoryType: 'saving' }, + { label: t('dashboard.investments'), budget: totals.investments_budget, actual: totals.investments_actual, categoryType: 'investment' }, ] return ( - - {t('dashboard.financialOverview')} + + {t('dashboard.financialOverview')} @@ -38,16 +39,20 @@ export function FinancialOverview({ budget }: Props) { {rows.map((row) => ( - + {row.label} {formatCurrency(row.budget, budget.currency)} - {formatCurrency(row.actual, budget.currency)} + + {formatCurrency(row.actual, budget.currency)} + ))} {t('dashboard.remaining')} - {formatCurrency(totals.available, budget.currency)} + + {formatCurrency(totals.available, budget.currency)} +
diff --git a/frontend/src/components/VariableExpenses.tsx b/frontend/src/components/VariableExpenses.tsx index 90c5e8e..c5193a6 100644 --- a/frontend/src/components/VariableExpenses.tsx +++ b/frontend/src/components/VariableExpenses.tsx @@ -1,11 +1,11 @@ -import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import { Input } from '@/components/ui/input' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts' import type { BudgetDetail } from '@/lib/api' import { formatCurrency } from '@/lib/format' +import { headerGradient, amountColorClass, palette } from '@/lib/palette' +import { InlineEditCell } from '@/components/InlineEditCell' interface Props { budget: BudgetDetail @@ -24,7 +24,7 @@ export function VariableExpenses({ budget, onUpdate }: Props) { return ( - + {t('dashboard.variableExpenses')} @@ -38,17 +38,27 @@ export function VariableExpenses({ budget, onUpdate }: Props) { - {expenses.map((item) => ( - onUpdate(item.id, { actual_amount: actual })} - /> - ))} - + {expenses.map((item) => { + const remaining = item.budgeted_amount - item.actual_amount + return ( + + {item.category_name} + + {formatCurrency(item.budgeted_amount, budget.currency)} + + onUpdate(item.id, { actual_amount: actual })} + className={amountColorClass({ type: 'variable_expense', actual: item.actual_amount, budgeted: item.budgeted_amount })} + /> + + {formatCurrency(remaining, budget.currency)} + + + ) + })} + {t('dashboard.budget')} {formatCurrency(expenses.reduce((s, i) => s + i.budgeted_amount, 0), budget.currency)} @@ -72,8 +82,8 @@ export function VariableExpenses({ budget, onUpdate }: Props) { - - + + @@ -82,61 +92,3 @@ export function VariableExpenses({ budget, onUpdate }: Props) { ) } - -function InlineEditRow({ - label, - budgeted, - actual, - currency, - onSave, -}: { - label: string - budgeted: number - actual: number - currency: string - onSave: (value: number) => Promise -}) { - const [editing, setEditing] = useState(false) - const [value, setValue] = useState(String(actual)) - - const handleBlur = async () => { - const num = parseFloat(value) - if (!isNaN(num) && num !== actual) { - await onSave(num) - } - setEditing(false) - } - - const remaining = budgeted - actual - - return ( - - {label} - {formatCurrency(budgeted, currency)} - - {editing ? ( - setValue(e.target.value)} - onBlur={handleBlur} - onKeyDown={(e) => e.key === 'Enter' && handleBlur()} - className="ml-auto w-28 text-right" - autoFocus - /> - ) : ( - { setValue(String(actual)); setEditing(true) }} - > - {formatCurrency(actual, currency)} - - )} - - - {formatCurrency(remaining, currency)} - - - ) -}