feat(registration): add birthdate & postcode

This commit is contained in:
2026-03-19 10:15:45 +01:00
parent 19d784b2e5
commit f113770348
11 changed files with 1660 additions and 5 deletions

View File

@@ -245,6 +245,169 @@ export function GuestList({
</span> </span>
)} )}
</div> </div>
{/* Birthdate */}
<div className="flex flex-col gap-2 md:col-span-2">
<p className="text-white/80">
Geboortedatum <span className="text-red-300">*</span>
</p>
<fieldset className="m-0 flex gap-2 border-0 p-0">
<legend className="sr-only">
Geboortedatum medebezoeker {idx + 1}
</legend>
<div className="flex flex-1 flex-col gap-1">
<label
htmlFor={`guest-${idx}-birthdateDay`}
className="sr-only"
>
Dag
</label>
<select
id={`guest-${idx}-birthdateDay`}
value={guest.birthdate?.split("-")[2] ?? ""}
onChange={(e) => {
const parts = (guest.birthdate || "--").split("-");
onChange(
idx,
"birthdate",
`${parts[0] || ""}-${parts[1] || ""}-${e.target.value.padStart(2, "0")}`,
);
}}
aria-label="Dag"
className={`border-b bg-transparent pb-2 text-sm text-white transition-colors focus:outline-none ${errors[idx]?.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
DD
</option>
{Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
<option
key={d}
value={String(d).padStart(2, "0")}
className="bg-[#214e51]"
>
{String(d).padStart(2, "0")}
</option>
))}
</select>
</div>
<div className="flex flex-1 flex-col gap-1">
<label
htmlFor={`guest-${idx}-birthdateMonth`}
className="sr-only"
>
Maand
</label>
<select
id={`guest-${idx}-birthdateMonth`}
value={guest.birthdate?.split("-")[1] ?? ""}
onChange={(e) => {
const parts = (guest.birthdate || "--").split("-");
onChange(
idx,
"birthdate",
`${parts[0] || ""}-${e.target.value.padStart(2, "0")}-${parts[2] || ""}`,
);
}}
aria-label="Maand"
className={`border-b bg-transparent pb-2 text-sm text-white transition-colors focus:outline-none ${errors[idx]?.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
MM
</option>
{[
"Jan",
"Feb",
"Mrt",
"Apr",
"Mei",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dec",
].map((m, i) => (
<option
key={m}
value={String(i + 1).padStart(2, "0")}
className="bg-[#214e51]"
>
{m}
</option>
))}
</select>
</div>
<div className="flex flex-1 flex-col gap-1">
<label
htmlFor={`guest-${idx}-birthdateYear`}
className="sr-only"
>
Jaar
</label>
<select
id={`guest-${idx}-birthdateYear`}
value={guest.birthdate?.split("-")[0] ?? ""}
onChange={(e) => {
const parts = (guest.birthdate || "--").split("-");
onChange(
idx,
"birthdate",
`${e.target.value}-${parts[1] || ""}-${parts[2] || ""}`,
);
}}
aria-label="Jaar"
className={`border-b bg-transparent pb-2 text-sm text-white transition-colors focus:outline-none ${errors[idx]?.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
JJJJ
</option>
{Array.from(
{ length: 100 },
(_, i) => new Date().getFullYear() - i,
).map((y) => (
<option
key={y}
value={String(y)}
className="bg-[#214e51]"
>
{y}
</option>
))}
</select>
</div>
</fieldset>
{errors[idx]?.birthdate && (
<span className="text-red-300 text-sm" role="alert">
{errors[idx].birthdate}
</span>
)}
</div>
{/* Postcode */}
<div className="flex flex-col gap-2">
<label
htmlFor={`guest-${idx}-postcode`}
className="text-white/80"
>
Postcode <span className="text-red-300">*</span>
</label>
<input
type="text"
id={`guest-${idx}-postcode`}
value={guest.postcode}
onChange={(e) => onChange(idx, "postcode", e.target.value)}
placeholder="bv. 9000"
autoComplete="off"
inputMode="numeric"
className={inputCls(!!errors[idx]?.postcode)}
/>
{errors[idx]?.postcode && (
<span className="text-red-300 text-sm" role="alert">
{errors[idx].postcode}
</span>
)}
</div>
</div> </div>
</div> </div>
))} ))}

View File

@@ -4,8 +4,10 @@ import { toast } from "sonner";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { import {
inputCls, inputCls,
validateBirthdate,
validateEmail, validateEmail,
validatePhone, validatePhone,
validatePostcode,
validateTextField, validateTextField,
} from "@/lib/registration"; } from "@/lib/registration";
import { orpc } from "@/utils/orpc"; import { orpc } from "@/utils/orpc";
@@ -16,6 +18,8 @@ interface PerformerErrors {
lastName?: string; lastName?: string;
email?: string; email?: string;
phone?: string; phone?: string;
birthdate?: string;
postcode?: string;
artForm?: string; artForm?: string;
isOver16?: string; isOver16?: string;
} }
@@ -34,6 +38,10 @@ export function PerformerForm({ onBack, onSuccess }: Props) {
lastName: "", lastName: "",
email: "", email: "",
phone: "", phone: "",
birthdateDay: "",
birthdateMonth: "",
birthdateYear: "",
postcode: "",
artForm: "", artForm: "",
experience: "", experience: "",
isOver16: false, isOver16: false,
@@ -64,12 +72,21 @@ export function PerformerForm({ onBack, onSuccess }: Props) {
}, },
}); });
function getBirthdate(): string {
const { birthdateYear, birthdateMonth, birthdateDay } = data;
if (!birthdateYear || !birthdateMonth || !birthdateDay) return "";
return `${birthdateYear}-${birthdateMonth.padStart(2, "0")}-${birthdateDay.padStart(2, "0")}`;
}
function validate(): boolean { function validate(): boolean {
const birthdate = getBirthdate();
const errs: PerformerErrors = { const errs: PerformerErrors = {
firstName: validateTextField(data.firstName, true, "Voornaam"), firstName: validateTextField(data.firstName, true, "Voornaam"),
lastName: validateTextField(data.lastName, true, "Achternaam"), lastName: validateTextField(data.lastName, true, "Achternaam"),
email: validateEmail(data.email), email: validateEmail(data.email),
phone: validatePhone(data.phone), phone: validatePhone(data.phone),
birthdate: validateBirthdate(birthdate),
postcode: validatePostcode(data.postcode),
artForm: !data.artForm.trim() ? "Kunstvorm is verplicht" : undefined, artForm: !data.artForm.trim() ? "Kunstvorm is verplicht" : undefined,
isOver16: !data.isOver16 isOver16: !data.isOver16
? "Je moet 16 jaar of ouder zijn om op te treden" ? "Je moet 16 jaar of ouder zijn om op te treden"
@@ -81,6 +98,8 @@ export function PerformerForm({ onBack, onSuccess }: Props) {
lastName: true, lastName: true,
email: true, email: true,
phone: true, phone: true,
birthdate: true,
postcode: true,
artForm: true, artForm: true,
isOver16: true, isOver16: true,
}); });
@@ -144,6 +163,8 @@ export function PerformerForm({ onBack, onSuccess }: Props) {
lastName: data.lastName.trim(), lastName: data.lastName.trim(),
email: data.email.trim(), email: data.email.trim(),
phone: data.phone.trim() || undefined, phone: data.phone.trim() || undefined,
birthdate: getBirthdate(),
postcode: data.postcode.trim(),
registrationType: "performer", registrationType: "performer",
artForm: data.artForm.trim() || undefined, artForm: data.artForm.trim() || undefined,
experience: data.experience.trim() || undefined, experience: data.experience.trim() || undefined,
@@ -305,6 +326,149 @@ export function PerformerForm({ onBack, onSuccess }: Props) {
</div> </div>
</div> </div>
{/* Birthdate + Postcode row */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-2">
<p className="text-white text-xl">
Geboortedatum <span className="text-red-300">*</span>
</p>
<fieldset className="m-0 flex gap-2 border-0 p-0">
<legend className="sr-only">Geboortedatum</legend>
<div className="flex flex-1 flex-col gap-1">
<label htmlFor="p-birthdateDay" className="sr-only">
Dag
</label>
<select
id="p-birthdateDay"
name="birthdateDay"
value={data.birthdateDay}
onChange={(e) =>
setData((prev) => ({
...prev,
birthdateDay: e.target.value,
}))
}
aria-label="Dag"
className={`border-b bg-transparent pb-2 text-lg text-white transition-colors focus:outline-none ${touched.birthdate && errors.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
DD
</option>
{Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
<option key={d} value={String(d)} className="bg-[#214e51]">
{String(d).padStart(2, "0")}
</option>
))}
</select>
</div>
<div className="flex flex-1 flex-col gap-1">
<label htmlFor="p-birthdateMonth" className="sr-only">
Maand
</label>
<select
id="p-birthdateMonth"
name="birthdateMonth"
value={data.birthdateMonth}
onChange={(e) =>
setData((prev) => ({
...prev,
birthdateMonth: e.target.value,
}))
}
aria-label="Maand"
className={`border-b bg-transparent pb-2 text-lg text-white transition-colors focus:outline-none ${touched.birthdate && errors.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
MM
</option>
{[
"Jan",
"Feb",
"Mrt",
"Apr",
"Mei",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dec",
].map((m, i) => (
<option
key={m}
value={String(i + 1)}
className="bg-[#214e51]"
>
{m}
</option>
))}
</select>
</div>
<div className="flex flex-1 flex-col gap-1">
<label htmlFor="p-birthdateYear" className="sr-only">
Jaar
</label>
<select
id="p-birthdateYear"
name="birthdateYear"
value={data.birthdateYear}
onChange={(e) =>
setData((prev) => ({
...prev,
birthdateYear: e.target.value,
}))
}
aria-label="Jaar"
className={`border-b bg-transparent pb-2 text-lg text-white transition-colors focus:outline-none ${touched.birthdate && errors.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
JJJJ
</option>
{Array.from(
{ length: 100 },
(_, i) => new Date().getFullYear() - i,
).map((y) => (
<option key={y} value={String(y)} className="bg-[#214e51]">
{y}
</option>
))}
</select>
</div>
</fieldset>
{touched.birthdate && errors.birthdate && (
<span className="text-red-300 text-sm" role="alert">
{errors.birthdate}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="p-postcode" className="text-white text-xl">
Postcode <span className="text-red-300">*</span>
</label>
<input
type="text"
id="p-postcode"
name="postcode"
value={data.postcode}
onChange={handleChange}
onBlur={handleBlur}
placeholder="bv. 9000"
autoComplete="postal-code"
inputMode="numeric"
aria-required="true"
aria-invalid={touched.postcode && !!errors.postcode}
className={inputCls(!!touched.postcode && !!errors.postcode)}
/>
{touched.postcode && errors.postcode && (
<span className="text-red-300 text-sm" role="alert">
{errors.postcode}
</span>
)}
</div>
</div>
{/* Performer-specific fields */} {/* Performer-specific fields */}
<div className="border border-amber-400/20 bg-amber-400/5 p-6"> <div className="border border-amber-400/20 bg-amber-400/5 p-6">
<p className="mb-5 text-amber-300/80 text-sm uppercase tracking-wider"> <p className="mb-5 text-amber-300/80 text-sm uppercase tracking-wider">

View File

@@ -7,9 +7,11 @@ import {
type GuestEntry, type GuestEntry,
type GuestErrors, type GuestErrors,
inputCls, inputCls,
validateBirthdate,
validateEmail, validateEmail,
validateGuests, validateGuests,
validatePhone, validatePhone,
validatePostcode,
validateTextField, validateTextField,
} from "@/lib/registration"; } from "@/lib/registration";
import { orpc } from "@/utils/orpc"; import { orpc } from "@/utils/orpc";
@@ -21,6 +23,8 @@ interface WatcherErrors {
lastName?: string; lastName?: string;
email?: string; email?: string;
phone?: string; phone?: string;
birthdate?: string;
postcode?: string;
} }
interface Props { interface Props {
@@ -39,6 +43,10 @@ export function WatcherForm({ onBack, onSuccess }: Props) {
lastName: "", lastName: "",
email: "", email: "",
phone: "", phone: "",
birthdateDay: "",
birthdateMonth: "",
birthdateYear: "",
postcode: "",
extraQuestions: "", extraQuestions: "",
}); });
const [errors, setErrors] = useState<WatcherErrors>({}); const [errors, setErrors] = useState<WatcherErrors>({});
@@ -69,12 +77,21 @@ export function WatcherForm({ onBack, onSuccess }: Props) {
}, },
}); });
function getBirthdate(): string {
const { birthdateYear, birthdateMonth, birthdateDay } = data;
if (!birthdateYear || !birthdateMonth || !birthdateDay) return "";
return `${birthdateYear}-${birthdateMonth.padStart(2, "0")}-${birthdateDay.padStart(2, "0")}`;
}
function validate(): boolean { function validate(): boolean {
const birthdate = getBirthdate();
const fieldErrs: WatcherErrors = { const fieldErrs: WatcherErrors = {
firstName: validateTextField(data.firstName, true, "Voornaam"), firstName: validateTextField(data.firstName, true, "Voornaam"),
lastName: validateTextField(data.lastName, true, "Achternaam"), lastName: validateTextField(data.lastName, true, "Achternaam"),
email: validateEmail(data.email), email: validateEmail(data.email),
phone: validatePhone(data.phone), phone: validatePhone(data.phone),
birthdate: validateBirthdate(birthdate),
postcode: validatePostcode(data.postcode),
}; };
setErrors(fieldErrs); setErrors(fieldErrs);
setTouched({ setTouched({
@@ -82,6 +99,8 @@ export function WatcherForm({ onBack, onSuccess }: Props) {
lastName: true, lastName: true,
email: true, email: true,
phone: true, phone: true,
birthdate: true,
postcode: true,
}); });
const { errors: gErrs, valid: gValid } = validateGuests(guests); const { errors: gErrs, valid: gValid } = validateGuests(guests);
setGuestErrors(gErrs); setGuestErrors(gErrs);
@@ -134,7 +153,14 @@ export function WatcherForm({ onBack, onSuccess }: Props) {
if (guests.length >= 9) return; if (guests.length >= 9) return;
setGuests((prev) => [ setGuests((prev) => [
...prev, ...prev,
{ firstName: "", lastName: "", email: "", phone: "" }, {
firstName: "",
lastName: "",
email: "",
phone: "",
birthdate: "",
postcode: "",
},
]); ]);
setGuestErrors((prev) => [...prev, {}]); setGuestErrors((prev) => [...prev, {}]);
} }
@@ -155,12 +181,16 @@ export function WatcherForm({ onBack, onSuccess }: Props) {
lastName: data.lastName.trim(), lastName: data.lastName.trim(),
email: data.email.trim(), email: data.email.trim(),
phone: data.phone.trim() || undefined, phone: data.phone.trim() || undefined,
birthdate: getBirthdate(),
postcode: data.postcode.trim(),
registrationType: "watcher", registrationType: "watcher",
guests: guests.map((g) => ({ guests: guests.map((g) => ({
firstName: g.firstName.trim(), firstName: g.firstName.trim(),
lastName: g.lastName.trim(), lastName: g.lastName.trim(),
email: g.email.trim() || undefined, email: g.email.trim() || undefined,
phone: g.phone.trim() || undefined, phone: g.phone.trim() || undefined,
birthdate: g.birthdate.trim(),
postcode: g.postcode.trim(),
})), })),
extraQuestions: data.extraQuestions.trim() || undefined, extraQuestions: data.extraQuestions.trim() || undefined,
giftAmount, giftAmount,
@@ -341,6 +371,146 @@ export function WatcherForm({ onBack, onSuccess }: Props) {
</div> </div>
</div> </div>
{/* Birthdate + Postcode row */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-2">
<p className="text-white text-xl">
Geboortedatum <span className="text-red-300">*</span>
</p>
<fieldset className="m-0 flex gap-2 border-0 p-0">
<legend className="sr-only">Geboortedatum</legend>
<div className="flex flex-1 flex-col gap-1">
<label htmlFor="w-birthdateDay" className="sr-only">
Dag
</label>
<select
id="w-birthdateDay"
value={data.birthdateDay}
onChange={(e) =>
setData((prev) => ({
...prev,
birthdateDay: e.target.value,
}))
}
aria-label="Dag"
className={`border-b bg-transparent pb-2 text-lg text-white transition-colors focus:outline-none ${touched.birthdate && errors.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
DD
</option>
{Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
<option key={d} value={String(d)} className="bg-[#214e51]">
{String(d).padStart(2, "0")}
</option>
))}
</select>
</div>
<div className="flex flex-1 flex-col gap-1">
<label htmlFor="w-birthdateMonth" className="sr-only">
Maand
</label>
<select
id="w-birthdateMonth"
value={data.birthdateMonth}
onChange={(e) =>
setData((prev) => ({
...prev,
birthdateMonth: e.target.value,
}))
}
aria-label="Maand"
className={`border-b bg-transparent pb-2 text-lg text-white transition-colors focus:outline-none ${touched.birthdate && errors.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
MM
</option>
{[
"Jan",
"Feb",
"Mrt",
"Apr",
"Mei",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dec",
].map((m, i) => (
<option
key={m}
value={String(i + 1)}
className="bg-[#214e51]"
>
{m}
</option>
))}
</select>
</div>
<div className="flex flex-1 flex-col gap-1">
<label htmlFor="w-birthdateYear" className="sr-only">
Jaar
</label>
<select
id="w-birthdateYear"
value={data.birthdateYear}
onChange={(e) =>
setData((prev) => ({
...prev,
birthdateYear: e.target.value,
}))
}
aria-label="Jaar"
className={`border-b bg-transparent pb-2 text-lg text-white transition-colors focus:outline-none ${touched.birthdate && errors.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
>
<option value="" className="bg-[#214e51]">
JJJJ
</option>
{Array.from(
{ length: 100 },
(_, i) => new Date().getFullYear() - i,
).map((y) => (
<option key={y} value={String(y)} className="bg-[#214e51]">
{y}
</option>
))}
</select>
</div>
</fieldset>
{touched.birthdate && errors.birthdate && (
<span className="text-red-300 text-sm" role="alert">
{errors.birthdate}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="w-postcode" className="text-white text-xl">
Postcode <span className="text-red-300">*</span>
</label>
<input
type="text"
id="w-postcode"
name="postcode"
value={data.postcode}
onChange={handleChange}
onBlur={handleBlur}
placeholder="bv. 9000"
autoComplete="postal-code"
inputMode="numeric"
aria-required="true"
aria-invalid={touched.postcode && !!errors.postcode}
className={inputCls(!!touched.postcode && !!errors.postcode)}
/>
{touched.postcode && errors.postcode && (
<span className="text-red-300 text-sm" role="alert">
{errors.postcode}
</span>
)}
</div>
</div>
{/* Guests */} {/* Guests */}
<GuestList <GuestList
guests={guests} guests={guests}

View File

@@ -29,6 +29,8 @@ export interface GuestEntry {
lastName: string; lastName: string;
email: string; email: string;
phone: string; phone: string;
birthdate: string;
postcode: string;
} }
export interface GuestErrors { export interface GuestErrors {
@@ -36,6 +38,8 @@ export interface GuestErrors {
lastName?: string; lastName?: string;
email?: string; email?: string;
phone?: string; phone?: string;
birthdate?: string;
postcode?: string;
} }
/** /**
@@ -52,6 +56,8 @@ export function parseGuests(raw: string | null | undefined): GuestEntry[] {
lastName: g.lastName ?? "", lastName: g.lastName ?? "",
email: g.email ?? "", email: g.email ?? "",
phone: g.phone ?? "", phone: g.phone ?? "",
birthdate: g.birthdate ?? "",
postcode: g.postcode ?? "",
})); }));
} catch { } catch {
return []; return [];
@@ -87,6 +93,22 @@ export function validatePhone(value: string): string | undefined {
return undefined; return undefined;
} }
export function validateBirthdate(value: string): string | undefined {
if (!value.trim()) return "Geboortedatum is verplicht";
// Expect YYYY-MM-DD format
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
return "Voer een geldige geboortedatum in";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "Voer een geldige geboortedatum in";
if (date > new Date()) return "Geboortedatum mag niet in de toekomst liggen";
return undefined;
}
export function validatePostcode(value: string): string | undefined {
if (!value.trim()) return "Postcode is verplicht";
return undefined;
}
/** Validates all guests and returns errors array + overall validity flag. */ /** Validates all guests and returns errors array + overall validity flag. */
export function validateGuests(guests: GuestEntry[]): { export function validateGuests(guests: GuestEntry[]): {
errors: GuestErrors[]; errors: GuestErrors[];
@@ -103,6 +125,8 @@ export function validateGuests(guests: GuestEntry[]): {
g.phone.trim() && !/^[\d\s\-+()]{10,}$/.test(g.phone.replace(/\s/g, "")) g.phone.trim() && !/^[\d\s\-+()]{10,}$/.test(g.phone.replace(/\s/g, ""))
? "Voer een geldig telefoonnummer in" ? "Voer een geldig telefoonnummer in"
: undefined, : undefined,
birthdate: validateBirthdate(g.birthdate),
postcode: validatePostcode(g.postcode),
})); }));
const valid = !errors.some((e) => Object.values(e).some(Boolean)); const valid = !errors.some((e) => Object.values(e).some(Boolean));
return { errors, valid }; return { errors, valid };

View File

@@ -117,6 +117,8 @@ interface EditFormProps {
lastName: string; lastName: string;
email: string; email: string;
phone: string | null; phone: string | null;
birthdate: string;
postcode: string;
registrationType: string; registrationType: string;
artForm: string | null; artForm: string | null;
experience: string | null; experience: string | null;
@@ -140,6 +142,8 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) {
lastName: initialData.lastName, lastName: initialData.lastName,
email: initialData.email, email: initialData.email,
phone: initialData.phone ?? "", phone: initialData.phone ?? "",
birthdate: initialData.birthdate ?? "",
postcode: initialData.postcode ?? "",
registrationType: initialType, registrationType: initialType,
artForm: initialData.artForm ?? "", artForm: initialData.artForm ?? "",
experience: initialData.experience ?? "", experience: initialData.experience ?? "",
@@ -185,6 +189,8 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) {
? formData.experience.trim() || undefined ? formData.experience.trim() || undefined
: undefined, : undefined,
isOver16: performer ? formData.isOver16 : false, isOver16: performer ? formData.isOver16 : false,
birthdate: formData.birthdate.trim(),
postcode: formData.postcode.trim(),
guests: performer guests: performer
? [] ? []
: formGuests.map((g) => ({ : formGuests.map((g) => ({
@@ -192,6 +198,8 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) {
lastName: g.lastName.trim(), lastName: g.lastName.trim(),
email: g.email.trim() || undefined, email: g.email.trim() || undefined,
phone: g.phone.trim() || undefined, phone: g.phone.trim() || undefined,
birthdate: g.birthdate.trim(),
postcode: g.postcode.trim(),
})), })),
extraQuestions: formData.extraQuestions.trim() || undefined, extraQuestions: formData.extraQuestions.trim() || undefined,
giftAmount, giftAmount,
@@ -393,7 +401,14 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) {
if (formGuests.length >= 9) return; if (formGuests.length >= 9) return;
setFormGuests((prev) => [ setFormGuests((prev) => [
...prev, ...prev,
{ firstName: "", lastName: "", email: "", phone: "" }, {
firstName: "",
lastName: "",
email: "",
phone: "",
birthdate: "",
postcode: "",
},
]); ]);
}} }}
onRemove={(idx) => onRemove={(idx) =>

View File

@@ -26,7 +26,7 @@ function PrivacyPage() {
<p> <p>
We verzamelen alleen de gegevens die je zelf invoert bij de We verzamelen alleen de gegevens die je zelf invoert bij de
registratie: voornaam, achternaam, e-mailadres, telefoonnummer, registratie: voornaam, achternaam, e-mailadres, telefoonnummer,
kunstvorm en ervaring. geboortedatum, postcode, kunstvorm en ervaring.
</p> </p>
</section> </section>
@@ -39,6 +39,12 @@ function PrivacyPage() {
Mic Night, om het programma samen te stellen en om je te Mic Night, om het programma samen te stellen en om je te
informeren over aanvullende details over het evenement. informeren over aanvullende details over het evenement.
</p> </p>
<p className="mt-3">
<strong className="text-white">Geboortedatum en postcode</strong>{" "}
worden gevraagd ter naleving van de rapportageverplichtingen van{" "}
<strong className="text-white">EJV</strong> en{" "}
<strong className="text-white">de Vlaamse Overheid</strong>.
</p>
</section> </section>
<section> <section>

View File

@@ -262,6 +262,8 @@ const guestSchema = z.object({
lastName: z.string().min(1), lastName: z.string().min(1),
email: z.string().email().optional().or(z.literal("")), email: z.string().email().optional().or(z.literal("")),
phone: z.string().optional(), phone: z.string().optional(),
birthdate: z.string().min(1),
postcode: z.string().min(1),
}); });
const coreRegistrationFields = { const coreRegistrationFields = {
@@ -269,6 +271,8 @@ const coreRegistrationFields = {
lastName: z.string().min(1), lastName: z.string().min(1),
email: z.string().email(), email: z.string().email(),
phone: z.string().optional(), phone: z.string().optional(),
birthdate: z.string().min(1),
postcode: z.string().min(1),
registrationType: registrationTypeSchema.default("watcher"), registrationType: registrationTypeSchema.default("watcher"),
artForm: z.string().optional(), artForm: z.string().optional(),
experience: z.string().optional(), experience: z.string().optional(),
@@ -366,6 +370,8 @@ export const appRouter = {
lastName: input.lastName, lastName: input.lastName,
email: input.email, email: input.email,
phone: input.phone || null, phone: input.phone || null,
birthdate: input.birthdate,
postcode: input.postcode,
registrationType: input.registrationType, registrationType: input.registrationType,
artForm: isPerformer ? input.artForm || null : null, artForm: isPerformer ? input.artForm || null : null,
experience: isPerformer ? input.experience || null : null, experience: isPerformer ? input.experience || null : null,
@@ -463,6 +469,8 @@ export const appRouter = {
lastName: input.lastName, lastName: input.lastName,
email: input.email, email: input.email,
phone: input.phone || null, phone: input.phone || null,
birthdate: input.birthdate,
postcode: input.postcode,
registrationType: input.registrationType, registrationType: input.registrationType,
artForm: isPerformer ? input.artForm || null : null, artForm: isPerformer ? input.artForm || null : null,
experience: isPerformer ? input.experience || null : null, experience: isPerformer ? input.experience || null : null,
@@ -662,6 +670,8 @@ export const appRouter = {
"Last Name", "Last Name",
"Email", "Email",
"Phone", "Phone",
"Birthdate",
"Postcode",
"Type", "Type",
"Art Form", "Art Form",
"Experience", "Experience",
@@ -673,38 +683,56 @@ export const appRouter = {
"Guest 1 Last Name", "Guest 1 Last Name",
"Guest 1 Email", "Guest 1 Email",
"Guest 1 Phone", "Guest 1 Phone",
"Guest 1 Birthdate",
"Guest 1 Postcode",
"Guest 2 First Name", "Guest 2 First Name",
"Guest 2 Last Name", "Guest 2 Last Name",
"Guest 2 Email", "Guest 2 Email",
"Guest 2 Phone", "Guest 2 Phone",
"Guest 2 Birthdate",
"Guest 2 Postcode",
"Guest 3 First Name", "Guest 3 First Name",
"Guest 3 Last Name", "Guest 3 Last Name",
"Guest 3 Email", "Guest 3 Email",
"Guest 3 Phone", "Guest 3 Phone",
"Guest 3 Birthdate",
"Guest 3 Postcode",
"Guest 4 First Name", "Guest 4 First Name",
"Guest 4 Last Name", "Guest 4 Last Name",
"Guest 4 Email", "Guest 4 Email",
"Guest 4 Phone", "Guest 4 Phone",
"Guest 4 Birthdate",
"Guest 4 Postcode",
"Guest 5 First Name", "Guest 5 First Name",
"Guest 5 Last Name", "Guest 5 Last Name",
"Guest 5 Email", "Guest 5 Email",
"Guest 5 Phone", "Guest 5 Phone",
"Guest 5 Birthdate",
"Guest 5 Postcode",
"Guest 6 First Name", "Guest 6 First Name",
"Guest 6 Last Name", "Guest 6 Last Name",
"Guest 6 Email", "Guest 6 Email",
"Guest 6 Phone", "Guest 6 Phone",
"Guest 6 Birthdate",
"Guest 6 Postcode",
"Guest 7 First Name", "Guest 7 First Name",
"Guest 7 Last Name", "Guest 7 Last Name",
"Guest 7 Email", "Guest 7 Email",
"Guest 7 Phone", "Guest 7 Phone",
"Guest 7 Birthdate",
"Guest 7 Postcode",
"Guest 8 First Name", "Guest 8 First Name",
"Guest 8 Last Name", "Guest 8 Last Name",
"Guest 8 Email", "Guest 8 Email",
"Guest 8 Phone", "Guest 8 Phone",
"Guest 8 Birthdate",
"Guest 8 Postcode",
"Guest 9 First Name", "Guest 9 First Name",
"Guest 9 Last Name", "Guest 9 Last Name",
"Guest 9 Email", "Guest 9 Email",
"Guest 9 Phone", "Guest 9 Phone",
"Guest 9 Birthdate",
"Guest 9 Postcode",
"Payment Status", "Payment Status",
"Paid At", "Paid At",
"Extra Questions", "Extra Questions",
@@ -717,7 +745,7 @@ export const appRouter = {
const rows = data.map((r) => { const rows = data.map((r) => {
const guests = parseGuestsJson(r.guests); const guests = parseGuestsJson(r.guests);
// Build guest columns (up to 9 guests, 4 fields each) // Build guest columns (up to 9 guests, 6 fields each)
const guestCols: string[] = []; const guestCols: string[] = [];
for (let i = 0; i < MAX_GUESTS; i++) { for (let i = 0; i < MAX_GUESTS; i++) {
const g = guests[i]; const g = guests[i];
@@ -725,6 +753,8 @@ export const appRouter = {
guestCols.push(g?.lastName ?? ""); guestCols.push(g?.lastName ?? "");
guestCols.push(g?.email ?? ""); guestCols.push(g?.email ?? "");
guestCols.push(g?.phone ?? ""); guestCols.push(g?.phone ?? "");
guestCols.push((g as { birthdate?: string })?.birthdate ?? "");
guestCols.push((g as { postcode?: string })?.postcode ?? "");
} }
return [ return [
@@ -733,6 +763,8 @@ export const appRouter = {
r.lastName, r.lastName,
r.email, r.email,
r.phone || "", r.phone || "",
r.birthdate || "",
r.postcode || "",
r.registrationType ?? "", r.registrationType ?? "",
r.artForm || "", r.artForm || "",
r.experience || "", r.experience || "",

View File

@@ -0,0 +1,101 @@
ALTER TABLE `registration` RENAME COLUMN "wants_to_perform" TO "registration_type";--> statement-breakpoint
CREATE TABLE `drinkkaart` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`balance` integer DEFAULT 0 NOT NULL,
`version` integer DEFAULT 0 NOT NULL,
`qr_secret` text NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `drinkkaart_user_id_unique` ON `drinkkaart` (`user_id`);--> statement-breakpoint
CREATE TABLE `drinkkaart_topup` (
`id` text PRIMARY KEY NOT NULL,
`drinkkaart_id` text NOT NULL,
`user_id` text NOT NULL,
`amount_cents` integer NOT NULL,
`balance_before` integer NOT NULL,
`balance_after` integer NOT NULL,
`type` text NOT NULL,
`mollie_payment_id` text,
`admin_id` text,
`reason` text,
`paid_at` integer NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `drinkkaart_topup_mollie_payment_id_unique` ON `drinkkaart_topup` (`mollie_payment_id`);--> statement-breakpoint
CREATE TABLE `drinkkaart_transaction` (
`id` text PRIMARY KEY NOT NULL,
`drinkkaart_id` text NOT NULL,
`user_id` text NOT NULL,
`admin_id` text NOT NULL,
`amount_cents` integer NOT NULL,
`balance_before` integer NOT NULL,
`balance_after` integer NOT NULL,
`type` text NOT NULL,
`reversed_by` text,
`reverses` text,
`note` text,
`created_at` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `reminder` (
`id` text PRIMARY KEY NOT NULL,
`email` text NOT NULL,
`sent_24h_at` integer,
`sent_at` integer,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE INDEX `reminder_email_idx` ON `reminder` (`email`);--> statement-breakpoint
DROP INDEX "admin_request_user_id_unique";--> statement-breakpoint
DROP INDEX "admin_request_userId_idx";--> statement-breakpoint
DROP INDEX "admin_request_status_idx";--> statement-breakpoint
DROP INDEX "account_userId_idx";--> statement-breakpoint
DROP INDEX "session_token_unique";--> statement-breakpoint
DROP INDEX "session_userId_idx";--> statement-breakpoint
DROP INDEX "user_email_unique";--> statement-breakpoint
DROP INDEX "verification_identifier_idx";--> statement-breakpoint
DROP INDEX "drinkkaart_user_id_unique";--> statement-breakpoint
DROP INDEX "drinkkaart_topup_mollie_payment_id_unique";--> statement-breakpoint
DROP INDEX "registration_management_token_unique";--> statement-breakpoint
DROP INDEX "registration_email_idx";--> statement-breakpoint
DROP INDEX "registration_registrationType_idx";--> statement-breakpoint
DROP INDEX "registration_artForm_idx";--> statement-breakpoint
DROP INDEX "registration_createdAt_idx";--> statement-breakpoint
DROP INDEX "registration_managementToken_idx";--> statement-breakpoint
DROP INDEX "registration_paymentStatus_idx";--> statement-breakpoint
DROP INDEX "registration_giftAmount_idx";--> statement-breakpoint
DROP INDEX "registration_molliePaymentId_idx";--> statement-breakpoint
DROP INDEX "reminder_email_idx";--> statement-breakpoint
ALTER TABLE `registration` ALTER COLUMN "registration_type" TO "registration_type" text NOT NULL DEFAULT 'watcher';--> statement-breakpoint
CREATE UNIQUE INDEX `admin_request_user_id_unique` ON `admin_request` (`user_id`);--> statement-breakpoint
CREATE INDEX `admin_request_userId_idx` ON `admin_request` (`user_id`);--> statement-breakpoint
CREATE INDEX `admin_request_status_idx` ON `admin_request` (`status`);--> statement-breakpoint
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);--> statement-breakpoint
CREATE UNIQUE INDEX `registration_management_token_unique` ON `registration` (`management_token`);--> statement-breakpoint
CREATE INDEX `registration_email_idx` ON `registration` (`email`);--> statement-breakpoint
CREATE INDEX `registration_registrationType_idx` ON `registration` (`registration_type`);--> statement-breakpoint
CREATE INDEX `registration_artForm_idx` ON `registration` (`art_form`);--> statement-breakpoint
CREATE INDEX `registration_createdAt_idx` ON `registration` (`created_at`);--> statement-breakpoint
CREATE INDEX `registration_managementToken_idx` ON `registration` (`management_token`);--> statement-breakpoint
CREATE INDEX `registration_paymentStatus_idx` ON `registration` (`payment_status`);--> statement-breakpoint
CREATE INDEX `registration_giftAmount_idx` ON `registration` (`gift_amount`);--> statement-breakpoint
CREATE INDEX `registration_molliePaymentId_idx` ON `registration` (`mollie_payment_id`);--> statement-breakpoint
ALTER TABLE `registration` ADD `is_over_16` integer DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE `registration` ADD `drink_card_value` integer DEFAULT 0;--> statement-breakpoint
ALTER TABLE `registration` ADD `guests` text;--> statement-breakpoint
ALTER TABLE `registration` ADD `birthdate` text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE `registration` ADD `postcode` text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE `registration` ADD `payment_status` text DEFAULT 'pending' NOT NULL;--> statement-breakpoint
ALTER TABLE `registration` ADD `payment_amount` integer DEFAULT 0;--> statement-breakpoint
ALTER TABLE `registration` ADD `gift_amount` integer DEFAULT 0;--> statement-breakpoint
ALTER TABLE `registration` ADD `mollie_payment_id` text;--> statement-breakpoint
ALTER TABLE `registration` ADD `paid_at` integer;--> statement-breakpoint
ALTER TABLE `registration` ADD `drinkkaart_credited_at` integer;--> statement-breakpoint
ALTER TABLE `registration` ADD `payment_reminder_sent_at` integer;

View File

@@ -0,0 +1,971 @@
{
"version": "6",
"dialect": "sqlite",
"id": "7994e607-3835-43a5-9251-fca48f0aa19a",
"prevId": "d9a4f07e-e6ae-45d0-be82-c919ae7fbe09",
"tables": {
"admin_request": {
"name": "admin_request",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"requested_at": {
"name": "requested_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"reviewed_at": {
"name": "reviewed_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reviewed_by": {
"name": "reviewed_by",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"admin_request_user_id_unique": {
"name": "admin_request_user_id_unique",
"columns": ["user_id"],
"isUnique": true
},
"admin_request_userId_idx": {
"name": "admin_request_userId_idx",
"columns": ["user_id"],
"isUnique": false
},
"admin_request_status_idx": {
"name": "admin_request_status_idx",
"columns": ["status"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"account": {
"name": "account",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"account_userId_idx": {
"name": "account_userId_idx",
"columns": ["user_id"],
"isUnique": false
}
},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"session_token_unique": {
"name": "session_token_unique",
"columns": ["token"],
"isUnique": true
},
"session_userId_idx": {
"name": "session_userId_idx",
"columns": ["user_id"],
"isUnique": false
}
},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email_verified": {
"name": "email_verified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
"user_email_unique": {
"name": "user_email_unique",
"columns": ["email"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"verification": {
"name": "verification",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
"verification_identifier_idx": {
"name": "verification_identifier_idx",
"columns": ["identifier"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"drinkkaart": {
"name": "drinkkaart",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"balance": {
"name": "balance",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"qr_secret": {
"name": "qr_secret",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"drinkkaart_user_id_unique": {
"name": "drinkkaart_user_id_unique",
"columns": ["user_id"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"drinkkaart_topup": {
"name": "drinkkaart_topup",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"drinkkaart_id": {
"name": "drinkkaart_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"amount_cents": {
"name": "amount_cents",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"balance_before": {
"name": "balance_before",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"balance_after": {
"name": "balance_after",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mollie_payment_id": {
"name": "mollie_payment_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"admin_id": {
"name": "admin_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reason": {
"name": "reason",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"paid_at": {
"name": "paid_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"drinkkaart_topup_mollie_payment_id_unique": {
"name": "drinkkaart_topup_mollie_payment_id_unique",
"columns": ["mollie_payment_id"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"drinkkaart_transaction": {
"name": "drinkkaart_transaction",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"drinkkaart_id": {
"name": "drinkkaart_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"admin_id": {
"name": "admin_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"amount_cents": {
"name": "amount_cents",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"balance_before": {
"name": "balance_before",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"balance_after": {
"name": "balance_after",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"reversed_by": {
"name": "reversed_by",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reverses": {
"name": "reverses",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"registration": {
"name": "registration",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"first_name": {
"name": "first_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"last_name": {
"name": "last_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"phone": {
"name": "phone",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"registration_type": {
"name": "registration_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'watcher'"
},
"art_form": {
"name": "art_form",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"experience": {
"name": "experience",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_over_16": {
"name": "is_over_16",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"drink_card_value": {
"name": "drink_card_value",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"guests": {
"name": "guests",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"birthdate": {
"name": "birthdate",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"postcode": {
"name": "postcode",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"extra_questions": {
"name": "extra_questions",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"management_token": {
"name": "management_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cancelled_at": {
"name": "cancelled_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_status": {
"name": "payment_status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"payment_amount": {
"name": "payment_amount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"gift_amount": {
"name": "gift_amount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"mollie_payment_id": {
"name": "mollie_payment_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"paid_at": {
"name": "paid_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"drinkkaart_credited_at": {
"name": "drinkkaart_credited_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_reminder_sent_at": {
"name": "payment_reminder_sent_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
"registration_management_token_unique": {
"name": "registration_management_token_unique",
"columns": ["management_token"],
"isUnique": true
},
"registration_email_idx": {
"name": "registration_email_idx",
"columns": ["email"],
"isUnique": false
},
"registration_registrationType_idx": {
"name": "registration_registrationType_idx",
"columns": ["registration_type"],
"isUnique": false
},
"registration_artForm_idx": {
"name": "registration_artForm_idx",
"columns": ["art_form"],
"isUnique": false
},
"registration_createdAt_idx": {
"name": "registration_createdAt_idx",
"columns": ["created_at"],
"isUnique": false
},
"registration_managementToken_idx": {
"name": "registration_managementToken_idx",
"columns": ["management_token"],
"isUnique": false
},
"registration_paymentStatus_idx": {
"name": "registration_paymentStatus_idx",
"columns": ["payment_status"],
"isUnique": false
},
"registration_giftAmount_idx": {
"name": "registration_giftAmount_idx",
"columns": ["gift_amount"],
"isUnique": false
},
"registration_molliePaymentId_idx": {
"name": "registration_molliePaymentId_idx",
"columns": ["mollie_payment_id"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"reminder": {
"name": "reminder",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sent_24h_at": {
"name": "sent_24h_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sent_at": {
"name": "sent_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
"reminder_email_idx": {
"name": "reminder_email_idx",
"columns": ["email"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {
"\"registration\".\"wants_to_perform\"": "\"registration\".\"registration_type\""
}
},
"internal": {
"indexes": {}
}
}

View File

@@ -57,6 +57,13 @@
"when": 1772931300000, "when": 1772931300000,
"tag": "0007_reminder", "tag": "0007_reminder",
"breakpoints": true "breakpoints": true
},
{
"idx": 8,
"version": "6",
"when": 1773910007494,
"tag": "0008_melodic_marrow",
"breakpoints": true
} }
] ]
} }

View File

@@ -19,9 +19,11 @@ export const registration = sqliteTable(
.default(false), .default(false),
// Watcher-specific fields // Watcher-specific fields
drinkCardValue: integer("drink_card_value").default(0), drinkCardValue: integer("drink_card_value").default(0),
// Guests: JSON array of {firstName, lastName, email?, phone?} objects // Guests: JSON array of {firstName, lastName, email?, phone?, birthdate, postcode} objects
guests: text("guests"), guests: text("guests"),
// Shared // Shared
birthdate: text("birthdate").notNull().default(""),
postcode: text("postcode").notNull().default(""),
extraQuestions: text("extra_questions"), extraQuestions: text("extra_questions"),
managementToken: text("management_token").unique(), managementToken: text("management_token").unique(),
cancelledAt: integer("cancelled_at", { mode: "timestamp_ms" }), cancelledAt: integer("cancelled_at", { mode: "timestamp_ms" }),