Expand task statuses and improve task filtering and sorting

Update task statuses to include a full Kanban workflow (backlog, todo, in_progress, review, testing, done, archived) and enhance filtering and sorting logic 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/GclqueU
This commit is contained in:
michaelschal committed 2025-08-15 13:08:55 +00:00
1 parent 1e8d8f8d19
commit a0e6e03667
4 files changed
+199 -140

No files matched your search

+9 -4
View File
@@ -31,7 +31,7 @@ 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"),
status: z.enum(["backlog", "todo", "in_progress", "review", "testing", "done", "archived"]).default("todo"),
assignedTo: z.string().min(1, "L'assignation est requise"),
dueDate: z.date().optional(),
});
@@ -118,7 +118,7 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
title: task?.title || "",
description: task?.description || "",
priority: (task?.priority as "low" | "medium" | "high") || "medium",
status: (task?.status as "pending" | "completed") || "pending",
status: (task?.status as "backlog" | "todo" | "in_progress" | "review" | "testing" | "done" | "archived") || "todo",
assignedTo: task?.assignedTo || "",
dueDate: task?.dueDate ? new Date(task.dueDate) : undefined,
},
@@ -346,8 +346,13 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="pending">En cours</SelectItem>
<SelectItem value="completed">Terminée</SelectItem>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="todo">À faire</SelectItem>
<SelectItem value="in_progress">En cours</SelectItem>
<SelectItem value="review">Révision</SelectItem>
<SelectItem value="testing">Test</SelectItem>
<SelectItem value="done">Terminé</SelectItem>
<SelectItem value="archived">Archivé</SelectItem>
</SelectContent>
</Select>
<FormMessage />
+188 -134
View File
@@ -194,7 +194,7 @@ export default function Tasks() {
if (!isThisWeek(dueDate)) return false;
break;
case "overdue":
if (!isPast(dueDate) || task.status === 'completed') return false;
if (!isPast(dueDate) || task.status === 'done' || task.status === 'archived') return false;
break;
case "no_due_date":
if (task.dueDate) return false;
@@ -207,9 +207,22 @@ export default function Tasks() {
return true;
})
.sort((a: TaskWithRelations, b: TaskWithRelations) => {
// Faire remonter les tâches non validées (pending) en premier
if (a.status === 'pending' && b.status === 'completed') return -1;
if (a.status === 'completed' && b.status === 'pending') return 1;
// Ordre des statuts (backlog > todo > in_progress > review > testing > done > archived)
const statusOrder = {
backlog: 7,
todo: 6,
in_progress: 5,
review: 4,
testing: 3,
done: 2,
archived: 1
};
const aStatusOrder = statusOrder[a.status as keyof typeof statusOrder] || 0;
const bStatusOrder = statusOrder[b.status as keyof typeof statusOrder] || 0;
if (aStatusOrder !== bStatusOrder) {
return bStatusOrder - aStatusOrder; // Ordre décroissant
}
// Pour les tâches de même statut, trier par priorité (high > medium > low)
const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 };
@@ -266,8 +279,8 @@ export default function Tasks() {
}
};
const getDueDateStatus = (dueDate: string | null, status: string) => {
if (!dueDate || status === 'completed') return null;
const getDueDateStatus = (dueDate: string | Date | null, status: string) => {
if (!dueDate || status === 'done' || status === 'archived') return null;
const due = new Date(dueDate);
const now = new Date();
@@ -308,6 +321,86 @@ export default function Tasks() {
};
};
// Fonction pour rendre une carte Kanban
const renderKanbanCard = (task: TaskWithRelations, canEditTasks: boolean, isCompleted = false) => {
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 ${isCompleted ? 'opacity-75' : ''}`}>
<CardContent className="p-2">
<div className="flex items-start justify-between mb-1">
<h5 className={`font-medium text-gray-900 text-xs ${isCompleted ? 'line-through' : ''}`}>{task.title}</h5>
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs">
<PriorityIcon className="w-2 h-2" />
{priorityConfig.label.charAt(0)}
</Badge>
</div>
{task.description && (
<p className="text-xs text-gray-600 mb-1 line-clamp-1">
{task.description}
</p>
)}
<div className="text-xs text-gray-500 mb-1">
{task.assignedTo}
</div>
{dueDateStatus && DueDateIcon && (
<div className="flex items-center gap-1 mb-1">
<Badge variant={dueDateStatus.color} className="flex items-center gap-1 text-xs">
<DueDateIcon className="w-2 h-2" />
{dueDateStatus.text}
</Badge>
</div>
)}
{isCompleted && (
<div className="text-xs text-gray-500 mb-1">
{task.completedAt ? format(new Date(task.completedAt), 'dd/MM', { locale: fr }) : ''}
</div>
)}
<div className="flex items-center gap-1 mt-1">
{canEditTasks && (
<>
{!isCompleted && (
<Button
size="sm"
variant="outline"
onClick={() => handleCompleteTask(task.id)}
className="text-green-600 hover:text-green-700 text-xs h-5 w-5 p-0"
>
<CheckCircle className="w-2 h-2" />
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleEditTask(task)}
className="text-xs h-5 w-5 p-0"
>
<Edit className="w-2 h-2" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteClick(task)}
className="text-red-600 hover:text-red-700 text-xs h-5 w-5 p-0"
>
<Trash2 className="w-2 h-2" />
</Button>
</>
)}
</div>
</CardContent>
</Card>
);
};
const canCreateTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
const canEditTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
@@ -401,8 +494,13 @@ export default function Tasks() {
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes</SelectItem>
<SelectItem value="pending">En cours</SelectItem>
<SelectItem value="completed">Terminées</SelectItem>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="todo">À faire</SelectItem>
<SelectItem value="in_progress">En cours</SelectItem>
<SelectItem value="review">Révision</SelectItem>
<SelectItem value="testing">Test</SelectItem>
<SelectItem value="done">Terminé</SelectItem>
<SelectItem value="archived">Archivé</SelectItem>
</SelectContent>
</Select>
</div>
@@ -665,138 +763,94 @@ export default function Tasks() {
<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})
<div className="grid grid-cols-7 gap-3 h-full text-sm">
{/* Colonne Backlog */}
<div className="bg-gray-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Circle className="w-4 h-4 text-gray-400" />
Backlog ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'backlog').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 className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'backlog').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</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})
{/* Colonne À faire */}
<div className="bg-blue-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Circle className="w-4 h-4 text-blue-500" />
À faire ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'todo').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 className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'todo').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne En cours */}
<div className="bg-yellow-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Clock className="w-4 h-4 text-yellow-500" />
En cours ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'in_progress').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'in_progress').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Révision */}
<div className="bg-purple-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Edit className="w-4 h-4 text-purple-500" />
Révision ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'review').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'review').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Test */}
<div className="bg-orange-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<AlertTriangle className="w-4 h-4 text-orange-500" />
Test ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'testing').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'testing').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Terminé */}
<div className="bg-green-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<CheckCircle className="w-4 h-4 text-green-500" />
Terminé ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'done').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'done').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks, true);
})}
</div>
</div>
{/* Colonne Archivé */}
<div className="bg-gray-100 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Circle className="w-4 h-4 text-gray-400" />
Archivé ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'archived').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'archived').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks, true);
})}
</div>
</div>