From d67bf45527fe0bee5d346db351339f16256e2197 Mon Sep 17 00:00:00 2001 From: Beto <43630417+betomoedano@users.noreply.github.com> Date: Tue, 20 Jan 2026 06:46:42 -0600 Subject: [PATCH] feat: integrate DateTimePickerAndroid for date/time selection (#26979) * feat: integrate DateTimePickerAndroid for date/time selection - Added @react-native-community/datetimepicker dependency for native date/time picker functionality. - Replaced modal-based date/time picker implementation with DateTimePickerAndroid for improved user experience. - Updated RescheduleScreen to utilize the new date/time picker methods, enhancing the selection process and ensuring better styling consistency. * feat: add LimitsTabDatePicker component for date selection - Introduced a new LimitsTabDatePicker component utilizing DateTimePickerAndroid for improved date selection on Android. - Implemented date parsing and formatting for user-friendly display. - Enhanced user interaction with a pressable element to open the date picker. * feat: add EditAvailabilityOverrideScreen for managing schedule overrides - Introduced EditAvailabilityOverrideScreen component to allow users to create and edit availability overrides. - Implemented date and time selection using DateTimePickerAndroid for improved user experience. - Added functionality to handle existing overrides, including editing and deleting options. - Enhanced user interaction with clear alerts for success and error states during save operations. * fixed lint warnings, info * address cubics comments --------- Co-authored-by: Dhairyashil --- companion/app.json | 21 + companion/bun.lock | 3 + .../tabs/LimitsTabDatePicker.android.tsx | 60 ++ ...EditAvailabilityOverrideScreen.android.tsx | 482 +++++++++++++++ .../screens/RescheduleScreen.android.tsx | 552 ++++++------------ companion/package.json | 1 + 6 files changed, 734 insertions(+), 385 deletions(-) create mode 100644 companion/components/event-type-detail/tabs/LimitsTabDatePicker.android.tsx create mode 100644 companion/components/screens/EditAvailabilityOverrideScreen.android.tsx diff --git a/companion/app.json b/companion/app.json index b29aff7f0d..6b2985f4fe 100644 --- a/companion/app.json +++ b/companion/app.json @@ -57,6 +57,27 @@ "image": "./assets/splash-icon.png", "imageWidth": 200 } + ], + [ + "@react-native-community/datetimepicker", + { + "android": { + "datePicker": { + "colorAccent": { + "light": "#000000" + }, + "textColorPrimary": { + "light": "#000000" + } + }, + "timePicker": { + "background": { "light": "#ffffff" }, + "numbersBackgroundColor": { "light": "#ffffff" }, + "headerBackground": { "light": "#000000" }, + "numbersSelectorColor": { "light": "#000000" } + } + } + } ] ], "experiments": { diff --git a/companion/bun.lock b/companion/bun.lock index a3b2de0953..d42c9d099b 100644 --- a/companion/bun.lock +++ b/companion/bun.lock @@ -7,6 +7,7 @@ "@expo/ui": "0.2.0-canary-20251230-fc48ddc", "@expo/vector-icons": "15.0.3", "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-community/datetimepicker": "8.4.4", "@react-native-community/netinfo": "11.4.1", "@react-native-segmented-control/segmented-control": "2.5.7", "@rn-primitives/alert-dialog": "1.2.0", @@ -548,6 +549,8 @@ "@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.2.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw=="], + "@react-native-community/datetimepicker": ["@react-native-community/datetimepicker@8.4.4", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": ">=52.0.0", "react": "*", "react-native": "*", "react-native-windows": "*" }, "optionalPeers": ["expo", "react-native-windows"] }, "sha512-bc4ZixEHxZC9/qf5gbdYvIJiLZ5CLmEsC3j+Yhe1D1KC/3QhaIfGDVdUcid0PdlSoGOSEq4VlB93AWyetEyBSQ=="], + "@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="], "@react-native-segmented-control/segmented-control": ["@react-native-segmented-control/segmented-control@2.5.7", "", { "peerDependencies": { "react": ">=16.0", "react-native": ">=0.62" } }, "sha512-l84YeVX8xAU3lvOJSvV4nK/NbGhIm2gBfveYolwaoCbRp+/SLXtc6mYrQmM9ScXNwU14mnzjQTpTHWl5YPnkzQ=="], diff --git a/companion/components/event-type-detail/tabs/LimitsTabDatePicker.android.tsx b/companion/components/event-type-detail/tabs/LimitsTabDatePicker.android.tsx new file mode 100644 index 0000000000..488f498180 --- /dev/null +++ b/companion/components/event-type-detail/tabs/LimitsTabDatePicker.android.tsx @@ -0,0 +1,60 @@ +import { useCallback, useMemo } from "react"; +import { Pressable, Text } from "react-native"; +import { DateTimePickerAndroid } from "@react-native-community/datetimepicker"; + +interface LimitsTabDatePickerProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; +} + +export function LimitsTabDatePicker({ value, onChange, placeholder }: LimitsTabDatePickerProps) { + // Parse value string to Date object + const dateValue = useMemo(() => { + if (!value) return new Date(); + const parsed = new Date(`${value}T00:00:00`); + return Number.isNaN(parsed.getTime()) ? new Date() : parsed; + }, [value]); + + // Format date for display (compact, human-readable) + const displayText = useMemo(() => { + if (!value) return placeholder || "Select date"; + return dateValue.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }); + }, [value, dateValue, placeholder]); + + // Handle date selection - convert to YYYY-MM-DD format + const handleDateChange = useCallback( + (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + onChange(`${year}-${month}-${day}`); + }, + [onChange] + ); + + // Open native Android date picker + const openDatePicker = useCallback(() => { + DateTimePickerAndroid.open({ + value: dateValue, + mode: "date", + onChange: (event, date) => { + if (event.type === "set" && date) { + handleDateChange(date); + } + }, + }); + }, [dateValue, handleDateChange]); + + return ( + + + {displayText} + + + ); +} diff --git a/companion/components/screens/EditAvailabilityOverrideScreen.android.tsx b/companion/components/screens/EditAvailabilityOverrideScreen.android.tsx new file mode 100644 index 0000000000..89acca0b1f --- /dev/null +++ b/companion/components/screens/EditAvailabilityOverrideScreen.android.tsx @@ -0,0 +1,482 @@ +import { Ionicons } from "@expo/vector-icons"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; +import { Alert, Pressable, ScrollView, Switch, Text, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { DateTimePickerAndroid } from "@react-native-community/datetimepicker"; +import { useUpdateSchedule } from "@/hooks/useSchedules"; +import type { Schedule } from "@/services/calcom"; +import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; + +// Convert 24-hour time to 12-hour format with AM/PM +const formatTime12Hour = (time24: string): string => { + const [hours, minutes] = time24.split(":"); + const hour = parseInt(hours, 10); + const min = minutes || "00"; + const period = hour >= 12 ? "PM" : "AM"; + const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + return `${hour12}:${min} ${period}`; +}; + +// Parse time string to Date object (for DatePicker) +const timeStringToDate = (timeStr: string): Date => { + const [hours, minutes] = timeStr.split(":"); + const date = new Date(); + date.setHours(parseInt(hours, 10)); + date.setMinutes(parseInt(minutes || "0", 10)); + date.setSeconds(0); + date.setMilliseconds(0); + return date; +}; + +// Convert Date to time string (HH:mm) +const dateToTimeString = (date: Date): string => { + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + return `${hours}:${minutes}`; +}; + +// Normalize time string to HH:mm format (API may return HH:mm:ss) +const normalizeTimeString = (time: string): string => { + return time.substring(0, 5); +}; + +// Convert Date to date string (YYYY-MM-DD) +const dateToDateString = (date: Date): string => { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + return `${year}-${month}-${day}`; +}; + +// Parse date string to Date object +const dateStringToDate = (dateStr: string): Date => { + const [year, month, day] = dateStr.split("-").map(Number); + return new Date(year, month - 1, day); +}; + +// Format date for display +const formatDateForDisplay = (date: Date): string => { + return date.toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }); +}; + +export interface EditAvailabilityOverrideScreenProps { + schedule: Schedule | null; + overrideIndex?: number; + onSuccess: () => void; + onSavingChange?: (isSaving: boolean) => void; + onEditOverride?: (index: number) => void; + transparentBackground?: boolean; +} + +export interface EditAvailabilityOverrideScreenHandle { + submit: () => void; +} + +export const EditAvailabilityOverrideScreen = forwardRef< + EditAvailabilityOverrideScreenHandle, + EditAvailabilityOverrideScreenProps +>(function EditAvailabilityOverrideScreen( + { + schedule, + overrideIndex, + onSuccess, + onSavingChange, + onEditOverride, + transparentBackground = false, + }, + ref +) { + const insets = useSafeAreaInsets(); + const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]"; + + // Use mutation hook for cache-synchronized updates + const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule(); + + const isEditing = overrideIndex !== undefined; + + const [selectedDate, setSelectedDate] = useState(new Date()); + const [isUnavailable, setIsUnavailable] = useState(false); + const [startTime, setStartTime] = useState(timeStringToDate("09:00")); + const [endTime, setEndTime] = useState(timeStringToDate("17:00")); + + // Initialize from existing override if editing + useEffect(() => { + if (schedule && isEditing && schedule.overrides) { + const override = schedule.overrides[overrideIndex]; + if (override) { + setSelectedDate(dateStringToDate(override.date ?? "")); + // Normalize time strings to HH:mm (API may return HH:mm:ss) + const start = normalizeTimeString(override.startTime ?? "00:00"); + const end = normalizeTimeString(override.endTime ?? "00:00"); + setIsUnavailable(start === "00:00" && end === "00:00"); + setStartTime(timeStringToDate(start === "00:00" ? "09:00" : start)); + setEndTime(timeStringToDate(end === "00:00" ? "17:00" : end)); + } + } + }, [schedule, isEditing, overrideIndex]); + + // Notify parent of saving state + useEffect(() => { + onSavingChange?.(isMutating); + }, [isMutating, onSavingChange]); + + // Open native Android date picker + const openDatePicker = useCallback(() => { + DateTimePickerAndroid.open({ + value: selectedDate, + mode: "date", + onChange: (event, date) => { + if (event.type === "set" && date) { + setSelectedDate(date); + } + }, + }); + }, [selectedDate]); + + // Open native Android time picker for start time + const openStartTimePicker = useCallback(() => { + DateTimePickerAndroid.open({ + value: startTime, + mode: "time", + is24Hour: false, + onChange: (event, date) => { + if (event.type === "set" && date) { + setStartTime(date); + } + }, + }); + }, [startTime]); + + // Open native Android time picker for end time + const openEndTimePicker = useCallback(() => { + DateTimePickerAndroid.open({ + value: endTime, + mode: "time", + is24Hour: false, + onChange: (event, date) => { + if (event.type === "set" && date) { + setEndTime(date); + } + }, + }); + }, [endTime]); + + const saveOverrides = useCallback( + ( + newOverrides: { date: string; startTime: string; endTime: string }[], + successMessage: string + ) => { + if (!schedule) return; + + updateSchedule( + { id: schedule.id, updates: { overrides: newOverrides } }, + { + onSuccess: () => { + showSuccessAlert("Success", successMessage); + onSuccess(); + }, + onError: () => { + showErrorAlert("Error", "Failed to save override. Please try again."); + }, + } + ); + }, + [schedule, onSuccess, updateSchedule] + ); + + const handleDeleteOverride = useCallback( + (indexToDelete: number) => { + if (!schedule || !schedule.overrides) return; + + const override = schedule.overrides[indexToDelete]; + const dateDisplay = formatDateForDisplay(dateStringToDate(override?.date ?? "")); + + Alert.alert( + "Delete Override", + `Are you sure you want to delete the override for ${dateDisplay}?`, + [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: async () => { + const newOverrides = schedule.overrides + ? schedule.overrides + .filter((_, idx) => idx !== indexToDelete) + .map((o) => ({ + date: o.date ?? "", + startTime: (o.startTime ?? "00:00").substring(0, 5), + endTime: (o.endTime ?? "00:00").substring(0, 5), + })) + : []; + await saveOverrides(newOverrides, "Override deleted successfully"); + }, + }, + ] + ); + }, + [schedule, saveOverrides] + ); + + const handleSubmit = useCallback(() => { + if (!schedule || isMutating) return; + + const dateStr = dateToDateString(selectedDate); + + // Validate end time is after start time (only when not marking as unavailable) + // Compare time strings to avoid issues with Date object day components + const startTimeStr = dateToTimeString(startTime); + const endTimeStr = dateToTimeString(endTime); + if (!isUnavailable && endTimeStr <= startTimeStr) { + showErrorAlert("Error", "End time must be after start time"); + return; + } + + // Build the new override + const newOverride = { + date: dateStr, + startTime: isUnavailable ? "00:00" : dateToTimeString(startTime), + endTime: isUnavailable ? "00:00" : dateToTimeString(endTime), + }; + + // Build new overrides array + let newOverrides: { date: string; startTime: string; endTime: string }[] = []; + + if (schedule.overrides && Array.isArray(schedule.overrides)) { + newOverrides = schedule.overrides.map((o) => ({ + date: o.date ?? "", + startTime: (o.startTime ?? "00:00").substring(0, 5), + endTime: (o.endTime ?? "00:00").substring(0, 5), + })); + } + + const successMessage = isEditing + ? "Override updated successfully" + : "Override added successfully"; + + if (isEditing && overrideIndex !== undefined) { + // Check if date was changed to one that already exists (exclude current override) + const existingIndex = newOverrides.findIndex( + (o, idx) => o.date === dateStr && idx !== overrideIndex + ); + if (existingIndex >= 0) { + Alert.alert( + "Date Already Exists", + "An override for this date already exists. Do you want to replace it?", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Replace", + onPress: () => { + // Remove the conflicting override and update the current one + const filteredOverrides = newOverrides.filter((_, idx) => idx !== existingIndex); + // Adjust index if the removed item was before our edit index + const adjustedIndex = + existingIndex < overrideIndex ? overrideIndex - 1 : overrideIndex; + filteredOverrides[adjustedIndex] = newOverride; + saveOverrides(filteredOverrides, "Override updated successfully"); + }, + }, + ] + ); + return; + } + // Update existing override + newOverrides[overrideIndex] = newOverride; + } else { + // Check if date already exists + const existingIndex = newOverrides.findIndex((o) => o.date === dateStr); + if (existingIndex >= 0) { + Alert.alert( + "Date Already Exists", + "An override for this date already exists. Do you want to replace it?", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Replace", + onPress: () => { + newOverrides[existingIndex] = newOverride; + saveOverrides(newOverrides, "Override replaced successfully"); + }, + }, + ] + ); + return; + } + // Add new override + newOverrides.push(newOverride); + } + + saveOverrides(newOverrides, successMessage); + }, [ + schedule, + selectedDate, + isUnavailable, + startTime, + endTime, + isEditing, + overrideIndex, + saveOverrides, + isMutating, + ]); + + // Expose submit to parent via ref + useImperativeHandle( + ref, + () => ({ + submit: handleSubmit, + }), + [handleSubmit] + ); + + if (!schedule) { + return ( + + No schedule data + + ); + } + + return ( + + {/* Date Picker */} + Date + + + + + + + {formatDateForDisplay(selectedDate)} + + + + + + {/* Unavailable Toggle */} + + + + + + + Mark as unavailable + Block this entire day + + + + + + {/* Time Range (only when available) */} + {!isUnavailable && ( + <> + Available Hours + + + + Start Time + + {formatTime12Hour(dateToTimeString(startTime))} + + + + – + + + End Time + + {formatTime12Hour(dateToTimeString(endTime))} + + + + + + )} + + {/* Existing Overrides (if any) */} + {!isEditing && schedule.overrides && schedule.overrides.length > 0 && ( + <> + + Existing Overrides + + + {schedule.overrides.map((override, index) => ( + 0 ? "border-t border-[#E5E5EA]" : "" + }`} + onPress={() => onEditOverride?.(index)} + > + + + {formatDateForDisplay(dateStringToDate(override.date ?? ""))} + + {normalizeTimeString(override.startTime ?? "00:00") === "00:00" && + normalizeTimeString(override.endTime ?? "00:00") === "00:00" ? ( + Unavailable + ) : ( + + {formatTime12Hour(normalizeTimeString(override.startTime ?? "00:00"))} –{" "} + {formatTime12Hour(normalizeTimeString(override.endTime ?? "00:00"))} + + )} + + + handleDeleteOverride(index)} + > + + + onEditOverride?.(index)} + > + + + + + ))} + + + )} + + ); +}); + +export default EditAvailabilityOverrideScreen; diff --git a/companion/components/screens/RescheduleScreen.android.tsx b/companion/components/screens/RescheduleScreen.android.tsx index 6e8fddf006..992b4fd98e 100644 --- a/companion/components/screens/RescheduleScreen.android.tsx +++ b/companion/components/screens/RescheduleScreen.android.tsx @@ -1,17 +1,16 @@ /** * RescheduleScreen Component (Android) * - * Android-specific implementation with modal-based date/time picker. - * Uses custom modal dialogs with Material Design 3 inspired styling. + * Android-specific implementation using the DateTimePickerAndroid imperative API + * for native Material Design date/time picker dialogs. */ import { Ionicons } from "@expo/vector-icons"; import { useRouter } from "expo-router"; -import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; import { Alert, KeyboardAvoidingView, - Modal, Pressable, ScrollView, Text, @@ -24,6 +23,7 @@ import { useRescheduleBooking } from "@/hooks/useBookings"; import type { Booking } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; import { safeLogError, safeLogInfo } from "@/utils/safeLogger"; +import { DateTimePickerAndroid } from "@react-native-community/datetimepicker"; export interface RescheduleScreenProps { booking: Booking | null; @@ -50,8 +50,6 @@ export const RescheduleScreen = forwardRef(new Date()); - const [showDatePicker, setShowDatePicker] = useState(false); - const [showTimePicker, setShowTimePicker] = useState(false); const [reason, setReason] = useState(""); // Use React Query mutation for automatic cache invalidation @@ -100,6 +98,40 @@ export const RescheduleScreen = forwardRef { + DateTimePickerAndroid.open({ + value: selectedDateTime, + mode: "date", + onChange: (event, date) => { + if (event.type === "set" && date) { + // Preserve the current time when changing date + const newDate = new Date(date); + newDate.setHours(selectedDateTime.getHours()); + newDate.setMinutes(selectedDateTime.getMinutes()); + setSelectedDateTime(newDate); + } + }, + minimumDate: new Date(), // Prevent selecting past dates + }); + }, [selectedDateTime]); + + const openTimePicker = useCallback(() => { + DateTimePickerAndroid.open({ + value: selectedDateTime, + mode: "time", + is24Hour: false, // Match current 12-hour format + onChange: (event, date) => { + if (event.type === "set" && date) { + // Preserve the current date when changing time + const newDate = new Date(selectedDateTime); + newDate.setHours(date.getHours()); + newDate.setMinutes(date.getMinutes()); + setSelectedDateTime(newDate); + } + }, + }); + }, [selectedDateTime]); + // Format date for display const formattedDate = selectedDateTime.toLocaleDateString(undefined, { weekday: "short", @@ -111,73 +143,6 @@ export const RescheduleScreen = forwardRef { - const options: { label: string; sublabel: string; value: Date }[] = []; - const today = new Date(); - for (let i = 0; i < 90; i++) { - const date = new Date(today); - date.setDate(today.getDate() + i); - const isToday = i === 0; - const isTomorrow = i === 1; - options.push({ - label: isToday - ? "Today" - : isTomorrow - ? "Tomorrow" - : date.toLocaleDateString(undefined, { - weekday: "long", - }), - sublabel: date.toLocaleDateString(undefined, { - month: "short", - day: "numeric", - year: "numeric", - }), - value: date, - }); - } - return options; - }, []); - - // Extract booking times for stable dependency references - const bookingStartTime = booking?.startTime; - const bookingEndTime = booking?.endTime; - - // Calculate booking duration in minutes - const bookingDurationMinutes = React.useMemo(() => { - if (!bookingStartTime || !bookingEndTime) return 30; // Default to 30 min - const start = new Date(bookingStartTime); - const end = new Date(bookingEndTime); - const durationMs = end.getTime() - start.getTime(); - const durationMin = Math.round(durationMs / (1000 * 60)); - // Ensure slot interval is reasonable (minimum 5 min, maximum 60 min) - if (durationMin < 5) return 5; - if (durationMin > 60) return 60; - return durationMin; - }, [bookingStartTime, bookingEndTime]); - - // Generate time options based on booking duration - const timeOptions = React.useMemo(() => { - const options: { - label: string; - value: { hour: number; minute: number }; - }[] = []; - const interval = bookingDurationMinutes; - - for (let totalMinutes = 0; totalMinutes < 24 * 60; totalMinutes += interval) { - const hour = Math.floor(totalMinutes / 60); - const minute = totalMinutes % 60; - const ampm = hour >= 12 ? "PM" : "AM"; - const displayHour = hour % 12 || 12; - const time = `${displayHour}:${String(minute).padStart(2, "0")} ${ampm}`; - options.push({ - label: time, - value: { hour, minute }, - }); - } - return options; - }, [bookingDurationMinutes]); - useImperativeHandle( ref, () => ({ @@ -195,344 +160,161 @@ export const RescheduleScreen = forwardRef - - {/* Header - only shown when not using native header */} - {!useNativeHeader && ( + + {/* Header - only shown when not using native header */} + {!useNativeHeader && ( + - router.back()} style={{ - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - minHeight: 44, + width: 40, + height: 40, + alignItems: "flex-start", + justifyContent: "center", }} > - router.back()} - style={{ - width: 40, - height: 40, - alignItems: "flex-start", - justifyContent: "center", - }} - > - - + + + + Reschedule + + + - Reschedule + Save - - - - Save - - - + - )} + + )} - - {/* Booking Title Card */} - - - - + + {/* Booking Title Card */} + + + + + + Rescheduling + + {booking.title} + + + + + {/* Form Card */} + + {/* Date picker trigger */} + ({ + backgroundColor: pressed ? "#F9F9F9" : "transparent", + })} + onPress={() => { + safeLogInfo("[RescheduleScreen] Opening date picker"); + openDatePicker(); + }} + disabled={isSaving} + > - Rescheduling - - {booking.title} - + New Date + {formattedDate} - + + - {/* Form Card */} - - {/* Date picker trigger */} - ({ - backgroundColor: pressed ? "#F9F9F9" : "transparent", - })} - onPress={() => { - safeLogInfo("[RescheduleScreen] Opening date picker"); - setShowDatePicker(true); - }} - disabled={isSaving} - > - - New Date - {formattedDate} - - - - - {/* Time picker trigger */} - ({ - backgroundColor: pressed ? "#F9F9F9" : "transparent", - })} - onPress={() => { - safeLogInfo("[RescheduleScreen] Opening time picker"); - setShowTimePicker(true); - }} - disabled={isSaving} - > - - New Time - {formattedTime} - - - - - {/* Reason input */} - - - Reason (optional) - - - - - - - - {/* Date Picker Modal */} - setShowDatePicker(false)} - > - + {/* Time picker trigger */} setShowDatePicker(false)} - /> - ({ + backgroundColor: pressed ? "#F9F9F9" : "transparent", + })} + onPress={() => { + safeLogInfo("[RescheduleScreen] Opening time picker"); + openTimePicker(); }} + disabled={isSaving} > - {/* Header */} - - setShowDatePicker(false)}> - Cancel - - Select Date - setShowDatePicker(false)}> - Done - + + New Time + {formattedTime} + + - {/* Date List */} - - {dateOptions.map((option, index) => { - const isSelected = - option.value.toDateString() === selectedDateTime.toDateString(); - return ( - ({ - backgroundColor: isSelected - ? "rgba(0, 122, 255, 0.1)" - : pressed - ? "#F5F5F5" - : "#fff", - borderBottomWidth: index < dateOptions.length - 1 ? 1 : 0, - borderBottomColor: "#F2F2F7", - })} - onPress={() => { - const newDate = new Date(option.value); - newDate.setHours(selectedDateTime.getHours()); - newDate.setMinutes(selectedDateTime.getMinutes()); - setSelectedDateTime(newDate); - }} - > - - - {option.label} - - - {option.sublabel} - - - {isSelected && } - - ); - })} - + {/* Reason input */} + + + Reason (optional) + + - - - {/* Time Picker Modal */} - setShowTimePicker(false)} - > - - setShowTimePicker(false)} - /> - - {/* Header */} - - setShowTimePicker(false)}> - Cancel - - Select Time - setShowTimePicker(false)}> - Done - - - - {/* Time Grid */} - - {timeOptions.map((option) => { - const isSelected = - selectedDateTime.getHours() === option.value.hour && - selectedDateTime.getMinutes() === option.value.minute; - return ( - { - const newDate = new Date(selectedDateTime); - newDate.setHours(option.value.hour); - newDate.setMinutes(option.value.minute); - setSelectedDateTime(newDate); - }} - > - - - {option.label} - - - - ); - })} - - - - - + + ); } ); diff --git a/companion/package.json b/companion/package.json index 8ed90cefd0..7c9b9cf669 100644 --- a/companion/package.json +++ b/companion/package.json @@ -52,6 +52,7 @@ "@expo/ui": "0.2.0-canary-20251230-fc48ddc", "@expo/vector-icons": "15.0.3", "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-community/datetimepicker": "8.4.4", "@react-native-community/netinfo": "11.4.1", "@react-native-segmented-control/segmented-control": "2.5.7", "@rn-primitives/alert-dialog": "1.2.0",