diff --git a/xtablo-expo/app/(app)/task/[id].tsx b/xtablo-expo/app/(app)/task/[id].tsx new file mode 100644 index 0000000..86bfd08 --- /dev/null +++ b/xtablo-expo/app/(app)/task/[id].tsx @@ -0,0 +1,343 @@ +import React, { useState, useEffect } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + StyleSheet, + SafeAreaView, + Alert, + Platform, +} from "react-native"; +import { useLocalSearchParams, router } from "expo-router"; +import { ArrowLeft, User, Layers, Calendar } from "lucide-react-native"; +import DateTimePicker from "@react-native-community/datetimepicker"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; +import { useTasksByTablo, useCreateTask, useUpdateTask, useDeleteTask } from "@/hooks/tasks"; +import { useTabloEtapes } from "@/hooks/etapes"; +import { useTabloMembers } from "@/hooks/members"; +import { TaskStatus, TASK_STATUSES } from "@/types/tasks.types"; +import StatusControl from "@/components/tasks/StatusControl"; +import AssigneePicker from "@/components/tasks/AssigneePicker"; +import EtapePicker from "@/components/tasks/EtapePicker"; + +export default function TaskDetailScreen() { + const { id, tabloId } = useLocalSearchParams<{ id: string; tabloId: string }>(); + const isCreateMode = id === "new"; + + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const bgColor = useThemeColor({ light: "#f8fafc", dark: "#111827" }, "background"); + const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text"); + const headerBg = useThemeColor({ light: "#ffffff", dark: "#1f2937" }, "background"); + const inputBg = useThemeColor({ light: "#f1f5f9", dark: "#374151" }, "background"); + const borderColor = isDark ? "#374151" : "#e5e7eb"; + const subtextColor = useThemeColor({ light: "#6b7280", dark: "#9ca3af" }, "text"); + + // Data + const { data: tasks } = useTasksByTablo(tabloId); + const { data: etapes } = useTabloEtapes(tabloId); + const { data: members } = useTabloMembers(tabloId); + const existingTask = isCreateMode ? null : tasks?.find((t) => t.id === id); + + // Form state + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [status, setStatus] = useState("todo"); + const [assigneeId, setAssigneeId] = useState(null); + const [parentTaskId, setParentTaskId] = useState(null); + const [dueDate, setDueDate] = useState(null); + const [showDatePicker, setShowDatePicker] = useState(false); + + // Picker visibility + const [assigneePickerVisible, setAssigneePickerVisible] = useState(false); + const [etapePickerVisible, setEtapePickerVisible] = useState(false); + + // Mutations + const { mutate: createTask, isPending: isCreating } = useCreateTask(); + const { mutate: updateTask, isPending: isUpdating } = useUpdateTask(); + const { mutate: deleteTask, isPending: isDeleting } = useDeleteTask(); + const isPending = isCreating || isUpdating || isDeleting; + + // Populate form for edit mode + useEffect(() => { + if (existingTask) { + setTitle(existingTask.title); + setDescription(existingTask.description ?? ""); + setStatus(existingTask.status); + setAssigneeId(existingTask.assignee_id); + setParentTaskId(existingTask.parent_task_id); + setDueDate(existingTask.due_date ? new Date(existingTask.due_date) : null); + } + }, [existingTask]); + + const handleSave = () => { + if (!title.trim() || !tabloId) return; + + const dueDateStr = dueDate + ? `${dueDate.getFullYear()}-${String(dueDate.getMonth() + 1).padStart(2, "0")}-${String(dueDate.getDate()).padStart(2, "0")}` + : null; + + if (isCreateMode) { + createTask( + { + tablo_id: tabloId, + title: title.trim(), + description: description.trim() || null, + status, + assignee_id: assigneeId, + parent_task_id: parentTaskId, + due_date: dueDateStr, + }, + { onSuccess: () => router.back() } + ); + } else { + updateTask( + { + id: id!, + tabloId: tabloId!, + title: title.trim(), + description: description.trim() || null, + status, + assignee_id: assigneeId, + parent_task_id: parentTaskId, + due_date: dueDateStr, + }, + { onSuccess: () => router.back() } + ); + } + }; + + const handleDelete = () => { + Alert.alert("Supprimer la tâche", "Cette action est irréversible.", [ + { text: "Annuler", style: "cancel" }, + { + text: "Supprimer", + style: "destructive", + onPress: () => { + deleteTask({ id: id!, tabloId: tabloId! }, { onSuccess: () => router.back() }); + }, + }, + ]); + }; + + // Derived display values + const assigneeName = members?.find((m) => m.id === assigneeId)?.name ?? "Non assigné"; + const etapeName = etapes?.find((e) => e.id === parentTaskId)?.title ?? "Sans Étape"; + const dueDateDisplay = dueDate + ? dueDate.toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric" }) + : "Aucune"; + + return ( + + {/* Header */} + + router.back()} style={styles.backButton}> + + + + {isCreateMode ? "Nouvelle tâche" : "Modifier la tâche"} + + + + + {/* Title */} + Titre + + + {/* Description */} + Description + + + {/* Status */} + Statut + + + {/* Assignee */} + Assigné à + setAssigneePickerVisible(true)} + > + + + {assigneeName} + + + + {/* Etape */} + Étape + setEtapePickerVisible(true)} + > + + + {etapeName} + + + + {/* Due date */} + Date limite + setShowDatePicker(true)} + > + + + {dueDateDisplay} + + {dueDate && ( + setDueDate(null)}> + Retirer + + )} + + + {showDatePicker && ( + { + setShowDatePicker(Platform.OS !== "ios"); + if (date) setDueDate(date); + }} + /> + )} + + {/* Save button */} + + + {isPending ? "..." : isCreateMode ? "Créer" : "Enregistrer"} + + + + {/* Delete button (edit mode only) */} + {!isCreateMode && ( + + Supprimer la tâche + + )} + + + + + {/* Pickers */} + setAssigneePickerVisible(false)} + members={members ?? []} + selectedId={assigneeId} + onSelect={setAssigneeId} + /> + setEtapePickerVisible(false)} + etapes={etapes ?? []} + selectedId={parentTaskId} + onSelect={setParentTaskId} + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 12, + paddingVertical: 14, + borderBottomWidth: StyleSheet.hairlineWidth, + gap: 10, + }, + backButton: { + padding: 4, + }, + headerTitle: { + fontSize: 18, + fontWeight: "700", + }, + form: { + flex: 1, + padding: 16, + }, + label: { + fontSize: 14, + fontWeight: "600", + marginTop: 16, + marginBottom: 6, + }, + input: { + borderRadius: 10, + paddingHorizontal: 14, + paddingVertical: 12, + fontSize: 15, + }, + textArea: { + minHeight: 100, + textAlignVertical: "top", + }, + pickerButton: { + flexDirection: "row", + alignItems: "center", + borderRadius: 10, + paddingHorizontal: 14, + paddingVertical: 12, + gap: 10, + }, + pickerText: { + flex: 1, + fontSize: 15, + }, + saveButton: { + backgroundColor: "#3b82f6", + borderRadius: 12, + paddingVertical: 14, + alignItems: "center", + marginTop: 24, + }, + saveButtonDisabled: { + opacity: 0.5, + }, + saveButtonText: { + color: "#ffffff", + fontSize: 16, + fontWeight: "700", + }, + deleteButton: { + borderRadius: 12, + paddingVertical: 14, + alignItems: "center", + marginTop: 12, + }, + deleteButtonText: { + color: "#ef4444", + fontSize: 15, + fontWeight: "600", + }, +}); diff --git a/xtablo-expo/app/(app)/task/_layout.tsx b/xtablo-expo/app/(app)/task/_layout.tsx new file mode 100644 index 0000000..41aa060 --- /dev/null +++ b/xtablo-expo/app/(app)/task/_layout.tsx @@ -0,0 +1,5 @@ +import { Stack } from "expo-router"; + +export default function TaskLayout() { + return ; +}