"use client"; import { useState } from "react"; import { useCreateTransaction, useParticipants, useTags } from "@/lib/hooks"; import { CATEGORIES, formatCategory } from "@/lib/categories"; import { PAYMENT_METHODS, PAYMENT_METHOD_LABELS, NON_RECONCILING_REASON, } from "@/lib/payment-methods"; const TRANSACTION_TYPES = ["debit", "credit", "payment", "refund", "fee", "interest", "transfer"]; interface Prefill { date?: string; description?: string; amount?: number; transaction_type?: string; merchant_normalized?: string; category?: string; splits?: { participant_id: number; share_percent: number }[]; } export function AddTransactionModal({ prefill, title, onClose, }: { prefill?: Prefill; title?: string; onClose: () => void; }) { const createTransaction = useCreateTransaction(); const { data: participants = [] } = useParticipants(); const { data: allTags = [] } = useTags(); const [date, setDate] = useState(prefill?.date ?? new Date().toISOString().slice(0, 10)); const [description, setDescription] = useState(prefill?.description ?? ""); const [amount, setAmount] = useState(prefill?.amount != null ? String(prefill.amount) : ""); const [type, setType] = useState(prefill?.transaction_type ?? "debit"); const [merchant, setMerchant] = useState(prefill?.merchant_normalized ?? ""); const [category, setCategory] = useState(prefill?.category ?? ""); // Cash and gift-card credits are excluded from reconciliation — neither will // ever produce a card line. See needsCardMatch() in queries.ts. const [paymentMethod, setPaymentMethod] = useState(""); const [selectedTagIds, setSelectedTagIds] = useState([]); const [splits, setSplits] = useState<{ participant_id: number; share_percent: number }[]>( prefill?.splits ?? [] ); function addSplit() { if (!participants.length) return; setSplits([...splits, { participant_id: participants[0].id, share_percent: 50 }]); } function updateSplit(i: number, patch: Partial<{ participant_id: number; share_percent: number }>) { setSplits(splits.map((s, idx) => (idx === i ? { ...s, ...patch } : s))); } function removeSplit(i: number) { setSplits(splits.filter((_, idx) => idx !== i)); } function toggleTag(id: number) { setSelectedTagIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] ); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const result = await createTransaction.mutateAsync({ date, description, amount: parseFloat(amount), transaction_type: type, merchant_normalized: merchant || undefined, category: category || undefined, payment_method: paymentMethod || undefined, splits: splits.length ? splits : undefined, }); if (selectedTagIds.length && result?.id) { await Promise.all( selectedTagIds.map((tagId) => fetch(`/api/transactions/${result.id}/tags`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tag_id: tagId }), }) ) ); } onClose(); } const splitTotal = splits.reduce((s, e) => s + (e.share_percent || 0), 0); return (
e.stopPropagation()} >

{title ?? "Add Transaction"}

setDate(e.target.value)} className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" />
setAmount(e.target.value)} placeholder="0.00" className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" />
setDescription(e.target.value)} placeholder="e.g. Coles Wyndham Vale" className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" />
{/* "Payment method", not "Paid by" — the edit modal uses that label for the owner, and they are different questions: which instrument the money left by, versus whose money it was. */} {NON_RECONCILING_REASON[paymentMethod] && (

{NON_RECONCILING_REASON[paymentMethod]}

)}
setMerchant(e.target.value)} placeholder="Normalized merchant name" className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" />
{/* Tags */} {allTags.length > 0 && (
{allTags.map((tag) => { const selected = selectedTagIds.includes(tag.id); return ( ); })}
)} {/* Splits */}
{participants.length > 0 && ( )}
{splits.map((s, i) => (
updateSplit(i, { share_percent: Number(e.target.value) })} className="w-16 bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" /> %
))}
); }