Merge pull request #4 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-12 12:25:19 +02:00
commit 9d17748137
4 files changed
+954 -203

No files matched your search

@@ -0,0 +1,200 @@
GET /api/deliveries 500 in 24ms
GET /api/orders 500 in 27ms
🔍 getUserWithGroups called for user: admin_local
🔍 getUserWithGroups called for user: admin_local
🔍 Querying user_groups for user: admin_local
🔍 Found 0 group assignments for user admin_local
🔍 Raw query result: []
✅ Processed 0 groups for user admin_local
🔍 getUserWithGroups called for user: admin_local
🔍 Querying user_groups for user: admin_local
🔍 Found 0 group assignments for user admin_local
🔍 Raw query result: []
✅ Processed 0 groups for user admin_local
🔍 getUserWithGroups called for user: admin_local
🔍 Querying user_groups for user: admin_local
🔍 Found 0 group assignments for user admin_local
🔍 Raw query result: []
✅ Processed 0 groups for user admin_local
Orders API called with: {
startDate: '2025-08-01',
endDate: '2025-08-31',
storeId: undefined,
userRole: 'admin'
}
Admin filtering with groupIds: undefined
Fetching orders by date range: 2025-08-01 to 2025-08-31
Error fetching orders: error: column orders_supplier.email does not exist
at /app/node_modules/pg-pool/index.js:45:11
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async file:///app/node_modules/drizzle-orm/node-postgres/session.js:83:22
at async DatabaseStorage.getOrdersByDateRange (file:///app/dist/index.js:1127:16)
at async file:///app/dist/index.js:3633:21 {
length: 120,
severity: 'ERROR',
code: '42703',
detail: undefined,
hint: undefined,
position: '555',
internalPosition: undefined,
internalQuery: undefined,
where: undefined,
schema: undefined,
table: undefined,
column: undefined,
dataType: undefined,
constraint: undefined,
file: 'parse_relation.c',
line: '3665',
routine: 'errorMissingColumn'
}
GET /api/orders 500 in 13ms
🔍 Querying user_groups for user: admin_local
🔍 Found 0 group assignments for user admin_local
🔍 Raw query result: []
✅ Processed 0 groups for user admin_local
🔍 getUserWithGroups called for user: admin_local
🔍 Querying user_groups for user: admin_local
🔍 Found 0 group assignments for user admin_local
🔍 Raw query result: []
✅ Processed 0 groups for user admin_local
🔍 getUserWithGroups called for user: admin_local
🔍 Querying user_groups for user: admin_local
🔍 Found 0 group assignments for user admin_local
🔍 Raw query result: []
✅ Processed 0 groups for user admin_local
Deliveries API called with: {
startDate: '2025-08-01',
endDate: '2025-08-31',
storeId: undefined,
withBL: undefined,
userRole: 'admin'
}
Admin filtering deliveries with groupIds: undefined
Fetching deliveries by date range: 2025-08-01 to 2025-08-31
Error fetching deliveries: error: column deliveries_supplier.email does not exist
at /app/node_modules/pg-pool/index.js:45:11
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async file:///app/node_modules/drizzle-orm/node-postgres/session.js:83:22
at async DatabaseStorage.getDeliveriesByDateRange (file:///app/dist/index.js:1186:25)
at async file:///app/dist/index.js:3848:25 {
length: 124,
severity: 'ERROR',
code: '42703',
detail: undefined,
hint: undefined,
position: '856',
internalPosition: undefined,
internalQuery: undefined,
where: undefined,
schema: undefined,
table: undefined,
column: undefined,
dataType: undefined,
constraint: undefined,
file: 'parse_relation.c',
line: '3665',
routine: 'errorMissingColumn'
}
GET /api/deliveries 500 in 42ms
HEAD /api/health 200 in 1ms
+125 -199
View File
@@ -14,13 +14,11 @@ import {
Search, Search,
Building, Building,
Phone, Phone,
Mail,
Edit, Edit,
Trash2, Trash2,
Package, Package,
Truck, Truck,
CheckCircle, CheckCircle,
XCircle,
Clock Clock
} from "lucide-react"; } from "lucide-react";
import type { Supplier } from "@shared/schema"; import type { Supplier } from "@shared/schema";
@@ -38,8 +36,6 @@ export default function Suppliers() {
name: "", name: "",
contact: "", contact: "",
phone: "", phone: "",
email: "",
address: "",
hasDlc: false, hasDlc: false,
automaticReconciliation: false, automaticReconciliation: false,
}); });
@@ -70,15 +66,7 @@ export default function Suppliers() {
}); });
queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] }); queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
setShowCreateModal(false); setShowCreateModal(false);
setFormData({ resetForm();
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}, },
onError: (error) => { onError: (error) => {
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
@@ -126,43 +114,10 @@ export default function Suppliers() {
return { previousSuppliers }; return { previousSuppliers };
}, },
onSuccess: async (result) => { onError: (error, newData, context) => {
console.log('✅ Frontend: Update mutation successful, result:', result); // Rollback en cas d'erreur
console.log('🔄 Frontend: Starting cache invalidation...'); console.log('⚠️ Frontend: Rolling back optimistic update due to error');
// Forcer une actualisation complète du cache
await queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
await queryClient.refetchQueries({
queryKey: ['/api/suppliers'],
type: 'active'
});
// Attendre un petit délai pour s'assurer que les données sont fraîches
await new Promise(resolve => setTimeout(resolve, 100));
console.log('✅ Frontend: Cache invalidation complete');
toast({
title: "Succès",
description: "Fournisseur modifié avec succès",
});
setShowEditModal(false);
setSelectedSupplier(null);
setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
},
onError: (error, variables, context) => {
// Rollback optimiste en cas d'erreur
if (context?.previousSuppliers) { if (context?.previousSuppliers) {
console.log('❌ Frontend: Error occurred, rolling back optimistic update');
queryClient.setQueryData(['/api/suppliers'], context.previousSuppliers); queryClient.setQueryData(['/api/suppliers'], context.previousSuppliers);
} }
@@ -183,6 +138,19 @@ export default function Suppliers() {
variant: "destructive", variant: "destructive",
}); });
}, },
onSuccess: () => {
toast({
title: "Succès",
description: "Fournisseur modifié avec succès",
});
queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
setShowEditModal(false);
setSelectedSupplier(null);
resetForm();
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
},
}); });
const deleteMutation = useMutation({ const deleteMutation = useMutation({
@@ -216,44 +184,47 @@ export default function Suppliers() {
}, },
}); });
const filteredSuppliers = Array.isArray(suppliers) ? suppliers.filter(supplier => const resetForm = () => {
supplier.name.toLowerCase().includes(searchTerm.toLowerCase()) || setFormData({
supplier.contact?.toLowerCase().includes(searchTerm.toLowerCase()) || name: "",
supplier.phone?.toLowerCase().includes(searchTerm.toLowerCase()) contact: "",
) : []; phone: "",
hasDlc: false,
automaticReconciliation: false,
});
};
const getSupplierStats = (supplierId: number) => { const handleChange = (field: string, value: any) => {
const supplierOrders = Array.isArray(orders) ? orders.filter(order => order.supplierId === supplierId) : []; setFormData(prev => ({ ...prev, [field]: value }));
const supplierDeliveries = Array.isArray(deliveries) ? deliveries.filter(delivery => delivery.supplierId === supplierId) : [];
return {
orders: supplierOrders.length,
deliveries: supplierDeliveries.length,
delivered: supplierDeliveries.filter(d => d.status === 'delivered').length,
};
}; };
const handleCreate = () => { const handleCreate = () => {
setFormData({ if (user?.role !== 'admin') {
name: "", toast({
contact: "", title: "Accès refusé",
phone: "", description: "Seuls les administrateurs peuvent créer des fournisseurs",
email: "", variant: "destructive",
address: "", });
hasDlc: false, return;
automaticReconciliation: false }
}); resetForm();
setShowCreateModal(true); setShowCreateModal(true);
}; };
const handleEdit = (supplier: Supplier) => { const handleEdit = (supplier: Supplier) => {
if (user?.role !== 'admin') {
toast({
title: "Accès refusé",
description: "Seuls les administrateurs peuvent modifier des fournisseurs",
variant: "destructive",
});
return;
}
setSelectedSupplier(supplier); setSelectedSupplier(supplier);
setFormData({ setFormData({
name: supplier.name, name: supplier.name || "",
contact: supplier.contact || "", contact: supplier.contact || "",
phone: supplier.phone || "", phone: supplier.phone || "",
email: supplier.email || "",
address: supplier.address || "",
hasDlc: supplier.hasDlc || false, hasDlc: supplier.hasDlc || false,
automaticReconciliation: supplier.automaticReconciliation || false, automaticReconciliation: supplier.automaticReconciliation || false,
}); });
@@ -261,34 +232,22 @@ export default function Suppliers() {
}; };
const handleDelete = (supplier: Supplier) => { const handleDelete = (supplier: Supplier) => {
const stats = getSupplierStats(supplier.id); if (user?.role !== 'admin') {
if (stats.orders > 0 || stats.deliveries > 0) {
toast({ toast({
title: "Suppression impossible", title: "Accès refusé",
description: "Ce fournisseur a des commandes ou livraisons associées", description: "Seuls les administrateurs peuvent supprimer des fournisseurs",
variant: "destructive", variant: "destructive",
}); });
return; return;
} }
if (window.confirm(`Êtes-vous sûr de vouloir supprimer le fournisseur "${supplier.name}" ?`)) { if (confirm(`Êtes-vous sûr de vouloir supprimer le fournisseur "${supplier.name}" ?`)) {
deleteMutation.mutate(supplier.id); deleteMutation.mutate(supplier.id);
} }
}; };
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!formData.name.trim()) {
toast({
title: "Erreur",
description: "Le nom du fournisseur est requis",
variant: "destructive",
});
return;
}
if (selectedSupplier) { if (selectedSupplier) {
updateMutation.mutate(formData); updateMutation.mutate(formData);
} else { } else {
@@ -296,56 +255,64 @@ export default function Suppliers() {
} }
}; };
const handleChange = (field: string, value: string | boolean) => { const filteredSuppliers = suppliers.filter(supplier =>
setFormData(prev => ({ ...prev, [field]: value })); supplier.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const getSupplierStats = (supplierId: number) => {
const supplierOrders = orders.filter((order: any) => order.supplierId === supplierId);
const supplierDeliveries = deliveries.filter((delivery: any) => delivery.supplierId === supplierId);
const deliveredCount = supplierDeliveries.filter((delivery: any) => delivery.status === 'delivered').length;
return {
orders: supplierOrders.length,
deliveries: supplierDeliveries.length,
delivered: deliveredCount,
};
}; };
const canManage = user?.role === 'admin' || user?.role === 'manager'; if (isLoading) {
if (!canManage) {
return ( return (
<div className="flex-1 flex flex-col items-center justify-center"> <div className="p-6">
<div className="text-center"> <div className="animate-pulse space-y-4">
<Building className="w-16 h-16 text-gray-400 mx-auto mb-4" /> <div className="h-8 bg-gray-200 rounded w-1/4"></div>
<h2 className="text-xl font-semibold text-gray-900 mb-2"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
Accès restreint {[1, 2, 3].map(i => (
</h2> <div key={i} className="h-48 bg-gray-200 rounded"></div>
<p className="text-gray-600"> ))}
Vous n'avez pas les permissions nécessaires pour accéder à cette page. </div>
</p>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="flex-1 flex flex-col overflow-hidden"> <div className="p-6 space-y-6">
{/* Header */} <div className="flex items-center justify-between">
<div className="bg-white border-b border-gray-200 p-6 shadow-sm"> <div>
<div className="flex items-center justify-between"> <h2 className="text-2xl font-semibold text-gray-900 flex items-center">
<div> <Building className="w-6 h-6 mr-3 text-blue-600" />
<h2 className="text-2xl font-semibold text-gray-900 flex items-center"> Fournisseurs
<Building className="w-6 h-6 mr-3 text-blue-600" /> </h2>
Gestion des Fournisseurs <p className="text-gray-600 mt-1">
</h2> Gestion des fournisseurs et partenaires
<p className="text-gray-600 mt-1"> </p>
{filteredSuppliers.length} fournisseur{filteredSuppliers.length !== 1 ? 's' : ''} </div>
</p>
</div> {user?.role === 'admin' && (
<Button <Button
onClick={handleCreate} onClick={handleCreate}
className="bg-blue-600 hover:bg-blue-700 text-white shadow-md" className="bg-primary hover:bg-blue-700 text-white"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Nouveau Fournisseur Nouveau fournisseur
</Button> </Button>
</div> )}
</div> </div>
{/* Search */} <div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
<div className="bg-gray-50 border-b border-gray-200 p-4"> <div className="relative">
<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" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input <Input
placeholder="Rechercher un fournisseur..." placeholder="Rechercher un fournisseur..."
value={searchTerm} value={searchTerm}
@@ -355,24 +322,20 @@ export default function Suppliers() {
</div> </div>
</div> </div>
{/* Suppliers List */} <div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
<div className="flex-1 overflow-auto"> {filteredSuppliers.length === 0 ? (
{isLoading ? ( <div className="text-center py-12">
<div className="flex items-center justify-center h-64"> <Building className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div> <h3 className="text-lg font-medium text-gray-900 mb-2">
</div>
) : filteredSuppliers.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
<Building className="w-16 h-16 mb-4 text-gray-300" />
<h3 className="text-lg font-medium mb-2">
{searchTerm ? "Aucun fournisseur trouvé" : "Aucun fournisseur"} {searchTerm ? "Aucun fournisseur trouvé" : "Aucun fournisseur"}
</h3> </h3>
<p className="text-center max-w-md"> <p className="text-gray-600">
{searchTerm {searchTerm
? "Aucun fournisseur ne correspond à votre recherche." ? "Aucun fournisseur ne correspond à votre recherche"
: "Vous n'avez pas encore de fournisseurs. Créez votre premier fournisseur pour commencer."} : "Commencez par créer votre premier fournisseur"
}
</p> </p>
{!searchTerm && ( {!searchTerm && user?.role === 'admin' && (
<Button <Button
onClick={handleCreate} onClick={handleCreate}
className="mt-4 bg-primary hover:bg-blue-700 text-white" className="mt-4 bg-primary hover:bg-blue-700 text-white"
@@ -399,29 +362,31 @@ export default function Suppliers() {
<p className="text-sm text-gray-500">#{supplier.id}</p> <p className="text-sm text-gray-500">#{supplier.id}</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> {user?.role === 'admin' && (
<Button <div className="flex items-center space-x-2">
variant="ghost" <Button
size="sm" variant="ghost"
onClick={() => handleEdit(supplier)} size="sm"
> onClick={() => handleEdit(supplier)}
<Edit className="w-4 h-4" /> >
</Button> <Edit className="w-4 h-4" />
<Button </Button>
variant="ghost" <Button
size="sm" variant="ghost"
onClick={() => handleDelete(supplier)} size="sm"
className="text-red-600 hover:text-red-700" onClick={() => handleDelete(supplier)}
> className="text-red-600 hover:text-red-700"
<Trash2 className="w-4 h-4" /> >
</Button> <Trash2 className="w-4 h-4" />
</div> </Button>
</div>
)}
</div> </div>
<div className="space-y-2 mb-4"> <div className="space-y-2 mb-4">
{supplier.contact && ( {supplier.contact && (
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
<Mail className="w-4 h-4 mr-2" /> <Building className="w-4 h-4 mr-2" />
{supplier.contact} {supplier.contact}
</div> </div>
)} )}
@@ -453,8 +418,6 @@ export default function Suppliers() {
Aucune option activée Aucune option activée
</div> </div>
)} )}
{/* Statistics section follows */}
</div> </div>
<div className="border-t pt-4"> <div className="border-t pt-4">
@@ -495,15 +458,7 @@ export default function Suppliers() {
setShowCreateModal(false); setShowCreateModal(false);
setShowEditModal(false); setShowEditModal(false);
setSelectedSupplier(null); setSelectedSupplier(null);
setFormData({ resetForm();
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}}> }}>
<DialogContent className="sm:max-w-md" aria-describedby="supplier-modal-description"> <DialogContent className="sm:max-w-md" aria-describedby="supplier-modal-description">
<DialogHeader> <DialogHeader>
@@ -547,27 +502,6 @@ export default function Suppliers() {
/> />
</div> </div>
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="Adresse email"
/>
</div>
<div>
<Label htmlFor="address">Adresse</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => handleChange('address', e.target.value)}
placeholder="Adresse complète"
/>
</div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="hasDlc" id="hasDlc"
@@ -598,15 +532,7 @@ export default function Suppliers() {
setShowCreateModal(false); setShowCreateModal(false);
setShowEditModal(false); setShowEditModal(false);
setSelectedSupplier(null); setSelectedSupplier(null);
setFormData({ resetForm();
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}} }}
> >
Annuler Annuler
@@ -626,4 +552,4 @@ export default function Suppliers() {
</Dialog> </Dialog>
</div> </div>
); );
} }
+627
View File
@@ -0,0 +1,627 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useToast } from "@/hooks/use-toast";
import { apiRequest } from "@/lib/queryClient";
import { isUnauthorizedError } from "@/lib/authUtils";
import {
Plus,
Search,
Building,
Phone,
Mail,
Edit,
Trash2,
Package,
Truck,
CheckCircle,
XCircle,
Clock
} from "lucide-react";
import type { Supplier } from "@shared/schema";
export default function Suppliers() {
const { user } = useAuthUnified();
const { toast } = useToast();
const queryClient = useQueryClient();
const [searchTerm, setSearchTerm] = useState("");
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [selectedSupplier, setSelectedSupplier] = useState<Supplier | null>(null);
const [formData, setFormData] = useState({
name: "",
contact: "",
phone: "",
hasDlc: false,
automaticReconciliation: false,
});
const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({
queryKey: ['/api/suppliers'],
});
const { data: orders = [] } = useQuery({
queryKey: ['/api/orders'],
});
const { data: deliveries = [] } = useQuery({
queryKey: ['/api/deliveries'],
});
const createMutation = useMutation({
mutationFn: async (data: any) => {
console.log('🚚 Frontend: Creating supplier with data:', data);
const result = await apiRequest("/api/suppliers", "POST", data);
console.log('🚚 Frontend: Supplier creation result:', result);
return result;
},
onSuccess: () => {
toast({
title: "Succès",
description: "Fournisseur créé avec succès",
});
queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
setShowCreateModal(false);
setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
},
onError: (error) => {
if (isUnauthorizedError(error)) {
toast({
title: "Non autorisé",
description: "Vous êtes déconnecté. Reconnexion...",
variant: "destructive",
});
setTimeout(() => {
window.location.href = "/api/login";
}, 500);
return;
}
toast({
title: "Erreur",
description: "Impossible de créer le fournisseur",
variant: "destructive",
});
},
});
const updateMutation = useMutation({
mutationFn: async (data: any) => {
console.log('🔧 Frontend: Updating supplier with data:', data);
const result = await apiRequest(`/api/suppliers/${selectedSupplier?.id}`, "PUT", data);
console.log('🔧 Frontend: Supplier update result:', result);
return result;
},
onMutate: async (newData) => {
// Mise à jour optimiste - mettre à jour l'interface immédiatement
console.log('🚀 Frontend: Optimistic update starting...');
await queryClient.cancelQueries({ queryKey: ['/api/suppliers'] });
const previousSuppliers = queryClient.getQueryData(['/api/suppliers']);
if (previousSuppliers && selectedSupplier) {
const updatedSuppliers = (previousSuppliers as any[]).map(supplier =>
supplier.id === selectedSupplier.id
? { ...supplier, ...newData, updatedAt: new Date().toISOString() }
: supplier
);
queryClient.setQueryData(['/api/suppliers'], updatedSuppliers);
console.log('✅ Frontend: Optimistic update applied');
}
return { previousSuppliers };
},
onSuccess: async (result) => {
console.log('✅ Frontend: Update mutation successful, result:', result);
console.log('🔄 Frontend: Starting cache invalidation...');
// Forcer une actualisation complète du cache
await queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
await queryClient.refetchQueries({
queryKey: ['/api/suppliers'],
type: 'active'
});
// Attendre un petit délai pour s'assurer que les données sont fraîches
await new Promise(resolve => setTimeout(resolve, 100));
console.log('✅ Frontend: Cache invalidation complete');
toast({
title: "Succès",
description: "Fournisseur modifié avec succès",
});
setShowEditModal(false);
setSelectedSupplier(null);
setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
},
onError: (error, variables, context) => {
// Rollback optimiste en cas d'erreur
if (context?.previousSuppliers) {
console.log('❌ Frontend: Error occurred, rolling back optimistic update');
queryClient.setQueryData(['/api/suppliers'], context.previousSuppliers);
}
if (isUnauthorizedError(error)) {
toast({
title: "Non autorisé",
description: "Vous êtes déconnecté. Reconnexion...",
variant: "destructive",
});
setTimeout(() => {
window.location.href = "/api/login";
}, 500);
return;
}
toast({
title: "Erreur",
description: "Impossible de modifier le fournisseur",
variant: "destructive",
});
},
});
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
await apiRequest(`/api/suppliers/${id}`, "DELETE");
},
onSuccess: () => {
toast({
title: "Succès",
description: "Fournisseur supprimé avec succès",
});
queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
},
onError: (error) => {
if (isUnauthorizedError(error)) {
toast({
title: "Non autorisé",
description: "Vous êtes déconnecté. Reconnexion...",
variant: "destructive",
});
setTimeout(() => {
window.location.href = "/api/login";
}, 500);
return;
}
toast({
title: "Erreur",
description: "Impossible de supprimer le fournisseur",
variant: "destructive",
});
},
});
const filteredSuppliers = Array.isArray(suppliers) ? suppliers.filter(supplier =>
supplier.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
supplier.contact?.toLowerCase().includes(searchTerm.toLowerCase()) ||
supplier.phone?.toLowerCase().includes(searchTerm.toLowerCase())
) : [];
const getSupplierStats = (supplierId: number) => {
const supplierOrders = Array.isArray(orders) ? orders.filter(order => order.supplierId === supplierId) : [];
const supplierDeliveries = Array.isArray(deliveries) ? deliveries.filter(delivery => delivery.supplierId === supplierId) : [];
return {
orders: supplierOrders.length,
deliveries: supplierDeliveries.length,
delivered: supplierDeliveries.filter(d => d.status === 'delivered').length,
};
};
const handleCreate = () => {
setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
setShowCreateModal(true);
};
const handleEdit = (supplier: Supplier) => {
setSelectedSupplier(supplier);
setFormData({
name: supplier.name,
contact: supplier.contact || "",
phone: supplier.phone || "",
email: supplier.email || "",
address: supplier.address || "",
hasDlc: supplier.hasDlc || false,
automaticReconciliation: supplier.automaticReconciliation || false,
});
setShowEditModal(true);
};
const handleDelete = (supplier: Supplier) => {
const stats = getSupplierStats(supplier.id);
if (stats.orders > 0 || stats.deliveries > 0) {
toast({
title: "Suppression impossible",
description: "Ce fournisseur a des commandes ou livraisons associées",
variant: "destructive",
});
return;
}
if (window.confirm(`Êtes-vous sûr de vouloir supprimer le fournisseur "${supplier.name}" ?`)) {
deleteMutation.mutate(supplier.id);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
toast({
title: "Erreur",
description: "Le nom du fournisseur est requis",
variant: "destructive",
});
return;
}
if (selectedSupplier) {
updateMutation.mutate(formData);
} else {
createMutation.mutate(formData);
}
};
const handleChange = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const canManage = user?.role === 'admin' || user?.role === 'manager';
if (!canManage) {
return (
<div className="flex-1 flex flex-col items-center justify-center">
<div className="text-center">
<Building className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Accès restreint
</h2>
<p className="text-gray-600">
Vous n'avez pas les permissions nécessaires pour accéder à cette page.
</p>
</div>
</div>
);
}
return (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-white border-b border-gray-200 p-6 shadow-sm">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold text-gray-900 flex items-center">
<Building className="w-6 h-6 mr-3 text-blue-600" />
Gestion des Fournisseurs
</h2>
<p className="text-gray-600 mt-1">
{filteredSuppliers.length} fournisseur{filteredSuppliers.length !== 1 ? 's' : ''}
</p>
</div>
<Button
onClick={handleCreate}
className="bg-blue-600 hover:bg-blue-700 text-white shadow-md"
>
<Plus className="w-4 h-4 mr-2" />
Nouveau Fournisseur
</Button>
</div>
</div>
{/* Search */}
<div className="bg-gray-50 border-b border-gray-200 p-4">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Rechercher un fournisseur..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border border-gray-300 shadow-sm"
/>
</div>
</div>
{/* Suppliers List */}
<div className="flex-1 overflow-auto">
{isLoading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
) : filteredSuppliers.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
<Building className="w-16 h-16 mb-4 text-gray-300" />
<h3 className="text-lg font-medium mb-2">
{searchTerm ? "Aucun fournisseur trouvé" : "Aucun fournisseur"}
</h3>
<p className="text-center max-w-md">
{searchTerm
? "Aucun fournisseur ne correspond à votre recherche."
: "Vous n'avez pas encore de fournisseurs. Créez votre premier fournisseur pour commencer."}
</p>
{!searchTerm && (
<Button
onClick={handleCreate}
className="mt-4 bg-primary hover:bg-blue-700 text-white"
>
<Plus className="w-4 h-4 mr-2" />
Créer un fournisseur
</Button>
)}
</div>
) : (
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredSuppliers.map((supplier) => {
const stats = getSupplierStats(supplier.id);
return (
<div key={supplier.id} className="bg-white border border-gray-200 shadow-lg hover:shadow-xl transition-shadow p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<div className="w-10 h-10 bg-blue-600 flex items-center justify-center mr-3">
<Building className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="font-medium text-gray-900">{supplier.name}</h3>
<p className="text-sm text-gray-500">#{supplier.id}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(supplier)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(supplier)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
<div className="space-y-2 mb-4">
{supplier.contact && (
<div className="flex items-center text-sm text-gray-600">
<Mail className="w-4 h-4 mr-2" />
{supplier.contact}
</div>
)}
{supplier.phone && (
<div className="flex items-center text-sm text-gray-600">
<Phone className="w-4 h-4 mr-2" />
{supplier.phone}
</div>
)}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-2">
{supplier.hasDlc && (
<div className="flex items-center text-green-600 bg-green-50 px-2 py-1 rounded-full">
<Clock className="w-3 h-3 mr-1" />
<span className="text-xs font-medium">DLC</span>
</div>
)}
{supplier.automaticReconciliation && (
<div className="flex items-center text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
<CheckCircle className="w-3 h-3 mr-1" />
<span className="text-xs font-medium">Auto-rapprochement</span>
</div>
)}
</div>
</div>
{!supplier.hasDlc && !supplier.automaticReconciliation && (
<div className="text-xs text-gray-500 mt-2">
Aucune option activée
</div>
)}
{/* Statistics section follows */}
</div>
<div className="border-t pt-4">
<div className="grid grid-cols-2 gap-4">
<div className="text-center">
<div className="flex items-center justify-center text-primary mb-1">
<Package className="w-4 h-4 mr-1" />
<span className="font-semibold">{stats.orders}</span>
</div>
<p className="text-xs text-gray-500">Commandes</p>
</div>
<div className="text-center">
<div className="flex items-center justify-center text-secondary mb-1">
<Truck className="w-4 h-4 mr-1" />
<span className="font-semibold">{stats.deliveries}</span>
</div>
<p className="text-xs text-gray-500">Livraisons</p>
</div>
</div>
{stats.deliveries > 0 && (
<div className="mt-2 text-center">
<p className="text-xs text-gray-500">
{stats.delivered} livrées ({Math.round((stats.delivered / stats.deliveries) * 100)}%)
</p>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
{/* Create/Edit Modal */}
<Dialog open={showCreateModal || showEditModal} onOpenChange={() => {
setShowCreateModal(false);
setShowEditModal(false);
setSelectedSupplier(null);
setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}}>
<DialogContent className="sm:max-w-md" aria-describedby="supplier-modal-description">
<DialogHeader>
<DialogTitle>
{selectedSupplier ? 'Modifier' : 'Nouveau'} Fournisseur
</DialogTitle>
<p id="supplier-modal-description" className="text-sm text-gray-600 mt-1">
{selectedSupplier ? 'Modifier les informations du fournisseur' : 'Créer un nouveau fournisseur'}
</p>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="name">Nom du fournisseur *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="Nom du fournisseur"
required
/>
</div>
<div>
<Label htmlFor="contact">Contact</Label>
<Input
id="contact"
value={formData.contact}
onChange={(e) => handleChange('contact', e.target.value)}
placeholder="Nom du contact"
/>
</div>
<div>
<Label htmlFor="phone">Téléphone</Label>
<Input
id="phone"
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
placeholder="Numéro de téléphone"
/>
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="Adresse email"
/>
</div>
<div>
<Label htmlFor="address">Adresse</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => handleChange('address', e.target.value)}
placeholder="Adresse complète"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="hasDlc"
checked={formData.hasDlc}
onCheckedChange={(checked) => handleChange('hasDlc', checked === true)}
/>
<Label htmlFor="hasDlc" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Gestion DLC
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="automaticReconciliation"
checked={formData.automaticReconciliation}
onCheckedChange={(checked) => handleChange('automaticReconciliation', checked === true)}
/>
<Label htmlFor="automaticReconciliation" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Rapprochement automatique BL/Factures
</Label>
</div>
<div className="flex items-center space-x-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => {
setShowCreateModal(false);
setShowEditModal(false);
setSelectedSupplier(null);
setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}}
>
Annuler
</Button>
<Button
type="submit"
disabled={createMutation.isPending || updateMutation.isPending}
className="bg-primary hover:bg-blue-700"
>
{createMutation.isPending || updateMutation.isPending
? "Enregistrement..."
: (selectedSupplier ? "Modifier" : "Créer")}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
);
}
+2 -4
View File
@@ -68,18 +68,16 @@ export const userGroups = pgTable("user_groups", {
createdAt: timestamp("created_at").defaultNow(), createdAt: timestamp("created_at").defaultNow(),
}); });
// Suppliers // Suppliers - Schema exact correspondant à la production
export const suppliers = pgTable("suppliers", { export const suppliers = pgTable("suppliers", {
id: serial("id").primaryKey(), id: serial("id").primaryKey(),
name: varchar("name").notNull(), name: varchar("name").notNull(),
contact: varchar("contact"), contact: varchar("contact"),
phone: varchar("phone"), phone: varchar("phone"),
email: varchar("email"),
address: text("address"),
hasDlc: boolean("has_dlc").default(false), // Coche DLC pour la gestion DLC hasDlc: boolean("has_dlc").default(false), // Coche DLC pour la gestion DLC
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures
createdAt: timestamp("created_at").defaultNow(), createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(),
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures
}); });
// Orders // Orders