diff --git a/pledge-now-pay-later/docs/UX_OVERHAUL_SPEC.md b/pledge-now-pay-later/docs/UX_OVERHAUL_SPEC.md new file mode 100644 index 0000000..bab1dcd --- /dev/null +++ b/pledge-now-pay-later/docs/UX_OVERHAUL_SPEC.md @@ -0,0 +1,169 @@ +# PNPL Dashboard — Complete UX Overhaul Spec + +## The Core Problem + +The dashboard was built feature-first: each page maps to a technical concept (Events, Pledges, Reconcile, Exports, Settings). But the **user doesn't think in features** — they think in **goals**: + +- "How do I get people to pledge?" +- "Has anyone paid yet?" +- "Someone said they paid but I can't find it" +- "My treasurer needs a report" + +The current UI has 3 fatal problems: + +1. **Jargon everywhere** — "Reconcile", "QR Sources", "Rail", "Initiated", "Pipeline by Status", "Conversion rate". A masjid volunteer doesn't speak SaaS. +2. **Two visual identities** — Landing pages are bold, typographic, high-contrast (BRAND.md). Dashboard is generic shadcn/ui with rounded cards, muted colours, and card-heavy layouts. They feel like different products. +3. **Feature-oriented, not goal-oriented** — The user has to learn the system's mental model instead of the system matching theirs. + +--- + +## Personas & Their Real Journeys + +### Persona 1: Charity Admin (Aaisha) +**Who:** Fundraising manager at a UK Islamic charity. Not technical. Uses WhatsApp all day. +**Her actual journey:** +1. "I need to collect pledges at our gala next Saturday" → Create campaign + links +2. "The event is tonight, give each table a QR code" → Print QR codes +3. "It's Monday, who pledged? Who's paid?" → Check the money +4. "My treasurer needs numbers for the trustees meeting" → Download a report +5. "Ahmed said he paid but we can't see it" → Find & match a payment + +**What confuses her today:** +- "Campaigns" (she calls them "appeals" or just "the dinner") +- "QR Sources" → she just wants "links for my volunteers" +- "Reconcile" → she'd say "check the bank" +- "Initiated" status → "what does that mean?" +- "Rail" → completely alien +- "Pipeline by Status" → ??? + +### Persona 2: Volunteer (Yusuf) +**Who:** Table captain at the gala. Has his own QR code. Checks his phone to see pledges. +**His actual journey:** +1. Gets a link from Aaisha → opens volunteer view +2. Shows QR code at his table → donors scan it +3. Checks "how am I doing?" on his phone +4. Wants to brag → shares leaderboard + +**He never sees the dashboard** — his entire journey is `/v/[code]`. But if Aaisha asks him to "help manage", he'd see the dashboard and be completely lost. + +### Persona 3: Treasurer (Fatima) +**Who:** Charity trustee responsible for financial oversight. Needs Gift Aid reports, bank reconciliation, clean data. +**Her actual journey:** +1. "Download last month's Gift Aid declarations" → Export +2. "Match bank statement to pledges" → Upload CSV, see matches +3. "Show me the money: what's collected vs outstanding" → Summary +4. "I need this for the trustees report" → Numbers she can screenshot + +**What confuses her today:** +- Column mapping UI (Date Column, Description Column...) → she just wants to upload a file and it works +- Technical status names +- No clear "money in vs money out" view + +### Persona Overlap +Aaisha does 80% of the work. She IS the charity admin, the event organiser, AND does the bank checking. Fatima only logs in once a month. Yusuf never logs in. + +**This means: optimise everything for Aaisha. Make Fatima's tasks findable but not in the way. Yusuf is a non-factor for dashboard design.** + +--- + +## The Redesign: Goal-Oriented Navigation + +### Before (feature-oriented) +``` +Overview | Campaigns | Pledges | Reconcile | Exports | Settings +``` + +### After (goal-oriented) +``` +Home | Collect | Money | Reports | Settings +``` + +| Old | New | Why | +|-----|-----|-----| +| Overview | **Home** | Everyone understands "Home" | +| Campaigns | **Collect** | "I want to collect pledges" — the verb, not the noun | +| Pledges | **Money** | "Where's the money?" — that's what they're checking | +| Reconcile | (merged into Money) | "Check the bank" is part of "where's the money" | +| Exports | **Reports** | Charities think in "reports", not "exports" | +| Settings | **Settings** | Fine as-is, but with friendlier sub-labels | + +### Terminology Overhaul + +| Old (SaaS jargon) | New (human language) | Where used | +|---|---|---| +| Campaign | Appeal / Collection | Nav, headers | +| QR Source | Pledge link | Cards, dialogs | +| Reconcile | Match payments | Nav, page title | +| Rail | Payment method | Tables, detail | +| Initiated | Said they've paid | Status badge | +| Pipeline by Status | How pledges are doing | Dashboard card | +| Conversion rate | % who pledged | Stats | +| CRM Export Pack | Full data download | Export card | +| Webhook / API | Connect to other tools | Export card | +| GoCardless | Direct Debit | Settings | +| Reference prefix | Code prefix | Settings | +| Auto-confirmed | Automatically matched | Reconcile | + +### Status Labels (the biggest confusion) + +| Old | New | Color | What it actually means | +|---|---|---|---| +| new | **Waiting** | Gray | Pledge made, no payment yet | +| initiated | **Said they paid** | Amber | Donor clicked "I've paid" or replied PAID | +| paid | **Received** ✓ | Green | Payment confirmed in bank | +| overdue | **Needs a nudge** | Red | No payment after reminder cycle | +| cancelled | **Cancelled** | Gray muted | Donor or staff cancelled | + +--- + +## Visual Identity: Bringing Brand into the Dashboard + +### Current Dashboard Style +- Generic shadcn/ui (rounded-lg cards, muted-foreground, hover:shadow-md) +- No brand colors except occasional trust-blue +- Card-heavy layout (everything in a Card with CardHeader/CardContent) +- Small text, low contrast +- Emoji as visual anchors (🤲, 💷, 🏦) + +### Target Dashboard Style (matching BRAND.md) +- **Sharp edges** — remove rounded-lg on cards, use squared or rounded-sm max +- **Typography as hero** — big bold numbers, not cards with icons +- **Gap-px grids** instead of card grids for stats +- **Dark sections** for key actions (1-2 per page) +- **Left-border accents** on feature items +- **No emoji in UI** — use the brand icon system +- **60-30-10 color rule** — Midnight + Paper base, Promise Blue actions, Gold/Green/Red status only +- **Signature numbered steps** (01, 02, 03) in setup flow + +--- + +## Implementation Plan + +### Phase 1: Navigation + Terminology (layout.tsx) +- Rename nav items +- Update sidebar visual to match brand +- Remove emoji from sidebar help box + +### Phase 2: Dashboard Home (page.tsx) +- Rewrite with brand typography +- Gap-px stat grid instead of card grid +- Human status labels +- Remove jargon + +### Phase 3: Collect page (events → collect) +- Rename to "Collect" +- Simplify campaign creation +- Better empty state + +### Phase 4: Money page (pledges + reconcile merged) +- Two tabs: "People" (pledge list) and "Bank" (reconcile) +- Human column labels +- Human status badges + +### Phase 5: Reports page (exports) +- Cleaner cards +- Brand style + +### Phase 6: Settings page +- Friendlier labels +- Remove technical jargon diff --git a/pledge-now-pay-later/src/app/dashboard/collect/page.tsx b/pledge-now-pay-later/src/app/dashboard/collect/page.tsx new file mode 100644 index 0000000..441a78f --- /dev/null +++ b/pledge-now-pay-later/src/app/dashboard/collect/page.tsx @@ -0,0 +1,5 @@ +/** + * /dashboard/collect — "I want people to pledge" + * This is the renamed "Campaigns/Events" page with human language. + */ +export { default } from "../events/page" diff --git a/pledge-now-pay-later/src/app/dashboard/events/page.tsx b/pledge-now-pay-later/src/app/dashboard/events/page.tsx index 8b9c181..87bb029 100644 --- a/pledge-now-pay-later/src/app/dashboard/events/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/events/page.tsx @@ -1,65 +1,49 @@ "use client" import { useState, useEffect } from "react" -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" -import { Badge } from "@/components/ui/badge" import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { formatPence } from "@/lib/utils" -import { Plus, QrCode, Calendar, MapPin, Target, ExternalLink } from "lucide-react" +import { Plus, ExternalLink } from "lucide-react" import Link from "next/link" interface EventSummary { - id: string - name: string - slug: string - eventDate: string | null - location: string | null - goalAmount: number | null - status: string - pledgeCount: number - qrSourceCount: number - totalPledged: number - totalCollected: number - paymentMode?: string - externalPlatform?: string + id: string; name: string; slug: string; eventDate: string | null + location: string | null; goalAmount: number | null; status: string + pledgeCount: number; qrSourceCount: number; totalPledged: number; totalCollected: number + paymentMode?: string; externalPlatform?: string } const platformNames: Record = { - launchgood: "🌙 LaunchGood", - enthuse: "💜 Enthuse", - justgiving: "💛 JustGiving", - gofundme: "💚 GoFundMe", - other: "🔗 External", + launchgood: "LaunchGood", enthuse: "Enthuse", justgiving: "JustGiving", + gofundme: "GoFundMe", other: "External", } -export default function EventsPage() { +export default function CollectPage() { const [events, setEvents] = useState([]) - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [loading, setLoading] = useState(true) const [showCreate, setShowCreate] = useState(false) + const [creating, setCreating] = useState(false) + const [orgType, setOrgType] = useState(null) + const [form, setForm] = useState({ + name: "", description: "", location: "", eventDate: "", goalAmount: "", + paymentMode: "self" as "self" | "external", externalUrl: "", externalPlatform: "", zakatEligible: false, + }) useEffect(() => { fetch("/api/events") - .then((r) => r.json()) - .then((data) => { - if (Array.isArray(data)) setEvents(data) - }) + .then(r => r.json()) + .then(data => { if (Array.isArray(data)) setEvents(data) }) .catch(() => {}) .finally(() => setLoading(false)) }, []) - const [creating, setCreating] = useState(false) - const [orgType, setOrgType] = useState(null) - const [form, setForm] = useState({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: "self" as "self" | "external", externalUrl: "", externalPlatform: "", zakatEligible: false }) - // Fetch org type to customize the form useEffect(() => { fetch("/api/onboarding").then(r => r.json()).then(d => { if (d.orgType) setOrgType(d.orgType) - // Auto-set external mode for fundraisers if (d.orgType === "fundraiser") setForm(f => ({ ...f, paymentMode: "external" })) }).catch(() => {}) }, []) @@ -71,274 +55,239 @@ export default function EventsPage() { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - name: form.name, - description: form.description || undefined, + name: form.name, description: form.description || undefined, location: form.location || undefined, goalAmount: form.goalAmount ? Math.round(parseFloat(form.goalAmount) * 100) : undefined, eventDate: form.eventDate ? new Date(form.eventDate).toISOString() : undefined, - paymentMode: form.paymentMode, - externalUrl: form.externalUrl || undefined, + paymentMode: form.paymentMode, externalUrl: form.externalUrl || undefined, externalPlatform: form.paymentMode === "external" ? (form.externalPlatform || "other") : undefined, zakatEligible: form.zakatEligible, }), }) if (res.ok) { const event = await res.json() - setEvents((prev) => [{ ...event, pledgeCount: 0, qrSourceCount: 0, totalPledged: 0, totalCollected: 0 }, ...prev]) + setEvents(prev => [{ ...event, pledgeCount: 0, qrSourceCount: 0, totalPledged: 0, totalCollected: 0 }, ...prev]) setShowCreate(false) setForm({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: orgType === "fundraiser" ? "external" : "self", externalUrl: "", externalPlatform: "", zakatEligible: false }) } - } catch { - // handle error - } + } catch { /* */ } setCreating(false) } return ( -
+
-

