simplify: zakat yes/no per campaign, remove 5 fund types, add I've Donated button for external pledges
- Event.zakatEligible (boolean) replaces Organization.zakatEnabled + 5 fund types - Pledge.isZakat (boolean) replaces Pledge.fundType enum - Removed fundAllocation from Event (campaign IS the allocation) - Identity step: simple checkbox instead of 5-option grid - Campaign creation: Zakat toggle + optional external URL for self-payment - External redirect step: 'I've Donated' button calls /api/pledges/[id]/mark-initiated - Landing page: simplified Zakat section (toggle preview, not 5 fund descriptions) - Settings: removed org-level Zakat toggle (it's per campaign now) - Migration: ALTER TABLE adds zakatEligible/isZakat, drops fundAllocation
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
-- Move Zakat to campaign level, simplify pledge
|
||||||
|
ALTER TABLE "Event" ADD COLUMN IF NOT EXISTS "zakatEligible" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE "Pledge" ADD COLUMN IF NOT EXISTS "isZakat" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
-- Keep fundType column for backward compat but it's deprecated
|
||||||
|
-- DROP fundAllocation — the campaign name IS the allocation
|
||||||
|
ALTER TABLE "Event" DROP COLUMN IF EXISTS "fundAllocation";
|
||||||
|
ALTER TABLE "Organization" DROP COLUMN IF EXISTS "zakatEnabled";
|
||||||
@@ -24,7 +24,6 @@ model Organization {
|
|||||||
gcAccessToken String?
|
gcAccessToken String?
|
||||||
gcEnvironment String @default("sandbox")
|
gcEnvironment String @default("sandbox")
|
||||||
whatsappConnected Boolean @default(false)
|
whatsappConnected Boolean @default(false)
|
||||||
zakatEnabled Boolean @default(false) // enables Zakat / Sadaqah / Lillah fund type picker
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ model Event {
|
|||||||
paymentMode String @default("self") // self = we show bank details, external = redirect to URL
|
paymentMode String @default("self") // self = we show bank details, external = redirect to URL
|
||||||
externalUrl String? // e.g. https://launchgood.com/my-campaign
|
externalUrl String? // e.g. https://launchgood.com/my-campaign
|
||||||
externalPlatform String? // launchgood, enthuse, justgiving, gofundme, other
|
externalPlatform String? // launchgood, enthuse, justgiving, gofundme, other
|
||||||
fundAllocation String? // e.g. "Mosque Building Fund" — tracks which fund this event raises for
|
zakatEligible Boolean @default(false) // is this campaign Zakat-eligible?
|
||||||
organizationId String
|
organizationId String
|
||||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -104,7 +103,7 @@ model Pledge {
|
|||||||
donorEmail String?
|
donorEmail String?
|
||||||
donorPhone String?
|
donorPhone String?
|
||||||
giftAid Boolean @default(false)
|
giftAid Boolean @default(false)
|
||||||
fundType String? // null=general, zakat, sadaqah, lillah, fitrana
|
isZakat Boolean @default(false) // donor marked this as Zakat
|
||||||
iPaidClickedAt DateTime?
|
iPaidClickedAt DateTime?
|
||||||
notes String?
|
notes String?
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export async function POST(request: NextRequest) {
|
|||||||
paymentMode: parsed.data.paymentMode || "self",
|
paymentMode: parsed.data.paymentMode || "self",
|
||||||
externalUrl: parsed.data.externalUrl,
|
externalUrl: parsed.data.externalUrl,
|
||||||
externalPlatform: parsed.data.externalPlatform,
|
externalPlatform: parsed.data.externalPlatform,
|
||||||
fundAllocation: parsed.data.fundAllocation,
|
zakatEligible: parsed.data.zakatEligible || false,
|
||||||
slug: slug + "-" + Date.now().toString(36),
|
slug: slug + "-" + Date.now().toString(36),
|
||||||
eventDate: parsed.data.eventDate ? new Date(parsed.data.eventDate) : null,
|
eventDate: parsed.data.eventDate ? new Date(parsed.data.eventDate) : null,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import prisma from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
|
|
||||||
export async function POST(
|
// Donor self-reports "I've donated" on external platform
|
||||||
request: NextRequest,
|
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
if (!prisma) {
|
|
||||||
return NextResponse.json({ error: "Database not configured" }, { status: 503 })
|
|
||||||
}
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
|
|
||||||
if (id.startsWith("demo-")) {
|
const pledge = await prisma.pledge.findUnique({ where: { id } })
|
||||||
return NextResponse.json({ ok: true })
|
if (!pledge) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.pledge.update({
|
// Only update if currently "new" or "initiated"
|
||||||
where: { id },
|
if (pledge.status === "new" || pledge.status === "initiated") {
|
||||||
data: {
|
await prisma.pledge.update({
|
||||||
status: "initiated",
|
where: { id },
|
||||||
iPaidClickedAt: new Date(),
|
data: { status: "initiated" },
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ok: true })
|
return NextResponse.json({ ok: true, status: "initiated" })
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error("Mark initiated error:", error)
|
console.error("mark-initiated error:", e)
|
||||||
return NextResponse.json({ error: "Internal error" }, { status: 500 })
|
return NextResponse.json({ error: "Failed" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export async function POST(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { amountPence, rail, donorName, donorEmail, donorPhone, giftAid, fundType, eventId, qrSourceId, scheduleMode, dueDate, installmentCount, installmentDates } = parsed.data
|
const { amountPence, rail, donorName, donorEmail, donorPhone, giftAid, isZakat, eventId, qrSourceId, scheduleMode, dueDate, installmentCount, installmentDates } = parsed.data
|
||||||
|
|
||||||
// Get event + org
|
// Get event + org
|
||||||
const event = await prisma.event.findUnique({
|
const event = await prisma.event.findUnique({
|
||||||
@@ -161,7 +161,7 @@ export async function POST(request: NextRequest) {
|
|||||||
donorEmail: donorEmail || null,
|
donorEmail: donorEmail || null,
|
||||||
donorPhone: donorPhone || null,
|
donorPhone: donorPhone || null,
|
||||||
giftAid,
|
giftAid,
|
||||||
fundType: fundType || null,
|
isZakat: isZakat || false,
|
||||||
eventId,
|
eventId,
|
||||||
qrSourceId: qrSourceId || null,
|
qrSourceId: qrSourceId || null,
|
||||||
organizationId: org.id,
|
organizationId: org.id,
|
||||||
@@ -231,7 +231,7 @@ export async function POST(request: NextRequest) {
|
|||||||
donorEmail: donorEmail || null,
|
donorEmail: donorEmail || null,
|
||||||
donorPhone: donorPhone || null,
|
donorPhone: donorPhone || null,
|
||||||
giftAid,
|
giftAid,
|
||||||
fundType: fundType || null,
|
isZakat: isZakat || false,
|
||||||
eventId,
|
eventId,
|
||||||
qrSourceId: qrSourceId || null,
|
qrSourceId: qrSourceId || null,
|
||||||
organizationId: org.id,
|
organizationId: org.id,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function GET(
|
|||||||
if (token === "demo") {
|
if (token === "demo") {
|
||||||
const event = await prisma.event.findFirst({
|
const event = await prisma.event.findFirst({
|
||||||
where: { status: "active" },
|
where: { status: "active" },
|
||||||
include: { organization: { select: { name: true, zakatEnabled: true } } },
|
include: { organization: { select: { name: true } } },
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
})
|
})
|
||||||
if (!event) {
|
if (!event) {
|
||||||
@@ -31,8 +31,8 @@ export async function GET(
|
|||||||
paymentMode: event.paymentMode || "self",
|
paymentMode: event.paymentMode || "self",
|
||||||
externalUrl: event.externalUrl || null,
|
externalUrl: event.externalUrl || null,
|
||||||
externalPlatform: event.externalPlatform || null,
|
externalPlatform: event.externalPlatform || null,
|
||||||
zakatEnabled: event.organization.zakatEnabled || false,
|
zakatEligible: event.zakatEligible || false,
|
||||||
fundAllocation: event.fundAllocation || null,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export async function GET(
|
|||||||
include: {
|
include: {
|
||||||
event: {
|
event: {
|
||||||
include: {
|
include: {
|
||||||
organization: { select: { name: true, zakatEnabled: true } },
|
organization: { select: { name: true } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -66,8 +66,8 @@ export async function GET(
|
|||||||
paymentMode: qrSource.event.paymentMode || "self",
|
paymentMode: qrSource.event.paymentMode || "self",
|
||||||
externalUrl: qrSource.event.externalUrl || null,
|
externalUrl: qrSource.event.externalUrl || null,
|
||||||
externalPlatform: qrSource.event.externalPlatform || null,
|
externalPlatform: qrSource.event.externalPlatform || null,
|
||||||
zakatEnabled: qrSource.event.organization.zakatEnabled || false,
|
zakatEligible: qrSource.event.zakatEligible || false,
|
||||||
fundAllocation: qrSource.event.fundAllocation || null,
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("QR resolve error:", error)
|
console.error("QR resolve error:", error)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export async function GET(request: NextRequest) {
|
|||||||
gcAccessToken: org.gcAccessToken ? "••••••••" : "",
|
gcAccessToken: org.gcAccessToken ? "••••••••" : "",
|
||||||
gcEnvironment: org.gcEnvironment,
|
gcEnvironment: org.gcEnvironment,
|
||||||
orgType: org.orgType || "charity",
|
orgType: org.orgType || "charity",
|
||||||
zakatEnabled: org.zakatEnabled || false,
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Settings GET error:", error)
|
console.error("Settings GET error:", error)
|
||||||
@@ -90,8 +89,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
data[key] = body[key]
|
data[key] = body[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Boolean fields
|
// (boolean fields can be added here as needed)
|
||||||
if ("zakatEnabled" in body) data.zakatEnabled = !!body.zakatEnabled
|
|
||||||
|
|
||||||
const org = await prisma.organization.update({
|
const org = await prisma.organization.update({
|
||||||
where: { id: orgId },
|
where: { id: orgId },
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function EventsPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [orgType, setOrgType] = useState<string | null>(null)
|
const [orgType, setOrgType] = useState<string | null>(null)
|
||||||
const [form, setForm] = useState({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: "self" as "self" | "external", externalUrl: "", externalPlatform: "", fundAllocation: "" })
|
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
|
// Fetch org type to customize the form
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -79,14 +79,14 @@ export default function EventsPage() {
|
|||||||
paymentMode: form.paymentMode,
|
paymentMode: form.paymentMode,
|
||||||
externalUrl: form.externalUrl || undefined,
|
externalUrl: form.externalUrl || undefined,
|
||||||
externalPlatform: form.paymentMode === "external" ? (form.externalPlatform || "other") : undefined,
|
externalPlatform: form.paymentMode === "external" ? (form.externalPlatform || "other") : undefined,
|
||||||
fundAllocation: form.fundAllocation || undefined,
|
zakatEligible: form.zakatEligible,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const event = await res.json()
|
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)
|
setShowCreate(false)
|
||||||
setForm({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: orgType === "fundraiser" ? "external" : "self", externalUrl: "", externalPlatform: "", fundAllocation: "" })
|
setForm({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: orgType === "fundraiser" ? "external" : "self", externalUrl: "", externalPlatform: "", zakatEligible: false })
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// handle error
|
// handle error
|
||||||
@@ -298,29 +298,37 @@ export default function EventsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fund allocation — for charities tracking which fund this goes to */}
|
{/* Zakat eligible toggle */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setForm(f => ({ ...f, zakatEligible: !f.zakatEligible }))}
|
||||||
|
className={`w-full flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all ${
|
||||||
|
form.zakatEligible ? "border-trust-blue bg-trust-blue/5" : "border-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold">🌙 Zakat eligible</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Let donors mark their pledge as Zakat</p>
|
||||||
|
</div>
|
||||||
|
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${form.zakatEligible ? "bg-trust-blue border-trust-blue" : "border-gray-300"}`}>
|
||||||
|
{form.zakatEligible && <span className="text-white text-xs font-bold">✓</span>}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* External URL for self-payment campaigns (for reference/allocation) */}
|
||||||
{form.paymentMode === "self" && (
|
{form.paymentMode === "self" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Fund allocation <span className="text-muted-foreground font-normal">(optional)</span></Label>
|
<Label>External fundraising page <span className="text-muted-foreground font-normal">(optional)</span></Label>
|
||||||
<Input
|
<div className="relative">
|
||||||
placeholder="e.g. Mosque Building Fund, Orphan Sponsorship"
|
<ExternalLink className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
value={form.fundAllocation}
|
<Input
|
||||||
onChange={(e) => setForm(f => ({ ...f, fundAllocation: e.target.value }))}
|
placeholder="https://launchgood.com/my-campaign"
|
||||||
/>
|
value={form.externalUrl}
|
||||||
<p className="text-[10px] text-muted-foreground">Track which fund this event raises for. Shows on pledge receipts & reports.</p>
|
onChange={(e) => setForm(f => ({ ...f, externalUrl: e.target.value }))}
|
||||||
<div className="space-y-2">
|
className="pl-9"
|
||||||
<Label>External fundraising page <span className="text-muted-foreground font-normal">(optional)</span></Label>
|
/>
|
||||||
<div className="relative">
|
|
||||||
<ExternalLink className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="https://launchgood.com/mosque-building"
|
|
||||||
value={form.externalUrl}
|
|
||||||
onChange={(e) => setForm(f => ({ ...f, externalUrl: e.target.value }))}
|
|
||||||
className="pl-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-muted-foreground">Link an external campaign page for reference & allocation tracking.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground">Link an external page for reference. Payments still go via your bank.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ interface OrgSettings {
|
|||||||
gcAccessToken: string
|
gcAccessToken: string
|
||||||
gcEnvironment: string
|
gcEnvironment: string
|
||||||
orgType: string
|
orgType: string
|
||||||
zakatEnabled: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -118,42 +117,6 @@ export default function SettingsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Zakat & Fund Types */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base flex items-center gap-2">☪️ Zakat & Fund Types</CardTitle>
|
|
||||||
<CardDescription className="text-xs">Let donors specify their donation type (Zakat, Sadaqah, Lillah, Fitrana)</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setSettings(s => s ? { ...s, zakatEnabled: !s.zakatEnabled } : s)
|
|
||||||
fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ zakatEnabled: !settings.zakatEnabled }) }).then(() => { setSaved("zakat"); setTimeout(() => setSaved(null), 2000) }).catch(() => {})
|
|
||||||
}}
|
|
||||||
className={`w-full flex items-center justify-between rounded-xl border-2 p-4 transition-all ${
|
|
||||||
settings.zakatEnabled ? "border-trust-blue bg-trust-blue/5" : "border-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="text-sm font-bold">{settings.zakatEnabled ? "Fund types enabled" : "Enable fund types"}</p>
|
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">Donors can choose: Zakat · Sadaqah · Lillah · Fitrana · General</p>
|
|
||||||
</div>
|
|
||||||
<div className={`w-11 h-6 rounded-full transition-colors ${settings.zakatEnabled ? "bg-trust-blue" : "bg-gray-200"}`}>
|
|
||||||
<div className={`w-5 h-5 bg-white rounded-full shadow-sm mt-0.5 transition-transform ${settings.zakatEnabled ? "translate-x-5.5 ml-[22px]" : "translate-x-0.5 ml-[2px]"}`} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{settings.zakatEnabled && (
|
|
||||||
<div className="rounded-xl bg-trust-blue/5 border border-trust-blue/10 p-3 space-y-1.5 text-xs text-muted-foreground animate-fade-in">
|
|
||||||
<p>🌙 <strong>Zakat</strong> — Obligatory 2.5% annual charity</p>
|
|
||||||
<p>🤲 <strong>Sadaqah / General</strong> — Voluntary donations</p>
|
|
||||||
<p>🌱 <strong>Sadaqah Jariyah</strong> — Ongoing charity (buildings, wells)</p>
|
|
||||||
<p>🕌 <strong>Lillah</strong> — For the mosque / institution</p>
|
|
||||||
<p>🍽️ <strong>Fitrana</strong> — Zakat al-Fitr (before Eid)</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Branding */}
|
{/* Branding */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface PledgeData {
|
|||||||
donorEmail: string
|
donorEmail: string
|
||||||
donorPhone: string
|
donorPhone: string
|
||||||
giftAid: boolean
|
giftAid: boolean
|
||||||
fundType?: string
|
isZakat: boolean
|
||||||
// Scheduling
|
// Scheduling
|
||||||
scheduleMode: "now" | "date" | "installments"
|
scheduleMode: "now" | "date" | "installments"
|
||||||
dueDate?: string
|
dueDate?: string
|
||||||
@@ -38,8 +38,7 @@ interface EventInfo {
|
|||||||
paymentMode: "self" | "external"
|
paymentMode: "self" | "external"
|
||||||
externalUrl: string | null
|
externalUrl: string | null
|
||||||
externalPlatform: string | null
|
externalPlatform: string | null
|
||||||
zakatEnabled: boolean
|
zakatEligible: boolean
|
||||||
fundAllocation: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -66,6 +65,7 @@ export default function PledgePage() {
|
|||||||
donorEmail: "",
|
donorEmail: "",
|
||||||
donorPhone: "",
|
donorPhone: "",
|
||||||
giftAid: false,
|
giftAid: false,
|
||||||
|
isZakat: false,
|
||||||
scheduleMode: "now",
|
scheduleMode: "now",
|
||||||
})
|
})
|
||||||
const [pledgeResult, setPledgeResult] = useState<{
|
const [pledgeResult, setPledgeResult] = useState<{
|
||||||
@@ -137,7 +137,7 @@ export default function PledgePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Submit pledge (from identity step, or card/DD steps)
|
// Submit pledge (from identity step, or card/DD steps)
|
||||||
const submitPledge = async (identity: { donorName: string; donorEmail: string; donorPhone: string; giftAid: boolean; fundType?: string }) => {
|
const submitPledge = async (identity: { donorName: string; donorEmail: string; donorPhone: string; giftAid: boolean; isZakat?: boolean }) => {
|
||||||
const finalData = { ...pledgeData, ...identity }
|
const finalData = { ...pledgeData, ...identity }
|
||||||
setPledgeData(finalData)
|
setPledgeData(finalData)
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ export default function PledgePage() {
|
|||||||
...finalData,
|
...finalData,
|
||||||
eventId: eventInfo?.id,
|
eventId: eventInfo?.id,
|
||||||
qrSourceId: eventInfo?.qrSourceId,
|
qrSourceId: eventInfo?.qrSourceId,
|
||||||
fundType: finalData.fundType || undefined,
|
isZakat: finalData.isZakat || false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const result = await res.json()
|
const result = await res.json()
|
||||||
@@ -209,7 +209,7 @@ export default function PledgePage() {
|
|||||||
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} />,
|
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} />,
|
||||||
1: <ScheduleStep amount={pledgeData.amountPence} onSelect={handleScheduleSelected} />,
|
1: <ScheduleStep amount={pledgeData.amountPence} onSelect={handleScheduleSelected} />,
|
||||||
2: <PaymentStep onSelect={handleRailSelected} amount={pledgeData.amountPence} />,
|
2: <PaymentStep onSelect={handleRailSelected} amount={pledgeData.amountPence} />,
|
||||||
3: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} zakatEnabled={eventInfo?.zakatEnabled} fundAllocation={eventInfo?.fundAllocation} />,
|
3: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} zakatEligible={eventInfo?.zakatEligible} />,
|
||||||
4: pledgeResult && <BankInstructionsStep pledge={pledgeResult} amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} donorPhone={pledgeData.donorPhone} />,
|
4: pledgeResult && <BankInstructionsStep pledge={pledgeResult} amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} donorPhone={pledgeData.donorPhone} />,
|
||||||
5: pledgeResult && (
|
5: pledgeResult && (
|
||||||
<ConfirmationStep
|
<ConfirmationStep
|
||||||
|
|||||||
@@ -90,10 +90,21 @@ export function ExternalRedirectStep({ pledge, amount, eventName, externalUrl, e
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Re-open link */}
|
{/* Confirm donation + re-open */}
|
||||||
<Button onClick={() => window.open(externalUrl, "_blank")} className="w-full" variant="outline">
|
<div className="space-y-2">
|
||||||
<ExternalLink className="h-4 w-4 mr-2" /> Open {platform.name} again
|
<Button
|
||||||
</Button>
|
onClick={() => {
|
||||||
|
fetch(`/api/pledges/${pledge.id}/mark-initiated`, { method: "POST" }).catch(() => {})
|
||||||
|
if (navigator.vibrate) navigator.vibrate([10, 50, 10])
|
||||||
|
}}
|
||||||
|
className="w-full bg-success-green hover:bg-success-green/90 text-white"
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 mr-2" /> I've Donated ✓
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => window.open(externalUrl, "_blank")} className="w-full" variant="outline">
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" /> Open {platform.name} again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Share */}
|
{/* Share */}
|
||||||
<div className="rounded-2xl bg-gradient-to-br from-warm-amber/5 to-orange-50 border border-warm-amber/20 p-5 space-y-3">
|
<div className="rounded-2xl bg-gradient-to-br from-warm-amber/5 to-orange-50 border border-warm-amber/20 p-5 space-y-3">
|
||||||
@@ -124,7 +135,7 @@ export function ExternalRedirectStep({ pledge, amount, eventName, externalUrl, e
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
We'll send you a gentle reminder if we don't see a donation come through.
|
Donated already? Tap "I've Donated" above, or reply <strong>PAID</strong> to the WhatsApp message.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,33 +4,24 @@ import { useState, useRef, useEffect } from "react"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Gift, Shield, Sparkles, Phone, Mail } from "lucide-react"
|
import { Gift, Shield, Sparkles, Phone, Mail } from "lucide-react"
|
||||||
|
|
||||||
const FUND_TYPES = [
|
|
||||||
{ id: "general", label: "General Donation", icon: "🤲", desc: "Sadaqah — used where most needed" },
|
|
||||||
{ id: "zakat", label: "Zakat", icon: "🌙", desc: "Obligatory annual charity (2.5%)" },
|
|
||||||
{ id: "sadaqah", label: "Sadaqah Jariyah", icon: "🌱", desc: "Ongoing charity — builds, wells, education" },
|
|
||||||
{ id: "lillah", label: "Lillah", icon: "🕌", desc: "For the mosque / institution itself" },
|
|
||||||
{ id: "fitrana", label: "Fitrana", icon: "🍽️", desc: "Zakat al-Fitr — given before Eid" },
|
|
||||||
]
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: (data: {
|
onSubmit: (data: {
|
||||||
donorName: string
|
donorName: string
|
||||||
donorEmail: string
|
donorEmail: string
|
||||||
donorPhone: string
|
donorPhone: string
|
||||||
giftAid: boolean
|
giftAid: boolean
|
||||||
fundType?: string
|
isZakat?: boolean
|
||||||
}) => void
|
}) => void
|
||||||
amount: number
|
amount: number
|
||||||
zakatEnabled?: boolean
|
zakatEligible?: boolean
|
||||||
fundAllocation?: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IdentityStep({ onSubmit, amount, zakatEnabled, fundAllocation }: Props) {
|
export function IdentityStep({ onSubmit, amount, zakatEligible }: Props) {
|
||||||
const [name, setName] = useState("")
|
const [name, setName] = useState("")
|
||||||
const [email, setEmail] = useState("")
|
const [email, setEmail] = useState("")
|
||||||
const [phone, setPhone] = useState("")
|
const [phone, setPhone] = useState("")
|
||||||
const [giftAid, setGiftAid] = useState(false)
|
const [giftAid, setGiftAid] = useState(false)
|
||||||
const [fundType, setFundType] = useState<string>(fundAllocation ? "general" : "general")
|
const [isZakat, setIsZakat] = useState(false)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [contactMode, setContactMode] = useState<"email" | "phone">("email")
|
const [contactMode, setContactMode] = useState<"email" | "phone">("email")
|
||||||
const nameRef = useRef<HTMLInputElement>(null)
|
const nameRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -46,7 +37,7 @@ export function IdentityStep({ onSubmit, amount, zakatEnabled, fundAllocation }:
|
|||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await onSubmit({ donorName: name, donorEmail: email, donorPhone: phone, giftAid, fundType: zakatEnabled ? fundType : undefined })
|
await onSubmit({ donorName: name, donorEmail: email, donorPhone: phone, giftAid, isZakat: zakatEligible ? isZakat : false })
|
||||||
} catch {
|
} catch {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
@@ -134,35 +125,28 @@ export function IdentityStep({ onSubmit, amount, zakatEnabled, fundAllocation }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fund Type — only when org has Zakat enabled */}
|
{/* Zakat — only when campaign is Zakat-eligible */}
|
||||||
{zakatEnabled && (
|
{zakatEligible && (
|
||||||
<div className="space-y-2 animate-fade-in">
|
<button
|
||||||
<p className="text-sm font-bold text-gray-900">
|
onClick={() => setIsZakat(!isZakat)}
|
||||||
{fundAllocation ? `Fund: ${fundAllocation}` : "What is this donation for?"}
|
className={`w-full text-left rounded-2xl border-2 p-4 transition-all ${
|
||||||
</p>
|
isZakat
|
||||||
<div className="grid grid-cols-2 gap-2">
|
? "border-trust-blue bg-trust-blue/5 shadow-sm"
|
||||||
{FUND_TYPES.map((ft) => (
|
: "border-gray-200 bg-white hover:border-trust-blue/40"
|
||||||
<button
|
}`}
|
||||||
key={ft.id}
|
>
|
||||||
onClick={() => setFundType(ft.id)}
|
<div className="flex items-center gap-3">
|
||||||
className={`text-left rounded-xl border-2 p-3 transition-all ${
|
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all ${isZakat ? "bg-trust-blue border-trust-blue" : "border-gray-300"}`}>
|
||||||
fundType === ft.id
|
{isZakat && <span className="text-white text-xs font-bold">✓</span>}
|
||||||
? "border-trust-blue bg-trust-blue/5 shadow-sm"
|
|
||||||
: "border-gray-100 hover:border-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-lg">{ft.icon}</span>
|
|
||||||
<p className={`text-xs font-bold mt-1 ${fundType === ft.id ? "text-trust-blue" : "text-gray-900"}`}>{ft.label}</p>
|
|
||||||
<p className="text-[10px] text-muted-foreground leading-tight">{ft.desc}</p>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{fundType === "zakat" && (
|
|
||||||
<div className="rounded-xl bg-warm-amber/5 border border-warm-amber/20 p-3 text-xs text-muted-foreground animate-fade-in">
|
|
||||||
☪️ Zakat is distributed according to the eight categories specified in the Quran (9:60). This charity is Zakat-eligible.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex-1">
|
||||||
</div>
|
<span className="font-bold text-sm">🌙 This is Zakat</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Mark this pledge as Zakat (obligatory charity). It will be tracked separately.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Gift Aid — the hero */}
|
{/* Gift Aid — the hero */}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export default function HomePage() {
|
|||||||
<div className="grid md:grid-cols-4 gap-6">
|
<div className="grid md:grid-cols-4 gap-6">
|
||||||
{[
|
{[
|
||||||
{ step: "1", icon: "🔗", title: "Share a pledge link", desc: "Create a trackable link. Share via QR code, WhatsApp, social media, email, or send directly to a major donor." },
|
{ step: "1", icon: "🔗", title: "Share a pledge link", desc: "Create a trackable link. Share via QR code, WhatsApp, social media, email, or send directly to a major donor." },
|
||||||
{ step: "2", icon: "🤲", title: "Donor pledges", desc: "They pick an amount, choose Zakat/Sadaqah, and decide: pay now, on a date, or in monthly instalments." },
|
{ step: "2", icon: "🤲", title: "Donor pledges", desc: "They pick an amount, mark as Zakat if eligible, and decide: pay now, on a date, or in monthly instalments." },
|
||||||
{ step: "3", icon: "💬", title: "WhatsApp follows up", desc: "Automated reminders with bank details or a link to your fundraising page. They reply PAID when done." },
|
{ step: "3", icon: "💬", title: "WhatsApp follows up", desc: "Automated reminders with bank details or a link to your fundraising page. They reply PAID when done." },
|
||||||
{ step: "4", icon: "📊", title: "You see everything", desc: "Live dashboard: who pledged, who paid, what fund, which source. Export for HMRC Gift Aid." },
|
{ step: "4", icon: "📊", title: "You see everything", desc: "Live dashboard: who pledged, who paid, what fund, which source. Export for HMRC Gift Aid." },
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
@@ -222,31 +222,17 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Fund Types */}
|
{/* Zakat */}
|
||||||
<section className="py-14 px-4">
|
<section className="py-14 px-4">
|
||||||
<div className="max-w-4xl mx-auto space-y-8">
|
<div className="max-w-3xl mx-auto text-center space-y-6">
|
||||||
<div className="text-center">
|
<h2 className="text-3xl font-black text-gray-900">🌙 Zakat tracking built-in</h2>
|
||||||
<h2 className="text-3xl font-black text-gray-900">Islamic fund types built-in</h2>
|
<p className="text-muted-foreground max-w-xl mx-auto">
|
||||||
<p className="text-muted-foreground mt-2">Donors choose their fund type. You get clean reporting. Zakat never mixes with Sadaqah.</p>
|
Mark any campaign as Zakat-eligible. Donors see a simple checkbox to flag their pledge as Zakat. You get clean reporting — Zakat pledges are tracked separately so funds never mix.
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
|
||||||
{[
|
|
||||||
{ name: "Zakat", icon: "🌙", desc: "Obligatory 2.5%" },
|
|
||||||
{ name: "Sadaqah", icon: "🤲", desc: "Voluntary" },
|
|
||||||
{ name: "Sadaqah Jariyah", icon: "🌱", desc: "Ongoing" },
|
|
||||||
{ name: "Lillah", icon: "🕌", desc: "For institution" },
|
|
||||||
{ name: "Fitrana", icon: "🍽️", desc: "Before Eid" },
|
|
||||||
].map((f) => (
|
|
||||||
<div key={f.name} className="rounded-2xl border bg-white p-4 text-center space-y-1">
|
|
||||||
<span className="text-2xl">{f.icon}</span>
|
|
||||||
<p className="text-sm font-bold">{f.name}</p>
|
|
||||||
<p className="text-[11px] text-muted-foreground">{f.desc}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-center text-xs text-muted-foreground">
|
|
||||||
Enable in Settings → Fund Types. Donors see the picker during pledge. Reports break down by type.
|
|
||||||
</p>
|
</p>
|
||||||
|
<div className="inline-flex items-center gap-3 rounded-2xl border-2 border-trust-blue/20 bg-trust-blue/5 px-6 py-4">
|
||||||
|
<div className="w-5 h-5 rounded border-2 bg-trust-blue border-trust-blue flex items-center justify-center"><span className="text-white text-xs font-bold">✓</span></div>
|
||||||
|
<span className="text-sm font-bold">🌙 This is Zakat</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -262,9 +248,9 @@ export default function HomePage() {
|
|||||||
{ icon: "📅", title: "Flexible Scheduling", desc: "Pay now, pick a date, or split into 2-12 monthly instalments. Each instalment tracked separately." },
|
{ icon: "📅", title: "Flexible Scheduling", desc: "Pay now, pick a date, or split into 2-12 monthly instalments. Each instalment tracked separately." },
|
||||||
{ icon: "💬", title: "WhatsApp Reminders", desc: "Automated multi-step: 2 days before → due day → gentle nudge → final. Donors reply PAID, HELP, STATUS." },
|
{ icon: "💬", title: "WhatsApp Reminders", desc: "Automated multi-step: 2 days before → due day → gentle nudge → final. Donors reply PAID, HELP, STATUS." },
|
||||||
{ icon: "🎁", title: "Gift Aid + HMRC Export", desc: "Collect declarations inline with live math. One-click HMRC-ready CSV export." },
|
{ icon: "🎁", title: "Gift Aid + HMRC Export", desc: "Collect declarations inline with live math. One-click HMRC-ready CSV export." },
|
||||||
{ icon: "☪️", title: "Fund Type Tracking", desc: "Zakat, Sadaqah, Lillah, Fitrana. Clean fund-level reporting. Optional — enable when you need it." },
|
{ icon: "🌙", title: "Zakat Tracking", desc: "Mark campaigns as Zakat-eligible. Donors tick a checkbox. Zakat pledges tracked separately." },
|
||||||
{ icon: "📊", title: "Live Dashboard", desc: "Real-time pipeline: new → initiated → paid → overdue. Needs-attention alerts. Auto-refreshes." },
|
{ icon: "📊", title: "Live Dashboard", desc: "Real-time pipeline: new → initiated → paid → overdue. Needs-attention alerts. Auto-refreshes." },
|
||||||
{ icon: "🏦", title: "Fund Allocation", desc: "Link campaigns to specific funds. Charities can also link external fundraising pages for allocation tracking." },
|
{ icon: "🏦", title: "External Payment Tracking", desc: "Donors pledge here, pay on LaunchGood/Enthuse. They click 'I\\'ve Donated' or reply PAID on WhatsApp." },
|
||||||
{ icon: "🏆", title: "Leaderboard", desc: "See which volunteer, table, or link source brings in the most pledges. Friendly competition." },
|
{ icon: "🏆", title: "Leaderboard", desc: "See which volunteer, table, or link source brings in the most pledges. Friendly competition." },
|
||||||
{ icon: "📱", title: "QR Codes for Events", desc: "Print a QR code per table. Works alongside WhatsApp sharing, social posts, and direct links." },
|
{ icon: "📱", title: "QR Codes for Events", desc: "Print a QR code per table. Works alongside WhatsApp sharing, social posts, and direct links." },
|
||||||
{ icon: "📤", title: "CRM Export", desc: "Download all pledge data as CSV. Filter by fund type, campaign, status, or source." },
|
{ icon: "📤", title: "CRM Export", desc: "Download all pledge data as CSV. Filter by fund type, campaign, status, or source." },
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const createEventSchema = z.object({
|
|||||||
paymentMode: z.enum(['self', 'external']).default('self'),
|
paymentMode: z.enum(['self', 'external']).default('self'),
|
||||||
externalUrl: z.string().url().max(1000).optional(),
|
externalUrl: z.string().url().max(1000).optional(),
|
||||||
externalPlatform: z.enum(['launchgood', 'enthuse', 'justgiving', 'gofundme', 'other']).optional(),
|
externalPlatform: z.enum(['launchgood', 'enthuse', 'justgiving', 'gofundme', 'other']).optional(),
|
||||||
fundAllocation: z.string().max(200).optional(),
|
zakatEligible: z.boolean().default(false),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createQrSourceSchema = z.object({
|
export const createQrSourceSchema = z.object({
|
||||||
@@ -26,7 +26,7 @@ export const createPledgeSchema = z.object({
|
|||||||
donorEmail: z.string().max(200).optional().default(''),
|
donorEmail: z.string().max(200).optional().default(''),
|
||||||
donorPhone: z.string().max(20).optional().default(''),
|
donorPhone: z.string().max(20).optional().default(''),
|
||||||
giftAid: z.boolean().default(false),
|
giftAid: z.boolean().default(false),
|
||||||
fundType: z.enum(['general', 'zakat', 'sadaqah', 'lillah', 'fitrana']).optional(),
|
isZakat: z.boolean().default(false),
|
||||||
eventId: z.string(),
|
eventId: z.string(),
|
||||||
qrSourceId: z.string().nullable().optional(),
|
qrSourceId: z.string().nullable().optional(),
|
||||||
// Payment scheduling
|
// Payment scheduling
|
||||||
|
|||||||
Reference in New Issue
Block a user