feat:gifts
This commit is contained in:
@@ -75,6 +75,7 @@ function AdminPage() {
|
||||
| "type"
|
||||
| "details"
|
||||
| "gasten"
|
||||
| "gift"
|
||||
| "betaling"
|
||||
| "datum";
|
||||
type SortDir = "asc" | "desc";
|
||||
@@ -190,6 +191,7 @@ function AdminPage() {
|
||||
const totalDrinkCardValue = totalRegistrations
|
||||
.filter((r) => r.registrationType === "watcher")
|
||||
.reduce((sum, r) => sum + (r.drinkCardValue ?? 0), 0);
|
||||
const totalGiftRevenue = (stats?.totalGiftRevenue as number | undefined) ?? 0;
|
||||
|
||||
const sortedRegistrations = useMemo(() => {
|
||||
const sorted = [...registrations].sort((a, b) => {
|
||||
@@ -232,6 +234,10 @@ function AdminPage() {
|
||||
valB = countGuests(b.guests as string | null);
|
||||
break;
|
||||
}
|
||||
case "gift":
|
||||
valA = a.giftAmount ?? 0;
|
||||
valB = b.giftAmount ?? 0;
|
||||
break;
|
||||
case "betaling":
|
||||
valA = a.paymentStatus ?? "pending";
|
||||
valB = b.paymentStatus ?? "pending";
|
||||
@@ -261,6 +267,12 @@ function AdminPage() {
|
||||
const thClass =
|
||||
"px-4 py-3 text-left font-medium text-xs text-white/60 uppercase tracking-wider cursor-pointer select-none hover:text-white/90 whitespace-nowrap";
|
||||
|
||||
const formatCents = (cents: number | null | undefined) => {
|
||||
if (!cents || cents <= 0) return "-";
|
||||
const euros = cents / 100;
|
||||
return `€${euros.toFixed(euros % 1 === 0 ? 0 : 2)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
{/* Header */}
|
||||
@@ -435,6 +447,22 @@ function AdminPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-pink-400/20 bg-pink-400/5">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-pink-300/70">
|
||||
Vrijwillige Gifts
|
||||
</CardDescription>
|
||||
<CardTitle className="font-['Intro',sans-serif] text-4xl text-pink-300">
|
||||
€{Math.round(totalGiftRevenue / 100)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className="text-sm text-white/40">
|
||||
Totale gift opbrengst
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
@@ -591,6 +619,9 @@ function AdminPage() {
|
||||
>
|
||||
Gasten <SortIcon col="gasten" />
|
||||
</th>
|
||||
<th className={thClass} onClick={() => handleSort("gift")}>
|
||||
Gift <SortIcon col="gift" />
|
||||
</th>
|
||||
<th
|
||||
className={thClass}
|
||||
onClick={() => handleSort("betaling")}
|
||||
@@ -609,7 +640,7 @@ function AdminPage() {
|
||||
{registrationsQuery.isLoading ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={9}
|
||||
colSpan={10}
|
||||
className="px-4 py-8 text-center text-white/60"
|
||||
>
|
||||
Laden...
|
||||
@@ -618,7 +649,7 @@ function AdminPage() {
|
||||
) : sortedRegistrations.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={9}
|
||||
colSpan={10}
|
||||
className="px-4 py-8 text-center text-white/60"
|
||||
>
|
||||
Geen registraties gevonden
|
||||
@@ -686,6 +717,9 @@ function AdminPage() {
|
||||
? `${guestCount} gast${guestCount === 1 ? "" : "en"}`
|
||||
: "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-pink-300/70 text-sm">
|
||||
{formatCents(reg.giftAmount)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{isPerformer ? (
|
||||
<span className="text-sm text-white/30">-</span>
|
||||
|
||||
@@ -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<GuestEntry[]>(
|
||||
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) {
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
isPaid={isPaid}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -426,6 +431,18 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gift selector */}
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
<h3 className="mb-4 font-['Intro',sans-serif] text-white text-xl">
|
||||
Vrijwillige Gift
|
||||
</h3>
|
||||
<GiftSelector
|
||||
value={giftAmount}
|
||||
onChange={setGiftAmount}
|
||||
disabled={isPaid && (initialData.giftAmount ?? 0) > 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
@@ -568,8 +585,8 @@ function ManageRegistrationPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payment status */}
|
||||
{!isPerformer && (
|
||||
{/* Payment status - shown for everyone with pending/extra payment or gift */}
|
||||
{(data.paymentStatus !== "paid" || (data.giftAmount ?? 0) > 0) && (
|
||||
<div className="mb-6">
|
||||
{data.paymentStatus === "paid" ? (
|
||||
<PaidBadge />
|
||||
@@ -581,6 +598,15 @@ function ManageRegistrationPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gift display */}
|
||||
{(data.giftAmount ?? 0) > 0 && (
|
||||
<div className="mb-6">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-pink-400/40 bg-pink-400/10 px-4 py-1.5 font-semibold text-pink-300 text-sm">
|
||||
<span>Vrijwillige gift: €{(data.giftAmount ?? 0) / 100}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Registration details */}
|
||||
<div className="space-y-6 rounded-lg border border-white/20 bg-white/5 p-6 md:p-8">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
@@ -656,20 +682,23 @@ function ManageRegistrationPage() {
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-8 flex flex-wrap items-center gap-4">
|
||||
{!isPerformer &&
|
||||
(data.paymentStatus === "pending" ||
|
||||
data.paymentStatus === "extra_payment_pending") && (
|
||||
{/* Show pay button if there's something to pay (for watchers: drink card, for performers: gift) */}
|
||||
{(data.paymentStatus === "pending" ||
|
||||
data.paymentStatus === "extra_payment_pending") &&
|
||||
(!isPerformer || (data.giftAmount ?? 0) > 0) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => checkoutMutation.mutate({ token })}
|
||||
disabled={checkoutMutation.isPending}
|
||||
className={`px-8 py-3 font-['Intro',sans-serif] text-lg transition-all hover:scale-105 disabled:opacity-50 ${data.paymentStatus === "extra_payment_pending" ? "bg-orange-400 text-white hover:bg-orange-300" : "bg-teal-400 text-[#214e51] hover:bg-teal-300"}`}
|
||||
className={`px-8 py-3 font-['Intro',sans-serif] text-lg transition-all hover:scale-105 disabled:opacity-50 ${data.paymentStatus === "extra_payment_pending" ? "bg-orange-400 text-white hover:bg-orange-300" : isPerformer ? "bg-pink-400 text-white hover:bg-pink-300" : "bg-teal-400 text-[#214e51] hover:bg-teal-300"}`}
|
||||
>
|
||||
{checkoutMutation.isPending
|
||||
? "Laden..."
|
||||
: data.paymentStatus === "extra_payment_pending"
|
||||
? `Extra betalen (€${((data.paymentAmount ?? 0) / 100).toFixed(0)})`
|
||||
: "Nu betalen"}
|
||||
: isPerformer
|
||||
? `Gift betalen (€${((data.giftAmount ?? 0) / 100).toFixed(0)})`
|
||||
: "Nu betalen"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user