Accounting: editable deposits (Recent Deposits list + edit/delete)

Add a "Recent Deposits" list to the Deposits page with Edit (date, bank
account, memo — GL re-posts via the deposit trigger) and Delete (releases the
deposited transactions/payments back to "awaiting deposit" and reverses the GL).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 19:57:10 -04:00
parent 8a2ea60824
commit f315d86e03
+129 -1
View File
@@ -9,9 +9,10 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { toast } from "sonner";
import { money, fmtDate } from "./lib/format";
import { Landmark, Loader2, Plus, Trash2 } from "lucide-react";
import { Landmark, Loader2, Plus, Trash2, Pencil } from "lucide-react";
import { EmptyState } from "./components/EmptyState";
import { ensureUndepositedFunds } from "./lib/undeposited";
@@ -32,6 +33,13 @@ export default function AccountingDepositsPage() {
const [lines, setLines] = useState<ManualLine[]>([]);
const [saving, setSaving] = useState(false);
// Edit-existing-deposit state
const [editDep, setEditDep] = useState<any | null>(null);
const [editDate, setEditDate] = useState("");
const [editBankId, setEditBankId] = useState("");
const [editMemo, setEditMemo] = useState("");
const [savingEdit, setSavingEdit] = useState(false);
useEffect(() => {
if (!cid) return;
ensureUndepositedFunds(cid).then((id) => setUndepositedId(id)).catch(() => {});
@@ -53,6 +61,13 @@ export default function AccountingDepositsPage() {
(await accounting.from("accounts").select("id,name,code,type,balance").eq("company_id", cid).order("type").order("code")).data ?? [],
});
const { data: recentDeposits = [] } = useQuery({
queryKey: ["recent-deposits", cid],
enabled: !!cid,
queryFn: async () =>
(await accounting.from("deposits").select("id,date,amount,memo,bank_account_id").eq("company_id", cid).order("date", { ascending: false }).limit(50)).data ?? [],
});
// Two sources of "awaiting deposit": transactions parked on the Undeposited
// Funds account (banking flow) and payments_received not yet deposited (incl.
// payments synced from the main app's owner ledger). Both are unified below.
@@ -206,6 +221,53 @@ export default function AccountingDepositsPage() {
}
};
const bankName = (id: string) => (bankAccounts as any[]).find((a) => a.id === id)?.name ?? "—";
const refreshDeposits = () => {
["undeposited-tx", "undeposited-pmt", "bank-accounts", "all-accounts", "accounts", "transactions", "recent-deposits"]
.forEach((k) => qc.invalidateQueries({ queryKey: [k, cid] }));
};
const openEditDeposit = (d: any) => {
setEditDep(d); setEditDate(d.date); setEditBankId(d.bank_account_id); setEditMemo(d.memo ?? "");
};
const saveEditDeposit = async () => {
if (!editDep) return;
setSavingEdit(true);
try {
const { error } = await accounting.from("deposits")
.update({ date: editDate, bank_account_id: editBankId, memo: editMemo || null })
.eq("id", editDep.id);
if (error) throw new Error(error.message);
// keep linked payments' bank account in sync (the deposit GL re-posts via trigger)
await accounting.from("payments_received").update({ bank_account_id: editBankId }).eq("deposit_id", editDep.id);
toast.success("Deposit updated");
setEditDep(null);
refreshDeposits();
} catch (e: any) {
toast.error(e?.message ?? "Failed to update deposit");
} finally {
setSavingEdit(false);
}
};
const deleteDeposit = async (d: any) => {
if (!confirm(`Delete the deposit of ${money(Number(d.amount), cur)} on ${fmtDate(d.date)}? Its items return to "awaiting deposit".`)) return;
try {
// Release the deposited items so they return to the queue, then remove the deposit.
await accounting.from("transactions").update({ deposit_id: null }).eq("deposit_id", d.id);
await accounting.from("payments_received").update({ deposited: false, deposit_id: null }).eq("deposit_id", d.id);
await accounting.from("deposit_lines").delete().eq("deposit_id", d.id);
const { error } = await accounting.from("deposits").delete().eq("id", d.id);
if (error) throw new Error(error.message);
toast.success("Deposit deleted");
refreshDeposits();
} catch (e: any) {
toast.error(e?.message ?? "Failed to delete deposit");
}
};
if (!associationId) return <p className="text-sm text-muted-foreground">Select an association.</p>;
if (companyLoading) return <div className="flex justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>;
if (companyError || !companyId) return <p className="text-sm text-muted-foreground text-center py-12">{companyError || "Accounting setup is not ready."}</p>;
@@ -360,6 +422,72 @@ export default function AccountingDepositsPage() {
{saving ? "Recording…" : `Deposit ${money(grandTotal, cur)}`}
</Button>
</div>
{/* Recent deposits — edit / delete */}
<Card>
<CardHeader><CardTitle className="text-base">Recent deposits</CardTitle></CardHeader>
<CardContent>
{(recentDeposits as any[]).length === 0 ? (
<p className="text-sm text-muted-foreground">No deposits recorded yet.</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Deposited to</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead>Memo</TableHead>
<TableHead className="w-20"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(recentDeposits as any[]).map((d) => (
<TableRow key={d.id}>
<TableCell>{fmtDate(d.date)}</TableCell>
<TableCell>{bankName(d.bank_account_id)}</TableCell>
<TableCell className="text-right tabular-nums">{money(Number(d.amount), cur)}</TableCell>
<TableCell className="text-muted-foreground">{d.memo ?? "—"}</TableCell>
<TableCell>
<div className="flex justify-end gap-1">
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => openEditDeposit(d)} aria-label="Edit deposit"><Pencil className="h-3.5 w-3.5" /></Button>
<Button size="icon" variant="ghost" className="h-7 w-7 text-destructive hover:text-destructive" onClick={() => deleteDeposit(d)} aria-label="Delete deposit"><Trash2 className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
<Dialog open={!!editDep} onOpenChange={(o) => { if (!o) setEditDep(null); }}>
<DialogContent>
<DialogHeader><DialogTitle>Edit deposit</DialogTitle></DialogHeader>
<div className="space-y-3">
<p className="text-xs text-muted-foreground">
Changing the date, bank account, or memo re-posts this deposit's GL entry. To change which items are included, delete it and re-create the deposit.
</p>
<div>
<Label>Deposit to</Label>
<Select value={editBankId} onValueChange={setEditBankId}>
<SelectTrigger><SelectValue placeholder="Bank account" /></SelectTrigger>
<SelectContent>
{(bankAccounts as any[]).map((a) => (
<SelectItem key={a.id} value={a.id}>{a.code ? `${a.code} · ` : ""}{a.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div><Label>Date</Label><Input type="date" value={editDate} onChange={(e) => setEditDate(e.target.value)} /></div>
<div><Label>Memo</Label><Textarea rows={2} value={editMemo} onChange={(e) => setEditMemo(e.target.value)} maxLength={200} /></div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditDep(null)}>Cancel</Button>
<Button onClick={saveEditDeposit} disabled={savingEdit || !editBankId}>{savingEdit ? "Saving…" : "Save changes"}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}