mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-17 20:27:10 +02:00
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
This commit is contained in:
1 parent
87cc2a0f88
commit
aa1906a9bb
7 files changed
+701
-512
No files matched your search
@@ -1,468 +1,2 @@
|
||||
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 } from "@/components/ui/form";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { insertTaskSchema, Task, Group } from "@shared/schema";
|
||||
import { z } from "zod";
|
||||
import { CalendarIcon } 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; };
|
||||
};
|
||||
|
||||
// Schéma avec date d'échéance incluse
|
||||
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"),
|
||||
dueDate: z.date().optional(),
|
||||
});
|
||||
|
||||
type TaskFormData = z.infer<typeof taskFormSchema>;
|
||||
|
||||
interface TaskFormProps {
|
||||
task?: TaskWithRelations;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function TaskForm({ task, onClose }: TaskFormProps) {
|
||||
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'],
|
||||
});
|
||||
|
||||
// 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 (même logique que CreateOrderModal)
|
||||
useEffect(() => {
|
||||
console.log('🏪 TaskForm - Store selection effect:', {
|
||||
groupsLength: groups.length,
|
||||
selectedStoreId,
|
||||
localSelectedStoreId,
|
||||
userRole: user?.role,
|
||||
allGroups: groups.map(g => ({ id: g.id, name: g.name })),
|
||||
});
|
||||
|
||||
if (groups.length > 0 && !localSelectedStoreId) {
|
||||
let defaultStoreId: number | null = null;
|
||||
|
||||
if (user?.role === 'admin') {
|
||||
// Pour l'admin : utiliser le magasin sélectionné dans le header (filtré), sinon le premier de la liste
|
||||
if (selectedStoreId && groups.find(g => g.id === selectedStoreId)) {
|
||||
defaultStoreId = selectedStoreId;
|
||||
} else {
|
||||
defaultStoreId = groups[0].id;
|
||||
}
|
||||
console.log('🏪 Admin task store selection:', {
|
||||
selectedStoreId,
|
||||
defaultStoreId,
|
||||
firstGroupId: groups[0].id,
|
||||
groupsAvailable: groups.map(g => g.name)
|
||||
});
|
||||
} else {
|
||||
// Pour les autres rôles : prendre le premier magasin attribué
|
||||
defaultStoreId = groups[0].id;
|
||||
console.log('🏪 Non-admin task store selection:', { defaultStoreId, firstGroupId: groups[0].id });
|
||||
}
|
||||
|
||||
if (defaultStoreId) {
|
||||
console.log('🏪 Setting default task store ID:', defaultStoreId, 'for group:', groups.find(g => g.id === defaultStoreId)?.name);
|
||||
setLocalSelectedStoreId(defaultStoreId);
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour localSelectedStoreId si selectedStoreId change (pour les admins)
|
||||
if (user?.role === 'admin' && selectedStoreId && selectedStoreId !== localSelectedStoreId) {
|
||||
console.log('🔄 Updating task form store because header store changed:', {
|
||||
oldStoreId: localSelectedStoreId,
|
||||
newStoreId: selectedStoreId
|
||||
});
|
||||
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 || "",
|
||||
dueDate: task?.dueDate ? new Date(task.dueDate) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation pour créer/modifier une tâche
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => {
|
||||
// Ajouter le groupId basé sur le magasin auto-sélectionné
|
||||
const taskData = {
|
||||
...data,
|
||||
groupId: localSelectedStoreId,
|
||||
createdBy: user?.id,
|
||||
dueDate: data.dueDate || null
|
||||
};
|
||||
console.log('🚀 Creating task with data:', {
|
||||
...taskData,
|
||||
dueDate: taskData.dueDate,
|
||||
dueDateType: typeof taskData.dueDate,
|
||||
dueDateISO: taskData.dueDate?.toISOString?.()
|
||||
});
|
||||
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 data:', {
|
||||
taskId: task?.id,
|
||||
data,
|
||||
dueDate: data.dueDate,
|
||||
dueDateType: typeof data.dueDate,
|
||||
dueDateISO: data.dueDate?.toISOString?.()
|
||||
});
|
||||
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) => {
|
||||
// Debug: log des données de soumission
|
||||
console.log('📝 TaskForm onSubmit:', {
|
||||
isEdit: !!task,
|
||||
taskId: task?.id,
|
||||
formData: data,
|
||||
dueDate: data.dueDate,
|
||||
dueDateType: typeof data.dueDate,
|
||||
dueDateISO: data.dueDate?.toISOString(),
|
||||
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,
|
||||
dueDate: data.dueDate || null,
|
||||
};
|
||||
console.log('🔄 Update task submitData:', submitData);
|
||||
updateMutation.mutate(submitData);
|
||||
} else {
|
||||
// Création d'une nouvelle tâche - le groupId sera ajouté dans createMutation
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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 rounded-md p-3">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</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 rounded-full"
|
||||
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 rounded-md p-3">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Date d'échéance */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dueDate"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date d'échéance (optionnel)</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-full pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP", { locale: fr })
|
||||
) : (
|
||||
<span>Sélectionner une date</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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Boutons d'action */}
|
||||
<div className="flex justify-end space-x-3 pt-6">
|
||||
<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 rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
{task ? "Modification..." : "Création..."}
|
||||
</div>
|
||||
) : (
|
||||
task ? "Modifier" : "Créer"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
// Re-export du nouveau formulaire de tâches avec dates de départ et d'échéance
|
||||
export { default } from "./NewTaskForm";
|
||||
Reference in new issue
Block a user