diff --git a/pledge-now-pay-later/src/app/dashboard/settings/page.tsx b/pledge-now-pay-later/src/app/dashboard/settings/page.tsx index 865312d..ffde8d2 100644 --- a/pledge-now-pay-later/src/app/dashboard/settings/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/settings/page.tsx @@ -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 = { - 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 = { + 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("loading") + useEffect(() => { Promise.all([ fetch("/api/settings").then(r => r.json()), @@ -139,323 +144,604 @@ 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 }) => ( - - ) + // Readiness checks + const bankReady = !!(settings.bankSortCode && settings.bankAccountNo && settings.bankAccountName) + const whatsappReady = waStatus === "CONNECTED" + const charityReady = !!settings.name return ( -
+
+ + {/* ── Header ── */}
+

{settings.name}

Settings

-

WhatsApp, team, bank account, and charity details

+
+ + {/* ── Readiness bar — "Am I set up?" ── */} +
+

Setup progress

+
+ {[ + { 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 => ( +
+
+
+

{item.label}

+
+

{item.detail}

+
+ ))} +
{error &&
{error}
} {/* ── 1. WhatsApp ── */} - + {/* ── 2. Team management ── */} {isAdmin && ( -
-
-
-

Team

- +
+
+
+
+ +
+
+

Team

+

People who can access your dashboard

+
-

People who can access your dashboard. Invite community leaders to track their pledges.

+
{/* Invite form */} {showInvite && !inviteResult && ( -
-

Invite a team member

+
+

Invite a team member

- - 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" /> + + 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" + />
- - 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" /> + + 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" + />
- +
- {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 ( + + ) + })}
-
- - +
)} - {/* Invite result — show credentials once */} + {/* Invite result — credentials shown once */} {inviteResult && ( -
+
- -

Invited!

+
+ +
+

Account created

-

Share these login details with them. The password is shown only once.

-
-

Email: {inviteResult.email}

-

Password: {inviteResult.tempPassword}

-

Login: {typeof window !== "undefined" ? window.location.origin : ""}/login

+
+

Share these login details. The password is shown only once.

-
+
+
+ Email + {inviteResult.email} + Password + {inviteResult.tempPassword} + Login URL + {typeof window !== "undefined" ? window.location.origin : ""}/login +
+
+
)} - {/* Team list */} -
- {team.map(m => { - const r = ROLE_LABELS[m.role] || ROLE_LABELS.staff - const isCurrentUser = m.id === currentUser?.id - const RoleIcon = r.icon - return ( -
-
- -
-
-
-

{m.name || m.email}

- {isCurrentUser && You} + {/* Team list — gap-px grid */} + {team.length > 0 ? ( +
+ {team.map(m => { + const r = ROLE_META[m.role] || ROLE_META.staff + const isMe = m.id === currentUser?.id + const RoleIcon = r.icon + return ( +
+
+ +
+
+
+

{m.name || m.email.split("@")[0]}

+ {isMe && You} +
+

{m.email}

+
+
+ {isAdmin && !isMe ? ( + + ) : ( + {r.label} + )} + {isAdmin && !isMe && ( + + )}
-

{m.email}

-
- {isAdmin && !isCurrentUser ? ( - - ) : ( - {r.label} - )} - {isAdmin && !isCurrentUser && ( - - )} -
-
- ) - })} -
-
+ ) + })} +
+ ) : ( +
+ +

Just you for now. Invite community leaders to track their pledges.

+
+ )} +
)} {/* ── 3. Bank account ── */} -
-
-

Bank account

-

Shown to donors so they know where to transfer. Each pledge gets a unique reference.

+
+
+
+ +
+
+
+

Bank account

+ {bankReady &&
} +
+

Where donors send their payment

+
-
-
update("bankName", e.target.value)} placeholder="e.g. Barclays" />
-
update("bankAccountName", e.target.value)} />
+ +
+
+

When someone pledges, they see these details with instructions to transfer. Each pledge gets a unique reference so you can match payments.

+
+ +
+ update("bankName", v)} placeholder="e.g. Barclays" /> + update("bankAccountName", v)} placeholder="e.g. Al Furqan Mosque" /> +
+
+ update("bankSortCode", v)} placeholder="20-30-80" /> + update("bankAccountNo", v)} placeholder="12345678" /> +
+
+ update("refPrefix", v)} maxLength={4} /> +
+

+ Donors see references like {settings.refPrefix || "PNPL"}-A2F4-50 +

+
+
+ + {/* Donor preview — what they actually see */} + {bankReady && ( +
+

What donors see after pledging

+
+

Transfer to:

+
+ Bank + {settings.bankName} + Name + {settings.bankAccountName} + Sort code + {settings.bankSortCode} + Account + {settings.bankAccountNo} + Reference + {settings.refPrefix || "PNPL"}-A2F4-50 +
+
+
+ )} + +
+

