Refactor: Verbessere CalDAV und Bookings für Multi-Treatment-Support
- 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
This commit is contained in:
@@ -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<any | null>(null);
|
||||
const [rescheduleProposal, setRescheduleProposal] = useState<RescheduleProposalDetails | null>(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<RescheduleProposalDetails>({
|
||||
...queryClient.cancellation.getRescheduleProposal.queryOptions({ input: { token } }),
|
||||
enabled: !!token && (!!bookingError || !booking),
|
||||
});
|
||||
@@ -311,12 +355,56 @@ export default function BookingStatusPage({ token }: BookingStatusPageProps) {
|
||||
<div className="border rounded-lg p-4 bg-gray-50">
|
||||
<div className="text-sm text-gray-500 font-semibold mb-1">Aktueller Termin</div>
|
||||
<div className="text-gray-900 font-medium">{rescheduleProposal.original.date} um {rescheduleProposal.original.time} Uhr</div>
|
||||
<div className="text-gray-700 text-sm">{rescheduleProposal.booking.treatmentName}</div>
|
||||
<div className="text-gray-700 text-sm mt-2">
|
||||
{rescheduleProposal.booking.treatments && rescheduleProposal.booking.treatments.length > 0 ? (
|
||||
<>
|
||||
{rescheduleProposal.booking.treatments.length <= 2 ? (
|
||||
rescheduleProposal.booking.treatments.map((t, i) => (
|
||||
<div key={i}>{t.name}</div>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{rescheduleProposal.booking.treatments.slice(0, 2).map((t, i) => (
|
||||
<div key={i}>{t.name}</div>
|
||||
))}
|
||||
<div className="text-gray-500 italic">+{rescheduleProposal.booking.treatments.length - 2} weitere</div>
|
||||
</>
|
||||
)}
|
||||
<div className="text-gray-600 mt-1 text-xs">
|
||||
{rescheduleProposal.booking.totalDuration} Min
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">Keine Behandlungen</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4 bg-orange-50">
|
||||
<div className="text-sm text-orange-700 font-semibold mb-1">Neuer Vorschlag</div>
|
||||
<div className="text-gray-900 font-medium">{rescheduleProposal.proposed.date} um {rescheduleProposal.proposed.time} Uhr</div>
|
||||
<div className="text-gray-700 text-sm">{rescheduleProposal.booking.treatmentName}</div>
|
||||
<div className="text-gray-700 text-sm mt-2">
|
||||
{rescheduleProposal.booking.treatments && rescheduleProposal.booking.treatments.length > 0 ? (
|
||||
<>
|
||||
{rescheduleProposal.booking.treatments.length <= 2 ? (
|
||||
rescheduleProposal.booking.treatments.map((t, i) => (
|
||||
<div key={i}>{t.name}</div>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{rescheduleProposal.booking.treatments.slice(0, 2).map((t, i) => (
|
||||
<div key={i}>{t.name}</div>
|
||||
))}
|
||||
<div className="text-gray-500 italic">+{rescheduleProposal.booking.treatments.length - 2} weitere</div>
|
||||
</>
|
||||
)}
|
||||
<div className="text-gray-600 mt-1 text-xs">
|
||||
{rescheduleProposal.booking.totalDuration} Min
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">Keine Behandlungen</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-800">
|
||||
@@ -478,20 +566,44 @@ export default function BookingStatusPage({ token }: BookingStatusPageProps) {
|
||||
<span className="text-gray-600">Uhrzeit:</span>
|
||||
<span className="font-medium text-gray-900">{booking?.appointmentTime} Uhr</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-100">
|
||||
<span className="text-gray-600">Behandlung:</span>
|
||||
<span className="font-medium text-gray-900">{booking?.treatmentName}</span>
|
||||
|
||||
{/* Treatments List */}
|
||||
<div className="py-2 border-b border-gray-100">
|
||||
<div className="text-gray-600 mb-2">Behandlungen:</div>
|
||||
{booking?.treatments && booking.treatments.length > 0 ? (
|
||||
<div className="bg-gray-50 rounded-lg p-3 space-y-2">
|
||||
{booking.treatments.map((treatment, index) => (
|
||||
<div key={index} className="flex justify-between items-center text-sm">
|
||||
<span className="font-medium text-gray-900">• {treatment.name}</span>
|
||||
<span className="text-gray-600">
|
||||
{treatment.duration} Min - {treatment.price.toFixed(2)} €
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-between items-center pt-2 mt-2 border-t border-gray-200 font-semibold">
|
||||
<span className="text-gray-900">Gesamt:</span>
|
||||
<span className="text-gray-900">
|
||||
{booking.totalDuration} Min - {booking.totalPrice.toFixed(2)} €
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<span className="text-gray-400 text-sm italic">Keine Behandlungen angegeben</span>
|
||||
{((booking?.totalDuration ?? 0) > 0 || (booking?.totalPrice ?? 0) > 0) && (
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex justify-between items-center font-semibold text-sm">
|
||||
<span className="text-gray-900">Gesamt:</span>
|
||||
<span className="text-gray-900">
|
||||
{booking?.totalDuration ?? 0} Min - {(booking?.totalPrice ?? 0).toFixed(2)} €
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-100">
|
||||
<span className="text-gray-600">Dauer:</span>
|
||||
<span className="font-medium text-gray-900">{booking?.treatmentDuration} Minuten</span>
|
||||
</div>
|
||||
{booking?.treatmentPrice && booking.treatmentPrice > 0 && (
|
||||
<div className="flex justify-between py-2 border-b border-gray-100">
|
||||
<span className="text-gray-600">Preis:</span>
|
||||
<span className="font-medium text-gray-900">{booking.treatmentPrice.toFixed(2)} €</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{booking?.hoursUntilAppointment && booking.hoursUntilAppointment > 0 && booking.status !== "cancelled" && booking.status !== "completed" && (
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-gray-600">Verbleibende Zeit:</span>
|
||||
|
Reference in New Issue
Block a user