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 <dhairyashil10101010@gmail.com>
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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 (
|
||||
<Pressable onPress={openDatePicker} className="rounded-lg bg-[#F2F2F7] px-3 py-2">
|
||||
<Text className={`text-[15px] ${value ? "text-black" : "text-[#8E8E93]"}`}>
|
||||
{displayText}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
@@ -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<Date>(new Date());
|
||||
const [isUnavailable, setIsUnavailable] = useState(false);
|
||||
const [startTime, setStartTime] = useState<Date>(timeStringToDate("09:00"));
|
||||
const [endTime, setEndTime] = useState<Date>(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 (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-[#8E8E93]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className={`flex-1 ${backgroundStyle}`}
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
}}
|
||||
showsVerticalScrollIndicator={!transparentBackground}
|
||||
>
|
||||
{/* Date Picker */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Date</Text>
|
||||
<Pressable
|
||||
onPress={openDatePicker}
|
||||
className={`mb-4 rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="flex-row items-center">
|
||||
<View className="mr-3 h-9 w-9 items-center justify-center rounded-lg bg-[#007AFF]/10">
|
||||
<Ionicons name="calendar" size={20} color="#007AFF" />
|
||||
</View>
|
||||
<Text className="text-[17px] text-black">{formatDateForDisplay(selectedDate)}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
{/* Unavailable Toggle */}
|
||||
<View
|
||||
className={`mb-4 flex-row items-center justify-between rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<View className="mr-3 h-9 w-9 items-center justify-center rounded-lg bg-[#FF3B30]/10">
|
||||
<Ionicons name="moon-outline" size={20} color="#FF3B30" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Mark as unavailable</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Block this entire day</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={isUnavailable}
|
||||
onValueChange={setIsUnavailable}
|
||||
trackColor={{ false: "#E5E5EA", true: "#000000" }}
|
||||
thumbColor="#fff"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Time Range (only when available) */}
|
||||
{!isUnavailable && (
|
||||
<>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Available Hours</Text>
|
||||
<View
|
||||
className={`mb-4 rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<Pressable onPress={openStartTimePicker} className="flex-1">
|
||||
<Text className="mb-1 text-[13px] text-[#8E8E93]">Start Time</Text>
|
||||
<Text className="text-[17px] text-black">
|
||||
{formatTime12Hour(dateToTimeString(startTime))}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text className="mx-4 text-[17px] text-[#8E8E93]">–</Text>
|
||||
|
||||
<Pressable onPress={openEndTimePicker} className="flex-1">
|
||||
<Text className="mb-1 text-[13px] text-[#8E8E93]">End Time</Text>
|
||||
<Text className="text-[17px] text-black">
|
||||
{formatTime12Hour(dateToTimeString(endTime))}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Existing Overrides (if any) */}
|
||||
{!isEditing && schedule.overrides && schedule.overrides.length > 0 && (
|
||||
<>
|
||||
<Text className="mb-2 mt-4 px-1 text-[13px] font-medium text-[#8E8E93]">
|
||||
Existing Overrides
|
||||
</Text>
|
||||
<View
|
||||
className={`overflow-hidden rounded-xl ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
{schedule.overrides.map((override, index) => (
|
||||
<Pressable
|
||||
key={override.date}
|
||||
className={`flex-row items-center justify-between px-4 py-3 ${
|
||||
index > 0 ? "border-t border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[15px] text-black">
|
||||
{formatDateForDisplay(dateStringToDate(override.date ?? ""))}
|
||||
</Text>
|
||||
{normalizeTimeString(override.startTime ?? "00:00") === "00:00" &&
|
||||
normalizeTimeString(override.endTime ?? "00:00") === "00:00" ? (
|
||||
<Text className="text-[13px] text-[#FF3B30]">Unavailable</Text>
|
||||
) : (
|
||||
<Text className="text-[13px] text-[#8E8E93]">
|
||||
{formatTime12Hour(normalizeTimeString(override.startTime ?? "00:00"))} –{" "}
|
||||
{formatTime12Hour(normalizeTimeString(override.endTime ?? "00:00"))}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<Pressable
|
||||
className="rounded-full bg-[#FF3B30]/10 p-2"
|
||||
onPress={() => handleDeleteOverride(index)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
className="ml-3 rounded-full bg-[#8E8E93]/10 p-2"
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<Ionicons name="chevron-forward" size={18} color="#8E8E93" />
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
});
|
||||
|
||||
export default EditAvailabilityOverrideScreen;
|
||||
@@ -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<RescheduleScreenHandle, RescheduleScr
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [selectedDateTime, setSelectedDateTime] = useState<Date>(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<RescheduleScreenHandle, RescheduleScr
|
||||
);
|
||||
}, [booking, selectedDateTime, reason, onSuccess, isSaving, rescheduleBooking]);
|
||||
|
||||
const openDatePicker = useCallback(() => {
|
||||
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<RescheduleScreenHandle, RescheduleScr
|
||||
// Format time for display (12-hour)
|
||||
const formattedTime = formatTime12Hour(selectedDateTime);
|
||||
|
||||
// Generate date options for picker (next 90 days)
|
||||
const dateOptions = React.useMemo(() => {
|
||||
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<RescheduleScreenHandle, RescheduleScr
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<KeyboardAvoidingView behavior="height" className="flex-1 bg-[#F2F2F7]">
|
||||
{/* Header - only shown when not using native header */}
|
||||
{!useNativeHeader && (
|
||||
<KeyboardAvoidingView behavior="height" className="flex-1 bg-[#F2F2F7]">
|
||||
{/* Header - only shown when not using native header */}
|
||||
{!useNativeHeader && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E5EA",
|
||||
elevation: 2,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E5EA",
|
||||
elevation: 2,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
minHeight: 44,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
minHeight: 44,
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color="#000" />
|
||||
</AppPressable>
|
||||
<Ionicons name="chevron-back" size={24} color="#000" />
|
||||
</AppPressable>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#000",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
Reschedule
|
||||
</Text>
|
||||
|
||||
<AppPressable
|
||||
onPress={handleSubmit}
|
||||
disabled={isSaving}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
opacity: isSaving ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#000",
|
||||
marginHorizontal: 10,
|
||||
color: "#007AFF",
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
Reschedule
|
||||
Save
|
||||
</Text>
|
||||
|
||||
<AppPressable
|
||||
onPress={handleSubmit}
|
||||
disabled={isSaving}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
opacity: isSaving ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#007AFF",
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</View>
|
||||
</AppPressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* Booking Title Card */}
|
||||
<View className="mb-4 flex-row items-start rounded-xl bg-white p-4">
|
||||
<View className="mr-3 h-10 w-10 items-center justify-center rounded-full bg-[#E8E8ED]">
|
||||
<Ionicons name="calendar" size={20} color="#6B7280" />
|
||||
</View>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* Booking Title Card */}
|
||||
<View className="mb-4 flex-row items-start rounded-xl bg-white p-4">
|
||||
<View className="mr-3 h-10 w-10 items-center justify-center rounded-full bg-[#E8E8ED]">
|
||||
<Ionicons name="calendar" size={20} color="#6B7280" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[13px] font-medium text-gray-500">Rescheduling</Text>
|
||||
<Text className="mt-0.5 text-[17px] font-medium text-[#000]" numberOfLines={2}>
|
||||
{booking.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Form Card */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
{/* Date picker trigger */}
|
||||
<Pressable
|
||||
className="flex-row items-center justify-between border-b border-gray-100 px-4 py-3"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: pressed ? "#F9F9F9" : "transparent",
|
||||
})}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening date picker");
|
||||
openDatePicker();
|
||||
}}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[13px] font-medium text-gray-500">Rescheduling</Text>
|
||||
<Text className="mt-0.5 text-[17px] font-medium text-[#000]" numberOfLines={2}>
|
||||
{booking.title}
|
||||
</Text>
|
||||
<Text className="mb-1 text-[13px] font-medium text-gray-500">New Date</Text>
|
||||
<Text className="text-[17px] text-[#000]">{formattedDate}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</Pressable>
|
||||
|
||||
{/* Form Card */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
{/* Date picker trigger */}
|
||||
<Pressable
|
||||
className="flex-row items-center justify-between border-b border-gray-100 px-4 py-3"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: pressed ? "#F9F9F9" : "transparent",
|
||||
})}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening date picker");
|
||||
setShowDatePicker(true);
|
||||
}}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-[13px] font-medium text-gray-500">New Date</Text>
|
||||
<Text className="text-[17px] text-[#000]">{formattedDate}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</Pressable>
|
||||
|
||||
{/* Time picker trigger */}
|
||||
<Pressable
|
||||
className="flex-row items-center justify-between border-b border-gray-100 px-4 py-3"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: pressed ? "#F9F9F9" : "transparent",
|
||||
})}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening time picker");
|
||||
setShowTimePicker(true);
|
||||
}}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-[13px] font-medium text-gray-500">New Time</Text>
|
||||
<Text className="text-[17px] text-[#000]">{formattedTime}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</Pressable>
|
||||
|
||||
{/* Reason input */}
|
||||
<View className="px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">
|
||||
Reason (optional)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="min-h-[80px] text-[17px] text-[#000]"
|
||||
placeholder="Enter reason for rescheduling..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={reason}
|
||||
onChangeText={setReason}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
editable={!isSaving}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
{/* Date Picker Modal */}
|
||||
<Modal
|
||||
visible={showDatePicker}
|
||||
transparent
|
||||
animationType="slide"
|
||||
onRequestClose={() => setShowDatePicker(false)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
{/* Time picker trigger */}
|
||||
<Pressable
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onPress={() => setShowDatePicker(false)}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
maxHeight: "70%",
|
||||
paddingBottom: insets.bottom,
|
||||
className="flex-row items-center justify-between border-b border-gray-100 px-4 py-3"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: pressed ? "#F9F9F9" : "transparent",
|
||||
})}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening time picker");
|
||||
openTimePicker();
|
||||
}}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
|
||||
<Pressable onPress={() => setShowDatePicker(false)}>
|
||||
<Text className="text-[17px] text-gray-500">Cancel</Text>
|
||||
</Pressable>
|
||||
<Text className="text-[17px] font-semibold text-[#000]">Select Date</Text>
|
||||
<Pressable onPress={() => setShowDatePicker(false)}>
|
||||
<Text className="text-[17px] font-semibold text-[#007AFF]">Done</Text>
|
||||
</Pressable>
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-[13px] font-medium text-gray-500">New Time</Text>
|
||||
<Text className="text-[17px] text-[#000]">{formattedTime}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</Pressable>
|
||||
|
||||
{/* Date List */}
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
{dateOptions.map((option, index) => {
|
||||
const isSelected =
|
||||
option.value.toDateString() === selectedDateTime.toDateString();
|
||||
return (
|
||||
<Pressable
|
||||
key={option.value.toISOString()}
|
||||
className="flex-row items-center justify-between px-4 py-3.5"
|
||||
style={({ pressed }) => ({
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-semibold text-[#007AFF]" : "font-medium text-[#000]"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text
|
||||
className={`mt-0.5 text-[13px] ${isSelected ? "text-[#007AFF]" : "text-gray-500"}`}
|
||||
>
|
||||
{option.sublabel}
|
||||
</Text>
|
||||
</View>
|
||||
{isSelected && <Ionicons name="checkmark" size={22} color="#007AFF" />}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
{/* Reason input */}
|
||||
<View className="px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">
|
||||
Reason (optional)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="min-h-[80px] text-[17px] text-[#000]"
|
||||
placeholder="Enter reason for rescheduling..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={reason}
|
||||
onChangeText={setReason}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
editable={!isSaving}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Time Picker Modal */}
|
||||
<Modal
|
||||
visible={showTimePicker}
|
||||
transparent
|
||||
animationType="slide"
|
||||
onRequestClose={() => setShowTimePicker(false)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onPress={() => setShowTimePicker(false)}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
maxHeight: "60%",
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
|
||||
<Pressable onPress={() => setShowTimePicker(false)}>
|
||||
<Text className="text-[17px] text-gray-500">Cancel</Text>
|
||||
</Pressable>
|
||||
<Text className="text-[17px] font-semibold text-[#000]">Select Time</Text>
|
||||
<Pressable onPress={() => setShowTimePicker(false)}>
|
||||
<Text className="text-[17px] font-semibold text-[#007AFF]">Done</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Time Grid */}
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
{timeOptions.map((option) => {
|
||||
const isSelected =
|
||||
selectedDateTime.getHours() === option.value.hour &&
|
||||
selectedDateTime.getMinutes() === option.value.minute;
|
||||
return (
|
||||
<Pressable
|
||||
key={option.label}
|
||||
style={{ width: "25%", padding: 4 }}
|
||||
onPress={() => {
|
||||
const newDate = new Date(selectedDateTime);
|
||||
newDate.setHours(option.value.hour);
|
||||
newDate.setMinutes(option.value.minute);
|
||||
setSelectedDateTime(newDate);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className={`items-center justify-center rounded-lg py-3 ${
|
||||
isSelected ? "bg-[#007AFF]" : "bg-[#F2F2F7]"
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className={`text-[13px] ${
|
||||
isSelected ? "font-semibold text-white" : "font-medium text-[#000]"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user