mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-14 02:38:58 +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
@@ -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<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'],
|
||||
});
|
||||
|
||||
// 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<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 rounded-md 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 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">
|
||||
<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 rounded-lg 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={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 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={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 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 rounded">
|
||||
<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 rounded-full 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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
+118
-34
@@ -32,9 +32,10 @@ import TaskForm from "@/components/tasks/TaskForm";
|
||||
import { Task } from "@shared/schema";
|
||||
|
||||
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; };
|
||||
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; // Pour les tâches futures (admin/directeur uniquement)
|
||||
};
|
||||
|
||||
export default function Tasks() {
|
||||
@@ -314,6 +315,45 @@ export default function Tasks() {
|
||||
};
|
||||
};
|
||||
|
||||
const getStartDateStatus = (startDate: Date | string | null) => {
|
||||
if (!startDate) return null;
|
||||
|
||||
const start = new Date(startDate);
|
||||
const now = new Date();
|
||||
const daysDiff = differenceInDays(start, now);
|
||||
|
||||
if (isPast(start)) {
|
||||
return {
|
||||
type: 'started',
|
||||
color: 'default' as const,
|
||||
icon: CheckCircle,
|
||||
label: 'Démarrée',
|
||||
text: 'Active'
|
||||
};
|
||||
} else if (isToday(start)) {
|
||||
return {
|
||||
type: 'today',
|
||||
color: 'secondary' as const,
|
||||
icon: CalendarClock,
|
||||
label: "Démarre aujourd'hui",
|
||||
text: "Aujourd'hui"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'future',
|
||||
color: 'outline' as const,
|
||||
icon: Clock,
|
||||
label: `Démarre dans ${daysDiff} jour${daysDiff > 1 ? 's' : ''}`,
|
||||
text: `+${daysDiff}j`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour déterminer si une tâche est future (pas encore commencée)
|
||||
const isTaskFuture = (task: TaskWithRelations) => {
|
||||
return task.isFutureTask || (task.startDate && !isPast(new Date(task.startDate)));
|
||||
};
|
||||
|
||||
const canCreateTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
|
||||
const canEditTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
|
||||
|
||||
@@ -485,19 +525,35 @@ export default function Tasks() {
|
||||
const priorityConfig = getPriorityConfig(task.priority);
|
||||
const PriorityIcon = priorityConfig.icon;
|
||||
|
||||
const isFuture = isTaskFuture(task);
|
||||
const startDateStatus = getStartDateStatus(task.startDate);
|
||||
|
||||
return (
|
||||
<Card key={task.id} className="hover:shadow-md transition-shadow">
|
||||
<Card key={task.id} className={cn(
|
||||
"hover:shadow-md transition-all duration-200",
|
||||
isFuture ? "bg-gray-50 border-dashed border-gray-300 opacity-75" : "bg-white border-solid"
|
||||
)}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h5 className="font-medium text-gray-900 truncate">
|
||||
<h5 className={cn(
|
||||
"font-medium truncate",
|
||||
isFuture ? "text-gray-600 italic" : "text-gray-900"
|
||||
)}>
|
||||
{task.title}
|
||||
{isFuture && " (Programmée)"}
|
||||
</h5>
|
||||
<Badge variant={priorityConfig.color} className="flex items-center gap-1">
|
||||
<PriorityIcon className="w-3 h-3" />
|
||||
{priorityConfig.label}
|
||||
</Badge>
|
||||
{isFuture && (
|
||||
<Badge variant="outline" className="flex items-center gap-1 text-xs border-orange-300 text-orange-700 bg-orange-50">
|
||||
<Clock className="w-3 h-3" />
|
||||
Future
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
@@ -514,37 +570,61 @@ export default function Tasks() {
|
||||
Créée: {format(new Date(task.createdAt), 'dd/MM/yyyy HH:mm', { locale: fr })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Affichage de l'échéance */}
|
||||
{(() => {
|
||||
const dueDateStatus = getDueDateStatus(task.dueDate, task.status);
|
||||
if (dueDateStatus) {
|
||||
const DueDateIcon = dueDateStatus.icon;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={dueDateStatus.color} className="flex items-center gap-1 text-xs">
|
||||
<DueDateIcon className="w-3 h-3" />
|
||||
{dueDateStatus.text}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">{dueDateStatus.label}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (task.dueDate) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="flex items-center gap-1 text-xs">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{format(new Date(task.dueDate), 'dd/MM', { locale: fr })}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{/* Affichage des dates : départ et échéance */}
|
||||
<div className="flex items-center gap-4 mb-2">{[
|
||||
// Date de départ
|
||||
startDateStatus && (
|
||||
<div key="start" className="flex items-center gap-2">
|
||||
<Badge variant={startDateStatus.color} className="flex items-center gap-1 text-xs">
|
||||
<startDateStatus.icon className="w-3 h-3" />
|
||||
📅 {startDateStatus.text}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">
|
||||
{startDateStatus.label}
|
||||
{task.startDate && ` (${format(new Date(task.startDate), 'dd/MM/yyyy', { locale: fr })})`}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
// Date d'échéance
|
||||
(() => {
|
||||
const dueDateStatus = getDueDateStatus(task.dueDate, task.status);
|
||||
if (dueDateStatus) {
|
||||
const DueDateIcon = dueDateStatus.icon;
|
||||
return (
|
||||
<div key="due" className="flex items-center gap-2">
|
||||
<Badge variant={dueDateStatus.color} className="flex items-center gap-1 text-xs">
|
||||
<DueDateIcon className="w-3 h-3" />
|
||||
⏰ {dueDateStatus.text}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">{dueDateStatus.label}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (task.dueDate) {
|
||||
return (
|
||||
<div key="due-normal" className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="flex items-center gap-1 text-xs">
|
||||
<Calendar className="w-3 h-3" />
|
||||
⏰ {format(new Date(task.dueDate), 'dd/MM', { locale: fr })}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()
|
||||
].filter(Boolean)}</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{canEditTasks && (
|
||||
{/* Avertissement pour les tâches futures (admin/directeur) */}
|
||||
{isFuture && (
|
||||
<div className="text-xs text-orange-600 bg-orange-100 px-2 py-1 rounded mr-2">
|
||||
Visible en avance ({user?.role})
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canEditTasks && !isFuture && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -559,6 +639,7 @@ export default function Tasks() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleEditTask(task)}
|
||||
className={isFuture ? "opacity-60" : ""}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -568,7 +649,10 @@ export default function Tasks() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDeleteClick(task)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
className={cn(
|
||||
"text-red-600 hover:text-red-700",
|
||||
isFuture && "opacity-60"
|
||||
)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user