From b9235f644f392217ff7e7c727ce55f66982679d4 Mon Sep 17 00:00:00 2001 From: renee-png Date: Fri, 12 Jun 2026 22:40:58 -0400 Subject: [PATCH] Accounting: period presets (Month/YTD/Prev-Year/Custom) + logo-free reports - Shared PeriodPicker (default This Month); wired into General Ledger report, Journal Entries page, and bank registers (Banking shows a Balance forward row carrying the running balance at period start) - Financial reports are now logo-free: drawBrandedHeader/reportPdf skip the logo block; ReportSheet drops the on-screen logo - reportPdf: appendStructuredReportPdf + skipFooter scaffolding for upcoming report batches (not yet wired) Co-Authored-By: Claude Opus 4.8 --- .../accounting/AccountingBankingPage.tsx | 53 +++++++++++--- .../AccountingJournalEntriesPage.tsx | 29 +++++++- .../components/GeneralLedgerReport.tsx | 18 ++--- .../accounting/components/PeriodPicker.tsx | 73 +++++++++++++++++++ .../accounting/components/ReportSheet.tsx | 8 +- src/pages/accounting/lib/reportHeader.ts | 9 +-- src/pages/accounting/lib/reportPdf.ts | 21 +++--- 7 files changed, 165 insertions(+), 46 deletions(-) create mode 100644 src/pages/accounting/components/PeriodPicker.tsx diff --git a/src/pages/accounting/AccountingBankingPage.tsx b/src/pages/accounting/AccountingBankingPage.tsx index 0e8a812..4a40f07 100644 --- a/src/pages/accounting/AccountingBankingPage.tsx +++ b/src/pages/accounting/AccountingBankingPage.tsx @@ -16,6 +16,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Plus, Trash2, Pencil, ArrowLeftRight, Printer, Link2, RefreshCw, Unlink, FileUp, Download, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; +import { PeriodPicker, periodRange, type PeriodPreset } from "./components/PeriodPicker"; import { generateCheckPDF } from "./lib/checkPdf"; import { parseCsv, pick, parseDateStr } from "./lib/csv"; import { usePlaidLink } from "react-plaid-link"; @@ -129,6 +130,9 @@ export default function AccountingBankingPage() { const [acctDialog, setAcctDialog] = useState(false); const [acctForm, setAcctForm] = useState({ name: "", code: "", type: "asset" as const, is_bank: true }); const [search, setSearch] = useState(""); + const [periodPreset, setPeriodPreset] = useState("month"); + const [periodFrom, setPeriodFrom] = useState(() => periodRange("month").from); + const [periodTo, setPeriodTo] = useState(() => periodRange("month").to); const [importOpen, setImportOpen] = useState(false); const [importing, setImporting] = useState(false); const [importResult, setImportResult] = useState<{ inserted: number; skipped: number } | null>(null); @@ -208,16 +212,31 @@ export default function AccountingBankingPage() { }); }, [txs]); + // Period view: running balances accumulate from inception (computed above), + // but only rows inside the period are shown; everything earlier rolls into + // the balance-forward figure. + const { periodRegister, balanceForward } = useMemo(() => { + let fwd = 0; + const rows: any[] = []; + for (const r of register) { + const d = String(r.date ?? "").slice(0, 10); + if (d < periodFrom) { fwd = r.running; continue; } + if (d > periodTo) continue; + rows.push(r); + } + return { periodRegister: rows, balanceForward: fwd }; + }, [register, periodFrom, periodTo]); + const filteredRegister = useMemo(() => { - if (!search.trim()) return register; + if (!search.trim()) return periodRegister; const q = search.toLowerCase(); - return register.filter( + return periodRegister.filter( (r) => r.description?.toLowerCase().includes(q) || r.category?.toLowerCase().includes(q) || r.reference?.toLowerCase().includes(q) ); - }, [register, search]); + }, [periodRegister, search]); const computedBalance = register.length > 0 ? register[register.length - 1].running : 0; const activeAccount = (accounts as any[]).find((a) => a.id === activeAccountId); @@ -731,12 +750,18 @@ export default function AccountingBankingPage() { - setSearch(e.target.value)} - className="max-w-sm mt-2" - /> +
+ setSearch(e.target.value)} + className="max-w-sm" + /> + { setPeriodPreset(n.preset); setPeriodFrom(n.from); setPeriodTo(n.to); }} + /> +
{selected.size > 0 && ( @@ -797,6 +822,16 @@ export default function AccountingBankingPage() { + {!search.trim() && ( + + + {fmtDate(periodFrom)} + + Balance forward + {money(balanceForward, cur)} + + + )} {filteredRegister.map((row) => ( diff --git a/src/pages/accounting/AccountingJournalEntriesPage.tsx b/src/pages/accounting/AccountingJournalEntriesPage.tsx index 28089ed..1221535 100644 --- a/src/pages/accounting/AccountingJournalEntriesPage.tsx +++ b/src/pages/accounting/AccountingJournalEntriesPage.tsx @@ -3,6 +3,7 @@ import { useMemo, useState } from "react"; import { accounting } from "@/lib/accountingClient"; import { useCompanyId } from "./lib/useCompanyId"; import { fetchAllJournalEntries } from "./lib/fetchJournalEntries"; +import { PeriodPicker, periodRange, type PeriodPreset } from "./components/PeriodPicker"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -40,6 +41,11 @@ export default function AccountingJournalEntriesPage() { const [saving, setSaving] = useState(false); const [editingId, setEditingId] = useState(null); + // Period filter — display only (all entries stay loaded for detail/edit) + const [preset, setPreset] = useState("month"); + const [periodFrom, setPeriodFrom] = useState(() => periodRange("month").from); + const [periodTo, setPeriodTo] = useState(() => periodRange("month").to); + const { data: entries = [], error: entriesError } = useQuery({ queryKey: ["journal-entries", cid], enabled: !!cid, @@ -53,6 +59,14 @@ export default function AccountingJournalEntriesPage() { (await accounting.from("accounts").select("*").eq("company_id", cid).eq("is_archived", false).order("code")).data ?? [], }); + const periodEntries = useMemo( + () => (entries as any[]).filter((e) => { + const d = String(e.date ?? "").slice(0, 10); + return d >= periodFrom && d <= periodTo; + }), + [entries, periodFrom, periodTo], + ); + const detailEntry = useMemo( () => (detailId ? (entries as any[]).find((e) => e.id === detailId) : null), [entries, detailId] @@ -199,6 +213,15 @@ export default function AccountingJournalEntriesPage() { + + + { setPreset(n.preset); setPeriodFrom(n.from); setPeriodTo(n.to); }} + /> + + + @@ -213,7 +236,7 @@ export default function AccountingJournalEntriesPage() { - {(entries as any[]).map((e) => { + {periodEntries.map((e) => { const debits = (e.journal_entry_lines ?? []).reduce((s: number, l: any) => s + Number(l.debit), 0); const credits = (e.journal_entry_lines ?? []).reduce((s: number, l: any) => s + Number(l.credit), 0); return ( @@ -248,10 +271,10 @@ export default function AccountingJournalEntriesPage() { ); })} - {entries.length === 0 && ( + {periodEntries.length === 0 && ( - No journal entries yet. + {entries.length === 0 ? "No journal entries yet." : "No journal entries in this period."} )} diff --git a/src/pages/accounting/components/GeneralLedgerReport.tsx b/src/pages/accounting/components/GeneralLedgerReport.tsx index 9a10a09..8ec2335 100644 --- a/src/pages/accounting/components/GeneralLedgerReport.tsx +++ b/src/pages/accounting/components/GeneralLedgerReport.tsx @@ -17,6 +17,7 @@ import { cn } from "@/lib/utils"; import jsPDF from "jspdf"; import autoTable from "jspdf-autotable"; import { ReportSheet } from "./ReportSheet"; +import { PeriodPicker, periodRange, type PeriodPreset } from "./PeriodPicker"; import { loadBrandedLogo, drawBrandedHeader, drawBrandedFooter } from "../lib/reportHeader"; const TEAL: [number, number, number] = [0, 137, 123]; @@ -38,8 +39,9 @@ type Txn = { }; export function GeneralLedgerReport({ companyId, companyName, logoUrl, initialAccountId }: { companyId: string; companyName: string; logoUrl?: string | null; initialAccountId?: string | null }) { - const [from, setFrom] = useState(startOfYear()); - const [to, setTo] = useState(today()); + const [preset, setPreset] = useState("month"); + const [from, setFrom] = useState(() => periodRange("month").from); + const [to, setTo] = useState(() => periodRange("month").to); const [selectedAccounts, setSelectedAccounts] = useState(initialAccountId ? [initialAccountId] : []); // When opened via a report drill-down, focus the chosen account. @@ -285,14 +287,10 @@ export function GeneralLedgerReport({ companyId, companyName, logoUrl, initialAc
-
- - setFrom(e.target.value)} className="w-40 mt-1" /> -
-
- - setTo(e.target.value)} className="w-40 mt-1" /> -
+ { setPreset(n.preset); setFrom(n.from); setTo(n.to); }} + />
diff --git a/src/pages/accounting/components/PeriodPicker.tsx b/src/pages/accounting/components/PeriodPicker.tsx new file mode 100644 index 0000000..693aab6 --- /dev/null +++ b/src/pages/accounting/components/PeriodPicker.tsx @@ -0,0 +1,73 @@ +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +export type PeriodPreset = "month" | "ytd" | "prev-year" | "custom"; + +const TZ_ET = "America/New_York"; +const etToday = () => new Date().toLocaleDateString("en-CA", { timeZone: TZ_ET }); // YYYY-MM-DD + +/** Date range for a preset (ET). Month = current calendar month to date. */ +export function periodRange(preset: Exclude): { from: string; to: string } { + const today = etToday(); + const [y, m] = today.split("-").map(Number); + if (preset === "month") return { from: `${today.slice(0, 7)}-01`, to: today }; + if (preset === "ytd") return { from: `${y}-01-01`, to: today }; + // prev-year + return { from: `${y - 1}-01-01`, to: `${y - 1}-12-31` }; +} + +export const PERIOD_LABELS: Record = { + month: "This Month", + ytd: "Year to Date", + "prev-year": "Previous Year", + custom: "Custom", +}; + +/** + * Shared period control for ledgers/registers: Month (default) / YTD / + * Previous Year / Custom. The owner holds preset+from+to state; on preset + * change the range is recomputed and pushed up. + */ +export function PeriodPicker({ + preset, from, to, onChange, className, +}: { + preset: PeriodPreset; + from: string; + to: string; + onChange: (next: { preset: PeriodPreset; from: string; to: string }) => void; + className?: string; +}) { + const setPreset = (p: PeriodPreset) => { + if (p === "custom") onChange({ preset: p, from, to }); + else onChange({ preset: p, ...periodRange(p) }); + }; + + return ( +
+
+ + +
+ {preset === "custom" && ( + <> +
+ + e.target.value && onChange({ preset, from: e.target.value, to })} className="w-40 mt-1" /> +
+
+ + e.target.value && onChange({ preset, to: e.target.value, from })} className="w-40 mt-1" /> +
+ + )} +
+ ); +} diff --git a/src/pages/accounting/components/ReportSheet.tsx b/src/pages/accounting/components/ReportSheet.tsx index 1632bce..519e0a9 100644 --- a/src/pages/accounting/components/ReportSheet.tsx +++ b/src/pages/accounting/components/ReportSheet.tsx @@ -1,5 +1,4 @@ import { Card, CardContent } from "@/components/ui/card"; -import acmLogoFull from "@/assets/acm-logo-full.png"; /** * Branded on-screen "sheet" for every accounting report, mirroring the printed @@ -27,12 +26,7 @@ export function ReportSheet({
- { (e.currentTarget as HTMLImageElement).style.display = "none"; }} - /> + {/* Logos intentionally omitted — financial reports are logo-free. */}

