"use client" import { useState, useEffect, useCallback } from "react" import { Card, CardContent } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Input } from "@/components/ui/input" import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table" import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@/components/ui/dropdown-menu" import { Progress } from "@/components/ui/progress" import { useToast } from "@/components/ui/toast" import { Search, MoreVertical, Calendar, Clock, AlertTriangle, CheckCircle2, XCircle, MessageCircle, Send, Filter, ChevronLeft, ChevronRight, Users, Loader2 } from "lucide-react" interface Pledge { id: string reference: string amountPence: number status: string rail: string donorName: string | null donorEmail: string | null donorPhone: string | null giftAid: boolean dueDate: string | null planId: string | null installmentNumber: number | null installmentTotal: number | null eventName: string qrSourceLabel: string | null volunteerName: string | null createdAt: string paidAt: string | null } const statusConfig: Record = { new: { label: "Pending", variant: "secondary", icon: Clock }, initiated: { label: "Initiated", variant: "warning", icon: Send }, paid: { label: "Paid", variant: "success", icon: CheckCircle2 }, overdue: { label: "Overdue", variant: "destructive", icon: AlertTriangle }, cancelled: { label: "Cancelled", variant: "secondary", icon: XCircle }, } const formatPence = (p: number) => `£${(p / 100).toLocaleString("en-GB", { minimumFractionDigits: 0 })}` function timeAgo(dateStr: string) { const d = new Date(dateStr) const now = new Date() const diff = now.getTime() - d.getTime() const days = Math.floor(diff / 86400000) if (days === 0) return "Today" if (days === 1) return "Yesterday" if (days < 7) return `${days}d ago` if (days < 30) return `${Math.floor(days / 7)}w ago` return d.toLocaleDateString("en-GB", { day: "numeric", month: "short" }) } function dueLabel(dueDate: string) { const d = new Date(dueDate) const now = new Date() const diff = d.getTime() - now.getTime() const days = Math.ceil(diff / 86400000) if (days < 0) return { text: `${Math.abs(days)}d overdue`, urgent: true } if (days === 0) return { text: "Due today", urgent: true } if (days === 1) return { text: "Due tomorrow", urgent: false } if (days <= 7) return { text: `Due in ${days}d`, urgent: false } return { text: d.toLocaleDateString("en-GB", { day: "numeric", month: "short" }), urgent: false } } export default function PledgesPage() { const [pledges, setPledges] = useState([]) const [total, setTotal] = useState(0) const [loading, setLoading] = useState(true) const [tab, setTab] = useState("all") const [search, setSearch] = useState("") const [page, setPage] = useState(0) const [updating, setUpdating] = useState(null) const { toast } = useToast() const pageSize = 25 // Stats const [stats, setStats] = useState({ total: 0, pending: 0, dueSoon: 0, overdue: 0, paid: 0, totalPledged: 0, totalCollected: 0 }) const fetchPledges = useCallback(async () => { const params = new URLSearchParams() params.set("limit", String(pageSize)) params.set("offset", String(page * pageSize)) if (tab !== "all") { if (tab === "due-soon") params.set("dueSoon", "true") else if (tab === "overdue") params.set("overdue", "true") else params.set("status", tab) } if (search) params.set("search", search) params.set("sort", tab === "due-soon" ? "dueDate" : "createdAt") params.set("dir", tab === "due-soon" ? "asc" : "desc") const res = await fetch(`/api/pledges?${params}`) const data = await res.json() setPledges(data.pledges || []) setTotal(data.total || 0) setLoading(false) }, [tab, search, page]) const fetchStats = useCallback(async () => { const res = await fetch("/api/dashboard") const data = await res.json() if (data.summary) { setStats({ total: data.summary.totalPledges, pending: data.byStatus?.new || 0, dueSoon: 0, // calculated client-side overdue: data.byStatus?.overdue || 0, paid: data.byStatus?.paid || 0, totalPledged: data.summary.totalPledgedPence, totalCollected: data.summary.totalCollectedPence, }) } }, []) useEffect(() => { fetchPledges() }, [fetchPledges]) useEffect(() => { fetchStats() }, [fetchStats]) // Auto-refresh useEffect(() => { const interval = setInterval(fetchPledges, 30000) return () => clearInterval(interval) }, [fetchPledges]) const updateStatus = async (pledgeId: string, newStatus: string) => { setUpdating(pledgeId) try { await fetch(`/api/pledges/${pledgeId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: newStatus }), }) setPledges(prev => prev.map(p => p.id === pledgeId ? { ...p, status: newStatus } : p)) toast(`Pledge marked as ${newStatus}`, "success") } catch { toast("Failed to update", "error") } setUpdating(null) } const sendReminder = async (pledge: Pledge) => { if (!pledge.donorPhone) { toast("No phone number — can't send WhatsApp", "error") return } try { await fetch("/api/whatsapp/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "reminder", phone: pledge.donorPhone, data: { donorName: pledge.donorName, amountPounds: (pledge.amountPence / 100).toFixed(0), eventName: pledge.eventName, reference: pledge.reference, daysSincePledge: Math.floor((Date.now() - new Date(pledge.createdAt).getTime()) / 86400000), step: 1, }, }), }) toast("Reminder sent via WhatsApp ✓", "success") } catch { toast("Failed to send", "error") } } const collectionRate = stats.totalPledged > 0 ? Math.round((stats.totalCollected / stats.totalPledged) * 100) : 0 const totalPages = Math.ceil(total / pageSize) return (
{/* Header */}

Pledges

{stats.total} total · {formatPence(stats.totalPledged)} pledged · {collectionRate}% collected

{ setSearch(e.target.value); setPage(0) }} className="pl-9 w-64" />
{/* Stats row */}
setTab("all")}>
All

