import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useRef, useState, useEffect } from "react"; import { accounting } from "@/lib/accountingClient"; import { supabase } from "@/integrations/supabase/client"; import { useCompanyId } from "./lib/useCompanyId"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Upload, X, Eye, Printer, Save, Info, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { generateCheckPDF, generateTestAlignmentPDF, CHECK_FIELD_KEYS, CHECK_FIELD_LABELS } from "./lib/checkPdf"; export default function AccountingCheckSetupPage() { const { companyId, loading: companyLoading, error: companyError, associationId } = useCompanyId(); const cid = companyId ?? ""; const qc = useQueryClient(); const sigRef = useRef(null); const [saving, setSaving] = useState(false); const [uploadingSig, setUploadingSig] = useState(false); const [bankName, setBankName] = useState(""); const [bankAddress, setBankAddress] = useState(""); const [routingNumber, setRoutingNumber] = useState(""); const [accountNumber, setAccountNumber] = useState(""); const [fractionalRouting, setFractionalRouting] = useState(""); const [printSignature, setPrintSignature] = useState(false); const [signatureUrl, setSignatureUrl] = useState(""); const [defaultStyle, setDefaultStyle] = useState("voucher"); const [defaultPosition, setDefaultPosition] = useState("top"); const [fontSize, setFontSize] = useState("medium"); const [nextCheckNumber, setNextCheckNumber] = useState(1001); 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 [fieldPositions, setFieldPositions] = useState({}); const { data: settings } = useQuery({ queryKey: ["check-settings", cid], enabled: !!cid, queryFn: async () => (await accounting.from("check_settings").select("*").eq("company_id", cid).maybeSingle()).data, }); const { data: company } = useQuery({ queryKey: ["company-detail", cid], enabled: !!cid, queryFn: async () => (await accounting.from("companies").select("name,address,logo_url").eq("id", cid).single()).data, }); useEffect(() => { if (!settings) return; const s = settings as any; setBankName(s.bank_name ?? ""); setBankAddress(s.bank_address ?? ""); setRoutingNumber(s.routing_number ?? ""); setAccountNumber(s.account_number ?? ""); setFractionalRouting(s.fractional_routing ?? ""); setPrintSignature(s.print_signature ?? false); setSignatureUrl(s.signature_url ?? ""); setDefaultStyle(s.default_style ?? "voucher"); setDefaultPosition(s.default_position ?? "top"); setFontSize(s.font_size ?? "medium"); setNextCheckNumber(s.next_check_number ?? 1001); 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)); setFieldPositions(s.field_positions ?? {}); }, [settings]); const routingValid = /^\d{9}$/.test(routingNumber.replace(/\D/g, "")); const accountValid = accountNumber.replace(/\D/g, "").length >= 4; const save = async () => { setSaving(true); const { error } = await accounting.from("check_settings").upsert({ company_id: cid, bank_name: bankName || null, bank_address: bankAddress || null, routing_number: routingNumber.replace(/\D/g, "") || null, account_number: accountNumber.replace(/\D/g, "") || null, fractional_routing: fractionalRouting || null, print_signature: printSignature, signature_url: signatureUrl || null, default_style: defaultStyle, default_position: defaultPosition, font_size: fontSize, next_check_number: nextCheckNumber, offset_x: offsetX, offset_y: offsetY, 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); toast.success("Check settings saved"); qc.invalidateQueries({ queryKey: ["check-settings", cid] }); }; const uploadSignature = async (file: File) => { setUploadingSig(true); const path = `${cid}/signature.png`; const { error } = await supabase.storage.from("signatures").upload(path, file, { contentType: file.type, upsert: true, }); if (error) { toast.error(error.message); setUploadingSig(false); return; } const { data } = supabase.storage.from("signatures").getPublicUrl(path); setSignatureUrl(data.publicUrl + `?t=${Date.now()}`); toast.success("Signature uploaded — save to keep it"); setUploadingSig(false); }; const previewCheck = () => { const dataUrl = generateCheckPDF([{ companyName: (company as any)?.name ?? "Association Name", companyAddress: (company as any)?.address ?? undefined, bankName: bankName || undefined, bankAddress: bankAddress || undefined, routingNumber: routingNumber.replace(/\D/g, "") || undefined, accountNumber: accountNumber.replace(/\D/g, "") || undefined, fractionalRouting: fractionalRouting || undefined, checkNumber: nextCheckNumber, date: new Date().toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric" }), payee: "Sample Payee Name", amount: 1234.56, memo: "Sample memo line", printSignature, signatureDataUrl: signatureUrl || undefined, }], { style: defaultStyle as any, position: defaultPosition as any, fontSize: fontSize as any, offsetX, offsetY, micrOffsetY, micrGap1, micrGap2, fieldPositions, }); const w = window.open(""); if (w) w.document.write(``); }; const printAlignment = () => { const dataUrl = generateTestAlignmentPDF({ style: defaultStyle as any, position: defaultPosition as any, fontSize: fontSize as any, offsetX, offsetY, micrOffsetY, micrGap1, micrGap2, fieldPositions, }); const w = window.open(""); if (w) w.document.write(``); }; 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

Select an association.

; if (companyLoading) return
; if (companyError || !companyId) return

{companyError || "Accounting setup is not ready."}

; return (
{/* ── Bank Account ── */} Bank Account Printed on the check face and MICR line.
setBankName(e.target.value)} placeholder="First National Bank" />
setBankAddress(e.target.value)} placeholder="Largo, FL" />
{routingNumber.length > 0 && ( {routingValid ? "✓ Valid" : "Must be 9 digits"} )}
setRoutingNumber(e.target.value.replace(/\D/g, "").slice(0, 9))} placeholder="123456789" className="font-mono" maxLength={9} />
{accountNumber.length > 0 && ( {accountValid ? "✓ Set" : "Too short"} )}
setAccountNumber(e.target.value.replace(/\D/g, ""))} placeholder="1234567890" className="font-mono" />
setFractionalRouting(e.target.value)} placeholder="12-3456/0789" className="font-mono max-w-[200px]" />

These numbers print in the MICR band at the bottom of every check. Ensure they exactly match your bank account.

{/* ── Signature ── */} Authorized Signature Upload a signature image to auto-print on checks.
{signatureUrl ? Signature preview : No signature uploaded }
{signatureUrl && ( )}

PNG with transparent background recommended. Max 400×100px.

{ const f = e.target.files?.[0]; if (f) uploadSignature(f); e.target.value = ""; }} />
{/* ── Print Settings ── */} Print Settings Default layout applied when printing from Bills or Banking.
setNextCheckNumber(parseInt(e.target.value) || 1001)} className="font-mono" />

Position adjustments (inches)

Run the alignment test first, measure the offset on blank paper, then adjust here and preview again.

MICR gaps (spaces between segments)

Element positions (nudge any element; inches, +right / +down)

Element
Show
X
Y
{CHECK_FIELD_KEYS.map((key) => { const fp = fieldPositions[key] || {}; return (
{CHECK_FIELD_LABELS[key]}
updateFieldPos(key, { hidden: !v })} />
updateFieldPos(key, { dx: parseFloat(e.target.value) || 0 })} />
updateFieldPos(key, { dy: parseFloat(e.target.value) || 0 })} />
); })}
); } 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 (

{label}

onChange(Math.max(0, parseInt(e.target.value) || 0))} className="h-7 text-center font-mono text-xs" />
); } function OffsetControl({ label, hint, value, onChange }: { label: string; hint: string; value: number; onChange: (v: number) => void; }) { const STEP = 0.05; const adj = (d: number) => onChange(Math.round((value + d) * 1000) / 1000); return (

{label}

onChange(parseFloat(e.target.value) || 0)} className="h-7 text-center font-mono text-xs" />

{hint}

); }