feat: remove FPX, add UK charity persona features
- Remove FPX payment rail entirely (Malaysian, not UK) - Add volunteer portal (/v/[code]) with live pledge tracking - Add public event page (/e/[slug]) with progress bar + social proof - Add fundraiser leaderboard (/dashboard/events/[id]/leaderboard) - Add WhatsApp share buttons on confirmation, bank instructions, volunteer view - Enhanced Gift Aid UX with +25% bonus display and HMRC declaration text - Gift Aid report export (HMRC-ready CSV filter) - Volunteer view link + WhatsApp share on QR code cards - Updated home page: 4 personas, 3 UK payment rails, 8 features - Public event API endpoint with privacy-safe donor name truncation - Volunteer API with stats, conversion rate, auto-refresh
This commit is contained in:
@@ -51,6 +51,9 @@ export async function GET(
|
||||
scanCount: s.scanCount,
|
||||
pledgeCount: s._count.pledges,
|
||||
totalPledged: s.pledges.reduce((sum: number, p: QrPledge) => sum + p.amountPence, 0),
|
||||
totalCollected: s.pledges
|
||||
.filter((p: QrPledge) => p.status === "paid")
|
||||
.reduce((sum: number, p: QrPledge) => sum + p.amountPence, 0),
|
||||
createdAt: s.createdAt,
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import prisma from "@/lib/prisma"
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params
|
||||
if (!prisma) {
|
||||
return NextResponse.json({ error: "Database not configured" }, { status: 503 })
|
||||
}
|
||||
|
||||
// Find event by slug (try both org-scoped and plain slug)
|
||||
const event = await prisma.event.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ slug },
|
||||
{ slug: { contains: slug } },
|
||||
],
|
||||
status: { in: ["active", "closed"] },
|
||||
},
|
||||
include: {
|
||||
organization: { select: { name: true } },
|
||||
qrSources: {
|
||||
select: { code: true, label: true, volunteerName: true },
|
||||
orderBy: { createdAt: "asc" },
|
||||
},
|
||||
pledges: {
|
||||
select: {
|
||||
donorName: true,
|
||||
amountPence: true,
|
||||
status: true,
|
||||
giftAid: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!event) {
|
||||
return NextResponse.json({ error: "Event not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
const pledges = event.pledges
|
||||
const totalPledged = pledges.reduce((s, p) => s + p.amountPence, 0)
|
||||
const totalPaid = pledges
|
||||
.filter((p) => p.status === "paid")
|
||||
.reduce((s, p) => s + p.amountPence, 0)
|
||||
const giftAidCount = pledges.filter((p) => p.giftAid).length
|
||||
|
||||
return NextResponse.json({
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
eventDate: event.eventDate,
|
||||
location: event.location,
|
||||
goalAmount: event.goalAmount,
|
||||
organizationName: event.organization.name,
|
||||
stats: {
|
||||
pledgeCount: pledges.length,
|
||||
totalPledged,
|
||||
totalPaid,
|
||||
giftAidCount,
|
||||
avgPledge: pledges.length > 0 ? Math.round(totalPledged / pledges.length) : 0,
|
||||
},
|
||||
recentPledges: pledges.slice(0, 10).map((p) => ({
|
||||
donorName: p.donorName ? p.donorName.split(" ")[0] + " " + (p.donorName.split(" ")[1]?.[0] || "") + "." : null,
|
||||
amountPence: p.amountPence,
|
||||
createdAt: p.createdAt,
|
||||
giftAid: p.giftAid,
|
||||
})),
|
||||
qrCodes: event.qrSources,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Public event error:", error)
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,13 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: "Organization not found" }, { status: 404 })
|
||||
}
|
||||
const eventId = request.nextUrl.searchParams.get("eventId")
|
||||
const giftAidOnly = request.nextUrl.searchParams.get("giftAidOnly") === "true"
|
||||
|
||||
const pledges = await prisma.pledge.findMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
...(eventId ? { eventId } : {}),
|
||||
...(giftAidOnly ? { giftAid: true } : {}),
|
||||
},
|
||||
include: {
|
||||
event: { select: { name: true } },
|
||||
@@ -65,10 +67,14 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const csv = formatCrmExportCsv(rows)
|
||||
|
||||
const fileName = giftAidOnly
|
||||
? `gift-aid-declarations-${new Date().toISOString().slice(0, 10)}.csv`
|
||||
: `crm-export-${new Date().toISOString().slice(0, 10)}.csv`
|
||||
|
||||
return new NextResponse(csv, {
|
||||
headers: {
|
||||
"Content-Type": "text/csv",
|
||||
"Content-Disposition": `attachment; filename="crm-export-${new Date().toISOString().slice(0, 10)}.csv"`,
|
||||
"Content-Disposition": `attachment; filename="${fileName}"`,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ token: string }> }
|
||||
) {
|
||||
const { token } = await params
|
||||
const db = prisma
|
||||
|
||||
const qrSource = await db.qrSource.findUnique({
|
||||
where: { code: token },
|
||||
include: {
|
||||
event: {
|
||||
include: { organization: { select: { name: true } } },
|
||||
},
|
||||
pledges: {
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
reference: true,
|
||||
amountPence: true,
|
||||
status: true,
|
||||
donorName: true,
|
||||
giftAid: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!qrSource) {
|
||||
return NextResponse.json({ error: "QR code not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
const pledges = qrSource.pledges
|
||||
const totalPledgedPence = pledges.reduce((s, p) => s + p.amountPence, 0)
|
||||
const totalPaidPence = pledges
|
||||
.filter((p) => p.status === "paid")
|
||||
.reduce((s, p) => s + p.amountPence, 0)
|
||||
|
||||
return NextResponse.json({
|
||||
qrSource: {
|
||||
label: qrSource.label,
|
||||
volunteerName: qrSource.volunteerName,
|
||||
code: qrSource.code,
|
||||
scanCount: qrSource.scanCount,
|
||||
},
|
||||
event: {
|
||||
name: qrSource.event.name,
|
||||
organizationName: qrSource.event.organization.name,
|
||||
},
|
||||
pledges,
|
||||
stats: {
|
||||
totalPledges: pledges.length,
|
||||
totalPledgedPence,
|
||||
totalPaidPence,
|
||||
conversionRate: qrSource.scanCount > 0
|
||||
? Math.round((pledges.length / qrSource.scanCount) * 100)
|
||||
: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user