mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
Enhance task management with due dates and filtering
Integrates calendar functionality for task due dates, adds due date filtering options (today, this week, overdue, no due date), and introduces visual indicators for task urgency and status. Also includes a Kanban view toggle for tasks. Replit-Commit-Author: Agent Replit-Commit-Session-Id: d43bd811-9372-45a7-8ac9-4a954c0538e1 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/d43bd811-9372-45a7-8ac9-4a954c0538e1/g7eox7N
This commit is contained in:
1 parent
7ff6a493fb
commit
1e8d8f8d19
3 files changed
+379
-53
No files matched your search
@@ -9,11 +9,16 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
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 { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { insertTaskSchema, Task, Group } from "@shared/schema";
|
import { insertTaskSchema, Task, Group } from "@shared/schema";
|
||||||
import { z } from "zod";
|
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 & {
|
type TaskWithRelations = Task & {
|
||||||
assignedUser: { id: string; username: string; firstName?: string; lastName?: string; };
|
assignedUser: { id: string; username: string; firstName?: string; lastName?: string; };
|
||||||
@@ -21,15 +26,14 @@ type TaskWithRelations = Task & {
|
|||||||
group: { id: number; name: string; color: string; };
|
group: { id: number; name: string; color: string; };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Schéma simplifié sans magasin et sans date d'échéance
|
// Schéma avec date d'échéance incluse
|
||||||
const taskFormSchema = insertTaskSchema.omit({
|
const taskFormSchema = z.object({
|
||||||
id: true,
|
title: z.string().min(1, "Le titre est requis"),
|
||||||
createdAt: true,
|
description: z.string().optional(),
|
||||||
updatedAt: true,
|
priority: z.enum(["low", "medium", "high"]).default("medium"),
|
||||||
completedAt: true,
|
status: z.enum(["pending", "completed"]).default("pending"),
|
||||||
createdBy: true,
|
assignedTo: z.string().min(1, "L'assignation est requise"),
|
||||||
groupId: true,
|
dueDate: z.date().optional(),
|
||||||
dueDate: true, // Suppression du champ échéance
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type TaskFormData = z.infer<typeof taskFormSchema>;
|
type TaskFormData = z.infer<typeof taskFormSchema>;
|
||||||
@@ -113,10 +117,10 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: task?.title || "",
|
title: task?.title || "",
|
||||||
description: task?.description || "",
|
description: task?.description || "",
|
||||||
priority: task?.priority || "medium",
|
priority: (task?.priority as "low" | "medium" | "high") || "medium",
|
||||||
status: task?.status || "pending",
|
status: (task?.status as "pending" | "completed") || "pending",
|
||||||
assignedTo: task?.assignedTo || "",
|
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,
|
priority: data.priority,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
assignedTo: data.assignedTo,
|
assignedTo: data.assignedTo,
|
||||||
|
dueDate: data.dueDate?.toISOString(),
|
||||||
};
|
};
|
||||||
updateMutation.mutate(submitData);
|
updateMutation.mutate(submitData);
|
||||||
} else {
|
} else {
|
||||||
@@ -217,7 +222,7 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit as any)} className="space-y-6">
|
||||||
{/* Affichage du magasin auto-sélectionné */}
|
{/* Affichage du magasin auto-sélectionné */}
|
||||||
{localSelectedStoreId && (
|
{localSelectedStoreId && (
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
|
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
|
||||||
@@ -236,7 +241,7 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
|
|||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full"
|
className="w-3 h-3 rounded-full"
|
||||||
style={{ backgroundColor: selectedGroup.color }}
|
style={{ backgroundColor: selectedGroup.color || "#1976D2" }}
|
||||||
/>
|
/>
|
||||||
{selectedGroup.name}
|
{selectedGroup.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -369,7 +374,49 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 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}
|
||||||
|
disabled={(date) =>
|
||||||
|
date < new Date(new Date().setHours(0, 0, 0, 0))
|
||||||
|
}
|
||||||
|
initialFocus
|
||||||
|
locale={fr}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Boutons d'action */}
|
{/* Boutons d'action */}
|
||||||
<div className="flex justify-end space-x-3 pt-6">
|
<div className="flex justify-end space-x-3 pt-6">
|
||||||
|
|||||||
+315
-36
@@ -10,6 +10,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Pagination, usePagination } from "@/components/ui/pagination";
|
import { Pagination, usePagination } from "@/components/ui/pagination";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
ListTodo,
|
ListTodo,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -19,9 +20,13 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Edit,
|
Edit,
|
||||||
Trash2
|
Trash2,
|
||||||
|
Calendar,
|
||||||
|
CalendarX,
|
||||||
|
CalendarClock,
|
||||||
|
Kanban
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format, isToday, isThisWeek, isPast, differenceInDays } from "date-fns";
|
||||||
import { fr } from "date-fns/locale";
|
import { fr } from "date-fns/locale";
|
||||||
import TaskForm from "@/components/tasks/TaskForm";
|
import TaskForm from "@/components/tasks/TaskForm";
|
||||||
import { Task } from "@shared/schema";
|
import { Task } from "@shared/schema";
|
||||||
@@ -42,6 +47,8 @@ export default function Tasks() {
|
|||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||||
const [priorityFilter, setPriorityFilter] = useState<string>("all");
|
const [priorityFilter, setPriorityFilter] = useState<string>("all");
|
||||||
|
const [dueDateFilter, setDueDateFilter] = useState<string>("all");
|
||||||
|
const [viewMode, setViewMode] = useState<string>("list");
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
@@ -176,6 +183,27 @@ export default function Tasks() {
|
|||||||
return false;
|
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;
|
return true;
|
||||||
})
|
})
|
||||||
.sort((a: TaskWithRelations, b: TaskWithRelations) => {
|
.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 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';
|
||||||
|
|
||||||
@@ -263,24 +333,40 @@ export default function Tasks() {
|
|||||||
{totalItems} tâche{totalItems !== 1 ? 's' : ''} trouvée{totalItems !== 1 ? 's' : ''}
|
{totalItems} tâche{totalItems !== 1 ? 's' : ''} trouvée{totalItems !== 1 ? 's' : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{canCreateTasks && (
|
<div className="flex items-center gap-3">
|
||||||
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
|
{/* Sélecteur de vue */}
|
||||||
<DialogTrigger asChild>
|
<Tabs value={viewMode} onValueChange={setViewMode} className="w-auto">
|
||||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white shadow-md">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<TabsTrigger value="list" className="flex items-center gap-2">
|
||||||
Nouvelle Tâche
|
<ListTodo className="w-4 h-4" />
|
||||||
</Button>
|
Liste
|
||||||
</DialogTrigger>
|
</TabsTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<TabsTrigger value="kanban" className="flex items-center gap-2">
|
||||||
<DialogHeader>
|
<Kanban className="w-4 h-4" />
|
||||||
<DialogTitle>Créer une nouvelle tâche</DialogTitle>
|
Kanban
|
||||||
</DialogHeader>
|
</TabsTrigger>
|
||||||
<TaskForm
|
</TabsList>
|
||||||
onClose={() => setShowCreateModal(false)}
|
</Tabs>
|
||||||
/>
|
|
||||||
</DialogContent>
|
{canCreateTasks && (
|
||||||
</Dialog>
|
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
|
||||||
)}
|
<DialogTrigger asChild>
|
||||||
|
<Button className="bg-blue-600 hover:bg-blue-700 text-white shadow-md">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Nouvelle Tâche
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Créer une nouvelle tâche</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<TaskForm
|
||||||
|
onClose={() => setShowCreateModal(false)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -338,14 +424,36 @@ export default function Tasks() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Filtre par échéance */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-2 block">
|
||||||
|
Échéance
|
||||||
|
</label>
|
||||||
|
<Select value={dueDateFilter} onValueChange={setDueDateFilter}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Toutes</SelectItem>
|
||||||
|
<SelectItem value="today">Aujourd'hui</SelectItem>
|
||||||
|
<SelectItem value="this_week">Cette semaine</SelectItem>
|
||||||
|
<SelectItem value="overdue">En retard</SelectItem>
|
||||||
|
<SelectItem value="no_due_date">Sans échéance</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zone principale avec les tâches */}
|
{/* Zone principale avec les tâches */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{/* Liste des tâches */}
|
{/* Contenu selon la vue sélectionnée */}
|
||||||
<div className="p-6">
|
<Tabs value={viewMode} onValueChange={setViewMode} className="h-full">
|
||||||
|
<TabsContent value="list" className="mt-0">
|
||||||
|
{/* Liste des tâches */}
|
||||||
|
<div className="p-6">
|
||||||
{totalItems === 0 ? (
|
{totalItems === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<ListTodo className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
<ListTodo className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||||
@@ -392,7 +500,7 @@ export default function Tasks() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
<div className="flex items-center gap-4 text-xs text-gray-500 mb-2">
|
||||||
<span>
|
<span>
|
||||||
Assigné à: {task.assignedTo}
|
Assigné à: {task.assignedTo}
|
||||||
</span>
|
</span>
|
||||||
@@ -400,6 +508,33 @@ 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 */}
|
||||||
|
{(() => {
|
||||||
|
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;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<div className="flex items-center gap-2 ml-4">
|
||||||
@@ -512,19 +647,163 @@ export default function Tasks() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalItems > 0 && (
|
{totalItems > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
totalItems={totalItems}
|
totalItems={totalItems}
|
||||||
itemsPerPage={itemsPerPage}
|
itemsPerPage={itemsPerPage}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
onItemsPerPageChange={setItemsPerPage}
|
onItemsPerPageChange={setItemsPerPage}
|
||||||
className="mt-4 border-t border-gray-200 pt-4 mx-6"
|
className="mt-4 border-t border-gray-200 pt-4 mx-6"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="kanban" className="mt-0">
|
||||||
|
{/* Vue Kanban */}
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid grid-cols-2 gap-6 h-full">
|
||||||
|
{/* Colonne En cours */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||||
|
<Circle className="w-5 h-5 text-yellow-500" />
|
||||||
|
En cours ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'pending').length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
||||||
|
{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 (
|
||||||
|
<Card key={task.id} className="hover:shadow-md transition-shadow cursor-pointer">
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<h5 className="font-medium text-gray-900 text-sm">{task.title}</h5>
|
||||||
|
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs">
|
||||||
|
<PriorityIcon className="w-3 h-3" />
|
||||||
|
{priorityConfig.label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{task.description && (
|
||||||
|
<p className="text-xs text-gray-600 mb-2 line-clamp-2">
|
||||||
|
{task.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-xs text-gray-500 mb-2">
|
||||||
|
Assigné à: {task.assignedTo}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dueDateStatus && DueDateIcon && (
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Badge variant={dueDateStatus.color} className="flex items-center gap-1 text-xs">
|
||||||
|
<DueDateIcon className="w-3 h-3" />
|
||||||
|
{dueDateStatus.text}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 mt-2">
|
||||||
|
{canEditTasks && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleCompleteTask(task.id)}
|
||||||
|
className="text-green-600 hover:text-green-700 text-xs h-7"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleEditTask(task)}
|
||||||
|
className="text-xs h-7"
|
||||||
|
>
|
||||||
|
<Edit className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleDeleteClick(task)}
|
||||||
|
className="text-red-600 hover:text-red-700 text-xs h-7"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Colonne Terminées */}
|
||||||
|
<div className="bg-green-50 rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||||
|
Terminées ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'completed').length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
||||||
|
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'completed').map((task: TaskWithRelations) => {
|
||||||
|
const priorityConfig = getPriorityConfig(task.priority);
|
||||||
|
const PriorityIcon = priorityConfig.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={task.id} className="hover:shadow-md transition-shadow cursor-pointer opacity-75">
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<h5 className="font-medium text-gray-900 text-sm line-through">{task.title}</h5>
|
||||||
|
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs">
|
||||||
|
<PriorityIcon className="w-3 h-3" />
|
||||||
|
{priorityConfig.label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Terminée le: {task.completedAt ? format(new Date(task.completedAt), 'dd/MM/yyyy', { locale: fr }) : 'Date inconnue'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 mt-2">
|
||||||
|
{canEditTasks && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleEditTask(task)}
|
||||||
|
className="text-xs h-7"
|
||||||
|
>
|
||||||
|
<Edit className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleDeleteClick(task)}
|
||||||
|
className="text-red-600 hover:text-red-700 text-xs h-7"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
- **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).
|
- **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.
|
- **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`.
|
- **Reporting:** Statistical reporting, including monthly summaries of deliveries based on `deliveredDate`.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user