diff --git a/client/src/components/tasks/NewTaskForm.tsx b/client/src/components/tasks/NewTaskForm.tsx new file mode 100644 index 0000000..2caec60 --- /dev/null +++ b/client/src/components/tasks/NewTaskForm.tsx @@ -0,0 +1,545 @@ +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query"; +import { useAuthUnified } from "@/hooks/useAuthUnified"; +import { useStore } from "@/components/Layout"; +import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Calendar } from "@/components/ui/calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@/components/ui/form"; +import { apiRequest } from "@/lib/queryClient"; +import { insertTaskSchema, Task, Group } from "@shared/schema"; +import { z } from "zod"; +import { CalendarIcon, Clock, AlertTriangle, Info } from "lucide-react"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { cn } from "@/lib/utils"; + +type TaskWithRelations = Task & { + assignedUser?: { id: string; username: string; firstName?: string; lastName?: string; }; + creator?: { id: string; username: string; firstName?: string; lastName?: string; }; + group?: { id: number; name: string; color: string; }; + isFutureTask?: boolean; +}; + +// Schéma avec les deux dates : départ et échéance +const taskFormSchema = z.object({ + title: z.string().min(1, "Le titre est requis"), + description: z.string().optional(), + priority: z.enum(["low", "medium", "high"]).default("medium"), + status: z.enum(["pending", "completed"]).default("pending"), + assignedTo: z.string().min(1, "L'assignation est requise"), + startDate: z.date().optional(), // Date de départ + dueDate: z.date().optional(), // Date d'échéance +}).refine((data) => { + // Validation : la date d'échéance ne peut pas être antérieure à la date de départ + if (data.startDate && data.dueDate) { + return data.dueDate >= data.startDate; + } + return true; +}, { + message: "La date d'échéance ne peut pas être antérieure à la date de départ", + path: ["dueDate"] +}); + +type TaskFormData = z.infer; + +interface NewTaskFormProps { + task?: TaskWithRelations; + onClose: () => void; +} + +export default function NewTaskForm({ task, onClose }: NewTaskFormProps) { + const { user } = useAuthUnified(); + const { selectedStoreId } = useStore(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // État local pour gérer le magasin sélectionné (auto-sélection intelligente) + const [localSelectedStoreId, setLocalSelectedStoreId] = useState(null); + + // Récupération des magasins pour l'auto-sélection + const { data: groupsData = [] } = useQuery({ + queryKey: ['/api/groups'], + }); + + // Filtrer les groupes selon le magasin sélectionné pour les admins + const groups = Array.isArray(groupsData) ? ( + user?.role === 'admin' && selectedStoreId + ? groupsData.filter(g => g.id === selectedStoreId) + : groupsData + ) : []; + + // Auto-sélectionner le magasin selon les règles + useEffect(() => { + if (groups.length > 0 && !localSelectedStoreId) { + let defaultStoreId: number | null = null; + + if (user?.role === 'admin') { + if (selectedStoreId && groups.find(g => g.id === selectedStoreId)) { + defaultStoreId = selectedStoreId; + } else { + defaultStoreId = groups[0].id; + } + } else { + defaultStoreId = groups[0].id; + } + + if (defaultStoreId) { + setLocalSelectedStoreId(defaultStoreId); + } + } + + if (user?.role === 'admin' && selectedStoreId && selectedStoreId !== localSelectedStoreId) { + setLocalSelectedStoreId(selectedStoreId); + } + }, [groups, selectedStoreId, user?.role, localSelectedStoreId]); + + const form = useForm({ + resolver: zodResolver(taskFormSchema), + defaultValues: { + title: task?.title || "", + description: task?.description || "", + priority: (task?.priority as "low" | "medium" | "high") || "medium", + status: (task?.status as "pending" | "completed") || "pending", + assignedTo: task?.assignedTo || "", + startDate: task?.startDate ? new Date(task.startDate) : undefined, + dueDate: task?.dueDate ? new Date(task.dueDate) : undefined, + }, + }); + + // Mutation pour créer/modifier une tâche + const createMutation = useMutation({ + mutationFn: (data: any) => { + const taskData = { + ...data, + groupId: localSelectedStoreId, + createdBy: user?.id, + startDate: data.startDate || null, + dueDate: data.dueDate || null + }; + console.log('🚀 Creating task with complete data:', { + ...taskData, + startDate: taskData.startDate, + startDateType: typeof taskData.startDate, + dueDate: taskData.dueDate, + dueDateType: typeof taskData.dueDate + }); + return apiRequest("/api/tasks", "POST", taskData); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); + toast({ + title: "Succès", + description: "Tâche créée avec succès", + }); + onClose(); + }, + onError: (error: any) => { + console.error("Error creating task:", error); + toast({ + title: "Erreur", + description: "Impossible de créer la tâche", + variant: "destructive", + }); + }, + }); + + const updateMutation = useMutation({ + mutationFn: (data: any) => { + console.log('🔄 Updating task with complete data:', { + taskId: task?.id, + data, + startDate: data.startDate, + startDateType: typeof data.startDate, + dueDate: data.dueDate, + dueDateType: typeof data.dueDate + }); + return apiRequest(`/api/tasks/${task?.id}`, "PUT", data); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); + toast({ + title: "Succès", + description: "Tâche modifiée avec succès", + }); + onClose(); + }, + onError: (error: any) => { + console.error("Error updating task:", error); + toast({ + title: "Erreur", + description: "Impossible de modifier la tâche", + variant: "destructive", + }); + }, + }); + + const onSubmit = (data: TaskFormData) => { + console.log('📝 NewTaskForm onSubmit:', { + isEdit: !!task, + taskId: task?.id, + formData: data, + startDate: data.startDate, + dueDate: data.dueDate, + localSelectedStoreId + }); + + // Vérifier qu'un magasin est auto-sélectionné + if (!localSelectedStoreId) { + toast({ + title: "Erreur", + description: "Aucun magasin disponible pour créer une tâche", + variant: "destructive", + }); + return; + } + + if (task) { + // Modification de tâche existante + const submitData = { + title: data.title, + description: data.description, + priority: data.priority, + status: data.status, + assignedTo: data.assignedTo, + startDate: data.startDate || null, + dueDate: data.dueDate || null, + }; + console.log('🔄 Update task submitData:', submitData); + updateMutation.mutate(submitData); + } else { + // Création d'une nouvelle tâche + console.log('➕ Create task data:', data); + createMutation.mutate(data); + } + }; + + const getPriorityLabel = (priority: string) => { + switch (priority) { + case 'high': return 'Élevée'; + case 'medium': return 'Moyenne'; + case 'low': return 'Faible'; + default: return 'Moyenne'; + } + }; + + const getStatusLabel = (status: string) => { + switch (status) { + case 'pending': return 'En cours'; + case 'completed': return 'Terminée'; + default: return 'En cours'; + } + }; + + const today = new Date(); + + return ( +
+
+
+

+ {task ? "Modifier la tâche" : "Nouvelle tâche"} +

+

+ {task ? "Modifiez les informations de cette tâche" : "Créez une nouvelle tâche avec dates de départ et d'échéance"} +

+
+
+ +
+ + {/* Affichage du magasin auto-sélectionné */} + {localSelectedStoreId && ( +
+
+
+ +
+
+

+ Magasin sélectionné :{" "} + {(() => { + const selectedGroup = groups.find(g => g.id === localSelectedStoreId); + return selectedGroup ? ( + +

+ {selectedGroup.name} + + ) : `Magasin ID: ${localSelectedStoreId}`; + })()} +

+
+
+
+ )} + + {/* Avertissement si aucun magasin n'est disponible */} + {!localSelectedStoreId && groups.length === 0 && ( +
+
+
+ +
+
+

+ Aucun magasin disponible. Veuillez contacter un administrateur. +

+
+
+
+ )} + + {/* Titre */} + ( + + Titre * + + + + + + )} + /> + + {/* Description */} + ( + + Description + +