import { randomUUID } from "node:crypto"; import { db } from "@kk/db"; import { adminRequest, registration } from "@kk/db/schema"; import { user } from "@kk/db/schema/auth"; import type { RouterClient } from "@orpc/server"; import { and, count, desc, eq, gte, like, lte } from "drizzle-orm"; import { z } from "zod"; import { adminProcedure, protectedProcedure, publicProcedure } from "../index"; const submitRegistrationSchema = z.object({ firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email(), phone: z.string().optional(), wantsToPerform: z.boolean().default(false), artForm: z.string().optional(), experience: z.string().optional(), extraQuestions: z.string().optional(), }); const getRegistrationsSchema = z.object({ search: z.string().optional(), artForm: z.string().optional(), fromDate: z.string().datetime().optional(), toDate: z.string().datetime().optional(), page: z.number().int().min(1).default(1), pageSize: z.number().int().min(1).max(100).default(50), }); export const appRouter = { healthCheck: publicProcedure.handler(() => { return "OK"; }), privateData: protectedProcedure.handler(({ context }) => { return { message: "This is private", user: context.session?.user, }; }), submitRegistration: publicProcedure .input(submitRegistrationSchema) .handler(async ({ input }) => { const result = await db.insert(registration).values({ id: randomUUID(), firstName: input.firstName, lastName: input.lastName, email: input.email, phone: input.phone || null, wantsToPerform: input.wantsToPerform, artForm: input.artForm || null, experience: input.experience || null, extraQuestions: input.extraQuestions || null, }); return { success: true, id: result.lastInsertRowid }; }), getRegistrations: adminProcedure .input(getRegistrationsSchema) .handler(async ({ input }) => { const conditions = []; if (input.search) { const searchTerm = `%${input.search}%`; conditions.push( and( like(registration.firstName, searchTerm), like(registration.lastName, searchTerm), like(registration.email, searchTerm), ), ); } if (input.artForm) { conditions.push(eq(registration.artForm, input.artForm)); } if (input.fromDate) { conditions.push(gte(registration.createdAt, new Date(input.fromDate))); } if (input.toDate) { conditions.push(lte(registration.createdAt, new Date(input.toDate))); } const whereClause = conditions.length > 0 ? and(...conditions) : undefined; const [data, countResult] = await Promise.all([ db .select() .from(registration) .where(whereClause) .orderBy(desc(registration.createdAt)) .limit(input.pageSize) .offset((input.page - 1) * input.pageSize), db.select({ count: count() }).from(registration).where(whereClause), ]); const total = countResult[0]?.count ?? 0; return { data, pagination: { page: input.page, pageSize: input.pageSize, total, totalPages: Math.ceil(total / input.pageSize), }, }; }), getRegistrationStats: adminProcedure.handler(async () => { const today = new Date(); today.setHours(0, 0, 0, 0); const [totalResult, todayResult, artFormResult] = await Promise.all([ db.select({ count: count() }).from(registration), db .select({ count: count() }) .from(registration) .where(gte(registration.createdAt, today)), db .select({ artForm: registration.artForm, count: count(), }) .from(registration) .groupBy(registration.artForm), ]); return { total: totalResult[0]?.count ?? 0, today: todayResult[0]?.count ?? 0, byArtForm: artFormResult.map((r) => ({ artForm: r.artForm, count: r.count, })), }; }), exportRegistrations: adminProcedure.handler(async () => { const data = await db .select() .from(registration) .orderBy(desc(registration.createdAt)); const headers = [ "ID", "First Name", "Last Name", "Email", "Phone", "Wants To Perform", "Art Form", "Experience", "Extra Questions", "Created At", ]; const rows = data.map((r) => [ r.id, r.firstName, r.lastName, r.email, r.phone || "", r.wantsToPerform ? "Yes" : "No", r.artForm || "", r.experience || "", r.extraQuestions || "", r.createdAt.toISOString(), ]); const csvContent = [ headers.join(","), ...rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(","), ), ].join("\n"); return { csv: csvContent, filename: `registrations-${new Date().toISOString().split("T")[0]}.csv`, }; }), // Admin Request Procedures requestAdminAccess: protectedProcedure.handler(async ({ context }) => { const userId = context.session.user.id; // Check if user is already an admin if (context.session.user.role === "admin") { return { success: false, message: "Je bent al een admin" }; } // Check if request already exists const existingRequest = await db .select() .from(adminRequest) .where(eq(adminRequest.userId, userId)) .limit(1); if (existingRequest.length > 0) { if (existingRequest[0].status === "pending") { return { success: false, message: "Je hebt al een aanvraag openstaan" }; } if (existingRequest[0].status === "approved") { return { success: false, message: "Je aanvraag is al goedgekeurd, log opnieuw in", }; } if (existingRequest[0].status === "rejected") { // Allow re-requesting if previously rejected await db .update(adminRequest) .set({ status: "pending", requestedAt: new Date(), reviewedAt: null, reviewedBy: null, }) .where(eq(adminRequest.userId, userId)); return { success: true, message: "Nieuwe aanvraag ingediend" }; } } // Create new request await db.insert(adminRequest).values({ id: randomUUID(), userId, status: "pending", }); return { success: true, message: "Admin toegang aangevraagd" }; }), getAdminRequests: adminProcedure.handler(async () => { const requests = await db .select({ id: adminRequest.id, userId: adminRequest.userId, status: adminRequest.status, requestedAt: adminRequest.requestedAt, reviewedAt: adminRequest.reviewedAt, reviewedBy: adminRequest.reviewedBy, userName: user.name, userEmail: user.email, }) .from(adminRequest) .leftJoin(user, eq(adminRequest.userId, user.id)) .orderBy(desc(adminRequest.requestedAt)); return requests; }), approveAdminRequest: adminProcedure .input(z.object({ requestId: z.string() })) .handler(async ({ input, context }) => { const request = await db .select() .from(adminRequest) .where(eq(adminRequest.id, input.requestId)) .limit(1); if (request.length === 0) { throw new Error("Aanvraag niet gevonden"); } if (request[0].status !== "pending") { throw new Error("Deze aanvraag is al behandeld"); } // Update request status await db .update(adminRequest) .set({ status: "approved", reviewedAt: new Date(), reviewedBy: context.session.user.id, }) .where(eq(adminRequest.id, input.requestId)); // Update user role to admin await db .update(user) .set({ role: "admin" }) .where(eq(user.id, request[0].userId)); return { success: true, message: "Admin toegang goedgekeurd" }; }), rejectAdminRequest: adminProcedure .input(z.object({ requestId: z.string() })) .handler(async ({ input, context }) => { const request = await db .select() .from(adminRequest) .where(eq(adminRequest.id, input.requestId)) .limit(1); if (request.length === 0) { throw new Error("Aanvraag niet gevonden"); } if (request[0].status !== "pending") { throw new Error("Deze aanvraag is al behandeld"); } await db .update(adminRequest) .set({ status: "rejected", reviewedAt: new Date(), reviewedBy: context.session.user.id, }) .where(eq(adminRequest.id, input.requestId)); return { success: true, message: "Admin toegang geweigerd" }; }), }; export type AppRouter = typeof appRouter; export type AppRouterClient = RouterClient;