mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Bill-payment checks: per-element positioning + visibility
The bill-payment check generator (checkPdf.ts) now supports per-field
position offsets (X/Y inches) and show/hide for every element — check
number, return address, bank block, date, pay-to, amounts, payee address,
memo, signature, and MICR — layered on the existing layout (defaults render
identically). Edited in Settings → Check Setup ("Element positions").
Stored in accounting.check_settings.field_positions (jsonb). Also replaced a
"→" with ">" in the MICR placeholder to avoid the UTF-16 spacing artifact.
Migration applied: check_settings.field_positions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Upload, X, Eye, Printer, Save, Info, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { generateCheckPDF, generateTestAlignmentPDF } from "./lib/checkPdf";
|
||||
import { generateCheckPDF, generateTestAlignmentPDF, CHECK_FIELD_KEYS, CHECK_FIELD_LABELS } from "./lib/checkPdf";
|
||||
|
||||
export default function AccountingCheckSetupPage() {
|
||||
const { companyId, loading: companyLoading, error: companyError, associationId } = useCompanyId();
|
||||
@@ -37,6 +37,7 @@ export default function AccountingCheckSetupPage() {
|
||||
const [micrOffsetY, setMicrOffsetY] = useState(0);
|
||||
const [micrGap1, setMicrGap1] = useState(1);
|
||||
const [micrGap2, setMicrGap2] = useState(1);
|
||||
const [fieldPositions, setFieldPositions] = useState({});
|
||||
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ["check-settings", cid],
|
||||
@@ -71,6 +72,7 @@ export default function AccountingCheckSetupPage() {
|
||||
setMicrOffsetY(Number(s.micr_offset_y ?? 0));
|
||||
setMicrGap1(Number(s.micr_gap_1 ?? 1));
|
||||
setMicrGap2(Number(s.micr_gap_2 ?? 1));
|
||||
setFieldPositions(s.field_positions ?? {});
|
||||
}, [settings]);
|
||||
|
||||
const routingValid = /^\d{9}$/.test(routingNumber.replace(/\D/g, ""));
|
||||
@@ -96,6 +98,7 @@ export default function AccountingCheckSetupPage() {
|
||||
micr_offset_y: micrOffsetY,
|
||||
micr_gap_1: micrGap1,
|
||||
micr_gap_2: micrGap2,
|
||||
field_positions: fieldPositions,
|
||||
}, { onConflict: "company_id" });
|
||||
setSaving(false);
|
||||
if (error) return toast.error(error.message);
|
||||
@@ -136,7 +139,7 @@ export default function AccountingCheckSetupPage() {
|
||||
style: defaultStyle as any,
|
||||
position: defaultPosition as any,
|
||||
fontSize: fontSize as any,
|
||||
offsetX, offsetY, micrOffsetY, micrGap1, micrGap2,
|
||||
offsetX, offsetY, micrOffsetY, micrGap1, micrGap2, fieldPositions,
|
||||
});
|
||||
const w = window.open("");
|
||||
if (w) w.document.write(`<iframe src="${dataUrl}" style="border:0;width:100%;height:100vh"></iframe>`);
|
||||
@@ -147,12 +150,17 @@ export default function AccountingCheckSetupPage() {
|
||||
style: defaultStyle as any,
|
||||
position: defaultPosition as any,
|
||||
fontSize: fontSize as any,
|
||||
offsetX, offsetY, micrOffsetY, micrGap1, micrGap2,
|
||||
offsetX, offsetY, micrOffsetY, micrGap1, micrGap2, fieldPositions,
|
||||
});
|
||||
const w = window.open("");
|
||||
if (w) w.document.write(`<iframe src="${dataUrl}" style="border:0;width:100%;height:100vh" onload="this.contentWindow.print()"></iframe>`);
|
||||
};
|
||||
|
||||
const updateFieldPos = (key, patch) =>
|
||||
setFieldPositions((prev) => ({ ...prev, [key]: { ...(prev[key] || {}), ...patch } }));
|
||||
const resetFieldPos = (key) =>
|
||||
setFieldPositions((prev) => { const n = { ...prev }; delete n[key]; return n; });
|
||||
|
||||
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>;
|
||||
@@ -358,6 +366,40 @@ export default function AccountingCheckSetupPage() {
|
||||
<GapControl label="Routing → Account" value={micrGap2} onChange={setMicrGap2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-3">
|
||||
<p className="text-sm font-medium">Element positions <span className="text-xs font-normal text-muted-foreground">(nudge any element; inches, +right / +down)</span></p>
|
||||
<div className="mt-2 grid grid-cols-12 gap-2 text-xs font-semibold text-muted-foreground px-1">
|
||||
<div className="col-span-5">Element</div>
|
||||
<div className="col-span-2 text-center">Show</div>
|
||||
<div className="col-span-2">X</div>
|
||||
<div className="col-span-2">Y</div>
|
||||
<div className="col-span-1" />
|
||||
</div>
|
||||
{CHECK_FIELD_KEYS.map((key) => {
|
||||
const fp = fieldPositions[key] || {};
|
||||
return (
|
||||
<div key={key} className="grid grid-cols-12 gap-2 items-center px-1 py-1 rounded hover:bg-background">
|
||||
<div className="col-span-5 text-sm">{CHECK_FIELD_LABELS[key]}</div>
|
||||
<div className="col-span-2 flex justify-center">
|
||||
<Switch checked={fp.hidden !== true} onCheckedChange={(v) => updateFieldPos(key, { hidden: !v })} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Input type="number" step="0.05" className="h-7 text-xs font-mono" value={fp.dx ?? 0}
|
||||
onChange={(e) => updateFieldPos(key, { dx: parseFloat(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Input type="number" step="0.05" className="h-7 text-xs font-mono" value={fp.dy ?? 0}
|
||||
onChange={(e) => updateFieldPos(key, { dy: parseFloat(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div className="col-span-1 text-right">
|
||||
<button type="button" onClick={() => resetFieldPos(key)} title="Reset"
|
||||
className="text-xs text-muted-foreground hover:text-foreground">↺</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
|
||||
Reference in New Issue
Block a user