diff --git a/src/i18n/de.json b/src/i18n/de.json index 5167529..5974499 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -61,7 +61,10 @@ "notes": "Notizen", "addItem": "Eintrag hinzufügen", "empty": "Noch keine Budgets. Erstelle dein erstes Monatsbudget.", - "deleteConfirm": "Bist du sicher, dass du dieses Budget löschen möchtest?" + "deleteConfirm": "Bist du sicher, dass du dieses Budget löschen möchtest?", + "month": "Monat", + "year": "Jahr", + "total": "{{label}} Gesamt" }, "dashboard": { "title": "Dashboard", diff --git a/src/i18n/en.json b/src/i18n/en.json index 642fd5a..a85fa41 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -61,7 +61,10 @@ "notes": "Notes", "addItem": "Add Item", "empty": "No budgets yet. Create your first monthly budget.", - "deleteConfirm": "Are you sure you want to delete this budget?" + "deleteConfirm": "Are you sure you want to delete this budget?", + "month": "Month", + "year": "Year", + "total": "{{label}} Total" }, "dashboard": { "title": "Dashboard", diff --git a/src/pages/BudgetListPage.tsx b/src/pages/BudgetListPage.tsx new file mode 100644 index 0000000..b49d635 --- /dev/null +++ b/src/pages/BudgetListPage.tsx @@ -0,0 +1,272 @@ +import { useState, useMemo } from "react" +import { Link } from "react-router-dom" +import { useTranslation } from "react-i18next" +import { Plus, ChevronRight } from "lucide-react" +import { toast } from "sonner" +import { useBudgets } from "@/hooks/useBudgets" +import type { Budget } from "@/lib/types" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Skeleton } from "@/components/ui/skeleton" +import { PageShell } from "@/components/shared/PageShell" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const CURRENT_YEAR = new Date().getFullYear() +const YEARS = Array.from({ length: 5 }, (_, i) => CURRENT_YEAR - 1 + i) + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Given a budget's start_date ISO string, return a human-readable "Month YYYY" + * label e.g. "March 2026", using the given locale for month names. + */ +function budgetLabel(budget: Budget, locale: string): string { + const [year, month] = budget.start_date.split("-").map(Number) + return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format( + new Date(year ?? 0, (month ?? 1) - 1, 1) + ) +} + +// --------------------------------------------------------------------------- +// Dialog state +// --------------------------------------------------------------------------- + +interface NewBudgetState { + month: number + year: number + useTemplate: boolean +} + +function defaultDialogState(): NewBudgetState { + const now = new Date() + return { + month: now.getMonth() + 1, + year: now.getFullYear(), + useTemplate: false, + } +} + +// --------------------------------------------------------------------------- +// Page component +// --------------------------------------------------------------------------- + +export default function BudgetListPage() { + const { t, i18n } = useTranslation() + const locale = i18n.language + const { budgets, loading, createBudget, generateFromTemplate } = useBudgets() + + const [dialogOpen, setDialogOpen] = useState(false) + const [form, setForm] = useState(defaultDialogState) + + const monthItems = useMemo( + () => + Array.from({ length: 12 }, (_, i) => ({ + value: i + 1, + label: new Intl.DateTimeFormat(locale, { month: "long" }).format( + new Date(2000, i, 1) + ), + })), + [locale] + ) + + if (loading) return ( + +
+ {[1, 2, 3, 4].map((i) => ( +
+ + + +
+ ))} +
+
+ ) + + function openDialog() { + setForm(defaultDialogState()) + setDialogOpen(true) + } + + function closeDialog() { + setDialogOpen(false) + } + + async function handleCreate() { + try { + const mutation = form.useTemplate ? generateFromTemplate : createBudget + const result = await mutation.mutateAsync({ + month: form.month, + year: form.year, + }) + closeDialog() + // Announce success via a toast showing the created month + toast.success(budgetLabel(result, locale)) + } catch { + toast.error(t("common.error")) + } + } + + const isSaving = createBudget.isPending || generateFromTemplate.isPending + + return ( + + + {t("budgets.newBudget")} + + } + > + {/* Empty state */} + {budgets.length === 0 ? ( +

{t("budgets.empty")}

+ ) : ( + + + + {t("budgets.title")} + {t("settings.currency")} + + + + + {budgets.map((budget) => ( + + + + {budgetLabel(budget, locale)} + + + + {budget.currency} + + + + + + + + ))} + +
+ )} + + {/* New budget dialog */} + + + + {t("budgets.newBudget")} + + +
+ {/* Month picker */} +
+
+ + +
+ +
+ + +
+
+ + {/* Template toggle */} +
+ + setForm((prev) => ({ ...prev, useTemplate: e.target.checked })) + } + /> + +
+ + {/* Actions */} +
+ + +
+
+
+
+
+ ) +}