- {/* Header/Columns Format i.e "Date, Transaction"... */}
-
- {/* collumn to be date and column to be amount and column to be category */}
-
-
- Date Column
-
-
+
+
+
-
- Amount Column
-
-
+ Description Column (Optional)
+
+
+ setImportForm((prev) => ({
+ ...prev,
+ descriptionColumn: e.target.value,
+ }))
+ }
+ placeholder="e.g., Description"
/>
-
-
- Category Column
-
-
+
+
+
+ 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
+
-
- Import
-
)}
+
+ {/* 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()
+ )}
+
+
+
+
);
}
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,
+ });
+}
From 774c83e5237503440e53c0fa99b0a8c87bd5659f Mon Sep 17 00:00:00 2001
From: Mohamed Omar
Date: Sat, 6 Sep 2025 15:05:56 +0100
Subject: [PATCH 4/5] Checkpoint from VS Code for coding agent session
---
sample_expenses.csv | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 sample_expenses.csv
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,,
From d3f53688381e28555dc18aa3987c651a46a931d6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 14:06:00 +0000
Subject: [PATCH 5/5] Initial plan