mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
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:
@@ -2,6 +2,7 @@ import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import { accounting } from "@/lib/accountingClient";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useCompanyId } from "./lib/useCompanyId";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -29,12 +30,17 @@ import {
|
||||
import { Lock } from "lucide-react";
|
||||
import { TrialBalanceReport } from "./components/TrialBalanceReport";
|
||||
import { GeneralLedgerReport } from "./components/GeneralLedgerReport";
|
||||
import { ReserveFundReport } from "./components/ReserveFundReport";
|
||||
import { ReportSheet } from "./components/ReportSheet";
|
||||
import { loadBrandedLogo, drawBrandedHeader, drawBrandedFooter, type BrandedLogo } from "./lib/reportHeader";
|
||||
import { generateBudgetVsActualPdf } from "@/lib/budgetVsActualPdf";
|
||||
|
||||
type ReportId =
|
||||
| "pnl" | "balance-sheet" | "cash-flow" | "movement-of-equity" | "budget-vs-actuals"
|
||||
| "trial-balance" | "general-ledger"
|
||||
| "invoice-summary" | "customer-balances" | "ar-aging" | "homeowner-summary" | "delinquency"
|
||||
| "expense-summary" | "vendor-balances" | "ap-aging" | "reconciliation";
|
||||
| "expense-summary" | "vendor-balances" | "ap-aging" | "reconciliation"
|
||||
| "reserve-fund";
|
||||
|
||||
const APP_NAME = "Cozy Books";
|
||||
const FINANCIAL: ReportId[] = ["pnl", "balance-sheet", "cash-flow", "movement-of-equity"];
|
||||
@@ -61,6 +67,9 @@ const GROUPS = [
|
||||
{ id: "expense-summary" as ReportId, name: "Expense Summary" },
|
||||
{ id: "vendor-balances" as ReportId, name: "Vendor Balance Summary" },
|
||||
]},
|
||||
{ name: "Reserves", reports: [
|
||||
{ id: "reserve-fund" as ReportId, name: "Reserve Fund Schedule" },
|
||||
]},
|
||||
{ name: "Audit", reports: [
|
||||
{ id: "reconciliation" as ReportId, name: "Reconciliation Checks" },
|
||||
]},
|
||||
@@ -188,8 +197,8 @@ function compareDates(mode: CompareMode, from: string, to: string, customFrom: s
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function AccountingReportsPage() {
|
||||
const { companyId, associationName } = useCompanyId();
|
||||
export default function AccountingReportsPage({ association }: { association?: { id: string; name?: string } | null } = {}) {
|
||||
const { companyId, associationName, associationId } = useCompanyId(association);
|
||||
const cid = companyId ?? "";
|
||||
const cur = "USD";
|
||||
const [active, setActive] = useState<ReportId>("pnl");
|
||||
@@ -218,7 +227,26 @@ export default function AccountingReportsPage() {
|
||||
// Toggles
|
||||
const [showCodes, setShowCodes] = useState(false);
|
||||
const [showZero, setShowZero] = useState(false);
|
||||
const [preview, setPreview] = useState(false);
|
||||
|
||||
const { data: companyMeta } = useQuery({
|
||||
queryKey: ["company-fy", cid],
|
||||
enabled: !!cid,
|
||||
queryFn: async () => (await accounting.from("companies").select("fiscal_year_start").eq("id", cid).maybeSingle()).data,
|
||||
});
|
||||
const fiscalYearStart = (companyMeta as any)?.fiscal_year_start || "01-01";
|
||||
|
||||
// Association logo for branded reports (ACM fallback handled downstream).
|
||||
const { data: assocMeta } = useQuery({
|
||||
queryKey: ["assoc-logo", associationId],
|
||||
enabled: !!associationId,
|
||||
queryFn: async () => (await supabase.from("associations").select("logo_url").eq("id", associationId!).maybeSingle()).data,
|
||||
});
|
||||
const logoUrl = (assocMeta as any)?.logo_url || null;
|
||||
// Preloaded logo dataURL for synchronous PDF header drawing.
|
||||
const { data: brandedLogo } = useQuery({
|
||||
queryKey: ["branded-logo", logoUrl],
|
||||
queryFn: async () => await loadBrandedLogo(logoUrl),
|
||||
});
|
||||
|
||||
const { data } = useReportData(cid, from, to);
|
||||
const { data: prevData } = useReportData(
|
||||
@@ -342,45 +370,35 @@ export default function AccountingReportsPage() {
|
||||
const hasOwnExport = active === "trial-balance" || active === "general-ledger" || active === "budget-vs-actuals";
|
||||
const anyExportable = !!(structured || flat || exportFlat);
|
||||
|
||||
const doExportPDF = () => {
|
||||
const doExportPDF = async () => {
|
||||
const fileBase = `${activeMeta.name.replace(/\s+/g, "-").toLowerCase()}-${from}-to-${to}`;
|
||||
const src = flat ?? exportFlat;
|
||||
const logo = brandedLogo ?? (await loadBrandedLogo(logoUrl));
|
||||
if (structured) {
|
||||
const doc = renderReportPdf(
|
||||
structured,
|
||||
{ companyName: associationName ?? "Company", appName: APP_NAME, rangeLabel, currency: cur, showCodes, showCompare, showZero },
|
||||
{ companyName: associationName ?? "Company", appName: APP_NAME, rangeLabel, currency: cur, showCodes, showCompare, showZero, logo },
|
||||
);
|
||||
doc.save(`${fileBase}.pdf`);
|
||||
} else if (src) {
|
||||
const doc = new jsPDF({ orientation: src.columns.length > 6 ? "landscape" : "portrait" });
|
||||
doc.setFont("helvetica", "bold"); doc.setFontSize(14); doc.setTextColor(33, 37, 41);
|
||||
doc.text(src.title, 14, 16);
|
||||
doc.setFont("helvetica", "bold"); doc.setFontSize(9);
|
||||
doc.text("Properties:", 14, 24);
|
||||
const lw = doc.getTextWidth("Properties:");
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.text(` ${associationName ?? ""}`, 14 + lw, 24);
|
||||
doc.setFont("helvetica", "bold"); doc.text("Period:", 14, 30);
|
||||
const lw2 = doc.getTextWidth("Period:");
|
||||
doc.setFont("helvetica", "normal"); doc.text(` ${rangeLabel}`, 14 + lw2, 30);
|
||||
const doc = new jsPDF({ unit: "pt", format: "letter", orientation: src.columns.length > 6 ? "landscape" : "portrait" });
|
||||
const startY = drawBrandedHeader(doc, {
|
||||
logo, title: src.title,
|
||||
metaLines: [{ label: "Properties:", value: associationName ?? "" }, { label: "Period:", value: rangeLabel }],
|
||||
});
|
||||
const lastCol = src.columns.length - 1;
|
||||
autoTable(doc, {
|
||||
head: [src.columns],
|
||||
body: src.rows.map(r => r.map(String)),
|
||||
startY: 36,
|
||||
startY,
|
||||
margin: { left: 40, right: 40 },
|
||||
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 },
|
||||
alternateRowStyles: { fillColor: [247, 248, 250] },
|
||||
columnStyles: { [lastCol]: { halign: "right" } },
|
||||
didParseCell: ({ row, cell }) => { if (src.boldRows?.includes(row.index)) cell.styles.fontStyle = "bold"; },
|
||||
didDrawPage: () => {
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
const pageH = doc.internal.pageSize.getHeight();
|
||||
doc.setFont("helvetica", "normal"); doc.setFontSize(8); doc.setTextColor(110, 116, 122);
|
||||
doc.text(`Created on ${new Date().toLocaleDateString("en-US")}`, 14, pageH - 8);
|
||||
doc.text(`Page ${doc.getNumberOfPages()}`, pageW - 14, pageH - 8, { align: "right" });
|
||||
},
|
||||
});
|
||||
drawBrandedFooter(doc);
|
||||
doc.save(`${fileBase}.pdf`);
|
||||
} else {
|
||||
toast.error("No data to export for this report");
|
||||
@@ -456,7 +474,6 @@ export default function AccountingReportsPage() {
|
||||
<span className="text-xs text-muted-foreground self-center">Export available inside the report ↓</span>
|
||||
) : (
|
||||
<>
|
||||
{isFinancial && <Button variant="outline" onClick={() => setPreview(true)}><Eye className="mr-1 h-4 w-4" /> Preview</Button>}
|
||||
<Button variant="outline" onClick={exportCSV} disabled={!anyExportable}><Download className="mr-1 h-4 w-4" /> CSV</Button>
|
||||
<Button onClick={exportPDF} disabled={!anyExportable}><FileDown className="mr-1 h-4 w-4" /> PDF</Button>
|
||||
</>
|
||||
@@ -525,20 +542,35 @@ export default function AccountingReportsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
{active === "budget-vs-actuals" && (
|
||||
<BudgetVsActuals companyId={cid} from={from} to={to} currency={cur} companyName={associationName ?? "Company"} rangeLabel={rangeLabel} />
|
||||
<BudgetVsActuals companyId={cid} from={from} to={to} currency={cur} companyName={associationName ?? "Company"} rangeLabel={rangeLabel} logoUrl={logoUrl} />
|
||||
)}
|
||||
{active === "trial-balance" && (
|
||||
<TrialBalanceReport companyId={cid} companyName={associationName ?? ""} />
|
||||
<TrialBalanceReport companyId={cid} companyName={associationName ?? ""} logoUrl={logoUrl} />
|
||||
)}
|
||||
{active === "general-ledger" && (
|
||||
<GeneralLedgerReport companyId={cid} companyName={associationName ?? ""} />
|
||||
<GeneralLedgerReport companyId={cid} companyName={associationName ?? ""} logoUrl={logoUrl} />
|
||||
)}
|
||||
{active === "reserve-fund" && (
|
||||
<ReserveFundReport companyId={cid} companyName={associationName ?? ""} fiscalYearStart={fiscalYearStart} logoUrl={logoUrl} />
|
||||
)}
|
||||
{active === "reconciliation" && (
|
||||
<ReconciliationReport d={data} currency={cur} />
|
||||
<ReportSheet title="Reconciliation Checks" companyName={associationName ?? "Company"} period={rangeLabel} logoUrl={logoUrl}>
|
||||
<ReconciliationReport d={data} currency={cur} />
|
||||
</ReportSheet>
|
||||
)}
|
||||
{!isFinancial && active !== "budget-vs-actuals" && active !== "trial-balance" && active !== "general-ledger" && active !== "reconciliation" && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
{isFinancial && (
|
||||
!data ? (
|
||||
<Card><CardContent className="p-6"><div className="py-8 text-center text-sm text-muted-foreground">Loading…</div></CardContent></Card>
|
||||
) : structured ? (
|
||||
<ReportSheet title={activeMeta.name} subtitle="Accrual basis" companyName={associationName ?? "Company"} period={rangeLabel} logoUrl={logoUrl}>
|
||||
<StructuredTable report={structured} showCodes={showCodes} showCompare={showCompare} showZero={showZero} currency={cur} />
|
||||
</ReportSheet>
|
||||
) : (
|
||||
<Card><CardContent className="p-6"><div className="py-12 text-center text-sm text-muted-foreground">No data for this report in the selected range.</div></CardContent></Card>
|
||||
)
|
||||
)}
|
||||
{!isFinancial && active !== "budget-vs-actuals" && active !== "trial-balance" && active !== "general-ledger" && active !== "reserve-fund" && active !== "reconciliation" && (
|
||||
<ReportSheet title={activeMeta.name} companyName={associationName ?? "Company"} period={rangeLabel} logoUrl={logoUrl}>
|
||||
{!data ? (
|
||||
<div className="text-sm text-muted-foreground">Loading…</div>
|
||||
) : structured ? (
|
||||
@@ -573,38 +605,10 @@ export default function AccountingReportsPage() {
|
||||
) : (
|
||||
<div className="py-12 text-center text-sm text-muted-foreground">No data for this report in the selected range.</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ReportSheet>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Print Preview Modal */}
|
||||
<Dialog open={preview} onOpenChange={setPreview}>
|
||||
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Print preview · {activeMeta.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-wrap items-center gap-5 border-y bg-muted/40 px-4 py-3 text-sm">
|
||||
<Toggle id="t-codes" checked={showCodes} onChange={setShowCodes} label="Show account codes" />
|
||||
<Toggle id="t-compare" checked={showCompare} onChange={(v) => setCompareMode(v ? "prior-year" : "none")} label="Show comparative period" />
|
||||
<Toggle id="t-zero" checked={showZero} onChange={setShowZero} label="Show zero-balance accounts" />
|
||||
</div>
|
||||
<div className="overflow-auto flex-1 bg-muted/30 p-6">
|
||||
<PreviewSheet
|
||||
report={structured ?? (flat ? flatToStructured(flat, activeMeta.name) : null)}
|
||||
companyName={associationName ?? "Company"}
|
||||
rangeLabel={rangeLabel}
|
||||
showCodes={showCodes}
|
||||
showCompare={showCompare && isFinancial}
|
||||
showZero={showZero}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setPreview(false)}>Close</Button>
|
||||
<Button onClick={exportPDF}><FileDown className="mr-1 h-4 w-4" /> Download PDF</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1390,7 +1394,7 @@ function computeBvaActuals(actualsData: any, grouped: Record<string, any[]>, bud
|
||||
return m;
|
||||
}
|
||||
|
||||
function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabel }: { companyId: string; from: string; to: string; currency: string; companyName: string; rangeLabel: string }) {
|
||||
function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabel, logoUrl }: { companyId: string; from: string; to: string; currency: string; companyName: string; rangeLabel: string; logoUrl?: string | null }) {
|
||||
const { data: budgets = [] } = useQuery({
|
||||
queryKey: ["budgets-active", companyId],
|
||||
enabled: !!companyId,
|
||||
@@ -1484,6 +1488,38 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
||||
const actualByAcct = useMemo(() => computeBvaActuals(actualsData, grouped, budgetByAcct), [actualsData, grouped, budgetByAcct]);
|
||||
const cmpActualByAcct = useMemo(() => computeBvaActuals(cmpActualsData, grouped, budgetByAcct), [cmpActualsData, grouped, budgetByAcct]);
|
||||
|
||||
// Comparison-window budget (pro-rated like budgetByAcct, over [cmpFrom, cmpTo]).
|
||||
const cmpBudgetByAcct = useMemo(() => {
|
||||
const m: Record<string, number> = {};
|
||||
if (!cmpOn || !cmpFrom || !cmpTo) return m;
|
||||
const pt = String(selectedBudget?.period_type ?? "annual");
|
||||
const fy = Number(selectedBudget?.fiscal_year) || new Date(cmpFrom || cmpTo || Date.now()).getFullYear();
|
||||
const fromT = new Date(cmpFrom).getTime();
|
||||
const toT = new Date(cmpTo).getTime();
|
||||
const DAY = 86400000;
|
||||
const span = (idx: number): [number, number] => {
|
||||
if (pt === "monthly") return [new Date(fy, idx, 1).getTime(), new Date(fy, idx + 1, 0).getTime()];
|
||||
if (pt === "quarterly") return [new Date(fy, idx * 3, 1).getTime(), new Date(fy, idx * 3 + 3, 0).getTime()];
|
||||
return [new Date(fy, 0, 1).getTime(), new Date(fy, 11, 31).getTime()];
|
||||
};
|
||||
for (const e of (entries as any[])) {
|
||||
const [s, en] = span(Number(e.period_index) || 0);
|
||||
const overlap = Math.max(0, Math.min(en, toT) - Math.max(s, fromT));
|
||||
const full = en - s;
|
||||
const weight = full > 0 ? Math.min(1, (overlap + DAY) / (full + DAY)) : 1;
|
||||
if (weight <= 0) continue;
|
||||
m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount) * weight;
|
||||
}
|
||||
return m;
|
||||
}, [entries, selectedBudget, cmpOn, cmpFrom, cmpTo]);
|
||||
|
||||
// Full-year budget per account (no pro-ration) for the Annual Budget column.
|
||||
const annualBudgetByAcct = useMemo(() => {
|
||||
const m: Record<string, number> = {};
|
||||
for (const e of (entries as any[])) m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount);
|
||||
return m;
|
||||
}, [entries]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
const sumGroup = (type: "income" | "expense") => {
|
||||
const accs = grouped[type] ?? [];
|
||||
@@ -1530,22 +1566,51 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const exportPDF = () => {
|
||||
const doc = new jsPDF({ unit: "pt", format: "letter" });
|
||||
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} · ${actualsLabel} · Budget pro-rated to period`, 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"; },
|
||||
// Export using the shared branded Budget vs. Actuals generator (current period +
|
||||
// optional comparison + Annual Budget columns), matching the main report style.
|
||||
const exportPDF = async () => {
|
||||
const nameById = new Map((accounts as any[]).map((a) => [a.id, a.name]));
|
||||
const parentIds = new Set((accounts as any[]).filter((a) => a.parent_account_id).map((a) => a.parent_account_id));
|
||||
const rows: any[] = [];
|
||||
for (const type of ["income", "expense"] as const) {
|
||||
for (const a of (grouped[type] ?? [])) {
|
||||
if (parentIds.has(a.id)) continue; // parent accounts render as group headers
|
||||
const budget = budgetByAcct[a.id] ?? 0;
|
||||
const actual = actualByAcct[a.id] ?? 0;
|
||||
const cmpA = cmpActualByAcct[a.id] ?? 0;
|
||||
const cmpB = cmpBudgetByAcct[a.id] ?? 0;
|
||||
const annual = annualBudgetByAcct[a.id] ?? 0;
|
||||
rows.push({
|
||||
id: a.id,
|
||||
category: a.code ? `${a.code} ${a.name}` : a.name,
|
||||
accountType: type,
|
||||
parentId: a.parent_account_id ?? null,
|
||||
parentCategory: a.parent_account_id ? (nameById.get(a.parent_account_id) ?? null) : null,
|
||||
budget, annualBudget: annual, actual, variance: actual - budget,
|
||||
pctOfBudget: budget ? (actual / budget) * 100 : 0,
|
||||
comparisonActual: cmpA, comparisonBudget: cmpB, comparisonVariance: cmpA - cmpB,
|
||||
comparisonPctOfBudget: cmpB ? (cmpA / cmpB) * 100 : 0, cmpDelta: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
const inc = rows.filter((r) => r.accountType === "income");
|
||||
const exp = rows.filter((r) => r.accountType !== "income");
|
||||
const sum = (rs: any[], k: string) => rs.reduce((s, r) => s + (Number(r[k]) || 0), 0);
|
||||
await generateBudgetVsActualPdf({
|
||||
association: { name: companyName, logo_url: logoUrl ?? null },
|
||||
fiscalYear: Number(selectedBudget?.fiscal_year) || new Date().getFullYear(),
|
||||
rangeLabel: actualsLabel,
|
||||
comparisonLabel: cmpOn && cmpFrom && cmpTo ? "Comparison" : null,
|
||||
comparisonRangeLabel: cmpOn && cmpFrom && cmpTo ? `${cmpFrom} to ${cmpTo}` : null,
|
||||
rows,
|
||||
totals: {
|
||||
incomeBudget: sum(inc, "budget"), incomeActual: sum(inc, "actual"),
|
||||
incomeCmp: sum(inc, "comparisonActual"), incomeCmpBudget: sum(inc, "comparisonBudget"),
|
||||
expenseBudget: sum(exp, "budget"), expenseActual: sum(exp, "actual"),
|
||||
expenseCmp: sum(exp, "comparisonActual"), expenseCmpBudget: sum(exp, "comparisonBudget"),
|
||||
},
|
||||
comparisonBudgetMonths: null,
|
||||
});
|
||||
doc.save(`${fileBase}.pdf`);
|
||||
};
|
||||
|
||||
if (!budgets.length) {
|
||||
@@ -1593,25 +1658,7 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Income vs Expenses</CardTitle></CardHeader>
|
||||
<CardContent className="h-72">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis tickFormatter={(v) => money(v, currency)} width={90} />
|
||||
<RTooltip formatter={(v: any) => money(Number(v), currency)} />
|
||||
<Legend />
|
||||
<Bar dataKey="Budget" fill="hsl(174 70% 45%)" />
|
||||
<Bar dataKey="Actual" fill="hsl(214 80% 55%)" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<ReportSheet title="Budget vs. Actuals" subtitle="Accrual basis" companyName={companyName} period={actualsLabel} logoUrl={logoUrl}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -1673,8 +1720,7 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ReportSheet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user