diff --git a/apps/web/src/components/registration/GiftSelector.tsx b/apps/web/src/components/registration/GiftSelector.tsx new file mode 100644 index 0000000..61be07d --- /dev/null +++ b/apps/web/src/components/registration/GiftSelector.tsx @@ -0,0 +1,164 @@ +import { useState } from "react"; + +interface GiftSelectorProps { + value: number; + onChange: (cents: number) => void; + id?: string; + disabled?: boolean; +} + +const PRESET_AMOUNTS = [ + { label: "€5", cents: 500 }, + { label: "€10", cents: 1000 }, + { label: "€25", cents: 2500 }, + { label: "€50", cents: 5000 }, +]; + +export function GiftSelector({ + value, + onChange, + id, + disabled, +}: GiftSelectorProps) { + const [customInput, setCustomInput] = useState(""); + const [isCustom, setIsCustom] = useState(false); + + // Check if current value matches a preset + const isPresetSelected = PRESET_AMOUNTS.some((p) => p.cents === value); + const showCustomInput = isCustom || (!isPresetSelected && value > 0); + + const handlePresetClick = (cents: number) => { + onChange(cents); + setIsCustom(false); + setCustomInput(""); + }; + + const handleCustomInputChange = (e: React.ChangeEvent) => { + const input = e.target.value.replace(/[^0-9,]/g, ""); + setCustomInput(input); + + // Parse euros to cents + if (input) { + const normalized = input.replace(",", "."); + const euros = Number.parseFloat(normalized); + if (!Number.isNaN(euros) && euros >= 0) { + onChange(Math.round(euros * 100)); + } + } else { + onChange(0); + } + }; + + const handleCustomClick = () => { + setIsCustom(true); + if (value > 0 && !isPresetSelected) { + // Keep existing custom value + setCustomInput((value / 100).toFixed(2).replace(".", ",")); + } else { + setCustomInput(""); + onChange(0); + } + }; + + const handleSkip = () => { + onChange(0); + setIsCustom(false); + setCustomInput(""); + }; + + // When disabled, show read-only view + if (disabled) { + return ( +
+

+ Deelname is gratis, maar een vrijwillige gift wordt zeer gewaardeerd. +

+ {value > 0 ? ( +
+

+ €{(value / 100).toFixed(2).replace(".", ",")} +

+

+ Gift is verwerkt en kan niet meer worden aangepast +

+
+ ) : ( +

Geen gift toegevoegd

+ )} +
+ ); + } + + return ( +
+

+ Deelname is gratis, maar een vrijwillige gift wordt zeer gewaardeerd. +

+ + {/* Preset amounts */} +
+ {PRESET_AMOUNTS.map((preset) => ( + + ))} + +
+ + {/* Custom amount input */} + {showCustomInput && ( +
+ + +
+ )} + + {/* Skip option */} + {value > 0 && ( + + )} + + {/* Selected amount display */} + {value > 0 && ( +

+ Geselecteerd:{" "} + + €{(value / 100).toFixed(2).replace(".", ",")} + +

+ )} +
+ ); +} diff --git a/apps/web/src/components/registration/GuestList.tsx b/apps/web/src/components/registration/GuestList.tsx index f06d8d7..1dd0a32 100644 --- a/apps/web/src/components/registration/GuestList.tsx +++ b/apps/web/src/components/registration/GuestList.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { type GuestEntry, type GuestErrors, @@ -13,6 +14,8 @@ interface Props { onRemove: (index: number) => void; /** Optional suffix rendered after the guest count header (e.g. price warning) */ headerNote?: React.ReactNode; + /** Whether payment has already been made (disables removal without warning) */ + isPaid?: boolean; } export function GuestList({ @@ -22,7 +25,29 @@ export function GuestList({ onAdd, onRemove, headerNote, + isPaid, }: Props) { + const [removingIndex, setRemovingIndex] = useState(null); + + const handleRemoveClick = (index: number) => { + if (isPaid) { + setRemovingIndex(index); + } else { + onRemove(index); + } + }; + + const confirmRemove = () => { + if (removingIndex !== null) { + onRemove(removingIndex); + setRemovingIndex(null); + } + }; + + const cancelRemove = () => { + setRemovingIndex(null); + }; + return (
@@ -60,6 +85,35 @@ export function GuestList({ )}
+ {/* Removal confirmation modal for paid registrations */} + {removingIndex !== null && ( +
+

+ Let op: gast verwijderen +

+

+ Als je al betaald hebt voor deze gast krijg je geen terugbetaling + bij het verwijderen. +

+
+ + +
+
+ )} + {guests.length === 0 && (

Kom je met iemand mee? Voeg je medebezoekers toe. Elke extra persoon @@ -82,7 +136,7 @@ export function GuestList({

+ {/* Gift selector */} +
+

+ Vrijwillige Gift +

+ +
+
{/* Filters */} @@ -591,6 +619,9 @@ function AdminPage() { > Gasten + handleSort("gift")}> + Gift + handleSort("betaling")} @@ -609,7 +640,7 @@ function AdminPage() { {registrationsQuery.isLoading ? ( Laden... @@ -618,7 +649,7 @@ function AdminPage() { ) : sortedRegistrations.length === 0 ? ( Geen registraties gevonden @@ -686,6 +717,9 @@ function AdminPage() { ? `${guestCount} gast${guestCount === 1 ? "" : "en"}` : "-"} + + {formatCents(reg.giftAmount)} + {isPerformer ? ( - diff --git a/apps/web/src/routes/manage.$token.tsx b/apps/web/src/routes/manage.$token.tsx index dca62f8..015919a 100644 --- a/apps/web/src/routes/manage.$token.tsx +++ b/apps/web/src/routes/manage.$token.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createFileRoute, Link, useParams } from "@tanstack/react-router"; import { useState } from "react"; import { toast } from "sonner"; +import { GiftSelector } from "@/components/registration/GiftSelector"; import { GuestList } from "@/components/registration/GuestList"; import { calculateDrinkCard, @@ -123,6 +124,7 @@ interface EditFormProps { extraQuestions: string | null; guests: string | null; paymentStatus: string | null; + giftAmount: number | null; }; onCancel: () => void; onSaved: () => void; @@ -147,6 +149,7 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) { const [formGuests, setFormGuests] = useState( parseGuests(initialData.guests), ); + const [giftAmount, setGiftAmount] = useState(initialData.giftAmount ?? 0); const updateMutation = useMutation({ ...orpc.updateRegistration.mutationOptions(), @@ -191,6 +194,7 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) { phone: g.phone.trim() || undefined, })), extraQuestions: formData.extraQuestions.trim() || undefined, + giftAmount, }); } @@ -407,6 +411,7 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) { )}

} + isPaid={isPaid} /> )} @@ -426,6 +431,18 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) { /> + {/* Gift selector */} +
+

+ Vrijwillige Gift +

+ 0} + /> +
+
)}