mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-19 13:08:23 +02:00
10 files changed
+44
-1260
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, CheckCircle, AlertCircle } from "lucide-react";
|
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload } 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,10 +58,6 @@ 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'],
|
||||||
@@ -217,116 +213,6 @@ 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: any) {
|
|
||||||
console.error('Erreur vérification:', error);
|
|
||||||
toast({
|
|
||||||
title: "Erreur de vérification",
|
|
||||||
description: error.response?.data?.message || error.message || "Erreur de vérification - vérifiez la configuration NocoDB",
|
|
||||||
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: any) {
|
|
||||||
console.error('Erreur recherche BL:', error);
|
|
||||||
toast({
|
|
||||||
title: "Erreur de recherche",
|
|
||||||
description: error.response?.data?.message || error.message || "Erreur de recherche BL - vérifiez la configuration NocoDB",
|
|
||||||
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
|
||||||
@@ -650,57 +536,9 @@ 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="flex items-center space-x-2">
|
<div className={`text-sm ${delivery.reconciled !== true ? 'font-medium text-gray-900' : 'text-gray-600'}`}>
|
||||||
{/* Coche verte si vérification réussie */}
|
{delivery.invoiceReference || (
|
||||||
{verificationResults[delivery.id]?.found && verificationResults[delivery.id]?.supplierMatch && (
|
<span className="text-gray-400 italic">Non renseigné</span>
|
||||||
<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,32 +19,22 @@ 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 avec Cache Production
|
|
||||||
✅ **Implémentation complète du système de vérification automatique des factures** :
|
|
||||||
|
|
||||||
**Fonctionnalités développées :**
|
|
||||||
- Service de vérification production (`server/services/productionInvoiceVerification.ts`)
|
|
||||||
- Utilise les tables existantes de cache (`INVOICE_VERIFICATION_CACHE`, `INVOICE_VERIFICATIONS`)
|
|
||||||
- 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 d'abord dans le cache (tables production)
|
|
||||||
- Si pas trouvé, appel réel à NocoDB pour récupérer les données
|
|
||||||
- Sauvegarde automatique des résultats dans le cache (1h expiration)
|
|
||||||
- Indicateurs visuels : coche verte (succès), rouge (erreur)
|
|
||||||
|
|
||||||
**Configuration technique :**
|
|
||||||
- Intégration avec tables cache existantes en production
|
|
||||||
- Appels API NocoDB authentifiés avec tokens personnels
|
|
||||||
- Mapping flexible des colonnes (facture, BL, montant, fournisseur)
|
|
||||||
- Gestion d'erreurs et notifications utilisateur améliorées
|
|
||||||
- Guide complet de configuration production fourni
|
|
||||||
|
|
||||||
### 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,7 +37,6 @@ 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 { ProductionInvoiceVerificationService } from "./services/productionInvoiceVerification";
|
|
||||||
|
|
||||||
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
|
||||||
@@ -890,81 +889,6 @@ 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 productionVerificationService = new ProductionInvoiceVerificationService(storage);
|
|
||||||
const result = await productionVerificationService.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 productionVerificationService = new ProductionInvoiceVerificationService(storage);
|
|
||||||
const result = await productionVerificationService.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 {
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
import { storage } from "../storage";
|
|
||||||
|
|
||||||
interface NocodbConfig {
|
|
||||||
id: number;
|
|
||||||
baseUrl: string;
|
|
||||||
apiToken: string;
|
|
||||||
projectId: string;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GroupConfig {
|
|
||||||
nocodbConfigId?: number;
|
|
||||||
nocodbTableName?: string;
|
|
||||||
invoiceColumnName?: string;
|
|
||||||
nocodbBlColumnName?: string;
|
|
||||||
nocodbAmountColumnName?: string;
|
|
||||||
nocodbSupplierColumnName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class NocodbVerificationService {
|
|
||||||
private async getNocodbConfig(configId: number): Promise<NocodbConfig | null> {
|
|
||||||
try {
|
|
||||||
return await storage.getNocodbConfig(configId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur récupération config NocoDB:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getGroupConfig(groupId: number): Promise<GroupConfig | null> {
|
|
||||||
try {
|
|
||||||
const group = await storage.getGroup(groupId);
|
|
||||||
if (!group) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
nocodbConfigId: group.nocodbConfigId || undefined,
|
|
||||||
nocodbTableName: group.nocodbTableName || undefined,
|
|
||||||
invoiceColumnName: group.invoiceColumnName || 'invoice_reference',
|
|
||||||
nocodbBlColumnName: group.nocodbBlColumnName || 'bl_number',
|
|
||||||
nocodbAmountColumnName: group.nocodbAmountColumnName || 'amount',
|
|
||||||
nocodbSupplierColumnName: group.nocodbSupplierColumnName || 'supplier'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur récupération config groupe:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async makeNocodbRequest(
|
|
||||||
config: NocodbConfig,
|
|
||||||
tableName: string,
|
|
||||||
queryParams: Record<string, any>
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
// Construire les filtres NocoDB
|
|
||||||
Object.entries(queryParams).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
params.append(`where`, `(${key},eq,${value})`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = `${config.baseUrl}/api/v1/db/data/${config.projectId}/${tableName}?${params.toString()}`;
|
|
||||||
|
|
||||||
console.log('🔍 Requête NocoDB:', { url, table: tableName, filters: queryParams });
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'xc-auth': config.apiToken,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('✅ Réponse NocoDB:', { count: data.list?.length || 0 });
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur requête NocoDB:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyInvoiceReference(
|
|
||||||
groupId: number,
|
|
||||||
invoiceReference: string,
|
|
||||||
supplierName: string
|
|
||||||
): Promise<{ found: boolean; supplierMatch: boolean; data?: any }> {
|
|
||||||
try {
|
|
||||||
console.log('🔍 Vérification facture:', { groupId, invoiceReference, supplierName });
|
|
||||||
|
|
||||||
// Récupérer la configuration du groupe
|
|
||||||
const groupConfig = await this.getGroupConfig(groupId);
|
|
||||||
if (!groupConfig?.nocodbConfigId || !groupConfig?.nocodbTableName) {
|
|
||||||
console.log('⚠️ Configuration NocoDB manquante pour le groupe', groupId);
|
|
||||||
return { found: false, supplierMatch: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer la configuration NocoDB
|
|
||||||
const nocodbConfig = await this.getNocodbConfig(groupConfig.nocodbConfigId);
|
|
||||||
if (!nocodbConfig || !nocodbConfig.isActive) {
|
|
||||||
console.log('⚠️ Configuration NocoDB inactive ou manquante');
|
|
||||||
return { found: false, supplierMatch: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Effectuer la recherche
|
|
||||||
const queryParams = {
|
|
||||||
[groupConfig.invoiceColumnName!]: invoiceReference
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await this.makeNocodbRequest(
|
|
||||||
nocodbConfig,
|
|
||||||
groupConfig.nocodbTableName,
|
|
||||||
queryParams
|
|
||||||
);
|
|
||||||
|
|
||||||
const records = response.list || response.rows || [];
|
|
||||||
|
|
||||||
if (records.length === 0) {
|
|
||||||
return { found: false, supplierMatch: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier la correspondance du fournisseur
|
|
||||||
const record = records[0];
|
|
||||||
const recordSupplier = record[groupConfig.nocodbSupplierColumnName!];
|
|
||||||
|
|
||||||
const supplierMatch = recordSupplier &&
|
|
||||||
recordSupplier.toString().toLowerCase().includes(supplierName.toLowerCase());
|
|
||||||
|
|
||||||
return {
|
|
||||||
found: true,
|
|
||||||
supplierMatch: !!supplierMatch,
|
|
||||||
data: record
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur vérification facture:', error);
|
|
||||||
return { found: false, supplierMatch: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchByBLNumber(
|
|
||||||
groupId: number,
|
|
||||||
blNumber: string,
|
|
||||||
supplierName: string
|
|
||||||
): Promise<{ found: boolean; invoiceReference?: string; invoiceAmount?: number; data?: any }> {
|
|
||||||
try {
|
|
||||||
console.log('🔍 Recherche par BL:', { groupId, blNumber, supplierName });
|
|
||||||
|
|
||||||
// Récupérer la configuration du groupe
|
|
||||||
const groupConfig = await this.getGroupConfig(groupId);
|
|
||||||
if (!groupConfig?.nocodbConfigId || !groupConfig?.nocodbTableName) {
|
|
||||||
console.log('⚠️ Configuration NocoDB manquante pour le groupe', groupId);
|
|
||||||
return { found: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer la configuration NocoDB
|
|
||||||
const nocodbConfig = await this.getNocodbConfig(groupConfig.nocodbConfigId);
|
|
||||||
if (!nocodbConfig || !nocodbConfig.isActive) {
|
|
||||||
console.log('⚠️ Configuration NocoDB inactive ou manquante');
|
|
||||||
return { found: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Effectuer la recherche par BL
|
|
||||||
const queryParams = {
|
|
||||||
[groupConfig.nocodbBlColumnName!]: blNumber,
|
|
||||||
[groupConfig.nocodbSupplierColumnName!]: supplierName
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await this.makeNocodbRequest(
|
|
||||||
nocodbConfig,
|
|
||||||
groupConfig.nocodbTableName,
|
|
||||||
queryParams
|
|
||||||
);
|
|
||||||
|
|
||||||
const records = response.list || response.rows || [];
|
|
||||||
|
|
||||||
if (records.length === 0) {
|
|
||||||
return { found: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const record = records[0];
|
|
||||||
const invoiceReference = record[groupConfig.invoiceColumnName!];
|
|
||||||
const invoiceAmount = record[groupConfig.nocodbAmountColumnName!];
|
|
||||||
|
|
||||||
return {
|
|
||||||
found: true,
|
|
||||||
invoiceReference: invoiceReference?.toString(),
|
|
||||||
invoiceAmount: invoiceAmount ? parseFloat(invoiceAmount.toString()) : undefined,
|
|
||||||
data: record
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur recherche par BL:', error);
|
|
||||||
return { found: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const nocodbVerificationService = new NocodbVerificationService();
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
import { IStorage } from "../storage";
|
|
||||||
|
|
||||||
interface VerificationResult {
|
|
||||||
exists: boolean;
|
|
||||||
matchType: string;
|
|
||||||
invoiceReference?: string;
|
|
||||||
invoiceAmount?: number;
|
|
||||||
errorMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ProductionInvoiceVerificationService {
|
|
||||||
constructor(private storage: IStorage) {}
|
|
||||||
|
|
||||||
// Vérifie une référence facture (avec cache)
|
|
||||||
async verifyInvoiceReference(
|
|
||||||
groupId: number,
|
|
||||||
invoiceReference: string,
|
|
||||||
supplierName?: string
|
|
||||||
): Promise<VerificationResult> {
|
|
||||||
console.log(`🔍 Production Verification - Invoice: ${invoiceReference}, Group: ${groupId}, Supplier: ${supplierName}`);
|
|
||||||
|
|
||||||
// 1. Vérifier d'abord dans les vérifications existantes
|
|
||||||
const existingVerifications = await this.storage.getInvoiceVerifications();
|
|
||||||
const existing = existingVerifications.find(v =>
|
|
||||||
v.groupId === groupId &&
|
|
||||||
v.invoiceReference === invoiceReference &&
|
|
||||||
(!supplierName || v.supplierName === supplierName)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
console.log(`✅ Production Cache Hit - Found existing verification: ${existing.exists}`);
|
|
||||||
return {
|
|
||||||
exists: existing.exists,
|
|
||||||
matchType: existing.matchType || 'direct',
|
|
||||||
invoiceReference: existing.invoiceReference
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Vérifier dans le cache
|
|
||||||
const cacheKey = `${groupId}_${invoiceReference}_${supplierName || 'any'}`;
|
|
||||||
const cache = await this.storage.getInvoiceVerificationCache(cacheKey);
|
|
||||||
|
|
||||||
if (cache && cache.expiresAt > new Date()) {
|
|
||||||
console.log(`⚡ Production Cache Hit - Using cached result: ${cache.exists}`);
|
|
||||||
return {
|
|
||||||
exists: cache.exists,
|
|
||||||
matchType: cache.matchType,
|
|
||||||
invoiceReference: cache.invoiceReference
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Faire la vérification réelle via NocoDB
|
|
||||||
try {
|
|
||||||
const result = await this.verifyWithNocoDB(groupId, invoiceReference, supplierName);
|
|
||||||
|
|
||||||
// 4. Sauvegarder dans le cache
|
|
||||||
await this.saveToCaches(groupId, invoiceReference, supplierName, result, 'delivery_verification');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Production NocoDB Error:`, error);
|
|
||||||
|
|
||||||
// Sauvegarder l'erreur dans le cache
|
|
||||||
const errorResult: VerificationResult = {
|
|
||||||
exists: false,
|
|
||||||
matchType: 'error',
|
|
||||||
errorMessage: error instanceof Error ? error.message : 'Erreur de vérification'
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.saveToCaches(groupId, invoiceReference, supplierName, errorResult, 'error');
|
|
||||||
return errorResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recherche par numéro BL (avec cache)
|
|
||||||
async searchByBLNumber(
|
|
||||||
groupId: number,
|
|
||||||
blNumber: string,
|
|
||||||
supplierName?: string
|
|
||||||
): Promise<VerificationResult> {
|
|
||||||
console.log(`🔍 Production BL Search - BL: ${blNumber}, Group: ${groupId}, Supplier: ${supplierName}`);
|
|
||||||
|
|
||||||
// Vérifier dans le cache BL
|
|
||||||
const cacheKey = `bl_${groupId}_${blNumber}_${supplierName || 'any'}`;
|
|
||||||
const cache = await this.storage.getInvoiceVerificationCache(cacheKey);
|
|
||||||
|
|
||||||
if (cache && cache.expiresAt > new Date()) {
|
|
||||||
console.log(`⚡ Production BL Cache Hit: ${cache.exists}`);
|
|
||||||
return {
|
|
||||||
exists: cache.exists,
|
|
||||||
matchType: cache.matchType,
|
|
||||||
invoiceReference: cache.invoiceReference,
|
|
||||||
invoiceAmount: cache.apiCallTime // On stocke le montant dans ce champ temporairement
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Faire la recherche réelle via NocoDB
|
|
||||||
try {
|
|
||||||
const result = await this.searchBLWithNocoDB(groupId, blNumber, supplierName);
|
|
||||||
|
|
||||||
// Sauvegarder dans le cache
|
|
||||||
await this.saveToCaches(groupId, blNumber, supplierName, result, 'bl_search');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Production BL Search Error:`, error);
|
|
||||||
|
|
||||||
const errorResult: VerificationResult = {
|
|
||||||
exists: false,
|
|
||||||
matchType: 'error',
|
|
||||||
errorMessage: error instanceof Error ? error.message : 'Erreur de recherche BL'
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.saveToCaches(groupId, blNumber, supplierName, errorResult, 'bl_error');
|
|
||||||
return errorResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérification réelle avec NocoDB
|
|
||||||
private async verifyWithNocoDB(
|
|
||||||
groupId: number,
|
|
||||||
invoiceReference: string,
|
|
||||||
supplierName?: string
|
|
||||||
): Promise<VerificationResult> {
|
|
||||||
// Récupérer la configuration du groupe
|
|
||||||
const group = await this.storage.getGroup(groupId);
|
|
||||||
if (!group || !group.nocodbConfigId || !group.nocodbTableName) {
|
|
||||||
throw new Error(`Configuration NocoDB manquante pour le groupe ${groupId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer la configuration NocoDB
|
|
||||||
const nocodbConfig = await this.storage.getNocodbConfig(group.nocodbConfigId);
|
|
||||||
if (!nocodbConfig || !nocodbConfig.isActive) {
|
|
||||||
throw new Error(`Configuration NocoDB inactive ou introuvable (ID: ${group.nocodbConfigId})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🌐 Production NocoDB Call - Table: ${group.nocodbTableName}, Invoice: ${invoiceReference}`);
|
|
||||||
|
|
||||||
// Construire la requête NocoDB
|
|
||||||
const baseUrl = nocodbConfig.baseUrl.replace(/\/+$/, ''); // Enlever les trailing slashes
|
|
||||||
const tableUrl = `${baseUrl}/api/v1/db/data/${nocodbConfig.projectId}/${group.nocodbTableName}`;
|
|
||||||
|
|
||||||
// Construire les conditions de recherche
|
|
||||||
const invoiceColumn = group.invoiceColumnName || 'invoice_reference';
|
|
||||||
const supplierColumn = group.nocodbSupplierColumnName || 'supplier';
|
|
||||||
|
|
||||||
let whereClause = `(${invoiceColumn},eq,${invoiceReference})`;
|
|
||||||
if (supplierName && supplierColumn) {
|
|
||||||
whereClause += `~and(${supplierColumn},eq,${supplierName})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchUrl = `${tableUrl}?where=${encodeURIComponent(whereClause)}&limit=1`;
|
|
||||||
|
|
||||||
console.log(`🌐 Production NocoDB URL: ${searchUrl}`);
|
|
||||||
|
|
||||||
// Faire l'appel API
|
|
||||||
const response = await fetch(searchUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'xc-token': nocodbConfig.apiToken,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`NocoDB API Error: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log(`✅ Production NocoDB Response:`, data);
|
|
||||||
|
|
||||||
const exists = data.list && data.list.length > 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
exists,
|
|
||||||
matchType: 'direct',
|
|
||||||
invoiceReference
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recherche BL avec NocoDB
|
|
||||||
private async searchBLWithNocoDB(
|
|
||||||
groupId: number,
|
|
||||||
blNumber: string,
|
|
||||||
supplierName?: string
|
|
||||||
): Promise<VerificationResult> {
|
|
||||||
// Récupérer la configuration du groupe
|
|
||||||
const group = await this.storage.getGroup(groupId);
|
|
||||||
if (!group || !group.nocodbConfigId || !group.nocodbTableName) {
|
|
||||||
throw new Error(`Configuration NocoDB manquante pour le groupe ${groupId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer la configuration NocoDB
|
|
||||||
const nocodbConfig = await this.storage.getNocodbConfig(group.nocodbConfigId);
|
|
||||||
if (!nocodbConfig || !nocodbConfig.isActive) {
|
|
||||||
throw new Error(`Configuration NocoDB inactive ou introuvable (ID: ${group.nocodbConfigId})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🌐 Production NocoDB BL Search - Table: ${group.nocodbTableName}, BL: ${blNumber}`);
|
|
||||||
|
|
||||||
// Construire la requête NocoDB pour recherche BL
|
|
||||||
const baseUrl = nocodbConfig.baseUrl.replace(/\/+$/, '');
|
|
||||||
const tableUrl = `${baseUrl}/api/v1/db/data/${nocodbConfig.projectId}/${group.nocodbTableName}`;
|
|
||||||
|
|
||||||
const blColumn = group.nocodbBlColumnName || 'bl_number';
|
|
||||||
const supplierColumn = group.nocodbSupplierColumnName || 'supplier';
|
|
||||||
const invoiceColumn = group.invoiceColumnName || 'invoice_reference';
|
|
||||||
const amountColumn = group.nocodbAmountColumnName || 'amount';
|
|
||||||
|
|
||||||
let whereClause = `(${blColumn},eq,${blNumber})`;
|
|
||||||
if (supplierName && supplierColumn) {
|
|
||||||
whereClause += `~and(${supplierColumn},eq,${supplierName})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchUrl = `${tableUrl}?where=${encodeURIComponent(whereClause)}&limit=1`;
|
|
||||||
|
|
||||||
console.log(`🌐 Production NocoDB BL URL: ${searchUrl}`);
|
|
||||||
|
|
||||||
// Faire l'appel API
|
|
||||||
const response = await fetch(searchUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'xc-token': nocodbConfig.apiToken,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`NocoDB API Error: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log(`✅ Production NocoDB BL Response:`, data);
|
|
||||||
|
|
||||||
if (data.list && data.list.length > 0) {
|
|
||||||
const record = data.list[0];
|
|
||||||
return {
|
|
||||||
exists: true,
|
|
||||||
matchType: 'bl_search',
|
|
||||||
invoiceReference: record[invoiceColumn],
|
|
||||||
invoiceAmount: parseFloat(record[amountColumn]) || undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
exists: false,
|
|
||||||
matchType: 'bl_search'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sauvegarder dans les caches
|
|
||||||
private async saveToCaches(
|
|
||||||
groupId: number,
|
|
||||||
reference: string,
|
|
||||||
supplierName: string | undefined,
|
|
||||||
result: VerificationResult,
|
|
||||||
context: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
// 1. Sauvegarder dans le cache avec expiration
|
|
||||||
const cacheKey = context.startsWith('bl_')
|
|
||||||
? `bl_${groupId}_${reference}_${supplierName || 'any'}`
|
|
||||||
: `${groupId}_${reference}_${supplierName || 'any'}`;
|
|
||||||
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setHours(expiresAt.getHours() + 1); // Cache 1 heure
|
|
||||||
|
|
||||||
await this.storage.createInvoiceVerificationCache({
|
|
||||||
cacheKey,
|
|
||||||
groupId,
|
|
||||||
invoiceReference: reference,
|
|
||||||
supplierName: supplierName || null,
|
|
||||||
exists: result.exists,
|
|
||||||
matchType: result.matchType,
|
|
||||||
errorMessage: result.errorMessage || null,
|
|
||||||
cacheHit: false,
|
|
||||||
apiCallTime: result.invoiceAmount ? Math.round(result.invoiceAmount) : undefined,
|
|
||||||
expiresAt
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Si c'est une vérification de livraison réussie, sauvegarder aussi dans les vérifications permanentes
|
|
||||||
if (context === 'delivery_verification' && result.exists) {
|
|
||||||
// On a besoin du deliveryId, pour l'instant on utilise 0 comme placeholder
|
|
||||||
await this.storage.createInvoiceVerification({
|
|
||||||
deliveryId: 0, // Sera mis à jour par l'API route
|
|
||||||
groupId,
|
|
||||||
invoiceReference: reference,
|
|
||||||
supplierName: supplierName || null,
|
|
||||||
exists: result.exists,
|
|
||||||
matchType: result.matchType,
|
|
||||||
isValid: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`💾 Production Cache Saved - Key: ${cacheKey}, Exists: ${result.exists}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Production Cache Save Error:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+19
-177
@@ -10,8 +10,6 @@ import {
|
|||||||
customerOrders,
|
customerOrders,
|
||||||
dlcProducts,
|
dlcProducts,
|
||||||
tasks,
|
tasks,
|
||||||
invoiceVerificationCache,
|
|
||||||
invoiceVerifications,
|
|
||||||
type User,
|
type User,
|
||||||
type UpsertUser,
|
type UpsertUser,
|
||||||
type Group,
|
type Group,
|
||||||
@@ -43,10 +41,6 @@ import {
|
|||||||
type DlcProductFrontend,
|
type DlcProductFrontend,
|
||||||
type InsertDlcProductFrontend,
|
type InsertDlcProductFrontend,
|
||||||
type DlcProductWithRelations,
|
type DlcProductWithRelations,
|
||||||
type InvoiceVerificationCache,
|
|
||||||
type InsertInvoiceVerificationCache,
|
|
||||||
type InvoiceVerification,
|
|
||||||
type InsertInvoiceVerification,
|
|
||||||
type Task,
|
type Task,
|
||||||
type InsertTask,
|
type InsertTask,
|
||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
@@ -158,12 +152,6 @@ export interface IStorage {
|
|||||||
updateTask(id: number, task: Partial<InsertTask>): Promise<Task>;
|
updateTask(id: number, task: Partial<InsertTask>): Promise<Task>;
|
||||||
deleteTask(id: number): Promise<void>;
|
deleteTask(id: number): Promise<void>;
|
||||||
completeTask(id: number, completedBy?: string): Promise<void>;
|
completeTask(id: number, completedBy?: string): Promise<void>;
|
||||||
|
|
||||||
// Invoice Verification operations - Tables existantes en production
|
|
||||||
getInvoiceVerifications(deliveryId?: number): Promise<InvoiceVerification[]>;
|
|
||||||
createInvoiceVerification(verification: InsertInvoiceVerification): Promise<InvoiceVerification>;
|
|
||||||
getInvoiceVerificationCache(cacheKey: string): Promise<InvoiceVerificationCache | undefined>;
|
|
||||||
createInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DatabaseStorage implements IStorage {
|
export class DatabaseStorage implements IStorage {
|
||||||
@@ -1085,26 +1073,19 @@ export class DatabaseStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getNocodbConfig(id: number): Promise<NocodbConfig | undefined> {
|
async getNocodbConfig(id: number): Promise<NocodbConfig | undefined> {
|
||||||
try {
|
const configs = await db.select({
|
||||||
const configs = await db.select({
|
id: nocodbConfig.id,
|
||||||
id: nocodbConfig.id,
|
name: nocodbConfig.name,
|
||||||
name: nocodbConfig.name,
|
baseUrl: nocodbConfig.baseUrl,
|
||||||
baseUrl: nocodbConfig.baseUrl,
|
projectId: nocodbConfig.projectId,
|
||||||
projectId: nocodbConfig.projectId,
|
apiToken: nocodbConfig.apiToken,
|
||||||
apiToken: nocodbConfig.apiToken,
|
description: nocodbConfig.description,
|
||||||
description: nocodbConfig.description,
|
isActive: nocodbConfig.isActive,
|
||||||
isActive: nocodbConfig.isActive,
|
createdBy: nocodbConfig.createdBy,
|
||||||
createdBy: nocodbConfig.createdBy,
|
createdAt: nocodbConfig.createdAt,
|
||||||
createdAt: nocodbConfig.createdAt,
|
updatedAt: nocodbConfig.updatedAt,
|
||||||
updatedAt: nocodbConfig.updatedAt,
|
}).from(nocodbConfig).where(eq(nocodbConfig.id, id));
|
||||||
}).from(nocodbConfig).where(eq(nocodbConfig.id, id));
|
return configs[0];
|
||||||
|
|
||||||
console.log(`🔍 Production NocoDB Config Query - ID: ${id}, Found: ${configs.length > 0}`);
|
|
||||||
return configs[0];
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Erreur récupération config NocoDB ${id}:`, error);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNocodbConfig(config: InsertNocodbConfig): Promise<NocodbConfig> {
|
async createNocodbConfig(config: InsertNocodbConfig): Promise<NocodbConfig> {
|
||||||
@@ -2038,52 +2019,6 @@ export class DatabaseStorage implements IStorage {
|
|||||||
})
|
})
|
||||||
.where(eq(tasks.id, id));
|
.where(eq(tasks.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoice Verification operations - Utilise les tables existantes en production
|
|
||||||
async getInvoiceVerifications(deliveryId?: number): Promise<InvoiceVerification[]> {
|
|
||||||
const query = db.select().from(invoiceVerifications);
|
|
||||||
if (deliveryId) {
|
|
||||||
return await query.where(eq(invoiceVerifications.deliveryId, deliveryId));
|
|
||||||
}
|
|
||||||
return await query.orderBy(desc(invoiceVerifications.verifiedAt));
|
|
||||||
}
|
|
||||||
|
|
||||||
async createInvoiceVerification(verification: InsertInvoiceVerification): Promise<InvoiceVerification> {
|
|
||||||
const [newVerification] = await db
|
|
||||||
.insert(invoiceVerifications)
|
|
||||||
.values({
|
|
||||||
...verification,
|
|
||||||
verifiedAt: new Date(),
|
|
||||||
lastCheckedAt: new Date(),
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
return newVerification;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getInvoiceVerificationCache(cacheKey: string): Promise<InvoiceVerificationCache | undefined> {
|
|
||||||
const [cache] = await db
|
|
||||||
.select()
|
|
||||||
.from(invoiceVerificationCache)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(invoiceVerificationCache.cacheKey, cacheKey),
|
|
||||||
sql`expires_at > NOW()`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache> {
|
|
||||||
const [newCache] = await db
|
|
||||||
.insert(invoiceVerificationCache)
|
|
||||||
.values({
|
|
||||||
...cache,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
return newCache;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple in-memory storage for development
|
// Simple in-memory storage for development
|
||||||
@@ -2118,43 +2053,11 @@ 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 with NocoDB configuration
|
// Add test groups
|
||||||
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: 1,
|
{ id: 2, name: 'Magasin B', color: '#33FF57', webhookUrl: 'https://webhook.site/test-facture-magasin-b', createdAt: new Date(), updatedAt: new Date() },
|
||||||
name: 'Magasin A',
|
{ id: 3, name: 'Magasin C', color: '#3357FF', createdAt: new Date(), updatedAt: new Date() }
|
||||||
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));
|
||||||
|
|
||||||
@@ -2318,9 +2221,7 @@ 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> {
|
async getGroup(id: number): Promise<Group | undefined> { return 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> {}
|
||||||
@@ -2548,20 +2449,7 @@ 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(id: number): Promise<any> {
|
async getNocodbConfig(): Promise<any> { return null; }
|
||||||
// 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> {}
|
||||||
@@ -2622,52 +2510,6 @@ class MemStorage implements IStorage {
|
|||||||
return { userId, roleId, assignedBy, assignedAt: new Date() };
|
return { userId, roleId, assignedBy, assignedAt: new Date() };
|
||||||
}
|
}
|
||||||
async removeRoleFromUser(): Promise<void> {}
|
async removeRoleFromUser(): Promise<void> {}
|
||||||
|
|
||||||
// Invoice Verification operations - Stubs pour développement
|
|
||||||
async getInvoiceVerifications(deliveryId?: number): Promise<InvoiceVerification[]> {
|
|
||||||
console.log('📋 MemStorage - getInvoiceVerifications called (development mode)');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async createInvoiceVerification(verification: InsertInvoiceVerification): Promise<InvoiceVerification> {
|
|
||||||
console.log('📋 MemStorage - createInvoiceVerification called (development mode)');
|
|
||||||
return {
|
|
||||||
id: 1,
|
|
||||||
deliveryId: verification.deliveryId,
|
|
||||||
groupId: verification.groupId,
|
|
||||||
invoiceReference: verification.invoiceReference,
|
|
||||||
supplierName: verification.supplierName,
|
|
||||||
exists: verification.exists,
|
|
||||||
matchType: verification.matchType,
|
|
||||||
verifiedAt: new Date(),
|
|
||||||
isValid: true,
|
|
||||||
lastCheckedAt: new Date()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getInvoiceVerificationCache(cacheKey: string): Promise<InvoiceVerificationCache | undefined> {
|
|
||||||
console.log('📋 MemStorage - getInvoiceVerificationCache called (development mode)');
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache> {
|
|
||||||
console.log('📋 MemStorage - createInvoiceVerificationCache called (development mode)');
|
|
||||||
return {
|
|
||||||
id: 1,
|
|
||||||
cacheKey: cache.cacheKey,
|
|
||||||
groupId: cache.groupId,
|
|
||||||
invoiceReference: cache.invoiceReference,
|
|
||||||
supplierName: cache.supplierName,
|
|
||||||
exists: cache.exists,
|
|
||||||
matchType: cache.matchType,
|
|
||||||
errorMessage: cache.errorMessage,
|
|
||||||
cacheHit: cache.cacheHit,
|
|
||||||
apiCallTime: cache.apiCallTime,
|
|
||||||
expiresAt: cache.expiresAt,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use MemStorage in development, DatabaseStorage in production
|
// Use MemStorage in development, DatabaseStorage in production
|
||||||
|
|||||||
+6
-53
@@ -143,16 +143,16 @@ export const publicityParticipations = pgTable("publicity_participations", {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// NocoDB configuration globale - Structure correspondant à la production
|
// NocoDB configuration globale (une seule instance NocoDB partagée)
|
||||||
export const nocodbConfig = pgTable("nocodb_config", {
|
export const nocodbConfig = pgTable("nocodb_config", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
name: varchar("name", { length: 255 }).notNull(), // Nom de la configuration
|
name: varchar("name").notNull(), // Nom de la configuration
|
||||||
baseUrl: varchar("base_url", { length: 255 }).notNull(), // URL de l'instance NocoDB
|
baseUrl: varchar("base_url").notNull(), // URL de l'instance NocoDB
|
||||||
projectId: varchar("project_id", { length: 255 }).notNull(), // NocoDB Project ID
|
projectId: varchar("project_id").notNull(), // NocoDB Project ID
|
||||||
apiToken: varchar("api_token", { length: 255 }).notNull(), // Personal API Token
|
apiToken: varchar("api_token").notNull(), // Personal API Token
|
||||||
description: text("description"), // Description de la configuration
|
description: text("description"), // Description de la configuration
|
||||||
isActive: boolean("is_active").default(true), // Configuration active ou non
|
isActive: boolean("is_active").default(true), // Configuration active ou non
|
||||||
createdBy: varchar("created_by", { length: 255 }), // Créateur (nullable en production)
|
createdBy: varchar("created_by").notNull(),
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
});
|
});
|
||||||
@@ -217,37 +217,6 @@ export const dlcProducts = pgTable("dlc_products", {
|
|||||||
updatedAt: timestamp("updated_at").defaultNow(), // Date de mise à jour
|
updatedAt: timestamp("updated_at").defaultNow(), // Date de mise à jour
|
||||||
});
|
});
|
||||||
|
|
||||||
// Invoice Verification Cache - Table existante en production
|
|
||||||
export const invoiceVerificationCache = pgTable("invoice_verification_cache", {
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
cacheKey: varchar("cache_key", { length: 255 }).notNull(),
|
|
||||||
groupId: integer("group_id").notNull(),
|
|
||||||
invoiceReference: varchar("invoice_reference", { length: 255 }).notNull(),
|
|
||||||
supplierName: varchar("supplier_name", { length: 255 }),
|
|
||||||
exists: boolean("exists").notNull(),
|
|
||||||
matchType: varchar("match_type", { length: 50 }).notNull(),
|
|
||||||
errorMessage: text("error_message"),
|
|
||||||
cacheHit: boolean("cache_hit").default(false),
|
|
||||||
apiCallTime: integer("api_call_time"),
|
|
||||||
expiresAt: timestamp("expires_at").notNull(),
|
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invoice Verifications - Table existante en production
|
|
||||||
export const invoiceVerifications = pgTable("invoice_verifications", {
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
deliveryId: integer("delivery_id").notNull(),
|
|
||||||
groupId: integer("group_id").notNull(),
|
|
||||||
invoiceReference: varchar("invoice_reference", { length: 255 }).notNull(),
|
|
||||||
supplierName: varchar("supplier_name", { length: 255 }),
|
|
||||||
exists: boolean("exists").notNull(),
|
|
||||||
matchType: varchar("match_type", { length: 50 }),
|
|
||||||
verifiedAt: timestamp("verified_at").defaultNow(),
|
|
||||||
isValid: boolean("is_valid").default(true),
|
|
||||||
lastCheckedAt: timestamp("last_checked_at").defaultNow(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tasks table
|
// Tasks table
|
||||||
export const tasks = pgTable("tasks", {
|
export const tasks = pgTable("tasks", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
@@ -512,18 +481,6 @@ export const insertDlcProductFrontendSchema = insertDlcProductSchema
|
|||||||
.omit({ expiryDate: true })
|
.omit({ expiryDate: true })
|
||||||
.extend({ dlcDate: z.coerce.date() });
|
.extend({ dlcDate: z.coerce.date() });
|
||||||
|
|
||||||
export const insertInvoiceVerificationCacheSchema = createInsertSchema(invoiceVerificationCache).omit({
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const insertInvoiceVerificationsSchema = createInsertSchema(invoiceVerifications).omit({
|
|
||||||
id: true,
|
|
||||||
verifiedAt: true,
|
|
||||||
lastCheckedAt: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const insertTaskSchema = createInsertSchema(tasks).omit({
|
export const insertTaskSchema = createInsertSchema(tasks).omit({
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
@@ -559,10 +516,6 @@ export type DlcProduct = typeof dlcProducts.$inferSelect;
|
|||||||
export type InsertDlcProduct = z.infer<typeof insertDlcProductSchema>;
|
export type InsertDlcProduct = z.infer<typeof insertDlcProductSchema>;
|
||||||
export type DlcProductFrontend = Omit<DlcProduct, 'expiryDate'> & { dlcDate: Date };
|
export type DlcProductFrontend = Omit<DlcProduct, 'expiryDate'> & { dlcDate: Date };
|
||||||
export type InsertDlcProductFrontend = z.infer<typeof insertDlcProductFrontendSchema>;
|
export type InsertDlcProductFrontend = z.infer<typeof insertDlcProductFrontendSchema>;
|
||||||
export type InvoiceVerificationCache = typeof invoiceVerificationCache.$inferSelect;
|
|
||||||
export type InsertInvoiceVerificationCache = z.infer<typeof insertInvoiceVerificationCacheSchema>;
|
|
||||||
export type InvoiceVerification = typeof invoiceVerifications.$inferSelect;
|
|
||||||
export type InsertInvoiceVerification = z.infer<typeof insertInvoiceVerificationsSchema>;
|
|
||||||
export type Task = typeof tasks.$inferSelect;
|
export type Task = typeof tasks.$inferSelect;
|
||||||
export type InsertTask = z.infer<typeof insertTaskSchema>;
|
export type InsertTask = z.infer<typeof insertTaskSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
# Guide Complet - Configuration NocoDB Production
|
|
||||||
|
|
||||||
## ❌ PROBLÈME IDENTIFIÉ
|
|
||||||
|
|
||||||
Votre serveur de production n'a **aucune configuration NocoDB** dans la base de données. C'est pourquoi vous ne voyez rien.
|
|
||||||
|
|
||||||
## ✅ SOLUTION : Configuration Complète
|
|
||||||
|
|
||||||
### Étape 1 : Ajout Configuration NocoDB
|
|
||||||
|
|
||||||
Connectez-vous à votre base PostgreSQL de production et exécutez :
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- 1. Vérifier si la table existe
|
|
||||||
\d nocodb_config
|
|
||||||
|
|
||||||
-- 2. Insérer votre configuration NocoDB
|
|
||||||
INSERT INTO nocodb_config (
|
|
||||||
name,
|
|
||||||
base_url,
|
|
||||||
project_id,
|
|
||||||
api_token,
|
|
||||||
description,
|
|
||||||
is_active
|
|
||||||
) VALUES (
|
|
||||||
'Production Principal',
|
|
||||||
'https://app.nocodb.com', -- ou votre URL NocoDB
|
|
||||||
'VOTRE_PROJECT_ID', -- À remplacer
|
|
||||||
'VOTRE_API_TOKEN', -- À remplacer
|
|
||||||
'Configuration principale pour vérification automatique factures',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 3. Vérifier l'insertion
|
|
||||||
SELECT * FROM nocodb_config;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Étape 2 : Configuration des Magasins
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Lister vos magasins existants
|
|
||||||
SELECT id, name FROM groups;
|
|
||||||
|
|
||||||
-- Configurer chaque magasin (exemple pour ID 1)
|
|
||||||
UPDATE groups SET
|
|
||||||
nocodb_config_id = 1, -- ID de la config créée
|
|
||||||
nocodb_table_name = 'VOTRE_TABLE_FACTURES', -- Nom réel de votre table NocoDB
|
|
||||||
invoice_column_name = 'reference_facture', -- Colonne référence
|
|
||||||
nocodb_bl_column_name = 'numero_bl', -- Colonne BL
|
|
||||||
nocodb_amount_column_name = 'montant', -- Colonne montant
|
|
||||||
nocodb_supplier_column_name = 'fournisseur' -- Colonne fournisseur
|
|
||||||
WHERE id = 1; -- Remplacer par l'ID réel
|
|
||||||
|
|
||||||
-- Répéter pour chaque magasin avec ses propres paramètres
|
|
||||||
```
|
|
||||||
|
|
||||||
### Étape 3 : Obtenir les Informations NocoDB
|
|
||||||
|
|
||||||
1. **Project ID** : Dans NocoDB, aller dans Project Settings
|
|
||||||
2. **API Token** : Account Settings > Tokens > Create new token
|
|
||||||
3. **Table Names** : Noter les noms exacts de vos tables
|
|
||||||
4. **Column Names** : Noter les noms exacts de vos colonnes
|
|
||||||
|
|
||||||
### Étape 4 : Test de Configuration
|
|
||||||
|
|
||||||
Une fois configuré, redémarrez votre serveur et vous devriez voir :
|
|
||||||
- Boutons loupe et coche dans le rapprochement
|
|
||||||
- Logs de débogage dans la console serveur
|
|
||||||
- Notifications de succès/erreur
|
|
||||||
|
|
||||||
### Étape 5 : Vérification
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Voir toutes les configurations
|
|
||||||
SELECT
|
|
||||||
g.id,
|
|
||||||
g.name as magasin,
|
|
||||||
g.nocodb_config_id,
|
|
||||||
g.nocodb_table_name,
|
|
||||||
g.invoice_column_name,
|
|
||||||
nc.base_url,
|
|
||||||
nc.is_active
|
|
||||||
FROM groups g
|
|
||||||
LEFT JOIN nocodb_config nc ON g.nocodb_config_id = nc.id;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Structure Attendue de vos Tables NocoDB
|
|
||||||
|
|
||||||
Chaque table de factures doit contenir au minimum :
|
|
||||||
- **Référence Facture** (ex: FAC-2024-001)
|
|
||||||
- **Numéro BL** (ex: BL123456)
|
|
||||||
- **Montant** (ex: 150.75)
|
|
||||||
- **Fournisseur** (ex: "Fournisseur ABC")
|
|
||||||
|
|
||||||
## 🚨 Points Critiques
|
|
||||||
|
|
||||||
1. **API Token** : Doit avoir accès lecture à vos tables
|
|
||||||
2. **Noms Exacts** : Table/colonne names doivent être exacts (sensible à la casse)
|
|
||||||
3. **Données Test** : Ayez quelques factures test dans NocoDB
|
|
||||||
4. **Connexion** : Vérifiez que votre serveur peut accéder à NocoDB
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
Si ça ne marche toujours pas après configuration :
|
|
||||||
1. Vérifiez les logs serveur Node.js
|
|
||||||
2. Testez l'API NocoDB manuellement avec curl
|
|
||||||
3. Confirmez que les noms de colonnes sont corrects
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# Configuration NocoDB pour Production
|
|
||||||
|
|
||||||
## 1. Configuration de la base de données
|
|
||||||
|
|
||||||
Votre table `nocodb_config` existe déjà. Vous devez y ajouter une configuration :
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Insertion d'une configuration NocoDB de base
|
|
||||||
INSERT INTO nocodb_config (
|
|
||||||
name,
|
|
||||||
base_url,
|
|
||||||
project_id,
|
|
||||||
api_token,
|
|
||||||
description,
|
|
||||||
is_active,
|
|
||||||
created_by,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES (
|
|
||||||
'Configuration Principale',
|
|
||||||
'https://votre-nocodb.domain.com',
|
|
||||||
'votre-project-id',
|
|
||||||
'votre-api-token-personnel',
|
|
||||||
'Configuration principale pour vérification factures',
|
|
||||||
true,
|
|
||||||
'admin',
|
|
||||||
CURRENT_TIMESTAMP,
|
|
||||||
CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Configuration des groupes (magasins)
|
|
||||||
|
|
||||||
Mettez à jour vos magasins avec les configurations NocoDB :
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Exemple pour Magasin A
|
|
||||||
UPDATE groups SET
|
|
||||||
nocodb_config_id = 1, -- ID de la config NocoDB créée ci-dessus
|
|
||||||
nocodb_table_name = 'nom_table_factures_magasin_a', -- Nom de votre table NocoDB
|
|
||||||
invoice_column_name = 'reference_facture', -- Colonne référence facture
|
|
||||||
nocodb_bl_column_name = 'numero_bl', -- Colonne numéro BL
|
|
||||||
nocodb_amount_column_name = 'montant', -- Colonne montant
|
|
||||||
nocodb_supplier_column_name = 'fournisseur' -- Colonne fournisseur
|
|
||||||
WHERE id = 1; -- Remplacer par l'ID de votre magasin
|
|
||||||
|
|
||||||
-- Répéter pour chaque magasin avec ses propres noms de table/colonnes
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. API Token NocoDB
|
|
||||||
|
|
||||||
Pour obtenir votre API Token :
|
|
||||||
1. Allez dans NocoDB > Account Settings > Tokens
|
|
||||||
2. Créez un nouveau Personal API Token
|
|
||||||
3. Copiez le token dans la configuration ci-dessus
|
|
||||||
|
|
||||||
## 4. Structure de votre table NocoDB
|
|
||||||
|
|
||||||
Assurez-vous que vos tables NocoDB contiennent ces colonnes :
|
|
||||||
- Référence facture (ex: `reference_facture`)
|
|
||||||
- Numéro BL (ex: `numero_bl`)
|
|
||||||
- Montant (ex: `montant`)
|
|
||||||
- Fournisseur (ex: `fournisseur`)
|
|
||||||
|
|
||||||
## 5. Test de la configuration
|
|
||||||
|
|
||||||
Une fois configuré, le système :
|
|
||||||
- ✅ Affichera des boutons de vérification dans le rapprochement BL/Factures
|
|
||||||
- ✅ Vérifiera automatiquement les références factures existantes
|
|
||||||
- ✅ Auto-remplira les champs manquants en recherchant par BL
|
|
||||||
- ✅ Affichera des coches vertes pour les factures vérifiées
|
|
||||||
|
|
||||||
## 6. Débogage
|
|
||||||
|
|
||||||
Les logs de production afficheront :
|
|
||||||
```
|
|
||||||
🔍 Vérification facture: {groupId: 1, invoiceReference: "FAC123", supplierName: "Fournisseur A"}
|
|
||||||
🌐 Requête NocoDB: https://votre-nocodb.domain.com/api/v1/db/data/project-id/table-name?where=...
|
|
||||||
✅ Réponse NocoDB: {count: 1}
|
|
||||||
```
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# Test de la Configuration Production
|
|
||||||
|
|
||||||
## Système Mis en Place
|
|
||||||
|
|
||||||
✅ **Service de Vérification Production** :
|
|
||||||
- Utilise vos tables `INVOICE_VERIFICATION_CACHE` et `INVOICE_VERIFICATIONS` pour le cache
|
|
||||||
- Fait les appels réels à NocoDB pour récupérer les données factures
|
|
||||||
- Sauvegarde les résultats dans le cache (1 heure d'expiration)
|
|
||||||
|
|
||||||
✅ **Interface Utilisateur** :
|
|
||||||
- Boutons de vérification (loupe et coche) dans le rapprochement BL/Factures
|
|
||||||
- Coches vertes pour les factures vérifiées
|
|
||||||
- Messages d'erreur explicites
|
|
||||||
|
|
||||||
## Configuration Requise sur Votre Serveur
|
|
||||||
|
|
||||||
**1. Configuration NocoDB :**
|
|
||||||
```sql
|
|
||||||
-- Insérer votre configuration
|
|
||||||
INSERT INTO nocodb_config (
|
|
||||||
name, base_url, project_id, api_token, description, is_active
|
|
||||||
) VALUES (
|
|
||||||
'Production NocoDB',
|
|
||||||
'https://app.nocodb.com', -- Votre URL NocoDB
|
|
||||||
'p_votre_project_id', -- Votre Project ID
|
|
||||||
'nc_votre_api_token', -- Votre API Token
|
|
||||||
'Configuration production pour vérification factures',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Configuration Groupes/Magasins :**
|
|
||||||
```sql
|
|
||||||
-- Pour chaque magasin
|
|
||||||
UPDATE groups SET
|
|
||||||
nocodb_config_id = 1, -- ID de votre config NocoDB
|
|
||||||
nocodb_table_name = 'nom_de_votre_table_factures',
|
|
||||||
invoice_column_name = 'nom_colonne_reference_facture',
|
|
||||||
nocodb_bl_column_name = 'nom_colonne_numero_bl',
|
|
||||||
nocodb_amount_column_name = 'nom_colonne_montant',
|
|
||||||
nocodb_supplier_column_name = 'nom_colonne_fournisseur'
|
|
||||||
WHERE id = 1; -- ID de votre magasin
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test du Système
|
|
||||||
|
|
||||||
Une fois configuré, vous devriez voir :
|
|
||||||
1. **Boutons** : loupe et coche dans la colonne "Actions" du rapprochement
|
|
||||||
2. **Vérification** : cliquer la loupe vérifie la facture dans NocoDB
|
|
||||||
3. **Auto-remplissage** : cliquer la coche recherche par BL et remplit automatiquement
|
|
||||||
4. **Coches vertes** : apparaissent quand la vérification réussit
|
|
||||||
5. **Logs** : dans la console serveur vous verrez les appels NocoDB
|
|
||||||
|
|
||||||
## Logs de Débogage
|
|
||||||
|
|
||||||
Surveillez ces logs dans votre console serveur :
|
|
||||||
```
|
|
||||||
🔍 Production Verification - Invoice: FAC123, Group: 1, Supplier: Fournisseur A
|
|
||||||
🌐 Production NocoDB Call - Table: ma_table_factures, Invoice: FAC123
|
|
||||||
🌐 Production NocoDB URL: https://app.nocodb.com/api/v1/db/data/...
|
|
||||||
✅ Production NocoDB Response: {list: [...]}
|
|
||||||
💾 Production Cache Saved - Key: 1_FAC123_Fournisseur A, Exists: true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dépannage
|
|
||||||
|
|
||||||
Si ça ne marche pas :
|
|
||||||
1. **Pas de boutons** : Configuration manquante dans la base
|
|
||||||
2. **Erreurs NocoDB** : Vérifiez l'API token et les noms de tables/colonnes
|
|
||||||
3. **Pas de coches** : Vérifiez les logs serveur pour voir les erreurs
|
|
||||||
Reference in new issue
Block a user