"use client"; import * as React from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { Users, Search, MoreHorizontal, Eye, Merge, Trash2, Link2, Loader2, ChevronLeft, ChevronRight, CalendarPlus, } from "lucide-react"; import { formatDistanceToNow, differenceInDays, format } from "date-fns"; import { Header } from "@/components/layout/header"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; type PersonRow = { id: string; firstName: string; lastName: string; email: string | null; cellPhone: string | null; createdAt: string; updatedAt: string; _count: { cards: number }; cards?: { createdAt: string }[]; }; type PeopleResponse = { people: PersonRow[]; total: number; page: number; limit: number; }; function formatDate(dateStr: string): string { const d = new Date(dateStr); if (differenceInDays(new Date(), d) < 30) { return formatDistanceToNow(d, { addSuffix: true }); } return format(d, "MMM d, yyyy"); } export default function PeoplePage() { const router = useRouter(); const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [search, setSearch] = React.useState(""); const [debouncedSearch, setDebouncedSearch] = React.useState(""); const [page, setPage] = React.useState(1); const [linking, setLinking] = React.useState(false); const [deletingId, setDeletingId] = React.useState(null); const limit = 20; React.useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(search); setPage(1); }, 300); return () => clearTimeout(timer); }, [search]); const fetchPeople = React.useCallback(async () => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), limit: String(limit), }); if (debouncedSearch) params.set("search", debouncedSearch); const res = await fetch(`/api/people?${params.toString()}`); if (!res.ok) throw new Error(); const json = await res.json(); setData(json); } catch { toast.error("Failed to load people"); } finally { setLoading(false); } }, [page, debouncedSearch]); React.useEffect(() => { fetchPeople(); }, [fetchPeople]); const totalPages = data ? Math.max(1, Math.ceil(data.total / limit)) : 1; const newThisMonth = React.useMemo(() => { if (!data) return 0; const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); return data.people.filter( (p) => new Date(p.createdAt) >= startOfMonth ).length; }, [data]); const handleLinkCards = async () => { setLinking(true); try { const res = await fetch("/api/people/link", { method: "POST" }); if (!res.ok) throw new Error(); const result = await res.json(); toast.success( `Linked ${result.linked} card${result.linked !== 1 ? "s" : ""}, created ${result.created} new ${result.created !== 1 ? "people" : "person"}` ); fetchPeople(); } catch { toast.error("Failed to link cards"); } finally { setLinking(false); } }; const handleDelete = async (personId: string) => { setDeletingId(personId); try { const res = await fetch(`/api/people/${personId}`, { method: "DELETE" }); if (!res.ok) throw new Error(); toast.success("Person deleted"); fetchPeople(); } catch { toast.error("Failed to delete person"); } finally { setDeletingId(null); } }; if (loading && !data) { return (
{Array.from({ length: 6 }).map((_, i) => ( ))}
); } const people = data?.people ?? []; const isEmpty = !loading && people.length === 0 && !debouncedSearch; const noResults = !loading && people.length === 0 && !!debouncedSearch; return (
{isEmpty ? (

No people yet

People are automatically created when response cards are processed.

) : ( <>
setSearch(e.target.value)} className="pl-9" />

{data?.total.toLocaleString() ?? "—"}

Total People

{newThisMonth}

New This Month

{noResults ? (

No results

No people match “{debouncedSearch}”

) : (
Name Email Phone Cards Last Seen {loading ? Array.from({ length: 5 }).map((_, i) => ( )) : people.map((person) => { const lastSeen = person.cards?.[0]?.createdAt ?? person.updatedAt; return ( {person.email || "—"} {person.cellPhone || "—"} {person._count.cards} {formatDate(lastSeen)} } > router.push(`/people/${person.id}`) } > View router.push( `/people/${person.id}?action=merge` ) } > Merge handleDelete(person.id)} > Delete ); })}
)} {totalPages > 1 && (

Page {page} of {totalPages}

)} )}
); }