mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-18 12:47:33 +02:00
latest
This commit is contained in:
1 parent
e3fb29ab1d
commit
2485f71dd7
95 files changed
+41842
No files matched your search
@@ -0,0 +1,494 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||
import { useStore } from "@/components/Layout";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import {
|
||||
ListTodo,
|
||||
Plus,
|
||||
Search,
|
||||
Circle,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Edit,
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { Task } from "@shared/schema";
|
||||
|
||||
type TaskWithRelations = Task & {
|
||||
creator: { id: string; username: string; name: string; };
|
||||
group: { id: number; name: string; color: string; };
|
||||
};
|
||||
|
||||
export default function TasksSimplified() {
|
||||
const { user } = useAuthUnified();
|
||||
const { selectedStoreId } = useStore();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// États locaux
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const [priorityFilter, setPriorityFilter] = useState<string>("all");
|
||||
|
||||
// Modals et tâche sélectionnée
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<TaskWithRelations | null>(null);
|
||||
|
||||
// Récupération des tâches
|
||||
const { data: tasks = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/tasks", selectedStoreId],
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedStoreId) {
|
||||
params.append('storeId', selectedStoreId);
|
||||
}
|
||||
return fetch(`/api/tasks?${params.toString()}`).then(res => res.json());
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Récupération des magasins
|
||||
const { data: stores = [] } = useQuery({
|
||||
queryKey: ["/api/groups"],
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Filtrer les tâches
|
||||
const filteredTasks = tasks.filter((task: TaskWithRelations) => {
|
||||
const matchesSearch = task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
task.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
task.assignedTo?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter;
|
||||
const matchesPriority = priorityFilter === "all" || task.priority === priorityFilter;
|
||||
|
||||
return matchesSearch && matchesStatus && matchesPriority;
|
||||
});
|
||||
|
||||
// Création d'une tâche
|
||||
const handleCreateTask = async (taskData: any) => {
|
||||
try {
|
||||
await apiRequest('/api/tasks', 'POST', {
|
||||
...taskData,
|
||||
groupId: selectedStoreId ? parseInt(selectedStoreId) : stores[0]?.id
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
setShowCreateModal(false);
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche créée avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de créer la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Modification d'une tâche
|
||||
const handleUpdateTask = async (taskData: any) => {
|
||||
if (!selectedTask) return;
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/tasks/${selectedTask.id}`, 'PUT', taskData);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
setShowEditModal(false);
|
||||
setSelectedTask(null);
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche modifiée avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de modifier la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Complétion d'une tâche
|
||||
const handleCompleteTask = async (taskId: number) => {
|
||||
try {
|
||||
await apiRequest(`/api/tasks/${taskId}/complete`, 'POST');
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche marquée comme terminée",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de compléter la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Suppression d'une tâche
|
||||
const handleDeleteTask = async (taskId: number) => {
|
||||
try {
|
||||
await apiRequest(`/api/tasks/${taskId}`, 'DELETE');
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche supprimée avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de supprimer la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTask = (task: TaskWithRelations) => {
|
||||
setSelectedTask(task);
|
||||
setShowEditModal(true);
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed": return "bg-green-100 text-green-800";
|
||||
case "pending": return "bg-yellow-100 text-yellow-800";
|
||||
default: return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case "high": return "bg-red-100 text-red-800";
|
||||
case "medium": return "bg-blue-100 text-blue-800";
|
||||
case "low": return "bg-green-100 text-green-800";
|
||||
default: return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-lg">Chargement des tâches...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-lg">Vous devez être connecté pour accéder aux tâches.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<ListTodo className="h-6 w-6 text-blue-600" />
|
||||
<h1 className="text-2xl font-bold">Gestion des Tâches</h1>
|
||||
</div>
|
||||
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nouvelle Tâche
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Créer une nouvelle tâche</DialogTitle>
|
||||
</DialogHeader>
|
||||
<TaskForm onSubmit={handleCreateTask} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Filtres</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Rechercher une tâche..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-[150px]">
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Statut" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous les statuts</SelectItem>
|
||||
<SelectItem value="pending">En attente</SelectItem>
|
||||
<SelectItem value="completed">Terminée</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="min-w-[150px]">
|
||||
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Priorité" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toutes priorités</SelectItem>
|
||||
<SelectItem value="high">Haute</SelectItem>
|
||||
<SelectItem value="medium">Moyenne</SelectItem>
|
||||
<SelectItem value="low">Basse</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Liste des tâches */}
|
||||
<div className="space-y-4">
|
||||
{filteredTasks.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center h-32">
|
||||
<div className="text-center">
|
||||
<ListTodo className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">Aucune tâche trouvée</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
filteredTasks.map((task: TaskWithRelations) => (
|
||||
<Card key={task.id} className={`hover:shadow-md transition-shadow ${task.status === 'completed' ? 'opacity-60 bg-gray-50' : ''}`}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className={`font-semibold text-lg ${task.status === 'completed' ? 'line-through text-gray-500' : ''}`}>{task.title}</h3>
|
||||
<Badge className={getStatusColor(task.status)}>
|
||||
{task.status === "completed" ? "Terminée" : "En attente"}
|
||||
</Badge>
|
||||
<Badge className={getPriorityColor(task.priority)}>
|
||||
{task.priority === "high" ? "Haute" : task.priority === "medium" ? "Moyenne" : "Basse"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
<p className={`mb-3 ${task.status === 'completed' ? 'text-gray-400 line-through' : 'text-gray-600'}`}>{task.description}</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-4 text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Assigné à:</span>
|
||||
<span>{task.assignedTo || "Non assigné"}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Magasin:</span>
|
||||
<span className="px-2 py-1 rounded text-xs" style={{ backgroundColor: task.group.color + '20', color: task.group.color }}>
|
||||
{task.group.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Créé par:</span>
|
||||
<span>{task.creator.name || task.creator.username}</span>
|
||||
</div>
|
||||
|
||||
{task.dueDate && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>
|
||||
Échéance: {format(new Date(task.dueDate), 'dd/MM/yyyy HH:mm', { locale: fr })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{task.status !== "completed" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCompleteTask(task.id)}
|
||||
className="text-green-600 hover:text-green-700"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleEditTask(task)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDeleteTask(task.id)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal de modification */}
|
||||
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Modifier la tâche</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedTask && (
|
||||
<TaskForm
|
||||
initialData={selectedTask}
|
||||
onSubmit={handleUpdateTask}
|
||||
isEditing={true}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Composant de formulaire simplifié
|
||||
function TaskForm({ onSubmit, initialData, isEditing = false }: {
|
||||
onSubmit: (data: any) => void;
|
||||
initialData?: TaskWithRelations;
|
||||
isEditing?: boolean;
|
||||
}) {
|
||||
const [title, setTitle] = useState(initialData?.title || "");
|
||||
const [description, setDescription] = useState(initialData?.description || "");
|
||||
const [assignedTo, setAssignedTo] = useState(initialData?.assignedTo || "");
|
||||
const [priority, setPriority] = useState(initialData?.priority || "medium");
|
||||
const [dueDate, setDueDate] = useState(
|
||||
initialData?.dueDate ? format(new Date(initialData.dueDate), 'yyyy-MM-dd') : ""
|
||||
);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!title.trim() || !assignedTo.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskData = {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
assignedTo: assignedTo.trim(),
|
||||
priority,
|
||||
dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
|
||||
};
|
||||
|
||||
onSubmit(taskData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium mb-1">
|
||||
Titre de la tâche *
|
||||
</label>
|
||||
<Input
|
||||
id="title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Titre de la tâche"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Description de la tâche"
|
||||
className="w-full p-2 border rounded-md"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="assignedTo" className="block text-sm font-medium mb-1">
|
||||
Assigné à *
|
||||
</label>
|
||||
<Input
|
||||
id="assignedTo"
|
||||
value={assignedTo}
|
||||
onChange={(e) => setAssignedTo(e.target.value)}
|
||||
placeholder="Nom de la personne responsable"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="priority" className="block text-sm font-medium mb-1">
|
||||
Priorité
|
||||
</label>
|
||||
<Select value={priority} onValueChange={setPriority}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Basse</SelectItem>
|
||||
<SelectItem value="medium">Moyenne</SelectItem>
|
||||
<SelectItem value="high">Haute</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="dueDate" className="block text-sm font-medium mb-1">
|
||||
Date d'échéance
|
||||
</label>
|
||||
<Input
|
||||
id="dueDate"
|
||||
type="date"
|
||||
value={dueDate}
|
||||
onChange={(e) => setDueDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="submit">
|
||||
{isEditing ? "Modifier" : "Créer"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user