diff --git a/client/src/components/tasks/TaskForm.tsx b/client/src/components/tasks/TaskForm.tsx index 8cacd1d..e278adc 100644 --- a/client/src/components/tasks/TaskForm.tsx +++ b/client/src/components/tasks/TaskForm.tsx @@ -9,11 +9,16 @@ 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; }; @@ -21,15 +26,14 @@ type TaskWithRelations = Task & { group: { id: number; name: string; color: string; }; }; -// Schéma simplifié sans magasin et sans date d'échéance -const taskFormSchema = insertTaskSchema.omit({ - id: true, - createdAt: true, - updatedAt: true, - completedAt: true, - createdBy: true, - groupId: true, - dueDate: true, // Suppression du champ échéance +// 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; @@ -113,10 +117,10 @@ export default function TaskForm({ task, onClose }: TaskFormProps) { defaultValues: { title: task?.title || "", description: task?.description || "", - priority: task?.priority || "medium", - status: task?.status || "pending", + 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, }, }); @@ -190,6 +194,7 @@ export default function TaskForm({ task, onClose }: TaskFormProps) { priority: data.priority, status: data.status, assignedTo: data.assignedTo, + dueDate: data.dueDate?.toISOString(), }; updateMutation.mutate(submitData); } else { @@ -217,7 +222,7 @@ export default function TaskForm({ task, onClose }: TaskFormProps) { return (
- + {/* Affichage du magasin auto-sélectionné */} {localSelectedStoreId && (
@@ -236,7 +241,7 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
{selectedGroup.name} @@ -369,7 +374,49 @@ export default function TaskForm({ task, onClose }: TaskFormProps) { )} /> - + {/* Date d'échéance */} + ( + + Date d'échéance (optionnel) + + + + + + + + + date < new Date(new Date().setHours(0, 0, 0, 0)) + } + initialFocus + locale={fr} + /> + + + + + )} + /> {/* Boutons d'action */}
diff --git a/client/src/pages/Tasks.tsx b/client/src/pages/Tasks.tsx index a585901..d55737b 100644 --- a/client/src/pages/Tasks.tsx +++ b/client/src/pages/Tasks.tsx @@ -10,6 +10,7 @@ import { Badge } from "@/components/ui/badge"; import { Pagination, usePagination } from "@/components/ui/pagination"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ListTodo, Plus, @@ -19,9 +20,13 @@ import { AlertTriangle, CheckCircle, Edit, - Trash2 + Trash2, + Calendar, + CalendarX, + CalendarClock, + Kanban } from "lucide-react"; -import { format } from "date-fns"; +import { format, isToday, isThisWeek, isPast, differenceInDays } from "date-fns"; import { fr } from "date-fns/locale"; import TaskForm from "@/components/tasks/TaskForm"; import { Task } from "@shared/schema"; @@ -42,6 +47,8 @@ export default function Tasks() { const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [priorityFilter, setPriorityFilter] = useState("all"); + const [dueDateFilter, setDueDateFilter] = useState("all"); + const [viewMode, setViewMode] = useState("list"); // Modals const [showCreateModal, setShowCreateModal] = useState(false); @@ -176,6 +183,27 @@ export default function Tasks() { return false; } + // Filtre par date d'échéance + if (dueDateFilter !== "all" && task.dueDate) { + const dueDate = new Date(task.dueDate); + switch (dueDateFilter) { + case "today": + if (!isToday(dueDate)) return false; + break; + case "this_week": + if (!isThisWeek(dueDate)) return false; + break; + case "overdue": + if (!isPast(dueDate) || task.status === 'completed') return false; + break; + case "no_due_date": + if (task.dueDate) return false; + break; + } + } else if (dueDateFilter === "no_due_date" && task.dueDate) { + return false; + } + return true; }) .sort((a: TaskWithRelations, b: TaskWithRelations) => { @@ -238,6 +266,48 @@ export default function Tasks() { } }; + const getDueDateStatus = (dueDate: string | null, status: string) => { + if (!dueDate || status === 'completed') return null; + + const due = new Date(dueDate); + const now = new Date(); + const daysDiff = differenceInDays(due, now); + + if (isPast(due)) { + return { + type: 'overdue', + color: 'destructive' as const, + icon: CalendarX, + label: `En retard de ${Math.abs(daysDiff)} jour${Math.abs(daysDiff) > 1 ? 's' : ''}`, + text: 'En retard' + }; + } else if (isToday(due)) { + return { + type: 'today', + color: 'secondary' as const, + icon: CalendarClock, + label: "Échéance aujourd'hui", + text: "Aujourd'hui" + }; + } else if (daysDiff <= 3) { + return { + type: 'soon', + color: 'default' as const, + icon: Calendar, + label: `Dans ${daysDiff} jour${daysDiff > 1 ? 's' : ''}`, + text: `${daysDiff}j` + }; + } + + return { + type: 'normal', + color: 'outline' as const, + icon: Calendar, + label: format(due, 'dd/MM/yyyy', { locale: fr }), + text: format(due, 'dd/MM', { locale: fr }) + }; + }; + const canCreateTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur'; const canEditTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur'; @@ -263,24 +333,40 @@ export default function Tasks() { {totalItems} tâche{totalItems !== 1 ? 's' : ''} trouvée{totalItems !== 1 ? 's' : ''}

- {canCreateTasks && ( - - - - - - - Créer une nouvelle tâche - - setShowCreateModal(false)} - /> - - - )} +
+ {/* Sélecteur de vue */} + + + + + Liste + + + + Kanban + + + + + {canCreateTasks && ( + + + + + + + Créer une nouvelle tâche + + setShowCreateModal(false)} + /> + + + )} +
@@ -338,14 +424,36 @@ export default function Tasks() { + + {/* Filtre par échéance */} +
+ + +
{/* Zone principale avec les tâches */}
- {/* Liste des tâches */} -
+ {/* Contenu selon la vue sélectionnée */} + + + {/* Liste des tâches */} +
{totalItems === 0 ? (
@@ -392,7 +500,7 @@ export default function Tasks() {

)} -
+
Assigné à: {task.assignedTo} @@ -400,6 +508,33 @@ export default function Tasks() { Créée: {format(new Date(task.createdAt), 'dd/MM/yyyy HH:mm', { locale: fr })}
+ + {/* Affichage de l'échéance */} + {(() => { + const dueDateStatus = getDueDateStatus(task.dueDate, task.status); + if (dueDateStatus) { + const DueDateIcon = dueDateStatus.icon; + return ( +
+ + + {dueDateStatus.text} + + {dueDateStatus.label} +
+ ); + } else if (task.dueDate) { + return ( +
+ + + {format(new Date(task.dueDate), 'dd/MM', { locale: fr })} + +
+ ); + } + return null; + })()}
@@ -512,19 +647,163 @@ export default function Tasks() {
)} - {/* Pagination */} - {totalItems > 0 && ( - - )} -
+ {/* Pagination */} + {totalItems > 0 && ( + + )} +
+
+ + + {/* Vue Kanban */} +
+
+ {/* Colonne En cours */} +
+

+ + En cours ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'pending').length}) +

