From aa1906a9bbe0dacf627b045c4c872639744f0e2d Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 19 Aug 2025 07:23:52 +0000 Subject: [PATCH] Enhance task management with start dates and improved visibility Introduce task start dates, allowing for future task scheduling and better visibility for managers and employees. Admins can now see all tasks, while other roles only see tasks that have started or have no start date. Includes UI updates to visually distinguish future tasks and a new form for creating tasks with start and due dates. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cc2d271b-82f0-4d6a-9302-a0b067ffb429 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/cc2d271b-82f0-4d6a-9302-a0b067ffb429/kX6A8MG --- client/src/components/tasks/NewTaskForm.tsx | 545 ++++++++++++++++++++ client/src/components/tasks/TaskForm.tsx | 470 +---------------- client/src/pages/Tasks.tsx | 152 ++++-- replit.md | 2 +- server/routes.ts | 2 +- server/storage.ts | 33 +- shared/schema.ts | 9 +- 7 files changed, 701 insertions(+), 512 deletions(-) create mode 100644 client/src/components/tasks/NewTaskForm.tsx 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 + +