feat: add improved pi agent with observatory, dashboard, and pledge-now-pay-later
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { generateQrBuffer } from "@/lib/qr"
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; qrId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { qrId } = await params
|
||||
const baseUrl = process.env.BASE_URL || "http://localhost:3000"
|
||||
|
||||
// qrId is actually used to look up the code, but for simplicity use the code from query
|
||||
const code = request.nextUrl.searchParams.get("code") || qrId
|
||||
|
||||
const buffer = await generateQrBuffer({
|
||||
baseUrl,
|
||||
code,
|
||||
width: 800,
|
||||
margin: 2,
|
||||
})
|
||||
|
||||
return new NextResponse(new Uint8Array(buffer), {
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Disposition": `attachment; filename="qr-${code}.png"`,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("QR download error:", error)
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
104
pledge-now-pay-later/src/app/api/events/[id]/qr/route.ts
Normal file
104
pledge-now-pay-later/src/app/api/events/[id]/qr/route.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import prisma from "@/lib/prisma"
|
||||
import { createQrSourceSchema } from "@/lib/validators"
|
||||
import { customAlphabet } from "nanoid"
|
||||
|
||||
const generateCode = customAlphabet("23456789abcdefghjkmnpqrstuvwxyz", 8)
|
||||
|
||||
interface QrPledge {
|
||||
amountPence: number
|
||||
status: string
|
||||
}
|
||||
|
||||
interface QrRow {
|
||||
id: string
|
||||
label: string
|
||||
code: string
|
||||
volunteerName: string | null
|
||||
tableName: string | null
|
||||
scanCount: number
|
||||
createdAt: Date
|
||||
_count: { pledges: number }
|
||||
pledges: QrPledge[]
|
||||
}
|
||||
|
||||
// GET QR sources for event
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
if (!prisma) {
|
||||
return NextResponse.json([])
|
||||
}
|
||||
const sources = await prisma.qrSource.findMany({
|
||||
where: { eventId: id },
|
||||
include: {
|
||||
_count: { select: { pledges: true } },
|
||||
pledges: { select: { amountPence: true, status: true } },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
}) as QrRow[]
|
||||
|
||||
return NextResponse.json(
|
||||
sources.map((s: QrRow) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
code: s.code,
|
||||
volunteerName: s.volunteerName,
|
||||
tableName: s.tableName,
|
||||
scanCount: s.scanCount,
|
||||
pledgeCount: s._count.pledges,
|
||||
totalPledged: s.pledges.reduce((sum: number, p: QrPledge) => sum + p.amountPence, 0),
|
||||
createdAt: s.createdAt,
|
||||
}))
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("QR sources GET error:", error)
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST create QR source
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
if (!prisma) {
|
||||
return NextResponse.json({ error: "Database not configured" }, { status: 503 })
|
||||
}
|
||||
const body = await request.json()
|
||||
|
||||
const parsed = createQrSourceSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid data", details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
|
||||
const event = await prisma.event.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
if (!event) {
|
||||
return NextResponse.json({ error: "Event not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
const code = generateCode()
|
||||
|
||||
const qrSource = await prisma.qrSource.create({
|
||||
data: {
|
||||
...parsed.data,
|
||||
code,
|
||||
eventId: id,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json(qrSource, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error("QR source creation error:", error)
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
107
pledge-now-pay-later/src/app/api/events/route.ts
Normal file
107
pledge-now-pay-later/src/app/api/events/route.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import prisma from "@/lib/prisma"
|
||||
import { createEventSchema } from "@/lib/validators"
|
||||
import { resolveOrgId } from "@/lib/org"
|
||||
|
||||
interface PledgeSummary {
|
||||
amountPence: number
|
||||
status: string
|
||||
}
|
||||
|
||||
interface EventRow {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
eventDate: Date | null
|
||||
location: string | null
|
||||
goalAmount: number | null
|
||||
status: string
|
||||
createdAt: Date
|
||||
_count: { pledges: number; qrSources: number }
|
||||
pledges: PledgeSummary[]
|
||||
}
|
||||
|
||||
// GET all events for org (TODO: auth middleware)
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
if (!prisma) {
|
||||
return NextResponse.json([])
|
||||
}
|
||||
const orgId = await resolveOrgId(request.headers.get("x-org-id"))
|
||||
if (!orgId) {
|
||||
return NextResponse.json({ error: "Organization not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
const events = await prisma.event.findMany({
|
||||
where: { organizationId: orgId },
|
||||
include: {
|
||||
_count: { select: { pledges: true, qrSources: true } },
|
||||
pledges: {
|
||||
select: { amountPence: true, status: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
}) as EventRow[]
|
||||
|
||||
const formatted = events.map((e: EventRow) => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
slug: e.slug,
|
||||
eventDate: e.eventDate,
|
||||
location: e.location,
|
||||
goalAmount: e.goalAmount,
|
||||
status: e.status,
|
||||
pledgeCount: e._count.pledges,
|
||||
qrSourceCount: e._count.qrSources,
|
||||
totalPledged: e.pledges.reduce((sum: number, p: PledgeSummary) => sum + p.amountPence, 0),
|
||||
totalCollected: e.pledges
|
||||
.filter((p: PledgeSummary) => p.status === "paid")
|
||||
.reduce((sum: number, p: PledgeSummary) => sum + p.amountPence, 0),
|
||||
createdAt: e.createdAt,
|
||||
}))
|
||||
|
||||
return NextResponse.json(formatted)
|
||||
} catch (error) {
|
||||
console.error("Events GET error:", error)
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST create event
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!prisma) {
|
||||
return NextResponse.json({ error: "Database not configured" }, { status: 503 })
|
||||
}
|
||||
const orgId = await resolveOrgId(request.headers.get("x-org-id"))
|
||||
if (!orgId) {
|
||||
return NextResponse.json({ error: "Organization not found" }, { status: 404 })
|
||||
}
|
||||
const body = await request.json()
|
||||
|
||||
const parsed = createEventSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid data", details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
|
||||
const slug = parsed.data.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 50)
|
||||
|
||||
const event = await prisma.event.create({
|
||||
data: {
|
||||
...parsed.data,
|
||||
slug: slug + "-" + Date.now().toString(36),
|
||||
eventDate: parsed.data.eventDate ? new Date(parsed.data.eventDate) : null,
|
||||
organizationId: orgId,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json(event, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error("Event creation error:", error)
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user