diff --git a/pledge-now-pay-later/prisma/migrations/20260303_simplify_zakat/migration.sql b/pledge-now-pay-later/prisma/migrations/20260303_simplify_zakat/migration.sql new file mode 100644 index 0000000..fba866c --- /dev/null +++ b/pledge-now-pay-later/prisma/migrations/20260303_simplify_zakat/migration.sql @@ -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"; diff --git a/pledge-now-pay-later/prisma/schema.prisma b/pledge-now-pay-later/prisma/schema.prisma index 76e3c2f..06556f7 100644 --- a/pledge-now-pay-later/prisma/schema.prisma +++ b/pledge-now-pay-later/prisma/schema.prisma @@ -24,7 +24,6 @@ model Organization { gcAccessToken String? gcEnvironment String @default("sandbox") whatsappConnected Boolean @default(false) - zakatEnabled Boolean @default(false) // enables Zakat / Sadaqah / Lillah fund type picker createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -63,7 +62,7 @@ model Event { paymentMode String @default("self") // self = we show bank details, external = redirect to URL externalUrl String? // e.g. https://launchgood.com/my-campaign 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 organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @@ -104,7 +103,7 @@ model Pledge { donorEmail String? donorPhone String? giftAid Boolean @default(false) - fundType String? // null=general, zakat, sadaqah, lillah, fitrana + isZakat Boolean @default(false) // donor marked this as Zakat iPaidClickedAt DateTime? notes String? diff --git a/pledge-now-pay-later/src/app/api/events/route.ts b/pledge-now-pay-later/src/app/api/events/route.ts index d732eef..31feaeb 100644 --- a/pledge-now-pay-later/src/app/api/events/route.ts +++ b/pledge-now-pay-later/src/app/api/events/route.ts @@ -104,7 +104,7 @@ export async function POST(request: NextRequest) { paymentMode: parsed.data.paymentMode || "self", externalUrl: parsed.data.externalUrl, externalPlatform: parsed.data.externalPlatform, - fundAllocation: parsed.data.fundAllocation, + zakatEligible: parsed.data.zakatEligible || false, slug: slug + "-" + Date.now().toString(36), eventDate: parsed.data.eventDate ? new Date(parsed.data.eventDate) : null, organizationId: orgId, diff --git a/pledge-now-pay-later/src/app/api/pledges/[id]/mark-initiated/route.ts b/pledge-now-pay-later/src/app/api/pledges/[id]/mark-initiated/route.ts index edc84a9..4224eb0 100644 --- a/pledge-now-pay-later/src/app/api/pledges/[id]/mark-initiated/route.ts +++ b/pledge-now-pay-later/src/app/api/pledges/[id]/mark-initiated/route.ts @@ -1,31 +1,27 @@ -import { NextRequest, NextResponse } from "next/server" -import prisma from "@/lib/prisma" +import { NextResponse } from "next/server" +import { prisma } from "@/lib/prisma" -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { +// Donor self-reports "I've donated" on external platform +export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) { try { - if (!prisma) { - return NextResponse.json({ error: "Database not configured" }, { status: 503 }) - } const { id } = await params - if (id.startsWith("demo-")) { - return NextResponse.json({ ok: true }) + const pledge = await prisma.pledge.findUnique({ where: { id } }) + if (!pledge) { + return NextResponse.json({ error: "Not found" }, { status: 404 }) } - await prisma.pledge.update({ - where: { id }, - data: { - status: "initiated", - iPaidClickedAt: new Date(), - }, - }) + // Only update if currently "new" or "initiated" + if (pledge.status === "new" || pledge.status === "initiated") { + await prisma.pledge.update({ + where: { id }, + data: { status: "initiated" }, + }) + } - return NextResponse.json({ ok: true }) - } catch (error) { - console.error("Mark initiated error:", error) - return NextResponse.json({ error: "Internal error" }, { status: 500 }) + return NextResponse.json({ ok: true, status: "initiated" }) + } catch (e) { + console.error("mark-initiated error:", e) + return NextResponse.json({ error: "Failed" }, { status: 500 }) } } diff --git a/pledge-now-pay-later/src/app/api/pledges/route.ts b/pledge-now-pay-later/src/app/api/pledges/route.ts index 324fadb..afaa91a 100644 --- a/pledge-now-pay-later/src/app/api/pledges/route.ts +++ b/pledge-now-pay-later/src/app/api/pledges/route.ts @@ -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 const event = await prisma.event.findUnique({ @@ -161,7 +161,7 @@ export async function POST(request: NextRequest) { donorEmail: donorEmail || null, donorPhone: donorPhone || null, giftAid, - fundType: fundType || null, + isZakat: isZakat || false, eventId, qrSourceId: qrSourceId || null, organizationId: org.id, @@ -231,7 +231,7 @@ export async function POST(request: NextRequest) { donorEmail: donorEmail || null, donorPhone: donorPhone || null, giftAid, - fundType: fundType || null, + isZakat: isZakat || false, eventId, qrSourceId: qrSourceId || null, organizationId: org.id, diff --git a/pledge-now-pay-later/src/app/api/qr/[token]/route.ts b/pledge-now-pay-later/src/app/api/qr/[token]/route.ts index 671a11d..cf54467 100644 --- a/pledge-now-pay-later/src/app/api/qr/[token]/route.ts +++ b/pledge-now-pay-later/src/app/api/qr/[token]/route.ts @@ -16,7 +16,7 @@ export async function GET( if (token === "demo") { const event = await prisma.event.findFirst({ where: { status: "active" }, - include: { organization: { select: { name: true, zakatEnabled: true } } }, + include: { organization: { select: { name: true } } }, orderBy: { createdAt: "asc" }, }) if (!event) { @@ -31,8 +31,8 @@ export async function GET( paymentMode: event.paymentMode || "self", externalUrl: event.externalUrl || null, externalPlatform: event.externalPlatform || null, - zakatEnabled: event.organization.zakatEnabled || false, - fundAllocation: event.fundAllocation || null, + zakatEligible: event.zakatEligible || false, + }) } @@ -41,7 +41,7 @@ export async function GET( include: { event: { include: { - organization: { select: { name: true, zakatEnabled: true } }, + organization: { select: { name: true } }, }, }, }, @@ -66,8 +66,8 @@ export async function GET( paymentMode: qrSource.event.paymentMode || "self", externalUrl: qrSource.event.externalUrl || null, externalPlatform: qrSource.event.externalPlatform || null, - zakatEnabled: qrSource.event.organization.zakatEnabled || false, - fundAllocation: qrSource.event.fundAllocation || null, + zakatEligible: qrSource.event.zakatEligible || false, + }) } catch (error) { console.error("QR resolve error:", error) diff --git a/pledge-now-pay-later/src/app/api/settings/route.ts b/pledge-now-pay-later/src/app/api/settings/route.ts index d977a14..73cbc59 100644 --- a/pledge-now-pay-later/src/app/api/settings/route.ts +++ b/pledge-now-pay-later/src/app/api/settings/route.ts @@ -26,7 +26,6 @@ export async function GET(request: NextRequest) { gcAccessToken: org.gcAccessToken ? "••••••••" : "", gcEnvironment: org.gcEnvironment, orgType: org.orgType || "charity", - zakatEnabled: org.zakatEnabled || false, }) } catch (error) { console.error("Settings GET error:", error) @@ -90,8 +89,7 @@ export async function PATCH(request: NextRequest) { data[key] = body[key] } } - // Boolean fields - if ("zakatEnabled" in body) data.zakatEnabled = !!body.zakatEnabled + // (boolean fields can be added here as needed) const org = await prisma.organization.update({ where: { id: orgId }, 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 7cf1ab4..ccea5f5 100644 --- a/pledge-now-pay-later/src/app/dashboard/events/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/events/page.tsx @@ -53,7 +53,7 @@ export default function EventsPage() { }, []) 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: "", 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 useEffect(() => { @@ -79,14 +79,14 @@ export default function EventsPage() { paymentMode: form.paymentMode, externalUrl: form.externalUrl || undefined, externalPlatform: form.paymentMode === "external" ? (form.externalPlatform || "other") : undefined, - fundAllocation: form.fundAllocation || undefined, + zakatEligible: form.zakatEligible, }), }) if (res.ok) { const event = await res.json() 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: "", fundAllocation: "" }) + setForm({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: orgType === "fundraiser" ? "external" : "self", externalUrl: "", externalPlatform: "", zakatEligible: false }) } } catch { // handle error @@ -298,29 +298,37 @@ export default function EventsPage() { )} - {/* Fund allocation — for charities tracking which fund this goes to */} + {/* Zakat eligible toggle */} + + + {/* External URL for self-payment campaigns (for reference/allocation) */} {form.paymentMode === "self" && (
- - setForm(f => ({ ...f, fundAllocation: e.target.value }))} - /> -

