From 66d9ab9ad8b89c60b3ab46bed059619f7a02a037 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Wed, 15 Apr 2026 14:07:42 +0200 Subject: [PATCH] fix(expo): address review findings - cache invalidation, date handling, cleanup - Add tasks cache invalidation to etape create/update hooks - Fix timezone-unsafe overdue date comparison in TaskRow - Add due date picker to EtapeSheet (was missing from spec) - Use initialized ref to prevent form state overwrite on refetch - Remove unused deleteTask import from TaskList Co-Authored-By: Claude Opus 4.6 (1M context) --- xtablo-expo/app/(app)/task/[id].tsx | 8 ++- xtablo-expo/components/tasks/EtapeSheet.tsx | 61 ++++++++++++++++++++- xtablo-expo/components/tasks/TaskList.tsx | 5 +- xtablo-expo/components/tasks/TaskRow.tsx | 2 +- xtablo-expo/hooks/etapes.ts | 2 + 5 files changed, 69 insertions(+), 9 deletions(-) diff --git a/xtablo-expo/app/(app)/task/[id].tsx b/xtablo-expo/app/(app)/task/[id].tsx index 86bfd08..4aa3f67 100644 --- a/xtablo-expo/app/(app)/task/[id].tsx +++ b/xtablo-expo/app/(app)/task/[id].tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { View, Text, @@ -61,9 +61,11 @@ export default function TaskDetailScreen() { const { mutate: deleteTask, isPending: isDeleting } = useDeleteTask(); const isPending = isCreating || isUpdating || isDeleting; - // Populate form for edit mode + // Populate form for edit mode (only once when data first arrives) + const initialized = useRef(false); useEffect(() => { - if (existingTask) { + if (existingTask && !initialized.current) { + initialized.current = true; setTitle(existingTask.title); setDescription(existingTask.description ?? ""); setStatus(existingTask.status); diff --git a/xtablo-expo/components/tasks/EtapeSheet.tsx b/xtablo-expo/components/tasks/EtapeSheet.tsx index 4cb5d2e..8bcd221 100644 --- a/xtablo-expo/components/tasks/EtapeSheet.tsx +++ b/xtablo-expo/components/tasks/EtapeSheet.tsx @@ -9,7 +9,8 @@ import { Platform, StyleSheet, } from "react-native"; -import { X } from "lucide-react-native"; +import { X, Calendar } from "lucide-react-native"; +import DateTimePicker from "@react-native-community/datetimepicker"; import { useThemeColor } from "@/hooks/useThemeColor"; import { useColorScheme } from "@/hooks/useColorScheme"; import { Etape } from "@/types/tasks.types"; @@ -29,9 +30,12 @@ export default function EtapeSheet({ visible, onClose, tabloId, etape }: EtapeSh const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text"); const inputBg = useThemeColor({ light: "#f1f5f9", dark: "#374151" }, "background"); const borderColor = isDark ? "#374151" : "#e5e7eb"; + const subtextColor = useThemeColor({ light: "#6b7280", dark: "#9ca3af" }, "text"); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); + const [dueDate, setDueDate] = useState(null); + const [showDatePicker, setShowDatePicker] = useState(false); const { mutate: createEtape, isPending: isCreating } = useCreateEtape(); const { mutate: updateEtape, isPending: isUpdating } = useUpdateEtape(); @@ -43,15 +47,24 @@ export default function EtapeSheet({ visible, onClose, tabloId, etape }: EtapeSh if (etape) { setTitle(etape.title); setDescription(etape.description ?? ""); + setDueDate(etape.due_date ? new Date(etape.due_date) : null); } else { setTitle(""); setDescription(""); + setDueDate(null); } + setShowDatePicker(false); }, [etape, visible]); + const formatDueDate = (date: Date) => { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; + }; + const handleSave = () => { if (!title.trim()) return; + const dueDateStr = dueDate ? formatDueDate(dueDate) : null; + if (isEditMode && etape) { updateEtape( { @@ -59,6 +72,7 @@ export default function EtapeSheet({ visible, onClose, tabloId, etape }: EtapeSh tabloId, title: title.trim(), description: description.trim() || null, + due_date: dueDateStr, }, { onSuccess: onClose } ); @@ -68,12 +82,17 @@ export default function EtapeSheet({ visible, onClose, tabloId, etape }: EtapeSh tabloId, title: title.trim(), description: description.trim() || null, + due_date: dueDateStr, }, { onSuccess: onClose } ); } }; + const dueDateDisplay = dueDate + ? dueDate.toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric" }) + : "Aucune"; + return ( + + Date limite + setShowDatePicker(true)} + > + + + {dueDateDisplay} + + {dueDate && ( + setDueDate(null)}> + Retirer + + )} + + + {showDatePicker && ( + { + setShowDatePicker(Platform.OS !== "ios"); + if (date) setDueDate(date); + }} + /> + )} >(new Set()); diff --git a/xtablo-expo/components/tasks/TaskRow.tsx b/xtablo-expo/components/tasks/TaskRow.tsx index 7ac1c5a..85a8031 100644 --- a/xtablo-expo/components/tasks/TaskRow.tsx +++ b/xtablo-expo/components/tasks/TaskRow.tsx @@ -18,7 +18,7 @@ export default function TaskRow({ task, onPress }: TaskRowProps) { const borderColor = isDark ? "#374151" : "#e5e7eb"; const statusColor = TASK_STATUSES.find((s) => s.value === task.status)?.color ?? "#9ca3af"; - const isOverdue = task.due_date && new Date(task.due_date) < new Date(); + const isOverdue = task.due_date && task.due_date < new Date().toISOString().slice(0, 10); return ( { }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ queryKey: ["tablo-etapes", variables.tabloId] }); + queryClient.invalidateQueries({ queryKey: ["tasks", "tablo", variables.tabloId] }); }, onError: () => { Alert.alert("Erreur", "Impossible de créer l'étape."); @@ -83,6 +84,7 @@ export const useUpdateEtape = () => { }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ queryKey: ["tablo-etapes", variables.tabloId] }); + queryClient.invalidateQueries({ queryKey: ["tasks", "tablo", variables.tabloId] }); }, onError: () => { Alert.alert("Erreur", "Impossible de modifier l'étape.");