Accounting platform: remove Zoho, unify reports, board access, vendor sharing

- Remove the Zoho Books integration (edge functions, sync libs, settings,
  reports/overview, banking links, fees tab, import dialog); preserve fee
  rules as a standalone FeesTab and the COA accounting_system classification.
- Financial Overview/Reports (staff + board) render the Accounting dashboard
  and reports; board reports mirror the rich Accounting Reports.
- New Reserve Fund Schedule report + an is_reserve flag on accounts.
- Unify all report exports to a branded format (logo + centered header +
  footer): shared ReportSheet (on-screen) and reportHeader (PDF). Budget vs
  Actuals and Bank Reconciliation PDFs now match the reference layout.
- Render financial reports inline (no preview pop-up).
- Budget Management mirrors Accounting Budgeting (staff-accessible) with SPA
  navigation; editable bills in the Accounting Bills page.
- Negative opening balances flow through to the GL and reports (allow negative
  input; keep non-zero on save; signed CSV import).
- Upload a per-account trial balance via CSV on Opening Balances.
- Board members: read-only RLS access to their association's accounting ledger;
  editable board-members panel on the association page; share vendor contacts
  with the board (toggle + directory section).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 18:29:31 -04:00
parent db20226d62
commit e302fb91f0
63 changed files with 2406 additions and 9514 deletions
+1 -109
View File
@@ -1,9 +1,8 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { supabase } from "@/integrations/supabase/client";
import { pushBillToZohoAfterCreate, pushBillPaymentToZohoAfterPay } from "@/lib/zohoBillSync";
import { useToast } from "@/hooks/use-toast";
import { UserCheck, Plus, Search, Eye, Upload, X, ArrowUpDown, Edit, Trash2, MoreHorizontal, AlertTriangle, Loader2, RefreshCw, ArrowUpFromLine, Bell, Printer, Sparkles, Download } from "lucide-react";
import { UserCheck, Plus, Search, Eye, Upload, X, ArrowUpDown, Edit, Trash2, MoreHorizontal, AlertTriangle, Loader2, Bell, Printer, Sparkles, Download } from "lucide-react";
import { downloadChecksPdf, type CheckData } from "@/utils/checkPdfGenerator";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
@@ -66,9 +65,6 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
const [editingBill, setEditingBill] = useState<any>(null);
const [editOpen, setEditOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<any>(null);
const [syncingId, setSyncingId] = useState<string | null>(null);
const [syncingAll, setSyncingAll] = useState(false);
const [resyncConfirm, setResyncConfirm] = useState<{ type: "single" | "all"; id?: string } | null>(null);
// Notify Board
const [notifyOpen, setNotifyOpen] = useState(false);
@@ -451,16 +447,6 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
}).select("id").single();
if (error) throw error;
if (newBill?.id) {
const syncResult = await pushBillToZohoAfterCreate(newBill.id);
if (!syncResult?.success) {
toast({
variant: "destructive",
title: "Zoho sync failed",
description: syncResult?.error || "Bill was created, but it could not be pushed to Zoho Books.",
});
}
}
// If board members were selected for approval, create approval requests for each
if (form.approval_member_ids.length > 0) {
@@ -579,15 +565,7 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
await supabase.from("invoices").update({ raw_pdf_url: attachmentUrl }).eq("id", editingBill.source_invoice_id);
}
const syncResult = await pushBillToZohoAfterCreate(editingBill.id);
toast({ title: "Bill Updated", description: "Bill changes have been saved." });
if (!syncResult?.success) {
toast({
variant: "destructive",
title: "Zoho sync failed",
description: syncResult?.error || "Bill was updated, but it could not be pushed to Zoho Books.",
});
}
setEditOpen(false);
setEditingBill(null);
setUploadFile(null);
@@ -618,52 +596,6 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
const getClientName = (bill: any) => bill.associations?.name || "—";
const getGlLabel = (bill: any) => bill.chart_of_accounts ? `${bill.chart_of_accounts.account_number} - ${bill.chart_of_accounts.account_name}` : "—";
const handlePushToZoho = async (billId: string) => {
setSyncingId(billId);
try {
const syncResult = await pushBillToZohoAfterCreate(billId);
if (syncResult?.success) {
toast({ title: "Zoho sync complete", description: "Bill was sent to Zoho Books." });
fetchData();
return;
}
toast({
variant: "destructive",
title: "Zoho sync failed",
description: syncResult?.error || "This bill could not be pushed to Zoho Books.",
});
} finally {
setSyncingId(null);
}
};
const pushAllToZoho = async (force = false) => {
setSyncingAll(true);
try {
const { data, error } = await supabase.functions.invoke("zoho-books", {
body: { action: "push_all_bills", params: { force } },
});
if (error) throw error;
const r = data?.data;
toast({ title: "Zoho Sync Complete", description: `${r?.synced || 0} synced, ${r?.errors || 0} errors out of ${r?.total || 0}.` });
fetchData();
} catch (err: any) {
toast({ title: "Zoho sync failed", description: err.message, variant: "destructive" });
} finally {
setSyncingAll(false);
}
};
const handleResyncConfirm = () => {
if (!resyncConfirm) return;
if (resyncConfirm.type === "single") {
handlePushToZoho(resyncConfirm.id!);
} else {
pushAllToZoho(true);
}
setResyncConfirm(null);
};
const getStatusDisplay = (bill: any) => {
// Check overdue
if (bill.status === "pending" && bill.due_date && new Date(bill.due_date) < new Date()) {
@@ -932,10 +864,6 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
check_id: u.checkId,
})
.eq("id", u.billId);
// Push payment to Zoho Books (records vendor payment + check info on the bill)
pushBillPaymentToZohoAfterPay(u.billId).then((r) => {
if (!r.success) console.warn("Zoho payment sync failed for bill", u.billId, r.error);
});
}
if (txInserts.length > 0) await supabase.from("bank_transactions").insert(txInserts);
@@ -985,14 +913,6 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
<Button onClick={openNotifyBoard} variant="outline" size="sm" className="gap-2">
<Bell className="w-4 h-4" /> Notify Board
</Button>
<Button onClick={() => setResyncConfirm({ type: "all" })} disabled={syncingAll} variant="outline" size="sm">
{syncingAll ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RefreshCw className="w-4 h-4 mr-2" />}
Resync All to Zoho
</Button>
<Button onClick={() => pushAllToZoho(false)} disabled={syncingAll} variant="outline" size="sm">
{syncingAll ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <ArrowUpFromLine className="w-4 h-4 mr-2" />}
Sync All to Zoho
</Button>
<Button
onClick={openPrintDialog}
disabled={selectedBillIds.size === 0 || printing}
@@ -1196,9 +1116,6 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
<DropdownMenuItem onClick={() => openEdit(b)}>
<Edit className="h-4 w-4 mr-2" /> Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handlePushToZoho(b.id)}>
<Upload className="h-4 w-4 mr-2" /> Push to Zoho Books
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive" onClick={() => setDeleteTarget(b)}>
<Trash2 className="h-4 w-4 mr-2" /> Delete
</DropdownMenuItem>
@@ -1433,13 +1350,6 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
{detailBill.description && (
<div><p className="text-xs text-muted-foreground uppercase">Description</p><p className="text-sm">{detailBill.description}</p></div>
)}
{!isBoardView && (
<div>
<Button variant="outline" onClick={() => handlePushToZoho(detailBill.id)} className="gap-2">
<Upload className="h-4 w-4" /> Push to Zoho Books
</Button>
</div>
)}
{/* Embedded PDF / Attachment Preview */}
{detailBill.attachment_url && (
<div className="border rounded-lg overflow-hidden">
@@ -1617,24 +1527,6 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
</AlertDialogContent>
</AlertDialog>
{/* Resync Confirmation */}
<AlertDialog open={!!resyncConfirm} onOpenChange={(open) => { if (!open) setResyncConfirm(null); }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Resync to Zoho Books?</AlertDialogTitle>
<AlertDialogDescription>
{resyncConfirm?.type === "all"
? "This will re-push all bills to Zoho Books, including ones already synced. Existing Zoho records will be overwritten. This action cannot be undone."
: "This will re-push this bill to Zoho Books, overwriting the existing Zoho record. This action cannot be undone."}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResyncConfirm}>Confirm Resync</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Notify Board Dialog */}
<Dialog open={notifyOpen} onOpenChange={setNotifyOpen}>
<DialogContent>