+
+ {filteredTasks.filter((task: TaskWithRelations) => task.status === 'pending').map((task: TaskWithRelations) => { + const priorityConfig = getPriorityConfig(task.priority); + const PriorityIcon = priorityConfig.icon; + const dueDateStatus = getDueDateStatus(task.dueDate, task.status); + const DueDateIcon = dueDateStatus?.icon; + + return ( + + +
+
{task.title}
+ + + {priorityConfig.label} + +
+ + {task.description && ( +

+ {task.description} +

+ )} + +
+ Assigné à: {task.assignedTo} +
+ + {dueDateStatus && DueDateIcon && ( +
+ + + {dueDateStatus.text} + +
+ )} + +
+ {canEditTasks && ( + <> + + + + + )} +
+
+
+ ); + })} +
+
+ + {/* Colonne Terminées */} +
+

+ + Terminées ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'completed').length}) +

+
+ {filteredTasks.filter((task: TaskWithRelations) => task.status === 'completed').map((task: TaskWithRelations) => { + const priorityConfig = getPriorityConfig(task.priority); + const PriorityIcon = priorityConfig.icon; + + return ( + + +
+
{task.title}
+ + + {priorityConfig.label} + +
+ +
+ Terminée le: {task.completedAt ? format(new Date(task.completedAt), 'dd/MM/yyyy', { locale: fr }) : 'Date inconnue'} +
+ +
+ {canEditTasks && ( + <> + + + + )} +
+
+
+ ); + })} +
+
+
+
+
+
diff --git a/replit.md b/replit.md index a4180bd..746a952 100644 --- a/replit.md +++ b/replit.md @@ -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. - **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). - **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 tri intelligent - les tâches non validées (status 'pending') remontent automatiquement en première page pour une meilleure visibilité et suivi prioritaire. Tri secondaire par priorité et date de création (Août 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 (Août 2025). - **User and Permission Management:** Granular access controls ensuring that users (e.g., employees) have appropriate permissions for tasks like creating customer orders. - **Reporting:** Statistical reporting, including monthly summaries of deliveries based on `deliveredDate`.