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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { money, fmtDate } from "./lib/format";
|
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 { EmptyState } from "./components/EmptyState";
|
||||||
import { ensureUndepositedFunds } from "./lib/undeposited";
|
import { ensureUndepositedFunds } from "./lib/undeposited";
|
||||||
|
|
||||||
@@ -32,6 +33,13 @@ export default function AccountingDepositsPage() {
|
|||||||
const [lines, setLines] = useState<ManualLine[]>([]);
|
const [lines, setLines] = useState<ManualLine[]>([]);
|
||||||
const [saving, setSaving] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!cid) return;
|
if (!cid) return;
|
||||||
ensureUndepositedFunds(cid).then((id) => setUndepositedId(id)).catch(() => {});
|
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 ?? [],
|
(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
|
// Two sources of "awaiting deposit": transactions parked on the Undeposited
|
||||||
// Funds account (banking flow) and payments_received not yet deposited (incl.
|
// Funds account (banking flow) and payments_received not yet deposited (incl.
|
||||||
// payments synced from the main app's owner ledger). Both are unified below.
|
// 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 (!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 (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>;
|
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)}`}
|
{saving ? "Recording…" : `Deposit ${money(grandTotal, cur)}`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user