{title}

{subtitle &&

{subtitle}

} diff --git a/src/pages/accounting/lib/reportHeader.ts b/src/pages/accounting/lib/reportHeader.ts index e537617..665ad39 100644 --- a/src/pages/accounting/lib/reportHeader.ts +++ b/src/pages/accounting/lib/reportHeader.ts @@ -43,13 +43,8 @@ export function drawBrandedHeader(doc: jsPDF, opts: BrandedHeaderOpts): number { const W = doc.internal.pageSize.getWidth(); const ML = 40; - if (opts.logo) { - try { - const h = 46; - const w = Math.min(170, h * (opts.logo.w / opts.logo.h)); - doc.addImage(opts.logo.dataURL, "PNG", ML, 28, w, h, undefined, "FAST"); - } catch { /* ignore logo failures */ } - } + // Logos intentionally NOT drawn — financial reports are logo-free per user + // preference. The `logo` opt is kept for caller compatibility. doc.setFont("helvetica", "bold"); doc.setFontSize(18); doc.setTextColor(20); doc.text(opts.title, W / 2, 52, { align: "center" }); diff --git a/src/pages/accounting/lib/reportPdf.ts b/src/pages/accounting/lib/reportPdf.ts index 820dbc1..483d66d 100644 --- a/src/pages/accounting/lib/reportPdf.ts +++ b/src/pages/accounting/lib/reportPdf.ts @@ -31,8 +31,10 @@ export type RenderOpts = { showZero: boolean; /** Optional metadata lines for the header block (bold label + value). */ meta?: { label: string; value: string }[]; - /** Preloaded logo (dataURL + dimensions) for the branded header. */ + /** Preloaded logo (dataURL + dimensions) — kept for compatibility; logos are no longer drawn. */ logo?: { dataURL: string; w: number; h: number } | null; + /** Batch mode: skip the per-report footer (the batch stamps one global footer). */ + skipFooter?: boolean; }; type RGB = [number, number, number]; @@ -93,15 +95,9 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js y += h; }; - // Full header (page 1): title + metadata block + column header + // Full header (page 1): title + metadata block + column header. + // (Logos intentionally not drawn — financial reports are logo-free.) const drawFullHeader = () => { - // Branded logo (top-left) + centered title — matches the main reports. - if (opts.logo) { - try { - const lh = 40; const lw = Math.min(150, lh * (opts.logo.w / opts.logo.h)); - doc.addImage(opts.logo.dataURL, "PNG", ML, 26, lw, lh, undefined, "FAST"); - } catch { /* ignore */ } - } doc.setFont("helvetica", "bold"); doc.setFontSize(16); doc.setTextColor(...TEXT); doc.text(report.title, W / 2, 48, { align: "center" }); y = 82; @@ -257,7 +253,7 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js y += 28; } - drawFooter(); + if (!opts.skipFooter) drawFooter(); return doc; } @@ -268,6 +264,11 @@ export function renderReportPdf(report: StructuredReport, opts: RenderOpts): jsP return buildReport(doc, report, opts); } +/** Append a structured financial report to an EXISTING doc, starting on its current page (batch packets). */ +export function appendStructuredReportPdf(doc: jsPDF, report: StructuredReport, opts: RenderOpts): jsPDF { + return buildReport(doc, report, opts); +} + /** * Render a financial report PDF that opens with the shared branded cover page * (same look as the general Report Generator), followed by the report body.