import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { supabase } from "@/integrations/supabase/client"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import RvRentalVacateButton from "@/components/RvRentalVacateButton"; import { ArrowLeft, Mail, Phone, MapPin, Send, AlertTriangle, StickyNote, Download, Filter, FileText, Shield, ClipboardCheck, MessageCircle, FolderOpen, Activity, Calendar, DollarSign, User, Home, Clock, ChevronRight, ExternalLink } from "lucide-react"; import { format } from "date-fns"; import { useToast } from "@/hooks/use-toast"; import UnitLedgerView from "@/components/unit-profile/UnitLedgerView"; interface Owner { id: string; first_name: string; last_name: string; email: string | null; phone: string | null; street_address: string | null; mailing_address: string | null; property_address: string | null; balance: number | null; unit_id: string | null; association_id: string; electronic_consent: boolean; zoho_contact_id: string | null; created_at: string; units?: { unit_number: string; address: string | null; status: string | null; account_number: string | null } | null; associations?: { name: string } | null; } export default function OwnerProfilePage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { toast } = useToast(); const [owner, setOwner] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState("ledger"); // Sub-data const [ledger, setLedger] = useState([]); const [computedBalance, setComputedBalance] = useState(null); const [violations, setViolations] = useState([]); const [arcApps, setArcApps] = useState([]); const [requests, setRequests] = useState([]); const [documents, setDocuments] = useState([]); const [activity, setActivity] = useState([]); useEffect(() => { if (id) fetchOwner(); }, [id]); useEffect(() => { if (owner) fetchTabData(); }, [owner, activeTab]); const fetchOwner = async () => { setLoading(true); const { data, error } = await supabase .from("owners") .select("*, units(unit_number, address, status, account_number), associations(name)") .eq("id", id!) .single(); if (error) { toast({ title: "Error", description: "Owner not found", variant: "destructive" }); navigate("/dashboard/owner-roster"); return; } setOwner(data as Owner); // Compute live balance from ledger entries at the UNIT level // This ensures co-owners see the full unit balance, not just their individual entries const unitId = (data as Owner).unit_id; if (unitId) { const { data: ledgerEntries } = await supabase .from("owner_ledger_entries") .select("debit, credit") .eq("unit_id", unitId); const liveBalance = (ledgerEntries || []).reduce((acc, e) => acc + (Number(e.debit) || 0) - (Number(e.credit) || 0), 0); setComputedBalance(liveBalance); } else { // Fallback: no unit linked, use owner_id const { data: ledgerEntries } = await supabase .from("owner_ledger_entries") .select("debit, credit") .eq("owner_id", id!); const liveBalance = (ledgerEntries || []).reduce((acc, e) => acc + (Number(e.debit) || 0) - (Number(e.credit) || 0), 0); setComputedBalance(liveBalance); } setLoading(false); }; const fetchTabData = async () => { if (!owner) return; switch (activeTab) { case "ledger": { const { data } = await supabase .from("owner_ledger_entries") .select("*") .eq("owner_id", owner.id) .order("date", { ascending: false }) .limit(100); setLedger(data || []); break; } case "violations": { // Fetch violations linked by owner_id, unit_id, or property address const filters: string[] = [`owner_id.eq.${owner.id}`]; if (owner.unit_id) filters.push(`unit_id.eq.${owner.unit_id}`); if (owner.property_address) filters.push(`address.eq.${owner.property_address}`); const { data } = await supabase .from("violations") .select("*") .or(filters.join(",")) .order("created_at", { ascending: false }); // Deduplicate by id const seen = new Set(); const unique = (data || []).filter(v => { if (seen.has(v.id)) return false; seen.add(v.id); return true; }); setViolations(unique); break; } case "arc": { const { data } = await supabase .from("arc_applications") .select("*") .eq("owner_id", owner.id) .order("created_at", { ascending: false }); setArcApps(data || []); break; } case "requests": { const { data } = await supabase .from("homeowner_requests") .select("*") .eq("owner_id", owner.id) .order("created_at", { ascending: false }); setRequests(data || []); break; } case "documents": { // Only show documents tied to this owner's unit if (!owner.unit_id) { setDocuments([]); break; } const { data } = await supabase .from("unit_documents") .select("*") .eq("unit_id", owner.unit_id) .order("created_at", { ascending: false }) .limit(100); setDocuments(data || []); break; } case "activity": { // Build activity from multiple sources const items: any[] = []; const { data: ledgerData } = await supabase .from("owner_ledger_entries") .select("id, date, description, transaction_type, debit, credit") .eq("owner_id", owner.id) .order("date", { ascending: false }) .limit(20); (ledgerData || []).forEach(e => items.push({ id: e.id, type: "payment", text: e.description || e.transaction_type, date: e.date, icon: "dollar" })); const violFilters: string[] = [`owner_id.eq.${owner.id}`]; if (owner.unit_id) violFilters.push(`unit_id.eq.${owner.unit_id}`); if (owner.property_address) violFilters.push(`address.eq.${owner.property_address}`); const { data: violData } = await supabase .from("violations") .select("id, created_at, violation_type, status") .or(violFilters.join(",")) .order("created_at", { ascending: false }) .limit(10); const violSeen = new Set(); (violData || []).filter(v => { if (violSeen.has(v.id)) return false; violSeen.add(v.id); return true; }).forEach(v => items.push({ id: v.id, type: "violation", text: `Violation: ${v.violation_type || "Notice"}`, date: v.created_at, icon: "shield" })); items.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); setActivity(items.slice(0, 20)); break; } } }; if (loading || !owner) { return (
); } const fullName = `${owner.first_name} ${owner.last_name}`; const balance = computedBalance ?? owner.balance ?? 0; const ownerActionParams = new URLSearchParams({ associationId: owner.association_id, ownerId: owner.id, name: fullName, }); if (owner.email) ownerActionParams.set("email", owner.email); if (owner.unit_id) ownerActionParams.set("unitId", owner.unit_id); return (
{/* Back nav */} {/* Header */}

{fullName}

0 ? "cc-badge-danger" : "cc-badge-success"}`}> {balance > 0 ? "Balance Due" : "Current"}

{owner.property_address || owner.units?.address || "No property address"}

{owner.units?.account_number && ( Acct #{owner.units.account_number} )} {owner.associations?.name || "—"}
{/* Balance & Actions */}

Account Balance

0 ? "text-destructive" : "text-emerald-600"}`}> ${Math.abs(balance).toLocaleString("en-US", { minimumFractionDigits: 2 })}