Changes apply to new pledges immediately

+ save("bank", { bankName: settings.bankName, bankSortCode: settings.bankSortCode, bankAccountNo: settings.bankAccountNo, bankAccountName: settings.bankAccountName, refPrefix: settings.refPrefix })} + /> +
-
-
update("bankSortCode", e.target.value)} placeholder="20-30-80" />
-
update("bankAccountNo", e.target.value)} placeholder="12345678" />
-
-
- - update("refPrefix", e.target.value)} maxLength={4} className="w-24" /> -

Donors see references like {settings.refPrefix}-XXXX-50

-
- -
+
{/* ── 4. Charity details ── */} -
-
-

Your charity

-

Name and colour shown on pledge pages and WhatsApp messages.

-
-
update("name", e.target.value)} />
-
- -
- update("primaryColor", e.target.value)} className="w-12 h-9 p-0.5" /> - update("primaryColor", e.target.value)} className="flex-1" /> +
+
+
+ +
+
+

Your charity

+

Name and brand shown on pledge pages and WhatsApp messages

- -
+ +
+ update("name", v)} placeholder="e.g. Al Furqan Mosque" /> + +
+ +
+
+ update("primaryColor", e.target.value)} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> +
+ 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" + /> +
+
+ + {/* Brand preview */} +
+

Preview — pledge page header

+
+
+
+ {(settings.name || "P")[0].toUpperCase()} +
+ {settings.name || "Your Charity"} +
+
+
+ +
+ save("brand", { name: settings.name, primaryColor: settings.primaryColor })} + /> +
+
+ {/* ── 5. Direct Debit (collapsed) ── */} -
- - Direct Debit GoCardless integration - -
-

Accept Direct Debit payments via GoCardless. Donors set up a mandate and payments are collected automatically.

-
- - update("gcAccessToken", e.target.value)} placeholder="sandbox_xxxxx or live_xxxxx" /> +
+ +
+
+
+

Direct Debit

+

GoCardless integration — advanced

+
+ +
+
+
+

Accept Direct Debit via GoCardless. Donors set up a mandate and payments are collected automatically. Most charities don't need this — bank transfer works fine.

+
+ update("gcAccessToken", v)} + placeholder="sandbox_xxxxx or live_xxxxx" + type="password" + />
- -
- {["sandbox", "live"].map(env => ( - ))}
- +
+ save("gc", { gcAccessToken: settings.gcAccessToken, gcEnvironment: settings.gcEnvironment })} + /> +
) } -// ─── 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 ( +
+ + 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" + /> +
+ ) +} + + +// ─── 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 ( + + ) +} + + +// ─── WhatsApp Connection Panel ─────────────────────────────── + +function WhatsAppPanel({ onStatusChange }: { onStatusChange?: (status: string) => void }) { const [status, setStatus] = useState("loading") const [qrImage, setQrImage] = useState(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 ( -
-
-

WhatsApp

- Connected +
+
+
+ +
+
+
+

WhatsApp

+ + Connected + +
+

{pushName || "WhatsApp"} · +{phone}

+
+
-
-
-

{pushName || "WhatsApp"}

+{phone}

- -
-
+ + {/* What's active */} +
{[ - { 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 => (

{f.label}

{f.desc}

))} + { label: "Receipts", desc: "Auto-sent when someone pledges" }, + { label: "Reminders", desc: "4-step nudge sequence" }, + { label: "Chatbot", desc: "PAID, HELP, CANCEL replies" }, + ].map(f => ( +
+

{f.label}

+

{f.desc}

+
+ ))}
-
+
) } + // ── QR scanning state ── if (status === "SCAN_QR_CODE" && showQr) { return ( -
-
-

WhatsApp

- Scan QR +
+
+
+ +
+
+

WhatsApp

+

Waiting for QR scan…

+
-
+ +
{qrImage ? (
{/* eslint-disable-next-line @next/next/no-img-element */} WhatsApp QR Code
) : ( -
+
+ +
)}

Scan with your phone

-

WhatsApp → Settings → Linked Devices → Link a Device

+
+

WhatsApp → Settings → Linked Devices → Link a Device

+
- +
-
+
) } + // ── Not connected state ── return ( -
-
-

WhatsApp

- Not connected -
-
-

Connect your WhatsApp number

-
-

When you connect, donors automatically receive:

-

• Pledge receipts with bank details

-

• Payment reminders on a 4-step schedule

-

• A chatbot (they reply PAID, HELP, or CANCEL)

+
+
+
+ +
+
+
+

WhatsApp

+ Not connected +
+

Connect to send automatic receipts and reminders

- -
+ +
+
+

When you connect, donors automatically receive:

+
+

Pledge receipt with your bank details — within seconds

+

Payment reminders — 4-step sequence over 14 days

+

Chatbot replies — they text PAID, HELP, or CANCEL

+
+
+ +
+ ) }