mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-19 05:07:57 +02:00
Restored to '1e8d8f8d195518d29262c2b52d36fcaac4155d9f'
Replit-Restored-To: 1e8d8f8d19
This commit is contained in:
1 parent
bf159b2447
commit
75b353153f
4 files changed
+172
-185
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(["backlog", "todo", "in_progress", "review", "testing", "blocked", "ready_to_deploy", "deployed", "done", "archived"]).default("todo"),
|
||||
status: z.enum(["pending", "completed"]).default("pending"),
|
||||
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 "backlog" | "todo" | "in_progress" | "review" | "testing" | "blocked" | "ready_to_deploy" | "deployed" | "done" | "archived") || "todo",
|
||||
status: (task?.status as "pending" | "completed") || "pending",
|
||||
assignedTo: task?.assignedTo || "",
|
||||
dueDate: task?.dueDate ? new Date(task.dueDate) : undefined,
|
||||
},
|
||||
@@ -346,16 +346,8 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<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="blocked">Bloqué</SelectItem>
|
||||
<SelectItem value="ready_to_deploy">Prêt Déploiement</SelectItem>
|
||||
<SelectItem value="deployed">Déployé</SelectItem>
|
||||
<SelectItem value="done">Terminé</SelectItem>
|
||||
<SelectItem value="archived">Archivé</SelectItem>
|
||||
<SelectItem value="pending">En cours</SelectItem>
|
||||
<SelectItem value="completed">Terminée</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
||||
+166
-171
@@ -48,7 +48,7 @@ export default function Tasks() {
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const [priorityFilter, setPriorityFilter] = useState<string>("all");
|
||||
const [dueDateFilter, setDueDateFilter] = useState<string>("all");
|
||||
const [viewMode, setViewMode] = useState<string>("kanban");
|
||||
const [viewMode, setViewMode] = useState<string>("list");
|
||||
|
||||
// Modals
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
@@ -194,7 +194,7 @@ export default function Tasks() {
|
||||
if (!isThisWeek(dueDate)) return false;
|
||||
break;
|
||||
case "overdue":
|
||||
if (!isPast(dueDate) || task.status === 'done' || task.status === 'archived') return false;
|
||||
if (!isPast(dueDate) || task.status === 'completed') return false;
|
||||
break;
|
||||
case "no_due_date":
|
||||
if (task.dueDate) return false;
|
||||
@@ -207,22 +207,9 @@ export default function Tasks() {
|
||||
return true;
|
||||
})
|
||||
.sort((a: TaskWithRelations, b: TaskWithRelations) => {
|
||||
// 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
|
||||
}
|
||||
// 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;
|
||||
|
||||
// Pour les tâches de même statut, trier par priorité (high > medium > low)
|
||||
const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 };
|
||||
@@ -279,8 +266,8 @@ export default function Tasks() {
|
||||
}
|
||||
};
|
||||
|
||||
const getDueDateStatus = (dueDate: string | Date | null, status: string) => {
|
||||
if (!dueDate || status === 'done' || status === 'archived') return null;
|
||||
const getDueDateStatus = (dueDate: string | null, status: string) => {
|
||||
if (!dueDate || status === 'completed') return null;
|
||||
|
||||
const due = new Date(dueDate);
|
||||
const now = new Date();
|
||||
@@ -321,86 +308,6 @@ 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';
|
||||
|
||||
@@ -430,14 +337,14 @@ export default function Tasks() {
|
||||
{/* Sélecteur de vue */}
|
||||
<Tabs value={viewMode} onValueChange={setViewMode} className="w-auto">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="kanban" className="flex items-center gap-2">
|
||||
<Kanban className="w-4 h-4" />
|
||||
Kanban
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="list" className="flex items-center gap-2">
|
||||
<ListTodo className="w-4 h-4" />
|
||||
Liste
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="kanban" className="flex items-center gap-2">
|
||||
<Kanban className="w-4 h-4" />
|
||||
Kanban
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -494,16 +401,8 @@ export default function Tasks() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toutes</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="blocked">Bloqué</SelectItem>
|
||||
<SelectItem value="ready_to_deploy">Prêt Déploiement</SelectItem>
|
||||
<SelectItem value="deployed">Déployé</SelectItem>
|
||||
<SelectItem value="done">Terminé</SelectItem>
|
||||
<SelectItem value="archived">Archivé</SelectItem>
|
||||
<SelectItem value="pending">En cours</SelectItem>
|
||||
<SelectItem value="completed">Terminées</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -552,39 +451,6 @@ export default function Tasks() {
|
||||
<div className="flex-1">
|
||||
{/* Contenu selon la vue sélectionnée */}
|
||||
<Tabs value={viewMode} onValueChange={setViewMode} className="h-full">
|
||||
<TabsContent value="kanban" className="mt-0">
|
||||
{/* Vue Kanban simple - 2 colonnes */}
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-2 gap-6 h-full">
|
||||
{/* Colonne À faire */}
|
||||
<div className="bg-blue-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-blue-500" />
|
||||
À faire ({filteredTasks.filter((task: TaskWithRelations) => task.status !== 'done' && task.status !== 'archived').length})
|
||||
</h3>
|
||||
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
||||
{filteredTasks.filter((task: TaskWithRelations) => task.status !== 'done' && task.status !== 'archived').map((task: TaskWithRelations) => {
|
||||
return renderKanbanCard(task, canEditTasks);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Colonne Terminé */}
|
||||
<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é ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'done' || task.status === 'archived').length})
|
||||
</h3>
|
||||
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
||||
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'done' || task.status === 'archived').map((task: TaskWithRelations) => {
|
||||
return renderKanbanCard(task, canEditTasks, true);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="list" className="mt-0">
|
||||
{/* Liste des tâches */}
|
||||
<div className="p-6">
|
||||
@@ -600,15 +466,15 @@ export default function Tasks() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Tâches À faire en premier */}
|
||||
{paginatedTasks.filter(task => ['todo', 'backlog', 'in_progress', 'review', 'testing', 'blocked', 'ready_to_deploy', 'deployed'].includes(task.status)).length > 0 && (
|
||||
{/* Tâches en cours */}
|
||||
{paginatedTasks.filter(task => task.status === 'pending').length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-3">
|
||||
Tâches à faire ({paginatedTasks.filter(task => ['todo', 'backlog', 'in_progress', 'review', 'testing', 'blocked', 'ready_to_deploy', 'deployed'].includes(task.status)).length})
|
||||
Tâches en cours ({paginatedTasks.filter(task => task.status === 'pending').length})
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{paginatedTasks
|
||||
.filter(task => ['todo', 'backlog', 'in_progress', 'review', 'testing', 'blocked', 'ready_to_deploy', 'deployed'].includes(task.status))
|
||||
.filter(task => task.status === 'pending')
|
||||
.map((task) => {
|
||||
const priorityConfig = getPriorityConfig(task.priority);
|
||||
const PriorityIcon = priorityConfig.icon;
|
||||
@@ -626,16 +492,6 @@ export default function Tasks() {
|
||||
<PriorityIcon className="w-3 h-3" />
|
||||
{priorityConfig.label}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{task.status === 'backlog' ? 'Backlog' :
|
||||
task.status === 'todo' ? 'À faire' :
|
||||
task.status === 'in_progress' ? 'En cours' :
|
||||
task.status === 'review' ? 'Révision' :
|
||||
task.status === 'testing' ? 'Test' :
|
||||
task.status === 'blocked' ? 'Bloqué' :
|
||||
task.status === 'ready_to_deploy' ? 'Prêt Déploiement' :
|
||||
task.status === 'deployed' ? 'Déployé' : task.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
@@ -721,15 +577,15 @@ export default function Tasks() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tâches terminées et archivées */}
|
||||
{paginatedTasks.filter(task => ['done', 'archived'].includes(task.status)).length > 0 && (
|
||||
{/* Tâches terminées */}
|
||||
{paginatedTasks.filter(task => task.status === 'completed').length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-3 mt-8">
|
||||
Tâches terminées ({paginatedTasks.filter(task => ['done', 'archived'].includes(task.status)).length})
|
||||
Tâches terminées ({paginatedTasks.filter(task => task.status === 'completed').length})
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{paginatedTasks
|
||||
.filter(task => ['done', 'archived'].includes(task.status))
|
||||
.filter(task => task.status === 'completed')
|
||||
.map((task) => {
|
||||
const priorityConfig = getPriorityConfig(task.priority);
|
||||
const PriorityIcon = priorityConfig.icon;
|
||||
@@ -743,12 +599,9 @@ export default function Tasks() {
|
||||
<h5 className="font-medium text-gray-500 truncate line-through">
|
||||
{task.title}
|
||||
</h5>
|
||||
<Badge
|
||||
variant={task.status === 'done' ? 'secondary' : 'outline'}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
{task.status === 'done' ? 'Terminée' : 'Archivée'}
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3 border border-gray-300 rounded p-0.5" />
|
||||
Terminée
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -808,6 +661,148 @@ export default function Tasks() {
|
||||
)}
|
||||
</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>
|
||||
|
||||
Reference in new issue
Block a user