From 084b8764e3f59031ff53fc6248a1123672595ed4 Mon Sep 17 00:00:00 2001 From: siddharthd Date: Sat, 14 Mar 2026 21:20:25 +1100 Subject: [PATCH] feat(transactions): Payment button to record existing transaction as debt payment --- src/app/transactions/page.tsx | 155 +++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx index cff0c05..214e33c 100644 --- a/src/app/transactions/page.tsx +++ b/src/app/transactions/page.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useRef, useEffect, Suspense } from "react"; import { useSearchParams } from "next/navigation"; -import { useTransactions, useBanks, useUpdateTransaction, useBulkAction, useTags, useStatement, useCreateRule } from "@/lib/hooks"; +import { useTransactions, useBanks, useUpdateTransaction, useBulkAction, useTags, useStatement, useCreateRule, useParticipants, useRecordPayment, useCurrentUser } from "@/lib/hooks"; import { CATEGORIES, formatCategory } from "@/lib/categories"; import { SplitModal } from "@/components/split-modal"; import { TagPicker } from "@/components/tag-picker"; @@ -206,6 +206,142 @@ function InlineEdit({ ); } +// ── Mark as Payment modal ───────────────────────────────────────────────────── +function MarkAsPaymentModal({ + transaction, + onClose, +}: { + transaction: TransactionRow; + onClose: () => void; +}) { + const { data: participants = [] } = useParticipants(); + const { data: me } = useCurrentUser(); + const record = useRecordPayment(); + + const others = participants.filter((p) => p.name !== "Me"); + + const [participantId, setParticipantId] = useState(others[0]?.id ?? ""); + // For credits/refunds the default direction is "they paid me" + const [direction, setDirection] = useState<"received" | "sent">( + SPEND_TYPES.has(transaction.transaction_type) ? "sent" : "received" + ); + const [amount, setAmount] = useState(transaction.amount.toFixed(2)); + const [date, setDate] = useState(transaction.transaction_date.slice(0, 10)); + const [notes, setNotes] = useState(""); + const [error, setError] = useState(""); + + const selectedParticipant = others.find((p) => p.id === participantId); + + async function handleSave() { + setError(""); + if (!participantId || !me) { setError("Select a participant"); return; } + const amt = parseFloat(amount); + if (!amt || amt <= 0) { setError("Enter a valid amount"); return; } + try { + await record.mutateAsync({ + from_participant_id: direction === "received" ? participantId : me.id, + to_participant_id: direction === "received" ? me.id : participantId, + amount: amt, + payment_date: date, + notes: notes || undefined, + linked_transaction_id: transaction.id, + }); + onClose(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to record"); + } + } + + return ( +
+
e.stopPropagation()} + > +
+

Record as Debt Payment

+

{transaction.description}

+
+ + {/* Participant */} +
+ + +
+ + {/* Direction */} +
+ + +
+ +
+
+ +
+ $ + setAmount(e.target.value)} + className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm pl-6" + /> +
+
+
+ + setDate(e.target.value)} + className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" + /> +
+
+ +
+ + setNotes(e.target.value)} + placeholder="e.g. bank transfer reference" + className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" + /> +
+ + {error &&

{error}

} + +
+ + +
+
+
+ ); +} + // ── Query bar parser ────────────────────────────────────────────────────────── interface QueryToken { key: string; label: string } interface ParsedQuery { text: string; amountMin?: number; amountMax?: number; tokens: QueryToken[] } @@ -365,6 +501,7 @@ function TransactionsContent() { const [splitModal, setSplitModal] = useState<{ transactionId?: number; transactionIds?: number[]; amount?: number; description: string; merchant?: string } | null>(null); const [addModal, setAddModal] = useState<{ prefill?: Parameters[0]["prefill"]; title?: string } | null>(null); const [editModal, setEditModal] = useState(null); + const [paymentModal, setPaymentModal] = useState(null); const [rulePrompt, setRulePrompt] = useState<{ tx: { id: number; effective_merchant: string; description: string; bank_name: string }; field: "category" | "merchant"; @@ -738,6 +875,15 @@ function TransactionsContent() { > Edit + {!SPEND_TYPES.has(t.transaction_type) && ( + + )}