mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Reconciliation: add deposits/withdrawals directly from the reconcile screen
- Deposit / Withdrawal buttons in the statement-transactions header open a dialog (date, amount, description, income/expense category, reference) - New transaction posts to the bank account, is marked cleared, and is auto-checked into the current reconciliation; category picker excludes bank and archived accounts. gl-managed companies post the offset via the existing transaction GL trigger; import-mode books add register-only (consistent with Banking), so imported GL isn't double-counted Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
|
||||
import { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown } from "lucide-react";
|
||||
import { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown, Plus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import { money, fmtDate } from "./lib/format";
|
||||
@@ -87,6 +87,13 @@ export default function AccountingReconcileDetailPage() {
|
||||
const [adjAccount, setAdjAccount] = useState("");
|
||||
const [adjNote, setAdjNote] = useState("Bank reconciliation adjustment");
|
||||
|
||||
// Add a deposit/withdrawal directly from the reconciliation screen.
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [addSaving, setAddSaving] = useState(false);
|
||||
const [addTx, setAddTx] = useState<{ type: "credit" | "debit"; date: string; amount: string; description: string; coa_account_id: string; reference: string }>(
|
||||
{ type: "credit", date: "", amount: "", description: "", coa_account_id: "", reference: "" },
|
||||
);
|
||||
|
||||
const { data: account } = useQuery({
|
||||
queryKey: ["account", accountId],
|
||||
enabled: !!accountId,
|
||||
@@ -118,7 +125,7 @@ export default function AccountingReconcileDetailPage() {
|
||||
queryKey: ["accounts", cid],
|
||||
enabled: !!cid,
|
||||
queryFn: async () =>
|
||||
(await accounting.from("accounts").select("id,name,type").eq("company_id", cid).eq("is_archived", false).order("name")).data ?? [],
|
||||
(await accounting.from("accounts").select("id,name,type,is_bank").eq("company_id", cid).eq("is_archived", false).order("name")).data ?? [],
|
||||
});
|
||||
|
||||
const { data: txs = [] } = useQuery({
|
||||
@@ -317,6 +324,51 @@ export default function AccountingReconcileDetailPage() {
|
||||
setChecked((prev) => new Set(prev).add(data.id));
|
||||
};
|
||||
|
||||
const openAdd = (type: "credit" | "debit") => {
|
||||
setAddTx({
|
||||
type,
|
||||
date: active?.statement_end_date ?? new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }),
|
||||
amount: "", description: "", coa_account_id: "", reference: "",
|
||||
});
|
||||
setAddOpen(true);
|
||||
};
|
||||
|
||||
const addTransaction = async () => {
|
||||
if (!active) return;
|
||||
const amt = Number(addTx.amount);
|
||||
if (!amt || amt <= 0) return toast.error("Enter an amount");
|
||||
if (!addTx.description.trim()) return toast.error("Enter a description");
|
||||
if (!addTx.coa_account_id) return toast.error("Pick a category account");
|
||||
setAddSaving(true);
|
||||
try {
|
||||
const coaName = (allAccounts as any[]).find((a) => a.id === addTx.coa_account_id)?.name ?? "";
|
||||
const { data, error } = await accounting
|
||||
.from("transactions")
|
||||
.insert({
|
||||
company_id: cid,
|
||||
account_id: accountId,
|
||||
date: addTx.date,
|
||||
description: addTx.description.trim(),
|
||||
amount: Math.abs(amt),
|
||||
type: addTx.type, // credit = deposit (money in), debit = withdrawal (money out)
|
||||
category: coaName,
|
||||
coa_account_id: addTx.coa_account_id,
|
||||
reference: addTx.reference.trim() || null,
|
||||
cleared: true,
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
if (error || !data) return toast.error(error?.message ?? "Failed to add transaction");
|
||||
setAddOpen(false);
|
||||
toast.success(`${addTx.type === "credit" ? "Deposit" : "Withdrawal"} added`);
|
||||
qc.invalidateQueries({ queryKey: ["recon-txs", accountId] });
|
||||
qc.invalidateQueries({ queryKey: ["transactions", cid] });
|
||||
setChecked((prev) => new Set(prev).add(data.id)); // auto-clear into this reconciliation
|
||||
} finally {
|
||||
setAddSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
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>;
|
||||
@@ -370,6 +422,12 @@ export default function AccountingReconcileDetailPage() {
|
||||
<SelectItem value="withdrawals">Withdrawals only</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button size="sm" variant="outline" className="h-9 gap-1 text-emerald-700" onClick={() => openAdd("credit")}>
|
||||
<Plus className="h-3.5 w-3.5" /> Deposit
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="h-9 gap-1 text-red-700" onClick={() => openAdd("debit")}>
|
||||
<Plus className="h-3.5 w-3.5" /> Withdrawal
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -590,6 +648,60 @@ export default function AccountingReconcileDetailPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Add deposit / withdrawal modal */}
|
||||
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add {addTx.type === "credit" ? "Deposit" : "Withdrawal"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Record a {addTx.type === "credit" ? "deposit (money in)" : "withdrawal (money out)"} on{" "}
|
||||
{(account as any)?.name ?? "this account"}. It's added to this reconciliation automatically.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>Date</Label>
|
||||
<Input type="date" value={addTx.date} onChange={(e) => setAddTx({ ...addTx, date: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Amount</Label>
|
||||
<Input type="number" step="0.01" min="0" value={addTx.amount} onChange={(e) => setAddTx({ ...addTx, amount: e.target.value })} placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Input value={addTx.description} onChange={(e) => setAddTx({ ...addTx, description: e.target.value })}
|
||||
placeholder={addTx.type === "credit" ? "e.g. Interest earned" : "e.g. Bank service charge"} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>{addTx.type === "credit" ? "Income" : "Expense"} account (category)</Label>
|
||||
<Select value={addTx.coa_account_id} onValueChange={(v) => setAddTx({ ...addTx, coa_account_id: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="Select account" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(allAccounts as any[])
|
||||
.filter((a: any) => a.id !== accountId && !a.is_bank)
|
||||
.map((a: any) => (
|
||||
<SelectItem key={a.id} value={a.id}>{a.name} ({a.type})</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Reference (optional)</Label>
|
||||
<Input value={addTx.reference} onChange={(e) => setAddTx({ ...addTx, reference: e.target.value })} placeholder="Check # / memo" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setAddOpen(false)}>Cancel</Button>
|
||||
<Button onClick={addTransaction} disabled={addSaving}>
|
||||
{addSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Add {addTx.type === "credit" ? "Deposit" : "Withdrawal"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Adjustment modal */}
|
||||
<Dialog open={adjOpen} onOpenChange={setAdjOpen}>
|
||||
<DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user