|
|
|
|
@@ -392,20 +392,23 @@ function AdminPage() {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<header className="border-white/10 border-b bg-[#214e51]/95 px-8 py-6">
|
|
|
|
|
<div className="mx-auto flex max-w-7xl items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Link to="/" className="text-white hover:opacity-80">
|
|
|
|
|
<header className="border-white/10 border-b bg-[#214e51]/95 px-4 py-4 sm:px-8 sm:py-6">
|
|
|
|
|
<div className="mx-auto flex max-w-7xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
|
|
|
|
|
<Link
|
|
|
|
|
to="/"
|
|
|
|
|
className="text-sm text-white hover:opacity-80 sm:text-base"
|
|
|
|
|
>
|
|
|
|
|
← Terug naar website
|
|
|
|
|
</Link>
|
|
|
|
|
<h1 className="font-['Intro',sans-serif] text-3xl text-white">
|
|
|
|
|
<h1 className="font-['Intro',sans-serif] text-2xl text-white sm:text-3xl">
|
|
|
|
|
Admin Dashboard
|
|
|
|
|
</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
|
|
|
|
<Link
|
|
|
|
|
to="/admin/drinkkaart"
|
|
|
|
|
className="inline-flex items-center rounded-lg border border-white/30 px-4 py-2 text-sm text-white/80 transition-colors hover:bg-white/10 hover:text-white"
|
|
|
|
|
className="inline-flex items-center justify-center rounded-lg border border-white/30 px-4 py-2 text-sm text-white/80 transition-colors hover:bg-white/10 hover:text-white"
|
|
|
|
|
>
|
|
|
|
|
Drinkkaart beheer
|
|
|
|
|
</Link>
|
|
|
|
|
@@ -422,30 +425,30 @@ function AdminPage() {
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{/* Main Content */}
|
|
|
|
|
<main className="mx-auto max-w-7xl p-8">
|
|
|
|
|
<main className="mx-auto max-w-7xl px-4 py-4 sm:px-6 sm:py-6 lg:px-8 lg:py-8">
|
|
|
|
|
{/* Pending Admin Requests */}
|
|
|
|
|
{pendingRequests.length > 0 && (
|
|
|
|
|
<Card className="mb-6 border-yellow-500/30 bg-yellow-500/10">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-xl text-yellow-200">
|
|
|
|
|
<Card className="mb-4 border-yellow-500/30 bg-yellow-500/10 sm:mb-6">
|
|
|
|
|
<CardHeader className="px-4 py-4 sm:px-6">
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-lg text-yellow-200 sm:text-xl">
|
|
|
|
|
Openstaande Admin Aanvragen ({pendingRequests.length})
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription className="text-yellow-200/60">
|
|
|
|
|
<CardDescription className="text-sm text-yellow-200/60">
|
|
|
|
|
Gebruikers die admin toegang hebben aangevraagd
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<CardContent className="px-4 pb-4 sm:px-6 sm:pb-6">
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{pendingRequests.map((request) => (
|
|
|
|
|
<div
|
|
|
|
|
key={request.id}
|
|
|
|
|
className="flex items-center justify-between rounded-lg border border-yellow-500/20 bg-yellow-500/5 p-4"
|
|
|
|
|
className="flex flex-col gap-3 rounded-lg border border-yellow-500/20 bg-yellow-500/5 p-3 sm:flex-row sm:items-center sm:justify-between sm:p-4"
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium text-white">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="truncate font-medium text-white">
|
|
|
|
|
{request.userName}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-white/60">
|
|
|
|
|
<p className="truncate text-sm text-white/60">
|
|
|
|
|
{request.userEmail}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-white/40 text-xs">
|
|
|
|
|
@@ -455,7 +458,7 @@ function AdminPage() {
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<div className="flex flex-col gap-2 sm:flex-row">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => handleApprove(request.id)}
|
|
|
|
|
disabled={approveRequestMutation.isPending}
|
|
|
|
|
@@ -463,7 +466,8 @@ function AdminPage() {
|
|
|
|
|
className="bg-green-600 text-white hover:bg-green-700"
|
|
|
|
|
>
|
|
|
|
|
<Check className="mr-1 h-4 w-4" />
|
|
|
|
|
Goedkeuren
|
|
|
|
|
<span className="hidden sm:inline">Goedkeuren</span>
|
|
|
|
|
<span className="sm:hidden">Goedkeuren</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => handleReject(request.id)}
|
|
|
|
|
@@ -484,13 +488,13 @@ function AdminPage() {
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Stats Cards */}
|
|
|
|
|
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-4">
|
|
|
|
|
<div className="mb-8 grid grid-cols-2 gap-3 sm:grid-cols-3 sm:gap-4 lg:grid-cols-5 lg:gap-6">
|
|
|
|
|
<Card className="border-white/10 bg-white/5">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardDescription className="text-white/60">
|
|
|
|
|
<CardDescription className="text-white/60 text-xs sm:text-sm">
|
|
|
|
|
Totaal inschrijvingen
|
|
|
|
|
</CardDescription>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-4xl text-white">
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-2xl text-white sm:text-3xl lg:text-4xl">
|
|
|
|
|
{stats?.total ?? 0}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
@@ -501,15 +505,15 @@ function AdminPage() {
|
|
|
|
|
|
|
|
|
|
<Card className="border-white/10 bg-white/5">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardDescription className="text-white/60">
|
|
|
|
|
<CardDescription className="text-white/60 text-xs sm:text-sm">
|
|
|
|
|
Vandaag ingeschreven
|
|
|
|
|
</CardDescription>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-4xl text-white">
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-2xl text-white sm:text-3xl lg:text-4xl">
|
|
|
|
|
{stats?.today ?? 0}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<span className="text-sm text-white/40">
|
|
|
|
|
<span className="text-white/40 text-xs sm:text-sm">
|
|
|
|
|
Nieuwe registraties vandaag
|
|
|
|
|
</span>
|
|
|
|
|
</CardContent>
|
|
|
|
|
@@ -517,21 +521,21 @@ function AdminPage() {
|
|
|
|
|
|
|
|
|
|
<Card className="border-amber-400/20 bg-amber-400/5">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardDescription className="text-amber-300/70">
|
|
|
|
|
<CardDescription className="text-amber-300/70 text-xs sm:text-sm">
|
|
|
|
|
Artiesten
|
|
|
|
|
</CardDescription>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-4xl text-amber-300">
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-2xl text-amber-300 sm:text-3xl lg:text-4xl">
|
|
|
|
|
{performerCount}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{stats?.byArtForm.slice(0, 4).map((item) => (
|
|
|
|
|
{stats?.byArtForm.slice(0, 2).map((item) => (
|
|
|
|
|
<div
|
|
|
|
|
key={item.artForm}
|
|
|
|
|
className="flex items-center justify-between text-xs"
|
|
|
|
|
>
|
|
|
|
|
<span className="text-amber-300/70">
|
|
|
|
|
<span className="truncate text-amber-300/70">
|
|
|
|
|
{item.artForm || "Onbekend"}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-amber-300">{item.count}</span>
|
|
|
|
|
@@ -543,10 +547,10 @@ function AdminPage() {
|
|
|
|
|
|
|
|
|
|
<Card className="border-teal-400/20 bg-teal-400/5">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardDescription className="text-teal-300/70">
|
|
|
|
|
<CardDescription className="text-teal-300/70 text-xs sm:text-sm">
|
|
|
|
|
Bezoekers
|
|
|
|
|
</CardDescription>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-4xl text-teal-300">
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-2xl text-teal-300 sm:text-3xl lg:text-4xl">
|
|
|
|
|
{watcherCount}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
@@ -564,7 +568,80 @@ function AdminPage() {
|
|
|
|
|
{totalWatcherAttendees}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="hidden sm:flex sm:items-center sm:justify-between">
|
|
|
|
|
<span className="text-teal-300/70">Drinkkaart</span>
|
|
|
|
|
<span className="text-teal-300">€{totalDrinkCardValue}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card className="border-pink-400/20 bg-pink-400/5">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardDescription className="text-pink-300/70 text-xs sm:text-sm">
|
|
|
|
|
Vrijwillige Gifts
|
|
|
|
|
</CardDescription>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-2xl text-white sm:text-3xl lg:text-4xl">
|
|
|
|
|
{stats?.today ?? 0}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<span className="text-white/40 text-xs sm:text-sm">
|
|
|
|
|
Nieuwe registraties vandaag
|
|
|
|
|
</span>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card className="border-amber-400/20 bg-amber-400/5">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardDescription className="text-amber-300/70">
|
|
|
|
|
Artiesten
|
|
|
|
|
</CardDescription>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-2xl text-amber-300 sm:text-3xl lg:text-4xl">
|
|
|
|
|
{performerCount}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{stats?.byArtForm.slice(0, 2).map((item) => (
|
|
|
|
|
<div
|
|
|
|
|
key={item.artForm}
|
|
|
|
|
className="flex items-center justify-between text-xs"
|
|
|
|
|
>
|
|
|
|
|
<span className="truncate text-amber-300/70">
|
|
|
|
|
{item.artForm || "Onbekend"}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-amber-300">{item.count}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card className="border-teal-400/20 bg-teal-400/5">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardDescription className="text-teal-300/70">
|
|
|
|
|
Bezoekers
|
|
|
|
|
</CardDescription>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-2xl text-teal-300 sm:text-3xl lg:text-4xl">
|
|
|
|
|
{watcherCount}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="space-y-1 text-xs">
|
|
|
|
|
{totalGuestCount > 0 && (
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-teal-300/70">Inclusief gasten</span>
|
|
|
|
|
<span className="text-teal-300">+{totalGuestCount}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-teal-300/70">Totaal aanwezig</span>
|
|
|
|
|
<span className="font-semibold text-teal-300">
|
|
|
|
|
{totalWatcherAttendees}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="hidden sm:flex sm:items-center sm:justify-between">
|
|
|
|
|
<span className="text-teal-300/70">Drinkkaart</span>
|
|
|
|
|
<span className="text-teal-300">€{totalDrinkCardValue}</span>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -577,12 +654,12 @@ function AdminPage() {
|
|
|
|
|
<CardDescription className="text-pink-300/70">
|
|
|
|
|
Vrijwillige Gifts
|
|
|
|
|
</CardDescription>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-4xl text-pink-300">
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-2xl text-pink-300 sm:text-3xl lg:text-4xl">
|
|
|
|
|
€{Math.round(totalGiftRevenue / 100)}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<span className="text-sm text-white/40">
|
|
|
|
|
<span className="text-white/40 text-xs sm:text-sm">
|
|
|
|
|
Totale gift opbrengst
|
|
|
|
|
</span>
|
|
|
|
|
</CardContent>
|
|
|
|
|
@@ -591,17 +668,17 @@ function AdminPage() {
|
|
|
|
|
|
|
|
|
|
{/* Filters */}
|
|
|
|
|
<Card className="mb-6 border-white/10 bg-white/5">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-white text-xl">
|
|
|
|
|
<CardHeader className="px-4 py-4 sm:px-6 sm:py-6">
|
|
|
|
|
<CardTitle className="font-['Intro',sans-serif] text-lg text-white sm:text-xl">
|
|
|
|
|
Filters
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-5">
|
|
|
|
|
<CardContent className="px-4 pb-4 sm:px-6 sm:pb-6">
|
|
|
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-5">
|
|
|
|
|
<div>
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="search"
|
|
|
|
|
className="mb-2 block text-sm text-white/60"
|
|
|
|
|
className="mb-1.5 block text-white/60 text-xs sm:mb-2 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
Zoeken
|
|
|
|
|
</label>
|
|
|
|
|
@@ -612,7 +689,7 @@ function AdminPage() {
|
|
|
|
|
placeholder="Naam of email..."
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
|
|
|
className="border-white/20 bg-white/10 pl-10 text-white placeholder:text-white/40"
|
|
|
|
|
className="border-white/20 bg-white/10 pl-10 text-sm text-white placeholder:text-white/40 sm:text-base"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -620,7 +697,7 @@ function AdminPage() {
|
|
|
|
|
<div>
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="typeFilter"
|
|
|
|
|
className="mb-2 block text-sm text-white/60"
|
|
|
|
|
className="mb-1.5 block text-white/60 text-xs sm:mb-2 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
Type
|
|
|
|
|
</label>
|
|
|
|
|
@@ -632,7 +709,7 @@ function AdminPage() {
|
|
|
|
|
e.target.value as "performer" | "watcher" | "",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
className="w-full rounded-md border border-white/20 bg-white/10 px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-white/20"
|
|
|
|
|
className="w-full rounded-md border border-white/20 bg-white/10 px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-white/20 sm:text-base"
|
|
|
|
|
>
|
|
|
|
|
<option value="" className="bg-[#214e51]">
|
|
|
|
|
Alle types
|
|
|
|
|
@@ -649,7 +726,7 @@ function AdminPage() {
|
|
|
|
|
<div>
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="artForm"
|
|
|
|
|
className="mb-2 block text-sm text-white/60"
|
|
|
|
|
className="mb-1.5 block text-white/60 text-xs sm:mb-2 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
Kunstvorm
|
|
|
|
|
</label>
|
|
|
|
|
@@ -658,14 +735,14 @@ function AdminPage() {
|
|
|
|
|
placeholder="Filter op kunstvorm..."
|
|
|
|
|
value={artForm}
|
|
|
|
|
onChange={(e) => setArtForm(e.target.value)}
|
|
|
|
|
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
|
|
|
|
|
className="border-white/20 bg-white/10 text-sm text-white placeholder:text-white/40 sm:text-base"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="fromDate"
|
|
|
|
|
className="mb-2 block text-sm text-white/60"
|
|
|
|
|
className="mb-1.5 block text-white/60 text-xs sm:mb-2 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
Vanaf
|
|
|
|
|
</label>
|
|
|
|
|
@@ -674,14 +751,14 @@ function AdminPage() {
|
|
|
|
|
type="date"
|
|
|
|
|
value={fromDate}
|
|
|
|
|
onChange={(e) => setFromDate(e.target.value)}
|
|
|
|
|
className="border-white/20 bg-white/10 text-white [color-scheme:dark]"
|
|
|
|
|
className="border-white/20 bg-white/10 text-sm text-white [color-scheme:dark] sm:text-base"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="toDate"
|
|
|
|
|
className="mb-2 block text-sm text-white/60"
|
|
|
|
|
className="mb-1.5 block text-white/60 text-xs sm:mb-2 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
Tot
|
|
|
|
|
</label>
|
|
|
|
|
@@ -690,7 +767,7 @@ function AdminPage() {
|
|
|
|
|
type="date"
|
|
|
|
|
value={toDate}
|
|
|
|
|
onChange={(e) => setToDate(e.target.value)}
|
|
|
|
|
className="border-white/20 bg-white/10 text-white [color-scheme:dark]"
|
|
|
|
|
className="border-white/20 bg-white/10 text-sm text-white [color-scheme:dark] sm:text-base"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -698,24 +775,25 @@ function AdminPage() {
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Export Button */}
|
|
|
|
|
<div className="mb-6 flex items-center justify-between">
|
|
|
|
|
<p className="text-white/60">
|
|
|
|
|
<div className="mb-4 flex flex-col gap-2 sm:mb-6 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
<p className="text-sm text-white/60">
|
|
|
|
|
{pagination?.total ?? 0} registraties gevonden
|
|
|
|
|
</p>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleExport}
|
|
|
|
|
disabled={exportMutation.isPending}
|
|
|
|
|
className="bg-white text-[#214e51] hover:bg-white/90"
|
|
|
|
|
className="w-full bg-white text-[#214e51] hover:bg-white/90 sm:w-auto"
|
|
|
|
|
>
|
|
|
|
|
<Download className="mr-2 h-4 w-4" />
|
|
|
|
|
{exportMutation.isPending ? "Exporteren..." : "Exporteer CSV"}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Registrations Table */}
|
|
|
|
|
{/* Registrations Table / Cards */}
|
|
|
|
|
<Card className="border-white/10 bg-white/5">
|
|
|
|
|
<CardContent className="p-0">
|
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
{/* Desktop Table */}
|
|
|
|
|
<div className="hidden overflow-x-auto lg:block">
|
|
|
|
|
<table className="w-full">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="border-white/10 border-b">
|
|
|
|
|
@@ -898,22 +976,163 @@ function AdminPage() {
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Mobile Cards */}
|
|
|
|
|
<div className="lg:hidden">
|
|
|
|
|
{registrationsQuery.isLoading ? (
|
|
|
|
|
<div className="px-4 py-8 text-center text-white/60">
|
|
|
|
|
Laden...
|
|
|
|
|
</div>
|
|
|
|
|
) : sortedRegistrations.length === 0 ? (
|
|
|
|
|
<div className="px-4 py-8 text-center text-white/60">
|
|
|
|
|
Geen registraties gevonden
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="divide-y divide-white/5">
|
|
|
|
|
{sortedRegistrations.map((reg) => {
|
|
|
|
|
const isPerformer = reg.registrationType === "performer";
|
|
|
|
|
|
|
|
|
|
const guestCount = (() => {
|
|
|
|
|
if (!reg.guests) return 0;
|
|
|
|
|
try {
|
|
|
|
|
const g = JSON.parse(reg.guests as string);
|
|
|
|
|
return Array.isArray(g) ? g.length : 0;
|
|
|
|
|
} catch {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
const detailLabel = isPerformer
|
|
|
|
|
? reg.artForm || "-"
|
|
|
|
|
: `€${reg.drinkCardValue ?? 5} drinkkaart`;
|
|
|
|
|
|
|
|
|
|
const dateLabel = (() => {
|
|
|
|
|
try {
|
|
|
|
|
return new Date(reg.createdAt).toLocaleDateString(
|
|
|
|
|
"nl-BE",
|
|
|
|
|
{
|
|
|
|
|
day: "2-digit",
|
|
|
|
|
month: "2-digit",
|
|
|
|
|
year: "2-digit",
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
return "-";
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={reg.id} className="p-4 hover:bg-white/5">
|
|
|
|
|
<div className="flex items-start justify-between gap-3">
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="truncate font-medium text-white">
|
|
|
|
|
{reg.firstName} {reg.lastName}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 flex items-center gap-2 text-white/60 text-xs">
|
|
|
|
|
<span className="truncate">{reg.email}</span>
|
|
|
|
|
{reg.phone && (
|
|
|
|
|
<span className="shrink-0">• {reg.phone}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span
|
|
|
|
|
className={`shrink-0 rounded-full px-2 py-0.5 font-semibold text-xs ${isPerformer ? "bg-amber-400/15 text-amber-300" : "bg-teal-400/15 text-teal-300"}`}
|
|
|
|
|
>
|
|
|
|
|
{isPerformer ? "Artiest" : "Bezoeker"}
|
|
|
|
|
</span>
|
|
|
|
|
{reg.managementToken && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
title="Kopieer beheerlink"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
handleCopyManageUrl(
|
|
|
|
|
reg.managementToken as string,
|
|
|
|
|
reg.id,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
className="shrink-0 rounded p-1.5 text-white/40 transition-colors hover:bg-white/10 hover:text-white"
|
|
|
|
|
>
|
|
|
|
|
{copiedId === reg.id ? (
|
|
|
|
|
<ClipboardCheck className="h-4 w-4 text-green-400" />
|
|
|
|
|
) : (
|
|
|
|
|
<Clipboard className="h-4 w-4" />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs">
|
|
|
|
|
<div className="text-white/70">
|
|
|
|
|
<span className="text-white/40">Details:</span>{" "}
|
|
|
|
|
{detailLabel}
|
|
|
|
|
</div>
|
|
|
|
|
{guestCount > 0 && (
|
|
|
|
|
<div className="text-white/70">
|
|
|
|
|
<span className="text-white/40">Gasten:</span>{" "}
|
|
|
|
|
{guestCount}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{(reg.giftAmount ?? 0) > 0 && (
|
|
|
|
|
<div className="text-pink-300">
|
|
|
|
|
<span className="text-white/40">Gift:</span>{" "}
|
|
|
|
|
{formatCents(reg.giftAmount)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!isPerformer && (
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-white/40">Betaling:</span>{" "}
|
|
|
|
|
{reg.paymentStatus === "paid" ? (
|
|
|
|
|
<span className="inline-flex items-center gap-1 font-medium text-green-400">
|
|
|
|
|
<Check className="h-3 w-3" />
|
|
|
|
|
Betaald
|
|
|
|
|
</span>
|
|
|
|
|
) : reg.paymentStatus ===
|
|
|
|
|
"extra_payment_pending" ? (
|
|
|
|
|
<span className="inline-flex items-center gap-1 font-medium text-orange-400">
|
|
|
|
|
<span className="h-1 w-1 rounded-full bg-orange-400" />
|
|
|
|
|
Extra (€
|
|
|
|
|
{((reg.paymentAmount ?? 0) / 100).toFixed(0)})
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-yellow-400">Open</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="text-white/50">{dateLabel}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Pagination */}
|
|
|
|
|
{pagination && pagination.totalPages > 1 && (
|
|
|
|
|
<div className="mt-6 flex items-center justify-center gap-2">
|
|
|
|
|
<div className="mt-4 flex items-center justify-center gap-2 sm:mt-6">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
|
|
|
disabled={page === 1}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="border-white/20 bg-transparent text-white hover:bg-white/10 disabled:opacity-50"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="border-white/20 bg-transparent px-2 text-sm text-white hover:bg-white/10 disabled:opacity-50 sm:px-4"
|
|
|
|
|
>
|
|
|
|
|
Vorige
|
|
|
|
|
<span className="hidden sm:inline">Vorige</span>
|
|
|
|
|
<span className="sm:hidden">←</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<span className="mx-4 text-white">
|
|
|
|
|
Pagina {page} van {pagination.totalPages}
|
|
|
|
|
<span className="mx-2 text-sm text-white sm:mx-4">
|
|
|
|
|
<span className="sm:hidden">
|
|
|
|
|
{page}/{pagination.totalPages}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="hidden sm:inline">
|
|
|
|
|
Pagina {page} van {pagination.totalPages}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() =>
|
|
|
|
|
@@ -921,9 +1140,11 @@ function AdminPage() {
|
|
|
|
|
}
|
|
|
|
|
disabled={page === pagination.totalPages}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="border-white/20 bg-transparent text-white hover:bg-white/10 disabled:opacity-50"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="border-white/20 bg-transparent px-2 text-sm text-white hover:bg-white/10 disabled:opacity-50 sm:px-4"
|
|
|
|
|
>
|
|
|
|
|
Volgende
|
|
|
|
|
<span className="hidden sm:inline">Volgende</span>
|
|
|
|
|
<span className="sm:hidden">→</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|