Files
LogiFlow/client/src/components/tasks/NewTaskForm.tsx
T
michaelschal d0cecf90f3 Refactor store context and improve responsiveness across the application
Refactored the `useStore` hook to be imported from a dedicated `StoreContext` to improve code organization and maintainability. Also, enhanced responsive design for better usability on various screen sizes, including tablet-specific adjustments for the sidebar and header.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 95e8aab4-f39f-4b48-aa57-a759ec15ba3d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/95e8aab4-f39f-4b48-aa57-a759ec15ba3d/A90CXrX
2025-09-11 06:14:18 +00:00

540 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "@/contexts/StoreContext";
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";
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<typeof taskFormSchema>;
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<number | null>(null);
// Récupération des magasins pour l'auto-sélection
const { data: groupsData = [] } = useQuery<Group[]>({
queryKey: ['/api/groups'],
enabled: !!user, // Seulement si l'utilisateur est connecté
});
// Filtrer les groupes selon le magasin sélectionné pour les admins
const groups = Array.isArray(groupsData) ? (
user?.role === 'admin' && selectedStoreId
? groupsData.filter(g => g && g.id === selectedStoreId)
: groupsData.filter(g => g)
) : [];
// 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<TaskFormData>({
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 (
<div className="space-y-6">
<div className="flex items-center justify-between border-b pb-4">
<div>
<h2 className="text-xl font-semibold">
{task ? "Modifier la tâche" : "Nouvelle tâche"}
</h2>
<p className="text-sm text-muted-foreground">
{task ? "Modifiez les informations de cette tâche" : "Créez une nouvelle tâche avec dates de départ et d'échéance"}
</p>
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit as any)} className="space-y-6">
{/* Affichage du magasin auto-sélectionné */}
{localSelectedStoreId && (
<div className="bg-blue-50 border border-blue-200 p-3">
<div className="flex items-center">
<div className="flex-shrink-0">
<Info className="h-5 w-5 text-blue-400" />
</div>
<div className="ml-3">
<p className="text-sm text-blue-800">
<span className="font-medium">Magasin sélectionné :</span>{" "}
{(() => {
const selectedGroup = groups.find(g => g.id === localSelectedStoreId);
return selectedGroup ? (
<span className="inline-flex items-center gap-2">
<div
className="w-3 h-3"
style={{ backgroundColor: selectedGroup.color || "#1976D2" }}
/>
{selectedGroup.name}
</span>
) : `Magasin ID: ${localSelectedStoreId}`;
})()}
</p>
</div>
</div>
</div>
)}
{/* Avertissement si aucun magasin n'est disponible */}
{!localSelectedStoreId && groups.length === 0 && (
<div className="bg-yellow-50 border border-yellow-200 p-3">
<div className="flex">
<div className="flex-shrink-0">
<AlertTriangle className="h-5 w-5 text-yellow-400" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-800">
Aucun magasin disponible. Veuillez contacter un administrateur.
</p>
</div>
</div>
</div>
)}
{/* Titre */}
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Titre *</FormLabel>
<FormControl>
<Input
placeholder="Titre de la tâche"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Description */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Description détaillée de la tâche (optionnel)"
rows={3}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Section des dates avec design clair */}
<div className="border p-4 bg-gray-50">
<div className="flex items-center mb-4">
<Clock className="h-5 w-5 text-blue-600 mr-2" />
<h3 className="font-medium text-gray-900">Planification temporelle</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Date de départ */}
<FormField
control={form.control}
name="startDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="text-sm font-medium">
📅 Date de départ
</FormLabel>
<FormDescription className="text-xs text-gray-600 mb-2">
Quand la tâche devient visible/active
</FormDescription>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={field.value ? "w-full pl-3 text-left font-normal" : "w-full pl-3 text-left font-normal text-muted-foreground"}
>
{field.value ? (
format(field.value, "PPP", { locale: fr })
) : (
<span>Sélectionner la date de départ</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
initialFocus
locale={fr}
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
{/* Date d'échéance */}
<FormField
control={form.control}
name="dueDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="text-sm font-medium">
⏰ Date d'échéance
</FormLabel>
<FormDescription className="text-xs text-gray-600 mb-2">
Date limite pour terminer la tâche
</FormDescription>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={field.value ? "w-full pl-3 text-left font-normal" : "w-full pl-3 text-left font-normal text-muted-foreground"}
>
{field.value ? (
format(field.value, "PPP", { locale: fr })
) : (
<span>Sélectionner la date d'échéance</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
initialFocus
locale={fr}
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Information sur les règles de visibilité */}
<div className="mt-3 p-3 bg-blue-50 border border-blue-200">
<div className="flex items-start">
<Info className="h-4 w-4 text-blue-600 mr-2 mt-0.5 flex-shrink-0" />
<div className="text-xs text-blue-700">
<p className="font-medium mb-1">Règles de visibilité :</p>
<ul className="list-disc list-inside space-y-1">
<li><strong>Managers/Employés</strong> : voient la tâche uniquement à partir de la date de départ</li>
<li><strong>Admin/Directeur</strong> : voient toujours la tâche (avec style différent avant la date de départ)</li>
</ul>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Priorité */}
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priorité *</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Sélectionner une priorité" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="low">🟢 Faible</SelectItem>
<SelectItem value="medium">🟡 Moyenne</SelectItem>
<SelectItem value="high">🔴 Élevée</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* Statut */}
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Statut</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Sélectionner un statut" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="pending">⏳ En cours</SelectItem>
<SelectItem value="completed">✅ Terminée</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Assigné à */}
<FormField
control={form.control}
name="assignedTo"
render={({ field }) => (
<FormItem>
<FormLabel>Assigné à *</FormLabel>
<FormControl>
<Input
placeholder="Nom de la personne assignée"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Boutons d'action */}
<div className="flex justify-end space-x-3 pt-6 border-t">
<Button type="button" variant="outline" onClick={onClose}>
Annuler
</Button>
<Button
type="submit"
disabled={createMutation.isPending || updateMutation.isPending}
className="bg-blue-600 hover:bg-blue-700"
>
{createMutation.isPending || updateMutation.isPending ? (
<div className="flex items-center">
<div className="animate-spin h-4 w-4 border-b-2 border-white mr-2"></div>
{task ? "Modification..." : "Création..."}
</div>
) : (
task ? "Modifier la tâche" : "Créer la tâche"
)}
</Button>
</div>
</form>
</Form>
</div>
);
}