Ship all P0/P1/P2 gaps + 11 AI features

P0 Critical (7):
- STOP/UNSUBSCRIBE keyword → CANCEL (PECR compliance)
- Rate limiting on pledge creation (10/IP/5min)
- Terms of Service + Privacy Policy pages
- WhatsApp onboarding gate (persistent dashboard banner)
- Demo account seeding (demo@pnpl.app)
- Footer legal links
- Basic accessibility (aria labels on donor flow)

P1 Within 2 Weeks (8):
- Pledge editing by staff (PATCH amount, name, email, phone, rail)
- Donor self-cancel page (/p/cancel) + API
- Donor 'My Pledges' lookup page (/p/my-pledges)
- Bulk QR code download (print-ready HTML)
- Public event progress bar (/e/[slug]/progress)
- Email-only donor handling (honest status + WhatsApp fallback)
- Email verification (format + disposable domain blocking)
- Organisations page rewrite (multi-campaign, not multi-org)

P2 Within First Month (10):
- Event cloning with QR sources
- Account deletion (GDPR Article 17)
- Daily digest cron via WhatsApp
- AI-6 Smart reminder timing (due date anchoring, cultural sensitivity)
- H1 Duplicate donor detection (email, phone, Jaro-Winkler name)
- H5 Bank CSV format presets (10 UK banks)
- H16 Partial payment matching (underpay, overpay, instalment)
- H10 Activity logging (audit trail for staff actions)
- AI nudge endpoint + AI column mapping + AI event setup wizard
- AI anomaly detection wired into daily digest

AI Features (11): smart reconciliation, social proof, auto column mapper,
daily digest, impact storyteller, smart timing, nudge composer, event wizard,
NLU concierge, anomaly detection, bank presets

22 new files, 15 modified files, 0 TypeScript errors, clean build.
This commit is contained in:
2026-03-04 20:10:34 +08:00
parent 59485579ec
commit fcfae1c1a4
36 changed files with 3405 additions and 46 deletions

View File

@@ -3,7 +3,8 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useSession, signOut } from "next-auth/react"
import { LayoutDashboard, Megaphone, FileBarChart, Upload, Download, Settings, Plus, ExternalLink, LogOut, Shield } from "lucide-react"
import { useState, useEffect } from "react"
import { LayoutDashboard, Megaphone, FileBarChart, Upload, Download, Settings, Plus, ExternalLink, LogOut, Shield, MessageCircle, AlertTriangle } from "lucide-react"
import { cn } from "@/lib/utils"
const navItems = [
@@ -132,9 +133,53 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
{/* Main content */}
<main className="flex-1 p-4 md:p-6 pb-20 md:pb-6 max-w-6xl">
<WhatsAppBanner />
{children}
</main>
</div>
</div>
)
}
/** Persistent WhatsApp connection banner — shows until connected */
function WhatsAppBanner() {
const [status, setStatus] = useState<string | null>(null)
const [dismissed, setDismissed] = useState(false)
const pathname = usePathname()
useEffect(() => {
// Don't show on settings page (they're already there)
if (pathname === "/dashboard/settings") { setStatus("skip"); return }
fetch("/api/whatsapp/send")
.then(r => r.json())
.then(data => setStatus(data.connected ? "CONNECTED" : "OFFLINE"))
.catch(() => setStatus("OFFLINE"))
}, [pathname])
if (status === "CONNECTED" || status === "skip" || status === null || dismissed) return null
return (
<div className="mb-4 rounded-lg border-2 border-amber-200 bg-amber-50 p-4 flex items-start gap-3">
<div className="w-9 h-9 rounded-lg bg-amber-100 flex items-center justify-center shrink-0 mt-0.5">
<AlertTriangle className="h-5 w-5 text-amber-600" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-gray-900">WhatsApp not connected reminders won&apos;t send</p>
<p className="text-xs text-gray-600 mt-0.5">
Connect your WhatsApp to auto-send pledge receipts and payment reminders to donors. Takes 60 seconds.
</p>
<div className="flex items-center gap-3 mt-2">
<Link
href="/dashboard/settings"
className="inline-flex items-center gap-1.5 bg-[#25D366] px-3 py-1.5 text-xs font-bold text-white hover:bg-[#25D366]/90 transition-colors rounded"
>
<MessageCircle className="h-3.5 w-3.5" /> Connect WhatsApp
</Link>
<button onClick={() => setDismissed(true)} className="text-xs text-gray-400 hover:text-gray-600">
Dismiss
</button>
</div>
</div>
</div>
)
}