{stats.total}

setTab("new")}>
Pending

{stats.pending}

setTab("due-soon")}>
Due Soon

{stats.dueSoon || "—"}

setTab("overdue")}>
Overdue

{stats.overdue}

setTab("paid")}>
Paid

{stats.paid}

{/* Collection progress */}
{formatPence(stats.totalCollected)} / {formatPence(stats.totalPledged)}
{/* Tabs + Table */} { setTab(v); setPage(0) }}> All Pending Due Soon Overdue Initiated Paid Cancelled {loading ? (
) : pledges.length === 0 ? (

No pledges found

{search ? `No results for "${search}"` : "Create an event and share QR codes to start collecting pledges"}

) : ( Donor Amount Event Status Due / Created Method {pledges.map((p) => { const sc = statusConfig[p.status] || statusConfig.new const due = p.dueDate ? dueLabel(p.dueDate) : null const isInstallment = p.installmentTotal && p.installmentTotal > 1 return (

{p.donorName || "Anonymous"}

{p.reference}

{p.donorPhone && (

WhatsApp

)}

{formatPence(p.amountPence)}

{p.giftAid && 🎁 +Gift Aid} {isInstallment && (

{p.installmentNumber}/{p.installmentTotal}

)}

{p.eventName}

{p.qrSourceLabel && (

{p.qrSourceLabel}

)}
{sc.label} {due ? ( {due.urgent && "⚠ "}{due.text} ) : ( {timeAgo(p.createdAt)} )} {p.rail === "gocardless" ? "Direct Debit" : p.rail} {p.status !== "paid" && ( updateStatus(p.id, "paid")}> Mark Paid )} {p.status !== "initiated" && p.status !== "paid" && ( updateStatus(p.id, "initiated")}> Mark Initiated )} {p.donorPhone && p.status !== "paid" && ( <> sendReminder(p)}> Send WhatsApp Reminder )} {p.status !== "cancelled" && p.status !== "paid" && ( <> updateStatus(p.id, "cancelled")}> Cancel Pledge )}
) })}
)}
{/* Pagination */} {totalPages > 1 && (

Showing {page * pageSize + 1}–{Math.min((page + 1) * pageSize, total)} of {total}

)}
) }