diff --git a/src/pages/accounting/AccountingCustomerDetailPage.tsx b/src/pages/accounting/AccountingCustomerDetailPage.tsx
index 99f3e7c..a9fd6ce 100644
--- a/src/pages/accounting/AccountingCustomerDetailPage.tsx
+++ b/src/pages/accounting/AccountingCustomerDetailPage.tsx
@@ -16,6 +16,7 @@ import { toast } from "sonner";
import { money, fmtDate } from "./lib/format";
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
+import { drawReportCoverPage } from "@/lib/reportCover";
function startOfFY() {
return new Date(new Date().getFullYear(), 0, 1).toISOString().slice(0, 10);
@@ -234,9 +235,17 @@ export default function AccountingCustomerDetailPage() {
qc.invalidateQueries({ queryKey: ["customers", cid] });
};
- const exportStatement = () => {
+ const exportStatement = async () => {
if (!homeowner) return;
const doc = new jsPDF({ unit: "pt", format: "letter" });
+ // Shared branded cover page (matches all other report exports), then detail.
+ await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), {
+ title: "Homeowner Statement",
+ date: `${fmtDate(from)} – ${fmtDate(to)}`,
+ companyName: associationName ?? "Association",
+ preparedBy: "Avria Community Management, LLC",
+ });
+ doc.addPage();
doc.setFontSize(16);
doc.text(associationName ?? "Association", 40, 50);
doc.setFontSize(14);
@@ -271,9 +280,9 @@ export default function AccountingCustomerDetailPage() {
doc.save(`statement-${homeowner.name.replace(/\s+/g, "_")}.pdf`);
};
- const emailStatement = () => {
+ const emailStatement = async () => {
if (!homeowner) return;
- exportStatement();
+ await exportStatement();
const subject = encodeURIComponent(`Statement from ${associationName ?? "us"}`);
const body = encodeURIComponent(
`Hello ${homeowner.name},\n\nPlease find attached your homeowner statement for the period ${fmtDate(from)} – ${fmtDate(to)}.\nCurrent balance due: ${money(currentBalance, cur)}.\n\nThank you.`
diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx
index 3895e26..969ab65 100644
--- a/src/pages/accounting/AccountingReportsPage.tsx
+++ b/src/pages/accounting/AccountingReportsPage.tsx
@@ -332,7 +332,7 @@ export default function AccountingReportsPage() {
}, [active, arOpen, data, flat, structured, cur, activeMeta.name]);
// Reports whose export is handled internally (own PDF/CSV buttons inside the component)
- const hasOwnExport = active === "trial-balance" || active === "general-ledger";
+ const hasOwnExport = active === "trial-balance" || active === "general-ledger" || active === "budget-vs-actuals";
const anyExportable = !!(structured || flat || exportFlat);
const doExportPDF = async () => {
@@ -528,7 +528,7 @@ export default function AccountingReportsPage() {
{active === "budget-vs-actuals" && (
-
+
)}
{active === "trial-balance" && (
@@ -1184,7 +1184,7 @@ function flatToStructured(flat: Flat, title: string): StructuredReport {
// ---------- Budget vs Actuals ----------
-function BudgetVsActuals({ companyId, from, to, currency }: { companyId: string; from: string; to: string; currency: string }) {
+function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabel }: { companyId: string; from: string; to: string; currency: string; companyName: string; rangeLabel: string }) {
const { data: budgets = [] } = useQuery({
queryKey: ["budgets-active", companyId],
enabled: !!companyId,
@@ -1311,6 +1311,59 @@ function BudgetVsActuals({ companyId, from, to, currency }: { companyId: string;
];
}, [grouped, budgetByAcct, actualByAcct]);
+ // Flattened rows (group totals + accounts) shared by the CSV and PDF exports.
+ const exportRows = useMemo(() => {
+ const rows: { label: string; budget: number; actual: number; variance: number; pct: string; group: boolean }[] = [];
+ for (const t of TYPES_LOCAL) {
+ const accs = grouped[t.value] ?? [];
+ if (!accs.length) continue;
+ const tb = accs.reduce((s, a) => s + (budgetByAcct[a.id] ?? 0), 0);
+ const ta = accs.reduce((s, a) => s + (actualByAcct[a.id] ?? 0), 0);
+ const tv = ta - tb;
+ rows.push({ label: t.label, budget: tb, actual: ta, variance: tv, pct: tb ? `${((tv / tb) * 100).toFixed(1)}%` : "—", group: true });
+ for (const a of accs) {
+ const b = budgetByAcct[a.id] ?? 0; const ac = actualByAcct[a.id] ?? 0; const v = ac - b;
+ rows.push({ label: a.code ? `${a.name} (${a.code})` : a.name, budget: b, actual: ac, variance: v, pct: b ? `${((v / b) * 100).toFixed(1)}%` : "—", group: false });
+ }
+ }
+ return rows;
+ }, [grouped, budgetByAcct, actualByAcct]);
+
+ const fileBase = `budget-vs-actuals-${from}-to-${to}`;
+
+ const exportCSV = () => {
+ const esc = (s: any) => `"${String(s).replace(/"/g, '""')}"`;
+ const lines = [["Account", "Budget", "Actual", "Variance", "Variance %"].join(",")];
+ for (const r of exportRows) lines.push([esc(r.label), r.budget.toFixed(2), r.actual.toFixed(2), r.variance.toFixed(2), esc(r.pct)].join(","));
+ const blob = new Blob([lines.join("\n")], { type: "text/csv;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url; a.download = `${fileBase}.csv`; a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ const exportPDF = async () => {
+ const doc = new jsPDF({ unit: "pt", format: "letter" });
+ await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), {
+ title: "Budget vs Actuals", date: rangeLabel, companyName, preparedBy: "Avria Community Management, LLC",
+ });
+ doc.addPage();
+ doc.setFont("helvetica", "bold"); doc.setFontSize(14); doc.setTextColor(33, 37, 41);
+ doc.text("Budget vs Actuals", 40, 50);
+ doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(110, 116, 122);
+ doc.text(`${companyName} · ${rangeLabel}`, 40, 66);
+ autoTable(doc, {
+ startY: 80,
+ head: [["Account", "Budget", "Actual", "Variance", "Variance %"]],
+ body: exportRows.map((r) => [r.label, money(r.budget, currency), money(r.actual, currency), money(r.variance, currency), r.pct]),
+ styles: { font: "helvetica", fontSize: 8, textColor: [33, 37, 41], lineColor: [222, 226, 230], lineWidth: 0.1 },
+ headStyles: { fillColor: [237, 239, 242], textColor: [33, 37, 41], fontStyle: "bold", lineColor: [196, 200, 205], lineWidth: 0.2 },
+ columnStyles: { 1: { halign: "right" }, 2: { halign: "right" }, 3: { halign: "right" }, 4: { halign: "right" } },
+ didParseCell: ({ row, cell, section }) => { if (section === "body" && exportRows[row.index]?.group) cell.styles.fontStyle = "bold"; },
+ });
+ doc.save(`${fileBase}.pdf`);
+ };
+
if (!budgets.length) {
return (
@@ -1330,6 +1383,10 @@ function BudgetVsActuals({ companyId, from, to, currency }: { companyId: string;
{budgets.map((b: any) => {b.name} (FY {b.fiscal_year}))}
+
+
+
+
diff --git a/src/pages/accounting/components/GeneralLedgerReport.tsx b/src/pages/accounting/components/GeneralLedgerReport.tsx
index 2850b3c..bc8c86d 100644
--- a/src/pages/accounting/components/GeneralLedgerReport.tsx
+++ b/src/pages/accounting/components/GeneralLedgerReport.tsx
@@ -15,6 +15,7 @@ import { FileDown, Download, ChevronDown, ChevronsDownUp, ChevronsUpDown, AlertT
import { fmtDate } from "../lib/format";
import { cn } from "@/lib/utils";
import jsPDF from "jspdf";
+import { drawReportCoverPage } from "@/lib/reportCover";
import autoTable from "jspdf-autotable";
const TEAL: [number, number, number] = [0, 137, 123];
@@ -142,12 +143,21 @@ export function GeneralLedgerReport({ companyId, companyName }: { companyId: str
const toggleAccount = (id: string) => setSelectedAccounts((s) => s.includes(id) ? s.filter((x) => x !== id) : [...s, id]);
- const exportPDF = () => {
+ const exportPDF = async () => {
const doc = new jsPDF({ unit: "pt", format: "letter" });
const W = doc.internal.pageSize.getWidth();
const H = doc.internal.pageSize.getHeight();
const ML = 54;
+ // Shared branded cover page, then the report on the next page.
+ await drawReportCoverPage(doc, W, H, {
+ title: "General Ledger",
+ date: `${fmtDate(from)} – ${fmtDate(to)}`,
+ companyName: companyName || "Association",
+ preparedBy: "Avria Community Management, LLC",
+ });
+ doc.addPage();
+
// Header
doc.setFillColor(...TEAL); doc.rect(ML, 40, 44, 44, "F");
doc.setFont("times", "bold"); doc.setFontSize(20); doc.setTextColor(255);
diff --git a/src/pages/accounting/components/TrialBalanceReport.tsx b/src/pages/accounting/components/TrialBalanceReport.tsx
index 44a62e2..042ccb3 100644
--- a/src/pages/accounting/components/TrialBalanceReport.tsx
+++ b/src/pages/accounting/components/TrialBalanceReport.tsx
@@ -11,6 +11,7 @@ import { Badge } from "@/components/ui/badge";
import { CheckCircle2, AlertTriangle, FileDown, Download } from "lucide-react";
import { fmtDate } from "../lib/format";
import jsPDF from "jspdf";
+import { drawReportCoverPage } from "@/lib/reportCover";
import autoTable from "jspdf-autotable";
type Account = {
@@ -91,11 +92,20 @@ export function TrialBalanceReport({ companyId, companyName }: { companyId: stri
const diff = totals.debit - totals.credit;
const inBalance = Math.abs(diff) < 0.005;
- const exportPDF = () => {
+ const exportPDF = async () => {
const doc = new jsPDF({ unit: "pt", format: "letter" });
const W = doc.internal.pageSize.getWidth();
const ML = 54;
+ // Shared branded cover page, then the report on the next page.
+ await drawReportCoverPage(doc, W, doc.internal.pageSize.getHeight(), {
+ title: "Trial Balance",
+ date: `As of ${fmtDate(asOf)}`,
+ companyName: companyName || "Association",
+ preparedBy: "Avria Community Management, LLC",
+ });
+ doc.addPage();
+
// Header
doc.setFillColor(...TEAL); doc.rect(ML, 40, 44, 44, "F");
doc.setFont("times", "bold"); doc.setFontSize(20); doc.setTextColor(255);