mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
229 lines
7.5 KiB
TypeScript
229 lines
7.5 KiB
TypeScript
"use server";
|
||
|
||
import { prisma } from "@/src/lib/prisma";
|
||
import { requireManager } from "@/src/lib/auth/getUser";
|
||
import { unstable_cache } from "next/cache";
|
||
import { combineDateAndTime } from "@/src/lib/course-time";
|
||
import { toEmailBranding } from "@/src/modules/settings/lib/to-email-branding";
|
||
import { KUMASY_BRANDING } from "@/src/lib/mail/templates/tokens";
|
||
import { paidSeatCounts } from "@/src/modules/courses/lib/seat-counts";
|
||
|
||
export type KundeBooking = {
|
||
bookingId: string;
|
||
courseId: string;
|
||
courseName: string;
|
||
date: string; // ISO
|
||
timeFrom: string;
|
||
timeTo: string;
|
||
roomName: string;
|
||
// Felder für den BookingDialog
|
||
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;
|
||
/** Plätze dieser Buchung - mehr als 1, wenn für Begleitpersonen mitgebucht wurde. */
|
||
seats: number;
|
||
/** Gebuchte Plätze bei Kursen mit Platzwahl, z.B. ["Reformer 3", "Reformer 4"]. */
|
||
spotLabels: string[];
|
||
isPast: boolean;
|
||
businessName: string;
|
||
businessBranding: { logoUrl: string | null; accentColor: string; accentTextColor: string };
|
||
};
|
||
|
||
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.findFirst({
|
||
where: {
|
||
id: kundeId,
|
||
// Sicherheits-Guard: nur zurückgeben wenn der User wirklich
|
||
// mindestens eine Buchung bei diesem Manager hat
|
||
courseBookings: {
|
||
some: { course: { businessId: { in: businessIds } } },
|
||
},
|
||
},
|
||
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,
|
||
seats: true,
|
||
spotLabels: 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,
|
||
business: { select: { name: true, branding: 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,
|
||
};
|
||
}
|
||
|
||
// Belegte Plätze pro Kurs - Summe der Plätze bezahlter Buchungen.
|
||
const seatCounts = await paidSeatCounts(
|
||
prisma,
|
||
userWithBookings.courseBookings.map((b) => b.course.id)
|
||
);
|
||
|
||
const now = new Date();
|
||
|
||
const bookings: KundeBooking[] = userWithBookings.courseBookings.map((b) => {
|
||
const courseStart = combineDateAndTime(b.course.date, b.course.timeFrom);
|
||
const branding = b.course.business ? toEmailBranding(b.course.business) : KUMASY_BRANDING;
|
||
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: seatCounts.get(b.course.id) ?? 0,
|
||
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,
|
||
seats: b.seats,
|
||
spotLabels: b.spotLabels,
|
||
isPast: courseStart < now,
|
||
businessName: b.course.business?.name ?? branding.companyName,
|
||
businessBranding: {
|
||
logoUrl: branding.logoUrl,
|
||
accentColor: branding.accentColor,
|
||
accentTextColor: branding.accentTextColor,
|
||
},
|
||
};
|
||
});
|
||
|
||
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." };
|
||
}
|
||
}
|