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:
Beto
2025-12-23 19:35:55 +00:00
committed by GitHub
co-authored by Dhairyashil Shinde
parent 75d8300635
commit 2abfae7f13
7 changed files with 470 additions and 358 deletions
+4 -1
View File
@@ -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} />
</>
);
}
-18
View File
@@ -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>
</>
);
}
+1 -1
View File
@@ -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 -1
View File
@@ -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"]
}