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",