diff --git a/src/pages/BudgetWorkbookPage.tsx b/src/pages/BudgetWorkbookPage.tsx index 35de4f4..0d27945 100644 --- a/src/pages/BudgetWorkbookPage.tsx +++ b/src/pages/BudgetWorkbookPage.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import jsPDF from "jspdf"; import autoTable from "jspdf-autotable"; -import { Building2, RefreshCw, Save, Upload, Download, FileText, Loader2, RotateCcw, ChevronLeft, ChevronRight } from "lucide-react"; +import { Building2, RefreshCw, Save, Upload, Download, FileText, Loader2, RotateCcw, ChevronLeft, ChevronRight, CheckCircle2 } from "lucide-react"; import { toast } from "sonner"; import { useAssociation } from "@/contexts/AssociationContext"; import { supabase } from "@/integrations/supabase/client"; @@ -59,6 +59,9 @@ export default function BudgetWorkbookPage() { const [unitOv, setUnitOv] = useState(""); // "" = use the live unit count const [saving, setSaving] = useState(false); const [pushing, setPushing] = useState(false); + const [certifying, setCertifying] = useState(false); + const [certified, setCertified] = useState(false); + const [certifiedAt, setCertifiedAt] = useState(null); const sortedAssoc = useMemo( () => [...(associations ?? [])].sort((a: any, b: any) => a.name.localeCompare(b.name)), @@ -126,8 +129,41 @@ export default function BudgetWorkbookPage() { setYtdOv(y); setInfl(i); setProjOv(p); if ((workbook.head as any)?.through_month) setThrough((workbook.head as any).through_month); setUnitOv((workbook.head as any)?.unit_override != null ? String((workbook.head as any).unit_override) : ""); + setCertified(!!(workbook.head as any)?.certified); + setCertifiedAt((workbook.head as any)?.certified_at ?? null); }, [workbook]); + // Certify the workbook as the board-approved budget (or revert to preliminary). + const toggleCertify = async () => { + if (!cid) return; + const next = !certified; + if (next && !confirm(`Certify the FY${fy} budget as approved? This marks the projections as the board-approved budget and removes the preliminary disclaimer.`)) return; + setCertifying(true); + try { + const { data: u } = await supabase.auth.getUser(); + const { error } = await accounting.from("budget_workbooks") + .upsert({ + company_id: cid, fiscal_year: fy, through_month: through, + unit_override: unitOv.trim() ? Math.round(Number(unitOv)) : null, + certified: next, + certified_at: next ? new Date().toISOString() : null, + certified_by: next ? (u?.user?.id ?? null) : null, + updated_at: new Date().toISOString(), + }, { onConflict: "company_id,fiscal_year" }); + if (error) throw new Error(error.message); + setCertified(next); + setCertifiedAt(next ? new Date().toISOString() : null); + toast.success(next ? "Budget certified as approved" : "Reverted to preliminary"); + qc.invalidateQueries({ queryKey: ["wb-state", cid, fy] }); + } catch (e: any) { + toast.error(e.message || "Failed to update certification"); + } finally { + setCertifying(false); + } + }; + + const PRELIM_NOTE = "These projections are for preliminary purposes only and are not subject to annual increases or inflation."; + // Computed YTD actual per account from the GL const ytdComputed = useMemo(() => { const typeById = new Map((accounts as any[]).map((a) => [a.id, a.type])); @@ -267,6 +303,23 @@ export default function BudgetWorkbookPage() { let y = 100; + // Certification status: bold red preliminary disclaimer until approved. + if (certified) { + doc.setTextColor(22, 101, 52); + doc.setFontSize(10); + doc.setFont("helvetica", "bold"); + doc.text(`APPROVED BUDGET${certifiedAt ? ` — Certified ${new Date(certifiedAt).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}` : ""}`, margin, y); + y += 22; + } else { + doc.setTextColor(220, 38, 38); + doc.setFontSize(9.5); + doc.setFont("helvetica", "bold"); + const lines = doc.splitTextToSize(PRELIM_NOTE, contentW); + doc.text(lines, margin, y); + y += lines.length * 13 + 10; + } + doc.setTextColor(30, 30, 30); + const sectionTable = (title: string, rows: any[], total: number) => { autoTable(doc, { startY: y, @@ -477,10 +530,25 @@ export default function BudgetWorkbookPage() { + + {certified ? ( +
+ + Approved budget. Certified{certifiedAt ? ` on ${new Date(certifiedAt).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}` : ""}. +
+ ) : ( +
+

{PRELIM_NOTE}

+
+ )} + {/* Summary */}
Annual Budget (Expenses)
{money(expenseAnnual)}