mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50: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,
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
|
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 { cn } from "@/lib/utils";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { money, fmtDate } from "./lib/format";
|
import { money, fmtDate } from "./lib/format";
|
||||||
@@ -93,6 +93,7 @@ export default function AccountingReconcileDetailPage() {
|
|||||||
|
|
||||||
// Add a deposit/withdrawal directly from the reconciliation screen.
|
// Add a deposit/withdrawal directly from the reconciliation screen.
|
||||||
const [addOpen, setAddOpen] = useState(false);
|
const [addOpen, setAddOpen] = useState(false);
|
||||||
|
const [editId, setEditId] = useState<string | null>(null);
|
||||||
const [addSaving, setAddSaving] = 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; vendor_id: string }>(
|
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: "" },
|
{ type: "credit", date: "", amount: "", description: "", coa_account_id: "", reference: "", vendor_id: "" },
|
||||||
@@ -156,7 +157,7 @@ export default function AccountingReconcileDetailPage() {
|
|||||||
// and not voided.
|
// and not voided.
|
||||||
const { data } = await accounting
|
const { data } = await accounting
|
||||||
.from("transactions")
|
.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)
|
.eq("account_id", accountId)
|
||||||
.is("reconciliation_id", null)
|
.is("reconciliation_id", null)
|
||||||
.eq("voided", false)
|
.eq("voided", false)
|
||||||
@@ -346,6 +347,7 @@ export default function AccountingReconcileDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openAdd = (type: "credit" | "debit") => {
|
const openAdd = (type: "credit" | "debit") => {
|
||||||
|
setEditId(null);
|
||||||
setAddTx({
|
setAddTx({
|
||||||
type,
|
type,
|
||||||
date: active?.statement_end_date ?? new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }),
|
date: active?.statement_end_date ?? new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }),
|
||||||
@@ -354,16 +356,53 @@ export default function AccountingReconcileDetailPage() {
|
|||||||
setAddOpen(true);
|
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 () => {
|
const addTransaction = async () => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
const amt = Number(addTx.amount);
|
const amt = Number(addTx.amount);
|
||||||
if (!amt || amt <= 0) return toast.error("Enter an amount");
|
if (!amt || amt <= 0) return toast.error("Enter an amount");
|
||||||
if (!addTx.description.trim()) return toast.error("Enter a description");
|
if (!addTx.description.trim()) return toast.error("Enter a description");
|
||||||
if (!addTx.coa_account_id) return toast.error("Pick a category account");
|
// Category/vendor are required when creating; on edit they're optional so
|
||||||
if (addTx.type === "debit" && !addTx.vendor_id) return toast.error("Vendor is required for withdrawals");
|
// 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);
|
setAddSaving(true);
|
||||||
try {
|
try {
|
||||||
const coaName = (allAccounts as any[]).find((a) => a.id === addTx.coa_account_id)?.name ?? "";
|
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
|
const { data, error } = await accounting
|
||||||
.from("transactions")
|
.from("transactions")
|
||||||
.insert({
|
.insert({
|
||||||
@@ -553,7 +592,7 @@ export default function AccountingReconcileDetailPage() {
|
|||||||
<SortHead col="reference" label="Ref #" sort={sort} onSort={toggleSort} />
|
<SortHead col="reference" label="Ref #" sort={sort} onSort={toggleSort} />
|
||||||
<SortHead col="deposit" label="Deposit" sort={sort} onSort={toggleSort} align="right" />
|
<SortHead col="deposit" label="Deposit" sort={sort} onSort={toggleSort} align="right" />
|
||||||
<SortHead col="withdrawal" label="Withdrawal" 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>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -587,10 +626,16 @@ export default function AccountingReconcileDetailPage() {
|
|||||||
{t.type === "debit" ? money(t.amount, cur) : ""}
|
{t.type === "debit" ? money(t.amount, cur) : ""}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<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"
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||||
title="Void this transaction" onClick={() => voidTx(t)}>
|
title="Void this transaction" onClick={() => voidTx(t)}>
|
||||||
<Ban className="h-4 w-4" />
|
<Ban className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
@@ -787,10 +832,11 @@ export default function AccountingReconcileDetailPage() {
|
|||||||
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add {addTx.type === "credit" ? "Deposit" : "Withdrawal"}</DialogTitle>
|
<DialogTitle>{editId ? "Edit" : "Add"} {addTx.type === "credit" ? "Deposit" : "Withdrawal"}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Record a {addTx.type === "credit" ? "deposit (money in)" : "withdrawal (money out)"} on{" "}
|
{editId
|
||||||
{(account as any)?.name ?? "this account"}. It's added to this reconciliation automatically.
|
? <>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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -845,7 +891,7 @@ export default function AccountingReconcileDetailPage() {
|
|||||||
<Button variant="ghost" onClick={() => setAddOpen(false)}>Cancel</Button>
|
<Button variant="ghost" onClick={() => setAddOpen(false)}>Cancel</Button>
|
||||||
<Button onClick={addTransaction} disabled={addSaving}>
|
<Button onClick={addTransaction} disabled={addSaving}>
|
||||||
{addSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{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>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user