"use client" import { useState, useEffect, useCallback } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Progress } from "@/components/ui/progress" import { formatPence } from "@/lib/utils" import { TrendingUp, Users, Banknote, AlertTriangle, Calendar, Clock, CheckCircle2, ArrowRight, Loader2, MessageCircle, ExternalLink } from "lucide-react" import Link from "next/link" interface DashboardData { summary: { totalPledges: number; totalPledgedPence: number; totalCollectedPence: number; collectionRate: number; overdueRate: number } byStatus: Record byRail: Record topSources: Array<{ label: string; count: number; amount: number }> pledges: Array<{ id: string; reference: string; amountPence: number; status: string; rail: string; donorName: string | null; donorEmail: string | null; donorPhone: string | null; eventName: string; source: string | null; giftAid: boolean; dueDate: string | null; isDeferred: boolean; planId: string | null; installmentNumber: number | null; installmentTotal: number | null; createdAt: string; paidAt: string | null; nextReminder: string | null; }> } const statusIcons: Record = { new: Clock, initiated: TrendingUp, paid: CheckCircle2, overdue: AlertTriangle } const statusColors: Record = { new: "secondary", initiated: "warning", paid: "success", overdue: "destructive" } export default function DashboardPage() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [whatsappStatus, setWhatsappStatus] = useState(null) const fetchData = useCallback(() => { fetch("/api/dashboard", { headers: { "x-org-id": "demo" } }) .then(r => r.json()) .then(d => { if (d.summary) setData(d) }) .catch(() => {}) .finally(() => setLoading(false)) }, []) useEffect(() => { fetchData() fetch("/api/whatsapp/send").then(r => r.json()).then(d => setWhatsappStatus(d.connected)).catch(() => {}) const interval = setInterval(fetchData, 15000) return () => clearInterval(interval) }, [fetchData]) if (loading) { return (
) } if (!data) { return (

Welcome to Pledge Now, Pay Later

Start by configuring your organisation's bank details, then create your first event.

) } const s = data.summary const upcomingPledges = data.pledges.filter(p => p.isDeferred && p.dueDate && p.status !== "paid" && p.status !== "cancelled" ).sort((a, b) => new Date(a.dueDate!).getTime() - new Date(b.dueDate!).getTime()) const recentPledges = data.pledges.filter(p => p.status !== "cancelled").slice(0, 8) const overduePledges = data.pledges.filter(p => p.status === "overdue") const needsAction = [...overduePledges, ...upcomingPledges.filter(p => { const due = new Date(p.dueDate!) return due.getTime() - Date.now() < 2 * 86400000 // due in 2 days })].slice(0, 5) return (

Dashboard

{whatsappStatus !== null && ( {whatsappStatus ? "WhatsApp connected" : "WhatsApp offline"} )} Auto-refreshes every 15s

{/* Stats */}

{s.totalPledges}

Total Pledges

{formatPence(s.totalPledgedPence)}

Total Pledged

{formatPence(s.totalCollectedPence)}

Collected ({s.collectionRate}%)

10 ? "border-danger-red/30" : ""}>

{data.byStatus.overdue || 0}

Overdue

{/* Collection progress */}
Pledged → Collected {s.collectionRate}%
{formatPence(s.totalCollectedPence)} collected {formatPence(s.totalPledgedPence - s.totalCollectedPence)} outstanding
{/* Needs attention */} 0 ? "border-warm-amber/30" : ""}> Needs Attention {needsAction.length > 0 && {needsAction.length}} {needsAction.length === 0 ? (

All clear! No urgent items.

) : ( needsAction.map(p => (

{p.donorName || "Anonymous"}

{formatPence(p.amountPence)} · {p.eventName} {p.dueDate && ` · Due ${new Date(p.dueDate).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}`}

{p.status === "overdue" ? "Overdue" : "Due soon"}
)) )} {needsAction.length > 0 && ( View all )}
{/* Upcoming payments */} Upcoming Payments {upcomingPledges.length === 0 ? (

No scheduled payments

) : ( upcomingPledges.slice(0, 5).map(p => (
{new Date(p.dueDate!).getDate()}
{new Date(p.dueDate!).toLocaleDateString("en-GB", { month: "short" })}

{p.donorName || "Anonymous"}

{p.eventName} {p.installmentTotal && p.installmentTotal > 1 && ` · ${p.installmentNumber}/${p.installmentTotal}`}

{formatPence(p.amountPence)}
)) )}
{/* Pipeline + Sources */}
Pipeline by Status {Object.entries(data.byStatus).map(([status, count]) => { const Icon = statusIcons[status] || Clock return (
{status}
{count}
) })}
Top Sources {data.topSources.length === 0 ? (

Create QR codes to track sources

) : ( data.topSources.slice(0, 6).map((src, i) => (
{i + 1} {src.label} {src.count} pledges
{formatPence(src.amount)}
)) )}
{/* Recent pledges */} Recent Pledges
{recentPledges.map(p => { const sc = statusColors[p.status] || "secondary" return (
{(p.donorName || "A")[0].toUpperCase()}

{p.donorName || "Anonymous"}

{p.eventName} · {new Date(p.createdAt).toLocaleDateString("en-GB", { day: "numeric", month: "short" })} {p.dueDate && !p.paidAt && ` · Due ${new Date(p.dueDate).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}`} {p.installmentTotal && p.installmentTotal > 1 && ` · ${p.installmentNumber}/${p.installmentTotal}`}

{formatPence(p.amountPence)} {p.status}
) })}
) }