"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, Circle, X, Building2, Heart } 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; }> } interface OnboardingData { steps: Array<{ id: string; label: string; desc: string; done: boolean; href: string; action?: string }> completed: number total: number allDone: boolean orgType: string | null needsRole: boolean orgName: string } const statusIcons: Record = { new: Clock, initiated: TrendingUp, paid: CheckCircle2, overdue: AlertTriangle } const statusColors: Record = { new: "secondary", initiated: "warning", paid: "success", overdue: "destructive" } // ─── Role Picker ──────────────────────────────────────────── function RolePicker({ onSelect }: { onSelect: (role: string) => void }) { return (
) } // ─── Getting Started Banner ───────────────────────────────── function GettingStartedBanner({ ob, onSetRole, dismissed, onDismiss, }: { ob: OnboardingData onSetRole: (role: string) => void dismissed: boolean onDismiss: () => void }) { const [showRolePicker, setShowRolePicker] = useState(!ob.orgType || ob.orgType === "charity") if (ob.allDone || dismissed) return null // First-time: show role picker const isFirstTime = ob.completed === 0 && (!ob.orgType || ob.orgType === "charity") return (
{/* Dismiss X */}
🤲

{isFirstTime ? "Welcome! What best describes you?" : `Getting started · ${ob.completed}/${ob.total}`}

{!isFirstTime && ( )}
{isFirstTime && showRolePicker ? ( { onSetRole(role); setShowRolePicker(false) }} /> ) : (
{ob.steps.map((step, i) => { const isNext = !step.done && ob.steps.slice(0, i).every(s => s.done) return (
{step.done ? ( ) : isNext ? (
{i + 1}
) : ( )}

{step.label}

{isNext && }
) })}
)}
) } // ─── Main Dashboard ───────────────────────────────────────── export default function DashboardPage() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [whatsappStatus, setWhatsappStatus] = useState(null) const [ob, setOb] = useState(null) const [bannerDismissed, setBannerDismissed] = useState(false) const fetchData = useCallback(() => { fetch("/api/dashboard") .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(() => {}) fetch("/api/onboarding").then(r => r.json()).then(d => { if (d.steps) setOb(d) }).catch(() => {}) const interval = setInterval(fetchData, 15000) return () => clearInterval(interval) }, [fetchData]) const handleSetRole = async (role: string) => { await fetch("/api/onboarding", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ orgType: role }), }) // Refresh onboarding state const res = await fetch("/api/onboarding") const d = await res.json() if (d.steps) setOb(d) } if (loading) { return (
) } const s = data?.summary || { totalPledges: 0, totalPledgedPence: 0, totalCollectedPence: 0, collectionRate: 0, overdueRate: 0 } const byStatus = data?.byStatus || {} const topSources = data?.topSources || [] const pledges = data?.pledges || [] const upcomingPledges = 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 = pledges.filter(p => p.status !== "cancelled").slice(0, 8) const needsAction = [ ...pledges.filter(p => p.status === "overdue"), ...upcomingPledges.filter(p => { const due = new Date(p.dueDate!) return due.getTime() - Date.now() < 2 * 86400000 }) ].slice(0, 5) const isEmpty = s.totalPledges === 0 return (
{/* Getting-started banner — always at top, not a blocker */} {ob && !ob.allDone && ( setBannerDismissed(true)} /> )}

Dashboard

{whatsappStatus !== null && ( {whatsappStatus ? "WhatsApp connected" : "WhatsApp offline"} )} {isEmpty ? "Your data will appear here" : "Auto-refreshes every 15s"}

{!isEmpty && ( )}
{/* Stats — always show, even with zeros */}

{s.totalPledges}

Total Pledges

{formatPence(s.totalPledgedPence)}

Total Pledged

{formatPence(s.totalCollectedPence)}

Collected ({s.collectionRate}%)

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

{byStatus.overdue || 0}

Overdue

{/* Collection progress — always visible */}
Pledged → Collected {s.collectionRate}%
{formatPence(s.totalCollectedPence)} collected {formatPence(s.totalPledgedPence - s.totalCollectedPence)} outstanding
{isEmpty ? ( /* Empty state — gentle nudge, not a blocker */

Your pledge data will appear here

Once you share your first link and donors start pledging, you'll see live stats, payment tracking, and reminders.

) : ( <>
{/* 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(byStatus).map(([status, count]) => { const Icon = statusIcons[status] || Clock return (
{status}
{count}
) })}
Top Sources {topSources.length === 0 ? (

Create QR codes to track sources

) : ( 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}
) })}
)}
) }