mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user