Accounting: A/P-clearing payments, check return address + MICR gaps, dashboard fixes

Bill/vendor payments:
- Bill "Pay" and bank-register vendor debits now clear Accounts Payable
  (Dr A/P / Cr Bank) instead of re-debiting the expense, which had
  double-counted expenses in the P&L and never cleared A/P. The expense
  account is kept as a display-only category on the payment.

Checks:
- Bill-payment checks now print the return address (payer name/address)
  from the company check layout (was hardcoded blank).
- Per-segment MICR gap control (check# / routing / account) in both check
  generators, wired to Check Setup (bill payments) and the Check Layout
  editor (Print Checks). New columns: check_settings.micr_gap_1/2 and
  check_layouts/company_check_layouts.micr_gap_1/2.

Accounting Dashboard / Financial Overview:
- Date-range selector (presets + custom) drives the charts, top expenses,
  and recent transactions.
- PDF title renamed to "Financial Overview" and shows the period.
- Fixed amounts rendering as "$ 5 0 0. 0 0": the U+2212 minus sign forced
  jsPDF into a UTF-16 fallback; replaced with an ASCII hyphen (also in the
  report PDF out-of-balance line).

DB migrations applied to the project: repost_gl_on_line_item_change,
budget_actuals_accrual_owner_income, add_micr_gap_controls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 00:35:30 -04:00
parent 5bf2a5887e
commit c3a0682e57
9 changed files with 228 additions and 67 deletions
@@ -35,6 +35,8 @@ export default function AccountingCheckSetupPage() {
const [offsetX, setOffsetX] = useState(0);
const [offsetY, setOffsetY] = useState(0);
const [micrOffsetY, setMicrOffsetY] = useState(0);
const [micrGap1, setMicrGap1] = useState(1);
const [micrGap2, setMicrGap2] = useState(1);
const { data: settings } = useQuery({
queryKey: ["check-settings", cid],
@@ -67,6 +69,8 @@ export default function AccountingCheckSetupPage() {
setOffsetX(Number(s.offset_x ?? 0));
setOffsetY(Number(s.offset_y ?? 0));
setMicrOffsetY(Number(s.micr_offset_y ?? 0));
setMicrGap1(Number(s.micr_gap_1 ?? 1));
setMicrGap2(Number(s.micr_gap_2 ?? 1));
}, [settings]);
const routingValid = /^\d{9}$/.test(routingNumber.replace(/\D/g, ""));
@@ -90,6 +94,8 @@ export default function AccountingCheckSetupPage() {
offset_x: offsetX,
offset_y: offsetY,
micr_offset_y: micrOffsetY,
micr_gap_1: micrGap1,
micr_gap_2: micrGap2,
}, { onConflict: "company_id" });
setSaving(false);
if (error) return toast.error(error.message);
@@ -130,7 +136,7 @@ export default function AccountingCheckSetupPage() {
style: defaultStyle as any,
position: defaultPosition as any,
fontSize: fontSize as any,
offsetX, offsetY, micrOffsetY,
offsetX, offsetY, micrOffsetY, micrGap1, micrGap2,
});
const w = window.open("");
if (w) w.document.write(`<iframe src="${dataUrl}" style="border:0;width:100%;height:100vh"></iframe>`);
@@ -141,7 +147,7 @@ export default function AccountingCheckSetupPage() {
style: defaultStyle as any,
position: defaultPosition as any,
fontSize: fontSize as any,
offsetX, offsetY, micrOffsetY,
offsetX, offsetY, micrOffsetY, micrGap1, micrGap2,
});
const w = window.open("");
if (w) w.document.write(`<iframe src="${dataUrl}" style="border:0;width:100%;height:100vh" onload="this.contentWindow.print()"></iframe>`);
@@ -345,6 +351,13 @@ export default function AccountingCheckSetupPage() {
<p className="text-xs text-muted-foreground">
Run the alignment test first, measure the offset on blank paper, then adjust here and preview again.
</p>
<div className="border-t pt-3">
<p className="text-sm font-medium">MICR gaps <span className="text-xs font-normal text-muted-foreground">(spaces between segments)</span></p>
<div className="grid grid-cols-2 gap-6 mt-2">
<GapControl label="Check # → Routing" value={micrGap1} onChange={setMicrGap1} />
<GapControl label="Routing → Account" value={micrGap2} onChange={setMicrGap2} />
</div>
</div>
</div>
<div className="flex gap-2 flex-wrap">
@@ -368,6 +381,35 @@ export default function AccountingCheckSetupPage() {
);
}
function GapControl({ label, value, onChange }: {
label: string; value: number; onChange: (v: number) => void;
}) {
const adj = (d: number) => onChange(Math.max(0, Math.round(value + d)));
return (
<div className="space-y-1.5">
<p className="text-xs font-medium text-foreground">{label}</p>
<div className="flex items-center gap-1">
<button onClick={() => adj(-1)}
className="h-7 w-7 rounded border bg-background hover:bg-muted flex items-center justify-center font-bold text-sm shrink-0">
</button>
<Input
type="number"
min={0}
step={1}
value={value}
onChange={(e) => onChange(Math.max(0, parseInt(e.target.value) || 0))}
className="h-7 text-center font-mono text-xs"
/>
<button onClick={() => adj(1)}
className="h-7 w-7 rounded border bg-background hover:bg-muted flex items-center justify-center font-bold text-sm shrink-0">
+
</button>
</div>
</div>
);
}
function OffsetControl({ label, hint, value, onChange }: {
label: string; hint: string; value: number; onChange: (v: number) => void;
}) {