Settings page: full telepathic redesign matching brand system

Before: Mediocre — shadcn <Input>, no visual hierarchy, no readiness
indicator, no donor preview, inconsistent headers, flat team list.

After: Every element matches the brand system used in Collect/Money/Reports.

Changes:
1. READINESS BAR (dark hero section)
   - 4-cell gap-px grid on #111827 background
   - Green/gray dots: WhatsApp ✓, Bank ✗, Charity ✓, Team: 2 members
   - Aaisha sees instantly what's configured and what's missing

2. SECTION HEADERS (consistent pattern)
   - All sections: colored icon box + title + description
   - border-b separator matching Reports/Money pattern
   - WhatsApp: green icon box. Bank: green when configured.

3. FIELD COMPONENT (no more shadcn Input)
   - Reusable <Field> with uppercase tracking-wide label
   - border-2 focus:border-[#1E40AF] (sharp, no rounded)
   - Consistent height (h-10) and padding across all inputs

4. BANK ACCOUNT — DONOR PREVIEW
   - New: shows exactly what donors see after pledging
   - Grid layout with bank name, sort code, account, reference
   - 'What donors see after pledging' preview card
   - Context tip: 'Changes apply to new pledges immediately'

5. CHARITY — BRAND PREVIEW
   - Shows logo mark (first letter in brand color square) + name
   - Color picker is now a swatch + hex input
   - 'Preview — pledge page header' section

6. TEAM MANAGEMENT
   - Role cards with icon boxes and colored badges
   - Gap-px grid for WhatsApp features (connected state)
   - Credentials grid layout (not prose)
   - Empty state with icon + helpful text
   - Role icons: Crown (admin), Users (leader), Eye (staff/volunteer)
   - Color-coded: blue admin, amber leader, gray staff

7. WHATSAPP PANEL
   - Connected: gap-px 3-column grid (Receipts/Reminders/Chatbot)
   - Not connected: border-l-2 accent list, PAID/HELP/CANCEL in mono
   - QR scanning: border-l-2 instructions
   - onStatusChange callback feeds the readiness bar

8. DIRECT DEBIT
   - Custom <details> with ChevronRight rotation
   - border-l-2 contextual tip ('most charities don't need this')

9. SAVE BUTTONS
   - Extracted <SaveBtn> component
   - Green flash on save (bg-[#16A34A])
   - 'Save changes' / 'Saving…' / 'Saved' states
This commit is contained in:
2026-03-04 22:19:31 +08:00
parent b477dc30d1
commit f75cc29980

View File

@@ -2,25 +2,27 @@
import { useState, useEffect, useCallback } from "react"
import { useSession } from "next-auth/react"
import { Input } from "@/components/ui/input"
import {
Check, Loader2, AlertCircle, MessageCircle, Radio, RefreshCw,
Smartphone, Wifi, WifiOff, QrCode, UserPlus, Trash2, Copy,
Users, Crown, Eye
Users, Crown, Eye, Building2, CreditCard, Palette, ChevronRight
} from "lucide-react"
/**
* /dashboard/settings — Aaisha's control panel
*
* Organised by what she's thinking, not by system concept:
* 1. WhatsApp — "I need to connect" (or see it's connected)
* 2. Team — "Who has access? I need to invite Imam Yusuf"
* 3. Bank — "Where donors send money"
* 4. Your charity — name, brand colour
* 5. Direct Debit — advanced, for later
* Telepathic approach: What is she thinking each time she visits?
*
* Team management is NEW — the missing feature.
* This is how community leaders get invited.
* First visit: "They told me to connect WhatsApp. Where?"
* Second visit: "Is everything working? What else do I need to set up?"
* Monthly: "I need to invite Imam Yusuf" / "Let me check bank details"
* Rarely: "What name shows to donors?" / "Direct Debit?"
*
* The page opens with a READINESS BAR — dark section showing
* what's configured vs what's missing. Aaisha sees instantly:
* "WhatsApp ✓ · Bank details ✗ · Team: 1 member"
*
* Then sections in order of how often she needs them.
*/
interface OrgSettings {
@@ -33,11 +35,11 @@ interface TeamMember {
id: string; email: string; name: string | null; role: string; createdAt: string
}
const ROLE_LABELS: Record<string, { label: string; desc: string; icon: typeof Crown }> = {
org_admin: { label: "Admin", desc: "Full access to everything", icon: Crown },
community_leader: { label: "Community Leader", desc: "Can see their links, pledges, and share. Can't change settings.", icon: Users },
staff: { label: "Staff", desc: "Can view pledges and reports", icon: Eye },
volunteer: { label: "Volunteer", desc: "Read-only access", icon: Eye },
const ROLE_META: Record<string, { label: string; desc: string; icon: typeof Crown; color: string; bg: string }> = {
org_admin: { label: "Admin", desc: "Full access — settings, money, everything", icon: Crown, color: "text-[#1E40AF]", bg: "bg-[#1E40AF]/10" },
community_leader: { label: "Community Leader", desc: "Their own links and pledges. Can't change settings.", icon: Users, color: "text-[#F59E0B]", bg: "bg-[#F59E0B]/10" },
staff: { label: "Staff", desc: "Can view pledges and reports, read-only", icon: Eye, color: "text-gray-600", bg: "bg-gray-100" },
volunteer: { label: "Volunteer", desc: "Minimal access — they mostly use the live feed link", icon: Eye, color: "text-gray-400", bg: "bg-gray-50" },
}
export default function SettingsPage() {
@@ -61,6 +63,9 @@ export default function SettingsPage() {
const [inviteResult, setInviteResult] = useState<{ email: string; tempPassword: string } | null>(null)
const [copiedCred, setCopiedCred] = useState(false)
// WhatsApp status (pulled from child, exposed for readiness bar)
const [waStatus, setWaStatus] = useState<string>("loading")
useEffect(() => {
Promise.all([
fetch("/api/settings").then(r => r.json()),
@@ -139,34 +144,59 @@ export default function SettingsPage() {
const update = (key: keyof OrgSettings, value: string) => setSettings(s => s ? { ...s, [key]: value } : s)
const isAdmin = currentUser?.role === "org_admin" || currentUser?.role === "super_admin"
const SaveButton = ({ section, data }: { section: string; data: Record<string, string> }) => (
<button
onClick={() => save(section, data)}
disabled={saving === section}
className="bg-[#111827] px-4 py-2 text-xs font-bold text-white hover:bg-gray-800 disabled:opacity-50 transition-colors"
>
{saving === section ? <><Loader2 className="h-3 w-3 mr-1.5 animate-spin inline" /> Saving</> : saved === section ? <><Check className="h-3 w-3 mr-1.5 inline" /> Saved!</> : "Save"}
</button>
)
// Readiness checks
const bankReady = !!(settings.bankSortCode && settings.bankAccountNo && settings.bankAccountName)
const whatsappReady = waStatus === "CONNECTED"
const charityReady = !!settings.name
return (
<div className="space-y-8 max-w-2xl">
<div className="space-y-8">
{/* ── Header ── */}
<div>
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">{settings.name}</p>
<h1 className="text-3xl font-black text-[#111827] tracking-tight">Settings</h1>
<p className="text-sm text-gray-500 mt-0.5">WhatsApp, team, bank account, and charity details</p>
</div>
{/* ── Readiness bar — "Am I set up?" ── */}
<div className="bg-[#111827] p-5">
<p className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-3">Setup progress</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-px bg-gray-700">
{[
{ label: "WhatsApp", ready: whatsappReady, detail: whatsappReady ? "Connected" : "Not connected" },
{ label: "Bank details", ready: bankReady, detail: bankReady ? `${settings.bankSortCode}` : "Not set" },
{ label: "Charity name", ready: charityReady, detail: charityReady ? settings.name : "Not set" },
{ label: "Team", ready: team.length > 0, detail: `${team.length} member${team.length !== 1 ? "s" : ""}` },
].map(item => (
<div key={item.label} className="bg-[#111827] p-3">
<div className="flex items-center gap-1.5 mb-1">
<div className={`w-2 h-2 ${item.ready ? "bg-[#4ADE80]" : "bg-gray-600"}`} />
<p className="text-[10px] font-bold text-gray-400">{item.label}</p>
</div>
<p className={`text-sm font-bold ${item.ready ? "text-white" : "text-gray-600"} truncate`}>{item.detail}</p>
</div>
))}
</div>
</div>
{error && <div className="border-l-2 border-[#DC2626] bg-[#DC2626]/5 p-3 text-sm text-[#DC2626]">{error}</div>}
{/* ── 1. WhatsApp ── */}
<WhatsAppPanel />
<WhatsAppPanel onStatusChange={setWaStatus} />
{/* ── 2. Team management ── */}
{isAdmin && (
<div className="bg-white border border-gray-200">
<div className="p-6 pb-4">
<div className="flex items-center justify-between mb-1">
<h3 className="text-base font-bold text-[#111827]">Team</h3>
<section className="bg-white border border-gray-200">
<div className="border-b border-gray-100 px-5 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-[#1E40AF]/10 flex items-center justify-center shrink-0">
<Users className="h-4 w-4 text-[#1E40AF]" />
</div>
<div>
<h2 className="text-sm font-bold text-[#111827]">Team</h2>
<p className="text-[10px] text-gray-500">People who can access your dashboard</p>
</div>
</div>
<button
onClick={() => { setShowInvite(!showInvite); setInviteResult(null) }}
className="bg-[#111827] px-3 py-1.5 text-xs font-bold text-white hover:bg-gray-800 transition-colors flex items-center gap-1.5"
@@ -174,115 +204,143 @@ export default function SettingsPage() {
<UserPlus className="h-3.5 w-3.5" /> Invite
</button>
</div>
<p className="text-xs text-gray-500">People who can access your dashboard. Invite community leaders to track their pledges.</p>
</div>
{/* Invite form */}
{showInvite && !inviteResult && (
<div className="mx-6 mb-4 border-2 border-[#1E40AF] p-4 space-y-3">
<p className="text-sm font-bold text-[#111827]">Invite a team member</p>
<div className="border-b border-gray-100 p-5 bg-[#F9FAFB] space-y-4">
<p className="text-xs font-bold text-[#111827]">Invite a team member</p>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[10px] font-bold text-gray-500 block mb-1">Email</label>
<input value={inviteEmail} onChange={e => setInviteEmail(e.target.value)} placeholder="imam@mosque.org" className="w-full h-9 px-3 border-2 border-gray-200 text-sm focus:border-[#1E40AF] outline-none" />
<label className="text-[10px] font-bold text-gray-500 uppercase tracking-wide block mb-1.5">Email</label>
<input
value={inviteEmail}
onChange={e => setInviteEmail(e.target.value)}
placeholder="imam@mosque.org"
className="w-full h-10 px-3 border-2 border-gray-200 bg-white text-sm placeholder:text-gray-300 focus:border-[#1E40AF] outline-none"
/>
</div>
<div>
<label className="text-[10px] font-bold text-gray-500 block mb-1">Name <span className="font-normal text-gray-400">(optional)</span></label>
<input value={inviteName} onChange={e => setInviteName(e.target.value)} placeholder="Imam Yusuf" className="w-full h-9 px-3 border-2 border-gray-200 text-sm focus:border-[#1E40AF] outline-none" />
<label className="text-[10px] font-bold text-gray-500 uppercase tracking-wide block mb-1.5">
Name <span className="font-normal text-gray-400 normal-case">(optional)</span>
</label>
<input
value={inviteName}
onChange={e => setInviteName(e.target.value)}
placeholder="Imam Yusuf"
className="w-full h-10 px-3 border-2 border-gray-200 bg-white text-sm placeholder:text-gray-300 focus:border-[#1E40AF] outline-none"
/>
</div>
</div>
<div>
<label className="text-[10px] font-bold text-gray-500 block mb-2">Role</label>
<label className="text-[10px] font-bold text-gray-500 uppercase tracking-wide block mb-2">What can they do?</label>
<div className="grid grid-cols-2 gap-2">
{Object.entries(ROLE_LABELS).filter(([k]) => k !== "org_admin" || currentUser?.role === "super_admin").map(([key, r]) => (
{Object.entries(ROLE_META).filter(([k]) => k !== "org_admin" || currentUser?.role === "super_admin").map(([key, r]) => {
const active = inviteRole === key
return (
<button
key={key}
onClick={() => setInviteRole(key)}
className={`border-2 p-3 text-left transition-all ${inviteRole === key ? "border-[#1E40AF] bg-[#1E40AF]/5" : "border-gray-200"}`}
className={`border-2 p-3 text-left transition-all ${active ? "border-[#1E40AF] bg-[#1E40AF]/5" : "border-gray-200 bg-white hover:border-gray-300"}`}
>
<div className="flex items-center gap-2 mb-1">
<div className={`w-5 h-5 flex items-center justify-center ${r.bg}`}>
<r.icon className={`h-3 w-3 ${r.color}`} />
</div>
<p className="text-xs font-bold text-[#111827]">{r.label}</p>
<p className="text-[10px] text-gray-500 mt-0.5">{r.desc}</p>
</div>
<p className="text-[10px] text-gray-500 leading-snug">{r.desc}</p>
</button>
))}
)
})}
</div>
</div>
<div className="flex gap-2">
<button onClick={() => setShowInvite(false)} className="flex-1 border border-gray-200 py-2 text-xs font-bold text-[#111827] hover:bg-gray-50">Cancel</button>
<button onClick={inviteMember} disabled={!inviteEmail.trim() || inviting} className="flex-1 bg-[#111827] py-2 text-xs font-bold text-white hover:bg-gray-800 disabled:opacity-40 flex items-center justify-center gap-1.5">
{inviting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Send invite"}
<div className="flex gap-2 pt-1">
<button onClick={() => setShowInvite(false)} className="flex-1 border-2 border-gray-200 py-2.5 text-xs font-bold text-[#111827] hover:bg-gray-50 transition-colors">Cancel</button>
<button onClick={inviteMember} disabled={!inviteEmail.trim() || inviting} className="flex-1 bg-[#111827] py-2.5 text-xs font-bold text-white hover:bg-gray-800 disabled:opacity-40 transition-colors flex items-center justify-center gap-1.5">
{inviting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Create account"}
</button>
</div>
</div>
)}
{/* Invite result — show credentials once */}
{/* Invite result — credentials shown once */}
{inviteResult && (
<div className="mx-6 mb-4 bg-[#16A34A]/5 border border-[#16A34A]/20 p-4 space-y-3">
<div className="border-b border-gray-100 p-5 bg-[#16A34A]/5 space-y-3">
<div className="flex items-center gap-2">
<Check className="h-4 w-4 text-[#16A34A]" />
<p className="text-sm font-bold text-[#111827]">Invited!</p>
<div className="w-5 h-5 bg-[#16A34A] flex items-center justify-center">
<Check className="h-3 w-3 text-white" />
</div>
<p className="text-xs text-gray-600">Share these login details with them. The password is shown only once.</p>
<div className="bg-white border border-gray-200 p-3 font-mono text-xs space-y-1">
<p>Email: <strong>{inviteResult.email}</strong></p>
<p>Password: <strong>{inviteResult.tempPassword}</strong></p>
<p>Login: <strong>{typeof window !== "undefined" ? window.location.origin : ""}/login</strong></p>
<p className="text-sm font-bold text-[#111827]">Account created</p>
</div>
<div className="flex gap-2">
<div className="border-l-2 border-[#16A34A] pl-3">
<p className="text-xs text-gray-600">Share these login details. The password is shown <strong>only once</strong>.</p>
</div>
<div className="bg-white border border-gray-200 p-4 space-y-2">
<div className="grid grid-cols-[80px_1fr] gap-1 text-xs">
<span className="text-gray-500">Email</span>
<span className="font-bold text-[#111827] font-mono">{inviteResult.email}</span>
<span className="text-gray-500">Password</span>
<span className="font-bold text-[#111827] font-mono">{inviteResult.tempPassword}</span>
<span className="text-gray-500">Login URL</span>
<span className="font-bold text-[#111827] font-mono text-[11px]">{typeof window !== "undefined" ? window.location.origin : ""}/login</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => copyCredentials(inviteResult.email, inviteResult.tempPassword)}
className="flex-1 bg-[#111827] py-2 text-xs font-bold text-white hover:bg-gray-800 flex items-center justify-center gap-1.5"
className={`py-2.5 text-xs font-bold transition-colors flex items-center justify-center gap-1.5 ${copiedCred ? "bg-[#16A34A] text-white" : "bg-[#111827] text-white hover:bg-gray-800"}`}
>
{copiedCred ? <><Check className="h-3 w-3" /> Copied</> : <><Copy className="h-3 w-3" /> Copy credentials</>}
{copiedCred ? <><Check className="h-3 w-3" /> Copied</> : <><Copy className="h-3 w-3" /> Copy all</>}
</button>
<button
onClick={() => {
const text = `Your login for ${settings?.name || "Pledge Now Pay Later"}:\n\nEmail: ${inviteResult.email}\nPassword: ${inviteResult.tempPassword}\nLogin: ${window.location.origin}/login`
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, "_blank")
}}
className="bg-[#25D366] py-2 px-4 text-xs font-bold text-white hover:bg-[#25D366]/90 flex items-center gap-1.5"
className="bg-[#25D366] py-2.5 text-xs font-bold text-white hover:bg-[#25D366]/90 transition-colors flex items-center justify-center gap-1.5"
>
<MessageCircle className="h-3 w-3" /> WhatsApp
<MessageCircle className="h-3 w-3" /> Send via WhatsApp
</button>
</div>
<button onClick={() => { setInviteResult(null); setShowInvite(false) }} className="text-xs text-gray-400 hover:text-gray-600">Done</button>
</div>
)}
{/* Team list */}
<div className="divide-y divide-gray-50">
{/* Team list — gap-px grid */}
{team.length > 0 ? (
<div className="divide-y divide-gray-100">
{team.map(m => {
const r = ROLE_LABELS[m.role] || ROLE_LABELS.staff
const isCurrentUser = m.id === currentUser?.id
const r = ROLE_META[m.role] || ROLE_META.staff
const isMe = m.id === currentUser?.id
const RoleIcon = r.icon
return (
<div key={m.id} className="px-6 py-3 flex items-center gap-3">
<div className="w-8 h-8 bg-[#1E40AF]/10 flex items-center justify-center shrink-0">
<RoleIcon className="h-4 w-4 text-[#1E40AF]" />
<div key={m.id} className="px-5 py-3 flex items-center gap-3 hover:bg-[#F9FAFB] transition-colors">
<div className={`w-8 h-8 flex items-center justify-center shrink-0 ${r.bg}`}>
<RoleIcon className={`h-3.5 w-3.5 ${r.color}`} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-medium text-[#111827] truncate">{m.name || m.email}</p>
{isCurrentUser && <span className="text-[9px] font-bold text-gray-400">You</span>}
<p className="text-sm font-medium text-[#111827] truncate">{m.name || m.email.split("@")[0]}</p>
{isMe && <span className="text-[8px] font-bold text-gray-400 uppercase tracking-wider">You</span>}
</div>
<p className="text-[10px] text-gray-500">{m.email}</p>
<p className="text-[10px] text-gray-400 truncate">{m.email}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
{isAdmin && !isCurrentUser ? (
{isAdmin && !isMe ? (
<select
value={m.role}
onChange={e => changeRole(m.id, e.target.value)}
className="text-[10px] font-bold border border-gray-200 px-2 py-1 bg-white"
className="text-[10px] font-bold border-2 border-gray-200 px-2 py-1.5 bg-white focus:border-[#1E40AF] outline-none"
>
{Object.entries(ROLE_LABELS).map(([key, v]) => (
{Object.entries(ROLE_META).map(([key, v]) => (
<option key={key} value={key}>{v.label}</option>
))}
</select>
) : (
<span className="text-[10px] font-bold px-2 py-0.5 bg-gray-100 text-gray-600">{r.label}</span>
<span className={`text-[10px] font-bold px-2 py-1 ${r.bg} ${r.color}`}>{r.label}</span>
)}
{isAdmin && !isCurrentUser && (
<button onClick={() => removeMember(m.id)} className="text-gray-300 hover:text-[#DC2626] p-1 transition-colors">
{isAdmin && !isMe && (
<button onClick={() => removeMember(m.id)} className="text-gray-300 hover:text-[#DC2626] p-1.5 transition-colors" title="Remove">
<Trash2 className="h-3.5 w-3.5" />
</button>
)}
@@ -291,171 +349,399 @@ export default function SettingsPage() {
)
})}
</div>
) : (
<div className="p-8 text-center">
<Users className="h-6 w-6 text-gray-200 mx-auto mb-2" />
<p className="text-xs text-gray-400">Just you for now. Invite community leaders to track their pledges.</p>
</div>
)}
</section>
)}
{/* ── 3. Bank account ── */}
<div className="bg-white border border-gray-200 p-6 space-y-4">
<div>
<h3 className="text-base font-bold text-[#111827]">Bank account</h3>
<p className="text-xs text-gray-500 mt-0.5">Shown to donors so they know where to transfer. Each pledge gets a unique reference.</p>
</div>
<div className="grid grid-cols-2 gap-3">
<div><label className="text-[10px] font-bold text-gray-500 block mb-1">Bank name</label><Input value={settings.bankName} onChange={e => update("bankName", e.target.value)} placeholder="e.g. Barclays" /></div>
<div><label className="text-[10px] font-bold text-gray-500 block mb-1">Account name</label><Input value={settings.bankAccountName} onChange={e => update("bankAccountName", e.target.value)} /></div>
</div>
<div className="grid grid-cols-2 gap-3">
<div><label className="text-[10px] font-bold text-gray-500 block mb-1">Sort code</label><Input value={settings.bankSortCode} onChange={e => update("bankSortCode", e.target.value)} placeholder="20-30-80" /></div>
<div><label className="text-[10px] font-bold text-gray-500 block mb-1">Account number</label><Input value={settings.bankAccountNo} onChange={e => update("bankAccountNo", e.target.value)} placeholder="12345678" /></div>
<section className="bg-white border border-gray-200">
<div className="border-b border-gray-100 px-5 py-4 flex items-center gap-3">
<div className={`w-8 h-8 flex items-center justify-center shrink-0 ${bankReady ? "bg-[#16A34A]/10" : "bg-gray-100"}`}>
<Building2 className={`h-4 w-4 ${bankReady ? "text-[#16A34A]" : "text-gray-400"}`} />
</div>
<div>
<label className="text-[10px] font-bold text-gray-500 block mb-1">Reference prefix</label>
<Input value={settings.refPrefix} onChange={e => update("refPrefix", e.target.value)} maxLength={4} className="w-24" />
<p className="text-[10px] text-gray-400 mt-1">Donors see references like <strong>{settings.refPrefix}-XXXX-50</strong></p>
<div className="flex items-center gap-2">
<h2 className="text-sm font-bold text-[#111827]">Bank account</h2>
{bankReady && <div className="w-2 h-2 bg-[#16A34A]" />}
</div>
<SaveButton section="bank" data={{ bankName: settings.bankName, bankSortCode: settings.bankSortCode, bankAccountNo: settings.bankAccountNo, bankAccountName: settings.bankAccountName, refPrefix: settings.refPrefix }} />
<p className="text-[10px] text-gray-500">Where donors send their payment</p>
</div>
</div>
<div className="p-5 space-y-4">
<div className="border-l-2 border-[#1E40AF] pl-3 text-xs text-gray-500">
<p>When someone pledges, they see these details with instructions to transfer. Each pledge gets a unique reference so you can match payments.</p>
</div>
<div className="grid grid-cols-2 gap-3">
<Field label="Bank name" value={settings.bankName} onChange={v => update("bankName", v)} placeholder="e.g. Barclays" />
<Field label="Account name" value={settings.bankAccountName} onChange={v => update("bankAccountName", v)} placeholder="e.g. Al Furqan Mosque" />
</div>
<div className="grid grid-cols-2 gap-3">
<Field label="Sort code" value={settings.bankSortCode} onChange={v => update("bankSortCode", v)} placeholder="20-30-80" />
<Field label="Account number" value={settings.bankAccountNo} onChange={v => update("bankAccountNo", v)} placeholder="12345678" />
</div>
<div className="grid grid-cols-[96px_1fr] gap-3 items-end">
<Field label="Reference prefix" value={settings.refPrefix} onChange={v => update("refPrefix", v)} maxLength={4} />
<div className="pb-[2px]">
<p className="text-[10px] text-gray-400">
Donors see references like <span className="font-bold text-[#111827]">{settings.refPrefix || "PNPL"}-A2F4-50</span>
</p>
</div>
</div>
{/* Donor preview — what they actually see */}
{bankReady && (
<div className="bg-[#F9FAFB] border border-gray-100 p-4 space-y-2">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-wide">What donors see after pledging</p>
<div className="bg-white border border-gray-200 p-4 space-y-3">
<p className="text-xs font-bold text-[#111827]">Transfer to:</p>
<div className="grid grid-cols-[80px_1fr] gap-y-1.5 text-xs">
<span className="text-gray-500">Bank</span>
<span className="font-bold text-[#111827]">{settings.bankName}</span>
<span className="text-gray-500">Name</span>
<span className="font-bold text-[#111827]">{settings.bankAccountName}</span>
<span className="text-gray-500">Sort code</span>
<span className="font-mono font-bold text-[#111827]">{settings.bankSortCode}</span>
<span className="text-gray-500">Account</span>
<span className="font-mono font-bold text-[#111827]">{settings.bankAccountNo}</span>
<span className="text-gray-500">Reference</span>
<span className="font-mono font-bold text-[#1E40AF]">{settings.refPrefix || "PNPL"}-A2F4-50</span>
</div>
</div>
</div>
)}
<div className="flex items-center justify-between pt-1">
<p className="text-[10px] text-gray-400">Changes apply to new pledges immediately</p>
<SaveBtn
section="bank"
saving={saving}
saved={saved}
onSave={() => save("bank", { bankName: settings.bankName, bankSortCode: settings.bankSortCode, bankAccountNo: settings.bankAccountNo, bankAccountName: settings.bankAccountName, refPrefix: settings.refPrefix })}
/>
</div>
</div>
</section>
{/* ── 4. Charity details ── */}
<div className="bg-white border border-gray-200 p-6 space-y-4">
<section className="bg-white border border-gray-200">
<div className="border-b border-gray-100 px-5 py-4 flex items-center gap-3">
<div className="w-8 h-8 bg-gray-100 flex items-center justify-center shrink-0">
<Palette className="h-4 w-4 text-gray-600" />
</div>
<div>
<h3 className="text-base font-bold text-[#111827]">Your charity</h3>
<p className="text-xs text-gray-500 mt-0.5">Name and colour shown on pledge pages and WhatsApp messages.</p>
<h2 className="text-sm font-bold text-[#111827]">Your charity</h2>
<p className="text-[10px] text-gray-500">Name and brand shown on pledge pages and WhatsApp messages</p>
</div>
<div><label className="text-[10px] font-bold text-gray-500 block mb-1">Charity name</label><Input value={settings.name} onChange={e => update("name", e.target.value)} /></div>
<div>
<label className="text-[10px] font-bold text-gray-500 block mb-1">Brand colour</label>
<div className="flex gap-2 mt-1">
<Input type="color" value={settings.primaryColor} onChange={e => update("primaryColor", e.target.value)} className="w-12 h-9 p-0.5" />
<Input value={settings.primaryColor} onChange={e => update("primaryColor", e.target.value)} className="flex-1" />
</div>
</div>
<SaveButton section="brand" data={{ name: settings.name, primaryColor: settings.primaryColor }} />
</div>
{/* ── 5. Direct Debit (collapsed) ── */}
<details className="bg-white border border-gray-200">
<summary className="p-6 text-base font-bold text-[#111827] cursor-pointer hover:bg-gray-50 transition-colors">
Direct Debit <span className="text-xs font-normal text-gray-400 ml-2">GoCardless integration</span>
</summary>
<div className="px-6 pb-6 space-y-4 border-t border-gray-100 pt-4">
<p className="text-xs text-gray-500">Accept Direct Debit payments via GoCardless. Donors set up a mandate and payments are collected automatically.</p>
<div className="p-5 space-y-4">
<Field label="Charity name" value={settings.name} onChange={v => update("name", v)} placeholder="e.g. Al Furqan Mosque" />
<div>
<label className="text-[10px] font-bold text-gray-500 block mb-1">GoCardless access token</label>
<Input type="password" value={settings.gcAccessToken} onChange={e => update("gcAccessToken", e.target.value)} placeholder="sandbox_xxxxx or live_xxxxx" />
<label className="text-[10px] font-bold text-gray-500 uppercase tracking-wide block mb-1.5">Brand colour</label>
<div className="flex gap-2">
<div
className="w-10 h-10 border-2 border-gray-200 shrink-0 cursor-pointer relative overflow-hidden"
style={{ backgroundColor: settings.primaryColor || "#1E40AF" }}
>
<input
type="color"
value={settings.primaryColor || "#1E40AF"}
onChange={e => update("primaryColor", e.target.value)}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<input
value={settings.primaryColor || "#1E40AF"}
onChange={e => update("primaryColor", e.target.value)}
className="flex-1 h-10 px-3 border-2 border-gray-200 text-sm font-mono placeholder:text-gray-300 focus:border-[#1E40AF] outline-none"
/>
</div>
</div>
{/* Brand preview */}
<div className="bg-[#F9FAFB] border border-gray-100 p-4 space-y-2">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-wide">Preview pledge page header</p>
<div className="bg-white border border-gray-200 p-4">
<div className="flex items-center gap-2.5">
<div className="h-8 w-8 flex items-center justify-center" style={{ backgroundColor: settings.primaryColor || "#1E40AF" }}>
<span className="text-white text-xs font-black">{(settings.name || "P")[0].toUpperCase()}</span>
</div>
<span className="font-black text-sm tracking-tight text-[#111827]">{settings.name || "Your Charity"}</span>
</div>
</div>
</div>
<div className="flex justify-end pt-1">
<SaveBtn
section="brand"
saving={saving}
saved={saved}
onSave={() => save("brand", { name: settings.name, primaryColor: settings.primaryColor })}
/>
</div>
</div>
</section>
{/* ── 5. Direct Debit (collapsed) ── */}
<details className="bg-white border border-gray-200 group">
<summary className="px-5 py-4 flex items-center gap-3 cursor-pointer hover:bg-[#F9FAFB] transition-colors list-none [&::-webkit-details-marker]:hidden">
<div className="w-8 h-8 bg-gray-100 flex items-center justify-center shrink-0">
<CreditCard className="h-4 w-4 text-gray-400" />
</div>
<div className="flex-1">
<h2 className="text-sm font-bold text-[#111827]">Direct Debit</h2>
<p className="text-[10px] text-gray-500">GoCardless integration advanced</p>
</div>
<ChevronRight className="h-4 w-4 text-gray-300 transition-transform group-open:rotate-90" />
</summary>
<div className="border-t border-gray-100 p-5 space-y-4">
<div className="border-l-2 border-gray-300 pl-3 text-xs text-gray-500">
<p>Accept Direct Debit via GoCardless. Donors set up a mandate and payments are collected automatically. Most charities don&apos;t need this bank transfer works fine.</p>
</div>
<Field
label="GoCardless access token"
value={settings.gcAccessToken}
onChange={v => update("gcAccessToken", v)}
placeholder="sandbox_xxxxx or live_xxxxx"
type="password"
/>
<div>
<label className="text-[10px] font-bold text-gray-500 block mb-1">Mode</label>
<div className="flex gap-2 mt-1">
{["sandbox", "live"].map(env => (
<button key={env} onClick={() => update("gcEnvironment", env)} className={`px-3 py-1.5 text-xs font-bold border-2 transition-colors ${settings.gcEnvironment === env ? env === "live" ? "border-[#DC2626] bg-[#DC2626]/5 text-[#DC2626]" : "border-[#1E40AF] bg-[#1E40AF]/5 text-[#1E40AF]" : "border-gray-200 text-gray-400"}`}>
<label className="text-[10px] font-bold text-gray-500 uppercase tracking-wide block mb-2">Mode</label>
<div className="flex gap-2">
{(["sandbox", "live"] as const).map(env => (
<button
key={env}
onClick={() => update("gcEnvironment", env)}
className={`px-4 py-2 text-xs font-bold border-2 transition-colors ${settings.gcEnvironment === env
? env === "live"
? "border-[#DC2626] bg-[#DC2626]/5 text-[#DC2626]"
: "border-[#1E40AF] bg-[#1E40AF]/5 text-[#1E40AF]"
: "border-gray-200 text-gray-400 hover:border-gray-300"}`}
>
{env === "sandbox" ? "Test mode" : "Live mode"}
</button>
))}
</div>
</div>
<SaveButton section="gc" data={{ gcAccessToken: settings.gcAccessToken, gcEnvironment: settings.gcEnvironment }} />
<div className="flex justify-end pt-1">
<SaveBtn
section="gc"
saving={saving}
saved={saved}
onSave={() => save("gc", { gcAccessToken: settings.gcAccessToken, gcEnvironment: settings.gcEnvironment })}
/>
</div>
</div>
</details>
</div>
)
}
// ─── WhatsApp Connection Panel (unchanged) ───────────────────
function WhatsAppPanel() {
// ─── Reusable Field component ────────────────────────────────
function Field({ label, value, onChange, placeholder, type = "text", maxLength }: {
label: string; value: string; onChange: (v: string) => void
placeholder?: string; type?: string; maxLength?: number
}) {
return (
<div>
<label className="text-[10px] font-bold text-gray-500 uppercase tracking-wide block mb-1.5">{label}</label>
<input
type={type}
value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder}
maxLength={maxLength}
className="w-full h-10 px-3 border-2 border-gray-200 text-sm placeholder:text-gray-300 focus:border-[#1E40AF] outline-none transition-colors"
/>
</div>
)
}
// ─── Save button component ───────────────────────────────────
function SaveBtn({ section, saving, saved, onSave }: {
section: string; saving: string | null; saved: string | null; onSave: () => void
}) {
const isSaving = saving === section
const isSaved = saved === section
return (
<button
onClick={onSave}
disabled={isSaving}
className={`px-5 py-2.5 text-xs font-bold transition-all flex items-center gap-1.5 ${
isSaved
? "bg-[#16A34A] text-white"
: "bg-[#111827] text-white hover:bg-gray-800 disabled:opacity-50"
}`}
>
{isSaving ? <><Loader2 className="h-3 w-3 animate-spin" /> Saving</>
: isSaved ? <><Check className="h-3 w-3" /> Saved</>
: "Save changes"}
</button>
)
}
// ─── WhatsApp Connection Panel ───────────────────────────────
function WhatsAppPanel({ onStatusChange }: { onStatusChange?: (status: string) => void }) {
const [status, setStatus] = useState<string>("loading")
const [qrImage, setQrImage] = useState<string | null>(null)
const [phone, setPhone] = useState(""); const [pushName, setPushName] = useState("")
const [starting, setStarting] = useState(false); const [showQr, setShowQr] = useState(false)
const [phone, setPhone] = useState("")
const [pushName, setPushName] = useState("")
const [starting, setStarting] = useState(false)
const [showQr, setShowQr] = useState(false)
const checkStatus = useCallback(async () => {
try {
const res = await fetch("/api/whatsapp/qr"); const data = await res.json()
const res = await fetch("/api/whatsapp/qr")
const data = await res.json()
setStatus(data.status)
onStatusChange?.(data.status)
if (data.screenshot) setQrImage(data.screenshot)
if (data.phone) setPhone(data.phone)
if (data.pushName) setPushName(data.pushName)
if (data.status === "CONNECTED") setShowQr(false)
} catch { setStatus("ERROR") }
}, [])
} catch {
setStatus("ERROR")
onStatusChange?.("ERROR")
}
}, [onStatusChange])
useEffect(() => { checkStatus() }, [checkStatus])
useEffect(() => { if (!showQr) return; const i = setInterval(checkStatus, 5000); return () => clearInterval(i) }, [showQr, checkStatus])
const startSession = async () => {
setStarting(true); setShowQr(true)
try { await fetch("/api/whatsapp/qr", { method: "POST" }); await new Promise(r => setTimeout(r, 3000)); await checkStatus() } catch { /* */ }
try {
await fetch("/api/whatsapp/qr", { method: "POST" })
await new Promise(r => setTimeout(r, 3000))
await checkStatus()
} catch { /* */ }
setStarting(false)
}
// ── Connected state ──
if (status === "CONNECTED") {
return (
<div className="bg-white border border-[#25D366]/30 p-6">
<div className="flex items-center gap-2 mb-4">
<h3 className="text-base font-bold text-[#111827]">WhatsApp</h3>
<span className="text-[10px] font-bold px-1.5 py-0.5 bg-[#25D366]/10 text-[#25D366] flex items-center gap-1"><Radio className="h-2.5 w-2.5" /> Connected</span>
<section className="bg-white border border-[#25D366]/30">
<div className="border-b border-[#25D366]/10 px-5 py-4 flex items-center gap-3">
<div className="w-8 h-8 bg-[#25D366]/10 flex items-center justify-center shrink-0">
<Smartphone className="h-4 w-4 text-[#25D366]" />
</div>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-[#25D366]/10 flex items-center justify-center"><Smartphone className="h-5 w-5 text-[#25D366]" /></div>
<div><p className="text-sm font-medium text-[#111827]">{pushName || "WhatsApp"}</p><p className="text-xs text-gray-500">+{phone}</p></div>
<Wifi className="h-5 w-5 text-[#25D366] ml-auto" />
<div className="flex-1">
<div className="flex items-center gap-2">
<h2 className="text-sm font-bold text-[#111827]">WhatsApp</h2>
<span className="text-[9px] font-bold px-1.5 py-0.5 bg-[#25D366]/10 text-[#25D366] flex items-center gap-1">
<Radio className="h-2 w-2" /> Connected
</span>
</div>
<div className="mt-4 pt-3 border-t border-[#25D366]/10 grid grid-cols-3 gap-3">
<p className="text-[10px] text-gray-500">{pushName || "WhatsApp"} · +{phone}</p>
</div>
<Wifi className="h-5 w-5 text-[#25D366]" />
</div>
{/* What's active */}
<div className="grid grid-cols-3 gap-px bg-[#25D366]/10">
{[
{ label: "Receipts", desc: "Auto-sends when someone pledges" },
{ label: "Reminders", desc: "4-step reminder sequence" },
{ label: "Chatbot", desc: "Donors reply PAID, HELP, etc." },
].map(f => (<div key={f.label} className="text-center"><p className="text-xs font-bold text-[#111827]">{f.label}</p><p className="text-[9px] text-gray-500 mt-0.5">{f.desc}</p></div>))}
{ label: "Receipts", desc: "Auto-sent when someone pledges" },
{ label: "Reminders", desc: "4-step nudge sequence" },
{ label: "Chatbot", desc: "PAID, HELP, CANCEL replies" },
].map(f => (
<div key={f.label} className="bg-white p-4 text-center">
<p className="text-xs font-bold text-[#111827]">{f.label}</p>
<p className="text-[9px] text-gray-500 mt-0.5">{f.desc}</p>
</div>
))}
</div>
</section>
)
}
// ── QR scanning state ──
if (status === "SCAN_QR_CODE" && showQr) {
return (
<div className="bg-white border border-[#F59E0B]/30 p-6">
<div className="flex items-center gap-2 mb-4">
<h3 className="text-base font-bold text-[#111827]">WhatsApp</h3>
<span className="text-[10px] font-bold px-1.5 py-0.5 bg-[#F59E0B]/10 text-[#F59E0B] flex items-center gap-1"><QrCode className="h-2.5 w-2.5" /> Scan QR</span>
<section className="bg-white border border-[#F59E0B]/30">
<div className="border-b border-[#F59E0B]/10 px-5 py-4 flex items-center gap-3">
<div className="w-8 h-8 bg-[#F59E0B]/10 flex items-center justify-center shrink-0">
<QrCode className="h-4 w-4 text-[#F59E0B]" />
</div>
<div className="flex flex-col items-center gap-4">
<div>
<h2 className="text-sm font-bold text-[#111827]">WhatsApp</h2>
<p className="text-[10px] text-[#F59E0B] font-bold">Waiting for QR scan</p>
</div>
</div>
<div className="p-8 flex flex-col items-center gap-4">
{qrImage ? (
<div className="w-64 h-64 border-2 border-[#25D366]/20 overflow-hidden bg-white">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={qrImage} alt="WhatsApp QR Code" className="w-[200%] h-auto max-w-none" style={{ marginLeft: "-30%", marginTop: "-35%" }} />
</div>
) : (
<div className="w-64 h-64 border-2 border-dashed border-gray-200 flex items-center justify-center"><Loader2 className="h-6 w-6 text-gray-400 animate-spin" /></div>
<div className="w-64 h-64 border-2 border-dashed border-gray-200 flex items-center justify-center">
<Loader2 className="h-6 w-6 text-gray-300 animate-spin" />
</div>
)}
<div className="text-center space-y-1">
<p className="text-sm font-bold text-[#111827]">Scan with your phone</p>
<div className="border-l-2 border-[#25D366] pl-3 text-left inline-block">
<p className="text-xs text-gray-500">WhatsApp Settings Linked Devices Link a Device</p>
</div>
<button onClick={checkStatus} className="border border-gray-200 px-3 py-1.5 text-xs font-semibold text-gray-600 hover:bg-gray-50 flex items-center gap-1.5"><RefreshCw className="h-3 w-3" /> Refresh</button>
</div>
<button onClick={checkStatus} className="border-2 border-gray-200 px-4 py-2 text-xs font-bold text-gray-600 hover:bg-gray-50 flex items-center gap-1.5 transition-colors">
<RefreshCw className="h-3 w-3" /> Refresh
</button>
</div>
</section>
)
}
// ── Not connected state ──
return (
<div className="bg-white border border-gray-200 p-6">
<div className="flex items-center gap-2 mb-4">
<h3 className="text-base font-bold text-[#111827]">WhatsApp</h3>
<span className="text-[10px] font-bold px-1.5 py-0.5 bg-gray-100 text-gray-500 flex items-center gap-1"><WifiOff className="h-2.5 w-2.5" /> Not connected</span>
<section className="bg-white border border-gray-200">
<div className="border-b border-gray-100 px-5 py-4 flex items-center gap-3">
<div className="w-8 h-8 bg-gray-100 flex items-center justify-center shrink-0">
<WifiOff className="h-4 w-4 text-gray-400" />
</div>
<div className="border-l-2 border-[#25D366] pl-4 space-y-2">
<p className="text-sm font-medium text-[#111827]">Connect your WhatsApp number</p>
<div className="text-xs text-gray-500 space-y-0.5">
<p>When you connect, donors automatically receive:</p>
<p className="font-medium text-gray-600"> Pledge receipts with bank details</p>
<p className="font-medium text-gray-600"> Payment reminders on a 4-step schedule</p>
<p className="font-medium text-gray-600"> A chatbot (they reply PAID, HELP, or CANCEL)</p>
<div>
<div className="flex items-center gap-2">
<h2 className="text-sm font-bold text-[#111827]">WhatsApp</h2>
<span className="text-[9px] font-bold px-1.5 py-0.5 bg-gray-100 text-gray-500">Not connected</span>
</div>
<p className="text-[10px] text-gray-500">Connect to send automatic receipts and reminders</p>
</div>
</div>
<button onClick={startSession} disabled={starting} className="mt-4 w-full bg-[#25D366] px-4 py-2.5 text-sm font-bold text-white hover:bg-[#25D366]/90 disabled:opacity-50 transition-colors flex items-center justify-center gap-2">
{starting ? <><Loader2 className="h-4 w-4 animate-spin" /> Starting...</> : <><MessageCircle className="h-4 w-4" /> Connect WhatsApp</>}
<div className="p-5 space-y-4">
<div className="border-l-2 border-[#25D366] pl-3 space-y-1.5 text-xs text-gray-600">
<p className="font-bold text-[#111827]">When you connect, donors automatically receive:</p>
<div className="grid grid-cols-1 gap-1">
<p>Pledge receipt with your bank details within seconds</p>
<p>Payment reminders 4-step sequence over 14 days</p>
<p>Chatbot replies they text <span className="font-mono font-bold text-[#111827]">PAID</span>, <span className="font-mono font-bold text-[#111827]">HELP</span>, or <span className="font-mono font-bold text-[#111827]">CANCEL</span></p>
</div>
</div>
<button
onClick={startSession}
disabled={starting}
className="w-full bg-[#25D366] px-4 py-3 text-sm font-bold text-white hover:bg-[#25D366]/90 disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
>
{starting ? <><Loader2 className="h-4 w-4 animate-spin" /> Starting</> : <><MessageCircle className="h-4 w-4" /> Connect WhatsApp</>}
</button>
</div>
</section>
)
}