import { NextRequest, NextResponse } from "next/server" import prisma from "@/lib/prisma" import { getOrgId } from "@/lib/session" interface PledgeRow { id: string reference: string amountPence: number status: string rail: string donorName: string | null donorEmail: string | null donorPhone: string | null giftAid: boolean dueDate: Date | null planId: string | null installmentNumber: number | null installmentTotal: number | null createdAt: Date paidAt: Date | null event: { name: string } qrSource: { label: string; volunteerName: string | null; tableName: string | null } | null reminders: Array<{ step: number; status: string; scheduledAt: Date }> } interface AnalyticsRow { eventType: string _count: number } interface ReminderRow { step: number status: string scheduledAt: Date } export async function GET(request: NextRequest) { try { if (!prisma) { return NextResponse.json({ summary: { totalPledges: 12, totalPledgedPence: 2450000, totalCollectedPence: 1820000, collectionRate: 74, overdueRate: 8, }, byStatus: { paid: 8, pending: 2, overdue: 1, cancelled: 1 }, byRail: { bank_transfer: 10, card: 2 }, topSources: [ { label: "Table 1 - Ahmed", count: 4, amount: 850000 }, { label: "Table 2 - Fatima", count: 3, amount: 620000 }, ], funnel: { qr_scan: 45, pledge_started: 32, pledge_completed: 12 }, pledges: [], }) } const orgId = await getOrgId(request.headers.get("x-org-id")) if (!orgId) { return NextResponse.json({ error: "Organization not found" }, { status: 404 }) } const eventId = request.nextUrl.searchParams.get("eventId") const where = { organizationId: orgId, ...(eventId ? { eventId } : {}), } const [pledges, analytics] = await Promise.all([ prisma.pledge.findMany({ where, include: { event: { select: { name: true } }, qrSource: { select: { label: true, volunteerName: true, tableName: true } }, reminders: { select: { step: true, status: true, scheduledAt: true } }, }, orderBy: { createdAt: "desc" }, }), prisma.analyticsEvent.groupBy({ by: ["eventType"], where: eventId ? { eventId } : {}, _count: true, }), ]) as [PledgeRow[], AnalyticsRow[]] // eslint-disable-next-line @typescript-eslint/no-explicit-any const confirmedPledges = pledges.filter((p: any) => !p.isConditional || p.conditionMet) // eslint-disable-next-line @typescript-eslint/no-explicit-any const conditionalPledges = pledges.filter((p: any) => p.isConditional && !p.conditionMet) const totalPledged = confirmedPledges.reduce((s: number, p: PledgeRow) => s + p.amountPence, 0) const totalConditional = conditionalPledges.reduce((s: number, p: PledgeRow) => s + p.amountPence, 0) const totalCollected = pledges .filter((p: PledgeRow) => p.status === "paid") .reduce((s: number, p: PledgeRow) => s + p.amountPence, 0) const collectionRate = totalPledged > 0 ? totalCollected / totalPledged : 0 const overdueCount = pledges.filter((p: PledgeRow) => p.status === "overdue").length const overdueRate = pledges.length > 0 ? overdueCount / pledges.length : 0 // Status breakdown const byStatus: Record = {} pledges.forEach((p: PledgeRow) => { byStatus[p.status] = (byStatus[p.status] || 0) + 1 }) // Rail breakdown const byRail: Record = {} pledges.forEach((p: PledgeRow) => { byRail[p.rail] = (byRail[p.rail] || 0) + 1 }) // Top QR sources const qrStats: Record = {} pledges.forEach((p: PledgeRow) => { if (p.qrSource) { const key = p.qrSource.label if (!qrStats[key]) qrStats[key] = { label: key, count: 0, amount: 0 } qrStats[key].count++ qrStats[key].amount += p.amountPence } }) // Funnel from analytics const funnel = Object.fromEntries(analytics.map((a: AnalyticsRow) => [a.eventType, a._count])) return NextResponse.json({ summary: { totalPledges: pledges.length, totalPledgedPence: totalPledged, totalCollectedPence: totalCollected, totalConditionalPence: totalConditional, conditionalCount: conditionalPledges.length, collectionRate: Math.round(collectionRate * 100), overdueRate: Math.round(overdueRate * 100), }, byStatus, byRail, topSources: Object.values(qrStats).sort((a: { amount: number }, b: { amount: number }) => b.amount - a.amount).slice(0, 10), funnel, pledges: pledges.map((p: PledgeRow) => ({ id: p.id, reference: p.reference, amountPence: p.amountPence, status: p.status, rail: p.rail, donorName: p.donorName, donorEmail: p.donorEmail, donorPhone: p.donorPhone, eventName: p.event.name, source: p.qrSource?.label || null, volunteerName: p.qrSource?.volunteerName || null, giftAid: p.giftAid, dueDate: p.dueDate, planId: p.planId, installmentNumber: p.installmentNumber, installmentTotal: p.installmentTotal, isDeferred: !!p.dueDate, // eslint-disable-next-line @typescript-eslint/no-explicit-any isConditional: (p as any).isConditional || false, // eslint-disable-next-line @typescript-eslint/no-explicit-any conditionText: (p as any).conditionText || null, // eslint-disable-next-line @typescript-eslint/no-explicit-any conditionMet: (p as any).conditionMet || false, createdAt: p.createdAt, paidAt: p.paidAt, nextReminder: p.reminders .filter((r: ReminderRow) => r.status === "pending") .sort((a: ReminderRow, b: ReminderRow) => a.scheduledAt.getTime() - b.scheduledAt.getTime())[0]?.scheduledAt || null, lastTouch: p.reminders .filter((r: ReminderRow) => r.status === "sent") .sort((a: ReminderRow, b: ReminderRow) => b.scheduledAt.getTime() - a.scheduledAt.getTime())[0]?.scheduledAt || null, })), }) } catch (error) { console.error("Dashboard error:", error) return NextResponse.json({ error: "Internal error" }, { status: 500 }) } }