diff --git a/companion/app.json b/companion/app.json index e002c996a1..8fe55d6366 100644 --- a/companion/app.json +++ b/companion/app.json @@ -40,6 +40,9 @@ } }, "owner": "calcoms-organization", - "plugins": ["expo-router", "expo-secure-store", "expo-web-browser"] + "plugins": ["expo-router", "expo-secure-store", "expo-web-browser"], + "experiments": { + "typedRoutes": true + } } } diff --git a/companion/app/(tabs)/(bookings)/_layout.tsx b/companion/app/(tabs)/(bookings)/_layout.tsx index 4521f9ea72..44898bcd33 100644 --- a/companion/app/(tabs)/(bookings)/_layout.tsx +++ b/companion/app/(tabs)/(bookings)/_layout.tsx @@ -5,6 +5,7 @@ export default function BookingsLayout() { return ( + ); } diff --git a/companion/app/(tabs)/(bookings)/booking-detail.tsx b/companion/app/(tabs)/(bookings)/booking-detail.tsx new file mode 100644 index 0000000000..1a9d1e13f0 --- /dev/null +++ b/companion/app/(tabs)/(bookings)/booking-detail.tsx @@ -0,0 +1,137 @@ +import { BookingDetailScreen } from "../../../components/screens/BookingDetailScreen"; +import { Stack, useLocalSearchParams } from "expo-router"; +import React from "react"; +import { Alert } from "react-native"; + +export default function BookingDetail() { + const { uid } = useLocalSearchParams<{ uid: string }>(); + + if (!uid) { + return null; + } + + // Define booking actions organized by sections + const bookingActionsSections = { + editEvent: [ + { + id: "reschedule", + label: "Reschedule Booking", + icon: "calendar", + onPress: () => Alert.alert("Reschedule", "Reschedule functionality coming soon"), + }, + { + id: "edit-location", + label: "Edit Location", + icon: "location", + onPress: () => Alert.alert("Edit Location", "Edit location functionality coming soon"), + }, + { + id: "add-guests", + label: "Add Guests", + icon: "person.badge.plus", + onPress: () => Alert.alert("Add Guests", "Add guests functionality coming soon"), + }, + ], + afterEvent: [ + { + id: "view-recordings", + label: "View Recordings", + icon: "video", + onPress: () => Alert.alert("View Recordings", "View recordings functionality coming soon"), + }, + { + id: "session-details", + label: "Meeting Session Details", + icon: "info.circle", + onPress: () => + Alert.alert("Session Details", "Meeting session details functionality coming soon"), + }, + { + id: "mark-no-show", + label: "Mark as No-Show", + icon: "eye.slash", + onPress: () => Alert.alert("Mark No-Show", "Mark as no-show functionality coming soon"), + }, + ], + standalone: [ + { + id: "report", + label: "Report Booking", + icon: "flag", + onPress: () => Alert.alert("Report", "Report booking functionality coming soon"), + destructive: true, + }, + { + id: "cancel", + label: "Cancel Event", + icon: "xmark.circle", + onPress: () => Alert.alert("Cancel Event", "Cancel event functionality coming soon"), + destructive: true, + }, + ], + } as const; + + return ( + <> + + + + Booking Details + + {/* Header right and left API only works on iOS ATM see: https://docs.expo.dev/versions/unversioned/sdk/router/#stackheaderright */} + + + Actions + + + {/* Edit Event Section */} + + {bookingActionsSections.editEvent.map((action) => ( + + {action.label} + + ))} + + + {/* After Event Section */} + + {bookingActionsSections.afterEvent.map((action) => ( + + {action.label} + + ))} + + + {/* Danger Zone Submenu */} + + {bookingActionsSections.standalone.map((action) => ( + + {action.label} + + ))} + + + + + + + + ); +} diff --git a/companion/app/booking-detail.tsx b/companion/app/booking-detail.tsx deleted file mode 100644 index 146b2c3c5f..0000000000 --- a/companion/app/booking-detail.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { BookingDetailScreen } from "../components/screens/BookingDetailScreen"; -import { useLocalSearchParams, Stack } from "expo-router"; -import React from "react"; - -export default function BookingDetail() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - - if (!uid) { - return null; - } - - return ( - <> - - - - ); -} diff --git a/companion/components/screens/BookingDetailScreen.tsx b/companion/components/screens/BookingDetailScreen.tsx index 40fbc43102..3a4ec14afc 100644 --- a/companion/components/screens/BookingDetailScreen.tsx +++ b/companion/components/screens/BookingDetailScreen.tsx @@ -10,10 +10,18 @@ import { BookingActionsModal } from "../BookingActionsModal"; import { FullScreenModal } from "../FullScreenModal"; import { SvgImage } from "../SvgImage"; import { Ionicons } from "@expo/vector-icons"; -import { GlassView } from "expo-glass-effect"; -import { useRouter } from "expo-router"; +import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; +import { Stack, useRouter } from "expo-router"; import React, { useState, useEffect } from "react"; -import { View, Text, ScrollView, Alert, ActivityIndicator, TextInput } from "react-native"; +import { + View, + Text, + ScrollView, + Alert, + ActivityIndicator, + TextInput, + Platform, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; // Format date: "Tuesday, November 25, 2025" @@ -334,371 +342,351 @@ export function BookingDetailScreen({ uid }: BookingDetailScreenProps) { const locationProvider = getLocationProvider(booking.location, booking.responses); return ( - - {/* Glass Header */} - - - router.back()} - > - - + <> + {/* This header right is used for non iOS platforms only */} + {Platform.OS !== "ios" && ( + ( + setShowActionsModal(true)} + > + + + ), + }} + /> + )} + + + {/* Title */} + + {booking.title} + + {dateFormatted} {timeFormatted} ({timezone}) + + - - {booking?.title || "Booking Details"} - - - setShowActionsModal(true)} - > - - - - - - - {/* Title */} - - {booking.title} - - {dateFormatted} {timeFormatted} ({timezone}) - - - - {/* Who Section */} - - Who - {/* Show host from user field or hosts array */} - {booking.user || (booking.hosts && booking.hosts.length > 0) ? ( - - {booking.user ? ( - - - - {getInitials(booking.user.name)} - - - - - {booking.user.name} - - host - - - {booking.user.email} - - - ) : booking.hosts && booking.hosts.length > 0 ? ( - booking.hosts.map((host, hostIndex) => ( - 0 ? "mt-4" : ""}`} - > + {/* Who Section */} + + Who + {/* Show host from user field or hosts array */} + {booking.user || (booking.hosts && booking.hosts.length > 0) ? ( + + {booking.user ? ( + - {getInitials(host.name || "Host")} + {getInitials(booking.user.name)} - {host.name || "Host"} + {booking.user.name} host - {host.email && {host.email}} + {booking.user.email} - )) - ) : null} - - ) : null} - {booking.attendees && booking.attendees.length > 0 ? ( - - {booking.attendees.map((attendee, index) => ( - 0 ? "mt-4" : ""}`}> - - - {getInitials(attendee.name)} - - - - {attendee.name} - {attendee.email} - - - ))} - - ) : null} - - - {/* Where Section */} - {locationProvider ? ( - - Where - {locationProvider.url ? ( - - {locationProvider.iconUrl ? ( - + ) : booking.hosts && booking.hosts.length > 0 ? ( + booking.hosts.map((host, hostIndex) => ( + 0 ? "mt-4" : ""}`} + > + + + {getInitials(host.name || "Host")} + + + + + + {host.name || "Host"} + + + host + + + {host.email && {host.email}} + + + )) ) : null} - {locationProvider.label}: - - {locationProvider.url} - - - ) : ( - - {locationProvider.iconUrl ? ( - - ) : null} - {locationProvider.label} - )} - - ) : null} - - {/* Recurring Event Section */} - {booking.recurringEventId || - (booking as { recurringBookingUid?: string }).recurringBookingUid ? ( - - Recurring Event - Every 2 weeks for 6 occurrences - - ) : null} - - {/* Description Section */} - {booking.description ? ( - - Description - {booking.description} - - ) : null} - - {/* Join Meeting Button */} - {locationProvider?.url ? ( - - {locationProvider.iconUrl ? ( - ) : null} - - Join {locationProvider.label} - - - ) : null} - + {booking.attendees && booking.attendees.length > 0 ? ( + + {booking.attendees.map((attendee, index) => ( + 0 ? "mt-4" : ""}`}> + + + {getInitials(attendee.name)} + + + + + {attendee.name} + + {attendee.email} + + + ))} + + ) : null} + - {/* Booking Actions Modal */} - setShowActionsModal(false)} - booking={booking} - hasLocationUrl={!!locationProvider?.url} - isUpcoming={booking ? new Date(booking.startTime) > new Date() : false} - isPast={booking ? new Date(booking.startTime) <= new Date() : false} - isCancelled={booking?.status?.toUpperCase() === "CANCELLED"} - isUnconfirmed={booking?.status?.toUpperCase() === "PENDING"} - onReschedule={openRescheduleModal} - onEditLocation={() => { - Alert.alert("Edit Location", "Edit location functionality coming soon"); - }} - onAddGuests={() => { - Alert.alert("Add Guests", "Add guests functionality coming soon"); - }} - onViewRecordings={() => { - Alert.alert("View Recordings", "View recordings functionality coming soon"); - }} - onMeetingSessionDetails={() => { - Alert.alert( - "Meeting Session Details", - "Meeting session details functionality coming soon" - ); - }} - onMarkNoShow={() => { - Alert.alert("Mark as No-Show", "Mark as no-show functionality coming soon"); - }} - onReportBooking={() => { - Alert.alert("Report Booking", "Report booking functionality coming soon"); - }} - onCancelBooking={() => { - Alert.alert("Cancel Booking", "Are you sure you want to cancel this booking?", [ - { text: "No", style: "cancel" }, - { - text: "Yes, Cancel", - style: "destructive", - onPress: () => { - // TODO: Implement cancel booking - if (__DEV__) - console.debug("[BookingDetailScreen] cancel booking (not implemented yet)"); + {/* Where Section */} + {locationProvider ? ( + + Where + {locationProvider.url ? ( + + {locationProvider.iconUrl ? ( + + ) : null} + {locationProvider.label}: + + {locationProvider.url} + + + ) : ( + + {locationProvider.iconUrl ? ( + + ) : null} + {locationProvider.label} + + )} + + ) : null} + + {/* Recurring Event Section */} + {booking.recurringEventId || + (booking as { recurringBookingUid?: string }).recurringBookingUid ? ( + + Recurring Event + Every 2 weeks for 6 occurrences + + ) : null} + + {/* Description Section */} + {booking.description ? ( + + Description + {booking.description} + + ) : null} + + {/* Join Meeting Button */} + {locationProvider?.url ? ( + + {locationProvider.iconUrl ? ( + + ) : null} + + Join {locationProvider.label} + + + ) : null} + + + {/* Booking Actions Modal */} + setShowActionsModal(false)} + booking={booking} + hasLocationUrl={!!locationProvider?.url} + isUpcoming={booking ? new Date(booking.startTime) > new Date() : false} + isPast={booking ? new Date(booking.startTime) <= new Date() : false} + isCancelled={booking?.status?.toUpperCase() === "CANCELLED"} + isUnconfirmed={booking?.status?.toUpperCase() === "PENDING"} + onReschedule={openRescheduleModal} + onEditLocation={() => { + Alert.alert("Edit Location", "Edit location functionality coming soon"); + }} + onAddGuests={() => { + Alert.alert("Add Guests", "Add guests functionality coming soon"); + }} + onViewRecordings={() => { + Alert.alert("View Recordings", "View recordings functionality coming soon"); + }} + onMeetingSessionDetails={() => { + Alert.alert( + "Meeting Session Details", + "Meeting session details functionality coming soon" + ); + }} + onMarkNoShow={() => { + Alert.alert("Mark as No-Show", "Mark as no-show functionality coming soon"); + }} + onReportBooking={() => { + Alert.alert("Report Booking", "Report booking functionality coming soon"); + }} + onCancelBooking={() => { + Alert.alert("Cancel Booking", "Are you sure you want to cancel this booking?", [ + { text: "No", style: "cancel" }, + { + text: "Yes, Cancel", + style: "destructive", + onPress: () => { + // TODO: Implement cancel booking + if (__DEV__) + console.debug("[BookingDetailScreen] cancel booking (not implemented yet)"); + }, }, - }, - ]); - }} - /> + ]); + }} + /> - {/* Reschedule Modal */} - setShowRescheduleModal(false)} - > - setShowRescheduleModal(false)} + {/* Reschedule Modal */} + setShowRescheduleModal(false)} > e.stopPropagation()} - style={shadows.xl()} + className="flex-1 items-center justify-center bg-black/50 p-2 md:p-4" + onPress={() => setShowRescheduleModal(false)} > - {/* Header */} - - - - - - - - Reschedule Booking - - - Select a new date and time for this booking. - + e.stopPropagation()} + style={shadows.xl()} + > + {/* Header */} + + + + + + + + Reschedule Booking + + + Select a new date and time for this booking. + + - - {/* Content */} - - {/* Date Input */} - - - New Date - (YYYY-MM-DD) - - - + {/* Content */} + + {/* Date Input */} + + + New Date + (YYYY-MM-DD) + + + - {/* Time Input */} - - - New Time - (HH:MM, 24-hour) - - - + {/* Time Input */} + + + New Time + (HH:MM, 24-hour) + + + - {/* Reason Input */} - - - Reason - (Optional) - - - - + {/* Reason Input */} + + + Reason + (Optional) + + + + - {/* Footer */} - - - { - setShowRescheduleModal(false); - setRescheduleDate(""); - setRescheduleTime(""); - setRescheduleReason(""); - }} - disabled={rescheduling} - > - Cancel - - - {rescheduling ? ( - - ) : ( - Reschedule - )} - + {/* Footer */} + + + { + setShowRescheduleModal(false); + setRescheduleDate(""); + setRescheduleTime(""); + setRescheduleReason(""); + }} + disabled={rescheduling} + > + Cancel + + + {rescheduling ? ( + + ) : ( + Reschedule + )} + + - + - - - + + + ); } diff --git a/companion/hooks/useBookingActions.ts b/companion/hooks/useBookingActions.ts index ca4233e1ef..2226df686c 100644 --- a/companion/hooks/useBookingActions.ts +++ b/companion/hooks/useBookingActions.ts @@ -69,7 +69,7 @@ export const useBookingActions = ({ */ const handleBookingPress = (booking: Booking) => { router.push({ - pathname: "/booking-detail", + pathname: "/(tabs)/(bookings)/booking-detail", params: { uid: booking.uid }, }); }; diff --git a/companion/tsconfig.json b/companion/tsconfig.json index b365f86f06..871ce9c924 100644 --- a/companion/tsconfig.json +++ b/companion/tsconfig.json @@ -2,5 +2,6 @@ "compilerOptions": { "types": ["nativewind/types"] }, - "extends": "expo/tsconfig.base" + "extends": "expo/tsconfig.base", + "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] }