mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-21 22:18:17 +02:00
Add invoice verification system using NocoDB for automated data checks
Integrates NocoDB to verify invoice references and supplier matches, and allows searching by BL number to auto-populate delivery details, enhancing reconciliation accuracy and efficiency. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c52a701b-4841-4d00-b8cf-5a41c12abdf3 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/c52a701b-4841-4d00-b8cf-5a41c12abdf3/3uCfEKb
This commit is contained in:
1 parent
d418fda3c9
commit
bc85434858
5 files changed
+528
-25
No files matched your search
@@ -12,7 +12,7 @@ import { useStore } from "@/components/Layout";
|
|||||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
import { usePermissions } from "@shared/permissions";
|
import { usePermissions } from "@shared/permissions";
|
||||||
import { Pagination, usePagination } from "@/components/ui/pagination";
|
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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -58,6 +58,10 @@ export default function BLReconciliation() {
|
|||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
|
// État pour la vérification des factures
|
||||||
|
const [verificationResults, setVerificationResults] = useState<Record<number, any>>({});
|
||||||
|
const [isVerifying, setIsVerifying] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
// Récupérer les fournisseurs pour la logique automatique
|
// Récupérer les fournisseurs pour la logique automatique
|
||||||
const { data: suppliers = [] } = useQuery<any[]>({
|
const { data: suppliers = [] } = useQuery<any[]>({
|
||||||
queryKey: ['/api/suppliers'],
|
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é
|
// Fonction pour déterminer si le bouton "Envoyer Facture" doit être affiché
|
||||||
const shouldShowInvoiceButton = (delivery: any) => {
|
const shouldShowInvoiceButton = (delivery: any) => {
|
||||||
// Conditions : ligne non validée OU pas de RefFacture renseignée
|
// Conditions : ligne non validée OU pas de RefFacture renseignée
|
||||||
@@ -536,9 +650,57 @@ export default function BLReconciliation() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className={`text-sm ${delivery.reconciled !== true ? 'font-medium text-gray-900' : 'text-gray-600'}`}>
|
<div className="flex items-center space-x-2">
|
||||||
{delivery.invoiceReference || (
|
{/* Coche verte si vérification réussie */}
|
||||||
<span className="text-gray-400 italic">Non renseigné</span>
|
{verificationResults[delivery.id]?.found && verificationResults[delivery.id]?.supplierMatch && (
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0" title="Facture vérifiée avec succès" />
|
||||||
|
)}
|
||||||
|
{/* Indicateur d'erreur si vérification échouée */}
|
||||||
|
{verificationResults[delivery.id]?.found && !verificationResults[delivery.id]?.supplierMatch && (
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" title="Fournisseur ne correspond pas" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Référence de facture */}
|
||||||
|
<div className={`text-sm ${delivery.reconciled !== true ? 'font-medium text-gray-900' : 'text-gray-600'}`}>
|
||||||
|
{delivery.invoiceReference || (
|
||||||
|
<span className="text-gray-400 italic">Non renseigné</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bouton de vérification si référence existe */}
|
||||||
|
{delivery.invoiceReference && delivery.group?.nocodbConfigId && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => verifyInvoiceReference(delivery)}
|
||||||
|
disabled={isVerifying[delivery.id]}
|
||||||
|
className="h-6 w-6 p-0 ml-1 flex-shrink-0"
|
||||||
|
title="Vérifier la référence facture dans NocoDB"
|
||||||
|
>
|
||||||
|
{isVerifying[delivery.id] ? (
|
||||||
|
<div className="w-3 h-3 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
) : (
|
||||||
|
<Search className="w-3 h-3 text-blue-600" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bouton d'auto-remplissage si pas de référence mais BL existe */}
|
||||||
|
{!delivery.invoiceReference && delivery.blNumber && delivery.group?.nocodbConfigId && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => searchByBLAndFill(delivery)}
|
||||||
|
disabled={isVerifying[delivery.id]}
|
||||||
|
className="h-6 w-6 p-0 ml-1 flex-shrink-0"
|
||||||
|
title="Rechercher et auto-remplir par numéro BL"
|
||||||
|
>
|
||||||
|
{isVerifying[delivery.id] ? (
|
||||||
|
<div className="w-3 h-3 border-2 border-green-500 border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
) : (
|
||||||
|
<CheckCircle className="w-3 h-3 text-green-600" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -19,22 +19,30 @@ La plateforme fournit une gestion robuste des flux de travail de livraison avec
|
|||||||
|
|
||||||
## Recent Changes
|
## 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
|
### 2025-08-13 - Uniformisation complète des interfaces utilisateur
|
||||||
✅ **Harmonisation du design des tableaux** terminée pour une expérience utilisateur cohérente :
|
✅ **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
|
|
||||||
|
|
||||||
### 2025-08-12 - Nettoyage des fichiers inutiles
|
### 2025-08-12 - Nettoyage des fichiers inutiles
|
||||||
✅ **Suppression complète des fichiers inutiles** effectuée pour optimiser le projet
|
✅ **Suppression complète des fichiers inutiles** effectuée pour optimiser le projet
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { hasPermission } from "@shared/permissions";
|
import { hasPermission } from "@shared/permissions";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { nocodbVerificationService } from "./services/nocodbVerification";
|
||||||
|
|
||||||
export async function registerRoutes(app: Express): Promise<Server> {
|
export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// Health check endpoint for Docker
|
// Health check endpoint for Docker
|
||||||
@@ -889,6 +890,79 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
// Statistics routes
|
||||||
app.get('/api/stats/monthly', isAuthenticated, async (req: any, res) => {
|
app.get('/api/stats/monthly', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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<InvoiceVerificationResult> {
|
||||||
|
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<BLSearchResult> {
|
||||||
|
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();
|
||||||
+53
-6
@@ -2053,11 +2053,43 @@ class MemStorage implements IStorage {
|
|||||||
this.users.set('admin_dev', adminUser);
|
this.users.set('admin_dev', adminUser);
|
||||||
this.users.set('admin', adminUser); // Also set by username for easy lookup
|
this.users.set('admin', adminUser); // Also set by username for easy lookup
|
||||||
|
|
||||||
// Add test groups
|
// Add test groups with NocoDB configuration
|
||||||
const testGroups = [
|
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: 1,
|
||||||
{ id: 3, name: 'Magasin C', color: '#3357FF', createdAt: new Date(), updatedAt: new Date() }
|
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));
|
testGroups.forEach(group => this.groups.set(group.id, group as Group));
|
||||||
|
|
||||||
@@ -2221,7 +2253,9 @@ class MemStorage implements IStorage {
|
|||||||
async getGroups(): Promise<Group[]> {
|
async getGroups(): Promise<Group[]> {
|
||||||
return Array.from(this.groups.values());
|
return Array.from(this.groups.values());
|
||||||
}
|
}
|
||||||
async getGroup(id: number): Promise<Group | undefined> { return undefined; }
|
async getGroup(id: number): Promise<Group | undefined> {
|
||||||
|
return this.groups.get(id);
|
||||||
|
}
|
||||||
async createGroup(group: InsertGroup): Promise<Group> { return {} as Group; }
|
async createGroup(group: InsertGroup): Promise<Group> { return {} as Group; }
|
||||||
async updateGroup(id: number, group: Partial<InsertGroup>): Promise<Group> { return {} as Group; }
|
async updateGroup(id: number, group: Partial<InsertGroup>): Promise<Group> { return {} as Group; }
|
||||||
async deleteGroup(id: number): Promise<void> {}
|
async deleteGroup(id: number): Promise<void> {}
|
||||||
@@ -2449,7 +2483,20 @@ class MemStorage implements IStorage {
|
|||||||
async removeUserFromGroup(): Promise<void> {}
|
async removeUserFromGroup(): Promise<void> {}
|
||||||
async getMonthlyStats(): Promise<any> { return {}; }
|
async getMonthlyStats(): Promise<any> { return {}; }
|
||||||
async getNocodbConfigs(): Promise<any[]> { return []; }
|
async getNocodbConfigs(): Promise<any[]> { return []; }
|
||||||
async getNocodbConfig(): Promise<any> { return null; }
|
async getNocodbConfig(id: number): Promise<any> {
|
||||||
|
// 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<any> { return {}; }
|
async createNocodbConfig(): Promise<any> { return {}; }
|
||||||
async updateNocodbConfig(): Promise<any> { return {}; }
|
async updateNocodbConfig(): Promise<any> { return {}; }
|
||||||
async deleteNocodbConfig(): Promise<void> {}
|
async deleteNocodbConfig(): Promise<void> {}
|
||||||
|
|||||||
Reference in new issue
Block a user