{/* Profile Summary + Property */}
{/* Contact Info */} Contact Information } label="Phone" value={owner.phone || "—"} /> } label="Email" value={owner.email || "—"} /> } label="Mailing Address" value={owner.mailing_address || "—"} /> } label="Street Address" value={owner.street_address || "—"} /> {/* Property Details */} Property Details } label="Unit" value={owner.units?.unit_number || "—"} /> } label="Property Address" value={owner.property_address || owner.units?.address || "—"} /> } label="Status" value={owner.units?.status || "Active"} /> } label="Member Since" value={format(new Date(owner.created_at), "MMM d, yyyy")} />
{/* Tabs */} Ledger Violations ARC Requests Documents Notes Activity {/* LEDGER */} {owner.unit_id ? ( ) : ( No unit linked to this owner. Assign a unit to view the full ledger. )} {/* VIOLATIONS */} Violations Type Status Date Issued Due Date Stage {violations.length === 0 ? ( No violations ) : ( violations.map((v) => ( navigate(`/dashboard/violations`)}> {v.violation_type || "—"} {v.created_at ? format(new Date(v.created_at), "MMM d, yyyy") : "—"} {v.due_date ? format(new Date(v.due_date), "MMM d, yyyy") : "—"} {v.current_stage || "—"} )) )}
{/* ARC */} ARC Applications Title Type Status Submitted Review Date {arcApps.length === 0 ? ( No ARC applications ) : ( arcApps.map((a) => ( {a.title} {a.project_type || "—"} {a.submitted_date ? format(new Date(a.submitted_date), "MMM d, yyyy") : "—"} {a.review_date ? format(new Date(a.review_date), "MMM d, yyyy") : "—"} )) )}
{/* REQUESTS */} Homeowner Requests Subject Category Status Submitted {requests.length === 0 ? ( No requests ) : ( requests.map((r) => ( {r.subject || r.title || "—"} {r.category || "—"} {r.created_at ? format(new Date(r.created_at), "MMM d, yyyy") : "—"} )) )}
{/* DOCUMENTS */} Documents
{documents.length === 0 ? (

No documents

) : ( documents.map((doc) => (

{doc.title}

{doc.category || "General"} • {doc.created_at ? format(new Date(doc.created_at), "MMM d, yyyy") : ""}

{doc.file_url && ( )}
)) )}
{/* NOTES */} Notes

No notes yet. Click "Note" above to add one.

{/* ACTIVITY */} Activity Timeline {activity.length === 0 ? (

No recent activity

) : (
{/* Timeline line */}
{activity.map((item, i) => (
{item.icon === "dollar" ? ( ) : ( )}

{item.text}

{item.date ? format(new Date(item.date), "MMM d, yyyy") : ""}

))}
)}
); } /* ── Helpers ────────────────────── */ function InfoRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) { return (
{icon}

{label}

{value}

); } function StatusBadge({ status }: { status: string | null }) { const s = (status || "").toLowerCase(); const cls = s === "open" || s === "submitted" || s === "pending" ? "cc-badge-warning" : s === "approved" || s === "resolved" || s === "completed" || s === "current" ? "cc-badge-success" : s === "denied" || s === "closed" ? "cc-badge-danger" : "cc-badge-neutral"; return ( {status || "—"} ); }