feat(companion): add optimistic updates to availability edit screens (#26931)

* feat(companion): add optimistic updates to EditAvailabilityNameScreen

- Refactor useUpdateSchedule hook with optimistic updates
- Update EditAvailabilityNameScreen to use mutation hook instead of direct API call
- Update EditAvailabilityNameScreen.ios.tsx with same pattern
- Cache is updated immediately on save, then synced with server
- On error, cache is rolled back to previous state

* fix(companion): fix cache update for schedule list when detail cache is empty

- Update list cache in onMutate even when detail cache doesn't exist
- Remove onSettled invalidation that was causing issues with staleTime: Infinity
- Add fallback invalidation in onSuccess when list cache doesn't exist

* fix(companion): fix AvailabilityDetailScreen not reflecting cache updates

The previous optimistic updates implementation in EditAvailabilityNameScreen
correctly updated the React Query cache, but AvailabilityDetailScreen was
still using direct CalComAPIService.getScheduleById() calls with local
useState, completely bypassing the cache.

The disconnect:
- EditAvailabilityNameScreen → useUpdateSchedule → Updates React Query Cache ✓
- AvailabilityDetailScreen → CalComAPIService.getScheduleById → Local state ✗

This meant when a user saved changes to a schedule's name/timezone, the
cache was updated but the detail screen (and subsequently the list screen)
never saw those updates because they weren't reading from the cache.

Changes:
- Refactor AvailabilityDetailScreen.tsx to use useScheduleById hook
- Refactor AvailabilityDetailScreen.ios.tsx to use useScheduleById hook
- Replace direct API calls with React Query for cache synchronization
- Add RefreshControl for pull-to-refresh support
- Use mutation hooks (useSetScheduleAsDefault, useDeleteSchedule) for actions
- Derive availability/overrides data using useMemo from query result

Now when EditAvailabilityNameScreen updates the cache, AvailabilityDetailScreen
automatically reflects those changes because both read from the same cache.

* feat(companion): refactor EditAvailabilityOverrideScreen to use mutation hook

- Replace direct CalComAPIService.updateSchedule calls with useUpdateSchedule hook
- Remove local isSaving state, use isPending from mutation hook instead
- Cache is now updated automatically via the mutation hook's optimistic updates
- Consistent pattern with EditAvailabilityNameScreen refactoring

* feat(companion): refactor EditAvailabilityDayScreen to use mutation hook

- Replace direct CalComAPIService.updateSchedule calls with useUpdateSchedule hook
- Remove local isSaving state, use isPending from mutation hook instead
- Cache is now updated automatically via the mutation hook's optimistic updates
- Consistent pattern with EditAvailabilityNameScreen and EditAvailabilityOverrideScreen

* fix(companion): refactor edit-availability-hours routes to use useScheduleById hook

The working hours page (page 2 in the flow) was showing stale data because
it used direct CalComAPIService.getScheduleById() calls with local useState,
bypassing the React Query cache.

The disconnect:
- EditAvailabilityDayScreen → useUpdateSchedule → Updates React Query Cache ✓
- edit-availability-hours route → CalComAPIService.getScheduleById → Local state ✗

This meant when a user saved changes to a day's availability, the cache was
updated but the working hours page never saw those updates because it wasn't
reading from the cache.

Changes:
- Refactor edit-availability-hours.tsx to use useScheduleById hook
- Refactor edit-availability-hours.ios.tsx to use useScheduleById hook
- Replace direct API calls with React Query for cache synchronization
- Now all 3 pages in the flow read from the same cache

* fix(companion): make Date Overrides section clickable when empty

Previously, when there were no date overrides, the Date Overrides section
was just a plain View without any press handler, making it impossible for
users to navigate to the edit override page to add new overrides.

This follows the same pattern as other sections (Weekly Schedule, Timezone)
which are always clickable regardless of their content state.

Changes:
- Wrap the 'No Overrides' section in AppPressable with navigation handler
- Add chevron-forward icon to indicate it's tappable
- Apply fix to both AvailabilityDetailScreen.tsx and .ios.tsx

* fix(companion): use useEffect instead of useMemo for side effects

- Change useMemo to useEffect for onActionsReady callback (side effect)
- Move error handling (showErrorAlert, router.back) to useEffect
- Keep early return for error state after useEffect hooks

Fixes Cubic AI review feedback (confidence 9/10)
This commit is contained in:
Dhairyashil Shinde
2026-01-16 19:08:52 +01:00
committed by GitHub
parent 25f7381393
commit ef0af95bb1
11 changed files with 464 additions and 432 deletions
+11 -16
View File
@@ -1,11 +1,11 @@
import { osName } from "expo-device";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect } from "react";
import { ActivityIndicator, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen.ios";
import { CalComAPIService, type Schedule } from "@/services/calcom";
import { useScheduleById } from "@/hooks/useSchedules";
import { showErrorAlert } from "@/utils/alerts";
// Semi-transparent background to prevent black flash while preserving glass effect
@@ -22,25 +22,20 @@ export default function EditAvailabilityHoursIOS() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const insets = useSafeAreaInsets();
const [schedule, setSchedule] = useState<Schedule | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Use React Query hook to read from cache (syncs with mutations)
const { data: schedule, isLoading, isError } = useScheduleById(id ? Number(id) : undefined);
// Handle missing ID or error
useEffect(() => {
if (id) {
setIsLoading(true);
CalComAPIService.getScheduleById(Number(id))
.then(setSchedule)
.catch(() => {
showErrorAlert("Error", "Failed to load schedule details");
router.back();
})
.finally(() => setIsLoading(false));
} else {
setIsLoading(false);
if (!id) {
showErrorAlert("Error", "Schedule ID is missing");
router.back();
} else if (isError) {
showErrorAlert("Error", "Failed to load schedule details");
router.back();
}
}, [id, router]);
}, [id, isError, router]);
const handleDayPress = useCallback(
(dayIndex: number) => {
+11 -16
View File
@@ -1,36 +1,31 @@
import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect } from "react";
import { ActivityIndicator, 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";
import { useScheduleById } from "@/hooks/useSchedules";
import { showErrorAlert } from "@/utils/alerts";
export default function EditAvailabilityHours() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const insets = useSafeAreaInsets();
const [schedule, setSchedule] = useState<Schedule | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Use React Query hook to read from cache (syncs with mutations)
const { data: schedule, isLoading, isError } = useScheduleById(id ? Number(id) : undefined);
// Handle missing ID or error
useEffect(() => {
if (id) {
setIsLoading(true);
CalComAPIService.getScheduleById(Number(id))
.then(setSchedule)
.catch(() => {
showErrorAlert("Error", "Failed to load schedule details");
router.back();
})
.finally(() => setIsLoading(false));
} else {
setIsLoading(false);
if (!id) {
showErrorAlert("Error", "Schedule ID is missing");
router.back();
} else if (isError) {
showErrorAlert("Error", "Failed to load schedule details");
router.back();
}
}, [id, router]);
}, [id, isError, router]);
const handleClose = useCallback(() => {
router.back();
@@ -3,15 +3,16 @@
*
* iOS-specific read-only display of availability schedule.
* Editing is done via separate bottom sheet screens.
* Uses React Query for cache synchronization with edit screens.
*/
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import { ActivityIndicator, Alert, ScrollView, Text, View } from "react-native";
import { forwardRef, useCallback, useImperativeHandle, useMemo } from "react";
import { ActivityIndicator, Alert, RefreshControl, ScrollView, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { CalComAPIService, type Schedule } from "@/services/calcom";
import { useDeleteSchedule, useScheduleById, useSetScheduleAsDefault } from "@/hooks/useSchedules";
import type { ScheduleAvailability } from "@/services/types";
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
@@ -69,137 +70,93 @@ export const AvailabilityDetailScreen = forwardRef<
const router = useRouter();
const insets = useSafeAreaInsets();
const [loading, setLoading] = useState(true);
const [_schedule, setSchedule] = useState<Schedule | null>(null);
const [scheduleName, setScheduleName] = useState("");
const [timeZone, setTimeZone] = useState("");
const [isDefault, setIsDefault] = useState(false);
const [availability, setAvailability] = useState<Record<number, ScheduleAvailability[]>>({});
const [overrides, setOverrides] = useState<
{
date: string;
startTime: string;
endTime: string;
}[]
>([]);
// Use React Query hook for data fetching and cache synchronization
const { data: schedule, isLoading, error, refetch, isRefetching } = useScheduleById(Number(id));
const processScheduleData = useCallback(
(scheduleData: NonNullable<Awaited<ReturnType<typeof CalComAPIService.getScheduleById>>>) => {
const name = scheduleData.name ?? "";
const tz = scheduleData.timeZone ?? "UTC";
const isDefaultSchedule = scheduleData.isDefault ?? false;
// Mutation hooks for actions
const { mutate: setAsDefaultMutation } = useSetScheduleAsDefault();
const { mutate: deleteScheduleMutation } = useDeleteSchedule();
setSchedule(scheduleData);
setScheduleName(name);
setTimeZone(tz);
setIsDefault(isDefaultSchedule);
// Derive schedule properties from the query data
const scheduleName = schedule?.name ?? "";
const timeZone = schedule?.timeZone ?? "";
const isDefault = schedule?.isDefault ?? false;
const availabilityMap: Record<number, ScheduleAvailability[]> = {};
// Process availability data into a map by day number
const availability = useMemo(() => {
const availabilityMap: Record<number, ScheduleAvailability[]> = {};
const availabilityArray = scheduleData.availability;
if (availabilityArray && Array.isArray(availabilityArray)) {
availabilityArray.forEach((slot) => {
let days: number[] = [];
if (Array.isArray(slot.days)) {
days = slot.days
.map((day) => {
if (typeof day === "string" && DAY_NAME_TO_NUMBER[day] !== undefined) {
return DAY_NAME_TO_NUMBER[day];
const availabilityArray = schedule?.availability;
if (availabilityArray && Array.isArray(availabilityArray)) {
availabilityArray.forEach((slot) => {
let days: number[] = [];
if (Array.isArray(slot.days)) {
days = slot.days
.map((day) => {
if (typeof day === "string" && DAY_NAME_TO_NUMBER[day] !== undefined) {
return DAY_NAME_TO_NUMBER[day];
}
if (typeof day === "string") {
const parsed = parseInt(day, 10);
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 6) {
return parsed;
}
if (typeof day === "string") {
const parsed = parseInt(day, 10);
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 6) {
return parsed;
}
}
if (typeof day === "number" && day >= 0 && day <= 6) {
return day;
}
return null;
})
.filter((day): day is number => day !== null);
}
if (typeof day === "number" && day >= 0 && day <= 6) {
return day;
}
return null;
})
.filter((day): day is number => day !== null);
}
days.forEach((day) => {
if (!availabilityMap[day]) {
availabilityMap[day] = [];
}
days.forEach((day) => {
if (!availabilityMap[day]) {
availabilityMap[day] = [];
}
const startTime = slot.startTime ?? "09:00:00";
const endTime = slot.endTime ?? "17:00:00";
availabilityMap[day].push({
days: [day.toString()],
startTime,
endTime,
});
const startTime = slot.startTime ?? "09:00:00";
const endTime = slot.endTime ?? "17:00:00";
availabilityMap[day].push({
days: [day.toString()],
startTime,
endTime,
});
});
}
setAvailability(availabilityMap);
const overridesArray = scheduleData.overrides;
if (overridesArray && Array.isArray(overridesArray)) {
const formattedOverrides = overridesArray.map((override) => {
const date = override.date ?? "";
const startTime = override.startTime ?? "00:00";
const endTime = override.endTime ?? "00:00";
return { date, startTime, endTime };
});
setOverrides(formattedOverrides);
} else {
setOverrides([]);
}
},
[]
);
const fetchSchedule = useCallback(async () => {
setLoading(true);
let scheduleData: Awaited<ReturnType<typeof CalComAPIService.getScheduleById>> = null;
try {
scheduleData = await CalComAPIService.getScheduleById(Number(id));
} catch (error) {
console.error("Error fetching schedule");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
console.debug("[AvailabilityDetailScreen.ios] fetchSchedule failed", {
message,
});
}
showErrorAlert("Error", "Failed to load availability. Please try again.");
router.back();
setLoading(false);
return;
});
}
if (scheduleData) {
processScheduleData(scheduleData);
}
setLoading(false);
}, [id, router, processScheduleData]);
return availabilityMap;
}, [schedule?.availability]);
useEffect(() => {
if (id) {
fetchSchedule();
// Process overrides data
const overrides = useMemo(() => {
const overridesArray = schedule?.overrides;
if (overridesArray && Array.isArray(overridesArray)) {
return overridesArray.map((override) => {
const date = override.date ?? "";
const startTime = override.startTime ?? "00:00";
const endTime = override.endTime ?? "00:00";
return { date, startTime, endTime };
});
}
}, [id, fetchSchedule]);
return [];
}, [schedule?.overrides]);
const handleSetAsDefault = useCallback(async () => {
const handleSetAsDefault = useCallback(() => {
if (isDefault) {
showInfoAlert("Info", "This schedule is already set as default");
return;
}
try {
await CalComAPIService.updateSchedule(Number(id), {
isDefault: true,
});
setIsDefault(true);
showSuccessAlert("Success", "Availability set as default successfully");
} catch {
showErrorAlert("Error", "Failed to set availability as default. Please try again.");
}
}, [isDefault, id]);
setAsDefaultMutation(Number(id), {
onSuccess: () => {
showSuccessAlert("Success", "Availability set as default successfully");
},
onError: () => {
showErrorAlert("Error", "Failed to set availability as default. Please try again.");
},
});
}, [isDefault, id, setAsDefaultMutation]);
const handleDelete = useCallback(() => {
if (isDefault) {
@@ -215,19 +172,21 @@ export const AvailabilityDetailScreen = forwardRef<
{
text: "Delete",
style: "destructive",
onPress: async () => {
try {
await CalComAPIService.deleteSchedule(Number(id));
Alert.alert("Success", "Availability deleted successfully", [
{ text: "OK", onPress: () => router.back() },
]);
} catch {
showErrorAlert("Error", "Failed to delete availability. Please try again.");
}
onPress: () => {
deleteScheduleMutation(Number(id), {
onSuccess: () => {
Alert.alert("Success", "Availability deleted successfully", [
{ text: "OK", onPress: () => router.back() },
]);
},
onError: () => {
showErrorAlert("Error", "Failed to delete availability. Please try again.");
},
});
},
},
]);
}, [isDefault, scheduleName, id, router]);
}, [isDefault, scheduleName, id, router, deleteScheduleMutation]);
// Expose handlers via ref
useImperativeHandle(
@@ -235,9 +194,9 @@ export const AvailabilityDetailScreen = forwardRef<
() => ({
setAsDefault: handleSetAsDefault,
delete: handleDelete,
refresh: fetchSchedule,
refresh: () => refetch(),
}),
[handleSetAsDefault, handleDelete, fetchSchedule]
[handleSetAsDefault, handleDelete, refetch]
);
// Expose handlers to parent for iOS header menu
@@ -250,10 +209,23 @@ export const AvailabilityDetailScreen = forwardRef<
}
}, [onActionsReady, handleSetAsDefault, handleDelete]);
// Handle error state - must be in useEffect to avoid side effects during render
useEffect(() => {
if (error) {
showErrorAlert("Error", "Failed to load availability. Please try again.");
router.back();
}
}, [error, router]);
// Count enabled days
const enabledDaysCount = Object.keys(availability).length;
if (loading) {
// Early return for error state (after useEffect hooks)
if (error) {
return null;
}
if (isLoading) {
return (
<View className="flex-1 items-center justify-center bg-[#f2f2f7]">
<ActivityIndicator size="large" color="#000000" />
@@ -272,6 +244,7 @@ export const AvailabilityDetailScreen = forwardRef<
paddingBottom: insets.bottom + 100,
}}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={() => refetch()} />}
>
{/* Schedule Title Section - iOS Calendar Style */}
<View className="mb-4">
@@ -409,13 +382,20 @@ export const AvailabilityDetailScreen = forwardRef<
</View>
)}
{/* No Overrides Message */}
{/* No Overrides Message - Still navigable to add overrides */}
{overrides.length === 0 && (
<View className="mb-4 overflow-hidden rounded-xl bg-white">
<View className="px-4 py-3.5">
<Text className="text-[17px] text-black">Date Overrides</Text>
<Text className="mt-1 text-[15px] text-[#8E8E93]">No date overrides set</Text>
</View>
<AppPressable
onPress={() => router.push(`/edit-availability-override?id=${id}` as never)}
>
<View className="flex-row items-center justify-between px-4 py-3.5">
<View>
<Text className="text-[17px] text-black">Date Overrides</Text>
<Text className="mt-1 text-[15px] text-[#8E8E93]">No date overrides set</Text>
</View>
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
</View>
</AppPressable>
</View>
)}
</ScrollView>
@@ -3,15 +3,16 @@
*
* Read-only display of availability schedule.
* Editing is done via separate modal screens.
* Uses React Query for cache synchronization with edit screens.
*/
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import { ActivityIndicator, Alert, ScrollView, Text, View } from "react-native";
import { forwardRef, useCallback, useImperativeHandle, useMemo } from "react";
import { ActivityIndicator, Alert, RefreshControl, ScrollView, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { CalComAPIService, type Schedule } from "@/services/calcom";
import { useDeleteSchedule, useScheduleById, useSetScheduleAsDefault } from "@/hooks/useSchedules";
import type { ScheduleAvailability } from "@/services/types";
import { showErrorAlert } from "@/utils/alerts";
@@ -69,137 +70,93 @@ export const AvailabilityDetailScreen = forwardRef<
const router = useRouter();
const insets = useSafeAreaInsets();
const [loading, setLoading] = useState(true);
const [_schedule, setSchedule] = useState<Schedule | null>(null);
const [scheduleName, setScheduleName] = useState("");
const [timeZone, setTimeZone] = useState("");
const [isDefault, setIsDefault] = useState(false);
const [availability, setAvailability] = useState<Record<number, ScheduleAvailability[]>>({});
const [overrides, setOverrides] = useState<
{
date: string;
startTime: string;
endTime: string;
}[]
>([]);
// Use React Query hook for data fetching and cache synchronization
const { data: schedule, isLoading, error, refetch, isRefetching } = useScheduleById(Number(id));
const processScheduleData = useCallback(
(scheduleData: NonNullable<Awaited<ReturnType<typeof CalComAPIService.getScheduleById>>>) => {
const name = scheduleData.name ?? "";
const tz = scheduleData.timeZone ?? "UTC";
const isDefaultSchedule = scheduleData.isDefault ?? false;
// Mutation hooks for actions
const { mutate: setAsDefaultMutation } = useSetScheduleAsDefault();
const { mutate: deleteScheduleMutation } = useDeleteSchedule();
setSchedule(scheduleData);
setScheduleName(name);
setTimeZone(tz);
setIsDefault(isDefaultSchedule);
// Derive schedule properties from the query data
const scheduleName = schedule?.name ?? "";
const timeZone = schedule?.timeZone ?? "";
const isDefault = schedule?.isDefault ?? false;
const availabilityMap: Record<number, ScheduleAvailability[]> = {};
// Process availability data into a map by day number
const availability = useMemo(() => {
const availabilityMap: Record<number, ScheduleAvailability[]> = {};
const availabilityArray = scheduleData.availability;
if (availabilityArray && Array.isArray(availabilityArray)) {
availabilityArray.forEach((slot) => {
let days: number[] = [];
if (Array.isArray(slot.days)) {
days = slot.days
.map((day) => {
if (typeof day === "string" && DAY_NAME_TO_NUMBER[day] !== undefined) {
return DAY_NAME_TO_NUMBER[day];
const availabilityArray = schedule?.availability;
if (availabilityArray && Array.isArray(availabilityArray)) {
availabilityArray.forEach((slot) => {
let days: number[] = [];
if (Array.isArray(slot.days)) {
days = slot.days
.map((day) => {
if (typeof day === "string" && DAY_NAME_TO_NUMBER[day] !== undefined) {
return DAY_NAME_TO_NUMBER[day];
}
if (typeof day === "string") {
const parsed = parseInt(day, 10);
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 6) {
return parsed;
}
if (typeof day === "string") {
const parsed = parseInt(day, 10);
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 6) {
return parsed;
}
}
if (typeof day === "number" && day >= 0 && day <= 6) {
return day;
}
return null;
})
.filter((day): day is number => day !== null);
}
if (typeof day === "number" && day >= 0 && day <= 6) {
return day;
}
return null;
})
.filter((day): day is number => day !== null);
}
days.forEach((day) => {
if (!availabilityMap[day]) {
availabilityMap[day] = [];
}
days.forEach((day) => {
if (!availabilityMap[day]) {
availabilityMap[day] = [];
}
const startTime = slot.startTime ?? "09:00:00";
const endTime = slot.endTime ?? "17:00:00";
availabilityMap[day].push({
days: [day.toString()],
startTime,
endTime,
});
const startTime = slot.startTime ?? "09:00:00";
const endTime = slot.endTime ?? "17:00:00";
availabilityMap[day].push({
days: [day.toString()],
startTime,
endTime,
});
});
}
setAvailability(availabilityMap);
const overridesArray = scheduleData.overrides;
if (overridesArray && Array.isArray(overridesArray)) {
const formattedOverrides = overridesArray.map((override) => {
const date = override.date ?? "";
const startTime = override.startTime ?? "00:00";
const endTime = override.endTime ?? "00:00";
return { date, startTime, endTime };
});
setOverrides(formattedOverrides);
} else {
setOverrides([]);
}
},
[]
);
const fetchSchedule = useCallback(async () => {
setLoading(true);
let scheduleData: Awaited<ReturnType<typeof CalComAPIService.getScheduleById>> = null;
try {
scheduleData = await CalComAPIService.getScheduleById(Number(id));
} catch (error) {
console.error("Error fetching schedule");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
console.debug("[AvailabilityDetailScreen] fetchSchedule failed", {
message,
});
}
showErrorAlert("Error", "Failed to load availability. Please try again.");
router.back();
setLoading(false);
return;
});
}
if (scheduleData) {
processScheduleData(scheduleData);
}
setLoading(false);
}, [id, router, processScheduleData]);
return availabilityMap;
}, [schedule?.availability]);
useEffect(() => {
if (id) {
fetchSchedule();
// Process overrides data
const overrides = useMemo(() => {
const overridesArray = schedule?.overrides;
if (overridesArray && Array.isArray(overridesArray)) {
return overridesArray.map((override) => {
const date = override.date ?? "";
const startTime = override.startTime ?? "00:00";
const endTime = override.endTime ?? "00:00";
return { date, startTime, endTime };
});
}
}, [id, fetchSchedule]);
return [];
}, [schedule?.overrides]);
const handleSetAsDefault = useCallback(async () => {
const handleSetAsDefault = useCallback(() => {
if (isDefault) {
Alert.alert("Info", "This schedule is already set as default");
return;
}
try {
await CalComAPIService.updateSchedule(Number(id), {
isDefault: true,
});
setIsDefault(true);
Alert.alert("Success", "Availability set as default successfully");
} catch {
showErrorAlert("Error", "Failed to set availability as default. Please try again.");
}
}, [isDefault, id]);
setAsDefaultMutation(Number(id), {
onSuccess: () => {
Alert.alert("Success", "Availability set as default successfully");
},
onError: () => {
showErrorAlert("Error", "Failed to set availability as default. Please try again.");
},
});
}, [isDefault, id, setAsDefaultMutation]);
const handleDelete = useCallback(() => {
if (isDefault) {
@@ -215,19 +172,21 @@ export const AvailabilityDetailScreen = forwardRef<
{
text: "Delete",
style: "destructive",
onPress: async () => {
try {
await CalComAPIService.deleteSchedule(Number(id));
Alert.alert("Success", "Availability deleted successfully", [
{ text: "OK", onPress: () => router.back() },
]);
} catch {
showErrorAlert("Error", "Failed to delete availability. Please try again.");
}
onPress: () => {
deleteScheduleMutation(Number(id), {
onSuccess: () => {
Alert.alert("Success", "Availability deleted successfully", [
{ text: "OK", onPress: () => router.back() },
]);
},
onError: () => {
showErrorAlert("Error", "Failed to delete availability. Please try again.");
},
});
},
},
]);
}, [isDefault, scheduleName, id, router]);
}, [isDefault, scheduleName, id, router, deleteScheduleMutation]);
// Expose handlers via ref
useImperativeHandle(
@@ -235,9 +194,9 @@ export const AvailabilityDetailScreen = forwardRef<
() => ({
setAsDefault: handleSetAsDefault,
delete: handleDelete,
refresh: fetchSchedule,
refresh: () => refetch(),
}),
[handleSetAsDefault, handleDelete, fetchSchedule]
[handleSetAsDefault, handleDelete, refetch]
);
// Expose handlers to parent for header menu
@@ -250,10 +209,23 @@ export const AvailabilityDetailScreen = forwardRef<
}
}, [onActionsReady, handleSetAsDefault, handleDelete]);
// Handle error state - must be in useEffect to avoid side effects during render
useEffect(() => {
if (error) {
showErrorAlert("Error", "Failed to load availability. Please try again.");
router.back();
}
}, [error, router]);
// Count enabled days
const enabledDaysCount = Object.keys(availability).length;
if (loading) {
// Early return for error state (after useEffect hooks)
if (error) {
return null;
}
if (isLoading) {
return (
<View className="flex-1 items-center justify-center bg-[#f2f2f7]">
<ActivityIndicator size="large" color="#000000" />
@@ -272,6 +244,7 @@ export const AvailabilityDetailScreen = forwardRef<
paddingBottom: insets.bottom + 100,
}}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={() => refetch()} />}
>
{/* Schedule Title Section */}
<View className="mb-4">
@@ -409,13 +382,20 @@ export const AvailabilityDetailScreen = forwardRef<
</View>
)}
{/* No Overrides Message */}
{/* No Overrides Message - Still navigable to add overrides */}
{overrides.length === 0 && (
<View className="mb-4 overflow-hidden rounded-xl bg-white">
<View className="px-4 py-3.5">
<Text className="text-[17px] text-black">Date Overrides</Text>
<Text className="mt-1 text-[15px] text-[#8E8E93]">No date overrides set</Text>
</View>
<AppPressable
onPress={() => router.push(`/edit-availability-override?id=${id}` as never)}
>
<View className="flex-row items-center justify-between px-4 py-3.5">
<View>
<Text className="text-[17px] text-black">Date Overrides</Text>
<Text className="mt-1 text-[15px] text-[#8E8E93]">No date overrides set</Text>
</View>
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
</View>
</AppPressable>
</View>
)}
</ScrollView>
@@ -4,8 +4,8 @@ import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } fro
import { Alert, ScrollView, Switch, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { useUpdateSchedule } from "@/hooks/useSchedules";
import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import type { ScheduleAvailability } from "@/services/types";
import { showErrorAlert } from "@/utils/alerts";
@@ -169,9 +169,11 @@ export const EditAvailabilityDayScreen = forwardRef<
const insets = useSafeAreaInsets();
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
// Use mutation hook for cache-synchronized updates
const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule();
const [isEnabled, setIsEnabled] = useState(false);
const [slots, setSlots] = useState<{ startTime: Date; endTime: Date }[]>([]);
const [isSaving, setIsSaving] = useState(false);
const dayName = DAYS[dayIndex] || "Day";
@@ -201,8 +203,8 @@ export const EditAvailabilityDayScreen = forwardRef<
// Notify parent of saving state
useEffect(() => {
onSavingChange?.(isSaving);
}, [isSaving, onSavingChange]);
onSavingChange?.(isMutating);
}, [isMutating, onSavingChange]);
const handleToggle = useCallback(
(value: boolean) => {
@@ -249,8 +251,8 @@ export const EditAvailabilityDayScreen = forwardRef<
});
}, []);
const handleSubmit = useCallback(async () => {
if (!schedule || isSaving) return;
const handleSubmit = useCallback(() => {
if (!schedule || isMutating) return;
// Validate all slots have end time after start time
// Compare time strings to avoid issues with Date object day components
@@ -276,20 +278,20 @@ export const EditAvailabilityDayScreen = forwardRef<
const fullAvailability = buildFullAvailability(schedule, dayIndex, daySlots);
setIsSaving(true);
try {
await CalComAPIService.updateSchedule(schedule.id, {
availability: fullAvailability,
});
Alert.alert("Success", `${dayName} updated successfully`, [
{ text: "OK", onPress: onSuccess },
]);
setIsSaving(false);
} catch {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
setIsSaving(false);
}
}, [schedule, dayIndex, dayName, isEnabled, slots, onSuccess, isSaving]);
updateSchedule(
{ id: schedule.id, updates: { availability: fullAvailability } },
{
onSuccess: () => {
Alert.alert("Success", `${dayName} updated successfully`, [
{ text: "OK", onPress: onSuccess },
]);
},
onError: () => {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
},
}
);
}, [schedule, dayIndex, dayName, isEnabled, slots, onSuccess, isMutating, updateSchedule]);
// Expose submit to parent via ref
useImperativeHandle(
@@ -4,8 +4,8 @@ import { Alert, Platform, ScrollView, Switch, Text, TouchableOpacity, View } fro
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { FullScreenModal } from "@/components/FullScreenModal";
import { useUpdateSchedule } from "@/hooks/useSchedules";
import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import type { ScheduleAvailability } from "@/services/types";
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
import { shadows } from "@/utils/shadows";
@@ -170,9 +170,11 @@ export const EditAvailabilityDayScreen = forwardRef<
>(function EditAvailabilityDayScreen({ schedule, dayIndex, onSuccess, onSavingChange }, ref) {
const insets = useSafeAreaInsets();
// Use mutation hook for cache-synchronized updates
const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule();
const [isEnabled, setIsEnabled] = useState(false);
const [slots, setSlots] = useState<{ startTime: string; endTime: string }[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [showTimePicker, setShowTimePicker] = useState<{
slotIndex: number;
type: "start" | "end";
@@ -252,8 +254,8 @@ export const EditAvailabilityDayScreen = forwardRef<
// Notify parent of saving state
useEffect(() => {
onSavingChange?.(isSaving);
}, [isSaving, onSavingChange]);
onSavingChange?.(isMutating);
}, [isMutating, onSavingChange]);
const handleToggle = useCallback(
(value: boolean) => {
@@ -297,8 +299,8 @@ export const EditAvailabilityDayScreen = forwardRef<
[showTimePicker]
);
const handleSubmit = useCallback(async () => {
if (!schedule || isSaving) return;
const handleSubmit = useCallback(() => {
if (!schedule || isMutating) return;
// Validate all slots have end time after start time
if (isEnabled) {
@@ -320,19 +322,19 @@ export const EditAvailabilityDayScreen = forwardRef<
const fullAvailability = buildFullAvailability(schedule, dayIndex, daySlots);
setIsSaving(true);
try {
await CalComAPIService.updateSchedule(schedule.id, {
availability: fullAvailability,
});
showSuccessAlert("Success", `${dayName} updated successfully`);
onSuccess();
setIsSaving(false);
} catch {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
setIsSaving(false);
}
}, [schedule, dayIndex, dayName, isEnabled, slots, onSuccess, isSaving]);
updateSchedule(
{ id: schedule.id, updates: { availability: fullAvailability } },
{
onSuccess: () => {
showSuccessAlert("Success", `${dayName} updated successfully`);
onSuccess();
},
onError: () => {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
},
}
);
}, [schedule, dayIndex, dayName, isEnabled, slots, onSuccess, isMutating, updateSchedule]);
useImperativeHandle(
ref,
@@ -6,8 +6,7 @@ import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } fro
import { Alert, KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { TIMEZONES as ALL_TIMEZONES } from "@/constants/timezones";
import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import { type Schedule, useUpdateSchedule } from "@/hooks/useSchedules";
import { showErrorAlert } from "@/utils/alerts";
// Format timezones for display
@@ -39,7 +38,9 @@ export const EditAvailabilityNameScreen = forwardRef<
const [name, setName] = useState("");
const [timezone, setTimezone] = useState("UTC");
const [isSaving, setIsSaving] = useState(false);
// Use the mutation hook for updating schedules with optimistic updates
const { mutate: updateSchedule, isPending: isSaving } = useUpdateSchedule();
// Initialize from schedule
useEffect(() => {
@@ -58,7 +59,7 @@ export const EditAvailabilityNameScreen = forwardRef<
setTimezone(tz);
}, []);
const handleSubmit = useCallback(async () => {
const handleSubmit = useCallback(() => {
if (!schedule || isSaving) return;
const trimmedName = name.trim();
@@ -67,19 +68,26 @@ export const EditAvailabilityNameScreen = forwardRef<
return;
}
setIsSaving(true);
try {
await CalComAPIService.updateSchedule(schedule.id, {
name: trimmedName,
timeZone: timezone,
});
Alert.alert("Success", "Schedule updated successfully", [{ text: "OK", onPress: onSuccess }]);
setIsSaving(false);
} catch {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
setIsSaving(false);
}
}, [schedule, name, timezone, onSuccess, isSaving]);
updateSchedule(
{
id: schedule.id,
updates: {
name: trimmedName,
timeZone: timezone,
},
},
{
onSuccess: () => {
Alert.alert("Success", "Schedule updated successfully", [
{ text: "OK", onPress: onSuccess },
]);
},
onError: () => {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
},
}
);
}, [schedule, name, timezone, onSuccess, isSaving, updateSchedule]);
// Expose submit to parent via ref
useImperativeHandle(
@@ -14,8 +14,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { FullScreenModal } from "@/components/FullScreenModal";
import { TIMEZONES as ALL_TIMEZONES } from "@/constants/timezones";
import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import { type Schedule, useUpdateSchedule } from "@/hooks/useSchedules";
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
import { shadows } from "@/utils/shadows";
@@ -44,9 +43,11 @@ export const EditAvailabilityNameScreen = forwardRef<
const [name, setName] = useState("");
const [timezone, setTimezone] = useState("UTC");
const [isSaving, setIsSaving] = useState(false);
const [showTimezoneModal, setShowTimezoneModal] = useState(false);
// Use the mutation hook for updating schedules with optimistic updates
const { mutate: updateSchedule, isPending: isSaving } = useUpdateSchedule();
// Initialize from schedule
useEffect(() => {
if (schedule) {
@@ -60,7 +61,7 @@ export const EditAvailabilityNameScreen = forwardRef<
onSavingChange?.(isSaving);
}, [isSaving, onSavingChange]);
const handleSubmit = useCallback(async () => {
const handleSubmit = useCallback(() => {
if (!schedule || isSaving) return;
const trimmedName = name.trim();
@@ -69,20 +70,25 @@ export const EditAvailabilityNameScreen = forwardRef<
return;
}
setIsSaving(true);
try {
await CalComAPIService.updateSchedule(schedule.id, {
name: trimmedName,
timeZone: timezone,
});
showSuccessAlert("Success", "Schedule updated successfully");
onSuccess();
setIsSaving(false);
} catch {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
setIsSaving(false);
}
}, [schedule, name, timezone, onSuccess, isSaving]);
updateSchedule(
{
id: schedule.id,
updates: {
name: trimmedName,
timeZone: timezone,
},
},
{
onSuccess: () => {
showSuccessAlert("Success", "Schedule updated successfully");
onSuccess();
},
onError: () => {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
},
}
);
}, [schedule, name, timezone, onSuccess, isSaving, updateSchedule]);
// Expose submit to parent via ref
useImperativeHandle(
@@ -3,8 +3,8 @@ 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 { useUpdateSchedule } from "@/hooks/useSchedules";
import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
// Convert 24-hour time to 12-hour format with AM/PM
@@ -89,13 +89,15 @@ export const EditAvailabilityOverrideScreen = forwardRef<
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"));
const [isSaving, setIsSaving] = useState(false);
// Initialize from existing override if editing
useEffect(() => {
@@ -114,8 +116,8 @@ export const EditAvailabilityOverrideScreen = forwardRef<
// Notify parent of saving state
useEffect(() => {
onSavingChange?.(isSaving);
}, [isSaving, onSavingChange]);
onSavingChange?.(isMutating);
}, [isMutating, onSavingChange]);
const handleDateChange = useCallback((date: Date) => {
setSelectedDate(date);
@@ -130,26 +132,26 @@ export const EditAvailabilityOverrideScreen = forwardRef<
}, []);
const saveOverrides = useCallback(
async (
(
newOverrides: { date: string; startTime: string; endTime: string }[],
successMessage: string
) => {
if (!schedule) return;
setIsSaving(true);
try {
await CalComAPIService.updateSchedule(schedule.id, {
overrides: newOverrides,
});
showSuccessAlert("Success", successMessage);
onSuccess();
setIsSaving(false);
} catch {
showErrorAlert("Error", "Failed to save override. Please try again.");
setIsSaving(false);
}
updateSchedule(
{ id: schedule.id, updates: { overrides: newOverrides } },
{
onSuccess: () => {
showSuccessAlert("Success", successMessage);
onSuccess();
},
onError: () => {
showErrorAlert("Error", "Failed to save override. Please try again.");
},
}
);
},
[schedule, onSuccess]
[schedule, onSuccess, updateSchedule]
);
const handleDeleteOverride = useCallback(
@@ -186,8 +188,8 @@ export const EditAvailabilityOverrideScreen = forwardRef<
[schedule, saveOverrides]
);
const handleSubmit = useCallback(async () => {
if (!schedule || isSaving) return;
const handleSubmit = useCallback(() => {
if (!schedule || isMutating) return;
const dateStr = dateToDateString(selectedDate);
@@ -236,9 +238,9 @@ export const EditAvailabilityOverrideScreen = forwardRef<
{ text: "Cancel", style: "cancel" },
{
text: "Replace",
onPress: async () => {
onPress: () => {
newOverrides[existingIndex] = newOverride;
await saveOverrides(newOverrides, "Override replaced successfully");
saveOverrides(newOverrides, "Override replaced successfully");
},
},
]
@@ -249,7 +251,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
newOverrides.push(newOverride);
}
await saveOverrides(newOverrides, successMessage);
saveOverrides(newOverrides, successMessage);
}, [
schedule,
selectedDate,
@@ -259,7 +261,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
isEditing,
overrideIndex,
saveOverrides,
isSaving,
isMutating,
]);
// Expose submit to parent via ref
@@ -13,8 +13,8 @@ import {
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { FullScreenModal } from "@/components/FullScreenModal";
import { useUpdateSchedule } from "@/hooks/useSchedules";
import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
import { shadows } from "@/utils/shadows";
@@ -77,13 +77,15 @@ export const EditAvailabilityOverrideScreen = forwardRef<
) {
const insets = useSafeAreaInsets();
// Use mutation hook for cache-synchronized updates
const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule();
const isEditing = overrideIndex !== undefined;
const [selectedDate, setSelectedDate] = useState("");
const [isUnavailable, setIsUnavailable] = useState(false);
const [startTime, setStartTime] = useState("09:00");
const [endTime, setEndTime] = useState("17:00");
const [isSaving, setIsSaving] = useState(false);
const [showTimePicker, setShowTimePicker] = useState<{
type: "start" | "end";
} | null>(null);
@@ -152,8 +154,8 @@ export const EditAvailabilityOverrideScreen = forwardRef<
// Notify parent of saving state
useEffect(() => {
onSavingChange?.(isSaving);
}, [isSaving, onSavingChange]);
onSavingChange?.(isMutating);
}, [isMutating, onSavingChange]);
const handleTimeSelect = useCallback(
(time: string) => {
@@ -170,30 +172,30 @@ export const EditAvailabilityOverrideScreen = forwardRef<
);
const saveOverrides = useCallback(
async (
(
newOverrides: { date: string; startTime: string; endTime: string }[],
successMessage: string
) => {
if (!schedule) return;
setIsSaving(true);
try {
await CalComAPIService.updateSchedule(schedule.id, {
overrides: newOverrides,
});
setIsSaving(false);
if (Platform.OS === "web") {
showSuccessAlert("Success", successMessage);
onSuccess();
} else {
Alert.alert("Success", successMessage, [{ text: "OK", onPress: onSuccess }]);
updateSchedule(
{ id: schedule.id, updates: { overrides: newOverrides } },
{
onSuccess: () => {
if (Platform.OS === "web") {
showSuccessAlert("Success", successMessage);
onSuccess();
} else {
Alert.alert("Success", successMessage, [{ text: "OK", onPress: onSuccess }]);
}
},
onError: () => {
showErrorAlert("Error", "Failed to save override. Please try again.");
},
}
} catch {
showErrorAlert("Error", "Failed to save override. Please try again.");
setIsSaving(false);
}
);
},
[schedule, onSuccess]
[schedule, onSuccess, updateSchedule]
);
const handleDeleteOverride = useCallback(
@@ -230,8 +232,8 @@ export const EditAvailabilityOverrideScreen = forwardRef<
[schedule, saveOverrides]
);
const handleSubmit = useCallback(async () => {
if (!schedule || isSaving) return;
const handleSubmit = useCallback(() => {
if (!schedule || isMutating) return;
if (!selectedDate) {
showErrorAlert("Error", "Please enter a date (YYYY-MM-DD format)");
@@ -283,9 +285,9 @@ export const EditAvailabilityOverrideScreen = forwardRef<
{ text: "Cancel", style: "cancel" },
{
text: "Replace",
onPress: async () => {
onPress: () => {
newOverrides[existingIndex] = newOverride;
await saveOverrides(newOverrides, "Override replaced successfully");
saveOverrides(newOverrides, "Override replaced successfully");
},
},
]
@@ -295,7 +297,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
newOverrides.push(newOverride);
}
await saveOverrides(newOverrides, successMessage);
saveOverrides(newOverrides, successMessage);
}, [
schedule,
selectedDate,
@@ -305,7 +307,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
isEditing,
overrideIndex,
saveOverrides,
isSaving,
isMutating,
]);
useImperativeHandle(
+67 -7
View File
@@ -156,7 +156,7 @@ export function useCreateSchedule() {
}
/**
* Hook to update a schedule
* Hook to update a schedule with optimistic updates
*
* @returns Mutation function and state
*
@@ -176,14 +176,74 @@ export function useUpdateSchedule() {
return useMutation({
mutationFn: ({ id, updates }: { id: number; updates: UpdateScheduleInput }) =>
CalComAPIService.updateSchedule(id, updates),
onSuccess: (updatedSchedule, variables) => {
// Invalidate the list
queryClient.invalidateQueries({ queryKey: queryKeys.schedules.lists() });
onMutate: async ({ id, updates }) => {
// Cancel any outgoing refetches to prevent overwriting optimistic update
await queryClient.cancelQueries({ queryKey: queryKeys.schedules.detail(id) });
await queryClient.cancelQueries({ queryKey: queryKeys.schedules.lists() });
// Update the specific schedule in cache
queryClient.setQueryData(queryKeys.schedules.detail(variables.id), updatedSchedule);
// Snapshot the previous values for rollback
const previousSchedule = queryClient.getQueryData<Schedule | null>(
queryKeys.schedules.detail(id)
);
const previousSchedules = queryClient.getQueryData<Schedule[]>(queryKeys.schedules.lists());
// Optimistically update the detail cache if it exists
if (previousSchedule) {
const optimisticSchedule: Schedule = {
...previousSchedule,
...updates,
name: updates.name ?? previousSchedule.name,
timeZone: updates.timeZone ?? previousSchedule.timeZone,
};
queryClient.setQueryData(queryKeys.schedules.detail(id), optimisticSchedule);
}
// Update the list cache optimistically (even if detail cache doesn't exist)
if (previousSchedules) {
const updatedList = previousSchedules.map((s) => {
if (s.id === id) {
// Merge updates into the existing schedule from the list
return {
...s,
...updates,
name: updates.name ?? s.name,
timeZone: updates.timeZone ?? s.timeZone,
};
}
return s;
});
queryClient.setQueryData(queryKeys.schedules.lists(), sortSchedules(updatedList));
}
return { previousSchedule, previousSchedules };
},
onError: (_error) => {
onSuccess: (updatedSchedule, variables) => {
// Update the specific schedule in cache with server response
queryClient.setQueryData(queryKeys.schedules.detail(variables.id), updatedSchedule);
// Update the list cache with the server response
const currentSchedules = queryClient.getQueryData<Schedule[]>(queryKeys.schedules.lists());
if (currentSchedules) {
const updatedList = currentSchedules.map((s) =>
s.id === variables.id ? updatedSchedule : s
);
queryClient.setQueryData(queryKeys.schedules.lists(), sortSchedules(updatedList));
} else {
// If list cache doesn't exist, invalidate to trigger refetch when user navigates to list
queryClient.invalidateQueries({ queryKey: queryKeys.schedules.lists() });
}
},
onError: (_error, variables, context) => {
// Rollback to previous values on error
if (context?.previousSchedule) {
queryClient.setQueryData(
queryKeys.schedules.detail(variables.id),
context.previousSchedule
);
}
if (context?.previousSchedules) {
queryClient.setQueryData(queryKeys.schedules.lists(), context.previousSchedules);
}
console.error("Failed to update schedule");
},
});