mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
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:
1 parent
1e8d8f8d19
commit
a0e6e03667
4 files changed
+199
-140
No files matched your search
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user