Reconciliation: require a vendor on manual withdrawals

The Add Deposit/Withdrawal dialog on the reconciliation screen now shows a
required vendor dropdown for withdrawals (debits) and stores vendor_id on the
transaction, matching the vendor-required rule on Bills/Expenses/Banking.
Deposits (credits) are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 09:53:07 -04:00
parent dedcbb8889
commit 42475a0e93
@@ -90,8 +90,8 @@ 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 [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 }>( 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: "" }, { type: "credit", date: "", amount: "", description: "", coa_account_id: "", reference: "", vendor_id: "" },
); );
const { data: account } = useQuery({ const { data: account } = useQuery({
@@ -128,6 +128,13 @@ export default function AccountingReconcileDetailPage() {
(await accounting.from("accounts").select("id,name,type,is_bank").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: vendors = [] } = useQuery({
queryKey: ["vendors-lookup", cid],
enabled: !!cid,
queryFn: async () =>
(await accounting.from("vendors").select("id,name").eq("company_id", cid).order("name")).data ?? [],
});
const { data: txs = [] } = useQuery({ const { data: txs = [] } = useQuery({
queryKey: ["recon-txs", accountId, active?.statement_end_date, priorReconDate], queryKey: ["recon-txs", accountId, active?.statement_end_date, priorReconDate],
enabled: !!accountId && !!active, enabled: !!accountId && !!active,
@@ -328,7 +335,7 @@ export default function AccountingReconcileDetailPage() {
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" }),
amount: "", description: "", coa_account_id: "", reference: "", amount: "", description: "", coa_account_id: "", reference: "", vendor_id: "",
}); });
setAddOpen(true); setAddOpen(true);
}; };
@@ -339,6 +346,7 @@ export default function AccountingReconcileDetailPage() {
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"); 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");
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 ?? "";
@@ -353,6 +361,7 @@ export default function AccountingReconcileDetailPage() {
type: addTx.type, // credit = deposit (money in), debit = withdrawal (money out) type: addTx.type, // credit = deposit (money in), debit = withdrawal (money out)
category: coaName, category: coaName,
coa_account_id: addTx.coa_account_id, coa_account_id: addTx.coa_account_id,
vendor_id: addTx.type === "debit" ? (addTx.vendor_id || null) : null,
reference: addTx.reference.trim() || null, reference: addTx.reference.trim() || null,
cleared: true, cleared: true,
}) })
@@ -687,6 +696,20 @@ export default function AccountingReconcileDetailPage() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{addTx.type === "debit" && (
<div>
<Label>Vendor <span className="text-destructive">*</span></Label>
<Select value={addTx.vendor_id} onValueChange={(v) => setAddTx({ ...addTx, vendor_id: v })}>
<SelectTrigger><SelectValue placeholder="Select vendor" /></SelectTrigger>
<SelectContent>
{(vendors as any[]).map((v: any) => <SelectItem key={v.id} value={v.id}>{v.name}</SelectItem>)}
</SelectContent>
</Select>
{(vendors as any[]).length === 0 && (
<p className="mt-1 text-xs text-muted-foreground">No vendors yet add one on the Vendors page first.</p>
)}
</div>
)}
<div> <div>
<Label>Reference (optional)</Label> <Label>Reference (optional)</Label>
<Input value={addTx.reference} onChange={(e) => setAddTx({ ...addTx, reference: e.target.value })} placeholder="Check # / memo" /> <Input value={addTx.reference} onChange={(e) => setAddTx({ ...addTx, reference: e.target.value })} placeholder="Check # / memo" />