added detail customer page in admin dashbaord with customer data and customer bookings

This commit is contained in:
Rajpreet0 2026-05-27 19:54:23 +02:00
parent 9231d9651c
commit 63d2632154
6 changed files with 710 additions and 34 deletions

View file

@ -1,6 +1,9 @@
import { requireManager } from "@/src/lib/auth/getUser";
import { getKundeDetail } from "@/src/modules/kunden/actions/kunde-detail-actions";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import KundeDetailView from "@/src/modules/kunden/ui/views/kunde-detail-view";
interface Props {
params: Promise<{ id: string }>;
@ -10,17 +13,22 @@ const KundeDetailPage = async ({ params }: Props) => {
await requireManager();
const { id } = await params;
const result = await getKundeDetail(id);
if (!result.success) {
notFound();
}
return (
<div className="p-2">
<Link
href="/kunden"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-6"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-6 transition-colors"
>
<ArrowLeft size={16} />
Zurück zur Übersicht
</Link>
<h1 className="text-4xl font-bold mb-2">Kundendetails</h1>
<p className="text-muted-foreground text-sm">Kunde ID: {id}</p>
<KundeDetailView kunde={result.kunde} />
</div>
);
};

View file

@ -187,7 +187,7 @@ function Sidebar({
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
className="bg-white text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,

View file

@ -39,20 +39,20 @@ const levelConfig: Record<string, { label: string; bg: string; text: string }> =
pro: { label: "Profi", bg: "bg-red-200", text: "text-red-700" },
};
interface TrainerProfile {
export interface TrainerProfile {
id: string;
firstName: string | null;
lastName: string | null;
pbSrc: string | null;
}
interface BusinessInfo {
export interface BusinessInfo {
address: string;
email: string;
tel?: string;
}
interface PublicCourse {
export interface PublicCourse {
id: string;
name: string;
sport: string[];
@ -102,22 +102,34 @@ const TrainerAvatar = ({ trainer, size }: { trainer: TrainerProfile; size: "sm"
);
};
const BookingDialog = ({
const ADMIN_PAYMENT_LABELS: Record<string, { label: string; bg: string; text: string }> = {
paid: { label: "Bezahlt", bg: "bg-green-100", text: "text-green-700" },
pending: { label: "Ausstehend", bg: "bg-yellow-100", text: "text-yellow-700" },
refunded: { label: "Storniert", bg: "bg-red-100", text: "text-red-600" },
failed: { label: "Fehlgeschlagen", bg: "bg-red-100", text: "text-red-600" },
};
export const BookingDialog = ({
course,
business,
open,
onOpenChange,
// Wenn gesetzt → Admin-Ansicht: kein Buchen-Button, nur Status-Badge
paymentStatus,
}: {
course: PublicCourse;
business?: BusinessInfo;
open: boolean;
onOpenChange: (v: boolean) => void;
paymentStatus?: string;
}) => {
const sanitizedDescription = useSanitizedHtml(course.description);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isBooked, setIsBooked] = useState(false);
const [isCheckingStatus, setIsCheckingStatus] = useState(true);
const isAdminView = paymentStatus !== undefined;
const safeCurrent = Math.max(0, course.currentParticipants);
const safeMax = Math.max(0, course.maxParticipants);
const isFull = safeCurrent >= safeMax;
@ -134,8 +146,9 @@ const BookingDialog = ({
currency: "EUR",
});
// Booking-Status nur laden wenn keine Admin-Ansicht
useEffect(() => {
if (!open) return;
if (!open || isAdminView) return;
let active = true;
const loadStatus = async () => {
@ -156,7 +169,7 @@ const BookingDialog = ({
return () => {
active = false;
};
}, [open, course.id]);
}, [open, course.id, isAdminView]);
const handleBooking = async () => {
setIsSubmitting(true);
@ -193,6 +206,10 @@ const BookingDialog = ({
}
};
const adminStatus = isAdminView
? (ADMIN_PAYMENT_LABELS[paymentStatus!] ?? ADMIN_PAYMENT_LABELS.pending)
: null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-3xl! w-[95vw] max-w-[calc(100%-1rem)] p-0 overflow-hidden gap-0 max-h-[90vh] sm:max-h-[85vh] flex flex-col">
@ -315,33 +332,54 @@ const BookingDialog = ({
</div>
</div>
{/* Sticky Footer: bleibt immer sichtbar, auf Mobile gestapelt */}
{/* Sticky Footer */}
<div className="shrink-0 border-t border-gray-200 bg-white px-4 sm:px-6 py-3 sm:py-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-lg font-bold text-gray-900">Preis: {formattedPrice}</span>
<div className="flex gap-2 w-full sm:w-auto">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="flex-1 sm:flex-initial"
>
Schließen
</Button>
{isCheckingStatus ? (
<Button disabled className="flex-1 sm:flex-initial">
Lade…
</Button>
) : isBooked ? (
<Button disabled className="flex-1 sm:flex-initial">
✓ Angemeldet
</Button>
<div className="flex items-center gap-3 w-full sm:w-auto">
{/* Admin-Ansicht: nur Status-Badge + Schließen */}
{isAdminView ? (
<>
<span
className={`text-sm font-semibold px-3 py-1.5 rounded-full ${adminStatus!.bg} ${adminStatus!.text}`}
>
{adminStatus!.label}
</span>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="flex-1 sm:flex-initial"
>
Schließen
</Button>
</>
) : (
<Button
onClick={handleBooking}
disabled={isSubmitting || isFull}
className="flex-1 sm:flex-initial"
>
{isSubmitting ? "Wird gebucht…" : isFull ? "Ausgebucht" : "Buchen"}
</Button>
/* Public-Ansicht: Buchen-Button */
<>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="flex-1 sm:flex-initial"
>
Schließen
</Button>
{isCheckingStatus ? (
<Button disabled className="flex-1 sm:flex-initial">
Lade…
</Button>
) : isBooked ? (
<Button disabled className="flex-1 sm:flex-initial">
✓ Angemeldet
</Button>
) : (
<Button
onClick={handleBooking}
disabled={isSubmitting || isFull}
className="flex-1 sm:flex-initial"
>
{isSubmitting ? "Wird gebucht…" : isFull ? "Ausgebucht" : "Buchen"}
</Button>
)}
</>
)}
</div>
</div>

View file

@ -0,0 +1,196 @@
"use server";
import { prisma } from "@/src/lib/prisma";
import { requireManager } from "@/src/lib/auth/getUser";
import { unstable_cache } from "next/cache";
export type KundeBooking = {
bookingId: string;
courseId: string;
courseName: string;
date: string; // ISO
timeFrom: string;
timeTo: string;
roomName: string;
// Felder für den PublicCourseCard-Dialog
level: string;
price: number;
maxParticipants: number;
currentParticipants: number;
coverImage: string | null;
description: string;
trainerProfiles: {
id: string;
firstName: string | null;
lastName: string | null;
pbSrc: string | null;
}[];
// Für die Card-Anzeige
trainers: string[];
trainersMap: Record<string, { label: string; pbSrc?: string }>;
paymentStatus: string;
isPast: boolean;
};
export type KundeDetail = {
id: string;
name: string;
email: string;
telefon: string;
gender: string | null;
birthday: Date | null;
pbSrc: string | null;
createdAt: Date | null;
bookings: KundeBooking[];
};
export async function getKundeDetail(
kundeId: string
): Promise<{ success: true; kunde: KundeDetail } | { success: false; error: string }> {
const manager = await requireManager();
const businessIds = manager.businesses.map((b) => b.id);
const fetchDetail = unstable_cache(
async () => {
// Alles parallel: User+Bookings und Räume des Managers
const [userWithBookings, allRooms] = await Promise.all([
prisma.user.findUnique({
where: { id: kundeId },
select: {
id: true,
name: true,
email: true,
tel: true,
gender: true,
birthday: true,
pbSrc: true,
createdAt: true,
courseBookings: {
where: {
course: { businessId: { in: businessIds } },
},
select: {
id: true,
paymentStatus: true,
course: {
select: {
id: true,
name: true,
date: true,
timeFrom: true,
timeTo: true,
room: true,
trainers: true,
// Dialog-Felder
level: true,
price: true,
maxParticipants: true,
coverImage: true,
description: true,
// Anzahl aller Buchungen für diesen Kurs
_count: { select: { bookings: true } },
},
},
},
orderBy: { createdAt: "desc" },
},
},
}),
prisma.room.findMany({
where: { createdBy: manager.id },
select: { id: true, name: true },
}),
]);
if (!userWithBookings) return null;
// Raum-ID → Name
const roomsMap: Record<string, string> = {};
for (const r of allRooms) roomsMap[r.id] = r.name;
// Alle Trainer-IDs sammeln
const allTrainerIds = new Set<string>();
for (const b of userWithBookings.courseBookings) {
for (const tid of b.course.trainers) allTrainerIds.add(tid);
}
// Trainer-Daten laden (ein Query für alle)
const trainersData = await prisma.employee.findMany({
where: { id: { in: Array.from(allTrainerIds) } },
select: { id: true, firstName: true, lastName: true, pbSrc: true },
});
// trainersMap für die Card-Avatare (id → label+pbSrc)
const trainersMap: Record<string, { label: string; pbSrc?: string }> = {};
for (const t of trainersData) {
const label = [t.firstName, t.lastName].filter(Boolean).join(" ") || "Trainer";
trainersMap[t.id] = { label, pbSrc: t.pbSrc ?? undefined };
}
// trainerProfiles-Map für den Dialog (id → volles Profil)
const trainerProfilesMap: Record<
string,
{ id: string; firstName: string | null; lastName: string | null; pbSrc: string | null }
> = {};
for (const t of trainersData) {
trainerProfilesMap[t.id] = {
id: t.id,
firstName: t.firstName,
lastName: t.lastName,
pbSrc: t.pbSrc,
};
}
const now = new Date();
const bookings: KundeBooking[] = userWithBookings.courseBookings.map((b) => {
const courseDate = new Date(b.course.date);
return {
bookingId: b.id,
courseId: b.course.id,
courseName: b.course.name,
date: b.course.date.toISOString(),
timeFrom: b.course.timeFrom,
timeTo: b.course.timeTo,
roomName: roomsMap[b.course.room] ?? b.course.room,
level: b.course.level,
price: b.course.price,
maxParticipants: b.course.maxParticipants,
currentParticipants: b.course._count.bookings,
coverImage: b.course.coverImage ?? null,
description: b.course.description,
trainerProfiles: b.course.trainers.map((tid) => trainerProfilesMap[tid]).filter(Boolean),
trainers: b.course.trainers,
trainersMap,
paymentStatus: b.paymentStatus,
isPast: courseDate < now,
};
});
return {
id: userWithBookings.id,
name: userWithBookings.name ?? userWithBookings.email,
email: userWithBookings.email,
telefon: userWithBookings.tel ?? "–",
gender: userWithBookings.gender,
birthday: userWithBookings.birthday,
pbSrc: userWithBookings.pbSrc,
createdAt: userWithBookings.createdAt,
bookings,
} satisfies KundeDetail;
},
[`kunde-detail-${kundeId}-${manager.id}`],
{
tags: [`kunde-detail-${kundeId}`, `manager-${manager.id}`],
revalidate: 60,
}
);
try {
const kunde = await fetchDetail();
if (!kunde) return { success: false, error: "Kunde nicht gefunden." };
return { success: true, kunde };
} catch {
return { success: false, error: "Kundendetails konnten nicht geladen werden." };
}
}

View file

@ -0,0 +1,129 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { MapPin, Clock } from "lucide-react";
import type { KundeBooking } from "../../actions/kunde-detail-actions";
import AbstractTooltip from "@/src/components/layout/AbstractTooltip";
import { BookingDialog } from "@/src/modules/courses/ui/components/PublicCourseCard";
import type { PublicCourse } from "@/src/modules/courses/ui/components/PublicCourseCard";
const getDiceBearUrl = (seed: string) =>
`https://api.dicebear.com/9.x/thumbs/svg?seed=${encodeURIComponent(seed)}`;
const PAYMENT_STATUS: Record<string, { label: string; bg: string; text: string }> = {
paid: { label: "Bezahlt", bg: "bg-green-100", text: "text-green-700" },
pending: { label: "Ausstehend", bg: "bg-yellow-100", text: "text-yellow-700" },
refunded: { label: "Storniert", bg: "bg-red-100", text: "text-red-600" },
failed: { label: "Fehlgeschlagen", bg: "bg-red-100", text: "text-red-600" },
};
/** KundeBooking → PublicCourse für den BookingDialog */
function toPublicCourse(b: KundeBooking): PublicCourse {
return {
id: b.courseId,
name: b.courseName,
sport: [],
level: b.level,
date: new Date(b.date),
timeFrom: b.timeFrom,
timeTo: b.timeTo,
maxParticipants: b.maxParticipants,
price: b.price,
currentParticipants: b.currentParticipants,
description: b.description,
roomName: b.roomName,
coverImage: b.coverImage ?? undefined,
trainerProfiles: b.trainerProfiles,
};
}
interface KundeBookingCardProps {
booking: KundeBooking;
}
const KundeBookingCard = ({ booking }: KundeBookingCardProps) => {
const [dialogOpen, setDialogOpen] = useState(false);
const status = PAYMENT_STATUS[booking.paymentStatus] ?? PAYMENT_STATUS.pending;
const dateFormatted = new Date(booking.date).toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
return (
<>
<div
onClick={() => setDialogOpen(true)}
className="bg-white/70 backdrop-blur-sm border border-white/50 rounded-xl p-4 shadow-sm hover:shadow-md hover:bg-white/85 transition-all cursor-pointer flex flex-col gap-3 select-none"
>
{/* Trainer Avatare + Uhrzeit */}
<div className="flex items-center justify-between">
<div className="flex items-center">
{booking.trainers.length > 0 ? (
booking.trainers.map((tid, idx) => {
const info = booking.trainersMap[tid];
const name = info?.label ?? "Trainer";
const src = info?.pbSrc ?? getDiceBearUrl(tid);
return (
<AbstractTooltip key={tid} tooltipText={name}>
<div
className={`relative w-8 h-8 rounded-full border-2 border-white overflow-hidden bg-gray-100 ${
idx !== 0 ? "-ml-2" : ""
}`}
style={{ zIndex: booking.trainers.length - idx }}
>
<Image src={src} alt={name} fill className="object-cover" unoptimized />
</div>
</AbstractTooltip>
);
})
) : (
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-xs text-gray-400">
–
</div>
)}
</div>
<span className="text-sm text-gray-500 flex items-center gap-1">
<Clock size={13} className="shrink-0" />
{booking.timeFrom} – {booking.timeTo}
</span>
</div>
{/* Kursname */}
<h3 className="font-semibold text-gray-900 text-base leading-tight">
{booking.courseName}
</h3>
{/* Datum + Raum */}
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{dateFormatted}</span>
<span className="flex items-center gap-1">
<MapPin size={13} className="shrink-0" />
{booking.roomName}
</span>
</div>
{/* Status Badge */}
<div>
<span
className={`inline-block text-xs font-semibold px-2.5 py-1 rounded-full ${status.bg} ${status.text}`}
>
{status.label}
</span>
</div>
</div>
{/* Exakt derselbe Dialog wie auf der Public Page, aber in der Admin-Ansicht */}
<BookingDialog
course={toPublicCourse(booking)}
open={dialogOpen}
onOpenChange={setDialogOpen}
paymentStatus={booking.paymentStatus}
/>
</>
);
};
export default KundeBookingCard;

View file

@ -0,0 +1,305 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import {
Phone,
Mail,
User,
Calendar,
Hash,
ChevronDown,
ChevronUp,
BookOpen,
History,
CreditCard,
Bell,
Scale,
CheckCircle2,
} from "lucide-react";
import type { KundeDetail } from "../../actions/kunde-detail-actions";
import KundeBookingCard from "../components/kunde-booking-card";
// ─── DiceBear Fallback ────────────────────────────────────────────────────────
const getDiceBearUrl = (seed: string) =>
`https://api.dicebear.com/9.x/thumbs/svg?seed=${encodeURIComponent(seed)}`;
// ─── ID kürzen: erste 8 Zeichen + … ──────────────────────────────────────────
const shortenId = (id: string) => `${id.slice(0, 8)}…${id.slice(-4)}`;
// ─── Kollapsible Sektion ──────────────────────────────────────────────────────
interface CollapsibleSectionProps {
icon: React.ReactNode;
title: string;
defaultOpen?: boolean;
children: React.ReactNode;
badge?: number | string;
}
const CollapsibleSection = ({
icon,
title,
defaultOpen = true,
children,
badge,
}: CollapsibleSectionProps) => {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="rounded-2xl border border-white/60 bg-white/55 backdrop-blur-xl shadow-sm overflow-hidden">
<button
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between px-5 py-4 hover:bg-white/50 transition-colors"
>
<div className="flex items-center gap-3">
<span className="text-blue">{icon}</span>
<span className="font-semibold text-gray-800 text-base">{title}</span>
{badge !== undefined && (
<span className="text-xs font-semibold px-2 py-0.5 rounded-full bg-blue/10 text-blue">
{badge}
</span>
)}
</div>
{open ? (
<ChevronUp size={18} className="text-gray-400" />
) : (
<ChevronDown size={18} className="text-gray-400" />
)}
</button>
{open && <div className="px-5 pb-5 pt-1 border-t border-white/40">{children}</div>}
</div>
);
};
// ─── Info-Zeile in der ID-Karte ───────────────────────────────────────────────
interface InfoRowProps {
icon: React.ReactNode;
label: string;
value: React.ReactNode;
}
const InfoRow = ({ icon, label, value }: InfoRowProps) => (
<div className="flex items-start gap-2.5">
<span className="mt-0.5 text-blue shrink-0">{icon}</span>
<div className="min-w-0">
<p className="text-xs text-gray-400 leading-none mb-0.5">{label}</p>
<p className="font-medium text-gray-800 text-sm truncate">{value}</p>
</div>
</div>
);
// ─── Haupt-View ───────────────────────────────────────────────────────────────
interface KundeDetailViewProps {
kunde: KundeDetail;
}
const KundeDetailView = ({ kunde }: KundeDetailViewProps) => {
const avatarSrc = kunde.pbSrc ?? getDiceBearUrl(kunde.id);
const upcomingBookings = kunde.bookings.filter((b) => !b.isPast);
const pastBookings = kunde.bookings.filter((b) => b.isPast);
const genderLabel: Record<string, string> = {
male: "Männlich",
female: "Weiblich",
diverse: "Divers",
};
const birthdayFormatted = kunde.birthday
? new Date(kunde.birthday).toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})
: "–";
const memberSinceFormatted = kunde.createdAt
? new Date(kunde.createdAt).toLocaleDateString("de-DE", {
day: "2-digit",
month: "long",
year: "numeric",
})
: "–";
return (
<div className="flex flex-col gap-5 w-full">
{/* Kundenname */}
<h1 className="text-4xl font-bold">{kunde.name}</h1>
{/* ── Glassmorphism ID-Karte ── */}
<div className="relative rounded-2xl overflow-hidden border border-white/60 shadow-lg w-full">
{/* weißes Glas-Hintergrundpanel */}
<div className="absolute inset-0 bg-white/40 backdrop-blur-2xl" />
{/* subtiler weißer Glanz-Schein oben */}
<div className="absolute inset-x-0 top-0 h-px bg-white/80" />
<div className="relative p-6 flex flex-col sm:flex-row items-start gap-6">
{/* Avatar – größer */}
<div className="relative w-28 h-28 rounded-full overflow-hidden border-4 border-white/80 shadow-lg shrink-0 bg-white/50">
<Image src={avatarSrc} alt={kunde.name} fill className="object-cover" unoptimized />
</div>
{/* Info-Grid */}
<div className="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-x-10 gap-y-4">
<InfoRow icon={<Phone size={15} />} label="Telefon" value={kunde.telefon} />
<InfoRow icon={<Mail size={15} />} label="E-Mail" value={kunde.email} />
<InfoRow
icon={<User size={15} />}
label="Geschlecht"
value={kunde.gender ? (genderLabel[kunde.gender] ?? kunde.gender) : "–"}
/>
<InfoRow icon={<Calendar size={15} />} label="Geburtsdatum" value={birthdayFormatted} />
{/* Kunden-ID über volle Breite */}
<div className="sm:col-span-2 flex items-start gap-2.5">
<span className="mt-0.5 text-blue shrink-0">
<Hash size={15} />
</span>
<div className="min-w-0">
<p className="text-xs text-gray-400 leading-none mb-0.5">Kunden-ID</p>
<p
className="font-mono text-sm text-gray-700 truncate cursor-default"
title={kunde.id}
>
{shortenId(kunde.id)}
</p>
</div>
</div>
</div>
{/* Mitglied-seit Chip – oben rechts auf Desktop */}
<div className="sm:absolute sm:top-4 sm:right-5 shrink-0">
<span className="text-xs text-gray-500 bg-white/70 border border-white/60 px-3 py-1 rounded-full shadow-sm whitespace-nowrap">
Mitglied seit {memberSinceFormatted}
</span>
</div>
</div>
</div>
{/* ── Gebuchte Kurse (zukünftig) ── */}
<CollapsibleSection
icon={<BookOpen size={18} />}
title="Gebuchte Kurse"
badge={upcomingBookings.length}
defaultOpen={true}
>
{upcomingBookings.length === 0 ? (
<p className="text-sm text-muted-foreground py-3">Keine bevorstehenden Kursbuchungen.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 pt-3">
{upcomingBookings.map((b) => (
<KundeBookingCard key={b.bookingId} booking={b} />
))}
</div>
)}
</CollapsibleSection>
{/* ── Vergangene Kurse ── */}
<CollapsibleSection
icon={<History size={18} />}
title="Vergangene Kurse"
badge={pastBookings.length}
defaultOpen={false}
>
{pastBookings.length === 0 ? (
<p className="text-sm text-muted-foreground py-3">Keine vergangenen Kursbuchungen.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 pt-3">
{pastBookings.map((b) => (
<KundeBookingCard key={b.bookingId} booking={b} />
))}
</div>
)}
</CollapsibleSection>
{/* ── Abonnements & Credits (Dummy) ── */}
<CollapsibleSection
icon={<CreditCard size={18} />}
title="Abonnements & Credits"
defaultOpen={false}
>
<div className="pt-3 flex flex-col gap-2.5">
{[
{ label: "Aktive Abonnements", value: "1 Abo" },
{ label: "Übrige Buchungen", value: "4" },
{ label: "Credits", value: "0" },
].map((row) => (
<div
key={row.label}
className="flex items-center justify-between rounded-xl bg-white/60 border border-white/50 px-4 py-3"
>
<span className="text-sm text-gray-600">{row.label}</span>
<span className="font-semibold text-gray-900 text-sm">{row.value}</span>
</div>
))}
</div>
</CollapsibleSection>
{/* ── Benachrichtigungen (Dummy) ── */}
<CollapsibleSection icon={<Bell size={18} />} title="Benachrichtigungen" defaultOpen={false}>
<div className="pt-3 grid grid-cols-1 sm:grid-cols-2 gap-2.5">
{[
{ label: "Newsletter", value: "Nein" },
{ label: "Marketing", value: "Nein" },
{ label: "Buchungsbestätigung", value: "Ja" },
{ label: "Warteliste", value: "Ja" },
{ label: "Erinnerungen", value: "Ja" },
{ label: "Kursbeginn", value: "Ja" },
{ label: "Kursänderungen", value: "Ja" },
{ label: "Abonnements-News", value: "Nein" },
].map((item) => (
<div
key={item.label}
className="flex items-center justify-between rounded-xl bg-white/60 border border-white/50 px-4 py-3"
>
<span className="text-sm text-gray-600">{item.label}</span>
<span
className={`text-sm font-semibold ${
item.value === "Ja" ? "text-green-600" : "text-red-500"
}`}
>
{item.value}
</span>
</div>
))}
</div>
</CollapsibleSection>
{/* ── Rechtliches (Dummy) ── */}
<CollapsibleSection icon={<Scale size={18} />} title="Rechtliches" defaultOpen={false}>
<div className="pt-3 grid grid-cols-1 sm:grid-cols-2 gap-2.5">
{/* AGB */}
<div className="rounded-xl bg-white/60 border border-white/50 px-4 py-4">
<div className="flex items-start gap-3">
<CheckCircle2 size={18} className="text-green-500 mt-0.5 shrink-0" />
<div>
<p className="font-semibold text-gray-800 text-sm">AGB</p>
<p className="text-xs text-gray-500 mt-0.5">Zustimmung: Ja</p>
<p className="text-xs text-gray-400 mt-0.5">Datum: 14.01.2025</p>
<p className="text-xs text-gray-400">Uhrzeit: 19:51</p>
<p className="text-xs text-gray-400 font-mono mt-1">Version 1.0.0</p>
</div>
</div>
</div>
{/* Datenschutzerklärung */}
<div className="rounded-xl bg-white/60 border border-white/50 px-4 py-4">
<div className="flex items-start gap-3">
<CheckCircle2 size={18} className="text-green-500 mt-0.5 shrink-0" />
<div>
<p className="font-semibold text-gray-800 text-sm">Datenschutzerklärung</p>
<p className="text-xs text-gray-500 mt-0.5">Zustimmung: Ja</p>
<p className="text-xs text-gray-400 mt-0.5">Datum: 14.01.2025</p>
<p className="text-xs text-gray-400">Uhrzeit: 19:51</p>
<button className="text-xs text-blue underline mt-2 hover:opacity-70 transition-opacity">
Datenschutzerklärung ansehen
</button>
</div>
</div>
</div>
</div>
</CollapsibleSection>
</div>
);
};
export default KundeDetailView;