feat(companion): availability detail page for android and extension (#26560)
* Availability Detail Page Header and fix event type payment amount badge, and index ui bug bottom navbar * feat(companion): implement read-only availability detail page for Android/Extension - Convert AvailabilityDetailScreen.tsx to read-only display with expandable sections - Add header dropdown menu with edit options in availability-detail.tsx - Update edit-availability-name.tsx with modal presentation pattern - Update edit-availability-hours.tsx with modal presentation pattern - Update edit-availability-override.tsx with modal presentation pattern - Match iOS flow: read-only detail page with edit actions via header menu * fix header on Availability Detail screen for android * make dialoges center on extension * update padding * AvailabilityDetailScreen for android and extension * added click to modal feat * fix Date Overrides Card was always shown --------- Co-authored-by: Volnei Munhoz <volnei@cal.com>
This commit is contained in:
1 parent
a748a75b08
commit
84d1ce5bff
17 files changed
+941
-1210
No files matched your search
@@ -5,7 +5,7 @@ export default function AvailabilityLayout() {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="availability-detail" options={{ headerShown: Platform.OS === "ios" }} />
|
||||
<Stack.Screen name="availability-detail" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +1,127 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Text, TouchableOpacity } from "react-native";
|
||||
import {
|
||||
AvailabilityDetailScreen,
|
||||
type AvailabilityDetailScreenHandle,
|
||||
} from "@/components/screens/AvailabilityDetailScreen";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
// Type for action handlers exposed by AvailabilityDetailScreen
|
||||
type ActionHandlers = {
|
||||
handleSetAsDefault: () => void;
|
||||
handleDelete: () => void;
|
||||
};
|
||||
|
||||
export default function AvailabilityDetail() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
|
||||
// Ref to store action handlers from AvailabilityDetailScreen
|
||||
const actionHandlersRef = useRef<ActionHandlers | null>(null);
|
||||
const screenRef = useRef<AvailabilityDetailScreenHandle>(null);
|
||||
|
||||
// Callback to receive action handlers from AvailabilityDetailScreen
|
||||
const handleActionsReady = useCallback((handlers: ActionHandlers) => {
|
||||
actionHandlersRef.current = handlers;
|
||||
}, []);
|
||||
|
||||
// Navigation handlers for edit modal screens
|
||||
const handleEditNameAndTimezone = useCallback(() => {
|
||||
router.push(`/edit-availability-name?id=${id}` as never);
|
||||
}, [router, id]);
|
||||
|
||||
const handleEditWorkingHours = useCallback(() => {
|
||||
router.push(`/edit-availability-hours?id=${id}` as never);
|
||||
}, [router, id]);
|
||||
|
||||
const handleEditOverride = useCallback(() => {
|
||||
router.push(`/edit-availability-override?id=${id}` as never);
|
||||
}, [router, id]);
|
||||
|
||||
// Action handlers for inline actions
|
||||
const handleSetAsDefault = useCallback(() => {
|
||||
if (actionHandlersRef.current?.handleSetAsDefault) {
|
||||
actionHandlersRef.current.handleSetAsDefault();
|
||||
} else if (screenRef.current?.setAsDefault) {
|
||||
screenRef.current.setAsDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (actionHandlersRef.current?.handleDelete) {
|
||||
actionHandlersRef.current.handleDelete();
|
||||
} else if (screenRef.current?.delete) {
|
||||
screenRef.current.delete();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
screenRef.current?.save();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Availability",
|
||||
headerBackTitle: "Availability",
|
||||
headerStyle: {
|
||||
backgroundColor: "#f2f2f7",
|
||||
},
|
||||
headerShadowVisible: false,
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
padding: 8,
|
||||
marginRight: -8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal-circle" size={24} color="#007AFF" />
|
||||
</TouchableOpacity>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem onPress={handleEditNameAndTimezone}>
|
||||
<Ionicons name="pencil-outline" size={18} color="#000" />
|
||||
<Text className="ml-2 text-[15px] text-black">Name and Timezone</Text>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onPress={handleEditWorkingHours}>
|
||||
<Ionicons name="time-outline" size={18} color="#000" />
|
||||
<Text className="ml-2 text-[15px] text-black">Working Hours</Text>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onPress={handleEditOverride}>
|
||||
<Ionicons name="calendar-outline" size={18} color="#000" />
|
||||
<Text className="ml-2 text-[15px] text-black">Date Override</Text>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onPress={handleSetAsDefault}>
|
||||
<Ionicons name="star-outline" size={18} color="#000" />
|
||||
<Text className="ml-2 text-[15px] text-black">Set as Default</Text>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onPress={handleDelete} variant="destructive">
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
<Text className="ml-2 text-[15px] text-[#FF3B30]">Delete Schedule</Text>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{Platform.OS === "ios" && (
|
||||
<Stack.Header>
|
||||
<Stack.Header.Left>
|
||||
<Stack.Header.Button onPress={() => router.back()}>
|
||||
<Stack.Header.Icon sf="chevron.left" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Left>
|
||||
|
||||
<Stack.Header.Title>Edit Availability</Stack.Header.Title>
|
||||
|
||||
<Stack.Header.Right>
|
||||
<Stack.Header.Button onPress={handleSave} variant="prominent" tintColor="#000">
|
||||
Save
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Right>
|
||||
</Stack.Header>
|
||||
)}
|
||||
|
||||
<AvailabilityDetailScreen ref={screenRef} id={id} useNativeHeader={Platform.OS === "ios"} />
|
||||
<AvailabilityDetailScreen ref={screenRef} id={id} onActionsReady={handleActionsReady} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1256,46 +1256,6 @@ export default function EventTypeDetail() {
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 200 }}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
>
|
||||
{/* Horizontal tabs only shown on web; Android uses header dropdown menu */}
|
||||
<Activity mode={Platform.OS === "web" ? "visible" : "hidden"}>
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: 12,
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingHorizontal: 12, gap: 4 }}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<TouchableOpacity
|
||||
key={tab.id}
|
||||
className={`min-w-[90px] items-center rounded-[24px] px-3 py-3 md:px-5 ${
|
||||
activeTab === tab.id ? "bg-[#EEEFF2]" : ""
|
||||
}`}
|
||||
onPress={() => setActiveTab(tab.id)}
|
||||
>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons
|
||||
name={tab.icon}
|
||||
size={18}
|
||||
color={activeTab === tab.id ? "#007AFF" : "#666"}
|
||||
/>
|
||||
<Text
|
||||
className={`text-base font-medium ${
|
||||
activeTab === tab.id ? "font-semibold text-[#007AFF]" : "text-[#666]"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Activity>
|
||||
|
||||
{activeTab === "basics" ? (
|
||||
<BasicsTab
|
||||
eventTitle={eventTitle}
|
||||
|
||||
@@ -70,6 +70,12 @@ function WebTabs() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
href: null,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="(event-types)"
|
||||
options={{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Alert, View } from "react-native";
|
||||
import { ActivityIndicator, Alert, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
|
||||
@@ -29,6 +31,10 @@ export default function EditAvailabilityHours() {
|
||||
}
|
||||
}, [id, router]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
const handleDayPress = useCallback(
|
||||
(dayIndex: number) => {
|
||||
router.push(`/edit-availability-day?id=${id}&day=${dayIndex}` as never);
|
||||
@@ -42,7 +48,12 @@ export default function EditAvailabilityHours() {
|
||||
className="flex-1 items-center justify-center bg-white"
|
||||
style={{ paddingBottom: insets.bottom }}
|
||||
>
|
||||
<Stack.Screen options={{ title: "Working Hours" }} />
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Working Hours",
|
||||
presentation: "modal",
|
||||
}}
|
||||
/>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
</View>
|
||||
);
|
||||
@@ -50,7 +61,25 @@ export default function EditAvailabilityHours() {
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white" style={{ paddingBottom: insets.bottom }}>
|
||||
<Stack.Screen options={{ title: "Working Hours" }} />
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Working Hours",
|
||||
presentation: "modal",
|
||||
contentStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
headerLeft: () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
|
||||
<Ionicons name="close" size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<EditAvailabilityHoursScreenComponent schedule={schedule} onDayPress={handleDayPress} />
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Text, View } from "react-native";
|
||||
import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen";
|
||||
@@ -34,6 +35,10 @@ export default function EditAvailabilityName() {
|
||||
}
|
||||
}, [id, router]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (isSaving) return;
|
||||
screenRef.current?.submit();
|
||||
@@ -49,7 +54,12 @@ export default function EditAvailabilityName() {
|
||||
className="flex-1 items-center justify-center bg-white"
|
||||
style={{ paddingBottom: insets.bottom }}
|
||||
>
|
||||
<Stack.Screen options={{ title: "Edit Name & Timezone" }} />
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Edit Name & Timezone",
|
||||
presentation: "modal",
|
||||
}}
|
||||
/>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
</View>
|
||||
);
|
||||
@@ -60,6 +70,20 @@ export default function EditAvailabilityName() {
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Edit Name & Timezone",
|
||||
presentation: "modal",
|
||||
contentStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
headerLeft: () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
|
||||
<Ionicons name="close" size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<Text
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Text, View } from "react-native";
|
||||
import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen";
|
||||
@@ -40,6 +41,10 @@ export default function EditAvailabilityOverride() {
|
||||
}
|
||||
}, [id, router]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (isSaving) return;
|
||||
screenRef.current?.submit();
|
||||
@@ -66,7 +71,12 @@ export default function EditAvailabilityOverride() {
|
||||
className="flex-1 items-center justify-center bg-white"
|
||||
style={{ paddingBottom: insets.bottom }}
|
||||
>
|
||||
<Stack.Screen options={{ title }} />
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title,
|
||||
presentation: "modal",
|
||||
}}
|
||||
/>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
</View>
|
||||
);
|
||||
@@ -77,6 +87,20 @@ export default function EditAvailabilityOverride() {
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title,
|
||||
presentation: "modal",
|
||||
contentStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
headerLeft: () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
|
||||
<Ionicons name="close" size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<Text
|
||||
|
||||
@@ -134,7 +134,15 @@ export function EventTypeBadges({
|
||||
|
||||
{/* Price Badge */}
|
||||
{hasPrice && formattedPrice ? (
|
||||
<Text className="text-sm font-medium text-cal-accent-success">{formattedPrice}</Text>
|
||||
<View
|
||||
className="rounded-md border border-cal-border bg-cal-border"
|
||||
style={{ height: 24, paddingHorizontal: 8, flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Ionicons name="card-outline" size={14} color="#000000" />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-cal-brand-black">
|
||||
{formattedPrice}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Repeats Badge */}
|
||||
|
||||
@@ -20,7 +20,21 @@ export function useEventTypeListItemData(item: EventType): EventTypeListItemData
|
||||
const formattedDuration = formatDuration(duration);
|
||||
const normalizedDescription = item.description ? normalizeMarkdown(item.description) : null;
|
||||
const hasPrice = item.price != null && item.price > 0;
|
||||
const formattedPrice = hasPrice ? `${item.currency || "$"}${item.price}` : null;
|
||||
let formattedPrice: string | null = null;
|
||||
|
||||
if (hasPrice && item.price) {
|
||||
// Price is in cents, convert to main unit
|
||||
const priceAmount = item.price / 100;
|
||||
try {
|
||||
formattedPrice = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: item.currency?.toUpperCase() || "USD",
|
||||
}).format(priceAmount);
|
||||
} catch {
|
||||
// Fallback if formatting fails
|
||||
formattedPrice = `${item.currency?.toUpperCase() || "$"}${priceAmount.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
duration,
|
||||
|
||||
@@ -114,8 +114,6 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
endTime: string;
|
||||
}[]
|
||||
>([]);
|
||||
const [daysExpanded, setDaysExpanded] = useState(true);
|
||||
const [overridesExpanded, setOverridesExpanded] = useState(true);
|
||||
|
||||
const processScheduleData = useCallback(
|
||||
(scheduleData: NonNullable<Awaited<ReturnType<typeof CalComAPIService.getScheduleById>>>) => {
|
||||
@@ -327,46 +325,20 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Working Hours Summary Card */}
|
||||
{/* Weekly Schedule Card - Navigable */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View className="px-4 py-3.5">
|
||||
<Text className="mb-2.5 text-[13px] font-medium uppercase tracking-wide text-[#8E8E93]">
|
||||
Working Hours
|
||||
</Text>
|
||||
{Object.keys(availability).length > 0 ? (
|
||||
<View>
|
||||
{formatAvailabilityDisplay(availability).map((line) => (
|
||||
<Text key={line} className="mb-1 text-[17px] text-black">
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
<AppPressable onPress={() => router.push(`/edit-availability-hours?id=${id}` as never)}>
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<Text className="text-[17px] text-black">Weekly Schedule</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
{enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</View>
|
||||
) : (
|
||||
<Text className="text-[17px] text-[#8E8E93]">No availability set</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Weekly Schedule Card - Expandable */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<AppPressable
|
||||
className="flex-row items-center justify-between px-4 py-3.5"
|
||||
onPress={() => setDaysExpanded(!daysExpanded)}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Weekly Schedule</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
{enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={daysExpanded ? "chevron-down" : "chevron-forward"}
|
||||
size={18}
|
||||
color="#C7C7CC"
|
||||
/>
|
||||
</View>
|
||||
</AppPressable>
|
||||
|
||||
{daysExpanded && (
|
||||
{/* Expanded Day List */}
|
||||
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
|
||||
{DAYS.map((day, dayIndex) => {
|
||||
const daySlots = availability[dayIndex] || [];
|
||||
@@ -412,38 +384,39 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</AppPressable>
|
||||
</View>
|
||||
|
||||
{/* Timezone Card */}
|
||||
{/* Timezone Card - Navigable */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<Text className="text-[17px] text-black">Timezone</Text>
|
||||
<Text className="text-[17px] text-[#8E8E93]">{timeZone}</Text>
|
||||
</View>
|
||||
<AppPressable onPress={() => router.push(`/edit-availability-name?id=${id}` as never)}>
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<Text className="text-[17px] text-black">Timezone</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">{timeZone}</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
</View>
|
||||
|
||||
{/* Date Overrides Card - Expandable */}
|
||||
{/* Date Overrides Card - Navigable */}
|
||||
{overrides.length > 0 && (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<AppPressable
|
||||
className="flex-row items-center justify-between px-4 py-3.5"
|
||||
onPress={() => setOverridesExpanded(!overridesExpanded)}
|
||||
onPress={() => router.push(`/edit-availability-override?id=${id}` as never)}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Date Overrides</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
{overrides.length} {overrides.length === 1 ? "override" : "overrides"}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={overridesExpanded ? "chevron-down" : "chevron-forward"}
|
||||
size={18}
|
||||
color="#C7C7CC"
|
||||
/>
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<Text className="text-[17px] text-black">Date Overrides</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
{overrides.length} {overrides.length === 1 ? "override" : "overrides"}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
|
||||
{overridesExpanded && (
|
||||
{/* Expanded Overrides List */}
|
||||
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
|
||||
{overrides.map((override, index) => (
|
||||
<View
|
||||
@@ -464,7 +437,7 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</AppPressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -9,6 +9,7 @@ import {
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
@@ -290,13 +291,13 @@ export function AvailabilityListScreen({
|
||||
autoCorrect={false}
|
||||
clearButtonMode="while-editing"
|
||||
/>
|
||||
<AppPressable
|
||||
<TouchableOpacity
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
|
||||
onPress={handleCreateNew}
|
||||
>
|
||||
<Ionicons name="add" size={18} color="#fff" />
|
||||
<Text className="text-base font-semibold text-white">New</Text>
|
||||
</AppPressable>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Activity>
|
||||
|
||||
@@ -439,12 +440,12 @@ export function AvailabilityListScreen({
|
||||
animationType="fade"
|
||||
onRequestClose={() => onShowCreateModalChange(false)}
|
||||
>
|
||||
<AppPressable
|
||||
<TouchableOpacity
|
||||
className="flex-1 items-center justify-center bg-black/50 p-2 md:p-4"
|
||||
activeOpacity={1}
|
||||
onPress={() => onShowCreateModalChange(false)}
|
||||
>
|
||||
<AppPressable
|
||||
<TouchableOpacity
|
||||
className="w-[90%] max-w-[500px] rounded-2xl bg-white"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
@@ -452,7 +453,10 @@ export function AvailabilityListScreen({
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="px-8 pb-4 pt-6">
|
||||
<Text className="text-2xl font-semibold text-gray-900">Add a new schedule</Text>
|
||||
<Text className="mb-2 text-2xl font-semibold text-gray-900">
|
||||
Add a new schedule
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">Create a new availability schedule.</Text>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
@@ -497,8 +501,8 @@ export function AvailabilityListScreen({
|
||||
</AppPressable>
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
</AppPressable>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</FullScreenModal>
|
||||
)}
|
||||
|
||||
|
||||
@@ -599,7 +599,7 @@ export function BookingDetailScreen({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<View className="flex-1 bg-[#]">
|
||||
<View className="flex-1 bg-[#f8f9fa]">
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 100 }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Alert, ScrollView, Switch, Text, View } from "react-native";
|
||||
import { Alert, Platform, ScrollView, Switch, Text, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
@@ -8,6 +8,7 @@ import type { Schedule } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import type { ScheduleAvailability } from "@/services/types";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { shadows } from "@/utils/shadows";
|
||||
|
||||
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
@@ -179,6 +180,57 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
|
||||
const dayName = DAYS[dayIndex] || "Day";
|
||||
|
||||
// Render modal content
|
||||
const renderTimePickerContent = () => (
|
||||
<>
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-4">
|
||||
<Text className="text-[17px] font-semibold">
|
||||
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
|
||||
</Text>
|
||||
<AppPressable onPress={() => setShowTimePicker(null)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView className="px-4 py-3">
|
||||
{TIME_OPTIONS.map((time) => {
|
||||
const slotIndex = showTimePicker?.slotIndex ?? 0;
|
||||
const currentTime =
|
||||
showTimePicker?.type === "start"
|
||||
? slots[slotIndex]?.startTime
|
||||
: slots[slotIndex]?.endTime;
|
||||
const isSelected = currentTime === time;
|
||||
|
||||
return (
|
||||
<AppPressable key={time} onPress={() => handleTimeSelect(time)}>
|
||||
<View
|
||||
className={`mb-2.5 rounded-xl border-2 px-4 py-4 ${
|
||||
isSelected
|
||||
? "border-[#007AFF] bg-blue-50 shadow-md"
|
||||
: "border-gray-200 bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-semibold text-[#007AFF]" : "font-medium text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{formatTime12Hour(time)}
|
||||
</Text>
|
||||
{isSelected && (
|
||||
<View className="rounded-full bg-[#007AFF] p-1.5">
|
||||
<Ionicons name="checkmark" size={14} color="#FFFFFF" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
|
||||
// Initialize from schedule
|
||||
useEffect(() => {
|
||||
if (schedule) {
|
||||
@@ -308,7 +360,7 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
}}
|
||||
>
|
||||
{/* Enable/Disable Toggle */}
|
||||
<View className="mb-4 flex-row items-center justify-between rounded-lg border border-gray-200 px-4 py-3.5">
|
||||
<View className="mb-4 flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5">
|
||||
<Text className="text-[17px] font-medium text-black">Available on {dayName}</Text>
|
||||
<Switch
|
||||
value={isEnabled}
|
||||
@@ -326,59 +378,117 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
{slots.map((slot, index) => (
|
||||
<View
|
||||
key={`slot-${slot.startTime}-${slot.endTime}-${index}`}
|
||||
className="mb-3 rounded-lg border border-gray-200 px-4 py-3"
|
||||
className="mb-3 rounded-xl border-2 border-gray-200 bg-white px-4 py-4"
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-[15px] font-medium text-gray-500">Slot {index + 1}</Text>
|
||||
{slots.length > 1 && (
|
||||
<AppPressable onPress={() => handleRemoveSlot(index)} className="p-1">
|
||||
<Ionicons name="trash-outline" size={20} color="#FF3B30" />
|
||||
<AppPressable
|
||||
onPress={() => handleRemoveSlot(index)}
|
||||
className="rounded-full bg-red-50 p-2"
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
</AppPressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="mt-3 flex-row items-center">
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-100 px-3 py-2"
|
||||
onPress={() => setShowTimePicker({ slotIndex: index, type: "start" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">Start</Text>
|
||||
<Text className="text-[17px] text-black">{formatTime12Hour(slot.startTime)}</Text>
|
||||
</AppPressable>
|
||||
{Platform.OS === "web" ? (
|
||||
<View className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100">
|
||||
<Text className="text-[13px] text-gray-500">Start</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={slot.startTime}
|
||||
onChange={(e) => {
|
||||
const newSlots = [...slots];
|
||||
newSlots[index] = { ...newSlots[index], startTime: e.target.value };
|
||||
setSlots(newSlots);
|
||||
}}
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: 500,
|
||||
color: "#000",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
fontFamily: "inherit",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100"
|
||||
onPress={() => setShowTimePicker({ slotIndex: index, type: "start" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">Start</Text>
|
||||
<Text className="text-[17px] font-medium text-black">
|
||||
{formatTime12Hour(slot.startTime)}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
)}
|
||||
|
||||
<Text className="mx-3 text-[17px] text-gray-400">–</Text>
|
||||
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-100 px-3 py-2"
|
||||
onPress={() => setShowTimePicker({ slotIndex: index, type: "end" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">End</Text>
|
||||
<Text className="text-[17px] text-black">{formatTime12Hour(slot.endTime)}</Text>
|
||||
</AppPressable>
|
||||
{Platform.OS === "web" ? (
|
||||
<View className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100">
|
||||
<Text className="text-[13px] text-gray-500">End</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={slot.endTime}
|
||||
onChange={(e) => {
|
||||
const newSlots = [...slots];
|
||||
newSlots[index] = { ...newSlots[index], endTime: e.target.value };
|
||||
setSlots(newSlots);
|
||||
}}
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: 500,
|
||||
color: "#000",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
fontFamily: "inherit",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100"
|
||||
onPress={() => setShowTimePicker({ slotIndex: index, type: "end" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">End</Text>
|
||||
<Text className="text-[17px] font-medium text-black">
|
||||
{formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Add Slot Button */}
|
||||
<AppPressable
|
||||
onPress={handleAddSlot}
|
||||
className="flex-row items-center justify-center rounded-lg border border-dashed border-gray-300 py-3.5"
|
||||
>
|
||||
<Ionicons name="add-circle" size={20} color="#007AFF" />
|
||||
<Text className="ml-2 text-[15px] font-medium text-[#007AFF]">Add Time Slot</Text>
|
||||
<AppPressable onPress={handleAddSlot}>
|
||||
<View className="flex-row items-center justify-center rounded-xl border-2 border-dashed border-gray-300 bg-gray-50 py-4">
|
||||
<Ionicons name="add-circle" size={22} color="#007AFF" />
|
||||
<Text className="ml-2 text-[17px] font-medium text-[#007AFF]">Add Time Slot</Text>
|
||||
</View>
|
||||
</AppPressable>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Unavailable Message */}
|
||||
{!isEnabled && (
|
||||
<View className="items-center rounded-lg border border-gray-200 px-4 py-8">
|
||||
<Ionicons name="moon-outline" size={40} color="#8E8E93" />
|
||||
<Text className="mt-3 text-center text-[17px] text-gray-500">
|
||||
You are unavailable on {dayName}
|
||||
<View className="items-center rounded-xl border-2 border-dashed border-gray-200 px-4 py-12">
|
||||
<View className="items-center justify-center rounded-full bg-gray-50 p-4">
|
||||
<Ionicons name="moon-outline" size={32} color="#9CA3AF" />
|
||||
</View>
|
||||
<Text className="mt-4 text-center text-[17px] font-medium text-gray-900">
|
||||
Unavailable
|
||||
</Text>
|
||||
<Text className="mt-1 text-center text-[15px] text-gray-400">
|
||||
Toggle on to set availability
|
||||
<Text className="mt-1 text-center text-[15px] text-gray-500">
|
||||
You are currently unavailable on {dayName}.{"\n"}Toggle the switch above to add hours.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -386,50 +496,27 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
{/* Time Picker Modal */}
|
||||
<FullScreenModal
|
||||
visible={!!showTimePicker}
|
||||
animationType="slide"
|
||||
animationType={Platform.OS === "web" ? "fade" : "slide"}
|
||||
onRequestClose={() => setShowTimePicker(null)}
|
||||
>
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
|
||||
<Text className="text-[17px] font-semibold">
|
||||
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
|
||||
</Text>
|
||||
<AppPressable onPress={() => setShowTimePicker(null)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView>
|
||||
{TIME_OPTIONS.map((time) => {
|
||||
const slotIndex = showTimePicker?.slotIndex ?? 0;
|
||||
const currentTime =
|
||||
showTimePicker?.type === "start"
|
||||
? slots[slotIndex]?.startTime
|
||||
: slots[slotIndex]?.endTime;
|
||||
const isSelected = currentTime === time;
|
||||
|
||||
return (
|
||||
<AppPressable
|
||||
key={time}
|
||||
onPress={() => handleTimeSelect(time)}
|
||||
className={`border-b border-gray-100 px-4 py-3.5 ${
|
||||
isSelected ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-medium text-[#007AFF]" : "text-black"
|
||||
}`}
|
||||
>
|
||||
{formatTime12Hour(time)}
|
||||
</Text>
|
||||
{isSelected && <Ionicons name="checkmark" size={20} color="#007AFF" />}
|
||||
</View>
|
||||
</AppPressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
{Platform.OS === "web" ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 items-center justify-center bg-black/50 p-4"
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowTimePicker(null)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
>
|
||||
{renderTimePickerContent()}
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View className="flex-1 bg-white p-2">{renderTimePickerContent()}</View>
|
||||
)}
|
||||
</FullScreenModal>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
@@ -110,53 +110,53 @@ export const EditAvailabilityHoursScreen = forwardRef<unknown, EditAvailabilityH
|
||||
Tap a day to edit its hours
|
||||
</Text>
|
||||
|
||||
<View className="overflow-hidden rounded-lg border border-gray-200">
|
||||
<View>
|
||||
{DAYS.map((day, dayIndex) => {
|
||||
const daySlots = availability[dayIndex] || [];
|
||||
const isEnabled = daySlots.length > 0;
|
||||
|
||||
return (
|
||||
<AppPressable
|
||||
key={day}
|
||||
className={`flex-row items-center px-4 py-3.5 ${
|
||||
dayIndex > 0 ? "border-t border-gray-200" : ""
|
||||
}`}
|
||||
onPress={() => onDayPress(dayIndex)}
|
||||
>
|
||||
{/* Day status indicator */}
|
||||
<AppPressable key={day} onPress={() => onDayPress(dayIndex)}>
|
||||
<View
|
||||
className={`mr-3 h-2.5 w-2.5 rounded-full ${
|
||||
isEnabled ? "bg-green-500" : "bg-gray-300"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Day name */}
|
||||
<Text
|
||||
className={`w-24 text-[17px] font-medium ${
|
||||
isEnabled ? "text-black" : "text-gray-400"
|
||||
className={`mb-2.5 flex-row items-center rounded-xl border-2 px-4 py-4 ${
|
||||
isEnabled ? "border-gray-200 bg-white" : "border-gray-100 bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{day}
|
||||
</Text>
|
||||
{/* Day status indicator */}
|
||||
<View
|
||||
className={`mr-3 h-2.5 w-2.5 rounded-full ${
|
||||
isEnabled ? "bg-green-500" : "bg-gray-300"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Time slots or unavailable */}
|
||||
<View className="flex-1">
|
||||
{isEnabled ? (
|
||||
daySlots.map((slot, slotIndex) => (
|
||||
<Text
|
||||
key={`${slotIndex}-${slot.startTime}`}
|
||||
className={`text-[15px] text-gray-500 ${slotIndex > 0 ? "mt-0.5" : ""}`}
|
||||
>
|
||||
{formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text className="text-[15px] text-gray-400">Unavailable</Text>
|
||||
)}
|
||||
{/* Day name */}
|
||||
<Text
|
||||
className={`w-28 text-[17px] font-medium ${
|
||||
isEnabled ? "text-black" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{day}
|
||||
</Text>
|
||||
|
||||
{/* Time slots or unavailable */}
|
||||
<View className="flex-1">
|
||||
{isEnabled ? (
|
||||
daySlots.map((slot, slotIndex) => (
|
||||
<Text
|
||||
key={`${slotIndex}-${slot.startTime}`}
|
||||
className={`text-[15px] text-gray-600 ${slotIndex > 0 ? "mt-0.5" : ""}`}
|
||||
>
|
||||
{formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text className="text-[15px] text-gray-400">Unavailable</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Chevron */}
|
||||
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
|
||||
</View>
|
||||
|
||||
{/* Chevron */}
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</AppPressable>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Alert, KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
@@ -8,6 +17,7 @@ import { TIMEZONES as ALL_TIMEZONES } from "@/constants/timezones";
|
||||
import type { Schedule } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { shadows } from "@/utils/shadows";
|
||||
|
||||
// Format timezones for display
|
||||
const TIMEZONES = ALL_TIMEZONES.map((tz) => ({
|
||||
@@ -84,6 +94,57 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
|
||||
const selectedTimezoneLabel = TIMEZONES.find((tz) => tz.id === timezone)?.label || timezone;
|
||||
|
||||
// Render timezone list content
|
||||
const renderTimezoneContent = () => (
|
||||
<>
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-4">
|
||||
<Text className="text-[17px] font-semibold">Select Timezone</Text>
|
||||
<AppPressable onPress={() => setShowTimezoneModal(false)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView className="px-4 py-3">
|
||||
{TIMEZONES.map((tz) => (
|
||||
<AppPressable
|
||||
key={tz.id}
|
||||
onPress={() => {
|
||||
setTimezone(tz.id);
|
||||
setShowTimezoneModal(false);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className={`mb-2.5 rounded-xl border-2 px-4 py-4 ${
|
||||
tz.id === timezone
|
||||
? "border-[#007AFF] bg-blue-50 shadow-md"
|
||||
: "border-gray-200 bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
tz.id === timezone
|
||||
? "font-semibold text-[#007AFF]"
|
||||
: "font-medium text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{tz.label}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">{tz.id}</Text>
|
||||
</View>
|
||||
{tz.id === timezone && (
|
||||
<View className="rounded-full bg-[#007AFF] p-1.5">
|
||||
<Ionicons name="checkmark" size={14} color="#FFFFFF" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-white">
|
||||
@@ -120,63 +181,49 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
<Text className="mb-2 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
Timezone
|
||||
</Text>
|
||||
<AppPressable
|
||||
className="flex-row items-center justify-between rounded-lg border border-gray-300 bg-gray-50 px-4 py-3"
|
||||
onPress={() => setShowTimezoneModal(true)}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons name="globe-outline" size={20} color="#007AFF" style={{ marginRight: 12 }} />
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">{selectedTimezoneLabel}</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">{timezone}</Text>
|
||||
<AppPressable onPress={() => setShowTimezoneModal(true)}>
|
||||
<View className="flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-4">
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons
|
||||
name="globe-outline"
|
||||
size={20}
|
||||
color="#007AFF"
|
||||
style={{ marginRight: 12 }}
|
||||
/>
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">{selectedTimezoneLabel}</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">{timezone}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons name="chevron-down" size={20} color="#C7C7CC" />
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
</AppPressable>
|
||||
</ScrollView>
|
||||
|
||||
{/* Timezone Modal */}
|
||||
<FullScreenModal
|
||||
visible={showTimezoneModal}
|
||||
animationType="slide"
|
||||
animationType={Platform.OS === "web" ? "fade" : "slide"}
|
||||
onRequestClose={() => setShowTimezoneModal(false)}
|
||||
>
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
|
||||
<Text className="text-[17px] font-semibold">Select Timezone</Text>
|
||||
<AppPressable onPress={() => setShowTimezoneModal(false)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView>
|
||||
{TIMEZONES.map((tz) => (
|
||||
<AppPressable
|
||||
key={tz.id}
|
||||
onPress={() => {
|
||||
setTimezone(tz.id);
|
||||
setShowTimezoneModal(false);
|
||||
}}
|
||||
className={`border-b border-gray-100 px-4 py-3.5 ${
|
||||
tz.id === timezone ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View>
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
tz.id === timezone ? "font-medium text-[#007AFF]" : "text-black"
|
||||
}`}
|
||||
>
|
||||
{tz.label}
|
||||
</Text>
|
||||
<Text className="text-[13px] text-gray-500">{tz.id}</Text>
|
||||
</View>
|
||||
{tz.id === timezone && <Ionicons name="checkmark" size={20} color="#007AFF" />}
|
||||
</View>
|
||||
</AppPressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
{Platform.OS === "web" ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 items-center justify-center bg-black/50 p-4"
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowTimezoneModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
>
|
||||
{renderTimezoneContent()}
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View className="flex-1 bg-white p-2">{renderTimezoneContent()}</View>
|
||||
)}
|
||||
</FullScreenModal>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Alert, ScrollView, Switch, Text, TextInput, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
import type { Schedule } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { shadows } from "@/utils/shadows";
|
||||
|
||||
// Generate time options (15-minute intervals)
|
||||
const generateTimeOptions = () => {
|
||||
@@ -78,6 +88,53 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
type: "start" | "end";
|
||||
} | null>(null);
|
||||
|
||||
// Render modal content
|
||||
const renderTimePickerContent = () => (
|
||||
<>
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
|
||||
<Text className="text-[17px] font-semibold">
|
||||
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
|
||||
</Text>
|
||||
<AppPressable onPress={() => setShowTimePicker(null)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView className="px-4 py-3">
|
||||
{TIME_OPTIONS.map((time) => {
|
||||
const currentTime = showTimePicker?.type === "start" ? startTime : endTime;
|
||||
const isSelected = currentTime === time;
|
||||
|
||||
return (
|
||||
<AppPressable key={time} onPress={() => handleTimeSelect(time)}>
|
||||
<View
|
||||
className={`mb-2.5 rounded-xl border-2 px-4 py-4 ${
|
||||
isSelected
|
||||
? "border-[#007AFF] bg-blue-50 shadow-md"
|
||||
: "border-gray-200 bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-semibold text-[#007AFF]" : "font-medium text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{formatTime12Hour(time)}
|
||||
</Text>
|
||||
{isSelected && (
|
||||
<View className="rounded-full bg-[#007AFF] p-1.5">
|
||||
<Ionicons name="checkmark" size={14} color="#FFFFFF" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
|
||||
// Initialize from existing override if editing
|
||||
useEffect(() => {
|
||||
if (schedule && isEditing && schedule.overrides && overrideIndex !== undefined) {
|
||||
@@ -272,26 +329,43 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
>
|
||||
{/* Date Input */}
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500">Date (YYYY-MM-DD)</Text>
|
||||
<View className="mb-4 rounded-lg border border-gray-200 px-4 py-3">
|
||||
<View className="mb-4 rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons name="calendar" size={20} color="#007AFF" style={{ marginRight: 12 }} />
|
||||
<TextInput
|
||||
className="flex-1 text-[17px] text-black"
|
||||
placeholder="2024-12-25"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={selectedDate}
|
||||
onChangeText={setSelectedDate}
|
||||
/>
|
||||
<Ionicons name="calendar-outline" size={20} color="#007AFF" style={{ marginRight: 12 }} />
|
||||
{Platform.OS === "web" ? (
|
||||
<input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
color: "#000",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
fontFamily: "inherit",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
className="flex-1 text-[17px] text-black"
|
||||
placeholder="2024-12-25"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={selectedDate}
|
||||
onChangeText={setSelectedDate}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{selectedDate && (
|
||||
{selectedDate ? (
|
||||
<Text className="mt-2 text-[15px] text-gray-500">
|
||||
{formatDateForDisplay(selectedDate)}
|
||||
</Text>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* Unavailable Toggle */}
|
||||
<View className="mb-4 flex-row items-center justify-between rounded-lg border border-gray-200 px-4 py-3.5">
|
||||
<View className="mb-4 flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5">
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons name="moon-outline" size={20} color="#FF3B30" style={{ marginRight: 12 }} />
|
||||
<View>
|
||||
@@ -311,25 +385,65 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
{!isUnavailable && (
|
||||
<>
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500">Available Hours</Text>
|
||||
<View className="mb-4 rounded-lg border border-gray-200 px-4 py-3.5">
|
||||
<View className="mb-4 rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5">
|
||||
<View className="flex-row items-center">
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-100 px-3 py-2"
|
||||
onPress={() => setShowTimePicker({ type: "start" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">Start Time</Text>
|
||||
<Text className="text-[17px] text-black">{formatTime12Hour(startTime)}</Text>
|
||||
</AppPressable>
|
||||
{Platform.OS === "web" ? (
|
||||
<View className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2">
|
||||
<Text className="text-[13px] text-gray-500">Start Time</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
style={{
|
||||
fontSize: 17,
|
||||
color: "#000",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
fontFamily: "inherit",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2"
|
||||
onPress={() => setShowTimePicker({ type: "start" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">Start Time</Text>
|
||||
<Text className="text-[17px] text-black">{formatTime12Hour(startTime)}</Text>
|
||||
</AppPressable>
|
||||
)}
|
||||
|
||||
<Text className="mx-4 text-[17px] text-gray-400">–</Text>
|
||||
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-100 px-3 py-2"
|
||||
onPress={() => setShowTimePicker({ type: "end" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">End Time</Text>
|
||||
<Text className="text-[17px] text-black">{formatTime12Hour(endTime)}</Text>
|
||||
</AppPressable>
|
||||
{Platform.OS === "web" ? (
|
||||
<View className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2">
|
||||
<Text className="text-[13px] text-gray-500">End Time</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
style={{
|
||||
fontSize: 17,
|
||||
color: "#000",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
fontFamily: "inherit",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2"
|
||||
onPress={() => setShowTimePicker({ type: "end" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">End Time</Text>
|
||||
<Text className="text-[17px] text-black">{formatTime12Hour(endTime)}</Text>
|
||||
</AppPressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
@@ -341,41 +455,41 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
<Text className="mb-2 mt-4 text-[13px] font-medium text-gray-500">
|
||||
Existing Overrides
|
||||
</Text>
|
||||
<View className="overflow-hidden rounded-lg border border-gray-200">
|
||||
<View>
|
||||
{schedule.overrides.map((override, index) => (
|
||||
<AppPressable
|
||||
key={override.date}
|
||||
className={`flex-row items-center justify-between px-4 py-3 ${
|
||||
index > 0 ? "border-t border-gray-200" : ""
|
||||
}`}
|
||||
className="mb-2.5"
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[15px] text-black">
|
||||
{formatDateForDisplay(override.date ?? "")}
|
||||
</Text>
|
||||
{override.startTime === "00:00" && override.endTime === "00:00" ? (
|
||||
<Text className="text-[13px] text-red-500">Unavailable</Text>
|
||||
) : (
|
||||
<Text className="text-[13px] text-gray-500">
|
||||
{formatTime12Hour(override.startTime ?? "00:00")} –{" "}
|
||||
{formatTime12Hour(override.endTime ?? "00:00")}
|
||||
<View className="flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-white px-4 py-3.5">
|
||||
<View className="flex-1">
|
||||
<Text className="text-[15px] font-medium text-black">
|
||||
{formatDateForDisplay(override.date ?? "")}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<AppPressable
|
||||
className="rounded-full bg-red-100 p-2"
|
||||
onPress={() => handleDeleteOverride(index)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
</AppPressable>
|
||||
<AppPressable
|
||||
className="ml-3 rounded-full bg-gray-100 p-2"
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<Ionicons name="chevron-forward" size={18} color="#8E8E93" />
|
||||
</AppPressable>
|
||||
{override.startTime === "00:00" && override.endTime === "00:00" ? (
|
||||
<Text className="mt-0.5 text-[13px] text-red-500">Unavailable</Text>
|
||||
) : (
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">
|
||||
{formatTime12Hour(override.startTime ?? "00:00")} –{" "}
|
||||
{formatTime12Hour(override.endTime ?? "00:00")}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<AppPressable
|
||||
className="rounded-full bg-red-50 p-2"
|
||||
onPress={() => handleDeleteOverride(index)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
</AppPressable>
|
||||
<Ionicons
|
||||
style={{ marginLeft: 12 }}
|
||||
name="chevron-forward"
|
||||
size={18}
|
||||
color="#C7C7CC"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
))}
|
||||
@@ -386,46 +500,27 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
{/* Time Picker Modal */}
|
||||
<FullScreenModal
|
||||
visible={!!showTimePicker}
|
||||
animationType="slide"
|
||||
animationType={Platform.OS === "web" ? "fade" : "slide"}
|
||||
onRequestClose={() => setShowTimePicker(null)}
|
||||
>
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
|
||||
<Text className="text-[17px] font-semibold">
|
||||
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
|
||||
</Text>
|
||||
<AppPressable onPress={() => setShowTimePicker(null)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView>
|
||||
{TIME_OPTIONS.map((time) => {
|
||||
const currentTime = showTimePicker?.type === "start" ? startTime : endTime;
|
||||
const isSelected = currentTime === time;
|
||||
|
||||
return (
|
||||
<AppPressable
|
||||
key={time}
|
||||
onPress={() => handleTimeSelect(time)}
|
||||
className={`border-b border-gray-100 px-4 py-3.5 ${
|
||||
isSelected ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-medium text-[#007AFF]" : "text-black"
|
||||
}`}
|
||||
>
|
||||
{formatTime12Hour(time)}
|
||||
</Text>
|
||||
{isSelected && <Ionicons name="checkmark" size={20} color="#007AFF" />}
|
||||
</View>
|
||||
</AppPressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
{Platform.OS === "web" ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 items-center justify-center bg-black/50 p-4"
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowTimePicker(null)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
>
|
||||
{renderTimePickerContent()}
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View className="flex-1 bg-white p-2">{renderTimePickerContent()}</View>
|
||||
)}
|
||||
</FullScreenModal>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user