feat(companion): Booking details screen navigation header improvements (#26163)
* feat(companion): restructure booking detail components and enhance type safety - Migrate BookingDetail component to a new file structure under (tabs)/(bookings) for better organization. - Introduce a new BookingDetailScreen with improved header and menu options. - Update app.json to enable typed routes and enhance TypeScript configuration in tsconfig.json. - Remove the old booking-detail.tsx file to clean up the codebase. - Adjust imports and routing to reflect the new structure, ensuring all components are properly linked. This refactor aims to improve maintainability and clarity in the booking detail flow. * add header options * refactor(booking-detail): rename "Standalone Actions" menu to "Danger Zone" for clarity --------- Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
This commit is contained in:
co-authored by
Dhairyashil Shinde
parent
75d8300635
commit
2abfae7f13
+4
-1
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export default function BookingsLayout() {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen name="index" options={{ headerShown: Platform.OS !== "ios" ? false : true }} />
|
||||
<Stack.Screen name="booking-detail" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerBackButtonDisplayMode: "minimal",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header style={{ shadowColor: "transparent" }}>
|
||||
<Stack.Header.Title>Booking Details</Stack.Header.Title>
|
||||
|
||||
{/* Header right and left API only works on iOS ATM see: https://docs.expo.dev/versions/unversioned/sdk/router/#stackheaderright */}
|
||||
<Stack.Header.Right>
|
||||
<Stack.Header.Menu>
|
||||
<Stack.Header.Label>Actions</Stack.Header.Label>
|
||||
<Stack.Header.Icon sf="ellipsis" />
|
||||
|
||||
{/* Edit Event Section */}
|
||||
<Stack.Header.Menu inline title="Edit Event">
|
||||
{bookingActionsSections.editEvent.map((action) => (
|
||||
<Stack.Header.MenuAction
|
||||
key={action.id}
|
||||
icon={action.icon}
|
||||
onPress={action.onPress}
|
||||
>
|
||||
{action.label}
|
||||
</Stack.Header.MenuAction>
|
||||
))}
|
||||
</Stack.Header.Menu>
|
||||
|
||||
{/* After Event Section */}
|
||||
<Stack.Header.Menu inline title="After Event">
|
||||
{bookingActionsSections.afterEvent.map((action) => (
|
||||
<Stack.Header.MenuAction
|
||||
key={action.id}
|
||||
icon={action.icon}
|
||||
onPress={action.onPress}
|
||||
>
|
||||
{action.label}
|
||||
</Stack.Header.MenuAction>
|
||||
))}
|
||||
</Stack.Header.Menu>
|
||||
|
||||
{/* Danger Zone Submenu */}
|
||||
<Stack.Header.Menu inline title="Danger Zone">
|
||||
{bookingActionsSections.standalone.map((action) => (
|
||||
<Stack.Header.MenuAction
|
||||
key={action.id}
|
||||
icon={action.icon}
|
||||
onPress={action.onPress}
|
||||
destructive
|
||||
>
|
||||
{action.label}
|
||||
</Stack.Header.MenuAction>
|
||||
))}
|
||||
</Stack.Header.Menu>
|
||||
</Stack.Header.Menu>
|
||||
</Stack.Header.Right>
|
||||
</Stack.Header>
|
||||
|
||||
<BookingDetailScreen uid={uid} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
<BookingDetailScreen uid={uid} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<View className="flex-1 bg-[#f8f9fa]">
|
||||
{/* Glass Header */}
|
||||
<GlassView
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 12,
|
||||
paddingTop: insets.top + 8,
|
||||
},
|
||||
]}
|
||||
glassEffectStyle="clear"
|
||||
>
|
||||
<View className="min-h-[44px] flex-row items-center justify-between">
|
||||
<AppPressable
|
||||
className="h-10 w-10 items-center justify-center rounded-full bg-[#F0F0F0]"
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color="#000" />
|
||||
</AppPressable>
|
||||
<>
|
||||
{/* This header right is used for non iOS platforms only */}
|
||||
{Platform.OS !== "ios" && (
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerRight: () => (
|
||||
<AppPressable
|
||||
className="h-10 w-10 items-center justify-center rounded-full"
|
||||
onPress={() => setShowActionsModal(true)}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={24} color="#000" />
|
||||
</AppPressable>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<View className="flex-1 bg-[#]">
|
||||
<ScrollView className="flex-1" contentContainerStyle={{ padding: 16, paddingBottom: 100 }}>
|
||||
{/* Title */}
|
||||
<View className="mb-3">
|
||||
<Text className="mb-2 text-2xl font-semibold text-[#333]">{booking.title}</Text>
|
||||
<Text className="text-base text-[#666]">
|
||||
{dateFormatted} {timeFormatted} ({timezone})
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
className="mx-2.5 flex-1 text-center text-lg font-semibold text-black"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{booking?.title || "Booking Details"}
|
||||
</Text>
|
||||
|
||||
<AppPressable
|
||||
className="h-10 w-10 items-center justify-center rounded-full bg-[#F0F0F0]"
|
||||
onPress={() => setShowActionsModal(true)}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={24} color="#000" />
|
||||
</AppPressable>
|
||||
</View>
|
||||
</GlassView>
|
||||
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
style={{ paddingTop: insets.top + 70 }}
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 100 }}
|
||||
>
|
||||
{/* Title */}
|
||||
<View className="mb-3">
|
||||
<Text className="mb-2 text-2xl font-semibold text-[#333]">{booking.title}</Text>
|
||||
<Text className="text-base text-[#666]">
|
||||
{dateFormatted} {timeFormatted} ({timezone})
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Who Section */}
|
||||
<View className="mb-2 rounded-2xl bg-white p-6">
|
||||
<Text className="mb-4 text-base font-medium text-[#666]">Who</Text>
|
||||
{/* Show host from user field or hosts array */}
|
||||
{booking.user || (booking.hosts && booking.hosts.length > 0) ? (
|
||||
<View className="mb-4">
|
||||
{booking.user ? (
|
||||
<View className="flex-row items-start">
|
||||
<View className="mr-3 h-12 w-12 items-center justify-center rounded-full bg-black">
|
||||
<Text className="text-base font-semibold text-white">
|
||||
{getInitials(booking.user.name)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<View className="mb-1 flex-row flex-wrap items-center">
|
||||
<Text className="text-base font-medium text-[#333]">{booking.user.name}</Text>
|
||||
<View className="ml-2 rounded bg-[#007AFF] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">host</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-sm text-[#666]">{booking.user.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : booking.hosts && booking.hosts.length > 0 ? (
|
||||
booking.hosts.map((host, hostIndex) => (
|
||||
<View
|
||||
key={hostIndex}
|
||||
className={`flex-row items-start ${hostIndex > 0 ? "mt-4" : ""}`}
|
||||
>
|
||||
{/* Who Section */}
|
||||
<View className="mb-2 rounded-2xl bg-white p-6">
|
||||
<Text className="mb-4 text-base font-medium text-[#666]">Who</Text>
|
||||
{/* Show host from user field or hosts array */}
|
||||
{booking.user || (booking.hosts && booking.hosts.length > 0) ? (
|
||||
<View className="mb-4">
|
||||
{booking.user ? (
|
||||
<View className="flex-row items-start">
|
||||
<View className="mr-3 h-12 w-12 items-center justify-center rounded-full bg-black">
|
||||
<Text className="text-base font-semibold text-white">
|
||||
{getInitials(host.name || "Host")}
|
||||
{getInitials(booking.user.name)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<View className="mb-1 flex-row flex-wrap items-center">
|
||||
<Text className="text-base font-medium text-[#333]">
|
||||
{host.name || "Host"}
|
||||
{booking.user.name}
|
||||
</Text>
|
||||
<View className="ml-2 rounded bg-[#007AFF] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">host</Text>
|
||||
</View>
|
||||
</View>
|
||||
{host.email && <Text className="text-sm text-[#666]">{host.email}</Text>}
|
||||
<Text className="text-sm text-[#666]">{booking.user.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
{booking.attendees && booking.attendees.length > 0 ? (
|
||||
<View>
|
||||
{booking.attendees.map((attendee, index) => (
|
||||
<View key={index} className={`flex-row items-start ${index > 0 ? "mt-4" : ""}`}>
|
||||
<View className="mr-3 h-12 w-12 items-center justify-center rounded-full bg-black">
|
||||
<Text className="text-base font-semibold text-white">
|
||||
{getInitials(attendee.name)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-base font-medium text-[#333]">{attendee.name}</Text>
|
||||
<Text className="text-sm text-[#666]">{attendee.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* Where Section */}
|
||||
{locationProvider ? (
|
||||
<View className="mb-2 rounded-2xl bg-white p-6">
|
||||
<Text className="mb-4 text-base font-medium text-[#666]">Where</Text>
|
||||
{locationProvider.url ? (
|
||||
<AppPressable onPress={handleJoinMeeting} className="flex-row flex-wrap items-center">
|
||||
{locationProvider.iconUrl ? (
|
||||
<SvgImage
|
||||
uri={locationProvider.iconUrl}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
) : booking.hosts && booking.hosts.length > 0 ? (
|
||||
booking.hosts.map((host, hostIndex) => (
|
||||
<View
|
||||
key={hostIndex}
|
||||
className={`flex-row items-start ${hostIndex > 0 ? "mt-4" : ""}`}
|
||||
>
|
||||
<View className="mr-3 h-12 w-12 items-center justify-center rounded-full bg-black">
|
||||
<Text className="text-base font-semibold text-white">
|
||||
{getInitials(host.name || "Host")}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<View className="mb-1 flex-row flex-wrap items-center">
|
||||
<Text className="text-base font-medium text-[#333]">
|
||||
{host.name || "Host"}
|
||||
</Text>
|
||||
<View className="ml-2 rounded bg-[#007AFF] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">host</Text>
|
||||
</View>
|
||||
</View>
|
||||
{host.email && <Text className="text-sm text-[#666]">{host.email}</Text>}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : null}
|
||||
<Text className="text-base text-[#007AFF]">{locationProvider.label}: </Text>
|
||||
<Text className="flex-1 text-base text-[#007AFF]" numberOfLines={1}>
|
||||
{locationProvider.url}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
) : (
|
||||
<View className="flex-row items-center">
|
||||
{locationProvider.iconUrl ? (
|
||||
<SvgImage
|
||||
uri={locationProvider.iconUrl}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
) : null}
|
||||
<Text className="text-base text-[#333]">{locationProvider.label}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Recurring Event Section */}
|
||||
{booking.recurringEventId ||
|
||||
(booking as { recurringBookingUid?: string }).recurringBookingUid ? (
|
||||
<View className="mb-2 rounded-2xl bg-white p-6">
|
||||
<Text className="mb-2 text-base font-medium text-[#666]">Recurring Event</Text>
|
||||
<Text className="text-base text-[#666]">Every 2 weeks for 6 occurrences</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Description Section */}
|
||||
{booking.description ? (
|
||||
<View className="mb-2 rounded-2xl bg-white p-6">
|
||||
<Text className="mb-2 text-base font-medium text-[#666]">Description</Text>
|
||||
<Text className="text-base leading-6 text-[#666]">{booking.description}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Join Meeting Button */}
|
||||
{locationProvider?.url ? (
|
||||
<AppPressable
|
||||
onPress={handleJoinMeeting}
|
||||
className="mb-2 flex-row items-center justify-center rounded-lg bg-black px-6 py-4"
|
||||
>
|
||||
{locationProvider.iconUrl ? (
|
||||
<SvgImage
|
||||
uri={locationProvider.iconUrl}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
) : null}
|
||||
<Text className="text-base font-semibold text-white">
|
||||
Join {locationProvider.label}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
{booking.attendees && booking.attendees.length > 0 ? (
|
||||
<View>
|
||||
{booking.attendees.map((attendee, index) => (
|
||||
<View key={index} className={`flex-row items-start ${index > 0 ? "mt-4" : ""}`}>
|
||||
<View className="mr-3 h-12 w-12 items-center justify-center rounded-full bg-black">
|
||||
<Text className="text-base font-semibold text-white">
|
||||
{getInitials(attendee.name)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-base font-medium text-[#333]">
|
||||
{attendee.name}
|
||||
</Text>
|
||||
<Text className="text-sm text-[#666]">{attendee.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* Booking Actions Modal */}
|
||||
<BookingActionsModal
|
||||
visible={showActionsModal}
|
||||
onClose={() => 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 ? (
|
||||
<View className="mb-2 rounded-2xl bg-white p-6">
|
||||
<Text className="mb-4 text-base font-medium text-[#666]">Where</Text>
|
||||
{locationProvider.url ? (
|
||||
<AppPressable
|
||||
onPress={handleJoinMeeting}
|
||||
className="flex-row flex-wrap items-center"
|
||||
>
|
||||
{locationProvider.iconUrl ? (
|
||||
<SvgImage
|
||||
uri={locationProvider.iconUrl}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
) : null}
|
||||
<Text className="text-base text-[#007AFF]">{locationProvider.label}: </Text>
|
||||
<Text className="flex-1 text-base text-[#007AFF]" numberOfLines={1}>
|
||||
{locationProvider.url}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
) : (
|
||||
<View className="flex-row items-center">
|
||||
{locationProvider.iconUrl ? (
|
||||
<SvgImage
|
||||
uri={locationProvider.iconUrl}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
) : null}
|
||||
<Text className="text-base text-[#333]">{locationProvider.label}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Recurring Event Section */}
|
||||
{booking.recurringEventId ||
|
||||
(booking as { recurringBookingUid?: string }).recurringBookingUid ? (
|
||||
<View className="mb-2 rounded-2xl bg-white p-6">
|
||||
<Text className="mb-2 text-base font-medium text-[#666]">Recurring Event</Text>
|
||||
<Text className="text-base text-[#666]">Every 2 weeks for 6 occurrences</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Description Section */}
|
||||
{booking.description ? (
|
||||
<View className="mb-2 rounded-2xl bg-white p-6">
|
||||
<Text className="mb-2 text-base font-medium text-[#666]">Description</Text>
|
||||
<Text className="text-base leading-6 text-[#666]">{booking.description}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Join Meeting Button */}
|
||||
{locationProvider?.url ? (
|
||||
<AppPressable
|
||||
onPress={handleJoinMeeting}
|
||||
className="mb-2 flex-row items-center justify-center rounded-lg bg-black px-6 py-4"
|
||||
>
|
||||
{locationProvider.iconUrl ? (
|
||||
<SvgImage
|
||||
uri={locationProvider.iconUrl}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
) : null}
|
||||
<Text className="text-base font-semibold text-white">
|
||||
Join {locationProvider.label}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
|
||||
{/* Booking Actions Modal */}
|
||||
<BookingActionsModal
|
||||
visible={showActionsModal}
|
||||
onClose={() => 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 */}
|
||||
<FullScreenModal
|
||||
visible={showRescheduleModal}
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowRescheduleModal(false)}
|
||||
>
|
||||
<AppPressable
|
||||
className="flex-1 items-center justify-center bg-black/50 p-2 md:p-4"
|
||||
onPress={() => setShowRescheduleModal(false)}
|
||||
{/* Reschedule Modal */}
|
||||
<FullScreenModal
|
||||
visible={showRescheduleModal}
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowRescheduleModal(false)}
|
||||
>
|
||||
<AppPressable
|
||||
className="w-[90%] max-w-[500px] rounded-2xl bg-white"
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
className="flex-1 items-center justify-center bg-black/50 p-2 md:p-4"
|
||||
onPress={() => setShowRescheduleModal(false)}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="px-8 pb-4 pt-6">
|
||||
<View className="flex-row items-start space-x-3">
|
||||
<View className="h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-[#F3F4F6]">
|
||||
<Ionicons name="calendar-outline" size={24} color="#111827" />
|
||||
</View>
|
||||
<View className="flex-1 pt-1">
|
||||
<Text className="mb-2 text-2xl font-semibold text-[#111827]">
|
||||
Reschedule Booking
|
||||
</Text>
|
||||
<Text className="text-sm text-[#6B7280]">
|
||||
Select a new date and time for this booking.
|
||||
</Text>
|
||||
<AppPressable
|
||||
className="w-[90%] max-w-[500px] rounded-2xl bg-white"
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="px-8 pb-4 pt-6">
|
||||
<View className="flex-row items-start space-x-3">
|
||||
<View className="h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-[#F3F4F6]">
|
||||
<Ionicons name="calendar-outline" size={24} color="#111827" />
|
||||
</View>
|
||||
<View className="flex-1 pt-1">
|
||||
<Text className="mb-2 text-2xl font-semibold text-[#111827]">
|
||||
Reschedule Booking
|
||||
</Text>
|
||||
<Text className="text-sm text-[#6B7280]">
|
||||
Select a new date and time for this booking.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollView className="max-h-[400px] px-8 pb-6">
|
||||
{/* Date Input */}
|
||||
<View className="mb-4">
|
||||
<Text className="mb-2 text-sm font-bold text-[#111827]">
|
||||
New Date
|
||||
<Text className="font-normal text-[#6B7280]"> (YYYY-MM-DD)</Text>
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-md border border-[#D1D5DB] bg-white px-3 py-3 text-base text-[#111827]"
|
||||
placeholder="2024-12-25"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={rescheduleDate}
|
||||
onChangeText={setRescheduleDate}
|
||||
editable={!rescheduling}
|
||||
keyboardType="default"
|
||||
/>
|
||||
</View>
|
||||
{/* Content */}
|
||||
<ScrollView className="max-h-[400px] px-8 pb-6">
|
||||
{/* Date Input */}
|
||||
<View className="mb-4">
|
||||
<Text className="mb-2 text-sm font-bold text-[#111827]">
|
||||
New Date
|
||||
<Text className="font-normal text-[#6B7280]"> (YYYY-MM-DD)</Text>
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-md border border-[#D1D5DB] bg-white px-3 py-3 text-base text-[#111827]"
|
||||
placeholder="2024-12-25"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={rescheduleDate}
|
||||
onChangeText={setRescheduleDate}
|
||||
editable={!rescheduling}
|
||||
keyboardType="default"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Time Input */}
|
||||
<View className="mb-4">
|
||||
<Text className="mb-2 text-sm font-bold text-[#111827]">
|
||||
New Time
|
||||
<Text className="font-normal text-[#6B7280]"> (HH:MM, 24-hour)</Text>
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-md border border-[#D1D5DB] bg-white px-3 py-3 text-base text-[#111827]"
|
||||
placeholder="14:30"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={rescheduleTime}
|
||||
onChangeText={setRescheduleTime}
|
||||
editable={!rescheduling}
|
||||
keyboardType="default"
|
||||
/>
|
||||
</View>
|
||||
{/* Time Input */}
|
||||
<View className="mb-4">
|
||||
<Text className="mb-2 text-sm font-bold text-[#111827]">
|
||||
New Time
|
||||
<Text className="font-normal text-[#6B7280]"> (HH:MM, 24-hour)</Text>
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-md border border-[#D1D5DB] bg-white px-3 py-3 text-base text-[#111827]"
|
||||
placeholder="14:30"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={rescheduleTime}
|
||||
onChangeText={setRescheduleTime}
|
||||
editable={!rescheduling}
|
||||
keyboardType="default"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Reason Input */}
|
||||
<View className="mb-2">
|
||||
<Text className="mb-2 text-sm font-bold text-[#111827]">
|
||||
Reason
|
||||
<Text className="font-normal text-[#6B7280]"> (Optional)</Text>
|
||||
</Text>
|
||||
<TextInput
|
||||
className="min-h-[80px] rounded-md border border-[#D1D5DB] bg-white px-3 py-3 text-base text-[#111827]"
|
||||
placeholder="Enter reason for rescheduling..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={rescheduleReason}
|
||||
onChangeText={setRescheduleReason}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
textAlignVertical="top"
|
||||
editable={!rescheduling}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
{/* Reason Input */}
|
||||
<View className="mb-2">
|
||||
<Text className="mb-2 text-sm font-bold text-[#111827]">
|
||||
Reason
|
||||
<Text className="font-normal text-[#6B7280]"> (Optional)</Text>
|
||||
</Text>
|
||||
<TextInput
|
||||
className="min-h-[80px] rounded-md border border-[#D1D5DB] bg-white px-3 py-3 text-base text-[#111827]"
|
||||
placeholder="Enter reason for rescheduling..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={rescheduleReason}
|
||||
onChangeText={setRescheduleReason}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
textAlignVertical="top"
|
||||
editable={!rescheduling}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Footer */}
|
||||
<View className="rounded-b-2xl border-t border-[#E5E7EB] bg-[#F9FAFB] px-8 py-4">
|
||||
<View className="flex-row justify-end gap-2 space-x-2">
|
||||
<AppPressable
|
||||
className="rounded-xl border border-[#D1D5DB] bg-white px-2 py-2 md:px-4"
|
||||
onPress={() => {
|
||||
setShowRescheduleModal(false);
|
||||
setRescheduleDate("");
|
||||
setRescheduleTime("");
|
||||
setRescheduleReason("");
|
||||
}}
|
||||
disabled={rescheduling}
|
||||
>
|
||||
<Text className="text-base font-medium text-[#374151]">Cancel</Text>
|
||||
</AppPressable>
|
||||
<AppPressable
|
||||
className={`rounded-xl bg-[#111827] px-2 py-2 md:px-4 ${
|
||||
rescheduling ? "opacity-60" : ""
|
||||
}`}
|
||||
onPress={handleReschedule}
|
||||
disabled={rescheduling}
|
||||
>
|
||||
{rescheduling ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Text className="text-base font-medium text-white">Reschedule</Text>
|
||||
)}
|
||||
</AppPressable>
|
||||
{/* Footer */}
|
||||
<View className="rounded-b-2xl border-t border-[#E5E7EB] bg-[#F9FAFB] px-8 py-4">
|
||||
<View className="flex-row justify-end gap-2 space-x-2">
|
||||
<AppPressable
|
||||
className="rounded-xl border border-[#D1D5DB] bg-white px-2 py-2 md:px-4"
|
||||
onPress={() => {
|
||||
setShowRescheduleModal(false);
|
||||
setRescheduleDate("");
|
||||
setRescheduleTime("");
|
||||
setRescheduleReason("");
|
||||
}}
|
||||
disabled={rescheduling}
|
||||
>
|
||||
<Text className="text-base font-medium text-[#374151]">Cancel</Text>
|
||||
</AppPressable>
|
||||
<AppPressable
|
||||
className={`rounded-xl bg-[#111827] px-2 py-2 md:px-4 ${
|
||||
rescheduling ? "opacity-60" : ""
|
||||
}`}
|
||||
onPress={handleReschedule}
|
||||
disabled={rescheduling}
|
||||
>
|
||||
{rescheduling ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Text className="text-base font-medium text-white">Reschedule</Text>
|
||||
)}
|
||||
</AppPressable>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
</AppPressable>
|
||||
</AppPressable>
|
||||
</FullScreenModal>
|
||||
</View>
|
||||
</FullScreenModal>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user