- {/* ── Header ─────────────────────────────────────────────────── */}
-
- {selectedAssocId !== "all" && (
-
- {zohoOrgId ? (
- zohoLoading ? (
- Loading from Zoho Books…
- ) : zohoError ? (
-
- Zoho pull failed — showing local data
-
- ) : (
-
- Live from Zoho Books
-
- )
- ) : (
-
- No Zoho organization linked — showing local data
-
- )}
-
- )}
-
- {/* ── Top Metrics ────────────────────────────────────────────── */}
-
- }
- trend={metrics.operatingBalance >= 0 ? "positive" : "negative"}
- iconBg="bg-primary/10 text-primary"
- />
- }
- trend="neutral"
- iconBg="bg-emerald-500/10 text-emerald-600"
- />
- }
- trend={metrics.accountsReceivable > 0 ? "warning" : "positive"}
- subtitle={`${
- zohoArCount !== null
- ? zohoArCount
- : filterByAssoc(owners).filter((o) => (o.balance || 0) > 0).length
- } accounts`}
- iconBg="bg-amber-500/10 text-amber-600"
- />
- }
- trend="neutral"
- subtitle={`${filterByAssoc(bills).filter(b => b.status === "pending").length} pending`}
- iconBg="bg-purple-500/10 text-purple-600"
- />
-
-
- {/* ── Charts Row ──────────────────────────────────────────────── */}
-
- {/* Income vs Expenses */}
-
-
-
-
Income vs Expenses
-
-
- Income
-
-
- Expenses
-
-
-
-
-
-
-
-
-
-
- `$${(v / 1000).toFixed(0)}k`} />
- } />
-
-
-
-
-
-
-
-
- {/* Expense Categories */}
-
-
- Expense Categories
-
-
-
- {expenseCategories.length > 0 ? (
-
-
-
- {expenseCategories.map((_, i) => (
- |
- ))}
-
- fmtFull(v)} />
- (
- {value}
- )}
- />
-
-
- ) : (
-
- No expense data available
-
- )}
-
-
-
-
-
- {/* ── Delinquency Trend ───────────────────────────────────────── */}
-
-
-
-
Delinquency Trend (6 Months)
-
-
- Amount
-
-
- Accounts
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `$${(v / 1000).toFixed(0)}k`} />
-
- } />
-
-
-
-
-
-
-
-
- {/* ── Bottom Row: Transactions + Quick Actions ─────────────── */}
-
- {/* Recent Transactions */}
-
-
-
- Recent Transactions
- navigate("/dashboard/payments")}>
- View All
-
-
-
-
- {recentTransactions.length > 0 ? (
-
-
-
- Date
- Description
- Type
- Amount
- Status
-
-
-
- {recentTransactions.map(tx => {
- const isCredit = (tx.credit || 0) > 0;
- const amount = isCredit ? tx.credit : tx.debit;
- return (
-
-
- {formatShortMonthDayEST(tx.date)}
-
-
-
- {tx.description || "Transaction"}
-
-
-
-
- {(tx.transaction_type || "other").replace(/_/g, " ")}
-
-
-
-
- {isCredit ? "+" : "-"}{fmtFull(amount || 0)}
-
-
-
-
- {tx.is_cleared ? "Cleared" : "Pending"}
-
-
-
- );
- })}
-
-
- ) : (
-
- No recent transactions found.
-
- )}
-
-
-
- {/* Quick Actions */}
-
-
- Quick Actions
-
-
- {quickActions.map(action => (
- navigate(action.path)}
- className="w-full flex items-center gap-3 p-3 rounded-lg border bg-card hover:bg-muted/50 transition-colors text-left group"
- >
-
-
-
-
+
+
+ {
+ const next = sorted.find((a) => a.id === id) ?? null;
+ setSelectedAssociation(next);
+ }}
+ >
+
+
+
+
+
+ {sorted.map((a) => (
+ {a.name}
))}
-
-
-
+
+
+
+
);
}
-
-// ─── Metric Card ──────────────────────────────────────────────────────
-function MetricCard({ title, value, icon, trend, subtitle, iconBg }: {
- title: string;
- value: string;
- icon: React.ReactNode;
- trend: "positive" | "negative" | "warning" | "neutral";
- subtitle?: string;
- iconBg: string;
-}) {
- return (
-
-
-
-
-
{title}
-
- {value}
-
- {subtitle &&
{subtitle}
}
-
-
- {icon}
-
-
-
- {/* Subtle accent bar */}
-
-
- );
-}
diff --git a/src/pages/FinancialReportsPage.tsx b/src/pages/FinancialReportsPage.tsx
new file mode 100644
index 0000000..f300105
--- /dev/null
+++ b/src/pages/FinancialReportsPage.tsx
@@ -0,0 +1,52 @@
+import { useMemo } from "react";
+import { Building2 } from "lucide-react";
+import { useAssociation } from "@/contexts/AssociationContext";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import AccountingReportsPage from "@/pages/accounting/AccountingReportsPage";
+
+/**
+ * Financial Reports now renders the same reporting suite used inside the
+ * Accounting module, scoped to a single association. Staff pick the association
+ * from the selector below (synced with the global association picker); board
+ * members are routed through their own scoped wrapper.
+ */
+export default function FinancialReportsPage() {
+ const { associations, selectedAssociation, setSelectedAssociation, loadingAssociations } =
+ useAssociation() as {
+ associations: { id: string; name: string }[];
+ selectedAssociation: { id: string; name: string } | null;
+ setSelectedAssociation: (a: { id: string; name: string } | null) => void;
+ loadingAssociations: boolean;
+ };
+
+ const sorted = useMemo(
+ () => [...(associations ?? [])].sort((a, b) => a.name.localeCompare(b.name)),
+ [associations],
+ );
+
+ return (
+
+
+ {
+ const next = sorted.find((a) => a.id === id) ?? null;
+ setSelectedAssociation(next);
+ }}
+ >
+
+
+
+
+
+ {sorted.map((a) => (
+ {a.name}
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/InvoiceTrackingPage.tsx b/src/pages/InvoiceTrackingPage.tsx
index 27016a9..0f82c77 100644
--- a/src/pages/InvoiceTrackingPage.tsx
+++ b/src/pages/InvoiceTrackingPage.tsx
@@ -15,7 +15,6 @@ import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
-import { pushBillToZohoAfterCreate } from "@/lib/zohoBillSync";
const statusColors: Record
= { pending: "bg-amber-100 text-amber-700", approved: "bg-blue-100 text-blue-700", paid: "bg-emerald-100 text-emerald-700", overdue: "bg-red-100 text-red-700" };
@@ -114,8 +113,7 @@ export default function InvoiceTrackingPage() {
if (editing) {
const { data: updatedInvoice, error } = await supabase.from("invoices").update(payload).eq("id", editing.id).select().single();
if (error) { toast({ title: "Error", description: error.message, variant: "destructive" }); return; }
- const billId = await syncInvoiceBillRecord(updatedInvoice);
- if (billId) pushBillToZohoAfterCreate(billId);
+ await syncInvoiceBillRecord(updatedInvoice);
toast({ title: "Invoice updated" });
} else {
const { data: assocs } = await supabase.from("associations").select("id").eq("status", "active").limit(1);
@@ -123,8 +121,7 @@ export default function InvoiceTrackingPage() {
const { data: userData } = await supabase.auth.getUser();
const { data: newInvoice, error } = await supabase.from("invoices").insert({ ...payload, association_id: assocs[0].id, created_by: userData?.user?.id || null }).select().single();
if (error) { toast({ title: "Error", description: error.message, variant: "destructive" }); return; }
- const billId = await syncInvoiceBillRecord(newInvoice);
- if (billId) pushBillToZohoAfterCreate(billId);
+ await syncInvoiceBillRecord(newInvoice);
toast({ title: "Invoice created" });
}
setDialogOpen(false); fetch();
@@ -229,8 +226,7 @@ export default function InvoiceTrackingPage() {
const { data: insertedInvoices, error } = await supabase.from("invoices").insert(payload).select();
if (error) throw error;
for (const invoice of insertedInvoices || []) {
- const billId = await syncInvoiceBillRecord(invoice);
- if (billId) pushBillToZohoAfterCreate(billId);
+ await syncInvoiceBillRecord(invoice);
}
toast({ title: `Imported ${rows.length} invoices` });
fetch();
diff --git a/src/pages/OwnerLedgerPage.tsx b/src/pages/OwnerLedgerPage.tsx
index b0e5cc7..1cf4b99 100644
--- a/src/pages/OwnerLedgerPage.tsx
+++ b/src/pages/OwnerLedgerPage.tsx
@@ -1,7 +1,6 @@
import { useState, useEffect, useMemo } from "react";
import { supabase } from "@/integrations/supabase/client";
import { useToast } from "@/hooks/use-toast";
-import { syncChargeToZoho } from "@/lib/zohoFinancialSync";
import { BookOpen, Plus, Search, Download, DollarSign } from "lucide-react";
import UnitOwnerSelect from "@/components/UnitOwnerSelect";
import { Button } from "@/components/ui/button";
@@ -108,7 +107,7 @@ export default function OwnerLedgerPage() {
const handleCreate = async () => {
if (!selectedOwnerId) return;
const owner = owners.find(o => o.id === selectedOwnerId);
- const { data: ledgerEntry } = await supabase.from("owner_ledger_entries").insert({
+ await supabase.from("owner_ledger_entries").insert({
owner_id: selectedOwnerId,
association_id: owner?.association_id,
unit_id: form.unit_id || owner?.unit_id || null,
@@ -117,7 +116,7 @@ export default function OwnerLedgerPage() {
description: form.description,
debit: parseFloat(form.debit) || 0,
credit: parseFloat(form.credit) || 0,
- }).select("id").single();
+ });
toast({ title: "Ledger entry added" });
setDialogOpen(false);
@@ -126,28 +125,6 @@ export default function OwnerLedgerPage() {
// Update owner balance
const newBal = currentBalance + (parseFloat(form.debit) || 0) - (parseFloat(form.credit) || 0);
await supabase.from("owners").update({ balance: newBal }).eq("id", selectedOwnerId);
-
- // Auto-sync to Zoho if association has zoho_organization_id
- if (ledgerEntry?.id && owner?.association_id) {
- const { data: assoc } = await supabase.from("associations").select("zoho_organization_id").eq("id", owner.association_id).single();
- if (assoc?.zoho_organization_id) {
- const isCharge = (parseFloat(form.debit) || 0) > 0;
- const isPayment = (parseFloat(form.credit) || 0) > 0;
- if (isCharge) {
- syncChargeToZoho(ledgerEntry.id).then(r => {
- if (r.success) toast({ title: "Synced to Zoho", description: "Charge pushed as invoice." });
- else console.error("Zoho sync failed:", r.error);
- });
- } else if (isPayment) {
- supabase.functions.invoke("zoho-books", {
- body: { action: "push_ledger_payment", params: { ledger_entry_id: ledgerEntry.id } },
- }).then(({ error }) => {
- if (!error) toast({ title: "Synced to Zoho", description: "Payment pushed." });
- else console.error("Zoho payment sync failed:", error);
- });
- }
- }
- }
};
const exportCSV = async () => {
diff --git a/src/pages/PaymentsPage.tsx b/src/pages/PaymentsPage.tsx
index 9637b04..ae6c231 100644
--- a/src/pages/PaymentsPage.tsx
+++ b/src/pages/PaymentsPage.tsx
@@ -1,6 +1,5 @@
import { useState, useEffect } from "react";
import { supabase } from "@/integrations/supabase/client";
-import { syncPaymentToZoho } from "@/lib/zohoFinancialSync";
import { useToast } from "@/hooks/use-toast";
import { CreditCard, Plus, Search, MoreHorizontal, Edit, Trash2, Receipt } from "lucide-react";
import RecordImportButton from "@/components/RecordImportButton";
@@ -108,15 +107,10 @@ export default function PaymentsPage() {
const { error } = await supabase.from("admin_payments").update(payload).eq("id", editingPayment.id);
if (error) { toast({ title: "Error", description: error.message, variant: "destructive" }); setSaving(false); return; }
toast({ title: "Payment updated" });
- // Best-effort: push to Zoho (records into banking journal)
- syncPaymentToZoho(editingPayment.id).catch((e) => console.warn("Zoho payment sync failed:", e));
} else {
- const { data: inserted, error } = await supabase.from("admin_payments").insert(payload).select("id").single();
+ const { error } = await supabase.from("admin_payments").insert(payload).select("id").single();
if (error) { toast({ title: "Error", description: error.message, variant: "destructive" }); setSaving(false); return; }
toast({ title: "Payment recorded" });
- if (inserted?.id) {
- syncPaymentToZoho(inserted.id).catch((e) => console.warn("Zoho payment sync failed:", e));
- }
}
setDialogOpen(false);
setSaving(false);
diff --git a/src/pages/RecordOwnerPaymentPage.tsx b/src/pages/RecordOwnerPaymentPage.tsx
index ace49e0..e32e2e5 100644
--- a/src/pages/RecordOwnerPaymentPage.tsx
+++ b/src/pages/RecordOwnerPaymentPage.tsx
@@ -93,19 +93,6 @@ export default function RecordOwnerPaymentPage() {
toast({ title: "Payment recorded successfully" });
- // Auto-sync to Zoho if association has zoho_organization_id
- if (ledgerEntry?.id && owner?.association_id) {
- const { data: assoc } = await supabase.from("associations").select("zoho_organization_id").eq("id", owner.association_id).single();
- if (assoc?.zoho_organization_id) {
- supabase.functions.invoke("zoho-books", {
- body: { action: "push_ledger_payment", params: { ledger_entry_id: ledgerEntry.id } },
- }).then(({ error }) => {
- if (!error) toast({ title: "Synced to Zoho", description: "Payment pushed to Zoho Books." });
- else console.error("Zoho payment sync failed:", error);
- });
- }
- }
-
setForm({ ...form, owner_id: "", amount: "", reference_number: "", description: "" });
setSaving(false);
diff --git a/src/pages/VendorsPage.tsx b/src/pages/VendorsPage.tsx
index 4bb1776..476a153 100644
--- a/src/pages/VendorsPage.tsx
+++ b/src/pages/VendorsPage.tsx
@@ -18,7 +18,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import VendorCoaMappings from "@/components/vendors/VendorCoaMappings";
import { BulkVendorEditDialog } from "@/components/vendors/BulkVendorEditDialog";
-const emptyForm = { name: "", contact_name: "", email: "", phone: "", address: "", tax_id: "", payment_terms: "30", notes: "", association_id: "", association_ids: [] as string[], is_active: true, is_1099_eligible: false, contract_end_date: "", insurance_carrier: "", insurance_policy_number: "", insurance_expiration_date: "" };
+const emptyForm = { name: "", contact_name: "", email: "", phone: "", address: "", tax_id: "", payment_terms: "30", notes: "", association_id: "", association_ids: [] as string[], is_active: true, is_1099_eligible: false, share_with_board: false, contract_end_date: "", insurance_carrier: "", insurance_policy_number: "", insurance_expiration_date: "" };
// Insurance status: active if expiration date is today or later
function getInsuranceStatus(v: any) {
@@ -110,6 +110,7 @@ export default function VendorsPage() {
association_ids: ids,
is_active: v.is_active,
is_1099_eligible: v.is_1099_eligible || false,
+ share_with_board: v.share_with_board || false,
contract_end_date: v.contract_end_date || "",
insurance_carrier: v.insurance_carrier || "",
insurance_policy_number: v.insurance_policy_number || "",
@@ -136,10 +137,10 @@ export default function VendorsPage() {
const primary = form.association_ids.includes(form.association_id) ? form.association_id : form.association_ids[0];
const payload = { ...form, association_id: primary, contract_end_date: form.contract_end_date || null, insurance_expiration_date: form.insurance_expiration_date || null, insurance_carrier: form.insurance_carrier || null, insurance_policy_number: form.insurance_policy_number || null };
if (editing) {
- await supabase.from("vendors").update(payload).eq("id", editing.id);
+ await supabase.from("vendors").update(payload as any).eq("id", editing.id);
toast({ title: "Vendor updated" });
} else {
- await supabase.from("vendors").insert(payload);
+ await supabase.from("vendors").insert(payload as any);
toast({ title: "Vendor created" });
}
setDialogOpen(false);
@@ -400,9 +401,14 @@ export default function VendorsPage() {
})()}
-
- {v.is_active ? "Active" : "Inactive"}
-
+
+
+ {v.is_active ? "Active" : "Inactive"}
+
+ {v.share_with_board && (
+ Shared with board
+ )}
+
@@ -521,6 +527,14 @@ export default function VendorsPage() {
/>
1099 Eligible