Track which fund this event raises for. Shows on pledge receipts & reports.

-
- -
- - setForm(f => ({ ...f, externalUrl: e.target.value }))} - className="pl-9" - /> -
-

Link an external campaign page for reference & allocation tracking.

+ +
+ + setForm(f => ({ ...f, externalUrl: e.target.value }))} + className="pl-9" + />
+

Link an external page for reference. Payments still go via your bank.

)} 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 2ff6c35..8abf72e 100644 --- a/pledge-now-pay-later/src/app/dashboard/settings/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/settings/page.tsx @@ -22,7 +22,6 @@ interface OrgSettings { gcAccessToken: string gcEnvironment: string orgType: string - zakatEnabled: boolean } export default function SettingsPage() { @@ -118,42 +117,6 @@ export default function SettingsPage() { - {/* Zakat & Fund Types */} - - - ☪️ Zakat & Fund Types - Let donors specify their donation type (Zakat, Sadaqah, Lillah, Fitrana) - - - - {settings.zakatEnabled && ( -
-

🌙 Zakat — Obligatory 2.5% annual charity

-

🤲 Sadaqah / General — Voluntary donations

-

🌱 Sadaqah Jariyah — Ongoing charity (buildings, wells)

-

🕌 Lillah — For the mosque / institution

-

🍽️ Fitrana — Zakat al-Fitr (before Eid)

-
- )} -
-
- {/* Branding */} diff --git a/pledge-now-pay-later/src/app/p/[token]/page.tsx b/pledge-now-pay-later/src/app/p/[token]/page.tsx index 6287d1f..cd9d93b 100644 --- a/pledge-now-pay-later/src/app/p/[token]/page.tsx +++ b/pledge-now-pay-later/src/app/p/[token]/page.tsx @@ -21,7 +21,7 @@ export interface PledgeData { donorEmail: string donorPhone: string giftAid: boolean - fundType?: string + isZakat: boolean // Scheduling scheduleMode: "now" | "date" | "installments" dueDate?: string @@ -38,8 +38,7 @@ interface EventInfo { paymentMode: "self" | "external" externalUrl: string | null externalPlatform: string | null - zakatEnabled: boolean - fundAllocation: string | null + zakatEligible: boolean } /* @@ -66,6 +65,7 @@ export default function PledgePage() { donorEmail: "", donorPhone: "", giftAid: false, + isZakat: false, scheduleMode: "now", }) const [pledgeResult, setPledgeResult] = useState<{ @@ -137,7 +137,7 @@ export default function PledgePage() { } // 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 } setPledgeData(finalData) @@ -149,7 +149,7 @@ export default function PledgePage() { ...finalData, eventId: eventInfo?.id, qrSourceId: eventInfo?.qrSourceId, - fundType: finalData.fundType || undefined, + isZakat: finalData.isZakat || false, }), }) const result = await res.json() @@ -209,7 +209,7 @@ export default function PledgePage() { 0: , 1: , 2: , - 3: , + 3: , 4: pledgeResult && , 5: pledgeResult && ( - {/* Re-open link */} - + {/* Confirm donation + re-open */} +
+ + +
{/* Share */}
@@ -124,7 +135,7 @@ export function ExternalRedirectStep({ pledge, amount, eventName, externalUrl, e

- 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 PAID to the WhatsApp message.

) diff --git a/pledge-now-pay-later/src/app/p/[token]/steps/identity-step.tsx b/pledge-now-pay-later/src/app/p/[token]/steps/identity-step.tsx index 47875c1..b916a91 100644 --- a/pledge-now-pay-later/src/app/p/[token]/steps/identity-step.tsx +++ b/pledge-now-pay-later/src/app/p/[token]/steps/identity-step.tsx @@ -4,33 +4,24 @@ import { useState, useRef, useEffect } from "react" import { Button } from "@/components/ui/button" 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 { onSubmit: (data: { donorName: string donorEmail: string donorPhone: string giftAid: boolean - fundType?: string + isZakat?: boolean }) => void amount: number - zakatEnabled?: boolean - fundAllocation?: string | null + zakatEligible?: boolean } -export function IdentityStep({ onSubmit, amount, zakatEnabled, fundAllocation }: Props) { +export function IdentityStep({ onSubmit, amount, zakatEligible }: Props) { const [name, setName] = useState("") const [email, setEmail] = useState("") const [phone, setPhone] = useState("") const [giftAid, setGiftAid] = useState(false) - const [fundType, setFundType] = useState(fundAllocation ? "general" : "general") + const [isZakat, setIsZakat] = useState(false) const [submitting, setSubmitting] = useState(false) const [contactMode, setContactMode] = useState<"email" | "phone">("email") const nameRef = useRef(null) @@ -46,7 +37,7 @@ export function IdentityStep({ onSubmit, amount, zakatEnabled, fundAllocation }: if (!isValid) return setSubmitting(true) 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 { setSubmitting(false) } @@ -134,35 +125,28 @@ export function IdentityStep({ onSubmit, amount, zakatEnabled, fundAllocation }: )} - {/* Fund Type — only when org has Zakat enabled */} - {zakatEnabled && ( -
-

- {fundAllocation ? `Fund: ${fundAllocation}` : "What is this donation for?"} -

-
- {FUND_TYPES.map((ft) => ( - - ))} -
- {fundType === "zakat" && ( -
- ☪️ Zakat is distributed according to the eight categories specified in the Quran (9:60). This charity is Zakat-eligible. + {/* Zakat — only when campaign is Zakat-eligible */} + {zakatEligible && ( +
+ )} {/* Gift Aid — the hero */} diff --git a/pledge-now-pay-later/src/app/page.tsx b/pledge-now-pay-later/src/app/page.tsx index 07ca9e8..eeb4ab7 100644 --- a/pledge-now-pay-later/src/app/page.tsx +++ b/pledge-now-pay-later/src/app/page.tsx @@ -161,7 +161,7 @@ export default function HomePage() {
{[ { 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: "4", icon: "📊", title: "You see everything", desc: "Live dashboard: who pledged, who paid, what fund, which source. Export for HMRC Gift Aid." }, ].map((s) => ( @@ -222,31 +222,17 @@ export default function HomePage() {
- {/* Fund Types */} + {/* Zakat */}
-
-
-

Islamic fund types built-in

-

Donors choose their fund type. You get clean reporting. Zakat never mixes with Sadaqah.

-
-
- {[ - { 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) => ( -
- {f.icon} -

{f.name}

-

{f.desc}

-
- ))} -
-

- Enable in Settings → Fund Types. Donors see the picker during pledge. Reports break down by type. +

+

🌙 Zakat tracking built-in

+

+ 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.

+
+
+ 🌙 This is Zakat +
@@ -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: "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: "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: "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: "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." }, diff --git a/pledge-now-pay-later/src/lib/validators.ts b/pledge-now-pay-later/src/lib/validators.ts index addd18d..45a1a73 100644 --- a/pledge-now-pay-later/src/lib/validators.ts +++ b/pledge-now-pay-later/src/lib/validators.ts @@ -10,7 +10,7 @@ export const createEventSchema = z.object({ paymentMode: z.enum(['self', 'external']).default('self'), externalUrl: z.string().url().max(1000).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({ @@ -26,7 +26,7 @@ export const createPledgeSchema = z.object({ donorEmail: z.string().max(200).optional().default(''), donorPhone: z.string().max(20).optional().default(''), giftAid: z.boolean().default(false), - fundType: z.enum(['general', 'zakat', 'sadaqah', 'lillah', 'fitrana']).optional(), + isZakat: z.boolean().default(false), eventId: z.string(), qrSourceId: z.string().nullable().optional(), // Payment scheduling