diff --git a/frontend/src/components/BillsTracker.tsx b/frontend/src/components/BillsTracker.tsx index 058610d..0ffa9a0 100644 --- a/frontend/src/components/BillsTracker.tsx +++ b/frontend/src/components/BillsTracker.tsx @@ -1,9 +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 { Skeleton } from '@/components/ui/skeleton' import type { BudgetDetail } from '@/lib/api' import { formatCurrency } from '@/lib/format' -import { headerGradient, amountColorClass } from '@/lib/palette' +import { headerGradient, amountColorClass, palette } from '@/lib/palette' import { InlineEditCell } from '@/components/InlineEditCell' interface Props { @@ -15,6 +17,38 @@ export function BillsTracker({ budget, onUpdate }: Props) { const { t } = useTranslation() const bills = budget.items.filter((i) => i.category_type === 'bill') + const [flashRowId, setFlashRowId] = useState(null) + const [errorRowId, setErrorRowId] = useState(null) + + const triggerFlash = (id: string, type: 'success' | 'error') => { + if (type === 'success') { + setFlashRowId(id) + setTimeout(() => setFlashRowId(null), 600) + } else { + setErrorRowId(id) + setTimeout(() => setErrorRowId(null), 600) + } + } + + if (bills.length === 0) { + return ( + + + {t('dashboard.billsTracker')} + + + {[1, 2, 3].map((i) => ( + + ))} + + + ) + } + return ( @@ -31,7 +65,17 @@ export function BillsTracker({ budget, onUpdate }: Props) { {bills.map((item) => ( - + {item.category_name} {formatCurrency(item.budgeted_amount, budget.currency)} @@ -40,6 +84,8 @@ export function BillsTracker({ budget, onUpdate }: Props) { value={item.actual_amount} currency={budget.currency} onSave={(actual) => onUpdate(item.id, { actual_amount: actual })} + onSaveSuccess={() => triggerFlash(item.id, 'success')} + onSaveError={() => triggerFlash(item.id, 'error')} className={amountColorClass({ type: 'bill', actual: item.actual_amount, budgeted: item.budgeted_amount })} /> diff --git a/frontend/src/components/DebtTracker.tsx b/frontend/src/components/DebtTracker.tsx index 9b59b15..64b6e1a 100644 --- a/frontend/src/components/DebtTracker.tsx +++ b/frontend/src/components/DebtTracker.tsx @@ -1,9 +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 { Skeleton } from '@/components/ui/skeleton' import type { BudgetDetail } from '@/lib/api' import { formatCurrency } from '@/lib/format' -import { headerGradient, amountColorClass } from '@/lib/palette' +import { headerGradient, amountColorClass, palette } from '@/lib/palette' import { InlineEditCell } from '@/components/InlineEditCell' interface Props { @@ -15,7 +17,37 @@ export function DebtTracker({ budget, onUpdate }: Props) { const { t } = useTranslation() const debts = budget.items.filter((i) => i.category_type === 'debt') - if (debts.length === 0) return null + const [flashRowId, setFlashRowId] = useState(null) + const [errorRowId, setErrorRowId] = useState(null) + + const triggerFlash = (id: string, type: 'success' | 'error') => { + if (type === 'success') { + setFlashRowId(id) + setTimeout(() => setFlashRowId(null), 600) + } else { + setErrorRowId(id) + setTimeout(() => setErrorRowId(null), 600) + } + } + + if (debts.length === 0) { + return ( + + + {t('dashboard.debtTracker')} + + + {[1, 2, 3].map((i) => ( + + ))} + + + ) + } return ( @@ -33,7 +65,17 @@ export function DebtTracker({ budget, onUpdate }: Props) { {debts.map((item) => ( - + {item.category_name} {formatCurrency(item.budgeted_amount, budget.currency)} @@ -42,6 +84,8 @@ export function DebtTracker({ budget, onUpdate }: Props) { value={item.actual_amount} currency={budget.currency} onSave={(actual) => onUpdate(item.id, { actual_amount: actual })} + onSaveSuccess={() => triggerFlash(item.id, 'success')} + onSaveError={() => triggerFlash(item.id, 'error')} className={amountColorClass({ type: 'debt', actual: item.actual_amount, budgeted: item.budgeted_amount })} /> diff --git a/frontend/src/components/VariableExpenses.tsx b/frontend/src/components/VariableExpenses.tsx index c5193a6..db89355 100644 --- a/frontend/src/components/VariableExpenses.tsx +++ b/frontend/src/components/VariableExpenses.tsx @@ -1,6 +1,8 @@ +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 { Skeleton } from '@/components/ui/skeleton' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts' import type { BudgetDetail } from '@/lib/api' import { formatCurrency } from '@/lib/format' @@ -16,12 +18,44 @@ export function VariableExpenses({ budget, onUpdate }: Props) { const { t } = useTranslation() const expenses = budget.items.filter((i) => i.category_type === 'variable_expense') + const [flashRowId, setFlashRowId] = useState(null) + const [errorRowId, setErrorRowId] = useState(null) + + const triggerFlash = (id: string, type: 'success' | 'error') => { + if (type === 'success') { + setFlashRowId(id) + setTimeout(() => setFlashRowId(null), 600) + } else { + setErrorRowId(id) + setTimeout(() => setErrorRowId(null), 600) + } + } + const chartData = expenses.map((item) => ({ name: item.category_name, [t('dashboard.budget')]: item.budgeted_amount, [t('dashboard.actual')]: item.actual_amount, })) + if (expenses.length === 0) { + return ( + + + {t('dashboard.variableExpenses')} + + + {[1, 2, 3].map((i) => ( + + ))} + + + ) + } + return ( @@ -41,7 +75,17 @@ export function VariableExpenses({ budget, onUpdate }: Props) { {expenses.map((item) => { const remaining = item.budgeted_amount - item.actual_amount return ( - + {item.category_name} {formatCurrency(item.budgeted_amount, budget.currency)} @@ -50,6 +94,8 @@ export function VariableExpenses({ budget, onUpdate }: Props) { value={item.actual_amount} currency={budget.currency} onSave={(actual) => onUpdate(item.id, { actual_amount: actual })} + onSaveSuccess={() => triggerFlash(item.id, 'success')} + onSaveError={() => triggerFlash(item.id, 'error')} className={amountColorClass({ type: 'variable_expense', actual: item.actual_amount, budgeted: item.budgeted_amount })} />