diff --git a/sample_expenses.csv b/sample_expenses.csv
new file mode 100644
index 0000000..11c645d
--- /dev/null
+++ b/sample_expenses.csv
@@ -0,0 +1,9 @@
+Date,Merchant Name,Description,Amount,Category,Notes,Account Provider,Account Name,Status,Sub Type
+2025-09-05,Tesco,TESCO EXTRA BIRMINGHAM,-45.67,Shopping,,Monzo,Monzo,,
+2025-09-04,Costa Coffee,COSTA COFFEE BIRMINGHAM,-3.85,Food & Drink,,Monzo,Monzo,,
+2025-09-03,Uber,UBER TRIP 12345,-12.50,Transportation,,Monzo,Monzo,,
+2025-09-02,Amazon,AMAZON PRIME VIDEO,-8.99,Entertainment,,Monzo,Monzo,,
+2025-09-01,Sainsbury's,SAINSBURYS BIRMINGHAM,-23.45,Shopping,,Monzo,Monzo,,
+2025-08-31,McDonald's,MCDONALDS RESTAURANT,-7.89,Food & Drink,,Monzo,Monzo,,
+2025-08-30,Shell,SHELL PETROL STATION,-55.00,Transportation,,TSB,TSB,,
+2025-08-29,Netflix,NETFLIX SUBSCRIPTION,-11.99,Entertainment,,TSB,TSB,,
diff --git a/src/app/api/expenses/import/route.ts b/src/app/api/expenses/import/route.ts
new file mode 100644
index 0000000..5a2fa59
--- /dev/null
+++ b/src/app/api/expenses/import/route.ts
@@ -0,0 +1,247 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getCurrentUser } from "@/lib/auth";
+import { db } from "@/lib/db";
+import { TransactionType, CategoryKind } from "../../../../../generated/prisma";
+import { tracer } from "@/lib/telemetry";
+import { Decimal } from "@prisma/client/runtime/library";
+
+interface CSVImportRequest {
+ csvData: string;
+ dateColumn: string;
+ amountColumn: string;
+ categoryColumn: string;
+ merchantColumn?: string;
+ descriptionColumn?: string;
+}
+
+export async function POST(request: NextRequest) {
+ return tracer.startActiveSpan("api.expenses.import.POST", async (span) => {
+ try {
+ span.setAttributes({
+ "http.method": "POST",
+ "http.route": "/api/expenses/import",
+ });
+
+ const user = await getCurrentUser();
+ if (!user) {
+ span.setAttributes({
+ "http.status_code": 401,
+ "auth.result": "unauthorized",
+ });
+ span.end();
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const {
+ csvData,
+ dateColumn,
+ amountColumn,
+ categoryColumn,
+ merchantColumn,
+ descriptionColumn,
+ }: CSVImportRequest = await request.json();
+
+ // Parse CSV data
+ const lines = csvData.trim().split("\n");
+ if (lines.length < 2) {
+ return NextResponse.json(
+ { error: "CSV must have at least a header and one data row" },
+ { status: 400 }
+ );
+ }
+
+ const headers = lines[0].split(",").map((h) => h.trim());
+ const dateColumnIndex = headers.indexOf(dateColumn);
+ const amountColumnIndex = headers.indexOf(amountColumn);
+ const categoryColumnIndex = headers.indexOf(categoryColumn);
+ const merchantColumnIndex = merchantColumn
+ ? headers.indexOf(merchantColumn)
+ : -1;
+ const descriptionColumnIndex = descriptionColumn
+ ? headers.indexOf(descriptionColumn)
+ : -1;
+
+ if (
+ dateColumnIndex === -1 ||
+ amountColumnIndex === -1 ||
+ categoryColumnIndex === -1
+ ) {
+ return NextResponse.json(
+ {
+ error: "Required columns not found in CSV",
+ missing: {
+ date: dateColumnIndex === -1,
+ amount: amountColumnIndex === -1,
+ category: categoryColumnIndex === -1,
+ },
+ },
+ { status: 400 }
+ );
+ }
+
+ const dataLines = lines.slice(1);
+ const transactions = [];
+ const categoriesMap = new Map();
+ const skippedRows: string[] = [];
+
+ // Get current date for filtering (last 90 days)
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 90);
+
+ span.setAttributes({
+ "csv.total_rows": dataLines.length,
+ "csv.headers_count": headers.length,
+ });
+
+ for (let i = 0; i < dataLines.length; i++) {
+ const row = dataLines[i];
+ const columns = row.split(",").map((c) => c.trim());
+
+ if (columns.length !== headers.length) {
+ skippedRows.push(`Row ${i + 2}: Column count mismatch`);
+ continue;
+ }
+
+ try {
+ // Parse date
+ const dateStr = columns[dateColumnIndex];
+ const transactionDate = new Date(dateStr);
+
+ if (isNaN(transactionDate.getTime())) {
+ skippedRows.push(`Row ${i + 2}: Invalid date format: ${dateStr}`);
+ continue;
+ }
+
+ // Only import transactions from the last 30 days
+ if (transactionDate < thirtyDaysAgo) {
+ continue;
+ }
+
+ // Parse amount (handle negative values and currency symbols)
+ const amountStr = columns[amountColumnIndex].replace(/[£$€,]/g, "");
+ const amount = parseFloat(amountStr);
+
+ if (isNaN(amount)) {
+ skippedRows.push(
+ `Row ${i + 2}: Invalid amount: ${columns[amountColumnIndex]}`
+ );
+ continue;
+ }
+
+ // Make sure it's treated as an expense (positive amount)
+ const expenseAmount = Math.abs(amount);
+
+ const categoryName = columns[categoryColumnIndex];
+ const merchant =
+ merchantColumnIndex !== -1
+ ? columns[merchantColumnIndex]
+ : undefined;
+ const description =
+ descriptionColumnIndex !== -1
+ ? columns[descriptionColumnIndex]
+ : undefined;
+
+ // Create or find category
+ let categoryId: string | undefined;
+ if (categoryName && categoryName !== "") {
+ if (!categoriesMap.has(categoryName)) {
+ // Create category if it doesn't exist
+ const existingCategory = await db.category.findFirst({
+ where: {
+ userId: user.id,
+ name: categoryName,
+ kind: CategoryKind.EXPENSE,
+ },
+ });
+
+ if (existingCategory) {
+ categoriesMap.set(categoryName, existingCategory.id);
+ } else {
+ const newCategory = await db.category.create({
+ data: {
+ userId: user.id,
+ name: categoryName,
+ kind: CategoryKind.EXPENSE,
+ color: getRandomColor(),
+ },
+ });
+ categoriesMap.set(categoryName, newCategory.id);
+ }
+ }
+ categoryId = categoriesMap.get(categoryName);
+ }
+
+ transactions.push({
+ userId: user.id,
+ type: TransactionType.EXPENSE,
+ amount: new Decimal(expenseAmount),
+ occurredAt: transactionDate.toISOString(),
+ description:
+ [merchant, description].filter(Boolean).join(" - ") || undefined,
+ categoryId,
+ });
+ } catch (error) {
+ skippedRows.push(`Row ${i + 2}: ${(error as Error).message}`);
+ continue;
+ }
+ }
+
+ // Bulk create transactions
+ let importedCount = 0;
+ if (transactions.length > 0) {
+ const result = await db.transaction.createMany({
+ data: transactions,
+ skipDuplicates: true,
+ });
+ importedCount = result.count;
+ }
+
+ span.setAttributes({
+ "transactions.created": importedCount,
+ "transactions.skipped": skippedRows.length,
+ "categories.created": categoriesMap.size,
+ success: true,
+ });
+
+ span.end();
+
+ return NextResponse.json({
+ success: true,
+ imported: importedCount,
+ skipped: skippedRows.length,
+ skippedReasons: skippedRows,
+ categoriesCreated: categoriesMap.size,
+ message: `Successfully imported ${importedCount} expense transactions from the last 30 days`,
+ });
+ } catch (error) {
+ span.recordException(error as Error);
+ span.setAttributes({
+ error: true,
+ "error.message": (error as Error).message,
+ });
+ span.end();
+
+ console.error("CSV import error:", error);
+ return NextResponse.json(
+ { error: "Failed to import CSV data" },
+ { status: 500 }
+ );
+ }
+ });
+}
+
+function getRandomColor(): string {
+ const colors = [
+ "#ef4444", // red
+ "#f97316", // orange
+ "#eab308", // yellow
+ "#22c55e", // green
+ "#06b6d4", // cyan
+ "#3b82f6", // blue
+ "#8b5cf6", // violet
+ "#ec4899", // pink
+ "#f59e0b", // amber
+ "#10b981", // emerald
+ ];
+ return colors[Math.floor(Math.random() * colors.length)];
+}
diff --git a/src/app/expenses/page.tsx b/src/app/expenses/page.tsx
new file mode 100644
index 0000000..bfb10a7
--- /dev/null
+++ b/src/app/expenses/page.tsx
@@ -0,0 +1,10 @@
+import ExpenseManager from "@/components/ExpenseManager";
+import React from "react";
+
+function Expenses() {
+ return
+
+
;
+}
+
+export default Expenses;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6a79c53..2878269 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -134,7 +134,7 @@ export default function RootLayout({
- {children}
+ {children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index c195ed7..7e63351 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,3 +1,4 @@
+import CurrentFinances from "@/components/dashboard/CurrentFinaces";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -6,6 +7,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
+import { useIncomeTransactions } from "@/hooks/useFinancialData";
import { SignedIn, SignedOut, SignUpButton } from "@clerk/nextjs";
import { currentUser } from "@clerk/nextjs/server";
import { Calendar, PoundSterling, Target, TrendingDown } from "lucide-react";
@@ -160,7 +162,7 @@ export default async function Home() {
availableBalance >= 0 ? "text-white" : "text-red-500"
}`}
>
- £{availableBalance.toLocaleString()}
+ £{ }
(null);
+
+ // TanStack Query hooks
+ const { data: categories = [], isLoading: categoriesLoading } =
+ useCategories();
+ const { data: transactions = [], isLoading: transactionsLoading } =
+ useExpenseTransactions();
+ const { data: expenseCategories = [] } = useExpenseCategories();
+
+ // Mutations
+ const createCategoryMutation = useCreateCategory({
+ onSuccess: () => {
+ tracer.startActiveSpan("ui.expense_category_created", (span) => {
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "category_created",
+ });
+ span.end();
+ });
+ setCategoryForm({ name: "", color: "#ef4444" });
+ setShowCategoryForm(false);
+ toast.success("Category created successfully");
+ },
+ onError: (error) => {
+ tracer.startActiveSpan("ui.expense_category_creation_failed", (span) => {
+ span.recordException(error);
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "category_creation_failed",
+ "error.message": error.message,
+ });
+ span.end();
+ });
+ console.error("Failed to create category:", error);
+ toast.error("Failed to create category");
+ },
+ });
+
+ const createTransactionMutation = useCreateTransaction({
+ onSuccess: () => {
+ tracer.startActiveSpan("ui.expense_transaction_created", (span) => {
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "transaction_created",
+ });
+ span.end();
+ });
+ setTransactionForm({
+ amount: "",
+ occurredAt: new Date().toISOString().split("T")[0],
+ description: "",
+ categoryId: "",
+ });
+ setShowExpenseForm(false);
+ toast.success("Expense added successfully");
+ },
+ onError: (error) => {
+ tracer.startActiveSpan(
+ "ui.expense_transaction_creation_failed",
+ (span) => {
+ span.recordException(error);
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "transaction_creation_failed",
+ "error.message": error.message,
+ });
+ span.end();
+ }
+ );
+ console.error("Failed to create transaction:", error);
+ toast.error("Failed to add expense");
+ },
+ });
+
+ const deleteTransactionMutation = useDeleteTransaction({
+ onSuccess: () => {
+ tracer.startActiveSpan("ui.expense_transaction_deleted", (span) => {
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "transaction_deleted",
+ });
+ span.end();
+ });
+ toast.success("Transaction deleted");
+ },
+ onError: (error) => {
+ tracer.startActiveSpan(
+ "ui.expense_transaction_deletion_failed",
+ (span) => {
+ span.recordException(error);
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "transaction_deletion_failed",
+ "error.message": error.message,
+ });
+ span.end();
+ }
+ );
+ console.error("Failed to delete transaction:", error);
+ toast.error("Failed to delete transaction");
+ },
+ });
+
+ const importExpensesMutation = useImportExpenses({
+ onSuccess: (result) => {
+ tracer.startActiveSpan("ui.expenses_imported", (span) => {
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "expenses_imported",
+ imported_count: result.imported,
+ skipped_count: result.skipped,
+ categories_created: result.categoriesCreated,
+ });
+ span.end();
+ });
+ setShowImportForm(false);
+ setImportForm({
+ dateColumn: "Date",
+ amountColumn: "Amount",
+ categoryColumn: "Category",
+ merchantColumn: "",
+ descriptionColumn: "",
+ });
+ if (fileRef.current) fileRef.current.value = "";
+ toast.success(result.message);
+ },
+ onError: (error) => {
+ tracer.startActiveSpan("ui.expenses_import_failed", (span) => {
+ span.recordException(error);
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "expenses_import_failed",
+ "error.message": error.message,
+ });
+ span.end();
+ });
+ console.error("Failed to import expenses:", error);
+ toast.error("Failed to import expenses");
+ },
+ });
+
+ const setupMutation = useSetupDefaults({
+ onSuccess: (result) => {
+ tracer.startActiveSpan("ui.expense_setup_completed", (span) => {
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "setup_completed",
+ categories_created: result.categoriesCreated,
+ was_skipped: result.skipped,
+ });
+ span.end();
+ });
+ toast.success("Default expense categories created");
+ },
+ onError: (error) => {
+ tracer.startActiveSpan("ui.expense_setup_failed", (span) => {
+ span.recordException(error);
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "setup_failed",
+ "error.message": error.message,
+ });
+ span.end();
+ });
+ console.error("Failed to setup defaults:", error);
+ toast.error("Failed to setup defaults");
+ },
+ });
+
+ // Form state
+ const [transactionForm, setTransactionForm] = React.useState({
+ amount: "",
+ occurredAt: new Date().toISOString().split("T")[0],
+ description: "",
+ categoryId: "",
+ });
+
+ const [categoryForm, setCategoryForm] = React.useState({
+ name: "",
+ color: "#ef4444",
+ });
+
+ const [importForm, setImportForm] = React.useState({
+ dateColumn: "Date",
+ amountColumn: "Amount",
+ categoryColumn: "Category",
+ merchantColumn: "Merchant Name",
+ descriptionColumn: "Description",
+ });
+
+ // Track component mounting for telemetry
+ React.useEffect(() => {
+ tracer.startActiveSpan("ui.expense_manager_mounted", (span) => {
+ span.setAttributes({
+ component: "ExpenseManager",
+ action: "mounted",
+ "categories.count": categories.length,
+ "transactions.count": transactions.length,
+ });
+ span.end();
+ });
+ }, [categories.length, transactions.length]);
+
+ const handleCreateCategory = (e: React.FormEvent) => {
+ e.preventDefault();
+ createCategoryMutation.mutate({
+ name: categoryForm.name,
+ kind: CategoryKind.EXPENSE,
+ color: categoryForm.color,
+ });
+ };
+
+ const handleCreateTransaction = (e: React.FormEvent) => {
+ e.preventDefault();
+ createTransactionMutation.mutate({
+ type: TransactionType.EXPENSE,
+ amount: parseFloat(transactionForm.amount),
+ occurredAt: new Date(transactionForm.occurredAt).toISOString(),
+ description: transactionForm.description || undefined,
+ categoryId: transactionForm.categoryId || undefined,
+ });
+ };
+
+ const handleImportCSV = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const file = fileRef.current?.files?.[0];
+ if (!file) {
+ toast.error("Please select a CSV file");
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const csvData = event.target?.result as string;
+ if (!csvData) {
+ toast.error("Failed to read CSV file");
+ return;
+ }
+
+ importExpensesMutation.mutate({
+ csvData,
+ dateColumn: importForm.dateColumn,
+ amountColumn: importForm.amountColumn,
+ categoryColumn: importForm.categoryColumn,
+ merchantColumn: importForm.merchantColumn || undefined,
+ descriptionColumn: importForm.descriptionColumn || undefined,
+ });
+ };
+
+ reader.readAsText(file);
+ };
+
+ const handleSetupDefaults = () => {
+ setupMutation.mutate();
+ };
+
+ const isLoading =
+ categoriesLoading ||
+ transactionsLoading ||
+ createCategoryMutation.isPending ||
+ createTransactionMutation.isPending ||
+ importExpensesMutation.isPending ||
+ setupMutation.isPending;
+
+ return (
+
+ {/* Header */}
+
+
+
+
Expense Management
+
+ Manage your expense categories and transactions. You can also
+ import a CSV file of your expenses.
+
+
+
+ {expenseCategories.length === 0 && (
+
+ {isLoading ? "Setting up..." : "Quick Setup"}
+
+ )}
+
setShowCategoryForm(true)}
+ variant="outline"
+ size="sm"
+ >
+
+ Add Category
+
+
setShowImportForm(true)}
+ variant="outline"
+ size="sm"
+ >
+
+ Import CSV
+
+
setShowExpenseForm(true)} size="sm">
+
+ Add Expense
+
+
+
+
+
+ {/* Category Form */}
+ {showCategoryForm && (
+
+
+ Add New Expense Category
+
+
+
+
+
+ )}
+
+ {/* Expense Transaction Form */}
+ {showExpenseForm && (
+
+
+
+ Add New Expense Transaction
+
+
+
+
+
+
+ )}
+
+ {/* CSV Import Form */}
+ {showImportForm && (
+
+
+
+
+ Import Expenses from CSV
+
+
+
+
+
+
+ CSV File
+
+
+
+
+
+
+
+
+
+
+ Description Column (Optional)
+
+
+ setImportForm((prev) => ({
+ ...prev,
+ descriptionColumn: e.target.value,
+ }))
+ }
+ placeholder="e.g., Description"
+ />
+
+
+
+
+ Note: Only transactions from the last 90 days
+ will be imported.
+
+
+ Your CSV should have headers matching the column names you
+ specify above. Amounts will be treated as expenses (positive
+ values).
+
+
+
+
+
+ {isLoading ? "Importing..." : "Import CSV"}
+
+ setShowImportForm(false)}
+ >
+ Cancel
+
+
+
+
+
+ )}
+
+ {/* Recent Transactions */}
+
+
+
+
+ Recent Expense Transactions
+
+
+
+ {transactions.length === 0 ? (
+
+
+
No expense transactions yet.
+
+ Add your first expense transaction above or import from CSV.
+
+
+ ) : (
+
+ {transactions.map((transaction) => (
+
+
+
+
+ -{formatCurrency(transaction.amount)}
+
+ {transaction.category && (
+
+ {transaction.category.name}
+
+ )}
+
+ {transaction.description && (
+
+ {transaction.description}
+
+ )}
+
+ {formatDate(transaction.occurredAt)}
+
+
+
+ {recentlyUpdated(24 * 60, transaction.createdAt) ? (
+ <>
+
{
+ e.preventDefault();
+ if (
+ window.confirm(
+ "Are you sure you want to delete this transaction?"
+ )
+ ) {
+ deleteTransactionMutation.mutate({
+ id: transaction.id,
+ });
+ }
+ }}
+ >
+
+ Delete
+
+ >
+ ) : (
+
+ {
+ toast("No longer editable", {
+ icon: ,
+ description:
+ "Transactions can only be edited or deleted within a day of creation.",
+ });
+ }}
+ />
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+ {/* Summary Cards */}
+
+
+
+
+
+
+
Expense Categories
+
{expenseCategories.length}
+
+
+
+
+
+
+
+
+
+
+
Recent Transactions
+
{transactions.length}
+
+
+
+
+
+
+
+ Total Expenses
+
+
+
+ -
+ {formatCurrency(
+ transactions
+ .reduce(
+ (acc, transaction) => acc + parseFloat(transaction.amount),
+ 0
+ )
+ .toString()
+ )}
+
+
+
+
+
+ );
+}
+
+export default ExpenseManager;
diff --git a/src/components/dashboard/CurrentFinaces.tsx b/src/components/dashboard/CurrentFinaces.tsx
new file mode 100644
index 0000000..2696c20
--- /dev/null
+++ b/src/components/dashboard/CurrentFinaces.tsx
@@ -0,0 +1,23 @@
+"use client";
+import { useIncomeTransactions } from "@/hooks/useFinancialData";
+import React from "react";
+
+function CurrentFinances() {
+ const { data: transactions = [], isLoading: transactionsLoading } =
+ useIncomeTransactions();
+
+ if (transactionsLoading) {
+ return Loading...
;
+ }
+
+ return (
+
+ {transactions.reduce(
+ (acc, transaction) => acc + parseInt(transaction.amount),
+ 0
+ ).toLocaleString()}
+
+ );
+}
+
+export default CurrentFinances;
diff --git a/src/hooks/useFinancialData.ts b/src/hooks/useFinancialData.ts
index 825a8c9..38009da 100644
--- a/src/hooks/useFinancialData.ts
+++ b/src/hooks/useFinancialData.ts
@@ -520,3 +520,90 @@ export function useExpenseCategories() {
export function useIncomeTransactions() {
return useTransactions({ type: TransactionType.INCOME, limit: 10 });
}
+
+export function useExpenseTransactions() {
+ return useTransactions({ type: TransactionType.EXPENSE, limit: 10 });
+}
+
+// CSV Import Hook
+export function useImportExpenses(
+ options?: UseMutationOptions<
+ {
+ success: boolean;
+ imported: number;
+ skipped: number;
+ skippedReasons: string[];
+ categoriesCreated: number;
+ message: string;
+ },
+ Error,
+ {
+ csvData: string;
+ dateColumn: string;
+ amountColumn: string;
+ categoryColumn: string;
+ merchantColumn?: string;
+ descriptionColumn?: string;
+ }
+ >
+) {
+ const queryClient = useQueryClient();
+ const {
+ onSuccess: userOnSuccess,
+ onError: userOnError,
+ ...rest
+ } = options ?? {};
+
+ return useMutation({
+ mutationFn: (data) =>
+ tracer.startActiveSpan("hook.importExpenses", async (span) => {
+ span.setAttributes({
+ "hook.name": "useImportExpenses",
+ operation: "import",
+ "csv.date_column": data.dateColumn,
+ "csv.amount_column": data.amountColumn,
+ "csv.category_column": data.categoryColumn,
+ });
+
+ try {
+ const response = await fetch("/api/expenses/import", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || "Failed to import expenses");
+ }
+
+ const result = await response.json();
+ span.setAttributes({
+ "import.transactions_imported": result.imported,
+ "import.transactions_skipped": result.skipped,
+ "import.categories_created": result.categoriesCreated,
+ success: true,
+ });
+ span.end();
+ return result;
+ } catch (error) {
+ span.recordException(error as Error);
+ span.setAttributes({ error: true });
+ span.end();
+ throw error;
+ }
+ }),
+ onSuccess: (result, variables, context) => {
+ // Invalidate relevant queries to refresh data
+ queryClient.invalidateQueries({ queryKey: queryKeys.transactions() });
+ queryClient.invalidateQueries({ queryKey: queryKeys.categories });
+ userOnSuccess?.(result, variables, context);
+ },
+ onError: (error, variables, context) => {
+ userOnError?.(error, variables, context);
+ },
+ ...rest,
+ });
+}
diff --git a/src/lib/routes.ts b/src/lib/routes.ts
index b9438fe..1ee98e0 100644
--- a/src/lib/routes.ts
+++ b/src/lib/routes.ts
@@ -8,7 +8,7 @@ export interface RouteConfig {
}
// Currently accessible routes
-export const enabledRoutes = ["/", "/settings", "/income"];
+export const enabledRoutes = ["/", "/settings", "/income", "/expenses"];
export const routes: RouteConfig[] = [
// Budget Control Items