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:
michaelschal committed 2025-08-19 07:23:52 +00:00
1 parent 87cc2a0f88
commit aa1906a9bb
7 files changed
+701 -512

No files matched your search

+545
View File
@@ -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>
);
}
+2 -468
View File
@@ -1,468 +1,2 @@
import { useState, useEffect } from "react"; // Re-export du nouveau formulaire de tâches avec dates de départ et d'échéance
import { useForm } from "react-hook-form"; export { default } from "./NewTaskForm";
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>
);
}
+118 -34
View File
@@ -32,9 +32,10 @@ import TaskForm from "@/components/tasks/TaskForm";
import { Task } from "@shared/schema"; import { Task } from "@shared/schema";
type TaskWithRelations = Task & { type TaskWithRelations = Task & {
assignedUser: { id: string; username: string; firstName?: string; lastName?: string; }; assignedUser?: { id: string; username: string; firstName?: string; lastName?: string; };
creator: { id: string; username: string; firstName?: string; lastName?: string; }; creator?: { id: string; username: string; firstName?: string; lastName?: string; };
group: { id: number; name: string; color: string; }; group?: { id: number; name: string; color: string; };
isFutureTask?: boolean; // Pour les tâches futures (admin/directeur uniquement)
}; };
export default function Tasks() { 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 canCreateTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
const canEditTasks = 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 priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon; const PriorityIcon = priorityConfig.icon;
const isFuture = isTaskFuture(task);
const startDateStatus = getStartDateStatus(task.startDate);
return ( 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"> <CardContent className="p-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2"> <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} {task.title}
{isFuture && " (Programmée)"}
</h5> </h5>
<Badge variant={priorityConfig.color} className="flex items-center gap-1"> <Badge variant={priorityConfig.color} className="flex items-center gap-1">
<PriorityIcon className="w-3 h-3" /> <PriorityIcon className="w-3 h-3" />
{priorityConfig.label} {priorityConfig.label}
</Badge> </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> </div>
{task.description && ( {task.description && (
@@ -514,37 +570,61 @@ export default function Tasks() {
Créée: {format(new Date(task.createdAt), 'dd/MM/yyyy HH:mm', { locale: fr })} Créée: {format(new Date(task.createdAt), 'dd/MM/yyyy HH:mm', { locale: fr })}
</span> </span>
</div> </div>
{/* Affichage de l'échéance */} {/* Affichage des dates : départ et échéance */}
{(() => { <div className="flex items-center gap-4 mb-2">{[
const dueDateStatus = getDueDateStatus(task.dueDate, task.status); // Date de départ
if (dueDateStatus) { startDateStatus && (
const DueDateIcon = dueDateStatus.icon; <div key="start" className="flex items-center gap-2">
return ( <Badge variant={startDateStatus.color} className="flex items-center gap-1 text-xs">
<div className="flex items-center gap-2"> <startDateStatus.icon className="w-3 h-3" />
<Badge variant={dueDateStatus.color} className="flex items-center gap-1 text-xs"> 📅 {startDateStatus.text}
<DueDateIcon className="w-3 h-3" /> </Badge>
{dueDateStatus.text} <span className="text-xs text-gray-500">
</Badge> {startDateStatus.label}
<span className="text-xs text-gray-500">{dueDateStatus.label}</span> {task.startDate && ` (${format(new Date(task.startDate), 'dd/MM/yyyy', { locale: fr })})`}
</div> </span>
); </div>
} else if (task.dueDate) { ),
return ( // Date d'échéance
<div className="flex items-center gap-2"> (() => {
<Badge variant="outline" className="flex items-center gap-1 text-xs"> const dueDateStatus = getDueDateStatus(task.dueDate, task.status);
<Calendar className="w-3 h-3" /> if (dueDateStatus) {
{format(new Date(task.dueDate), 'dd/MM', { locale: fr })} const DueDateIcon = dueDateStatus.icon;
</Badge> return (
</div> <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" />
return null; ⏰ {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>
<div className="flex items-center gap-2 ml-4"> <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 <Button
size="sm" size="sm"
variant="outline" variant="outline"
@@ -559,6 +639,7 @@ export default function Tasks() {
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => handleEditTask(task)} onClick={() => handleEditTask(task)}
className={isFuture ? "opacity-60" : ""}
> >
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</Button> </Button>
@@ -568,7 +649,10 @@ export default function Tasks() {
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => handleDeleteClick(task)} 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" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
+1 -1
View File
@@ -34,7 +34,7 @@ The platform is built with a clear separation of concerns, utilizing a modern fu
- **Reconciliation:** Functionality for reconciling Bills of Lading (BL) and invoices with immediate UI updates. - **Reconciliation:** Functionality for reconciling Bills of Lading (BL) and invoices with immediate UI updates.
- **Invoice Verification Cache:** Robust caching system for invoice verification results with automatic expiration and conflict resolution. Enhanced with unique constraint handling and graceful error management for production stability (August 2025). Fixed production SSL certificate errors and fetch timeout loops with enhanced error handling, network timeout protection, and error result caching to prevent infinite retry loops (August 2025). **MAJOR BUG FIX (August 2025)**: Resolved critical cache system failure - MemStorage was returning `undefined` for all cache lookups, preventing cache functionality entirely. Implemented working in-memory cache with Map-based storage and expiration logic. Fixed development mode to properly save verification results to cache. Added comprehensive cache operation logging for debugging. Verified complete cache workflow: miss → save → hit cycle working perfectly. **CRITICAL PRODUCTION FIX (August 2025)**: Fixed DatabaseStorage cache expiration bug - production was returning expired cache entries instead of checking expiration dates, causing repeated API calls despite existing cache. Added proper expiration verification and automatic cleanup of expired cache entries in PostgreSQL, matching MemStorage behavior. - **Invoice Verification Cache:** Robust caching system for invoice verification results with automatic expiration and conflict resolution. Enhanced with unique constraint handling and graceful error management for production stability (August 2025). Fixed production SSL certificate errors and fetch timeout loops with enhanced error handling, network timeout protection, and error result caching to prevent infinite retry loops (August 2025). **MAJOR BUG FIX (August 2025)**: Resolved critical cache system failure - MemStorage was returning `undefined` for all cache lookups, preventing cache functionality entirely. Implemented working in-memory cache with Map-based storage and expiration logic. Fixed development mode to properly save verification results to cache. Added comprehensive cache operation logging for debugging. Verified complete cache workflow: miss → save → hit cycle working perfectly. **CRITICAL PRODUCTION FIX (August 2025)**: Fixed DatabaseStorage cache expiration bug - production was returning expired cache entries instead of checking expiration dates, causing repeated API calls despite existing cache. Added proper expiration verification and automatic cleanup of expired cache entries in PostgreSQL, matching MemStorage behavior.
- **UI Scrollbar Optimization:** Eliminated redundant vertical scrollbar on the right side of the screen while preserving necessary internal page scrolling. Implemented global CSS rules with `overflow: hidden` on html/body/root and Layout height management to prevent vertical overflow issues in production (August 2025). - **UI Scrollbar Optimization:** Eliminated redundant vertical scrollbar on the right side of the screen while preserving necessary internal page scrolling. Implemented global CSS rules with `overflow: hidden` on html/body/root and Layout height management to prevent vertical overflow issues in production (August 2025).
- **Task Management:** Système complet de gestion des tâches avec fonctionnalités temporelles avancées - tri intelligent (pending > priorité > date), dates d'échéance avec sélecteur calendrier, indicateurs visuels pour tâches en retard/urgentes, filtres par échéance (aujourd'hui, cette semaine, en retard, sans échéance), vue Kanban et liste, badges colorés pour statut temporel. Interface optimisée pour suivi prioritaire des tâches non validées. Correction critique de l'isolation des données : chaque magasin ne voit maintenant que ses propres tâches (Août 2025). **PRODUCTION FIX (Août 2025)** : Résolution du problème de rechargements multiples nécessaires pour l'administrateur - le cache des tâches s'invalide maintenant automatiquement lors du changement de magasin sélectionné. **SÉLECTION DATE ÉCHÉANCE FIX (Août 2025)** : Correction du problème empêchant la sélection de dates d'échéance autres que le jour courant - suppression de la restriction `disabled` dans le calendrier, uniformisation du format d'envoi des dates, et alignement du schéma Drizzle (date au lieu de timestamp) avec la structure PostgreSQL production. - **Task Management:** Système complet de gestion des tâches avec fonctionnalités temporelles avancées - tri intelligent (pending > priorité > date), dates d'échéance avec sélecteur calendrier, indicateurs visuels pour tâches en retard/urgentes, filtres par échéance (aujourd'hui, cette semaine, en retard, sans échéance), vue Kanban et liste, badges colorés pour statut temporel. Interface optimisée pour suivi prioritaire des tâches non validées. Correction critique de l'isolation des données : chaque magasin ne voit maintenant que ses propres tâches (Août 2025). **PRODUCTION FIX (Août 2025)** : Résolution du problème de rechargements multiples nécessaires pour l'administrateur - le cache des tâches s'invalide maintenant automatiquement lors du changement de magasin sélectionné. **SÉLECTION DATE ÉCHÉANCE FIX (Août 2025)** : Correction du problème empêchant la sélection de dates d'échéance autres que le jour courant - suppression de la restriction `disabled` dans le calendrier, uniformisation du format d'envoi des dates, et alignement du schéma Drizzle (date au lieu de timestamp) avec la structure PostgreSQL production. **DATES DE DÉPART IMPLÉMENTÉES (Août 2025)** : Système complet de dates de départ avec logique de visibilité basée sur les rôles - Managers/Employés ne voient les tâches qu'à partir de leur date de départ, Admin/Directeur voient toutes les tâches avec styles visuels différents pour les tâches futures (fond gris, bordures pointillées, texte italique, badge "Future"). Interface utilisateur refonte complète avec NewTaskForm.tsx présentant clairement les deux dates (📅 départ, ⏰ échéance) avec règles de validation et explications des rôles de visibilité.
- **Information System (Announcements):** Système d'informations complet remplaçant la carte commandes clients dans le dashboard - gestion d'annonces avec niveaux de priorité (normal, important, urgent), contrôle d'accès basé sur les rôles (admin : CRUD complet, autres rôles : lecture seule), tri intelligent par priorité et date, filtrage par magasin pour administrateurs, interface moderne avec badges colorés et indicateurs visuels. **PRODUCTION FIX MAJEUR (Août 2025)** : Correction critique du problème d'affichage en production PostgreSQL - fix du filtrage pour inclure annonces globales + magasin sélectionné, correction des relations d'auteur avec username au lieu d'ID, design adapté pour correspondre exactement aux autres cartes du dashboard. **SUPPRESSION FIX (Août 2025)** : Correction du problème de suppression en production - ajout de la logique PostgreSQL manquante dans la route DELETE, les annonces peuvent maintenant être supprimées correctement en production. **MODAL DASHBOARD (Août 2025)** : Ajout d'un modal automatique dans le dashboard qui affiche la dernière annonce créée il y a moins de 2 jours - système de mémorisation localStorage pour éviter les re-affichages, interface avec priorités colorées, formatage français des dates, filtrage par magasin pour admins. **UTILISATEUR INCONNU FIX (Août 2025)** : Correction critique du problème "utilisateur inconnu" en production - création de la table `dashboard_messages` manquante, correction des noms de colonnes (`first_name` au lieu de `firstName`), amélioration de la logique de récupération des noms d'utilisateur avec fallback intelligent. **AFFICHAGE SIMPLIFIÉ (Août 2025)** : Suppression de l'affichage du nom de l'auteur des annonces pour simplifier l'interface - seul le nom du magasin/groupe est affiché si applicable. **FIX MODIFICATION PRODUCTION (Août 2025)** : Correction critique du problème de modification des annonces en production - la jointure PostgreSQL utilisait incorrectement `users.id` au lieu de `users.username` pour la relation avec `createdBy`, causant des erreurs 500. Fix appliqué avec correction de la jointure et ajout de logs de debug dans `announcementStorage.ts`. **MÉTHODE MANQUANTE FIX (Août 2025)** : Identification et correction de l'erreur "getAnnouncement is not a function" - ajout de la méthode `getAnnouncement` manquante dans l'interface `IAnnouncementStorage` et ses implémentations `AnnouncementDatabaseStorage` et `AnnouncementMemoryStorage` pour résoudre définitivement les erreurs 500 en production. - **Information System (Announcements):** Système d'informations complet remplaçant la carte commandes clients dans le dashboard - gestion d'annonces avec niveaux de priorité (normal, important, urgent), contrôle d'accès basé sur les rôles (admin : CRUD complet, autres rôles : lecture seule), tri intelligent par priorité et date, filtrage par magasin pour administrateurs, interface moderne avec badges colorés et indicateurs visuels. **PRODUCTION FIX MAJEUR (Août 2025)** : Correction critique du problème d'affichage en production PostgreSQL - fix du filtrage pour inclure annonces globales + magasin sélectionné, correction des relations d'auteur avec username au lieu d'ID, design adapté pour correspondre exactement aux autres cartes du dashboard. **SUPPRESSION FIX (Août 2025)** : Correction du problème de suppression en production - ajout de la logique PostgreSQL manquante dans la route DELETE, les annonces peuvent maintenant être supprimées correctement en production. **MODAL DASHBOARD (Août 2025)** : Ajout d'un modal automatique dans le dashboard qui affiche la dernière annonce créée il y a moins de 2 jours - système de mémorisation localStorage pour éviter les re-affichages, interface avec priorités colorées, formatage français des dates, filtrage par magasin pour admins. **UTILISATEUR INCONNU FIX (Août 2025)** : Correction critique du problème "utilisateur inconnu" en production - création de la table `dashboard_messages` manquante, correction des noms de colonnes (`first_name` au lieu de `firstName`), amélioration de la logique de récupération des noms d'utilisateur avec fallback intelligent. **AFFICHAGE SIMPLIFIÉ (Août 2025)** : Suppression de l'affichage du nom de l'auteur des annonces pour simplifier l'interface - seul le nom du magasin/groupe est affiché si applicable. **FIX MODIFICATION PRODUCTION (Août 2025)** : Correction critique du problème de modification des annonces en production - la jointure PostgreSQL utilisait incorrectement `users.id` au lieu de `users.username` pour la relation avec `createdBy`, causant des erreurs 500. Fix appliqué avec correction de la jointure et ajout de logs de debug dans `announcementStorage.ts`. **MÉTHODE MANQUANTE FIX (Août 2025)** : Identification et correction de l'erreur "getAnnouncement is not a function" - ajout de la méthode `getAnnouncement` manquante dans l'interface `IAnnouncementStorage` et ses implémentations `AnnouncementDatabaseStorage` et `AnnouncementMemoryStorage` pour résoudre définitivement les erreurs 500 en production.
- **DLC Module Permissions:** Correction des permissions de validation DLC pour permettre aux admin, directeur et manager de valider les produits DLC. Ajout de logs de debug pour traçabilité des validations (Août 2025). - **DLC Module Permissions:** Correction des permissions de validation DLC pour permettre aux admin, directeur et manager de valider les produits DLC. Ajout de logs de debug pour traçabilité des validations (Août 2025).
- **DLC UX Enhancement:** Ajout de tooltips explicatifs sur les icônes stock épuisé (PackageX) et restauration (RotateCcw) pour clarifier la différence entre "périmé" et "stock épuisé" et améliorer l'expérience utilisateur (Août 2025). - **DLC UX Enhancement:** Ajout de tooltips explicatifs sur les icônes stock épuisé (PackageX) et restauration (RotateCcw) pour clarifier la différence entre "périmé" et "stock épuisé" et améliorer l'expérience utilisateur (Août 2025).
+1 -1
View File
@@ -1362,7 +1362,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
const tasks = await storage.getTasks(groupIds); const tasks = await storage.getTasks(groupIds, user.role);
console.log('📋 Tasks returned:', { console.log('📋 Tasks returned:', {
count: tasks.length, count: tasks.length,
userId: user.id, userId: user.id,
+28 -5
View File
@@ -172,8 +172,8 @@ export interface IStorage {
restoreDlcProductStock(id: number): Promise<DlcProductFrontend>; restoreDlcProductStock(id: number): Promise<DlcProductFrontend>;
getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }>; getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }>;
// Task operations // Task operations
getTasks(groupIds?: number[]): Promise<any[]>; getTasks(groupIds?: number[], userRole?: string): Promise<any[]>;
getTask(id: number): Promise<any | undefined>; getTask(id: number): Promise<any | undefined>;
createTask(task: InsertTask): Promise<Task>; createTask(task: InsertTask): Promise<Task>;
updateTask(id: number, task: Partial<InsertTask>): Promise<Task>; updateTask(id: number, task: Partial<InsertTask>): Promise<Task>;
@@ -1584,7 +1584,10 @@ export class DatabaseStorage implements IStorage {
}; };
} }
async getTasks(groupIds?: number[]): Promise<TaskWithRelations[]> { async getTasks(groupIds?: number[], userRole?: string): Promise<TaskWithRelations[]> {
const today = new Date();
today.setHours(0, 0, 0, 0);
let query = db let query = db
.select({ .select({
task: tasks, task: tasks,
@@ -1593,14 +1596,34 @@ export class DatabaseStorage implements IStorage {
.from(tasks) .from(tasks)
.leftJoin(groups, eq(tasks.groupId, groups.id)); .leftJoin(groups, eq(tasks.groupId, groups.id));
// Appliquer le filtrage par magasin si spécifié
let whereConditions = [];
if (groupIds && groupIds.length > 0) { if (groupIds && groupIds.length > 0) {
query = query.where(inArray(tasks.groupId, groupIds)); whereConditions.push(inArray(tasks.groupId, groupIds));
}
// Filtrage par rôle pour la date de départ
if (userRole === 'manager' || userRole === 'employee') {
// Managers et employés : seulement les tâches dont la date de départ est atteinte ou passée
whereConditions.push(
or(
isNull(tasks.startDate), // Tâches sans date de départ (toujours visibles)
lte(tasks.startDate, today) // Tâches dont la date de départ est arrivée
)
);
}
// Admin et directeur voient toutes les tâches (pas de filtre additionnel)
if (whereConditions.length > 0) {
query = query.where(and(...whereConditions));
} }
const results = await query.orderBy(desc(tasks.createdAt)); const results = await query.orderBy(desc(tasks.createdAt));
return results.map((row: any) => ({ return results.map((row: any) => ({
...row.task, ...row.task,
group: row.group group: row.group,
// Ajouter le statut "future" pour les tâches futures (admin/directeur)
isFutureTask: row.task.startDate && new Date(row.task.startDate) > today
})); }));
} }
+6 -3
View File
@@ -248,6 +248,7 @@ export const tasks = pgTable("tasks", {
id: serial("id").primaryKey(), id: serial("id").primaryKey(),
title: varchar("title").notNull(), // Titre de la tâche (requis) title: varchar("title").notNull(), // Titre de la tâche (requis)
description: text("description"), // Description de la tâche (optionnel) description: text("description"), // Description de la tâche (optionnel)
startDate: timestamp("start_date"), // Date de départ (optionnel) - aligné avec production
dueDate: date("due_date"), // Date d'échéance (optionnel) dueDate: date("due_date"), // Date d'échéance (optionnel)
priority: varchar("priority").notNull().default("medium"), // low, medium, high priority: varchar("priority").notNull().default("medium"), // low, medium, high
status: varchar("status").notNull().default("pending"), // pending, completed status: varchar("status").notNull().default("pending"), // pending, completed
@@ -532,7 +533,8 @@ export const insertTaskSchema = createInsertSchema(tasks).omit({
completedAt: true, completedAt: true,
completedBy: true, completedBy: true,
}).extend({ }).extend({
dueDate: z.coerce.date().optional().nullable(), // Convertit automatiquement les chaînes en Date startDate: z.coerce.date().optional().nullable(), // Date de départ (optionnel)
dueDate: z.coerce.date().optional().nullable(), // Date d'échéance (optionnel)
}); });
export const insertDashboardMessageSchema = createInsertSchema(dashboardMessages).omit({ export const insertDashboardMessageSchema = createInsertSchema(dashboardMessages).omit({
@@ -648,8 +650,9 @@ export type InvoiceVerificationCache = typeof invoiceVerificationCache.$inferSel
// Task with relations type // Task with relations type
export type TaskWithRelations = Task & { export type TaskWithRelations = Task & {
creator: User; creator?: User;
group: Group; group?: Group;
isFutureTask?: boolean; // Pour distinguer les tâches futures (admin/directeur)
}; };
// Dashboard Message with relations type // Dashboard Message with relations type