"use client"; import { useState } from "react"; import { useUpdateTransaction, useTags, useAddTransactionTag, useRemoveTransactionTag, useTransactionSplits, useTrips, useParticipants, useCurrentUser, } from "@/lib/hooks"; import { SplitModal } from "./split-modal"; import { OrderDetails } from "./order-details"; import { CATEGORIES, formatCategory } from "@/lib/categories"; import type { TransactionRow, TagRow } from "@/lib/queries"; const TRANSACTION_TYPES = ["debit", "credit", "payment", "refund", "fee", "interest", "transfer"]; const SPEND_TYPES = new Set(["debit", "fee", "interest"]); function formatAmount(amount: number, type: string) { const formatted = `$${Number(amount).toFixed(2)}`; return SPEND_TYPES.has(type) ? formatted : `+${formatted}`; } function InlineTags({ transactionId, initialTags }: { transactionId: number; initialTags: TagRow[] }) { const { data: allTags = [] } = useTags(); const addTag = useAddTransactionTag(); const removeTag = useRemoveTransactionTag(); const [tags, setTags] = useState(initialTags); const [showPicker, setShowPicker] = useState(false); const available = allTags.filter((t) => !tags.find((ct) => ct.id === t.id)); return (
{tags.map((tag) => ( {tag.name} ))} {available.length > 0 && ( )}
{showPicker && (
{available.map((tag) => ( ))}
)}
); } export function EditTransactionModal({ transaction, onClose, }: { transaction: TransactionRow; onClose: () => void; }) { const isManual = !transaction.statement_id; const updateTxn = useUpdateTransaction(); const { data: trips = [] } = useTrips(); const { data: participants = [] } = useParticipants(); const { data: me } = useCurrentUser(); // Editable override fields const [merchant, setMerchant] = useState(transaction.merchant_override ?? transaction.merchant_normalized ?? ""); const [category, setCategory] = useState(transaction.effective_category ?? ""); const [type, setType] = useState(transaction.transaction_type); const [notes, setNotes] = useState(transaction.notes ?? ""); // Manual-only direct fields const [date, setDate] = useState(transaction.transaction_date?.slice(0, 10) ?? ""); const [description, setDescription] = useState(transaction.description); const [amount, setAmount] = useState(String(transaction.amount)); const [tripId, setTripId] = useState(transaction.trip_id ?? null); const [ownerId, setOwnerId] = useState(transaction.owner_id ?? null); // Splits — live via hook so they refresh after SplitModal saves const { data: liveSplits = [] } = useTransactionSplits(transaction.id); const [showSplitModal, setShowSplitModal] = useState(false); const [error, setError] = useState(""); // Set when the API refuses an owner change that would hide the row from me. // Holding it in state (rather than confirm()) keeps the way out — add a // split — one click away instead of behind a dialog. const [releasePrompt, setReleasePrompt] = useState(false); const ownerChanged = ownerId !== null && ownerId !== (transaction.owner_id ?? null); const iHoldASplit = liveSplits.some( (s: { participant_id: number }) => s.participant_id === me?.id ); async function handleSave(release = false) { setError(""); try { const patch: Parameters[0] = { id: transaction.id }; // Override fields (always) if (merchant !== (transaction.merchant_override ?? transaction.merchant_normalized ?? "")) patch.merchant_normalized = merchant; if (category !== (transaction.effective_category ?? "")) patch.category = category; if (type !== transaction.transaction_type) patch.transaction_type = type; if (notes !== (transaction.notes ?? "")) patch.notes = notes; // Direct fields (manual only) if (isManual) { if (date !== transaction.transaction_date?.slice(0, 10)) patch.transaction_date = date; if (description !== transaction.description) patch.description = description; if (parseFloat(amount) !== transaction.amount) patch.amount = parseFloat(amount); } if (tripId !== (transaction.trip_id ?? null)) patch.trip_id = tripId; // Owner is manual-only: a statement row's owner comes from its statement. if (isManual && ownerChanged) { patch.owner_id = ownerId!; if (release) patch.release = true; } await updateTxn.mutateAsync(patch); onClose(); } catch (e) { if ((e as { code?: string })?.code === "would_lose_access") { setReleasePrompt(true); } setError(e instanceof Error ? e.message : "Failed to save"); } } return ( <>
e.stopPropagation()} > {/* Header */}

Edit Transaction

{transaction.bank_name}

{/* Core fields — read-only for statement, editable for manual */} {isManual ? (
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)} className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" />
setDescription(e.target.value)} className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm" />
) : (

{transaction.description}

{formatAmount(transaction.amount, transaction.transaction_type)}

{new Date(transaction.transaction_date).toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" })}

)} {/* Override fields */}
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" />