"use client" import { useState, useEffect, useCallback } from "react" import { useRouter } from "next/navigation" import { useSession } from "next-auth/react" import Image from "next/image" import { formatPence } from "@/lib/utils" import { ArrowRight, Loader2, MessageCircle, Copy, Check, Share2, Upload } from "lucide-react" import Link from "next/link" /** * Context-aware dashboard — feels like a landing page for YOUR data. * * Every state has: * 1. A hero section (brand photography + dark panel with the key metric) * 2. Content sections that use landing page patterns (gap-px, border-l-2) * 3. Human language throughout */ const STATUS_LABELS: Record = { new: { label: "Waiting", color: "text-gray-600", bg: "bg-gray-100" }, initiated: { label: "Said they paid", color: "text-[#F59E0B]", bg: "bg-[#F59E0B]/10" }, paid: { label: "Received ✓", color: "text-[#16A34A]", bg: "bg-[#16A34A]/10" }, overdue: { label: "Needs a nudge", color: "text-[#DC2626]", bg: "bg-[#DC2626]/10" }, cancelled: { label: "Cancelled", color: "text-gray-400", bg: "bg-gray-50" }, } export default function DashboardPage() { const router = useRouter() const { data: session } = useSession() // eslint-disable-next-line @typescript-eslint/no-explicit-any const userRole = (session?.user as any)?.role // eslint-disable-next-line @typescript-eslint/no-explicit-any const [data, setData] = useState(null) // eslint-disable-next-line @typescript-eslint/no-explicit-any const [ob, setOb] = useState(null) // eslint-disable-next-line @typescript-eslint/no-explicit-any const [events, setEvents] = useState([]) const [loading, setLoading] = useState(true) const [waConnected, setWaConnected] = useState(null) const [pledgeLink, setPledgeLink] = useState("") const [copied, setCopied] = useState(false) const fetchAll = useCallback(async () => { try { const [dashRes, obRes, evRes, waRes] = await Promise.all([ fetch("/api/dashboard").then(r => r.json()), fetch("/api/onboarding").then(r => r.json()), fetch("/api/events").then(r => r.json()), fetch("/api/whatsapp/send").then(r => r.json()).catch(() => ({ connected: false })), ]) if (dashRes.summary) setData(dashRes) if (obRes.steps) setOb(obRes) if (Array.isArray(evRes)) setEvents(evRes) setWaConnected(waRes.connected) } catch { /* */ } setLoading(false) }, []) useEffect(() => { if (userRole === "community_leader" || userRole === "volunteer") { router.replace("/dashboard/community") } }, [userRole, router]) useEffect(() => { fetchAll() const i = setInterval(fetchAll, 15000) return () => clearInterval(i) }, [fetchAll]) useEffect(() => { if (events.length > 0) { fetch(`/api/events/${events[0].id}/qr`) .then(r => r.json()) .then(qrs => { if (Array.isArray(qrs) && qrs.length > 0) { const base = typeof window !== "undefined" ? window.location.origin : "" setPledgeLink(`${base}/p/${qrs[0].code}`) } }) .catch(() => {}) } }, [events]) if (loading) { return
} const hasEvents = events.length > 0 if (!hasEvents && ob) { const eventDone = ob.steps?.find((s: { id: string; done: boolean }) => s.id === "event" || s.id === "share")?.done if (!eventDone) { router.replace("/dashboard/welcome") return
} } const s = data?.summary || { totalPledges: 0, totalPledgedPence: 0, totalCollectedPence: 0, collectionRate: 0 } const byStatus = data?.byStatus || {} const pledges = data?.pledges || [] const topSources = data?.topSources || [] const isEmpty = s.totalPledges === 0 const hasSaidPaid = (byStatus.initiated || 0) > 0 const allCollected = !isEmpty && s.collectionRate === 100 const outstanding = s.totalPledgedPence - s.totalCollectedPence const recentPledges = pledges.filter((p: { status: string }) => p.status !== "cancelled").slice(0, 8) const needsAttention = [ ...pledges.filter((p: { status: string }) => p.status === "overdue"), ...pledges.filter((p: { status: string }) => p.status === "initiated"), ].slice(0, 5) const activeEvent = events[0] const copyLink = async () => { if (!pledgeLink) return await navigator.clipboard.writeText(pledgeLink) setCopied(true) setTimeout(() => setCopied(false), 2000) } // Pick contextual hero photo const heroPhoto = allCollected ? "/images/brand/impact-02-thank-you-letter.jpg" : isEmpty ? "/images/brand/product-setup-05-briefing-volunteers.jpg" : "/images/brand/event-03-table-conversation.jpg" const heroAlt = allCollected ? "Woman reading a thank-you card — the quiet moment when every pledge gets through" : isEmpty ? "Event organiser briefing volunteers before a fundraising dinner" : "Friends at a charity dinner — where pledges begin" return (
{/* ━━ HERO — Brand photography + the one thing that matters ━━━ */}
{heroAlt}
{activeEvent && (

{activeEvent.name}

)} {allCollected ? ( <>

Every pledge collected

{formatPence(s.totalCollectedPence)} received from {s.totalPledges} donors

) : isEmpty ? ( <>

Ready to collect pledges

Share your link — pledges will appear here as they come in.

{waConnected !== null && (

{waConnected ? "WhatsApp connected — reminders active" : "WhatsApp not connected"}

)} ) : ( <>

{formatPence(outstanding)}

still to come · {s.collectionRate}% collected

{/* Contextual CTA — what should they do RIGHT NOW? */} {hasSaidPaid ? ( Upload bank statement ) : (byStatus.overdue || 0) > 0 ? ( {byStatus.overdue} {byStatus.overdue === 1 ? "pledge needs" : "pledges need"} a nudge ) : null} )}
{/* ── Content with padding ── */}
{/* ── Empty state: Share your link ── */} {isEmpty && pledgeLink && (

Your pledge link

{pledgeLink}

Create more pledge links →

Give each volunteer or table their own link to see who brings in the most pledges

)} {/* ── Empty state: educational guidance ── */} {isEmpty && !pledgeLink && (

Share your pledge link to get started

Go to Collect to create an appeal and get your pledge link.

How it works

{[ { n: "01", title: "Create a pledge link", desc: "Give it a name like 'Table 5' or 'Ramadan 2026'. Print the QR or share the link." }, { n: "02", title: "Donors pledge in 60 seconds", desc: "Name, phone, amount, Gift Aid — done. No account, no app download." }, { n: "03", title: "They get your bank details", desc: "Instantly via WhatsApp. With a unique reference so you can match their payment." }, { n: "04", title: "Reminders go out automatically", desc: "Day 2, day 7, day 14. Warm and never pushy. They stop when the donor pays." }, { n: "05", title: "Upload your bank statement", desc: "We match payments to pledges automatically. No spreadsheet cross-referencing." }, ].map(s => (
{s.n}

{s.title}

{s.desc}

))}
)} {/* ── Has pledges: Stats + Feed + Education ── */} {!isEmpty && ( <> {/* Stats — gap-px grid */}
{[ { value: String(s.totalPledges), label: "Pledges" }, { value: formatPence(s.totalPledgedPence), label: "Promised" }, { value: formatPence(s.totalCollectedPence), label: "Received", accent: "text-[#16A34A]" }, { value: `${s.collectionRate}%`, label: "Collected" }, ].map(stat => (

{stat.value}

{stat.label}

))}
{/* Progress bar */}
Promised → Received {s.collectionRate}%
{formatPence(s.totalCollectedPence)} received {formatPence(outstanding)} still to come
{/* ── "Said they paid" prompt ── */} {hasSaidPaid && (

{byStatus.initiated} {byStatus.initiated === 1 ? "person says" : "people say"} they've paid

Upload your bank statement to confirm their payments automatically

)}
{/* LEFT column: Data */}
{/* Needs attention */} {needsAttention.length > 0 && (

Needs attention

{needsAttention.length}
{needsAttention.map((p: { id: string; donorName: string | null; amountPence: number; eventName: string; status: string }) => { const sl = STATUS_LABELS[p.status] || STATUS_LABELS.new return (

{p.donorName || "Anonymous"}

{formatPence(p.amountPence)}

{sl.label}
) })}
View all →
)} {/* Recent pledges */}

Recent pledges

View all
{recentPledges.map((p: { id: string; donorName: string | null; amountPence: number; status: string; eventName: string; createdAt: string; donorPhone: string | null; installmentNumber: number | null; installmentTotal: number | null; }) => { const sl = STATUS_LABELS[p.status] || STATUS_LABELS.new const initial = (p.donorName || "A")[0].toUpperCase() const days = Math.floor((Date.now() - new Date(p.createdAt).getTime()) / 86400000) const when = days === 0 ? "Today" : days === 1 ? "Yesterday" : days < 7 ? `${days}d ago` : new Date(p.createdAt).toLocaleDateString("en-GB", { day: "numeric", month: "short" }) return (
{initial}

{p.donorName || "Anonymous"}

{p.donorPhone && }

{when}

{formatPence(p.amountPence)}

{sl.label}
) })} {recentPledges.length === 0 && (
Pledges will appear here as they come in
)}
{/* RIGHT column: Status + Sources + Guidance */}
{/* How pledges are doing */}

How pledges are doing

{Object.entries(byStatus).map(([status, count]) => { const sl = STATUS_LABELS[status] || STATUS_LABELS.new return (
{sl.label} {count as number}
) })}
{/* Where pledges come from */} {topSources.length > 0 && (

Where pledges come from

{topSources.slice(0, 5).map((src: { label: string; count: number; amount: number }, i: number) => (
{i + 1} {src.label}
{formatPence(src.amount)}
))}
)} {/* What to do next — contextual guidance */}

What to do next

{s.collectionRate < 100 && (byStatus.initiated || 0) > 0 && (

Upload your bank statement to confirm {byStatus.initiated} {byStatus.initiated === 1 ? "payment" : "payments"} automatically

)} {s.collectionRate < 50 && (

Share your link more widely — WhatsApp groups, social media, or print the QR

)}

Check your messages — see what donors receive and improve wording

Download for your treasurer — Gift Aid report, full pledge data, HMRC-ready CSV

{/* Understanding statuses */}

What the statuses mean

{[ { label: "Waiting", desc: "Pledged but hasn't paid yet — reminders are being sent" }, { label: "Said they paid", desc: "Donor replied PAID — upload bank statement to confirm" }, { label: "Received ✓", desc: "Payment confirmed in your bank account" }, { label: "Needs a nudge", desc: "It's been a while — you can send a manual reminder" }, ].map(s => (

{s.label} — {s.desc}

))}
)}
) }