feat: add improved pi agent with observatory, dashboard, and pledge-now-pay-later
This commit is contained in:
156
pledge-now-pay-later/src/app/api/dashboard/route.ts
Normal file
156
pledge-now-pay-later/src/app/api/dashboard/route.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import prisma from "@/lib/prisma"
|
||||
import { resolveOrgId } from "@/lib/org"
|
||||
|
||||
interface PledgeRow {
|
||||
id: string
|
||||
reference: string
|
||||
amountPence: number
|
||||
status: string
|
||||
rail: string
|
||||
donorName: string | null
|
||||
donorEmail: string | null
|
||||
donorPhone: string | null
|
||||
giftAid: boolean
|
||||
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 resolveOrgId(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[]]
|
||||
|
||||
const totalPledged = pledges.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<string, number> = {}
|
||||
pledges.forEach((p: PledgeRow) => {
|
||||
byStatus[p.status] = (byStatus[p.status] || 0) + 1
|
||||
})
|
||||
|
||||
// Rail breakdown
|
||||
const byRail: Record<string, number> = {}
|
||||
pledges.forEach((p: PledgeRow) => {
|
||||
byRail[p.rail] = (byRail[p.rail] || 0) + 1
|
||||
})
|
||||
|
||||
// Top QR sources
|
||||
const qrStats: Record<string, { label: string; count: number; amount: number }> = {}
|
||||
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,
|
||||
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,
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user