From ebd9d8a72ef22d614b2f5fe4c45aa9dd6a981d98 Mon Sep 17 00:00:00 2001 From: elpatron Date: Wed, 8 Oct 2025 19:50:16 +0200 Subject: [PATCH] =?UTF-8?q?Refactor:=20Verbessere=20CalDAV=20und=20Booking?= =?UTF-8?q?s=20f=C3=BCr=20Multi-Treatment-Support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CalDAV SUMMARY zeigt jetzt alle Treatment-Namen als Liste statt Anzahl - Treatments-Array im Booking-Type optional für Rückwärtskompatibilität - Neue addMinutesToTime Helper-Funktion für saubere DTEND-Berechnung - getTreatmentNames filtert leere Namen und liefert sicheren Fallback --- src/client/components/admin-bookings.tsx | 55 ++++++- src/client/components/booking-status-page.tsx | 146 ++++++++++++++++-- src/server/routes/caldav.ts | 55 +++++-- 3 files changed, 223 insertions(+), 33 deletions(-) diff --git a/src/client/components/admin-bookings.tsx b/src/client/components/admin-bookings.tsx index b7e07ad..7482ac4 100644 --- a/src/client/components/admin-bookings.tsx +++ b/src/client/components/admin-bookings.tsx @@ -64,8 +64,20 @@ export function AdminBookings() { }) ); - const getTreatmentName = (treatmentId: string) => { - return treatments?.find(t => t.id === treatmentId)?.name || "Unbekannte Behandlung"; + const getTreatmentNames = (booking: any) => { + // Handle new treatments array structure + if (booking.treatments && Array.isArray(booking.treatments) && booking.treatments.length > 0) { + const names = booking.treatments + .map((t: any) => t.name) + .filter((name: string) => name && name.trim()) + .join(", "); + return names || "Keine Behandlung"; + } + // Fallback to deprecated treatmentId for backward compatibility + if (booking.treatmentId) { + return treatments?.find(t => t.id === booking.treatmentId)?.name || "Unbekannte Behandlung"; + } + return "Keine Behandlung"; }; const getStatusColor = (status: string) => { @@ -260,8 +272,8 @@ export function AdminBookings() {
{booking.customerPhone || '—'}
- -
{getTreatmentName(booking.treatmentId)}
+ +
{getTreatmentNames(booking)}
{booking.notes && (
Notizen: {booking.notes}
)} @@ -445,6 +457,15 @@ export function AdminBookings() { const booking = bookings?.find(b => b.id === showMessageModal); if (!booking) return null; + // Calculate totals for multiple treatments + const hasTreatments = booking.treatments && Array.isArray(booking.treatments) && booking.treatments.length > 0; + const totalDuration = hasTreatments + ? booking.treatments.reduce((sum: number, t: any) => sum + (t.duration || 0), 0) + : (booking.bookedDurationMinutes || 0); + const totalPrice = hasTreatments + ? booking.treatments.reduce((sum: number, t: any) => sum + (t.price || 0), 0) + : 0; + return (

@@ -456,9 +477,29 @@ export function AdminBookings() {

Termin: {new Date(booking.appointmentDate).toLocaleDateString()} um {booking.appointmentTime}

-

- Behandlung: {getTreatmentName(booking.treatmentId)} -

+
+ Behandlungen: + {hasTreatments ? ( +
+ {booking.treatments.map((treatment: any, index: number) => ( +
+ • {treatment.name} ({treatment.duration} Min., {treatment.price}€) +
+ ))} + {booking.treatments.length > 1 && ( +
+ Gesamt: {totalDuration} Min., {totalPrice.toFixed(2)}€ +
+ )} +
+ ) : booking.treatmentId ? ( +
+ • {treatments?.find(t => t.id === booking.treatmentId)?.name || "Unbekannte Behandlung"} +
+ ) : ( + Keine Behandlung + )} +
); })()} diff --git a/src/client/components/booking-status-page.tsx b/src/client/components/booking-status-page.tsx index d366c1b..5fe4189 100644 --- a/src/client/components/booking-status-page.tsx +++ b/src/client/components/booking-status-page.tsx @@ -8,6 +8,50 @@ interface BookingStatusPageProps { type BookingStatus = "pending" | "confirmed" | "cancelled" | "completed"; +interface Treatment { + id: string; + name: string; + duration: number; + price: number; +} + +interface BookingDetails { + id: string; + customerName: string; + customerEmail?: string; + customerPhone?: string; + appointmentDate: string; + appointmentTime: string; + treatments: Treatment[]; + totalDuration: number; + totalPrice: number; + status: BookingStatus; + notes?: string; + formattedDate: string; + createdAt: string; + canCancel: boolean; + hoursUntilAppointment: number; +} + +interface RescheduleProposalDetails { + booking: { + id: string; + customerName: string; + customerEmail?: string; + customerPhone?: string; + status: BookingStatus; + treatments: Treatment[]; + totalDuration: number; + totalPrice: number; + }; + original: { date: string; time: string }; + proposed: { date: string; time: string }; + expiresAt: string; + hoursUntilExpiry: number; + isExpired: boolean; + canRespond: boolean; +} + function getStatusInfo(status: BookingStatus) { switch (status) { case "pending": @@ -57,7 +101,7 @@ export default function BookingStatusPage({ token }: BookingStatusPageProps) { const [showCancelConfirm, setShowCancelConfirm] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const [cancellationResult, setCancellationResult] = useState<{ success: boolean; message: string; formattedDate?: string } | null>(null); - const [rescheduleProposal, setRescheduleProposal] = useState(null); + const [rescheduleProposal, setRescheduleProposal] = useState(null); const [rescheduleResult, setRescheduleResult] = useState<{ success: boolean; message: string } | null>(null); const [isAccepting, setIsAccepting] = useState(false); const [isDeclining, setIsDeclining] = useState(false); @@ -71,7 +115,7 @@ export default function BookingStatusPage({ token }: BookingStatusPageProps) { ); // Try fetching reschedule proposal if booking not found or error - const rescheduleQuery = useQuery({ + const rescheduleQuery = useQuery({ ...queryClient.cancellation.getRescheduleProposal.queryOptions({ input: { token } }), enabled: !!token && (!!bookingError || !booking), }); @@ -311,12 +355,56 @@ export default function BookingStatusPage({ token }: BookingStatusPageProps) {
Aktueller Termin
{rescheduleProposal.original.date} um {rescheduleProposal.original.time} Uhr
-
{rescheduleProposal.booking.treatmentName}
+
+ {rescheduleProposal.booking.treatments && rescheduleProposal.booking.treatments.length > 0 ? ( + <> + {rescheduleProposal.booking.treatments.length <= 2 ? ( + rescheduleProposal.booking.treatments.map((t, i) => ( +
{t.name}
+ )) + ) : ( + <> + {rescheduleProposal.booking.treatments.slice(0, 2).map((t, i) => ( +
{t.name}
+ ))} +
+{rescheduleProposal.booking.treatments.length - 2} weitere
+ + )} +
+ {rescheduleProposal.booking.totalDuration} Min +
+ + ) : ( + Keine Behandlungen + )} +
Neuer Vorschlag
{rescheduleProposal.proposed.date} um {rescheduleProposal.proposed.time} Uhr
-
{rescheduleProposal.booking.treatmentName}
+
+ {rescheduleProposal.booking.treatments && rescheduleProposal.booking.treatments.length > 0 ? ( + <> + {rescheduleProposal.booking.treatments.length <= 2 ? ( + rescheduleProposal.booking.treatments.map((t, i) => ( +
{t.name}
+ )) + ) : ( + <> + {rescheduleProposal.booking.treatments.slice(0, 2).map((t, i) => ( +
{t.name}
+ ))} +
+{rescheduleProposal.booking.treatments.length - 2} weitere
+ + )} +
+ {rescheduleProposal.booking.totalDuration} Min +
+ + ) : ( + Keine Behandlungen + )} +
@@ -478,20 +566,44 @@ export default function BookingStatusPage({ token }: BookingStatusPageProps) { Uhrzeit: {booking?.appointmentTime} Uhr
-
- Behandlung: - {booking?.treatmentName} + + {/* Treatments List */} +
+
Behandlungen:
+ {booking?.treatments && booking.treatments.length > 0 ? ( +
+ {booking.treatments.map((treatment, index) => ( +
+ • {treatment.name} + + {treatment.duration} Min - {treatment.price.toFixed(2)} € + +
+ ))} +
+ Gesamt: + + {booking.totalDuration} Min - {booking.totalPrice.toFixed(2)} € + +
+
+ ) : ( +
+ Keine Behandlungen angegeben + {((booking?.totalDuration ?? 0) > 0 || (booking?.totalPrice ?? 0) > 0) && ( +
+
+ Gesamt: + + {booking?.totalDuration ?? 0} Min - {(booking?.totalPrice ?? 0).toFixed(2)} € + +
+
+ )} +
+ )}
-
- Dauer: - {booking?.treatmentDuration} Minuten -
- {booking?.treatmentPrice && booking.treatmentPrice > 0 && ( -
- Preis: - {booking.treatmentPrice.toFixed(2)} € -
- )} + {booking?.hoursUntilAppointment && booking.hoursUntilAppointment > 0 && booking.status !== "cancelled" && booking.status !== "completed" && (
Verbleibende Zeit: diff --git a/src/server/routes/caldav.ts b/src/server/routes/caldav.ts index 3e6d73b..1c6dddd 100644 --- a/src/server/routes/caldav.ts +++ b/src/server/routes/caldav.ts @@ -5,7 +5,7 @@ import { assertOwner } from "../lib/auth.js"; // Types für Buchungen (vereinfacht für CalDAV) type Booking = { id: string; - treatmentId: string; + treatments?: Array<{id: string, name: string, duration: number, price: number}>; customerName: string; customerEmail?: string; customerPhone?: string; @@ -13,6 +13,8 @@ type Booking = { appointmentTime: string; // HH:MM status: "pending" | "confirmed" | "cancelled" | "completed"; notes?: string; + // Deprecated fields for backward compatibility + treatmentId?: string; bookedDurationMinutes?: number; createdAt: string; }; @@ -44,6 +46,14 @@ function formatDateTime(dateStr: string, timeStr: string): string { return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); } +function addMinutesToTime(timeStr: string, minutesToAdd: number): string { + const [hours, minutes] = timeStr.split(':').map(Number); + const totalMinutes = hours * 60 + minutes + minutesToAdd; + const newHours = Math.floor(totalMinutes / 60); + const newMinutes = totalMinutes % 60; + return `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`; +} + function generateICSContent(bookings: Booking[], treatments: Treatment[]): string { const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); @@ -63,14 +73,41 @@ X-WR-TIMEZONE:Europe/Berlin ); for (const booking of activeBookings) { - const treatment = treatments.find(t => t.id === booking.treatmentId); - const treatmentName = treatment?.name || 'Unbekannte Behandlung'; - const duration = booking.bookedDurationMinutes || treatment?.duration || 60; + // Handle new treatments array structure + let treatmentNames: string; + let duration: number; + let treatmentDetails: string; + let totalPrice = 0; + + if (booking.treatments && Array.isArray(booking.treatments) && booking.treatments.length > 0) { + // Use new treatments array + treatmentNames = booking.treatments.map(t => t.name).join(', '); + + duration = booking.treatments.reduce((sum, t) => sum + (t.duration || 0), 0); + totalPrice = booking.treatments.reduce((sum, t) => sum + (t.price || 0), 0); + + // Build detailed treatment list for description + treatmentDetails = booking.treatments + .map(t => `- ${t.name} (${t.duration} Min., ${t.price}€)`) + .join('\\n'); + + if (booking.treatments.length > 1) { + treatmentDetails += `\\n\\nGesamt: ${duration} Min., ${totalPrice.toFixed(2)}€`; + } + } else { + // Fallback to deprecated treatmentId for backward compatibility + const treatment = booking.treatmentId ? treatments.find(t => t.id === booking.treatmentId) : null; + treatmentNames = treatment?.name || 'Unbekannte Behandlung'; + duration = booking.bookedDurationMinutes || treatment?.duration || 60; + treatmentDetails = `Behandlung: ${treatmentNames}`; + if (treatment?.price) { + treatmentDetails += ` (${duration} Min., ${treatment.price}€)`; + } + } const startTime = formatDateTime(booking.appointmentDate, booking.appointmentTime); - const endTime = formatDateTime(booking.appointmentDate, - `${String(Math.floor((parseInt(booking.appointmentTime.split(':')[0]) * 60 + parseInt(booking.appointmentTime.split(':')[1]) + duration) / 60)).padStart(2, '0')}:${String((parseInt(booking.appointmentTime.split(':')[0]) * 60 + parseInt(booking.appointmentTime.split(':')[1]) + duration) % 60).padStart(2, '0')}` - ); + const endTimeStr = addMinutesToTime(booking.appointmentTime, duration); + const endTime = formatDateTime(booking.appointmentDate, endTimeStr); // UID für jeden Termin (eindeutig) const uid = `booking-${booking.id}@stargirlnails.de`; @@ -83,8 +120,8 @@ UID:${uid} DTSTAMP:${now} DTSTART:${startTime} DTEND:${endTime} -SUMMARY:${treatmentName} - ${booking.customerName} -DESCRIPTION:Behandlung: ${treatmentName}\\nKunde: ${booking.customerName}${booking.customerPhone ? `\\nTelefon: ${booking.customerPhone}` : ''}${booking.notes ? `\\nNotizen: ${booking.notes}` : ''} +SUMMARY:${treatmentNames} - ${booking.customerName} +DESCRIPTION:${treatmentDetails}\\n\\nKunde: ${booking.customerName}${booking.customerPhone ? `\\nTelefon: ${booking.customerPhone}` : ''}${booking.notes ? `\\nNotizen: ${booking.notes}` : ''} STATUS:${status} TRANSP:OPAQUE END:VEVENT