diff --git a/client/src/components/ResponsiveTasks.tsx b/client/src/components/ResponsiveTasks.tsx new file mode 100644 index 0000000..76e8d84 --- /dev/null +++ b/client/src/components/ResponsiveTasks.tsx @@ -0,0 +1,19 @@ +/** + * ResponsiveTasks - Wrapper component that automatically detects screen size + * and renders either the mobile or desktop version of the Tasks page. + */ +import { useScreenSize } from "@/hooks/use-screen-size"; +import Tasks from "@/pages/Tasks"; +import MobileTasksPage from "@/pages/mobile/TasksPage"; + +export default function ResponsiveTasks() { + const { isMobile } = useScreenSize(); + + // Render mobile version for screens < 768px + if (isMobile) { + return ; + } + + // Render desktop version for larger screens + return ; +} diff --git a/client/src/components/RouterProduction.tsx b/client/src/components/RouterProduction.tsx index 421731e..10b18a3 100644 --- a/client/src/components/RouterProduction.tsx +++ b/client/src/components/RouterProduction.tsx @@ -20,6 +20,7 @@ import DlcPage from "@/pages/DlcPage"; import BackupManager from "@/pages/BackupManager"; import Utilities from "@/pages/Utilities"; import Tasks from "@/pages/Tasks"; +import ResponsiveTasks from "@/components/ResponsiveTasks"; import SavTickets from "@/pages/SavTickets"; import Avoirs from "@/pages/Avoirs"; import WeatherSettings from "@/pages/WeatherSettings"; @@ -30,7 +31,7 @@ import Layout from "@/components/Layout"; function RouterProduction() { const { isAuthenticated, isLoading, user, environment, error } = useAuthUnified(); - + // Debug uniquement en développement if (import.meta.env.DEV) { console.log('🔍 RouterProduction Debug:', { @@ -43,7 +44,7 @@ function RouterProduction() { error: error?.message }); } - + // Debug minimal basé sur l'environnement if (environment === 'production' && error && import.meta.env.DEV) { console.error('🚨 Production Auth Error:', error); @@ -76,7 +77,7 @@ function RouterProduction() { ); } - + return ( @@ -102,19 +103,19 @@ function RouterProduction() { - + - + {/* Routes de compatibilité - redirection vers utilities */} - + {/* Redirection depuis /auth vers dashboard après authentification */} {() => { @@ -127,7 +128,7 @@ function RouterProduction() { return ; }} - + diff --git a/client/src/pages/mobile/TasksPage.tsx b/client/src/pages/mobile/TasksPage.tsx new file mode 100644 index 0000000..c5544f6 --- /dev/null +++ b/client/src/pages/mobile/TasksPage.tsx @@ -0,0 +1,472 @@ +import { useState, useEffect } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useAuthUnified } from "@/hooks/useAuthUnified"; +import { useStore } from "@/contexts/StoreContext"; +import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "@/components/ui/card"; +import { + ListTodo, + Plus, + Search, + CheckCircle, + Edit, + Trash2, + Calendar, + AlertTriangle, + Clock, + Circle, + X +} from "lucide-react"; +import { format, isPast, differenceInDays, isToday } from "date-fns"; +import { fr } from "date-fns/locale"; +import type { 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; +}; + +// Mobile Task Form Component +function MobileTaskForm({ task, onClose, selectedStoreId, user }: any) { + const { toast } = useToast(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + 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 assignedTo = formData.get('assignedTo') as string; + const dueDate = formData.get('dueDate') as string; + + if (!title?.trim()) { + toast({ title: "Erreur", description: "Le titre est requis", variant: "destructive" }); + setIsSubmitting(false); + return; + } + + try { + const taskData = { + title: title.trim(), + description: description?.trim() || "", + priority: priority || "medium", + status: task?.status || "pending", + assignedTo: assignedTo?.trim() || "Non assigné", + dueDate: dueDate || null, + }; + + const url = task ? `/api/tasks/${task.id}` : '/api/tasks'; + const method = task ? 'PUT' : 'POST'; + + if (!task) { + (taskData as any).groupId = selectedStoreId ? parseInt(selectedStoreId.toString()) : 1; + (taskData as any).createdBy = user?.username || 'admin'; + } + + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(taskData), + }); + + if (!response.ok) throw new Error(`Erreur ${response.status}`); + + toast({ + title: "Succès", + description: task ? "Tâche modifiée" : "Tâche créée", + }); + window.location.reload(); + + } catch (error) { + toast({ title: "Erreur", description: "Erreur lors de l'opération", variant: "destructive" }); + setIsSubmitting(false); + } + }; + + return ( + + {/* Header */} + + {task ? "Modifier" : "Nouvelle tâche"} + + + + + + {/* Form */} + + + Titre * + + + + + Description + + + + + Priorité + + 🟢 Faible + 🟡 Moyenne + 🔴 Élevée + + + + + Date d'échéance + + + + + Assigné à + + + + {/* Submit Button - Fixed at bottom */} + + + {isSubmitting ? "..." : (task ? "Modifier" : "Créer la tâche")} + + + + + ); +} + +// Task Card Component +function TaskCard({ + task, + onComplete, + onEdit, + onDelete, + canEdit +}: { + task: TaskWithRelations; + onComplete: () => void; + onEdit: () => void; + onDelete: () => void; + canEdit: boolean; +}) { + const getPriorityConfig = (priority: string) => { + switch (priority) { + case 'high': return { color: 'bg-red-100 text-red-700 border-red-200', icon: AlertTriangle, label: 'Élevée' }; + case 'medium': return { color: 'bg-yellow-100 text-yellow-700 border-yellow-200', icon: Clock, label: 'Moyenne' }; + case 'low': return { color: 'bg-green-100 text-green-700 border-green-200', icon: Circle, label: 'Faible' }; + default: return { color: 'bg-gray-100 text-gray-700 border-gray-200', icon: Circle, label: 'Moyenne' }; + } + }; + + const getDueDateInfo = () => { + if (!task.dueDate || task.status === 'completed') return null; + const due = new Date(task.dueDate); + const daysDiff = differenceInDays(due, new Date()); + + if (isPast(due) && !isToday(due)) { + return { text: `En retard (${Math.abs(daysDiff)}j)`, color: 'text-red-600' }; + } else if (isToday(due)) { + return { text: "Aujourd'hui", color: 'text-orange-600' }; + } else if (daysDiff <= 3) { + return { text: `Dans ${daysDiff}j`, color: 'text-yellow-600' }; + } + return { text: format(due, 'dd/MM', { locale: fr }), color: 'text-gray-500' }; + }; + + const priority = getPriorityConfig(task.priority); + const PriorityIcon = priority.icon; + const dueDateInfo = getDueDateInfo(); + const isCompleted = task.status === 'completed'; + + return ( + + + {/* Header: Priority + Due Date */} + + + + {priority.label} + + {dueDateInfo && ( + + + {dueDateInfo.text} + + )} + + + {/* Title */} + + {task.title} + + + {/* Description */} + {task.description && ( + {task.description} + )} + + {/* Assigned To */} + {task.assignedTo && ( + 👤 {task.assignedTo} + )} + + {/* Actions */} + + {!isCompleted && ( + + + Terminer + + )} + {canEdit && ( + <> + + + + + + + > + )} + + + + ); +} + +// Main Mobile Tasks Page +export default function MobileTasksPage() { + const { user } = useAuthUnified(); + const { selectedStoreId } = useStore(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + const [searchTerm, setSearchTerm] = useState(""); + const [activeTab, setActiveTab] = useState<'all' | 'pending' | 'completed'>('pending'); + const [showForm, setShowForm] = useState(false); + const [editingTask, setEditingTask] = useState(null); + const [deletingTask, setDeletingTask] = useState(null); + + // Fetch tasks + const { data: tasks = [], isLoading } = useQuery({ + queryKey: ["/api/tasks", selectedStoreId], + queryFn: async () => { + const params = new URLSearchParams(); + if (selectedStoreId) params.append('storeId', selectedStoreId.toString()); + + const response = await fetch(`/api/tasks?${params.toString()}`, { credentials: 'include' }); + if (!response.ok) throw new Error('Erreur chargement'); + + const data = await response.json(); + return Array.isArray(data) ? data.filter((t: any) => t && t.id) : []; + }, + enabled: !!user, + }); + + // Filter tasks + const filteredTasks = tasks + .filter((task: TaskWithRelations) => { + // Tab filter + if (activeTab === 'pending' && task.status === 'completed') return false; + if (activeTab === 'completed' && task.status !== 'completed') return false; + + // Search filter + if (searchTerm && !task.title.toLowerCase().includes(searchTerm.toLowerCase())) { + return false; + } + return true; + }) + .sort((a: TaskWithRelations, b: TaskWithRelations) => { + // Pending first, then by priority + if (a.status !== b.status) return a.status === 'pending' ? -1 : 1; + const priorityOrder = { high: 3, medium: 2, low: 1 }; + return (priorityOrder[b.priority as keyof typeof priorityOrder] || 2) - + (priorityOrder[a.priority as keyof typeof priorityOrder] || 2); + }); + + // Handlers + const handleComplete = async (taskId: number) => { + try { + await fetch(`/api/tasks/${taskId}/complete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({}), + }); + queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); + toast({ title: "✅ Tâche terminée" }); + } catch (error) { + toast({ title: "Erreur", variant: "destructive" }); + } + }; + + const handleDelete = async () => { + if (!deletingTask) return; + try { + await fetch(`/api/tasks/${deletingTask.id}`, { method: 'DELETE', credentials: 'include' }); + queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); + toast({ title: "🗑️ Tâche supprimée" }); + setDeletingTask(null); + } catch (error) { + toast({ title: "Erreur", variant: "destructive" }); + } + }; + + const canEdit = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur'; + + // Loading + if (isLoading) { + return ( + + + + ); + } + + return ( + + {/* Header */} + + + + + + Tâches + + {filteredTasks.length} + + + {/* Search */} + + + setSearchTerm(e.target.value)} + className="pl-10 h-11" + /> + + + {/* Tabs */} + + {(['pending', 'completed', 'all'] as const).map((tab) => ( + setActiveTab(tab)} + className="flex-1 h-10" + > + {tab === 'pending' && 'En cours'} + {tab === 'completed' && 'Terminées'} + {tab === 'all' && 'Toutes'} + + ))} + + + + + {/* Task List */} + + {filteredTasks.length === 0 ? ( + + + Aucune tâche trouvée + + ) : ( + filteredTasks.map((task: TaskWithRelations) => ( + handleComplete(task.id)} + onEdit={() => { setEditingTask(task); setShowForm(true); }} + onDelete={() => setDeletingTask(task)} + canEdit={canEdit} + /> + )) + )} + + + {/* FAB Button */} + {canEdit && ( + { setEditingTask(null); setShowForm(true); }} + className="fixed bottom-20 right-4 w-14 h-14 rounded-full shadow-lg bg-blue-600 hover:bg-blue-700 z-40" + > + + + )} + + {/* Form Modal */} + {showForm && ( + { setShowForm(false); setEditingTask(null); }} + selectedStoreId={selectedStoreId} + user={user} + /> + )} + + {/* Delete Confirmation */} + {deletingTask && ( + + + Supprimer cette tâche ? + {deletingTask.title} + + setDeletingTask(null)} className="flex-1 h-12"> + Annuler + + + Supprimer + + + + + )} + + ); +}
{task.description}
👤 {task.assignedTo}
Aucune tâche trouvée
{deletingTask.title}