mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
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 {
|
||||
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<Owner | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState("ledger");
|
||||
|
||||
// Sub-data
|
||||
const [ledger, setLedger] = useState<any[]>([]);
|
||||
const [computedBalance, setComputedBalance] = useState<number | null>(null);
|
||||
const [violations, setViolations] = useState<any[]>([]);
|
||||
const [arcApps, setArcApps] = useState<any[]>([]);
|
||||
const [requests, setRequests] = useState<any[]>([]);
|
||||
const [documents, setDocuments] = useState<any[]>([]);
|
||||
const [activity, setActivity] = useState<any[]>([]);
|
||||
|
||||
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<string>();
|
||||
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<string>();
|
||||
(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 (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="max-w-[1440px] mx-auto p-5 space-y-5">
|
||||
{/* Back nav */}
|
||||
<Button variant="ghost" size="sm" className="text-[13px] text-muted-foreground hover:text-foreground gap-1.5 -ml-2 h-8" onClick={() => navigate("/dashboard/owner-roster")}>
|
||||
<ArrowLeft className="w-3.5 h-3.5" /> Owner Roster
|
||||
</Button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 shrink-0">
|
||||
<User className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<h1 className="text-[22px] font-semibold tracking-tight">{fullName}</h1>
|
||||
<Badge variant="outline" className={`text-2xs ${balance > 0 ? "cc-badge-danger" : "cc-badge-success"}`}>
|
||||
{balance > 0 ? "Balance Due" : "Current"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-[13px] text-muted-foreground mt-0.5">
|
||||
{owner.property_address || owner.units?.address || "No property address"}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
{owner.units?.account_number && (
|
||||
<span className="text-[12px] text-muted-foreground">Acct #{owner.units.account_number}</span>
|
||||
)}
|
||||
<span className="text-[12px] text-muted-foreground">•</span>
|
||||
<span className="text-[12px] text-muted-foreground">{owner.associations?.name || "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Balance & Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-right mr-2">
|
||||
<p className="text-[11px] text-muted-foreground uppercase tracking-wide font-medium">Account Balance</p>
|
||||
<p className={`text-[24px] font-semibold tracking-tight leading-none mt-0.5 ${balance > 0 ? "text-destructive" : "text-emerald-600"}`}>
|
||||
${Math.abs(balance).toLocaleString("en-US", { minimumFractionDigits: 2 })}
|
||||
</p>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-10" />
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button size="sm" variant="outline" className="h-8 text-[12px] gap-1.5" onClick={() => navigate(`/dashboard/compose-email?${ownerActionParams.toString()}`)}>
|
||||
<Send className="w-3.5 h-3.5" /> Message
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="h-8 text-[12px] gap-1.5" onClick={() => navigate(`/dashboard/violations?new=1&${ownerActionParams.toString()}`)}>
|
||||
<AlertTriangle className="w-3.5 h-3.5" /> Violation
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="h-8 text-[12px] gap-1.5" onClick={() => navigate(`/dashboard/owner-updates?${ownerActionParams.toString()}`)}>
|
||||
<StickyNote className="w-3.5 h-3.5" /> Note
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Summary + Property */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Contact Info */}
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="pb-2 px-4 pt-4">
|
||||
<CardTitle className="text-[15px] font-semibold flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-primary" /> Contact Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4 space-y-3">
|
||||
<InfoRow icon={<Phone className="w-3.5 h-3.5" />} label="Phone" value={owner.phone || "—"} />
|
||||
<InfoRow icon={<Mail className="w-3.5 h-3.5" />} label="Email" value={owner.email || "—"} />
|
||||
<InfoRow icon={<MapPin className="w-3.5 h-3.5" />} label="Mailing Address" value={owner.mailing_address || "—"} />
|
||||
<InfoRow icon={<MapPin className="w-3.5 h-3.5" />} label="Street Address" value={owner.street_address || "—"} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Property Details */}
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="pb-2 px-4 pt-4">
|
||||
<CardTitle className="text-[15px] font-semibold flex items-center gap-2">
|
||||
<Home className="w-4 h-4 text-primary" /> Property Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4 space-y-3">
|
||||
<InfoRow icon={<Home className="w-3.5 h-3.5" />} label="Unit" value={owner.units?.unit_number || "—"} />
|
||||
<InfoRow icon={<MapPin className="w-3.5 h-3.5" />} label="Property Address" value={owner.property_address || owner.units?.address || "—"} />
|
||||
<InfoRow icon={<Shield className="w-3.5 h-3.5" />} label="Status" value={owner.units?.status || "Active"} />
|
||||
<InfoRow icon={<Calendar className="w-3.5 h-3.5" />} label="Member Since" value={format(new Date(owner.created_at), "MMM d, yyyy")} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="bg-muted/50 h-9">
|
||||
<TabsTrigger value="ledger" className="text-[13px] h-7 gap-1.5"><DollarSign className="w-3.5 h-3.5" /> Ledger</TabsTrigger>
|
||||
<TabsTrigger value="violations" className="text-[13px] h-7 gap-1.5"><Shield className="w-3.5 h-3.5" /> Violations</TabsTrigger>
|
||||
<TabsTrigger value="arc" className="text-[13px] h-7 gap-1.5"><ClipboardCheck className="w-3.5 h-3.5" /> ARC</TabsTrigger>
|
||||
<TabsTrigger value="requests" className="text-[13px] h-7 gap-1.5"><MessageCircle className="w-3.5 h-3.5" /> Requests</TabsTrigger>
|
||||
<TabsTrigger value="documents" className="text-[13px] h-7 gap-1.5"><FolderOpen className="w-3.5 h-3.5" /> Documents</TabsTrigger>
|
||||
<TabsTrigger value="notes" className="text-[13px] h-7 gap-1.5"><StickyNote className="w-3.5 h-3.5" /> Notes</TabsTrigger>
|
||||
<TabsTrigger value="activity" className="text-[13px] h-7 gap-1.5"><Activity className="w-3.5 h-3.5" /> Activity</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* LEDGER */}
|
||||
<TabsContent value="ledger">
|
||||
{owner.unit_id ? (
|
||||
<UnitLedgerView unitId={owner.unit_id} associationId={owner.association_id} />
|
||||
) : (
|
||||
<Card className="border shadow-sm">
|
||||
<CardContent className="py-8 text-center text-muted-foreground text-sm">
|
||||
No unit linked to this owner. Assign a unit to view the full ledger.
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* VIOLATIONS */}
|
||||
<TabsContent value="violations">
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
<CardTitle className="text-[15px] font-semibold">Violations</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Type</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Status</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Date Issued</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Due Date</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Stage</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4 w-8"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{violations.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={6} className="text-center text-[13px] text-muted-foreground py-8">No violations</TableCell></TableRow>
|
||||
) : (
|
||||
violations.map((v) => (
|
||||
<TableRow key={v.id} className="hover:bg-muted/30 cursor-pointer" onClick={() => navigate(`/dashboard/violations`)}>
|
||||
<TableCell className="text-[13px] py-2.5 px-4 font-medium">{v.violation_type || "—"}</TableCell>
|
||||
<TableCell className="py-2.5 px-4">
|
||||
<StatusBadge status={v.status} />
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px] py-2.5 px-4">{v.created_at ? format(new Date(v.created_at), "MMM d, yyyy") : "—"}</TableCell>
|
||||
<TableCell className="text-[13px] py-2.5 px-4">{v.due_date ? format(new Date(v.due_date), "MMM d, yyyy") : "—"}</TableCell>
|
||||
<TableCell className="text-[13px] py-2.5 px-4">{v.current_stage || "—"}</TableCell>
|
||||
<TableCell className="py-2.5 px-4"><ChevronRight className="w-3.5 h-3.5 text-muted-foreground/40" /></TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* ARC */}
|
||||
<TabsContent value="arc">
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
<CardTitle className="text-[15px] font-semibold">ARC Applications</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Title</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Type</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Status</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Submitted</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Review Date</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{arcApps.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={5} className="text-center text-[13px] text-muted-foreground py-8">No ARC applications</TableCell></TableRow>
|
||||
) : (
|
||||
arcApps.map((a) => (
|
||||
<TableRow key={a.id} className="hover:bg-muted/30">
|
||||
<TableCell className="text-[13px] py-2.5 px-4 font-medium">{a.title}</TableCell>
|
||||
<TableCell className="text-[13px] py-2.5 px-4">{a.project_type || "—"}</TableCell>
|
||||
<TableCell className="py-2.5 px-4"><StatusBadge status={a.status} /></TableCell>
|
||||
<TableCell className="text-[13px] py-2.5 px-4">{a.submitted_date ? format(new Date(a.submitted_date), "MMM d, yyyy") : "—"}</TableCell>
|
||||
<TableCell className="text-[13px] py-2.5 px-4">{a.review_date ? format(new Date(a.review_date), "MMM d, yyyy") : "—"}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* REQUESTS */}
|
||||
<TabsContent value="requests">
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
<CardTitle className="text-[15px] font-semibold">Homeowner Requests</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Subject</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Category</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Status</TableHead>
|
||||
<TableHead className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground bg-muted/50 py-2 px-4">Submitted</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{requests.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center text-[13px] text-muted-foreground py-8">No requests</TableCell></TableRow>
|
||||
) : (
|
||||
requests.map((r) => (
|
||||
<TableRow key={r.id} className="hover:bg-muted/30">
|
||||
<TableCell className="text-[13px] py-2.5 px-4 font-medium">{r.subject || r.title || "—"}</TableCell>
|
||||
<TableCell className="text-[13px] py-2.5 px-4">{r.category || "—"}</TableCell>
|
||||
<TableCell className="py-2.5 px-4"><StatusBadge status={r.status} /></TableCell>
|
||||
<TableCell className="text-[13px] py-2.5 px-4">{r.created_at ? format(new Date(r.created_at), "MMM d, yyyy") : "—"}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* DOCUMENTS */}
|
||||
<TabsContent value="documents">
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
<CardTitle className="text-[15px] font-semibold">Documents</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y divide-border">
|
||||
{documents.length === 0 ? (
|
||||
<p className="text-center text-[13px] text-muted-foreground py-8">No documents</p>
|
||||
) : (
|
||||
documents.map((doc) => (
|
||||
<div key={doc.id} className="flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 shrink-0">
|
||||
<FileText className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[13px] font-medium truncate">{doc.title}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{doc.category || "General"} • {doc.created_at ? format(new Date(doc.created_at), "MMM d, yyyy") : ""}</p>
|
||||
</div>
|
||||
</div>
|
||||
{doc.file_url && (
|
||||
<Button variant="ghost" size="sm" className="h-7 text-[11px] gap-1 text-muted-foreground hover:text-primary">
|
||||
<ExternalLink className="w-3 h-3" /> View
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* NOTES */}
|
||||
<TabsContent value="notes">
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
<CardTitle className="text-[15px] font-semibold">Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4">
|
||||
<p className="text-[13px] text-muted-foreground text-center py-8">No notes yet. Click "Note" above to add one.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* ACTIVITY */}
|
||||
<TabsContent value="activity">
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
<CardTitle className="text-[15px] font-semibold flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-primary" /> Activity Timeline
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4">
|
||||
{activity.length === 0 ? (
|
||||
<p className="text-[13px] text-muted-foreground text-center py-8">No recent activity</p>
|
||||
) : (
|
||||
<div className="relative pl-6 space-y-0">
|
||||
{/* Timeline line */}
|
||||
<div className="absolute left-[9px] top-2 bottom-2 w-px bg-border" />
|
||||
{activity.map((item, i) => (
|
||||
<div key={item.id + i} className="relative flex items-start gap-3 py-2.5">
|
||||
<div className={`absolute left-[-15px] top-3 flex h-5 w-5 items-center justify-center rounded-full border-2 border-background ${item.icon === "dollar" ? "bg-emerald-100" : "bg-destructive/10"}`}>
|
||||
{item.icon === "dollar" ? (
|
||||
<DollarSign className="w-2.5 h-2.5 text-emerald-600" />
|
||||
) : (
|
||||
<Shield className="w-2.5 h-2.5 text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[13px] font-medium text-foreground">{item.text}</p>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">{item.date ? format(new Date(item.date), "MMM d, yyyy") : ""}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Helpers ────────────────────── */
|
||||
|
||||
function InfoRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-muted/80 text-muted-foreground shrink-0 mt-0.5">
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[11px] text-muted-foreground font-medium uppercase tracking-wide">{label}</p>
|
||||
<p className="text-[13px] text-foreground">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-2xs font-medium border capitalize ${cls}`}>
|
||||
{status || "—"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user