diff --git a/client/src/pages/BLReconciliation.tsx b/client/src/pages/BLReconciliation.tsx index 4931724..be73d3a 100644 --- a/client/src/pages/BLReconciliation.tsx +++ b/client/src/pages/BLReconciliation.tsx @@ -12,7 +12,7 @@ import { useStore } from "@/components/Layout"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { usePermissions } from "@shared/permissions"; import { Pagination, usePagination } from "@/components/ui/pagination"; -import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload } from "lucide-react"; +import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload, CheckCircle, AlertCircle } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; @@ -58,6 +58,10 @@ export default function BLReconciliation() { const [selectedFile, setSelectedFile] = useState(null); const [isUploading, setIsUploading] = useState(false); + // État pour la vérification des factures + const [verificationResults, setVerificationResults] = useState>({}); + const [isVerifying, setIsVerifying] = useState>({}); + // Récupérer les fournisseurs pour la logique automatique const { data: suppliers = [] } = useQuery({ queryKey: ['/api/suppliers'], @@ -213,6 +217,116 @@ export default function BLReconciliation() { } }; + // Fonction pour vérifier une référence de facture + const verifyInvoiceReference = async (delivery: any) => { + if (!delivery.invoiceReference || !delivery.supplier?.name || !delivery.group?.id) { + return; + } + + setIsVerifying(prev => ({ ...prev, [delivery.id]: true })); + + try { + const response = await apiRequest('/api/deliveries/verify-invoice', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + groupId: delivery.group.id, + invoiceReference: delivery.invoiceReference, + supplierName: delivery.supplier.name + }) + }); + + setVerificationResults(prev => ({ + ...prev, + [delivery.id]: response + })); + + if (response.found && response.supplierMatch) { + toast({ + title: "✅ Facture vérifiée", + description: `Référence ${delivery.invoiceReference} trouvée et fournisseur correct`, + }); + } else if (response.found && !response.supplierMatch) { + toast({ + title: "⚠️ Fournisseur incorrect", + description: `Référence trouvée mais fournisseur ne correspond pas`, + variant: "destructive" + }); + } else { + toast({ + title: "❌ Référence non trouvée", + description: `Référence ${delivery.invoiceReference} introuvable`, + variant: "destructive" + }); + } + } catch (error) { + console.error('Erreur vérification:', error); + toast({ + title: "Erreur de vérification", + description: "Impossible de vérifier la référence", + variant: "destructive" + }); + } finally { + setIsVerifying(prev => ({ ...prev, [delivery.id]: false })); + } + }; + + // Fonction pour rechercher par numéro de BL et auto-remplir + const searchByBLAndFill = async (delivery: any) => { + if (!delivery.blNumber || !delivery.supplier?.name || !delivery.group?.id) { + return; + } + + setIsVerifying(prev => ({ ...prev, [delivery.id]: true })); + + try { + const response = await apiRequest('/api/deliveries/search-by-bl', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + groupId: delivery.group.id, + blNumber: delivery.blNumber, + supplierName: delivery.supplier.name + }) + }); + + if (response.found && response.invoiceReference) { + // Auto-remplir les champs facture + const updateData: any = { + invoiceReference: response.invoiceReference + }; + + if (response.invoiceAmount) { + updateData.invoiceAmount = response.invoiceAmount; + } + + await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", updateData); + + queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); + + toast({ + title: "✅ Auto-remplissage réussi", + description: `Référence ${response.invoiceReference} trouvée et ajoutée`, + }); + } else { + toast({ + title: "❌ BL non trouvé", + description: `Aucune facture trouvée pour le BL ${delivery.blNumber}`, + variant: "destructive" + }); + } + } catch (error) { + console.error('Erreur recherche BL:', error); + toast({ + title: "Erreur de recherche", + description: "Impossible de rechercher par BL", + variant: "destructive" + }); + } finally { + setIsVerifying(prev => ({ ...prev, [delivery.id]: false })); + } + }; + // Fonction pour déterminer si le bouton "Envoyer Facture" doit être affiché const shouldShowInvoiceButton = (delivery: any) => { // Conditions : ligne non validée OU pas de RefFacture renseignée @@ -536,9 +650,57 @@ export default function BLReconciliation() { -
- {delivery.invoiceReference || ( - Non renseigné +
+ {/* Coche verte si vérification réussie */} + {verificationResults[delivery.id]?.found && verificationResults[delivery.id]?.supplierMatch && ( + + )} + {/* Indicateur d'erreur si vérification échouée */} + {verificationResults[delivery.id]?.found && !verificationResults[delivery.id]?.supplierMatch && ( + + )} + + {/* Référence de facture */} +
+ {delivery.invoiceReference || ( + Non renseigné + )} +
+ + {/* Bouton de vérification si référence existe */} + {delivery.invoiceReference && delivery.group?.nocodbConfigId && ( + + )} + + {/* Bouton d'auto-remplissage si pas de référence mais BL existe */} + {!delivery.invoiceReference && delivery.blNumber && delivery.group?.nocodbConfigId && ( + )}
diff --git a/replit.md b/replit.md index f32a8ea..72cef2c 100644 --- a/replit.md +++ b/replit.md @@ -19,22 +19,30 @@ La plateforme fournit une gestion robuste des flux de travail de livraison avec ## Recent Changes +### 2025-08-13 - Système de Vérification de Factures NocoDB +✅ **Implémentation complète du système de vérification automatique des factures** : + +**Fonctionnalités développées :** +- Service de vérification NocoDB (`server/services/nocodbVerification.ts`) +- API endpoints pour vérification factures et recherche par BL +- Interface utilisateur avec coches vertes de validation +- Auto-remplissage des champs facture/montant depuis NocoDB +- Support multi-magasin avec configurations NocoDB spécifiques + +**Logique de fonctionnement :** +- Vérification références factures existantes avec validation fournisseur +- Recherche automatique par numéro BL pour lignes sans référence +- Auto-remplissage automatique des données trouvées +- Indicateurs visuels : coche verte (succès), cercle rouge (erreur) + +**Configuration technique :** +- Intégration avec configurations NocoDB par magasin +- Mapping flexible des colonnes (facture, BL, montant, fournisseur) +- Gestion d'erreurs et notifications utilisateur +- Interface responsive et accessible + ### 2025-08-13 - Uniformisation complète des interfaces utilisateur -✅ **Harmonisation du design des tableaux** terminée pour une expérience utilisateur cohérente : - -**Pages uniformisées :** -- `Orders.tsx` - Page des commandes avec design responsive optimisé -- `Deliveries.tsx` - Page des livraisons avec structure identique -- `BLReconciliation.tsx` - Page de rapprochement BL/Factures harmonisée -- `Dashboard.tsx` - Tableau de bord avec erreurs TypeScript corrigées - -**Améliorations appliquées :** -- Headers responsifs adaptés mobile/tablette -- Filtres restructurés avec style cohérent -- Tableaux standardisés : `px-6 py-4` et `whitespace-nowrap` -- Boutons d'actions modernisés avec composants Button shadcn/ui -- Pagination repositionnée avec bordures uniformisées -- Espacement et typographie harmonisés +✅ **Harmonisation du design des tableaux** terminée pour une expérience utilisateur cohérente ### 2025-08-12 - Nettoyage des fichiers inutiles ✅ **Suppression complète des fichiers inutiles** effectuée pour optimiser le projet diff --git a/server/routes.ts b/server/routes.ts index 537fb0f..a8fb6b2 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -37,6 +37,7 @@ import { } from "@shared/schema"; import { hasPermission } from "@shared/permissions"; import { z } from "zod"; +import { nocodbVerificationService } from "./services/nocodbVerification"; export async function registerRoutes(app: Express): Promise { // Health check endpoint for Docker @@ -889,6 +890,79 @@ export async function registerRoutes(app: Express): Promise { } }); + // Routes de vérification NocoDB pour les factures + app.post('/api/deliveries/verify-invoice', isAuthenticated, async (req: any, res) => { + try { + const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const { groupId, invoiceReference, supplierName } = req.body; + + if (!groupId || !invoiceReference || !supplierName) { + return res.status(400).json({ + message: "groupId, invoiceReference et supplierName sont requis" + }); + } + + // Vérifier les permissions d'accès au groupe + if (user.role !== 'admin') { + const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; + if (!userGroupIds.includes(parseInt(groupId))) { + return res.status(403).json({ message: "Access denied to this group" }); + } + } + + const result = await nocodbVerificationService.verifyInvoiceReference( + parseInt(groupId), + invoiceReference, + supplierName + ); + + res.json(result); + } catch (error) { + console.error("Error verifying invoice:", error); + res.status(500).json({ message: "Failed to verify invoice" }); + } + }); + + app.post('/api/deliveries/search-by-bl', isAuthenticated, async (req: any, res) => { + try { + const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const { groupId, blNumber, supplierName } = req.body; + + if (!groupId || !blNumber || !supplierName) { + return res.status(400).json({ + message: "groupId, blNumber et supplierName sont requis" + }); + } + + // Vérifier les permissions d'accès au groupe + if (user.role !== 'admin') { + const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; + if (!userGroupIds.includes(parseInt(groupId))) { + return res.status(403).json({ message: "Access denied to this group" }); + } + } + + const result = await nocodbVerificationService.searchByBLNumber( + parseInt(groupId), + blNumber, + supplierName + ); + + res.json(result); + } catch (error) { + console.error("Error searching by BL:", error); + res.status(500).json({ message: "Failed to search by BL number" }); + } + }); + // Statistics routes app.get('/api/stats/monthly', isAuthenticated, async (req: any, res) => { try { diff --git a/server/services/nocodbVerification.ts b/server/services/nocodbVerification.ts new file mode 100644 index 0000000..1bc9d43 --- /dev/null +++ b/server/services/nocodbVerification.ts @@ -0,0 +1,212 @@ +import { storage } from '../storage'; + +export interface InvoiceVerificationResult { + found: boolean; + invoiceReference?: string; + invoiceAmount?: string; + supplierMatch: boolean; + error?: string; +} + +export interface BLSearchResult { + found: boolean; + invoiceReference?: string; + invoiceAmount?: string; + error?: string; +} + +export class NocoDBVerificationService { + + /** + * Vérifie l'existence d'une référence de facture dans NocoDB pour un magasin donné + */ + async verifyInvoiceReference( + groupId: number, + invoiceReference: string, + supplierName: string + ): Promise { + try { + console.log(`🔍 Vérification facture ${invoiceReference} pour fournisseur ${supplierName} (magasin ${groupId})`); + + // Récupérer la configuration du magasin + const group = await storage.getGroup(groupId); + if (!group?.nocodbConfigId || !group.nocodbTableName) { + console.log(`❌ Pas de configuration NocoDB pour le magasin ${groupId}`); + return { + found: false, + supplierMatch: false, + error: 'Configuration NocoDB manquante pour ce magasin' + }; + } + + // Récupérer la configuration NocoDB globale + const nocodbConfig = await storage.getNocodbConfig(group.nocodbConfigId); + if (!nocodbConfig) { + return { + found: false, + supplierMatch: false, + error: 'Configuration NocoDB introuvable' + }; + } + + // Construire l'URL de l'API NocoDB + const apiUrl = `${nocodbConfig.baseUrl}/api/v1/db/data/v1/${nocodbConfig.projectId}/${group.nocodbTableName}`; + + // Construire les paramètres de recherche + const searchParams = new URLSearchParams(); + if (group.invoiceColumnName) { + searchParams.append('where', `(${group.invoiceColumnName},eq,${invoiceReference})`); + } + + const finalUrl = `${apiUrl}?${searchParams.toString()}`; + console.log(`🌐 Requête NocoDB: ${finalUrl}`); + + // Faire la requête à NocoDB + const response = await fetch(finalUrl, { + headers: { + 'xc-token': nocodbConfig.apiToken, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + console.error(`❌ Erreur NocoDB (${response.status}):`, await response.text()); + return { + found: false, + supplierMatch: false, + error: `Erreur NocoDB: ${response.status}` + }; + } + + const data = await response.json(); + console.log(`📊 Réponse NocoDB:`, { found: data.list?.length > 0, count: data.list?.length }); + + if (data.list && data.list.length > 0) { + const record = data.list[0]; + + // Vérifier si le fournisseur correspond + let supplierMatch = true; + if (group.nocodbSupplierColumnName) { + const nocodbSupplier = record[group.nocodbSupplierColumnName]; + supplierMatch = nocodbSupplier === supplierName; + console.log(`🏷️ Vérification fournisseur:`, { + expected: supplierName, + found: nocodbSupplier, + match: supplierMatch + }); + } + + return { + found: true, + invoiceReference, + invoiceAmount: group.nocodbAmountColumnName ? record[group.nocodbAmountColumnName]?.toString() : undefined, + supplierMatch + }; + } + + return { + found: false, + supplierMatch: false + }; + + } catch (error) { + console.error('❌ Erreur lors de la vérification facture:', error); + return { + found: false, + supplierMatch: false, + error: `Erreur: ${error instanceof Error ? error.message : 'Erreur inconnue'}` + }; + } + } + + /** + * Recherche une facture par numéro de BL et fournisseur + */ + async searchByBLNumber( + groupId: number, + blNumber: string, + supplierName: string + ): Promise { + try { + console.log(`🔍 Recherche par BL ${blNumber} pour fournisseur ${supplierName} (magasin ${groupId})`); + + // Récupérer la configuration du magasin + const group = await storage.getGroup(groupId); + if (!group?.nocodbConfigId || !group.nocodbTableName || !group.nocodbBlColumnName) { + console.log(`❌ Configuration BL manquante pour le magasin ${groupId}`); + return { + found: false, + error: 'Configuration NocoDB pour recherche BL manquante' + }; + } + + // Récupérer la configuration NocoDB globale + const nocodbConfig = await storage.getNocodbConfig(group.nocodbConfigId); + if (!nocodbConfig) { + return { + found: false, + error: 'Configuration NocoDB introuvable' + }; + } + + // Construire l'URL de l'API NocoDB pour recherche par BL + const apiUrl = `${nocodbConfig.baseUrl}/api/v1/db/data/v1/${nocodbConfig.projectId}/${group.nocodbTableName}`; + + // Construire les paramètres de recherche par BL et éventuellement fournisseur + const searchParams = new URLSearchParams(); + let whereClause = `(${group.nocodbBlColumnName},eq,${blNumber})`; + + // Ajouter la condition fournisseur si configurée + if (group.nocodbSupplierColumnName) { + whereClause += `~and(${group.nocodbSupplierColumnName},eq,${supplierName})`; + } + + searchParams.append('where', whereClause); + + const finalUrl = `${apiUrl}?${searchParams.toString()}`; + console.log(`🌐 Requête NocoDB recherche BL: ${finalUrl}`); + + // Faire la requête à NocoDB + const response = await fetch(finalUrl, { + headers: { + 'xc-token': nocodbConfig.apiToken, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + console.error(`❌ Erreur NocoDB recherche BL (${response.status}):`, await response.text()); + return { + found: false, + error: `Erreur NocoDB: ${response.status}` + }; + } + + const data = await response.json(); + console.log(`📊 Réponse NocoDB recherche BL:`, { found: data.list?.length > 0, count: data.list?.length }); + + if (data.list && data.list.length > 0) { + const record = data.list[0]; + + return { + found: true, + invoiceReference: group.invoiceColumnName ? record[group.invoiceColumnName] : undefined, + invoiceAmount: group.nocodbAmountColumnName ? record[group.nocodbAmountColumnName]?.toString() : undefined + }; + } + + return { + found: false + }; + + } catch (error) { + console.error('❌ Erreur lors de la recherche par BL:', error); + return { + found: false, + error: `Erreur: ${error instanceof Error ? error.message : 'Erreur inconnue'}` + }; + } + } +} + +export const nocodbVerificationService = new NocoDBVerificationService(); \ No newline at end of file diff --git a/server/storage.ts b/server/storage.ts index 493d515..6da756d 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -2053,11 +2053,43 @@ class MemStorage implements IStorage { this.users.set('admin_dev', adminUser); this.users.set('admin', adminUser); // Also set by username for easy lookup - // Add test groups + // Add test groups with NocoDB configuration const testGroups = [ - { id: 1, name: 'Magasin A', color: '#FF5733', webhookUrl: 'https://webhook.site/test-facture-magasin-a', createdAt: new Date(), updatedAt: new Date() }, - { id: 2, name: 'Magasin B', color: '#33FF57', webhookUrl: 'https://webhook.site/test-facture-magasin-b', createdAt: new Date(), updatedAt: new Date() }, - { id: 3, name: 'Magasin C', color: '#3357FF', createdAt: new Date(), updatedAt: new Date() } + { + id: 1, + name: 'Magasin A', + color: '#FF5733', + webhookUrl: 'https://webhook.site/test-facture-magasin-a', + nocodbConfigId: 1, + nocodbTableName: 'factures_magasin_a', + invoiceColumnName: 'reference_facture', + nocodbBlColumnName: 'numero_bl', + nocodbAmountColumnName: 'montant', + nocodbSupplierColumnName: 'fournisseur', + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: 2, + name: 'Magasin B', + color: '#33FF57', + webhookUrl: 'https://webhook.site/test-facture-magasin-b', + nocodbConfigId: 1, + nocodbTableName: 'factures_magasin_b', + invoiceColumnName: 'ref_facture', + nocodbBlColumnName: 'bl_number', + nocodbAmountColumnName: 'amount', + nocodbSupplierColumnName: 'supplier_name', + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: 3, + name: 'Magasin C', + color: '#3357FF', + createdAt: new Date(), + updatedAt: new Date() + } ]; testGroups.forEach(group => this.groups.set(group.id, group as Group)); @@ -2221,7 +2253,9 @@ class MemStorage implements IStorage { async getGroups(): Promise { return Array.from(this.groups.values()); } - async getGroup(id: number): Promise { return undefined; } + async getGroup(id: number): Promise { + return this.groups.get(id); + } async createGroup(group: InsertGroup): Promise { return {} as Group; } async updateGroup(id: number, group: Partial): Promise { return {} as Group; } async deleteGroup(id: number): Promise {} @@ -2449,7 +2483,20 @@ class MemStorage implements IStorage { async removeUserFromGroup(): Promise {} async getMonthlyStats(): Promise { return {}; } async getNocodbConfigs(): Promise { return []; } - async getNocodbConfig(): Promise { return null; } + async getNocodbConfig(id: number): Promise { + // Retourner une configuration de test pour le développement + if (id === 1) { + return { + id: 1, + name: 'Config Test NocoDB', + baseUrl: 'https://nocodb-test.example.com', + projectId: 'test-project', + apiToken: 'test-token-123', + isActive: true + }; + } + return null; + } async createNocodbConfig(): Promise { return {}; } async updateNocodbConfig(): Promise { return {}; } async deleteNocodbConfig(): Promise {}