mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
f315d86e03
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>
494 lines
22 KiB
TypeScript
494 lines
22 KiB
TypeScript
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { accounting } from "@/lib/accountingClient";
|
|
import { useCompanyId } from "./lib/useCompanyId";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
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, Pencil } from "lucide-react";
|
|
import { EmptyState } from "./components/EmptyState";
|
|
import { ensureUndepositedFunds } from "./lib/undeposited";
|
|
|
|
type ManualLine = { account_id: string; amount: string; memo: string };
|
|
const EMPTY_LINE: ManualLine = { account_id: "", amount: "", memo: "" };
|
|
|
|
export default function AccountingDepositsPage() {
|
|
const { companyId, loading: companyLoading, error: companyError, associationId } = useCompanyId();
|
|
const cid = companyId ?? "";
|
|
const cur = "USD";
|
|
const qc = useQueryClient();
|
|
|
|
const [undepositedId, setUndepositedId] = useState<string>("");
|
|
const [bankAccountId, setBankAccountId] = useState("");
|
|
const [depositDate, setDepositDate] = useState(new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }));
|
|
const [memo, setMemo] = useState("");
|
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
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(() => {});
|
|
}, [cid]);
|
|
|
|
const { data: bankAccounts = [] } = useQuery({
|
|
queryKey: ["bank-accounts", cid],
|
|
enabled: !!cid,
|
|
queryFn: async () =>
|
|
(await accounting.from("accounts").select("id,name,code,balance").eq("company_id", cid).eq("is_bank", true).order("code")).data ?? [],
|
|
});
|
|
|
|
// All accounts — for the source-account picker on deposit lines (income, A/R,
|
|
// reserve, clearing, etc.), so a deposit isn't forced through Undeposited Funds.
|
|
const { data: allAccounts = [] } = useQuery({
|
|
queryKey: ["all-accounts", cid],
|
|
enabled: !!cid,
|
|
queryFn: async () =>
|
|
(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.
|
|
const { data: pendingTx = [] } = useQuery({
|
|
queryKey: ["undeposited-tx", cid, undepositedId],
|
|
enabled: !!cid && !!undepositedId,
|
|
queryFn: async () => {
|
|
const { data } = await accounting
|
|
.from("transactions")
|
|
.select("*")
|
|
.eq("company_id", cid)
|
|
.eq("account_id", undepositedId)
|
|
.eq("type", "debit")
|
|
.is("deposit_id", null)
|
|
.order("date", { ascending: false });
|
|
return data ?? [];
|
|
},
|
|
});
|
|
|
|
const { data: pendingPmt = [] } = useQuery({
|
|
queryKey: ["undeposited-pmt", cid],
|
|
enabled: !!cid,
|
|
queryFn: async () => {
|
|
const { data } = await accounting
|
|
.from("payments_received")
|
|
.select("id,payment_date,amount,method,reference,memo,customer_id")
|
|
.eq("company_id", cid)
|
|
.eq("deposited", false)
|
|
.order("payment_date", { ascending: false });
|
|
return data ?? [];
|
|
},
|
|
});
|
|
|
|
type PendingRow = { key: string; kind: "tx" | "pmt"; id: string; date: string; description: string; reference: string | null; amount: number };
|
|
|
|
const pending = useMemo<PendingRow[]>(() => {
|
|
const rows: PendingRow[] = [
|
|
...(pendingTx as any[]).map((t) => ({
|
|
key: `tx:${t.id}`, kind: "tx" as const, id: t.id, date: t.date,
|
|
description: t.description, reference: t.reference ?? null, amount: Number(t.amount),
|
|
})),
|
|
...(pendingPmt as any[]).map((p) => ({
|
|
key: `pmt:${p.id}`, kind: "pmt" as const, id: p.id, date: p.payment_date,
|
|
description: [p.method, p.memo].filter(Boolean).join(" · ") || "Customer payment",
|
|
reference: p.reference ?? null, amount: Number(p.amount),
|
|
})),
|
|
];
|
|
return rows.sort((a, b) => b.date.localeCompare(a.date));
|
|
}, [pendingTx, pendingPmt]);
|
|
|
|
const undepositedTotal = useMemo(
|
|
() => pending.filter((r) => selected.has(r.key)).reduce((s, r) => s + r.amount, 0),
|
|
[pending, selected]
|
|
);
|
|
const manualTotal = useMemo(
|
|
() => lines.reduce((s, l) => s + (Number(l.amount) || 0), 0),
|
|
[lines]
|
|
);
|
|
const grandTotal = undepositedTotal + manualTotal;
|
|
|
|
const toggleAll = () => {
|
|
if (selected.size === pending.length) setSelected(new Set());
|
|
else setSelected(new Set(pending.map((r) => r.key)));
|
|
};
|
|
|
|
const addLine = () => setLines((ls) => [...ls, { ...EMPTY_LINE }]);
|
|
const updateLine = (i: number, patch: Partial<ManualLine>) =>
|
|
setLines((ls) => ls.map((l, idx) => (idx === i ? { ...l, ...patch } : l)));
|
|
const removeLine = (i: number) => setLines((ls) => ls.filter((_, idx) => idx !== i));
|
|
|
|
const submitDeposit = async () => {
|
|
if (!bankAccountId) return toast.error("Choose a bank account");
|
|
if (grandTotal <= 0) return toast.error("Add at least one payment or deposit line");
|
|
|
|
// Validate manual lines: each must have an account and a positive amount.
|
|
const cleanLines = lines.filter((l) => l.account_id || Number(l.amount));
|
|
for (const l of cleanLines) {
|
|
if (!l.account_id) return toast.error("Every deposit line needs a source account");
|
|
if (!(Number(l.amount) > 0)) return toast.error("Every deposit line needs a positive amount");
|
|
}
|
|
|
|
// Optional guard: warn (non-blocking) if crediting Undeposited Funds would exceed
|
|
// what is currently held there — i.e. depositing more than is sitting in it.
|
|
const manualToUndeposited = cleanLines
|
|
.filter((l) => l.account_id === undepositedId)
|
|
.reduce((s, l) => s + Number(l.amount), 0);
|
|
const toUndeposited = undepositedTotal + manualToUndeposited;
|
|
if (toUndeposited > 0 && undepositedId) {
|
|
const held = Number((allAccounts as any[]).find((a) => a.id === undepositedId)?.balance ?? 0);
|
|
if (toUndeposited > held + 0.005) {
|
|
toast.warning(`Crediting ${money(toUndeposited, cur)} to Undeposited Funds, which holds ${money(held, cur)}.`);
|
|
}
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
const chosen = pending.filter((r) => selected.has(r.key));
|
|
const txIds = chosen.filter((r) => r.kind === "tx").map((r) => r.id);
|
|
const pmtIds = chosen.filter((r) => r.kind === "pmt").map((r) => r.id);
|
|
|
|
// 1) Deposit header — amount is the sum of all credit lines.
|
|
const { data: dep, error: depErr } = await accounting
|
|
.from("deposits")
|
|
.insert({ company_id: cid, bank_account_id: bankAccountId, date: depositDate, amount: grandTotal, memo: memo || null })
|
|
.select()
|
|
.single();
|
|
if (depErr || !dep) throw new Error(depErr?.message ?? "Failed to create deposit");
|
|
|
|
// 2) Deposit lines (credit side). The selected payments collapse into one
|
|
// Undeposited Funds line; manual lines book to their chosen accounts.
|
|
// accounting.post_deposit_gl posts Dr Bank (total) / Cr each line.
|
|
const lineRows: any[] = [];
|
|
if (undepositedTotal > 0 && undepositedId) {
|
|
lineRows.push({
|
|
deposit_id: dep.id, company_id: cid, account_id: undepositedId,
|
|
amount: undepositedTotal, memo: `Cleared ${chosen.length} payment${chosen.length !== 1 ? "s" : ""}`,
|
|
});
|
|
}
|
|
for (const l of cleanLines) {
|
|
lineRows.push({ deposit_id: dep.id, company_id: cid, account_id: l.account_id, amount: Number(l.amount), memo: l.memo || null });
|
|
}
|
|
if (lineRows.length) {
|
|
const { error: lineErr } = await accounting.from("deposit_lines").insert(lineRows);
|
|
if (lineErr) throw new Error(lineErr.message);
|
|
}
|
|
|
|
// 3) Clear the deposited items from the awaiting-deposit queue.
|
|
if (txIds.length) {
|
|
await accounting.from("transactions").update({ deposit_id: dep.id }).in("id", txIds);
|
|
}
|
|
if (pmtIds.length) {
|
|
await accounting.from("payments_received")
|
|
.update({ deposited: true, deposit_id: dep.id, bank_account_id: bankAccountId })
|
|
.in("id", pmtIds);
|
|
}
|
|
|
|
toast.success(`Deposit of ${money(grandTotal, cur)} recorded`);
|
|
setSelected(new Set());
|
|
setLines([]);
|
|
setMemo("");
|
|
qc.invalidateQueries({ queryKey: ["undeposited-tx", cid] });
|
|
qc.invalidateQueries({ queryKey: ["undeposited-pmt", cid] });
|
|
qc.invalidateQueries({ queryKey: ["bank-accounts", cid] });
|
|
qc.invalidateQueries({ queryKey: ["all-accounts", cid] });
|
|
qc.invalidateQueries({ queryKey: ["accounts", cid] });
|
|
qc.invalidateQueries({ queryKey: ["transactions", cid] });
|
|
} catch (e: any) {
|
|
toast.error(e?.message ?? "Failed");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
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>;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold">Make Deposit</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Deposit customer payments held in Undeposited Funds, or record a deposit straight to an
|
|
income, A/R, or other account by adding deposit lines.
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader><CardTitle className="text-base">Deposit details</CardTitle></CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-3 md:grid-cols-4">
|
|
<div>
|
|
<Label>Deposit to</Label>
|
|
<Select value={bankAccountId} onValueChange={setBankAccountId}>
|
|
<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={depositDate} onChange={(e) => setDepositDate(e.target.value)} />
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<Label>Memo</Label>
|
|
<Textarea rows={1} value={memo} onChange={(e) => setMemo(e.target.value)} maxLength={200} />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle className="text-base">Payments awaiting deposit</CardTitle>
|
|
<div className="text-sm">
|
|
Selected: <b>{money(undepositedTotal, cur)}</b> ({selected.size} of {pending.length})
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-10">
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.size > 0 && selected.size === pending.length}
|
|
onChange={toggleAll}
|
|
/>
|
|
</TableHead>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead>Description</TableHead>
|
|
<TableHead>Reference</TableHead>
|
|
<TableHead className="text-right">Amount</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{pending.map((r) => (
|
|
<TableRow key={r.key} className="cursor-pointer" onClick={() => {
|
|
const s = new Set(selected);
|
|
s.has(r.key) ? s.delete(r.key) : s.add(r.key);
|
|
setSelected(s);
|
|
}}>
|
|
<TableCell><input type="checkbox" checked={selected.has(r.key)} readOnly /></TableCell>
|
|
<TableCell>{fmtDate(r.date)}</TableCell>
|
|
<TableCell>{r.description}</TableCell>
|
|
<TableCell className="text-muted-foreground">{r.reference ?? "—"}</TableCell>
|
|
<TableCell className="text-right tabular-nums">{money(r.amount, cur)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{pending.length === 0 && (
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableCell colSpan={5} className="p-0">
|
|
<EmptyState
|
|
icon={Landmark}
|
|
title="No payments awaiting deposit"
|
|
description="Customer payments received to Undeposited Funds will appear here."
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-base">Other deposit lines</CardTitle>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Book a deposit directly to an account — interest income, a refund, a reimbursement, etc.
|
|
</p>
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={addLine}><Plus className="h-4 w-4 mr-1" />Add line</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{lines.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No additional lines. Add one to deposit to a specific account.</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{lines.map((l, i) => (
|
|
<div key={i} className="grid gap-2 md:grid-cols-12 items-center">
|
|
<div className="md:col-span-5">
|
|
<Select value={l.account_id} onValueChange={(v) => updateLine(i, { account_id: v })}>
|
|
<SelectTrigger><SelectValue placeholder="Source account" /></SelectTrigger>
|
|
<SelectContent>
|
|
{(allAccounts as any[]).map((a) => (
|
|
<SelectItem key={a.id} value={a.id}>
|
|
{a.code ? `${a.code} · ` : ""}{a.name}<span className="text-muted-foreground"> · {a.type}</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="md:col-span-4">
|
|
<Input value={l.memo} placeholder="Memo (optional)" onChange={(e) => updateLine(i, { memo: e.target.value })} />
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<Input
|
|
type="number" inputMode="decimal" step="0.01" placeholder="0.00"
|
|
className="text-right tabular-nums"
|
|
value={l.amount} onChange={(e) => updateLine(i, { amount: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="md:col-span-1 flex justify-end">
|
|
<Button size="icon" variant="ghost" onClick={() => removeLine(i)} aria-label="Remove line">
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="flex items-center justify-end gap-4">
|
|
<div className="text-sm text-muted-foreground">
|
|
{money(undepositedTotal, cur)} from Undeposited + {money(manualTotal, cur)} direct
|
|
</div>
|
|
<Button onClick={submitDeposit} disabled={saving || grandTotal <= 0 || !bankAccountId}>
|
|
{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>
|
|
);
|
|
}
|