mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-17 20:27:10 +02:00
chore: remove committed session cookies, debug artifacts and dead code
Repo hygiene pass: - cookie.txt / cookies.txt held real session cookies and must never be committed; .gitignore now blocks them along with .env files - half a megabyte of debug screenshots, one-shot production hotfix scripts (all superseded by the automatic startup migrations), scratch files and the unused attached_assets folder (with its dangling @assets vite alias) - dead code: server/storage-old.ts (unreferenced, 167 of the project's 430 TypeScript errors) and five client pages no route ever imported (BLReconciliationNative, Avoirs_backup, TasksSimplified, TasksListSimple, TasksProductionSimple) TypeScript error count drops from 430 to 261 with no behavior change; the client build is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe
This commit is contained in:
47 files changed
+10
-6523
No files matched your search
@@ -1 +0,0 @@
|
||||
// Backup temporaire pour restaurer si nécessaire
|
||||
@@ -1,778 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { safeFormat } from "@/lib/dateUtils";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { useStore } from "@/contexts/StoreContext";
|
||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||
import { usePermissions } from "@shared/permissions";
|
||||
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import type { Supplier } from "@shared/schema";
|
||||
|
||||
export default function BLReconciliation() {
|
||||
const { user } = useAuthUnified();
|
||||
const { selectedStoreId } = useStore();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const permissions = usePermissions(user?.role);
|
||||
|
||||
// Vérification des permissions d'accès au module
|
||||
if (!permissions.canView('reconciliation')) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="bg-orange-50 border-l-4 border-orange-400 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<FileText className="h-5 w-5 text-orange-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-orange-700">
|
||||
<strong>Accès restreint</strong><br />
|
||||
Seuls les directeurs et administrateurs peuvent accéder au module de rapprochement BL/Factures.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [activeTab, setActiveTab] = useState("manual");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// Récupérer les fournisseurs pour la logique automatique
|
||||
const { data: suppliers = [], isLoading: suppliersLoading } = useQuery<Supplier[]>({
|
||||
queryKey: ['/api/suppliers'],
|
||||
});
|
||||
|
||||
// Récupérer les livraisons validées avec BL
|
||||
const { data: deliveriesWithBL = [], isLoading } = useQuery({
|
||||
queryKey: ['/api/deliveries/bl', selectedStoreId],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({});
|
||||
if (selectedStoreId && user?.role === 'admin') {
|
||||
params.append('storeId', selectedStoreId.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/deliveries?${params.toString()}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch deliveries');
|
||||
}
|
||||
|
||||
const deliveries = await response.json();
|
||||
const filtered = Array.isArray(deliveries) ? deliveries.filter((d: any) => d.status === 'delivered') : [];
|
||||
|
||||
return filtered.sort((a: any, b: any) => new Date(b.deliveredDate || b.updatedAt).getTime() - new Date(a.deliveredDate || a.updatedAt).getTime());
|
||||
},
|
||||
});
|
||||
|
||||
// Séparer les livraisons selon le mode de rapprochement du fournisseur
|
||||
const manualReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => {
|
||||
const supplier = suppliers.find((s: any) => s.id === delivery.supplierId);
|
||||
return !supplier?.automaticReconciliation;
|
||||
});
|
||||
|
||||
const automaticReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => {
|
||||
const supplier = suppliers.find((s: any) => s.id === delivery.supplierId);
|
||||
return supplier?.automaticReconciliation;
|
||||
});
|
||||
|
||||
// Mutations pour les actions
|
||||
const validateManualMutation = useMutation({
|
||||
mutationFn: async (deliveryId: number) => {
|
||||
return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
|
||||
reconciled: true,
|
||||
validatedAt: new Date().toISOString()
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Rapprochement validé avec succès",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de valider le rapprochement",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const devalidateMutation = useMutation({
|
||||
mutationFn: async (deliveryId: number) => {
|
||||
return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
|
||||
reconciled: false,
|
||||
validatedAt: null
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Rapprochement dévalidé avec succès",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de dévalider le rapprochement",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (deliveryId: number) => {
|
||||
return await apiRequest(`/api/deliveries/${deliveryId}`, "DELETE");
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Ligne supprimée avec succès",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de supprimer la ligne",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Fonctions de gestion des actions
|
||||
const handleValidateManual = (deliveryId: number) => {
|
||||
if (!permissions.canValidate('reconciliation')) {
|
||||
toast({
|
||||
title: "Accès refusé",
|
||||
description: "Vous n'avez pas les permissions pour valider les rapprochements",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
validateManualMutation.mutate(deliveryId);
|
||||
};
|
||||
|
||||
const handleDevalidate = (deliveryId: number) => {
|
||||
if (!permissions.canEdit('reconciliation')) {
|
||||
toast({
|
||||
title: "Accès refusé",
|
||||
description: "Seuls les administrateurs peuvent dévalider les rapprochements",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (confirm("Êtes-vous sûr de vouloir dévalider ce rapprochement ?")) {
|
||||
devalidateMutation.mutate(deliveryId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (deliveryId: number) => {
|
||||
if (!permissions.canDelete('reconciliation')) {
|
||||
toast({
|
||||
title: "Accès refusé",
|
||||
description: "Seuls les administrateurs peuvent supprimer les lignes",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette ligne ? Cette action est irréversible.")) {
|
||||
deleteMutation.mutate(deliveryId);
|
||||
}
|
||||
};
|
||||
|
||||
// Filtrage des livraisons par recherche
|
||||
const filterDeliveries = (deliveries: any[]) => {
|
||||
return deliveries.filter((delivery: any) => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return (
|
||||
delivery.supplier?.name?.toLowerCase().includes(searchLower) ||
|
||||
delivery.blNumber?.toLowerCase().includes(searchLower) ||
|
||||
delivery.invoiceReference?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries);
|
||||
const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries);
|
||||
|
||||
// Pagination
|
||||
const getCurrentPageData = (data: any[]) => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return data.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
const getTotalPages = (data: any[]) => {
|
||||
return Math.ceil(data.length / itemsPerPage);
|
||||
};
|
||||
|
||||
const currentManualData = getCurrentPageData(filteredManualDeliveries);
|
||||
const currentAutomaticData = getCurrentPageData(filteredAutomaticDeliveries);
|
||||
|
||||
const totalManualPages = getTotalPages(filteredManualDeliveries);
|
||||
const totalAutomaticPages = getTotalPages(filteredAutomaticDeliveries);
|
||||
|
||||
// Reset page when changing tabs or searching
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [activeTab, searchTerm]);
|
||||
|
||||
// Vérifier si une ligne est dévalidée (pour la coloration)
|
||||
const isDevalidated = (delivery: any) => {
|
||||
return delivery.reconciled === false && delivery.validatedAt === null;
|
||||
};
|
||||
|
||||
// Composant de pagination simplifiée
|
||||
const NativePagination = ({ currentPage, totalPages, onPageChange, totalItems, itemsPerPage }: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
totalItems: number;
|
||||
itemsPerPage: number;
|
||||
}) => {
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200">
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<span>
|
||||
Affichage de {startItem} à {endItem} sur {totalItems} éléments
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 text-sm border border-gray-300 rounded bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
<span>Précédent</span>
|
||||
</button>
|
||||
|
||||
<span className="px-3 py-1 text-sm bg-gray-100 rounded">
|
||||
Page {currentPage} sur {totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1 text-sm border border-gray-300 rounded bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
||||
>
|
||||
<span>Suivant</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Badge native
|
||||
const NativeBadge = ({ children, variant = "default", className = "" }: { children: React.ReactNode, variant?: string, className?: string }) => {
|
||||
const baseClasses = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium";
|
||||
const variantClasses = {
|
||||
default: "bg-blue-100 text-blue-800",
|
||||
secondary: "bg-gray-100 text-gray-800",
|
||||
destructive: "bg-red-100 text-red-800",
|
||||
outline: "border border-gray-300 text-gray-700 bg-white"
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`${baseClasses} ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Button native
|
||||
const NativeButton = ({ children, onClick, disabled = false, variant = "default", size = "default", className = "", title = "" }: {
|
||||
children: React.ReactNode,
|
||||
onClick?: () => void,
|
||||
disabled?: boolean,
|
||||
variant?: string,
|
||||
size?: string,
|
||||
className?: string,
|
||||
title?: string
|
||||
}) => {
|
||||
const baseClasses = "inline-flex items-center justify-center font-medium rounded transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
|
||||
const variantClasses = {
|
||||
default: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
|
||||
ghost: "text-gray-600 hover:text-gray-900 hover:bg-gray-100",
|
||||
outline: "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
||||
};
|
||||
const sizeClasses = {
|
||||
default: "px-4 py-2 text-sm",
|
||||
sm: "px-2 py-1 text-xs"
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
className={`${baseClasses} ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${sizeClasses[size as keyof typeof sizeClasses] || sizeClasses.default} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading || suppliersLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header avec onglets */}
|
||||
<div className="bg-white border-b border-gray-200 p-6 shadow-sm -m-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 flex items-center">
|
||||
<FileText className="w-6 h-6 mr-3 text-blue-600" />
|
||||
Rapprochement BL/Factures
|
||||
</h2>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Gestion des rapprochements manuels et automatiques
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<NativeBadge variant="outline" className="text-sm border border-gray-300">
|
||||
{manualReconciliationDeliveries.length} rapprochements manuels
|
||||
</NativeBadge>
|
||||
<NativeBadge variant="outline" className="text-sm border border-gray-300 bg-blue-50">
|
||||
{automaticReconciliationDeliveries.length} rapprochements automatiques
|
||||
</NativeBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Onglets natifs */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab("manual")}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 ${
|
||||
activeTab === "manual"
|
||||
? "border-blue-500 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
<span>Rapprochement Manuel</span>
|
||||
<NativeBadge variant="secondary" className="ml-2">
|
||||
{manualReconciliationDeliveries.length}
|
||||
</NativeBadge>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("automatic")}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 ${
|
||||
activeTab === "automatic"
|
||||
? "border-blue-500 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span>Rapprochement Automatique</span>
|
||||
<NativeBadge variant="secondary" className="ml-2">
|
||||
{automaticReconciliationDeliveries.length}
|
||||
</NativeBadge>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtre de recherche */}
|
||||
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
||||
<div className="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher par fournisseur, BL ou facture..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu des onglets */}
|
||||
{activeTab === "manual" && (
|
||||
<div className="space-y-6">
|
||||
{filteredManualDeliveries.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Aucun rapprochement manuel trouvé
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Les livraisons de fournisseurs en mode manuel apparaîtront ici.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
||||
{/* Pagination du haut */}
|
||||
<NativePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalManualPages}
|
||||
onPageChange={setCurrentPage}
|
||||
totalItems={filteredManualDeliveries.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
|
||||
<div className="table-container">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fournisseur</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">N° BL</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Livr.</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Montant BL</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ref. Facture</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Montant Fact.</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{currentManualData.map((delivery: any) => (
|
||||
<tr
|
||||
key={delivery.id}
|
||||
className={`hover:bg-gray-50 ${
|
||||
delivery.reconciled ? 'bg-green-50' :
|
||||
isDevalidated(delivery) ? 'bg-red-50' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="font-medium text-gray-900 truncate max-w-32">
|
||||
{delivery.supplier?.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.blNumber || (
|
||||
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="font-medium text-gray-900">
|
||||
{delivery.blAmount ?
|
||||
`${parseFloat(delivery.blAmount).toFixed(2)}€` :
|
||||
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.invoiceReference || (
|
||||
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.invoiceAmount ?
|
||||
`${parseFloat(delivery.invoiceAmount).toFixed(2)}€` :
|
||||
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
{delivery.reconciled ? (
|
||||
<NativeBadge variant="default" className="bg-green-600 text-white">
|
||||
Validé
|
||||
</NativeBadge>
|
||||
) : isDevalidated(delivery) ? (
|
||||
<NativeBadge variant="destructive">
|
||||
Dévalidé
|
||||
</NativeBadge>
|
||||
) : (
|
||||
<NativeBadge variant="outline">
|
||||
En attente
|
||||
</NativeBadge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{permissions.canEdit('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-600 hover:text-blue-600 p-1"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
|
||||
{!delivery.reconciled && permissions.canValidate('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleValidateManual(delivery.id)}
|
||||
disabled={validateManualMutation.isPending}
|
||||
className="text-green-600 hover:text-green-700 p-1"
|
||||
title="Valider"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
|
||||
{delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDevalidate(delivery.id)}
|
||||
disabled={devalidateMutation.isPending}
|
||||
className="text-orange-600 hover:text-orange-700 p-1"
|
||||
title="Dévalider"
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
|
||||
{permissions.canDelete('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(delivery.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="text-red-600 hover:text-red-700 p-1"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination du bas */}
|
||||
<NativePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalManualPages}
|
||||
onPageChange={setCurrentPage}
|
||||
totalItems={filteredManualDeliveries.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "automatic" && (
|
||||
<div className="space-y-6">
|
||||
{/* Message d'information */}
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertTriangle className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-blue-900">Mode rapprochement automatique</h4>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
Les livraisons de fournisseurs en mode automatique sont validées automatiquement lorsqu'elles ont le statut "delivered" et un numéro de BL.
|
||||
{permissions.canEdit('reconciliation') ? (
|
||||
" Vous pouvez dévalider ces rapprochements si nécessaire."
|
||||
) : (
|
||||
" Seuls les directeurs et administrateurs peuvent modifier ces rapprochements."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredAutomaticDeliveries.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Settings className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Aucun rapprochement automatique trouvé
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Les livraisons de fournisseurs en mode automatique apparaîtront ici.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
||||
{/* Pagination du haut */}
|
||||
<NativePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalAutomaticPages}
|
||||
onPageChange={setCurrentPage}
|
||||
totalItems={filteredAutomaticDeliveries.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
|
||||
<div className="table-container">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Fournisseur
|
||||
<NativeBadge variant="secondary" className="ml-2 text-xs">AUTO</NativeBadge>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">N° BL</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Livr.</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Valid.</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Montant BL</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ref. Facture</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{currentAutomaticData.map((delivery: any) => (
|
||||
<tr
|
||||
key={delivery.id}
|
||||
className={`hover:bg-gray-50 ${
|
||||
isDevalidated(delivery) ? 'bg-red-50' : 'bg-green-50'
|
||||
}`}
|
||||
>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="font-medium text-gray-900 truncate max-w-32">
|
||||
{delivery.supplier?.name}
|
||||
</div>
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full" title="Mode automatique" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900 font-medium">
|
||||
{delivery.blNumber || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.reconciled && delivery.validatedAt ?
|
||||
safeFormat(delivery.validatedAt, 'dd/MM/yy') :
|
||||
'-'
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="font-medium text-gray-900">
|
||||
{delivery.blAmount ?
|
||||
`${parseFloat(delivery.blAmount).toFixed(2)}€` :
|
||||
'-'
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.invoiceReference || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
{isDevalidated(delivery) ? (
|
||||
<NativeBadge variant="destructive">
|
||||
Dévalidé
|
||||
</NativeBadge>
|
||||
) : (
|
||||
<NativeBadge variant="default" className="bg-green-600 text-white">
|
||||
Auto-validé
|
||||
</NativeBadge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
|
||||
<>
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-600 hover:text-blue-600 p-1"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleValidateManual(delivery.id)}
|
||||
disabled={validateManualMutation.isPending}
|
||||
className="text-green-600 hover:text-green-700 p-1"
|
||||
title="Valider"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDevalidate(delivery.id)}
|
||||
disabled={devalidateMutation.isPending}
|
||||
className="text-orange-600 hover:text-orange-700 p-1"
|
||||
title="Dévalider"
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
|
||||
{permissions.canDelete('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(delivery.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="text-red-600 hover:text-red-700 p-1"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination du bas */}
|
||||
<NativePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalAutomaticPages}
|
||||
onPageChange={setCurrentPage}
|
||||
totalItems={filteredAutomaticDeliveries.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,568 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
// Interface pour les tâches
|
||||
interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
status: 'pending' | 'completed';
|
||||
assignedTo: string;
|
||||
startDate?: string | null;
|
||||
dueDate?: string | null;
|
||||
createdAt?: string;
|
||||
groupId?: number;
|
||||
createdBy?: string;
|
||||
}
|
||||
|
||||
export default function TasksListSimple() {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
|
||||
// Charger les tâches
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tasks');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTasks(data);
|
||||
} else {
|
||||
console.error('Erreur lors du chargement des tâches:', response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des tâches:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, []);
|
||||
|
||||
// Créer une tâche
|
||||
const createTask = async (formData: FormData) => {
|
||||
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()) {
|
||||
alert("Le titre est requis");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!assignedTo?.trim()) {
|
||||
alert("L'assignation est requise");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const taskData = {
|
||||
title: title.trim(),
|
||||
description: description?.trim() || "",
|
||||
priority: priority || "medium",
|
||||
status: status || "pending",
|
||||
assignedTo: assignedTo.trim(),
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null,
|
||||
groupId: 1,
|
||||
createdBy: 'admin',
|
||||
};
|
||||
|
||||
console.log('📤 Creating task:', taskData);
|
||||
|
||||
const response = await fetch('/api/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(taskData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('❌ Create Error:', { status: response.status, error: errorText });
|
||||
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
alert("Tâche créée avec succès");
|
||||
loadTasks();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création:', error);
|
||||
alert("Erreur lors de la création");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Modifier une tâche
|
||||
const updateTask = async (id: number, formData: FormData) => {
|
||||
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()) {
|
||||
alert("Le titre est requis");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!assignedTo?.trim()) {
|
||||
alert("L'assignation est requise");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Préparer les données exactement comme le serveur les attend
|
||||
const taskData: Partial<Task> = {
|
||||
title: title.trim(),
|
||||
description: description?.trim() || "",
|
||||
priority: (priority as Task['priority']) || "medium",
|
||||
status: (status as Task['status']) || "pending",
|
||||
assignedTo: assignedTo.trim(),
|
||||
};
|
||||
|
||||
// Gestion spéciale des dates pour éviter les erreurs de format
|
||||
if (startDate && startDate.trim() !== '') {
|
||||
taskData.startDate = startDate;
|
||||
} else {
|
||||
taskData.startDate = null;
|
||||
}
|
||||
|
||||
if (dueDate && dueDate.trim() !== '') {
|
||||
taskData.dueDate = dueDate;
|
||||
} else {
|
||||
taskData.dueDate = null;
|
||||
}
|
||||
|
||||
console.log('📤 Updating task:', { id, taskData });
|
||||
|
||||
const response = await fetch(`/api/tasks/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(taskData),
|
||||
});
|
||||
|
||||
console.log('📥 Update Response:', { status: response.status, ok: response.ok });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('❌ Update Error:', { status: response.status, error: errorText });
|
||||
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
alert("Tâche modifiée avec succès");
|
||||
setEditingId(null);
|
||||
loadTasks();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la modification:', error);
|
||||
alert("Erreur lors de la modification");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Filtrer les tâches
|
||||
const filteredTasks = tasks.filter(task => {
|
||||
const matchesSearch = !searchTerm || task.title?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
// Formulaire inline
|
||||
const TaskForm = ({ task, onSubmit, onCancel }: {
|
||||
task?: Task;
|
||||
onSubmit: (formData: FormData) => Promise<boolean>;
|
||||
onCancel: () => void;
|
||||
}) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const success = await onSubmit(formData);
|
||||
if (success && !task) {
|
||||
(e.target as HTMLFormElement).reset();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tr style={{ backgroundColor: '#f9fafb' }}>
|
||||
<td colSpan={6} style={{ padding: '16px' }}>
|
||||
<form onSubmit={handleSubmit} style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', alignItems: 'end' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Titre *
|
||||
</label>
|
||||
<input
|
||||
name="title"
|
||||
type="text"
|
||||
defaultValue={task?.title || ""}
|
||||
placeholder="Titre de la tâche"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
name="description"
|
||||
type="text"
|
||||
defaultValue={task?.description || ""}
|
||||
placeholder="Description (optionnel)"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Priorité
|
||||
</label>
|
||||
<select
|
||||
name="priority"
|
||||
defaultValue={task?.priority || "medium"}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
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: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Statut
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
defaultValue={task?.status || "pending"}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
<option value="pending">⏳ En cours</option>
|
||||
<option value="completed">✅ Terminée</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
📅 Date de début
|
||||
</label>
|
||||
<input
|
||||
name="startDate"
|
||||
type="date"
|
||||
defaultValue={task?.startDate ? new Date(task.startDate).toISOString().split('T')[0] : ""}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
⏰ Date d'échéance
|
||||
</label>
|
||||
<input
|
||||
name="dueDate"
|
||||
type="date"
|
||||
defaultValue={task?.dueDate ? new Date(task.dueDate).toISOString().split('T')[0] : ""}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Assigné à *
|
||||
</label>
|
||||
<input
|
||||
name="assignedTo"
|
||||
type="text"
|
||||
defaultValue={task?.assignedTo || "admin"}
|
||||
placeholder="Nom de la personne"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{task ? "Modifier" : "Créer"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '24px', textAlign: 'center' }}>
|
||||
<p>Chargement des tâches...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', maxWidth: '1400px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: '600', margin: '0', color: '#1f2937' }}>
|
||||
📋 Gestion des Tâches
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => setShowCreateForm(!showCreateForm)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: showCreateForm ? '#dc2626' : '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{showCreateForm ? '❌ Annuler' : '➕ Nouvelle Tâche'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Rechercher..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
minWidth: '150px'
|
||||
}}
|
||||
>
|
||||
<option value="all">Tous les statuts</option>
|
||||
<option value="pending">En cours</option>
|
||||
<option value="completed">Terminées</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tableau des tâches */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead style={{ backgroundColor: '#f9fafb' }}>
|
||||
<tr>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Titre</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Priorité</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Statut</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Assigné à</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Dates</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{showCreateForm && (
|
||||
<TaskForm
|
||||
onSubmit={createTask}
|
||||
onCancel={() => setShowCreateForm(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filteredTasks.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ padding: '48px', textAlign: 'center', color: '#6b7280' }}>
|
||||
Aucune tâche trouvée
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredTasks.map((task) => (
|
||||
editingId === task.id ? (
|
||||
<TaskForm
|
||||
key={task.id}
|
||||
task={task}
|
||||
onSubmit={(formData) => updateTask(task.id, formData)}
|
||||
onCancel={() => setEditingId(null)}
|
||||
/>
|
||||
) : (
|
||||
<tr key={task.id} style={{ borderBottom: '1px solid #e5e7eb' }}>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div>
|
||||
<div style={{
|
||||
fontWeight: '500',
|
||||
color: task.status === 'completed' ? '#6b7280' : '#1f2937',
|
||||
textDecoration: task.status === 'completed' ? 'line-through' : 'none'
|
||||
}}>
|
||||
{task.title}
|
||||
</div>
|
||||
{task.description && (
|
||||
<div style={{ fontSize: '12px', color: '#6b7280', marginTop: '2px' }}>
|
||||
{task.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<span style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '500',
|
||||
backgroundColor: task.priority === 'high' ? '#fee2e2' : task.priority === 'medium' ? '#fef3c7' : '#d1fae5',
|
||||
color: task.priority === 'high' ? '#dc2626' : task.priority === 'medium' ? '#d97706' : '#059669'
|
||||
}}>
|
||||
{task.priority === 'high' ? '🔴 Élevée' : task.priority === 'medium' ? '🟡 Moyenne' : '🟢 Faible'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<span style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '500',
|
||||
backgroundColor: task.status === 'completed' ? '#d1fae5' : '#fef3c7',
|
||||
color: task.status === 'completed' ? '#059669' : '#d97706'
|
||||
}}>
|
||||
{task.status === 'completed' ? '✅ Terminée' : '⏳ En cours'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#6b7280' }}>
|
||||
{task.assignedTo}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: '12px', color: '#6b7280' }}>
|
||||
{task.startDate && (
|
||||
<div>📅 {new Date(task.startDate).toLocaleDateString('fr-FR')}</div>
|
||||
)}
|
||||
{task.dueDate && (
|
||||
<div>⏰ {new Date(task.dueDate).toLocaleDateString('fr-FR')}</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<button
|
||||
onClick={() => setEditingId(task.id)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
✏️ Modifier
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,546 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
// Version 100% autonome sans aucune dépendance externe
|
||||
export default function TasksProductionSimple() {
|
||||
const [tasks, setTasks] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<any>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
// Charger les tâches
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tasks');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTasks(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des tâches:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, []);
|
||||
|
||||
// Filtrer les tâches
|
||||
const filteredTasks = tasks.filter(task => {
|
||||
const matchesSearch = task.title?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
// Composant formulaire inline
|
||||
const TaskForm = ({ task, onClose }: any) => {
|
||||
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()) {
|
||||
alert("Le titre est requis");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assignedTo?.trim()) {
|
||||
alert("L'assignation est requise");
|
||||
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(),
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null,
|
||||
};
|
||||
|
||||
const url = task ? `/api/tasks/${task.id}` : '/api/tasks';
|
||||
const method = task ? 'PUT' : 'POST';
|
||||
|
||||
if (!task) {
|
||||
(taskData as any).groupId = 1;
|
||||
(taskData as any).createdBy = 'admin';
|
||||
}
|
||||
|
||||
console.log('📤 Sending request:', { url, method, taskData });
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
alert(task ? "Tâche modifiée avec succès" : "Tâche créée avec succès");
|
||||
onClose();
|
||||
loadTasks(); // Recharger les tâches
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
alert("Erreur lors de l'opération");
|
||||
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>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
float: 'right',
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</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: '6px',
|
||||
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: '6px',
|
||||
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: '6px',
|
||||
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: '6px',
|
||||
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: '6px',
|
||||
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: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
Assigné à *
|
||||
</label>
|
||||
<input
|
||||
name="assignedTo"
|
||||
type="text"
|
||||
defaultValue={task?.assignedTo || "admin"}
|
||||
placeholder="Nom de la personne assignée"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
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: '6px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{task ? "Modifier" : "Créer"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '24px', textAlign: 'center' }}>
|
||||
<p>Chargement des tâches...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
marginBottom: '24px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: '600', margin: '0', color: '#1f2937' }}>
|
||||
📋 Gestion des Tâches
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
➕ Nouvelle Tâche
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Rechercher..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
minWidth: '150px'
|
||||
}}
|
||||
>
|
||||
<option value="all">Tous les statuts</option>
|
||||
<option value="pending">En cours</option>
|
||||
<option value="completed">Terminées</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Liste des tâches */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div style={{ padding: '48px', textAlign: 'center', color: '#6b7280' }}>
|
||||
<p>Aucune tâche trouvée</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredTasks.map((task, index) => (
|
||||
<div
|
||||
key={task.id}
|
||||
style={{
|
||||
padding: '16px 24px',
|
||||
borderBottom: index < filteredTasks.length - 1 ? '1px solid #e5e7eb' : 'none',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{
|
||||
margin: '0 0 8px 0',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
color: task.status === 'completed' ? '#6b7280' : '#1f2937',
|
||||
textDecoration: task.status === 'completed' ? 'line-through' : 'none'
|
||||
}}>
|
||||
{task.title}
|
||||
</h3>
|
||||
<div style={{ display: 'flex', gap: '16px', fontSize: '14px', color: '#6b7280' }}>
|
||||
<span>
|
||||
Priorité: {task.priority === 'high' ? '🔴 Élevée' : task.priority === 'medium' ? '🟡 Moyenne' : '🟢 Faible'}
|
||||
</span>
|
||||
<span>
|
||||
Statut: {task.status === 'completed' ? '✅ Terminée' : '⏳ En cours'}
|
||||
</span>
|
||||
<span>Assigné à: {task.assignedTo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTask(task);
|
||||
setShowEditModal(true);
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
✏️ Modifier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal de création */}
|
||||
{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: '8px',
|
||||
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'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
margin: '0'
|
||||
}}>
|
||||
Créer une nouvelle tâche
|
||||
</h2>
|
||||
</div>
|
||||
<TaskForm onClose={() => setShowCreateModal(false)} />
|
||||
</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: '8px',
|
||||
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'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
margin: '0'
|
||||
}}>
|
||||
Modifier la tâche
|
||||
</h2>
|
||||
</div>
|
||||
<TaskForm
|
||||
task={selectedTask}
|
||||
onClose={() => {
|
||||
setShowEditModal(false);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,504 +0,0 @@
|
||||
import { useState } 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 { 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.toString());
|
||||
}
|
||||
return fetch(`/api/tasks?${params.toString()}`).then(res => res.json());
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Récupération des magasins
|
||||
const { data: stores = [] } = useQuery<any[]>({
|
||||
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 {
|
||||
const finalData = {
|
||||
...taskData,
|
||||
groupId: selectedStoreId ? parseInt(selectedStoreId) : stores[0]?.id
|
||||
};
|
||||
|
||||
console.log('🔍 TasksSimplified - Creating task with data:', {
|
||||
selectedStoreId,
|
||||
storesLength: stores.length,
|
||||
finalGroupId: finalData.groupId,
|
||||
taskData: finalData
|
||||
});
|
||||
|
||||
await apiRequest('/api/tasks', 'POST', finalData);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
setShowCreateModal(false);
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche créée avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ TasksSimplified - Error creating task:', 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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskData = {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
assignedTo: assignedTo?.trim() || "Non assigné",
|
||||
priority,
|
||||
startDate: null, // Toujours inclure startDate même si null
|
||||
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
|
||||
};
|
||||
|
||||
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é à <span className="text-xs text-gray-500">(optionnel)</span>
|
||||
</label>
|
||||
<Input
|
||||
id="assignedTo"
|
||||
value={assignedTo}
|
||||
onChange={(e) => setAssignedTo(e.target.value)}
|
||||
placeholder="Nom de la personne responsable"
|
||||
/>
|
||||
</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