mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Reconcile screen: edit transactions inline
Add an edit (pencil) action to each row in the reconciliation working list, reusing the add dialog in edit mode. On edit, category/vendor are optional (so imported GL register rows can be edited without forcing re-categorization); category/coa are only overwritten when a category is chosen. 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, Plus, Ban } from "lucide-react";
|
||||
import { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown, Plus, Ban, Pencil } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import { money, fmtDate } from "./lib/format";
|
||||
@@ -93,6 +93,7 @@ export default function AccountingReconcileDetailPage() {
|
||||
|
||||
// Add a deposit/withdrawal directly from the reconciliation screen.
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [editId, setEditId] = useState<string | null>(null);
|
||||
const [addSaving, setAddSaving] = useState(false);
|
||||
const [addTx, setAddTx] = useState<{ type: "credit" | "debit"; date: string; amount: string; description: string; coa_account_id: string; reference: string; vendor_id: string }>(
|
||||
{ type: "credit", date: "", amount: "", description: "", coa_account_id: "", reference: "", vendor_id: "" },
|
||||
@@ -156,7 +157,7 @@ export default function AccountingReconcileDetailPage() {
|
||||
// and not voided.
|
||||
const { data } = await accounting
|
||||
.from("transactions")
|
||||
.select("id,date,description,reference,amount,type,cleared,reconciliation_id,voided,vendors(name),customers(name)")
|
||||
.select("id,date,description,reference,amount,type,cleared,reconciliation_id,voided,coa_account_id,vendor_id,category,vendors(name),customers(name)")
|
||||
.eq("account_id", accountId)
|
||||
.is("reconciliation_id", null)
|
||||
.eq("voided", false)
|
||||
@@ -346,6 +347,7 @@ export default function AccountingReconcileDetailPage() {
|
||||
};
|
||||
|
||||
const openAdd = (type: "credit" | "debit") => {
|
||||
setEditId(null);
|
||||
setAddTx({
|
||||
type,
|
||||
date: active?.statement_end_date ?? new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }),
|
||||
@@ -354,16 +356,53 @@ export default function AccountingReconcileDetailPage() {
|
||||
setAddOpen(true);
|
||||
};
|
||||
|
||||
// Edit an existing transaction from the reconciliation working list.
|
||||
const openEdit = (t: any) => {
|
||||
setEditId(t.id);
|
||||
setAddTx({
|
||||
type: t.type,
|
||||
date: t.date,
|
||||
amount: String(Math.abs(Number(t.amount))),
|
||||
description: t.description ?? "",
|
||||
coa_account_id: t.coa_account_id ?? "",
|
||||
reference: t.reference ?? "",
|
||||
vendor_id: t.vendor_id ?? "",
|
||||
});
|
||||
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");
|
||||
if (addTx.type === "debit" && !addTx.vendor_id) return toast.error("Vendor is required for withdrawals");
|
||||
// Category/vendor are required when creating; on edit they're optional so
|
||||
// imported (GL) register rows can be edited without re-categorizing.
|
||||
if (!editId && !addTx.coa_account_id) return toast.error("Pick a category account");
|
||||
if (!editId && addTx.type === "debit" && !addTx.vendor_id) return toast.error("Vendor is required for withdrawals");
|
||||
setAddSaving(true);
|
||||
try {
|
||||
const coaName = (allAccounts as any[]).find((a) => a.id === addTx.coa_account_id)?.name ?? "";
|
||||
if (editId) {
|
||||
// Only overwrite category/coa when a category was chosen, so we don't
|
||||
// wipe an imported row's existing category text.
|
||||
const payload: any = {
|
||||
date: addTx.date,
|
||||
description: addTx.description.trim(),
|
||||
amount: Math.abs(amt),
|
||||
type: addTx.type,
|
||||
reference: addTx.reference.trim() || null,
|
||||
vendor_id: addTx.vendor_id || null,
|
||||
};
|
||||
if (addTx.coa_account_id) { payload.coa_account_id = addTx.coa_account_id; payload.category = coaName; }
|
||||
const { error } = await accounting.from("transactions").update(payload).eq("id", editId);
|
||||
if (error) return toast.error(error.message);
|
||||
setAddOpen(false);
|
||||
toast.success("Transaction updated");
|
||||
qc.invalidateQueries({ queryKey: ["recon-txs", accountId] });
|
||||
qc.invalidateQueries({ queryKey: ["transactions", cid] });
|
||||
return;
|
||||
}
|
||||
const { data, error } = await accounting
|
||||
.from("transactions")
|
||||
.insert({
|
||||
@@ -553,7 +592,7 @@ export default function AccountingReconcileDetailPage() {
|
||||
<SortHead col="reference" label="Ref #" sort={sort} onSort={toggleSort} />
|
||||
<SortHead col="deposit" label="Deposit" sort={sort} onSort={toggleSort} align="right" />
|
||||
<SortHead col="withdrawal" label="Withdrawal" sort={sort} onSort={toggleSort} align="right" />
|
||||
<TableHead className="w-10 text-right">Void</TableHead>
|
||||
<TableHead className="w-16 text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -587,10 +626,16 @@ export default function AccountingReconcileDetailPage() {
|
||||
{t.type === "debit" ? money(t.amount, cur) : ""}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-primary"
|
||||
title="Edit this transaction" onClick={() => openEdit(t)}>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||
title="Void this transaction" onClick={() => voidTx(t)}>
|
||||
<Ban className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
@@ -787,10 +832,11 @@ export default function AccountingReconcileDetailPage() {
|
||||
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add {addTx.type === "credit" ? "Deposit" : "Withdrawal"}</DialogTitle>
|
||||
<DialogTitle>{editId ? "Edit" : "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.
|
||||
{editId
|
||||
? <>Edit this {addTx.type === "credit" ? "deposit" : "withdrawal"} on {(account as any)?.name ?? "this account"}.</>
|
||||
: <>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">
|
||||
@@ -845,7 +891,7 @@ export default function AccountingReconcileDetailPage() {
|
||||
<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"}
|
||||
{editId ? "Save Changes" : `Add ${addTx.type === "credit" ? "Deposit" : "Withdrawal"}`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user