feat:multiple bezoekers
This commit is contained in:
@@ -12,19 +12,34 @@ import {
|
||||
} from "../email";
|
||||
import { adminProcedure, protectedProcedure, publicProcedure } from "../index";
|
||||
|
||||
const registrationTypeSchema = z.enum(["performer", "watcher"]);
|
||||
|
||||
const guestSchema = z.object({
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email().optional().or(z.literal("")),
|
||||
phone: z.string().optional(),
|
||||
});
|
||||
|
||||
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),
|
||||
registrationType: registrationTypeSchema.default("watcher"),
|
||||
// Performer-specific
|
||||
artForm: z.string().optional(),
|
||||
experience: z.string().optional(),
|
||||
isOver16: z.boolean().optional(),
|
||||
// Watcher-specific: drinkCardValue is computed server-side, guests are named
|
||||
guests: z.array(guestSchema).max(9).optional(),
|
||||
// Shared
|
||||
extraQuestions: z.string().optional(),
|
||||
});
|
||||
|
||||
const getRegistrationsSchema = z.object({
|
||||
search: z.string().optional(),
|
||||
registrationType: registrationTypeSchema.optional(),
|
||||
artForm: z.string().optional(),
|
||||
fromDate: z.string().datetime().optional(),
|
||||
toDate: z.string().datetime().optional(),
|
||||
@@ -48,15 +63,22 @@ export const appRouter = {
|
||||
.input(submitRegistrationSchema)
|
||||
.handler(async ({ input }) => {
|
||||
const managementToken = randomUUID();
|
||||
const isPerformer = input.registrationType === "performer";
|
||||
const guests = isPerformer ? [] : (input.guests ?? []);
|
||||
// €5 for primary registrant + €2 per extra guest
|
||||
const drinkCardValue = isPerformer ? 0 : 5 + guests.length * 2;
|
||||
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,
|
||||
registrationType: input.registrationType,
|
||||
artForm: isPerformer ? input.artForm || null : null,
|
||||
experience: isPerformer ? input.experience || null : null,
|
||||
isOver16: isPerformer ? (input.isOver16 ?? false) : false,
|
||||
drinkCardValue,
|
||||
guests: guests.length > 0 ? JSON.stringify(guests) : null,
|
||||
extraQuestions: input.extraQuestions || null,
|
||||
managementToken,
|
||||
});
|
||||
@@ -65,7 +87,7 @@ export const appRouter = {
|
||||
to: input.email,
|
||||
firstName: input.firstName,
|
||||
managementToken,
|
||||
wantsToPerform: input.wantsToPerform,
|
||||
wantsToPerform: isPerformer,
|
||||
artForm: input.artForm,
|
||||
}).catch((err) =>
|
||||
console.error("Failed to send confirmation email:", err),
|
||||
@@ -98,9 +120,11 @@ export const appRouter = {
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
phone: z.string().optional(),
|
||||
wantsToPerform: z.boolean().default(false),
|
||||
registrationType: registrationTypeSchema.default("watcher"),
|
||||
artForm: z.string().optional(),
|
||||
experience: z.string().optional(),
|
||||
isOver16: z.boolean().optional(),
|
||||
guests: z.array(guestSchema).max(9).optional(),
|
||||
extraQuestions: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
@@ -119,6 +143,9 @@ export const appRouter = {
|
||||
const row = rows[0];
|
||||
if (!row) throw new Error("Inschrijving niet gevonden of al geannuleerd");
|
||||
|
||||
const isPerformer = input.registrationType === "performer";
|
||||
const guests = isPerformer ? [] : (input.guests ?? []);
|
||||
const drinkCardValue = isPerformer ? 0 : 5 + guests.length * 2;
|
||||
await db
|
||||
.update(registration)
|
||||
.set({
|
||||
@@ -126,9 +153,12 @@ export const appRouter = {
|
||||
lastName: input.lastName,
|
||||
email: input.email,
|
||||
phone: input.phone || null,
|
||||
wantsToPerform: input.wantsToPerform,
|
||||
artForm: input.artForm || null,
|
||||
experience: input.experience || null,
|
||||
registrationType: input.registrationType,
|
||||
artForm: isPerformer ? input.artForm || null : null,
|
||||
experience: isPerformer ? input.experience || null : null,
|
||||
isOver16: isPerformer ? (input.isOver16 ?? false) : false,
|
||||
drinkCardValue,
|
||||
guests: guests.length > 0 ? JSON.stringify(guests) : null,
|
||||
extraQuestions: input.extraQuestions || null,
|
||||
})
|
||||
.where(eq(registration.managementToken, input.token));
|
||||
@@ -137,7 +167,7 @@ export const appRouter = {
|
||||
to: input.email,
|
||||
firstName: input.firstName,
|
||||
managementToken: input.token,
|
||||
wantsToPerform: input.wantsToPerform,
|
||||
wantsToPerform: isPerformer,
|
||||
artForm: input.artForm,
|
||||
}).catch((err) => console.error("Failed to send update email:", err));
|
||||
|
||||
@@ -192,6 +222,12 @@ export const appRouter = {
|
||||
);
|
||||
}
|
||||
|
||||
if (input.registrationType) {
|
||||
conditions.push(
|
||||
eq(registration.registrationType, input.registrationType),
|
||||
);
|
||||
}
|
||||
|
||||
if (input.artForm) {
|
||||
conditions.push(eq(registration.artForm, input.artForm));
|
||||
}
|
||||
@@ -235,20 +271,29 @@ export const appRouter = {
|
||||
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),
|
||||
]);
|
||||
const [totalResult, todayResult, artFormResult, typeResult] =
|
||||
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)
|
||||
.where(eq(registration.registrationType, "performer"))
|
||||
.groupBy(registration.artForm),
|
||||
db
|
||||
.select({
|
||||
registrationType: registration.registrationType,
|
||||
count: count(),
|
||||
})
|
||||
.from(registration)
|
||||
.groupBy(registration.registrationType),
|
||||
]);
|
||||
|
||||
return {
|
||||
total: totalResult[0]?.count ?? 0,
|
||||
@@ -257,6 +302,10 @@ export const appRouter = {
|
||||
artForm: r.artForm,
|
||||
count: r.count,
|
||||
})),
|
||||
byType: typeResult.map((r) => ({
|
||||
registrationType: r.registrationType,
|
||||
count: r.count,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -272,24 +321,46 @@ export const appRouter = {
|
||||
"Last Name",
|
||||
"Email",
|
||||
"Phone",
|
||||
"Wants To Perform",
|
||||
"Type",
|
||||
"Art Form",
|
||||
"Experience",
|
||||
"Is Over 16",
|
||||
"Drink Card Value",
|
||||
"Guest Count",
|
||||
"Guests",
|
||||
"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 rows = data.map((r) => {
|
||||
const guests: Array<{
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}> = r.guests ? JSON.parse(r.guests) : [];
|
||||
const guestSummary = guests
|
||||
.map(
|
||||
(g) =>
|
||||
`${g.firstName} ${g.lastName}${g.email ? ` <${g.email}>` : ""}${g.phone ? ` (${g.phone})` : ""}`,
|
||||
)
|
||||
.join(" | ");
|
||||
return [
|
||||
r.id,
|
||||
r.firstName,
|
||||
r.lastName,
|
||||
r.email,
|
||||
r.phone || "",
|
||||
r.registrationType,
|
||||
r.artForm || "",
|
||||
r.experience || "",
|
||||
r.isOver16 ? "Yes" : "No",
|
||||
String(r.drinkCardValue ?? 0),
|
||||
String(guests.length),
|
||||
guestSummary,
|
||||
r.extraQuestions || "",
|
||||
r.createdAt.toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
@@ -321,16 +392,17 @@ export const appRouter = {
|
||||
.limit(1);
|
||||
|
||||
if (existingRequest.length > 0) {
|
||||
if (existingRequest[0].status === "pending") {
|
||||
const existing = existingRequest[0]!;
|
||||
if (existing.status === "pending") {
|
||||
return { success: false, message: "Je hebt al een aanvraag openstaan" };
|
||||
}
|
||||
if (existingRequest[0].status === "approved") {
|
||||
if (existing.status === "approved") {
|
||||
return {
|
||||
success: false,
|
||||
message: "Je aanvraag is al goedgekeurd, log opnieuw in",
|
||||
};
|
||||
}
|
||||
if (existingRequest[0].status === "rejected") {
|
||||
if (existing.status === "rejected") {
|
||||
// Allow re-requesting if previously rejected
|
||||
await db
|
||||
.update(adminRequest)
|
||||
@@ -387,7 +459,8 @@ export const appRouter = {
|
||||
throw new Error("Aanvraag niet gevonden");
|
||||
}
|
||||
|
||||
if (request[0].status !== "pending") {
|
||||
const req = request[0]!;
|
||||
if (req.status !== "pending") {
|
||||
throw new Error("Deze aanvraag is al behandeld");
|
||||
}
|
||||
|
||||
@@ -405,7 +478,7 @@ export const appRouter = {
|
||||
await db
|
||||
.update(user)
|
||||
.set({ role: "admin" })
|
||||
.where(eq(user.id, request[0].userId));
|
||||
.where(eq(user.id, req.userId));
|
||||
|
||||
return { success: true, message: "Admin toegang goedgekeurd" };
|
||||
}),
|
||||
@@ -423,7 +496,8 @@ export const appRouter = {
|
||||
throw new Error("Aanvraag niet gevonden");
|
||||
}
|
||||
|
||||
if (request[0].status !== "pending") {
|
||||
const req = request[0]!;
|
||||
if (req.status !== "pending") {
|
||||
throw new Error("Deze aanvraag is al behandeld");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import dotenv from "dotenv";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
dotenv.config({
|
||||
path: "../../apps/web/.env",
|
||||
path: "../env/.env",
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Migration: Replace wantsToPerform with registrationType + add isOver16 + drinkCardValue
|
||||
-- Migrate existing 'wants_to_perform' data to 'registration_type' before dropping
|
||||
|
||||
ALTER TABLE `registration` ADD `registration_type` text NOT NULL DEFAULT 'watcher';--> statement-breakpoint
|
||||
ALTER TABLE `registration` ADD `is_over_16` integer NOT NULL DEFAULT false;--> statement-breakpoint
|
||||
ALTER TABLE `registration` ADD `drink_card_value` integer DEFAULT 0;--> statement-breakpoint
|
||||
|
||||
-- Backfill registration_type from wants_to_perform
|
||||
UPDATE `registration` SET `registration_type` = 'performer' WHERE `wants_to_perform` = 1;--> statement-breakpoint
|
||||
UPDATE `registration` SET `registration_type` = 'watcher' WHERE `wants_to_perform` = 0;--> statement-breakpoint
|
||||
|
||||
-- Backfill drink_card_value for watchers
|
||||
UPDATE `registration` SET `drink_card_value` = 5 WHERE `wants_to_perform` = 0;--> statement-breakpoint
|
||||
|
||||
-- Create index on registration_type
|
||||
CREATE INDEX `registration_registrationType_idx` ON `registration` (`registration_type`);--> statement-breakpoint
|
||||
|
||||
-- Drop old wants_to_perform column (SQLite requires recreating the table)
|
||||
-- SQLite doesn't support DROP COLUMN directly in older versions, but Turso/libSQL does
|
||||
ALTER TABLE `registration` DROP COLUMN `wants_to_perform`;
|
||||
2
packages/db/src/migrations/0003_add_guests.sql
Normal file
2
packages/db/src/migrations/0003_add_guests.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Migration: Add guests column (JSON text) to registration table
|
||||
ALTER TABLE `registration` ADD `guests` text;
|
||||
@@ -15,6 +15,13 @@
|
||||
"when": 1772480778632,
|
||||
"tag": "0001_third_stark_industries",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1772520000000,
|
||||
"tag": "0002_registration_type_redesign",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,11 +9,19 @@ export const registration = sqliteTable(
|
||||
lastName: text("last_name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
phone: text("phone"),
|
||||
wantsToPerform: integer("wants_to_perform", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
// registrationType: 'performer' | 'watcher'
|
||||
registrationType: text("registration_type").notNull().default("watcher"),
|
||||
// Performer-specific fields
|
||||
artForm: text("art_form"),
|
||||
experience: text("experience"),
|
||||
isOver16: integer("is_over_16", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
// Watcher-specific fields
|
||||
drinkCardValue: integer("drink_card_value").default(0),
|
||||
// Guests: JSON array of {firstName, lastName, email?, phone?} objects
|
||||
guests: text("guests"),
|
||||
// Shared
|
||||
extraQuestions: text("extra_questions"),
|
||||
managementToken: text("management_token").unique(),
|
||||
cancelledAt: integer("cancelled_at", { mode: "timestamp_ms" }),
|
||||
@@ -23,6 +31,7 @@ export const registration = sqliteTable(
|
||||
},
|
||||
(table) => [
|
||||
index("registration_email_idx").on(table.email),
|
||||
index("registration_registrationType_idx").on(table.registrationType),
|
||||
index("registration_artForm_idx").on(table.artForm),
|
||||
index("registration_createdAt_idx").on(table.createdAt),
|
||||
index("registration_managementToken_idx").on(table.managementToken),
|
||||
|
||||
@@ -29,7 +29,7 @@ export const web = await TanStackStart("web", {
|
||||
SMTP_PASS: getEnvVar("SMTP_PASS"),
|
||||
SMTP_FROM: getEnvVar("SMTP_FROM"),
|
||||
},
|
||||
domains: ["kunstenkamp.be"],
|
||||
domains: ["kunstenkamp.be", "www.kunstenkamp.be"],
|
||||
});
|
||||
|
||||
console.log(`Web -> ${web.url}`);
|
||||
|
||||
Reference in New Issue
Block a user