Files
LogiFlow/client/src/pages/Tasks.tsx
T
michaelschal 76d6988ae6 Improve task display for director role by enabling data fetching
Update Tasks.tsx to correctly enable data fetching for director role, ensuring tasks are displayed without manual page reloads. The `enabled` flag in the `useQuery` hook is now set to `!!user` for the tasks query, and the filter logic for `user.role` has been removed.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869/yiEpJFx
2025-09-09 08:25:38 +00:00

1431 lines
56 KiB
TypeScript

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 { 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 { Pagination, usePagination } from "@/components/ui/pagination";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
ListTodo,
Plus,
Search,
Circle,
Clock,
AlertTriangle,
CheckCircle,
Edit,
Trash2,
Calendar,
CalendarX,
CalendarClock,
Kanban
} from "lucide-react";
import { format, isToday, isThisWeek, isPast, differenceInDays } from "date-fns";
import { fr } from "date-fns/locale";
// TaskForm inline pour éviter les problèmes d'import en production
import { Task } from "@shared/schema";
type TaskWithRelations = Task & {
assignedUser?: { id: string; username: string; firstName?: string; lastName?: string; };
creator?: { id: string; username: string; firstName?: string; lastName?: string; };
group?: { id: number; name: string; color: string; };
isFutureTask?: boolean; // Pour les tâches futures (admin/directeur uniquement)
};
// Composant TaskForm inline - Version production ultra-simple
function TaskFormInline({ task, onClose, selectedStoreId, user }: any) {
const { toast } = useToast();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const title = formData.get('title') as string;
const description = formData.get('description') as string;
const priority = formData.get('priority') as string;
const status = formData.get('status') as string;
const assignedTo = formData.get('assignedTo') as string;
const startDate = formData.get('startDate') as string;
const dueDate = formData.get('dueDate') as string;
if (!title?.trim()) {
toast({
title: "Erreur",
description: "Le titre est requis",
variant: "destructive",
});
return;
}
const submitBtn = e.currentTarget.querySelector('button[type="submit"]') as HTMLButtonElement;
submitBtn.disabled = true;
submitBtn.textContent = "...";
try {
const taskData = {
title: title.trim(),
description: description?.trim() || "",
priority: priority || "medium",
status: status || "pending",
assignedTo: assignedTo?.trim() || "Non assigné",
startDate: startDate || null,
dueDate: dueDate || null,
};
const url = task ? `/api/tasks/${task.id}` : '/api/tasks';
const method = task ? 'PUT' : 'POST';
if (!task) {
// Utiliser le magasin sélectionné ou le premier disponible comme fallback
(taskData as any).groupId = selectedStoreId ? parseInt(selectedStoreId.toString()) : 1;
(taskData as any).createdBy = user?.username || 'admin';
}
console.log('📤 Tasks.tsx - Sending request:', {
url,
method,
taskData,
selectedStoreId,
finalGroupId: taskData.groupId
});
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData),
});
console.log('📥 Response:', { status: response.status, ok: response.ok });
if (!response.ok) {
const errorText = await response.text();
console.error('❌ API Error:', { status: response.status, error: errorText });
throw new Error(`Erreur ${response.status}: ${errorText}`);
}
toast({
title: "Succès",
description: task ? "Tâche modifiée avec succès" : "Tâche créée avec succès",
});
window.location.reload();
} catch (error) {
console.error('Erreur:', error);
toast({
title: "Erreur",
description: "Erreur lors de l'opération",
variant: "destructive",
});
submitBtn.disabled = false;
submitBtn.textContent = task ? "Modifier" : "Créer";
}
};
return (
<div style={{ padding: '24px', backgroundColor: 'white' }}>
<div style={{ borderBottom: '1px solid #e5e7eb', paddingBottom: '16px', marginBottom: '24px' }}>
<p style={{ fontSize: '14px', color: '#6b7280', margin: '0' }}>
📅 Date de début = Quand la tâche devient visible<br/>
⏰ Date d'échéance = Quand la tâche doit être terminée
</p>
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
Titre *
</label>
<input
name="title"
type="text"
defaultValue={task?.title || ""}
placeholder="Titre de la tâche"
required
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '0px',
fontSize: '14px'
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
Description
</label>
<textarea
name="description"
defaultValue={task?.description || ""}
placeholder="Description de la tâche (optionnel)"
rows={3}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '0px',
fontSize: '14px',
resize: 'vertical'
}}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
Priorité *
</label>
<select
name="priority"
defaultValue={task?.priority || "medium"}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '0px',
fontSize: '14px'
}}
>
<option value="low">🟢 Faible</option>
<option value="medium">🟡 Moyenne</option>
<option value="high">🔴 Élevée</option>
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
Statut
</label>
<select
name="status"
defaultValue={task?.status || "pending"}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '0px',
fontSize: '14px'
}}
>
<option value="pending">⏳ En cours</option>
<option value="completed">✅ Terminée</option>
</select>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
📅 Date de début <span style={{ fontSize: '12px', color: '#6b7280' }}>(optionnel)</span>
</label>
<input
name="startDate"
type="date"
defaultValue={task?.startDate ? new Date(task.startDate).toISOString().split('T')[0] : ""}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '0px',
fontSize: '14px'
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
⏰ Date d'échéance <span style={{ fontSize: '12px', color: '#6b7280' }}>(optionnel)</span>
</label>
<input
name="dueDate"
type="date"
defaultValue={task?.dueDate ? new Date(task.dueDate).toISOString().split('T')[0] : ""}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '0px',
fontSize: '14px'
}}
/>
</div>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
Assigné à <span style={{ fontSize: '12px', color: '#6b7280' }}>(optionnel)</span>
</label>
<input
name="assignedTo"
type="text"
defaultValue={task?.assignedTo || ""}
placeholder="Nom de la personne assignée"
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '0px',
fontSize: '14px'
}}
/>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button
type="button"
onClick={onClose}
style={{
padding: '8px 16px',
border: '1px solid #d1d5db',
borderRadius: '0px',
backgroundColor: 'white',
cursor: 'pointer'
}}
>
Annuler
</button>
<button
type="submit"
style={{
padding: '8px 16px',
border: 'none',
borderRadius: '0px',
backgroundColor: '#3b82f6',
color: 'white',
cursor: 'pointer'
}}
>
{task ? "Modifier" : "Créer"}
</button>
</div>
</form>
</div>
);
}
export default function Tasks() {
const { user } = useAuthUnified();
const { selectedStoreId, storeInitialized } = 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");
const [dueDateFilter, setDueDateFilter] = useState<string>("all");
const [viewMode, setViewMode] = useState<string>("list");
// Modals
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedTask, setSelectedTask] = useState<TaskWithRelations | null>(null);
const [taskToDelete, setTaskToDelete] = useState<TaskWithRelations | null>(null);
// Fetch tasks - attendre que l'initialisation des stores soit terminée pour les admins
const { data: tasks = [], isLoading, error } = useQuery({
queryKey: ["/api/tasks", selectedStoreId],
queryFn: async () => {
try {
const params = new URLSearchParams();
// CRITICAL FIX: Appliquer le filtrage par storeId pour TOUS les rôles, pas seulement admin
if (selectedStoreId) {
params.append('storeId', selectedStoreId.toString());
}
console.log('📋 TASKS QUERY - Fetching with params:', {
selectedStoreId,
userRole: user?.role,
storeInitialized,
url: `/api/tasks?${params.toString()}`,
willFilterByStore: !!selectedStoreId,
enabled: !!user,
timestamp: new Date().toISOString()
});
const response = await fetch(`/api/tasks?${params.toString()}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('📋 TASKS QUERY - Response received:', {
dataType: typeof data,
isArray: Array.isArray(data),
length: data?.length,
firstTask: data?.[0] ? {
id: data[0].id,
title: data[0].title,
hasStartDate: !!data[0].startDate,
hasGroup: !!data[0].group
} : null
});
// Valider et nettoyer les données reçues
if (!Array.isArray(data)) {
console.error('Tasks API returned non-array data:', data);
return [];
}
return data.filter(task => task && typeof task === 'object' && task.id);
} catch (error) {
console.error('Error fetching tasks:', error);
throw error;
}
},
enabled: !!user,
});
// Fetch users for task assignment - seulement pour admin/manager/directeur
const { data: users = [] } = useQuery({
queryKey: ["/api/users"],
queryFn: () => fetch('/api/users', {
credentials: 'include'
}).then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
}),
enabled: !!user && (user.role === 'admin' || user.role === 'manager' || user.role === 'directeur'),
});
const handleEditTask = (task: TaskWithRelations) => {
setSelectedTask(task);
setShowEditModal(true);
};
const handleCompleteTask = async (taskId: number) => {
try {
const response = await fetch(`/api/tasks/${taskId}/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error('Erreur lors de la completion de la tâche');
}
await queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
toast({
title: "Succès",
description: "Tâche marquée comme terminée",
});
} catch (error) {
console.error("Error completing task:", error);
toast({
title: "Erreur",
description: "Impossible de terminer la tâche",
variant: "destructive",
});
}
};
const handleDeleteClick = (task: TaskWithRelations) => {
setTaskToDelete(task);
setShowDeleteModal(true);
};
const handleConfirmDelete = async () => {
if (!taskToDelete) return;
try {
const response = await fetch(`/api/tasks/${taskToDelete.id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Erreur lors de la suppression');
}
await queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
toast({
title: "Succès",
description: "Tâche supprimée avec succès",
});
setShowDeleteModal(false);
setTaskToDelete(null);
} catch (error) {
console.error("Error deleting task:", error);
toast({
title: "Erreur",
description: "Impossible de supprimer la tâche",
variant: "destructive",
});
}
};
// Filtrer et trier les tâches
const filteredTasks = tasks
.filter((task: TaskWithRelations) => {
// Filtre par recherche
if (searchTerm && !task.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
!task.description?.toLowerCase().includes(searchTerm.toLowerCase())) {
return false;
}
// Filtre par statut
if (statusFilter !== "all" && task.status !== statusFilter) {
return false;
}
// Filtre par priorité
if (priorityFilter !== "all" && task.priority !== priorityFilter) {
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;
})
.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;
// Pour les tâches de même statut, trier par priorité (high > medium > low)
const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 };
const aPriority = priorityOrder[a.priority as keyof typeof priorityOrder] || 2;
const bPriority = priorityOrder[b.priority as keyof typeof priorityOrder] || 2;
if (aPriority !== bPriority) {
return bPriority - aPriority; // Ordre décroissant (high en premier)
}
// Enfin, trier par date de création (plus récent en premier)
const aDate = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const bDate = b.createdAt ? new Date(b.createdAt).getTime() : 0;
return bDate - aDate;
});
// Pagination
const {
currentPage,
setCurrentPage,
itemsPerPage,
setItemsPerPage,
totalPages,
paginatedData: paginatedTasks,
totalItems
} = usePagination(filteredTasks, 10);
const getPriorityConfig = (priority: string) => {
switch (priority) {
case 'high':
return {
color: 'destructive' as const,
icon: AlertTriangle,
label: 'Élevée'
};
case 'medium':
return {
color: 'default' as const,
icon: Clock,
label: 'Moyenne'
};
case 'low':
return {
color: 'secondary' as const,
icon: Circle,
label: 'Faible'
};
default:
return {
color: 'secondary' as const,
icon: Circle,
label: 'Moyenne'
};
}
};
const getDueDateStatus = (dueDate: Date | 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 getStartDateStatus = (startDate: Date | string | null) => {
if (!startDate) return null;
try {
const start = new Date(startDate);
if (isNaN(start.getTime())) return null;
const now = new Date();
const daysDiff = differenceInDays(start, now);
if (isPast(start)) {
return {
type: 'started',
color: 'default' as const,
icon: CheckCircle,
label: 'Démarrée',
text: 'Active'
};
} else if (isToday(start)) {
return {
type: 'today',
color: 'secondary' as const,
icon: CalendarClock,
label: "Démarre aujourd'hui",
text: "Aujourd'hui"
};
} else {
return {
type: 'future',
color: 'outline' as const,
icon: Clock,
label: `Démarre dans ${daysDiff} jour${daysDiff > 1 ? 's' : ''}`,
text: `+${daysDiff}j`
};
}
} catch (error) {
console.warn('Error parsing start date:', error, startDate);
return null;
}
};
// Fonction pour déterminer si une tâche est future (pas encore commencée)
const isTaskFuture = (task: TaskWithRelations) => {
if (!task) return false;
try {
return task.isFutureTask || (task.startDate && !isPast(new Date(task.startDate)));
} catch (error) {
console.warn('Error checking if task is future:', error, task);
return false;
}
};
const canCreateTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
const canEditTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
if (isLoading) {
return (
<div className="flex-1 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="bg-white border-b border-gray-200 p-6 shadow-sm -m-6 mb-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold text-gray-900 flex items-center">
<ListTodo className="w-6 h-6 mr-3 text-blue-600" />
Gestion des Tâches
</h2>
<p className="text-gray-600 mt-1">
{totalItems} tâche{totalItems !== 1 ? 's' : ''} trouvée{totalItems !== 1 ? 's' : ''}
</p>
</div>
<div className="flex items-center gap-3">
{/* Sélecteur de vue */}
<Tabs value={viewMode} onValueChange={setViewMode} className="w-auto">
<TabsList className="grid w-full grid-cols-2">
<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>
{canCreateTasks && (
<>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white shadow-md"
onClick={() => setShowCreateModal(true)}
>
<Plus className="w-4 h-4 mr-2" />
Nouvelle Tâche
</Button>
{showCreateModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '0px',
maxWidth: '600px',
width: '90%',
maxHeight: '90vh',
overflow: 'auto',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)'
}}>
<div style={{
padding: '24px 24px 16px',
borderBottom: '1px solid #e5e7eb',
position: 'relative'
}}>
<h2 style={{
fontSize: '18px',
fontWeight: '600',
margin: '0'
}}>
Créer une nouvelle tâche
</h2>
<button
onClick={() => setShowCreateModal(false)}
style={{
position: 'absolute',
top: '16px',
right: '16px',
background: 'none',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
padding: '4px',
borderRadius: '0px',
color: '#6b7280',
transition: 'color 0.2s'
}}
onMouseEnter={(e) => (e.target as HTMLButtonElement).style.color = '#374151'}
onMouseLeave={(e) => (e.target as HTMLButtonElement).style.color = '#6b7280'}
>
✕
</button>
</div>
<TaskFormInline
selectedStoreId={selectedStoreId}
user={user}
onClose={() => setShowCreateModal(false)}
/>
</div>
</div>
)}
</>
)}
</div>
</div>
</div>
<div className="flex gap-6">
{/* Sidebar avec filtres */}
<div className="w-80 bg-gray-50 border-r border-gray-200 p-4 flex-shrink-0">
{/* Filtres */}
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-lg">Filtres</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Recherche */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Rechercher..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* Filtre par statut */}
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">
Statut
</label>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes</SelectItem>
<SelectItem value="pending">En cours</SelectItem>
<SelectItem value="completed">Terminées</SelectItem>
</SelectContent>
</Select>
</div>
{/* Filtre par priorité */}
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">
Priorité
</label>
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes</SelectItem>
<SelectItem value="high">Élevée</SelectItem>
<SelectItem value="medium">Moyenne</SelectItem>
<SelectItem value="low">Faible</SelectItem>
</SelectContent>
</Select>
</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>
</Card>
</div>
{/* Zone principale avec les tâches */}
<div className="flex-1">
{/* Contenu selon la vue sélectionnée */}
<Tabs value={viewMode} onValueChange={setViewMode} className="h-full">
<TabsContent value="list" className="mt-0">
{/* Liste des tâches */}
<div className="p-6">
{totalItems === 0 ? (
<div className="text-center py-12">
<ListTodo className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
Aucune tâche
</h3>
<p className="text-gray-600">
Aucune tâche trouvée avec les filtres sélectionnés.
</p>
</div>
) : (
<div className="space-y-4">
{/* 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 en cours ({paginatedTasks.filter(task => task.status === 'pending').length})
</h4>
<div className="space-y-3">
{paginatedTasks
.filter(task => task.status === 'pending')
.map((task) => {
const priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon;
const isFuture = isTaskFuture(task);
const startDateStatus = getStartDateStatus(task.startDate);
return (
<Card key={task.id} className={isFuture ?
"hover:shadow-md transition-all duration-200 bg-gray-50 border-dashed border-gray-300 opacity-75" :
"hover:shadow-md transition-all duration-200 bg-white border-solid"}>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h5 className={isFuture ? "font-medium truncate text-gray-600 italic" : "font-medium truncate text-gray-900"}>
{task.title}
{isFuture && " (Programmée)"}
</h5>
<Badge variant={priorityConfig.color} className="flex items-center gap-1">
<PriorityIcon className="w-3 h-3" />
{priorityConfig.label}
</Badge>
{isFuture && (
<Badge variant="outline" className="flex items-center gap-1 text-xs border-orange-300 text-orange-700 bg-orange-50">
<Clock className="w-3 h-3" />
Future
</Badge>
)}
</div>
{task.description && (
<p className="text-sm text-gray-600 mb-2 line-clamp-2">
{task.description}
</p>
)}
<div className="flex items-center gap-4 text-xs text-gray-500 mb-2">
<span>
Assigné à: {task.assignedTo}
</span>
<span>
Créée: {format(new Date(task.createdAt), 'dd/MM/yyyy HH:mm', { locale: fr })}
</span>
</div>
{/* Affichage des dates : départ et échéance */}
<div className="flex items-center gap-4 mb-2">{[
// Date de départ
startDateStatus && (
<div key="start" className="flex items-center gap-2">
<Badge variant={startDateStatus.color} className="flex items-center gap-1 text-xs">
<startDateStatus.icon className="w-3 h-3" />
📅 {startDateStatus.text}
</Badge>
<span className="text-xs text-gray-500">
{startDateStatus.label}
{task.startDate && ` (${format(new Date(task.startDate), 'dd/MM/yyyy', { locale: fr })})`}
</span>
</div>
),
// Date d'échéance
(() => {
const dueDateStatus = getDueDateStatus(task.dueDate, task.status);
if (dueDateStatus) {
const DueDateIcon = dueDateStatus.icon;
return (
<div key="due" 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 key="due-normal" 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;
})()
].filter(Boolean)}</div>
</div>
<div className="flex items-center gap-2 ml-4">
{/* Avertissement pour les tâches futures (admin/directeur) */}
{isFuture && (
<div className="text-xs text-orange-600 bg-orange-100 px-2 py-1 rounded mr-2">
Visible en avance ({user?.role})
</div>
)}
{canEditTasks && !isFuture && (
<Button
size="sm"
variant="outline"
onClick={() => handleCompleteTask(task.id)}
className="text-green-600 hover:text-green-700"
>
<CheckCircle className="w-4 h-4" />
</Button>
)}
{canEditTasks && (
<Button
size="sm"
variant="outline"
onClick={() => handleEditTask(task)}
className={isFuture ? "opacity-60" : ""}
>
<Edit className="w-4 h-4" />
</Button>
)}
{canEditTasks && (
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteClick(task)}
className={isFuture ? "text-red-600 hover:text-red-700 opacity-60" : "text-red-600 hover:text-red-700"}
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
</div>
)}
{/* 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 => task.status === 'completed').length})
</h4>
<div className="space-y-3">
{paginatedTasks
.filter(task => task.status === 'completed')
.map((task) => {
const priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon;
return (
<Card key={task.id} className="opacity-60 bg-gray-50">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h5 className="font-medium text-gray-500 truncate line-through">
{task.title}
</h5>
<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>
{task.description && (
<p className="text-sm text-gray-400 mb-2 line-clamp-2 line-through">
{task.description}
</p>
)}
<div className="flex items-center gap-4 text-xs text-gray-500">
<span>
Assigné à: {task.assignedTo}
</span>
<span>
Créée: {format(new Date(task.createdAt), 'dd/MM/yyyy HH:mm', { locale: fr })}
</span>
{task.completedAt && (
<span>
Terminée: {format(new Date(task.completedAt), 'dd/MM/yyyy HH:mm', { locale: fr })}
</span>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteClick(task)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
</div>
)}
</div>
)}
{/* Pagination */}
{totalItems > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={setItemsPerPage}
className="mt-4 border-t border-gray-200 pt-4 mx-6"
/>
)}
</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>
{/* Modal d'édition */}
{showEditModal && selectedTask && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '0px',
maxWidth: '600px',
width: '90%',
maxHeight: '90vh',
overflow: 'auto',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)'
}}>
<div style={{
padding: '24px 24px 16px',
borderBottom: '1px solid #e5e7eb',
position: 'relative'
}}>
<h2 style={{
fontSize: '18px',
fontWeight: '600',
margin: '0'
}}>
Modifier la tâche
</h2>
<button
onClick={() => {
setShowEditModal(false);
setSelectedTask(null);
}}
style={{
position: 'absolute',
top: '16px',
right: '16px',
background: 'none',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
padding: '4px',
borderRadius: '0px',
color: '#6b7280',
transition: 'color 0.2s'
}}
onMouseEnter={(e) => (e.target as HTMLButtonElement).style.color = '#374151'}
onMouseLeave={(e) => (e.target as HTMLButtonElement).style.color = '#6b7280'}
>
✕
</button>
</div>
<TaskFormInline
task={selectedTask}
selectedStoreId={selectedStoreId}
user={user}
onClose={() => {
setShowEditModal(false);
setSelectedTask(null);
}}
/>
</div>
</div>
)}
{/* Modal de confirmation de suppression */}
{showDeleteModal && taskToDelete && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '0px',
maxWidth: '400px',
width: '90%',
padding: '24px',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)'
}}>
<h2 style={{
fontSize: '18px',
fontWeight: '600',
margin: '0 0 16px 0',
color: '#dc2626',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
⚠️ Confirmer la suppression
</h2>
<div style={{ marginBottom: '24px' }}>
<p style={{
color: '#374151',
lineHeight: '1.5',
margin: '0 0 8px 0'
}}>
Êtes-vous sûr de vouloir supprimer la tâche "{taskToDelete.title}" ?
</p>
<p style={{
fontSize: '14px',
color: '#6b7280',
margin: '0'
}}>
Cette action est irréversible.
</p>
</div>
<div style={{
display: 'flex',
gap: '12px',
justifyContent: 'flex-end'
}}>
<button
onClick={() => {
setShowDeleteModal(false);
setTaskToDelete(null);
}}
style={{
padding: '8px 16px',
border: '1px solid #d1d5db',
borderRadius: '0px',
backgroundColor: 'white',
cursor: 'pointer'
}}
>
Annuler
</button>
<button
onClick={handleConfirmDelete}
style={{
padding: '8px 16px',
border: 'none',
borderRadius: '0px',
backgroundColor: '#dc2626',
color: 'white',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
🗑️ Supprimer
</button>
</div>
</div>
</div>
)}
</div>
);
}