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
@@ -197,20 +197,28 @@ export default function AccountingReconcileDetailPage() {
}
}
const deposits = (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "credit")
.map((t) => ({ date: t.date, description: t.description, amount: Number(t.amount) }));
const withdrawals = (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "debit")
.map((t) => ({ date: t.date, description: t.description, amount: Number(t.amount) }));
const mapRow = (t: Tx) => ({ name: t.description ?? "", date: t.date, number: t.reference ?? "", memo: "", amount: Number(t.amount) });
const clearedDepRows = (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "credit").map(mapRow);
const clearedWdRows = (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "debit").map(mapRow);
const unclearedDepRows = (txs as Tx[]).filter((t) => !checked.has(t.id) && t.type === "credit").map(mapRow);
const unclearedWdRows = (txs as Tx[]).filter((t) => !checked.has(t.id) && t.type === "debit").map(mapRow);
const sumAmt = (rs: { amount: number }[]) => rs.reduce((s, r) => s + r.amount, 0);
const begin = Number(active.opening_balance);
const ending = begin + sumAmt(clearedDepRows) - sumAmt(clearedWdRows);
const book = ending + sumAmt(unclearedDepRows) - sumAmt(unclearedWdRows);
const acctLabel = (account as any)?.code ? `${(account as any).code} ${(account as any).name}` : ((account as any)?.name ?? "Account");
setSuccessData({
companyName: associationName ?? "Company",
accountName: (account as any)?.name ?? "Account",
accountName: acctLabel,
statementEndDate: active.statement_end_date,
openingBalance: active.opening_balance,
statementBalance: active.statement_balance,
difference: 0,
deposits,
withdrawals,
beginningBalance: begin,
endingBalance: ending,
bookBalance: book,
clearedDeposits: clearedDepRows,
clearedWithdrawals: clearedWdRows,
unclearedDeposits: unclearedDepRows,
unclearedWithdrawals: unclearedWdRows,
preparedBy: user?.email ?? "—",
currency: cur,
completedAt: new Date().toISOString(),
@@ -465,20 +473,23 @@ export default function AccountingReconcileDetailPage() {
<Button size="sm" variant="ghost" onClick={async () => {
const { data: rows } = await accounting
.from("transactions")
.select("date,description,amount,type")
.select("date,description,reference,amount,type")
.eq("reconciliation_id", h.id);
const deposits = (rows ?? []).filter((r: any) => r.type === "credit")
.map((r: any) => ({ date: r.date, description: r.description, amount: Number(r.amount) }));
const withdrawals = (rows ?? []).filter((r: any) => r.type === "debit")
.map((r: any) => ({ date: r.date, description: r.description, amount: Number(r.amount) }));
const mapRow = (r: any) => ({ name: r.description ?? "", date: r.date, number: r.reference ?? "", memo: "", amount: Number(r.amount) });
const clearedDeposits = (rows ?? []).filter((r: any) => r.type === "credit").map(mapRow);
const clearedWithdrawals = (rows ?? []).filter((r: any) => r.type === "debit").map(mapRow);
const sumAmt = (rs: { amount: number }[]) => rs.reduce((s, r) => s + r.amount, 0);
const begin = Number(h.opening_balance);
const ending = begin + sumAmt(clearedDeposits) - sumAmt(clearedWithdrawals);
const acctLabel = (account as any)?.code ? `${(account as any).code} ${(account as any).name}` : ((account as any)?.name ?? "Account");
renderReconciliationPdf({
companyName: associationName ?? "Company",
accountName: (account as any)?.name ?? "Account",
accountName: acctLabel,
statementEndDate: h.statement_end_date,
openingBalance: Number(h.opening_balance),
statementBalance: Number(h.statement_balance),
difference: 0,
deposits, withdrawals,
beginningBalance: begin,
endingBalance: ending,
bookBalance: ending,
clearedDeposits, clearedWithdrawals, unclearedDeposits: [], unclearedWithdrawals: [],
preparedBy: user?.email ?? "—",
currency: cur,
completedAt: h.completed_at ?? new Date().toISOString(),
@@ -579,8 +590,8 @@ export default function AccountingReconcileDetailPage() {
{successData && (
<div className="space-y-2 text-sm">
<div className="flex justify-between"><span className="text-muted-foreground">Period reconciled</span><span>{fmtDate(successData.statementEndDate)}</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">Statement balance</span><span>{money(successData.statementBalance, successData.currency)}</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">Cleared transactions</span><span>{successData.deposits.length + successData.withdrawals.length}</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">Ending balance</span><span>{money(successData.endingBalance, successData.currency)}</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">Cleared transactions</span><span>{successData.clearedDeposits.length + successData.clearedWithdrawals.length}</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">Date completed</span><span>{fmtDate(successData.completedAt)}</span></div>
</div>
)}