Campaigns

-

Create campaigns, share pledge links, and track donations

+

Collect

+

Create appeals, share pledge links, and track who's pledged

- +
- {/* Event cards */} -
- {events.map((event) => { - const progress = event.goalAmount ? Math.round((event.totalPledged / event.goalAmount) * 100) : 0 - - return ( - - -
-
- {event.name} - - {event.eventDate && ( - - - {new Date(event.eventDate).toLocaleDateString("en-GB")} + {/* Appeal cards — brand style: gap-px grid on desktop, stacked on mobile */} + {loading ? ( +
Loading appeals...
+ ) : events.length === 0 ? ( +
+

01

+

Create your first appeal

+

+ An appeal is a collection — your gala dinner, Ramadan campaign, mosque fund, or any cause you're raising for. +

+ +
+ ) : ( +
+ {events.map(event => { + const progress = event.goalAmount ? Math.min(100, Math.round((event.totalPledged / event.goalAmount) * 100)) : 0 + return ( +
+
+ {/* Header */} +
+
+

{event.name}

+
+ {event.eventDate && {new Date(event.eventDate).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" })}} + {event.location && {event.location}} +
+
+
+ {event.paymentMode === "external" && event.externalPlatform && ( + + {platformNames[event.externalPlatform] || "External"} )} - {event.location && ( - - - {event.location} - - )} - -
-
- {event.paymentMode === "external" && event.externalPlatform && ( - {platformNames[event.externalPlatform] || "External"} - )} - - {event.status} - -
-
- - -
-
-

{event.pledgeCount}

-

Pledges

-
-
-

{formatPence(event.totalPledged)}

-

Pledged

-
-
-

{formatPence(event.totalCollected)}

-

Collected

-
-
- - {event.goalAmount && ( -
-
- {progress}% of goal - - {formatPence(event.goalAmount)} + + {event.status === "active" ? "Live" : event.status}
-
-
+
+ + {/* Stats — gap-px grid */} +
+
+

{event.pledgeCount}

+

Pledges

+
+
+

{formatPence(event.totalPledged)}

+

Promised

+
+
+

{formatPence(event.totalCollected)}

+

Received

- )} -
- - - - - - + {/* Goal bar */} + {event.goalAmount && ( +
+
+ {progress}% of target + {formatPence(event.goalAmount)} +
+
+
+
+
+ )} + + {/* Actions */} +
+ + + + + + +
- - - ) - })} -
+
+ ) + })} +
+ )} {/* Create dialog */} - New Campaign + New Appeal
- + setForm((f) => ({ ...f, name: e.target.value }))} + onChange={e => setForm(f => ({ ...f, name: e.target.value }))} />
- -