import type { Express, Request, Response } from "express"; import { createServer, type Server } from "http"; import { storage } from "./storage"; import { setupLocalAuth, requireAuth } from "./localAuth"; import { requireModulePermission, requireAdmin, requirePermission } from "./permissions"; import { stripSmtpPassword } from "./sanitize"; import { normalizeDateString } from "./dateUtils"; import { registerExternalApi } from "./externalApi"; // Corps de requête sans les champs secrets : pour les logs uniquement function redactBody(body: any): any { if (!body || typeof body !== 'object') return body; const redacted = { ...body }; for (const key of ['smtpPassword', 'apiToken', 'password']) { if (key in redacted && redacted[key]) redacted[key] = '[REDACTED]'; } return redacted; } import { sendSupplierDocumentRequest, verifySmtpConfig, getMissingSmtpFields, } from "./emailService"; import { buildSupplierMailSubject } from "@shared/supplierMail"; import { db, pool } from "./db"; import { createRequire } from "module"; const require = createRequire(import.meta.url); // Suppression de l'import form-data car nous utilisons le FormData natif de Node.js 18+ // const FormData = require('form-data'); console.log('🔍 Using development storage and authentication'); // Simple hash password function using crypto async function hashPasswordSimple(password: string) { const crypto = await import('crypto'); const { promisify } = await import('util'); const scryptAsync = promisify(crypto.scrypt); const salt = crypto.randomBytes(16).toString("hex"); const buf = (await scryptAsync(password, salt, 64)) as Buffer; return `${buf.toString("hex")}.${salt}`; } // Alias pour compatibilité const isAuthenticated = requireAuth; const setupAuth = setupLocalAuth; import { insertGroupSchema, insertSupplierSchema, insertOrderSchema, insertDeliverySchema, insertUserGroupSchema, insertPublicitySchema, insertCustomerOrderSchema, insertCustomerOrderFrontendSchema, insertDlcProductSchema, insertDlcProductFrontendSchema, insertTaskSchema, insertAnnouncementSchema, insertNocodbConfigSchema, insertSavTicketSchema, insertWeatherDataSchema, insertWeatherSettingsSchema, insertWebhookBapConfigSchema, insertUtilitiesSchema, insertAvoirSchema, insertReconciliationCommentSchema, insertContactSchema, users, groups, userGroups, suppliers, orders, deliveries, publicities, publicityParticipations, customerOrders, nocodbConfig, dlcProducts, tasks, invoiceVerificationCache, dashboardMessages, webhookBapConfig, utilities, avoirs, type UserWithGroups } from "@shared/schema"; import { hasPermission } from "@shared/permissions"; import { z } from "zod"; import { eq, desc, or, isNull } from "drizzle-orm"; import { invoiceVerificationService } from "./invoiceVerification"; import { backupService } from "./backupService"; import { weatherService } from "./weatherService.js"; import fetch from "node-fetch"; // Utilisateur courant avec ses magasins. deserializeUser (localAuth.ts) le // recharge déjà depuis la base à chaque requête authentifiée : inutile de le // relire dans chaque handler. function getCurrentUser(req: any): UserWithGroups | undefined { return req.user; } export async function registerRoutes(app: Express): Promise { // Detect environment const environment = process.env.NODE_ENV || 'development'; console.log('🌍 Environment detected:', environment); // API externe de rapprochement (clé d'API, hors session) : docs/API-RAPPROCHEMENT.md registerExternalApi(app); // Le mot de passe SMTP des magasins ne doit jamais sortir du serveur. // Les objets "group" sont joints à de nombreuses réponses (livraisons, // commandes, utilisateurs...) : plutôt que de filtrer chaque requête, on // nettoie une seule fois à la sortie. Le client reçoit à la place un booléen // smtpPasswordSet lui indiquant si un mot de passe est enregistré. app.use('/api', (req, res, next) => { const originalJson = res.json.bind(res); res.json = (body: any) => originalJson(stripSmtpPassword(body)); // Données privées par magasin : jamais stockées par un cache partagé, et // toujours revalidées par le navigateur (les 304 via ETag restent possibles) res.set('Cache-Control', 'private, no-cache'); next(); }); // Health check endpoint for Docker app.get('/api/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString(), environment: process.env.NODE_ENV || 'development', database: 'connected' // We could add a real DB check here if needed }); }); // Auth middleware await setupAuth(app); // Routes pour configuration webhook BAP app.get('/api/webhook-bap-config', isAuthenticated, async (req: any, res) => { try { // Vérifier que l'utilisateur est admin const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); if (!user || user.role !== 'admin') { return res.status(403).json({ error: 'Accès refusé - Admin uniquement' }); } const config = await storage.getWebhookBapConfig(); res.json(config || null); } catch (error: any) { console.error('❌ Erreur récupération config webhook BAP:', error); // Si la table n'existe pas, retourner une configuration par défaut if (error.code === '42P01') { // Relation does not exist console.log('⚠️ Table webhook_bap_config n\'existe pas, retour config par défaut'); return res.json({ id: 1, name: "Configuration BAP", webhookUrl: "https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d", description: "Configuration par défaut (table non créée)", isActive: true, needsTableCreation: true }); } res.status(500).json({ error: 'Erreur serveur', details: error.message }); } }); app.post('/api/webhook-bap-config', isAuthenticated, async (req: any, res) => { try { // Vérifier que l'utilisateur est admin const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); if (!user || user.role !== 'admin') { return res.status(403).json({ error: 'Accès refusé - Admin uniquement' }); } // Valider les données avec Zod const validatedData = insertWebhookBapConfigSchema.parse(req.body); // Vérifier si une configuration existe déjà const existingConfig = await storage.getWebhookBapConfig(); let config: any; if (existingConfig) { // Mettre à jour la configuration existante config = await storage.updateWebhookBapConfig(existingConfig.id, validatedData); } else { // Créer une nouvelle configuration config = await storage.createWebhookBapConfig(validatedData); } console.log('✅ Configuration webhook BAP sauvegardée:', { id: config.id, name: config.name, isActive: config.isActive }); res.json(config); } catch (error: any) { console.error('❌ Erreur sauvegarde config webhook BAP:', error); res.status(500).json({ error: 'Erreur serveur', details: error.message }); } }); app.post('/api/webhook-bap-config/test', isAuthenticated, async (req: any, res) => { try { // Vérifier que l'utilisateur est admin const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); if (!user || user.role !== 'admin') { return res.status(403).json({ error: 'Accès refusé - Admin uniquement' }); } const { webhookUrl } = req.body; if (!webhookUrl) { return res.status(400).json({ error: 'URL webhook requise' }); } console.log('🔍 Test webhook BAP:', { url: webhookUrl }); // Tester la connectivité avec le webhook const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 secondes const testResponse = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: true, timestamp: new Date().toISOString(), message: 'Test de connectivité depuis LogiFlow' }), signal: controller.signal }); clearTimeout(timeoutId); console.log('🌐 Réponse test webhook:', { status: testResponse.status, ok: testResponse.ok }); if (!testResponse.ok) { throw new Error(`HTTP ${testResponse.status}: ${testResponse.statusText}`); } const result = await testResponse.text(); res.json({ success: true, status: testResponse.status, message: 'Webhook accessible', response: result.substring(0, 200) // Limiter la réponse }); } catch (error: any) { console.error('❌ Erreur test webhook BAP:', error); let errorMessage = 'Échec du test de connectivité'; if (error.name === 'AbortError') { errorMessage = 'Timeout - Le webhook ne répond pas'; } else if (error.message) { errorMessage = error.message; } res.status(500).json({ error: errorMessage, details: error.message }); } }); // Routes pour configuration utilities app.get('/api/utilities', isAuthenticated, async (req: any, res) => { try { const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); if (!user || (user.role !== 'admin' && user.role !== 'directeur')) { return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' }); } const config = await storage.getUtilities(); res.json(config || null); } catch (error: any) { console.error('❌ Erreur récupération utilities:', error); res.status(500).json({ error: 'Erreur serveur', details: error.message }); } }); app.post('/api/utilities', isAuthenticated, async (req: any, res) => { try { const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); if (!user || (user.role !== 'admin' && user.role !== 'directeur')) { return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' }); } const validatedData = insertUtilitiesSchema.parse(req.body); const existingConfig = await storage.getUtilities(); let config: any; if (existingConfig) { config = await storage.updateUtilities(existingConfig.id, validatedData); } else { config = await storage.createUtilities(validatedData); } console.log('✅ Configuration utilities sauvegardée:', { id: config.id, salesAnalysisUrl: config.salesAnalysisUrl }); res.json(config); } catch (error: any) { console.error('❌ Erreur sauvegarde utilities:', error); res.status(500).json({ error: 'Erreur serveur', details: error.message }); } }); // Route pour récupérer l'échéancier des paiements fournisseurs app.get('/api/payment-schedule', isAuthenticated, async (req: any, res) => { try { const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); if (!user || (user.role !== 'admin' && user.role !== 'directeur')) { return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' }); } // Validation du groupId avec Zod const groupIdSchema = z.coerce.number().int().positive(); const validation = groupIdSchema.safeParse(req.query.groupId); if (!validation.success) { return res.status(400).json({ error: 'groupId invalide - doit être un entier positif' }); } const groupId = validation.data; // Vérifier l'autorisation : directeur ne peut voir que ses groupes if (user.role === 'directeur') { const userGroups = await storage.getUserGroups(user.id); const userGroupIds = userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(groupId)) { return res.status(403).json({ error: 'Accès refusé - Vous ne pouvez accéder qu\'aux données de votre groupe' }); } } // Récupérer le groupe const group = await storage.getGroup(groupId); if (!group) { return res.json({ schedules: [], message: 'Groupe non trouvé' }); } // Récupérer les livraisons du groupe (filtre magasin fait en base) const allDeliveries = await storage.getDeliveries([groupId]); const groupDeliveries = allDeliveries.filter((d: any) => d.groupId === groupId && d.invoiceReference); // Séparer les livraisons avec et sans dueDate const deliveriesWithDueDate = groupDeliveries.filter((d: any) => d.dueDate); const deliveriesWithoutDueDate = groupDeliveries.filter((d: any) => !d.dueDate); // FALLBACK : Pour les livraisons sans dueDate ou sans TTC, interroger NocoDB const { InvoiceVerificationService } = await import('./invoiceVerification.js'); const verificationService = new InvoiceVerificationService(); // Traiter les livraisons sans dueDate for (const delivery of deliveriesWithoutDueDate) { try { const result = await verificationService.verifyInvoice( delivery.invoiceReference!, delivery.groupId, false, // Ne pas forcer le refresh, utiliser cache si disponible delivery.reconciled || false ); const updateData: any = {}; if (result.exists && result.dueDate) { // Normaliser la date avant de la stocker const normalizedDateString = normalizeDateString(result.dueDate); if (normalizedDateString) { const normalizedDate = new Date(normalizedDateString); updateData.dueDate = normalizedDate; delivery.dueDate = normalizedDate; } } // Récupérer aussi le montant si manquant if (result.exists && result.invoiceAmount && !delivery.invoiceAmount) { // @ts-ignore updateData.invoiceAmount = result.invoiceAmount.toString(); // @ts-ignore delivery.invoiceAmount = result.invoiceAmount.toString(); } // Mettre à jour si nécessaire if (Object.keys(updateData).length > 0) { await storage.updateDelivery(delivery.id, updateData); console.log(`📅 Fallback: données récupérées pour livraison #${delivery.id}`, updateData); } } catch (error) { console.error(`❌ Fallback échoué pour livraison #${delivery.id}:`, error); } } // Combiner toutes les livraisons qui ont maintenant une dueDate const allDeliveriesWithDueDate = [...deliveriesWithDueDate, ...deliveriesWithoutDueDate.filter((d: any) => d.dueDate)]; // FALLBACK TTC : Pour les livraisons qui ont une dueDate mais pas de TTC const deliveriesNeedingTTC = allDeliveriesWithDueDate.filter((d: any) => !d.invoiceAmountTTC || parseFloat(d.invoiceAmountTTC) === 0); for (const delivery of deliveriesNeedingTTC) { try { const result = await verificationService.verifyInvoice( delivery.invoiceReference!, delivery.groupId, false, delivery.reconciled || false ); if (result.exists && result.invoiceAmount) { const updates: any = { invoiceAmount: result.invoiceAmount.toString(), supplierName: result.supplierName }; if (result.dueDate) { updates.dueDate = new Date(result.dueDate); } await storage.updateDelivery(delivery.id, updates); // @ts-ignore delivery.invoiceAmount = result.invoiceAmount.toString(); console.log(`💰 Montant récupéré pour livraison #${delivery.id}: ${result.invoiceAmount}€`); } } catch (error) { console.error(`❌ Fallback TTC échoué pour livraison #${delivery.id}:`, error); } } // Récupérer tous les fournisseurs pour le mapping du mode de paiement const allSuppliers = await storage.getSuppliers(); const supplierMap = new Map(allSuppliers.map((s: any) => [s.id, s])); // Formatter les échéances const schedules = allDeliveriesWithDueDate.map((delivery: any) => { const supplier = supplierMap.get(delivery.supplierId); return { id: delivery.id, invoiceReference: delivery.invoiceReference, dueDate: delivery.dueDate, amount: delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0, amountTTC: delivery.invoiceAmountTTC ? parseFloat(delivery.invoiceAmountTTC) : 0, supplierName: supplier?.name || 'Fournisseur inconnu', paymentMethod: supplier?.paymentMethod || null, groupId: group.id, groupName: group.name }; }); res.json({ schedules }); } catch (error: any) { console.error('❌ Erreur récupération échéances:', error); res.status(500).json({ error: 'Erreur serveur', details: error.message }); } }); // Route pour exporter l'échéancier vers Excel app.post('/api/payment-schedule/export', isAuthenticated, async (req: any, res) => { try { console.log('📊 [EXPORT] Début de l\'export Excel'); const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { console.error('❌ [EXPORT] Utilisateur non authentifié'); return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); console.log(`🔍 [EXPORT] Utilisateur: ${user?.username} (${user?.role})`); if (!user || (user.role !== 'admin' && user.role !== 'directeur')) { console.error('❌ [EXPORT] Accès refusé'); return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' }); } // Récupérer les paramètres d'export const { groupId, month, paymentMethods, includeHT, includeTTC } = req.body; console.log('📋 [EXPORT] Paramètres:', { groupId, month, paymentMethods, includeHT, includeTTC }); // Validation const groupIdSchema = z.coerce.number().int().positive(); const monthSchema = z.string().regex(/^\d{4}-\d{2}$/); const paymentMethodsSchema = z.array(z.string()).min(1); const validatedGroupId = groupIdSchema.parse(groupId); const validatedMonth = monthSchema.parse(month); const validatedPaymentMethods = paymentMethodsSchema.parse(paymentMethods); // Vérifier l'autorisation pour les directeurs if (user.role === 'directeur') { const userGroups = await storage.getUserGroups(user.id); const userGroupIds = userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(validatedGroupId)) { return res.status(403).json({ error: 'Accès refusé - Vous ne pouvez accéder qu\'aux données de votre groupe' }); } } // Récupérer le groupe const group = await storage.getGroup(validatedGroupId); if (!group) { return res.status(404).json({ error: 'Groupe non trouvé' }); } // Récupérer les livraisons du groupe avec échéance (filtre magasin fait en base) const allDeliveries = await storage.getDeliveries([validatedGroupId]); const groupDeliveries = allDeliveries.filter((d: any) => d.groupId === validatedGroupId && d.invoiceReference && d.dueDate ); // Filtrer par mois const { parseISO, startOfMonth, endOfMonth, isWithinInterval, format } = await import('date-fns'); const { fr } = await import('date-fns/locale'); const monthStart = parseISO(`${validatedMonth}-01`); const monthEnd = endOfMonth(monthStart); const filteredDeliveries = groupDeliveries.filter((d: any) => { try { const dueDate = new Date(d.dueDate); return isWithinInterval(dueDate, { start: monthStart, end: monthEnd }); } catch { return false; } }); // Récupérer les fournisseurs const allSuppliers = await storage.getSuppliers(); const supplierMap = new Map(allSuppliers.map((s: any) => [s.id, s])); // Filtrer par modes de paiement sélectionnés const schedules = filteredDeliveries .map((delivery: any) => { const supplier = supplierMap.get(delivery.supplierId); return { dueDate: new Date(delivery.dueDate), supplierName: supplier?.name || 'Fournisseur inconnu', invoiceReference: delivery.invoiceReference, paymentMethod: supplier?.paymentMethod || 'Non défini', amountHT: delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0, amountTTC: delivery.invoiceAmountTTC ? parseFloat(delivery.invoiceAmountTTC) : 0, }; }) .filter((schedule: any) => validatedPaymentMethods.includes(schedule.paymentMethod)) .sort((a: any, b: any) => a.dueDate.getTime() - b.dueDate.getTime()); // Générer le fichier CSV avec point-virgule (séparateur Excel France) console.log('📄 [EXPORT] Génération du fichier CSV avec point-virgule...'); // Préparer les en-têtes const headers = ['Date d\'échéance', 'Fournisseur', 'Facture', 'Mode de paiement']; if (includeHT) headers.push('Montant HT'); if (includeTTC) headers.push('Montant TTC'); // Créer les lignes de données const csvLines: string[] = []; // Ajouter la ligne d'en-tête csvLines.push(headers.join(';')); // Ajouter les données schedules.forEach((schedule: any) => { const row: string[] = [ format(schedule.dueDate, 'dd/MM/yyyy', { locale: fr }), schedule.supplierName, schedule.invoiceReference, schedule.paymentMethod, ]; if (includeHT) row.push(schedule.amountHT.toFixed(2)); if (includeTTC) row.push(schedule.amountTTC.toFixed(2)); csvLines.push(row.join(';')); }); // Calculer les totaux const totalHT = schedules.reduce((sum: number, s: any) => sum + s.amountHT, 0); const totalTTC = schedules.reduce((sum: number, s: any) => sum + s.amountTTC, 0); // Ajouter une ligne de total const totalRow: string[] = ['', '', '', 'TOTAL']; if (includeHT) totalRow.push(totalHT.toFixed(2)); if (includeTTC) totalRow.push(totalTTC.toFixed(2)); csvLines.push(totalRow.join(';')); // Générer le contenu CSV avec BOM UTF-8 pour Excel const BOM = '\uFEFF'; const csvContent = BOM + csvLines.join('\n'); // Envoyer le fichier res.setHeader('Content-Disposition', `attachment; filename="echeancier_${validatedMonth}.csv"`); res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.send(csvContent); console.log(`📊 [EXPORT] CSV généré: ${schedules.length} échéances pour ${group.name}`); } catch (error: any) { console.error('❌ [EXPORT] Erreur export Excel:', error); console.error('❌ [EXPORT] Message:', error.message); console.error('❌ [EXPORT] Stack:', error.stack); res.status(500).json({ error: 'Erreur lors de l\'export', details: error.message, stack: process.env.NODE_ENV === 'development' ? error.stack : undefined }); } }); // Route BAP pour envoi webhook n8n app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => { try { console.log('🔍 BAP: Requête reçue', { hasUser: !!req.user, userType: typeof req.user, hasClaims: !!(req.user?.claims), hasId: !!(req.user?.id), bodyKeys: Object.keys(req.body || {}) }); // Vérifier que l'utilisateur est admin const userId = req.user?.claims?.sub || req.user?.id; console.log('🔍 BAP: User ID extracted:', userId); if (!userId) { console.error('❌ BAP: No user ID found'); return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); console.log('🔍 BAP: User found:', { id: user?.id, role: user?.role }); if (!user || user.role !== 'admin') { console.error('❌ BAP: Access denied', { user: user?.role }); return res.status(403).json({ error: 'Accès refusé - Admin uniquement' }); } // Récupérer les données JSON du body const { pdfBase64, fileName, recipient } = req.body; // Valider les données if (!pdfBase64 || !fileName || !recipient) { return res.status(400).json({ error: 'Données manquantes: pdfBase64, fileName ou recipient' }); } if (!['Laurie', 'Jeremy'].includes(recipient)) { return res.status(400).json({ error: 'Destinataire invalide' }); } // Vérifier l'extension PDF if (!fileName.toLowerCase().endsWith('.pdf')) { return res.status(400).json({ error: 'Le fichier doit être un PDF' }); } // Décoder le base64 en buffer let fileBuffer: Buffer; try { fileBuffer = Buffer.from(pdfBase64, 'base64'); } catch (error) { return res.status(400).json({ error: 'Format base64 invalide' }); } console.log('📤 BAP: Envoi webhook n8n', { recipient, fileName, fileSize: fileBuffer.length, userId: user.id }); // Préparer les données JSON pour le webhook n8n (sans form-data) const webhookPayload = { recipient: recipient, fileName: fileName, fileSize: fileBuffer.length, pdfBase64: pdfBase64, // On renvoie le base64 directement contentType: 'application/pdf' }; console.log('✅ BAP: Payload JSON préparé', { recipient, fileName, fileSize: fileBuffer.length }); // Récupérer l'URL du webhook depuis la configuration const webhookConfig = await storage.getWebhookBapConfig(); if (!webhookConfig || !webhookConfig.isActive) { console.error('❌ BAP: Configuration webhook non trouvée ou inactive'); return res.status(500).json({ error: 'Configuration webhook BAP non disponible' }); } const webhookUrl = webhookConfig.webhookUrl; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes console.log('🌐 BAP: Envoi vers webhook n8n (POST avec body JSON)...'); const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(webhookPayload), signal: controller.signal }); clearTimeout(timeoutId); console.log('🌐 BAP: Réponse webhook reçue', { status: response.status, ok: response.ok }); if (!response.ok) { const errorText = await response.text(); console.error('❌ BAP: Erreur webhook', { status: response.status, errorText }); throw new Error(`Erreur webhook: ${response.status} ${response.statusText}`); } const result = await response.text(); console.log('✅ BAP: Webhook n8n réussi', { recipient, result: result.substring(0, 100) }); res.json({ success: true, message: 'Fichier envoyé avec succès', webhookResponse: result }); } catch (error: any) { console.error('❌ BAP: Erreur complète:', { name: error.name, message: error.message, stack: error.stack?.substring(0, 500) }); let errorMessage = 'Erreur lors de l\'envoi du fichier'; if (error.name === 'AbortError') { errorMessage = 'Timeout - Le traitement a pris trop de temps'; } else if (error.message) { errorMessage = error.message; } res.status(500).json({ error: errorMessage, details: error.message }); } }); // Parser multipart/form-data manuellement (sans busboy/multer) function parseMultipart(buffer: Buffer, boundary: string) { const parts: any = {}; const boundaryBuffer = Buffer.from(`--${boundary}`); const sections = []; let start = 0; while (true) { const boundaryIndex = buffer.indexOf(boundaryBuffer, start); if (boundaryIndex === -1) break; if (start > 0) { sections.push(buffer.slice(start, boundaryIndex)); } start = boundaryIndex + boundaryBuffer.length; } for (const section of sections) { const headerEnd = section.indexOf('\r\n\r\n'); if (headerEnd === -1) continue; const headers = section.slice(0, headerEnd).toString(); const content = section.slice(headerEnd + 4, section.length - 2); const nameMatch = headers.match(/name="([^"]+)"/); const filenameMatch = headers.match(/filename="([^"]+)"/); const contentTypeMatch = headers.match(/Content-Type: ([^\r\n]+)/); if (!nameMatch) continue; const name = nameMatch[1]; if (filenameMatch) { parts[name] = { filename: filenameMatch[1], contentType: contentTypeMatch ? contentTypeMatch[1] : 'application/octet-stream', buffer: content }; } else { parts[name] = content.toString('utf-8'); } } return parts; } // Route proxy SIMPLE pour envoi de factures (admin + directeur) app.post('/api/reconciliation/send-invoice', isAuthenticated, async (req: any, res) => { try { const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { return res.status(401).json({ error: 'Non authentifié' }); } const user = getCurrentUser(req); if (!user || (user.role !== 'admin' && user.role !== 'directeur')) { return res.status(403).json({ error: 'Accès refusé' }); } // Parser le multipart/form-data manuellement const contentType = req.headers['content-type'] || ''; // Gestion plus robuste du boundary (avec ou sans guillemets) const boundaryMatch = contentType.match(/boundary="?([^";]+)"?/i); if (!boundaryMatch) { console.error('❌ INVOICE PROXY: Boundary manquant dans Content-Type', contentType); return res.status(400).json({ error: 'Format multipart invalide' }); } const boundary = boundaryMatch[1]; const chunks: Buffer[] = []; await new Promise((resolve, reject) => { req.on('data', (chunk: Buffer) => chunks.push(chunk)); req.on('end', () => resolve()); req.on('error', reject); }); const buffer = Buffer.concat(chunks); const parts = parseMultipart(buffer, boundary); if (!parts.file || !parts.webhookUrl) { return res.status(400).json({ error: 'Fichier ou webhook manquant' }); } console.log('📤 INVOICE PROXY: Envoi facture', { userId: user.id, role: user.role, fileName: parts.file.filename, size: parts.file.buffer.length }); // Utilisation du FormData natif de Node.js (global) // On utilise Blob pour transformer le Buffer en objet compatible const formData = new FormData(); const fileBlob = new Blob([parts.file.buffer], { type: parts.file.contentType }); formData.append('file', fileBlob, parts.file.filename); formData.append('supplier', parts.supplier || ''); formData.append('blNumber', parts.blNumber || ''); formData.append('type', parts.type || 'Facture'); console.log('🔗 INVOICE PROXY: Calling webhook URL:', parts.webhookUrl); // En utilisant le FormData natif avec fetch, pas besoin de headers manuels (boundary géré automatiquement) const response = await fetch(parts.webhookUrl, { method: 'POST', body: formData as any }); if (!response.ok) { const errorBody = await response.text().catch(() => 'No body'); console.error('❌ INVOICE PROXY: Webhook error', { status: response.status, statusText: response.statusText, body: errorBody }); return res.status(500).json({ error: `Webhook error: ${response.status}`, details: errorBody }); } console.log('✅ INVOICE PROXY: Success'); res.json({ success: true, message: 'Facture envoyée' }); } catch (error: any) { console.error('❌ INVOICE PROXY: Error', error); res.status(500).json({ error: error.message || 'Erreur serveur' }); } }); // Auth routes handled by authSwitch (local or Replit) // Groups routes app.get('/api/groups', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Only admin sees all groups, all other roles (manager, employee, directeur) see only their assigned groups if (user.role === 'admin') { const groups = await storage.getGroups(); res.json(groups); } else { const userGroups = (user as any).userGroups?.map((ug: any) => ug.group).filter(Boolean) || []; res.json(userGroups); } } catch (error) { console.error("Error fetching groups:", error); res.status(500).json({ message: "Failed to fetch groups" }); } }); app.post('/api/groups', isAuthenticated, async (req: any, res) => { try { // Debug logging pour la création de groupe console.log('📨 POST /api/groups - Headers:', { 'content-type': req.headers['content-type'], 'content-length': req.headers['content-length'], 'user-agent': req.headers['user-agent']?.substring(0, 50) + '...' }); console.log('📋 POST /api/groups - Request body:', JSON.stringify(redactBody(req.body), null, 2)); // Déterminer l'ID utilisateur selon l'environnement let userId; if (req.user.claims && req.user.claims.sub) { userId = req.user.claims.sub; // Production Replit Auth console.log('🔐 Using Replit Auth user ID:', userId); } else if (req.user.id) { userId = req.user.id; // Développement local console.log('🔐 Using local auth user ID:', userId); } else { console.error('❌ No user ID found in request:', { user: req.user }); return res.status(401).json({ message: "User authentication failed" }); } console.log('🔐 User requesting group creation:', userId); // Vérifier l'utilisateur const user = await storage.getUser(userId); if (!user) { console.error('❌ User not found:', userId); return res.status(404).json({ message: "User not found" }); } console.log('✅ User found:', { username: user.username, role: user.role }); // Vérifier les permissions if (user.role !== 'admin' && user.role !== 'manager') { console.error('❌ Insufficient permissions:', { userRole: user.role, required: ['admin', 'manager'] }); return res.status(403).json({ message: "Insufficient permissions" }); } console.log('✅ User has permission to create group'); // Valider les données console.log('🔍 Validating group data with schema...'); const data = insertGroupSchema.parse(req.body); console.log('✅ Group data validation passed:', data); // Créer le groupe console.log('🏪 Creating group in database...'); const group = await storage.createGroup(data); console.log('✅ Group creation successful:', { id: group.id, name: group.name }); res.json(group); } catch (error: any) { console.error('❌ Failed to create group:', { error: error?.message || 'Unknown error', stack: error?.stack, body: redactBody(req.body), userId: req.user?.id || req.user?.claims?.sub || 'unknown' }); // Erreur de validation Zod if (error?.name === 'ZodError') { console.error('❌ Validation error details:', error.errors); return res.status(400).json({ message: "Validation failed", errors: error.errors }); } res.status(500).json({ message: "Failed to create group" }); } }); app.put('/api/groups/:id', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || (user.role !== 'admin' && user.role !== 'manager')) { return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(req.params.id); const data = insertGroupSchema.partial().parse(req.body); // Le mot de passe SMTP n'est jamais renvoyé au client : un champ vide // signifie "inchangé", pas "effacer". On ne l'écrase que s'il est fourni. if (!data.smtpPassword) { delete (data as any).smtpPassword; } const group = await storage.updateGroup(id, data); res.json(group); } catch (error: any) { console.error("Error updating group:", error); res.status(500).json({ message: "Failed to update group" }); } }); // Test de la configuration SMTP d'un magasin (aucun message envoyé) app.post('/api/groups/:id/test-smtp', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || (user.role !== 'admin' && user.role !== 'manager')) { return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(req.params.id); const group = await storage.getGroup(id); if (!group) { return res.status(404).json({ message: "Magasin introuvable" }); } const missing = getMissingSmtpFields(group as any); if (missing.length > 0) { return res.status(400).json({ success: false, message: `Configuration incomplète : ${missing.join(', ')}` }); } await verifySmtpConfig(group as any); res.json({ success: true, message: "Connexion au serveur SMTP réussie" }); } catch (error: any) { console.error("Erreur test SMTP:", error); res.status(400).json({ success: false, message: error?.message || "Impossible de joindre le serveur SMTP" }); } }); app.delete('/api/groups/:id', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || (user.role !== 'admin' && user.role !== 'manager')) { return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(req.params.id); await storage.deleteGroup(id); res.json({ message: "Group deleted successfully" }); } catch (error) { console.error("Error deleting group:", error); res.status(500).json({ message: "Failed to delete group" }); } }); // Suppliers routes app.get('/api/suppliers', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || (user.role !== 'admin' && user.role !== 'manager' && user.role !== 'directeur' && user.role !== 'employee')) { return res.status(403).json({ message: "Insufficient permissions" }); } // Check if DLC filter is requested const dlcFilter = req.query.dlc === 'true'; const suppliers = await storage.getSuppliers(); // Filter suppliers for DLC enabled only if requested if (dlcFilter) { const dlcSuppliers = suppliers.filter(supplier => supplier.hasDlc === true); res.json(dlcSuppliers); } else { res.json(suppliers); } } catch (error) { console.error("Error fetching suppliers:", error); res.status(500).json({ message: "Failed to fetch suppliers" }); } }); app.post('/api/suppliers', isAuthenticated, async (req: any, res) => { try { // Debug logging pour la création de fournisseur console.log('📨 POST /api/suppliers - Headers:', { 'content-type': req.headers['content-type'], 'content-length': req.headers['content-length'] }); console.log('📋 POST /api/suppliers - Request body:', JSON.stringify(redactBody(req.body), null, 2)); // Déterminer l'ID utilisateur selon l'environnement let userId; if (req.user.claims && req.user.claims.sub) { userId = req.user.claims.sub; // Production Replit Auth console.log('🔐 Using Replit Auth user ID:', userId); } else if (req.user.id) { userId = req.user.id; // Développement local console.log('🔐 Using local auth user ID:', userId); } else { console.error('❌ No user ID found in request:', { user: req.user }); return res.status(401).json({ message: "User authentication failed" }); } console.log('🔐 User requesting supplier creation:', userId); // Vérifier l'utilisateur const user = await storage.getUser(userId); if (!user) { console.error('❌ User not found:', userId); return res.status(404).json({ message: "User not found" }); } console.log('✅ User found:', { username: user.username, role: user.role }); // Vérifier les permissions if (user.role !== 'admin' && user.role !== 'manager' && user.role !== 'directeur') { console.error('❌ Insufficient permissions:', { userRole: user.role, required: ['admin', 'manager', 'directeur'] }); return res.status(403).json({ message: "Insufficient permissions" }); } console.log('✅ User has permission to create supplier'); // Valider les données console.log('🔍 Validating supplier data with schema...'); const data = insertSupplierSchema.parse(req.body); console.log('✅ Supplier data validation passed:', data); // Créer le fournisseur console.log('🚚 Creating supplier in database...'); const supplier = await storage.createSupplier(data); console.log('✅ Supplier creation successful:', { id: supplier.id, name: supplier.name }); res.json(supplier); } catch (error: any) { console.error('❌ Failed to create supplier:', { error: (error as Error).message, stack: error.stack, body: redactBody(req.body), userId: req.user?.id || req.user?.claims?.sub || 'unknown' }); // Erreur de validation Zod if (error.name === 'ZodError') { console.error('❌ Validation error details:', error.errors); return res.status(400).json({ message: "Validation failed", errors: error.errors }); } res.status(500).json({ message: "Failed to create supplier" }); } }); app.put('/api/suppliers/:id', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || (user.role !== 'admin' && user.role !== 'manager' && user.role !== 'directeur')) { return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(req.params.id); const data = insertSupplierSchema.partial().parse(req.body); const supplier = await storage.updateSupplier(id, data); res.json(supplier); } catch (error) { console.error("Error updating supplier:", error); res.status(500).json({ message: "Failed to update supplier" }); } }); app.delete('/api/suppliers/:id', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || (user.role !== 'admin' && user.role !== 'directeur')) { return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(req.params.id); await storage.deleteSupplier(id); res.json({ message: "Supplier deleted successfully" }); } catch (error) { console.error("Error deleting supplier:", error); res.status(500).json({ message: "Failed to delete supplier" }); } }); // Contacts routes app.get('/api/contacts', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) return res.status(404).json({ message: "User not found" }); let groupIds: number[] | undefined; if (user.role !== 'admin') { groupIds = (user.userGroups || []).map((ug: any) => ug.groupId); } else if (req.query.groupId) { groupIds = [parseInt(req.query.groupId as string)]; } const result = await storage.getContacts(groupIds); res.json(result); } catch (error) { console.error("Error fetching contacts:", error); res.status(500).json({ message: "Failed to fetch contacts" }); } }); app.post('/api/contacts', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || !['admin', 'directeur', 'manager'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions" }); } const data = insertContactSchema.parse(req.body); const contact = await storage.createContact(data); res.json(contact); } catch (error: any) { if (error.name === 'ZodError') { return res.status(400).json({ message: "Validation failed", errors: error.errors }); } console.error("Error creating contact:", error); res.status(500).json({ message: "Failed to create contact" }); } }); app.put('/api/contacts/:id', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || !['admin', 'directeur', 'manager'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(req.params.id); const data = insertContactSchema.partial().parse(req.body); const contact = await storage.updateContact(id, data); res.json(contact); } catch (error) { console.error("Error updating contact:", error); res.status(500).json({ message: "Failed to update contact" }); } }); app.delete('/api/contacts/:id', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || !['admin', 'directeur', 'manager'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(req.params.id); await storage.deleteContact(id); res.json({ message: "Contact deleted successfully" }); } catch (error) { console.error("Error deleting contact:", error); res.status(500).json({ message: "Failed to delete contact" }); } }); // Orders routes app.get('/api/orders', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { startDate, endDate, storeId } = req.query; let orders; if (user.role === 'admin') { let groupIds: number[] | undefined; // If admin selected a specific store, filter by it if (storeId) { groupIds = [parseInt(storeId as string)]; } // Only filter by date if both startDate and endDate are provided if (startDate && endDate) { orders = await storage.getOrdersByDateRange(startDate as string, endDate as string, groupIds); } else { orders = await storage.getOrders(groupIds); } } else { // For manager and employee roles, filter by their assigned groups const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || []; let groupIds: number[] | undefined; if (storeId) { // If a specific store is requested, verify user has access to it const requestedStoreId = parseInt(storeId as string); if (userGroupIds.includes(requestedStoreId)) { groupIds = [requestedStoreId]; } else { // User doesn't have access to this store, return empty array return res.json([]); } } else { // For directeur role, automatically use their assigned store (but with full permissions within that store) if (user.role === 'directeur') { if (userGroupIds.length > 0) { groupIds = [userGroupIds[0]]; // Use first assigned store automatically } else { return res.json([]); } } // For manager role, automatically use their assigned store else if (user.role === 'manager') { if (userGroupIds.length > 0) { groupIds = [userGroupIds[0]]; // Use first assigned store automatically } else { return res.json([]); } } else { // For employee role, require explicit store selection return res.json([]); } } // Only filter by date if both startDate and endDate are provided if (startDate && endDate) { orders = await storage.getOrdersByDateRange(startDate as string, endDate as string, groupIds); } else { orders = await storage.getOrders(groupIds); } } res.json(orders); } catch (error) { console.error("Error fetching orders:", error); res.status(500).json({ message: "Failed to fetch orders" }); } }); // Statistiques agrégées commandes/livraisons. // // Les pages Magasins et Fournisseurs n'affichent que des comptages. Elles // chargeaient tout l'historique via /api/orders et /api/deliveries pour ne // faire que des .filter().length côté navigateur : le comptage est fait en // base et seules les valeurs agrégées transitent. const resolveStatsGroupIds = (user: any, storeId?: string): number[] | undefined | null => { const userGroupIds: number[] = user.userGroups?.map((ug: any) => ug.groupId) || []; if (user.role === 'admin') { return storeId ? [parseInt(storeId)] : undefined; // undefined = tous les magasins } if (storeId) { const requestedStoreId = parseInt(storeId); // null = accès refusé au magasin demandé return userGroupIds.includes(requestedStoreId) ? [requestedStoreId] : null; } return userGroupIds.length > 0 ? userGroupIds : null; }; app.get('/api/stats/by-group', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const groupIds = resolveStatsGroupIds(user, req.query.storeId as string | undefined); if (groupIds === null) { return res.json([]); } res.json(await storage.getOrderDeliveryStatsByGroup(groupIds)); } catch (error) { console.error("Error fetching group stats:", error); res.status(500).json({ message: "Failed to fetch group stats" }); } }); app.get('/api/stats/by-supplier', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const groupIds = resolveStatsGroupIds(user, req.query.storeId as string | undefined); if (groupIds === null) { return res.json([]); } res.json(await storage.getOrderDeliveryStatsBySupplier(groupIds)); } catch (error) { console.error("Error fetching supplier stats:", error); res.status(500).json({ message: "Failed to fetch supplier stats" }); } }); app.get('/api/orders/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const order = await storage.getOrder(id); if (!order) { return res.status(404).json({ message: "Order not found" }); } // Check if user has access to this order (only admin can access all orders) if (user.role !== 'admin') { const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(order.groupId)) { return res.status(403).json({ message: "Access denied" }); } } res.json(order); } catch (error) { console.error("Error fetching order:", error); res.status(500).json({ message: "Failed to fetch order" }); } }); app.post('/api/orders', isAuthenticated, async (req: any, res) => { try { console.log('📦 Order creation started:', { userId: req.user?.id || req.user?.claims?.sub, body: redactBody(req.body), environment: process.env.NODE_ENV }); const user = getCurrentUser(req); if (!user) { console.log('❌ User not found in order creation'); return res.status(404).json({ message: "User not found" }); } console.log('👤 User found for order creation:', { id: user.id, role: user.role, groupsCount: user.userGroups.length, groups: user.userGroups.map(ug => ({ groupId: ug.groupId, groupName: ug.group?.name })) }); const data = insertOrderSchema.parse({ ...req.body, createdBy: user.id, }); console.log('✅ Order data validated:', data); // Check if user has access to the group (only admin can access all groups) if (user.role !== 'admin') { const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(data.groupId)) { console.log('❌ Access denied to group:', { requestedGroupId: data.groupId, userGroups: userGroupIds }); return res.status(403).json({ message: "Access denied to this group" }); } } console.log('🚀 Creating order in storage...'); const order = await storage.createOrder(data); console.log('✅ Order created successfully:', { id: order.id, groupId: order.groupId, plannedDate: order.plannedDate, supplierId: order.supplierId }); res.json(order); } catch (error: any) { console.error("❌ Error creating order:", { error: (error as Error).message, stack: error.stack, body: redactBody(req.body), userId: req.user?.id || req.user?.claims?.sub || 'unknown' }); // Erreur de validation Zod if (error.name === 'ZodError') { console.error('❌ Order validation error details:', error.errors); return res.status(400).json({ message: "Validation failed", errors: error.errors }); } res.status(500).json({ message: "Failed to create order" }); } }); app.put('/api/orders/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const order = await storage.getOrder(id); if (!order) { return res.status(404).json({ message: "Order not found" }); } // Check edit permissions using the shared permission system if (!hasPermission(user.role, 'orders', 'edit')) { return res.status(403).json({ message: "Insufficient permissions to edit orders" }); } if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(order.groupId)) { return res.status(403).json({ message: "Access denied" }); } } const data = insertOrderSchema.partial().parse(req.body); const updatedOrder = await storage.updateOrder(id, data); res.json(updatedOrder); } catch (error) { console.error("Error updating order:", error); res.status(500).json({ message: "Failed to update order" }); } }); app.delete('/api/orders/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const order = await storage.getOrder(id); if (!order) { return res.status(404).json({ message: "Order not found" }); } // Check delete permissions using the shared permission system if (!hasPermission(user.role, 'orders', 'delete')) { return res.status(403).json({ message: "Insufficient permissions to delete orders" }); } if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(order.groupId)) { return res.status(403).json({ message: "Access denied" }); } } await storage.deleteOrder(id); res.json({ message: "Order deleted successfully" }); } catch (error) { console.error("Error deleting order:", error); res.status(500).json({ message: "Failed to delete order" }); } }); // Route pour diagnostiquer et synchroniser les statuts commandes/livraisons app.post('/api/sync-order-delivery-status', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } console.log('🔄 Starting order-delivery status synchronization...'); // Diagnostic: Trouver les commandes avec livraisons livrées mais pas en statut "delivered" const orders = await storage.getOrders(); const problematicOrders = []; const fixedOrders = []; for (const order of orders) { // getOrders() renvoie déjà les livraisons de chaque commande : les // recharger ici relisait toute la table à chaque itération. const orderDeliveries = order.deliveries ?? []; if (orderDeliveries.length > 0) { const hasDeliveredDeliveries = orderDeliveries.some((d: any) => d.status === 'delivered'); if (hasDeliveredDeliveries && order.status !== 'delivered') { console.log(`🔍 Found problematic order: #CMD-${order.id} (status: ${order.status}) with delivered deliveries`); problematicOrders.push({ orderId: order.id, currentStatus: order.status, deliveredDeliveries: orderDeliveries.filter((d: any) => d.status === 'delivered').length, totalDeliveries: orderDeliveries.length }); // Fixer automatiquement try { await storage.updateOrder(order.id, { status: 'delivered' }); console.log(`✅ Fixed order #CMD-${order.id} status to 'delivered'`); fixedOrders.push(order.id); } catch (error) { console.error(`❌ Failed to fix order #CMD-${order.id}:`, error); } } } } console.log('🔄 Synchronization completed'); res.json({ message: "Synchronization completed", diagnostics: { problematicOrdersFound: problematicOrders.length, ordersFixed: fixedOrders.length, problematicOrders, fixedOrders } }); } catch (error) { console.error("❌ Error in sync operation:", error); res.status(500).json({ message: "Failed to synchronize statuses", error: (error as Error).message }); } }); // Deliveries routes app.get('/api/deliveries', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { startDate, endDate, storeId, withBL } = req.query; let deliveries; // Filtre de statut optionnel, appliqué en base (ex. rapprochement BL : // status=delivered). Toute autre valeur est ignorée. const allowedStatuses = ['pending', 'planned', 'delivered']; const status = typeof req.query.status === 'string' && allowedStatuses.includes(req.query.status) ? req.query.status : undefined; if (user.role === 'admin') { let groupIds: number[] | undefined; // If admin selected a specific store, filter by it if (storeId) { groupIds = [parseInt(storeId as string)]; } // Only filter by date if both startDate and endDate are provided if (startDate && endDate) { deliveries = await storage.getDeliveriesByDateRange(startDate as string, endDate as string, groupIds); } else { deliveries = await storage.getDeliveries(groupIds, { status }); } } else { // For manager and employee roles, filter by their assigned groups const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || []; let groupIds: number[] | undefined; if (storeId) { // If a specific store is requested, verify user has access to it const requestedStoreId = parseInt(storeId as string); if (userGroupIds.includes(requestedStoreId)) { groupIds = [requestedStoreId]; } else { // User doesn't have access to this store, return empty array return res.json([]); } } else { // For directeur role, automatically use their assigned store (but with full permissions within that store) if (user.role === 'directeur') { if (userGroupIds.length > 0) { groupIds = [userGroupIds[0]]; // Use first assigned store automatically } else { return res.json([]); } } // For manager role, automatically use their assigned store else if (user.role === 'manager') { if (userGroupIds.length > 0) { groupIds = [userGroupIds[0]]; // Use first assigned store automatically } else { return res.json([]); } } else { // For employee role, require explicit store selection return res.json([]); } } // Only filter by date if both startDate and endDate are provided if (startDate && endDate) { deliveries = await storage.getDeliveriesByDateRange(startDate as string, endDate as string, groupIds); } else { deliveries = await storage.getDeliveries(groupIds, { status }); } } // La lecture par plage de dates ne filtre pas le statut en base if (status && startDate && endDate) { deliveries = deliveries.filter((d: any) => d.status === status); } // Filter for BL if requested if (withBL === 'true') { deliveries = deliveries.filter((d: any) => d.blNumber && d.status === 'delivered'); } res.json(deliveries); } catch (error) { console.error("Error fetching deliveries:", error); res.status(500).json({ message: "Failed to fetch deliveries" }); } }); app.get('/api/deliveries/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const delivery = await storage.getDelivery(id); if (!delivery) { return res.status(404).json({ message: "Delivery not found" }); } // Check if user has access to this delivery if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(delivery.groupId)) { return res.status(403).json({ message: "Access denied" }); } } res.json(delivery); } catch (error) { console.error("Error fetching delivery:", error); res.status(500).json({ message: "Failed to fetch delivery" }); } }); app.put('/api/deliveries/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const delivery = await storage.getDelivery(id); if (!delivery) { return res.status(404).json({ message: "Delivery not found" }); } // Check edit permissions using the shared permission system if (!hasPermission(user.role, 'deliveries', 'edit')) { return res.status(403).json({ message: "Insufficient permissions to edit deliveries" }); } if (user.role !== 'admin') { const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(delivery.groupId)) { return res.status(403).json({ message: "Access denied" }); } } // Transform data types before validation const transformedData = { ...req.body }; // Convert decimal amounts to strings or null (schema expects string for decimal fields) if (transformedData.blAmount !== undefined) { transformedData.blAmount = (transformedData.blAmount === null || transformedData.blAmount === '') ? null : transformedData.blAmount.toString(); } if (transformedData.invoiceAmount !== undefined) { transformedData.invoiceAmount = (transformedData.invoiceAmount === null || transformedData.invoiceAmount === '') ? null : transformedData.invoiceAmount.toString(); } // Convert text fields - allow null for clearing if (transformedData.invoiceReference !== undefined) { transformedData.invoiceReference = (transformedData.invoiceReference === '') ? null : transformedData.invoiceReference; } if (transformedData.blNumber !== undefined) { transformedData.blNumber = (transformedData.blNumber === '') ? null : transformedData.blNumber; } // Convert timestamp fields to Date objects or null (schema expects Date for timestamp fields) if (transformedData.validatedAt !== undefined) { transformedData.validatedAt = (transformedData.validatedAt === null || transformedData.validatedAt === '') ? null : new Date(transformedData.validatedAt); } if (transformedData.dueDate !== undefined) { transformedData.dueDate = (transformedData.dueDate === null || transformedData.dueDate === '') ? null : new Date(transformedData.dueDate); } const data = insertDeliverySchema.partial().parse(transformedData); // CRITICAL FIX: Si une commande est liée lors de la modification, vérifier qu'elle appartient au même magasin if (data.orderId !== undefined) { if (data.orderId !== null) { const linkedOrder = await storage.getOrder(data.orderId); if (!linkedOrder) { return res.status(400).json({ message: "La commande liée n'existe pas" }); } if (linkedOrder.groupId !== delivery.groupId) { return res.status(400).json({ message: `Impossible de lier une livraison du magasin ${delivery.groupId} avec une commande du magasin ${linkedOrder.groupId}` }); } console.log(`✅ Validation OK: Livraison #${id} et commande #${data.orderId} appartiennent au même magasin ${delivery.groupId}`); } } // GESTION ÉCHÉANCE : Si la référence facture change, mettre à jour la date d'échéance if (data.invoiceReference !== undefined && data.invoiceReference !== delivery.invoiceReference) { if (data.invoiceReference && data.invoiceReference.trim()) { // Nouvelle référence facture : reverifier dans NocoDB pour récupérer l'échéance try { const { InvoiceVerificationService } = await import('./invoiceVerification.js'); const verificationService = new InvoiceVerificationService(); const result = await verificationService.verifyInvoice( data.invoiceReference, delivery.groupId, true, // forceRefresh delivery.reconciled || false ); if (result.exists && result.dueDate) { // Normaliser la date avant de la stocker const normalizedDateString = normalizeDateString(result.dueDate); if (normalizedDateString) { // Convertir la string normalisée en objet Date pour Drizzle data.dueDate = new Date(normalizedDateString); console.log(`📅 Date d'échéance récupérée et normalisée: ${normalizedDateString} (original: ${result.dueDate})`); } else { data.dueDate = null; console.log(`📅 Date d'échéance invalide, ignorée`); } } else { data.dueDate = null; console.log(`📅 Aucune date d'échéance trouvée dans NocoDB`); } } catch (error) { console.error('❌ Erreur récupération échéance:', error); // Ne pas bloquer la mise à jour, juste ne pas avoir d'échéance data.dueDate = null; } } else { // Référence facture vidée : vider aussi l'échéance data.dueDate = null; console.log(`📅 Référence facture vidée, échéance également vidée`); } } const updatedDelivery = await storage.updateDelivery(id, data); // SYNCHRONISATION AUTOMATIQUE : Si livraison devient "delivered", marquer la commande associée comme "delivered" // MAIS seulement après validation explicite (pas juste mise à jour status) // Cette sync sera gérée dans validateDelivery endpoint uniquement // AUTO-VALIDATION RAPPROCHEMENT AUTOMATIQUE : Si fournisseur en mode automatique, livraison delivered + BL → auto-valider if (data.status === 'delivered' || data.blNumber) { try { // Récupérer le fournisseur pour vérifier le mode automatique : il est // déjà joint à la livraison, la table n'est relue que s'il a changé const supplier = (updatedDelivery.supplierId === delivery.supplierId && delivery.supplier) ? delivery.supplier : (await storage.getSuppliers()).find((s: any) => s.id === updatedDelivery.supplierId); if (supplier?.automaticReconciliation && updatedDelivery.status === 'delivered' && updatedDelivery.blNumber) { console.log(`🤖 Auto-reconciliation: Delivery #${id} from automatic supplier ${supplier.name}, auto-validating...`); // Auto-valider le rapprochement await storage.updateDelivery(id, { reconciled: true, validatedAt: new Date() }); console.log(`✅ Auto-reconciliation: Delivery #${id} automatically validated for supplier ${supplier.name}`); } } catch (error) { console.error(`❌ Auto-reconciliation failed for delivery #${id}:`, error); } } res.json(updatedDelivery); } catch (error) { console.error("Error updating delivery:", error); res.status(500).json({ message: "Failed to update delivery" }); } }); app.post('/api/deliveries', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } console.log('🚛 Creating delivery with data:', req.body); // Traiter les données pour orderId - "none" devient null const processedBody = { ...req.body }; if (processedBody.orderId === "none" || processedBody.orderId === "") { processedBody.orderId = null; } else if (processedBody.orderId) { processedBody.orderId = parseInt(processedBody.orderId); } const data = insertDeliverySchema.parse({ ...processedBody, createdBy: user.id, }); console.log('🚛 Processed delivery data:', data); // Check if user has access to the group if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(data.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } // CRITICAL FIX: Si une commande est liée, vérifier qu'elle appartient au même magasin if (data.orderId) { const linkedOrder = await storage.getOrder(data.orderId); if (!linkedOrder) { return res.status(400).json({ message: "La commande liée n'existe pas" }); } if (linkedOrder.groupId !== data.groupId) { return res.status(400).json({ message: `Impossible de lier une livraison du magasin ${data.groupId} avec une commande du magasin ${linkedOrder.groupId}` }); } console.log(`✅ Validation OK: Livraison et commande #${data.orderId} appartiennent au même magasin ${data.groupId}`); } const delivery = await storage.createDelivery(data); // Log de liaison avec commande if (data.orderId) { console.log(`🔗 Delivery #${delivery.id} linked to order #${data.orderId}`); } else { console.log(`🚛 Delivery #${delivery.id} created without order link`); } res.json(delivery); } catch (error) { console.error("Error creating delivery:", error); res.status(500).json({ message: "Failed to create delivery" }); } }); // SUPPRIMÉ : Doublon d'endpoint PUT /api/deliveries/:id (le premier endpoint avec logique complète est conservé) app.delete('/api/deliveries/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const delivery = await storage.getDelivery(id); if (!delivery) { return res.status(404).json({ message: "Delivery not found" }); } // Check permissions using the shared permission system if (!hasPermission(user.role, 'deliveries', 'delete')) { return res.status(403).json({ message: "Insufficient permissions to delete deliveries" }); } if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(delivery.groupId)) { return res.status(403).json({ message: "Access denied" }); } } await storage.deleteDelivery(id); res.json({ message: "Delivery deleted successfully" }); } catch (error) { console.error("Error deleting delivery:", error); res.status(500).json({ message: "Failed to delete delivery" }); } }); // Route obsolète supprimée - utiliser le système de commentaires de rapprochement à la place // Les commentaires sont désormais gérés via /api/deliveries/:id/reconciliation-comments // Routes pour les commentaires de rapprochement // GET - Récupérer les commentaires d'une livraison app.get('/api/deliveries/:id/reconciliation-comments', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const deliveryId = parseInt(req.params.id); const delivery = await storage.getDelivery(deliveryId); if (!delivery) { return res.status(404).json({ message: "Delivery not found" }); } // Check permissions if (!hasPermission(user.role, 'deliveries', 'view')) { return res.status(403).json({ message: "Insufficient permissions" }); } // Check group access if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(delivery.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } const comments = await storage.getReconciliationComments(deliveryId); res.json(comments); } catch (error) { console.error("Error fetching reconciliation comments:", error); res.status(500).json({ message: "Failed to fetch reconciliation comments" }); } }); // POST - Créer un nouveau commentaire app.post('/api/deliveries/:id/reconciliation-comments', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const deliveryId = parseInt(req.params.id); const delivery = await storage.getDelivery(deliveryId); if (!delivery) { return res.status(404).json({ message: "Delivery not found" }); } // Check permissions if (!hasPermission(user.role, 'deliveries', 'edit')) { return res.status(403).json({ message: "Insufficient permissions to create comments" }); } // Check group access if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(delivery.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } const commentData = insertReconciliationCommentSchema.parse({ ...req.body, deliveryId, authorId: user.id, groupId: delivery.groupId, }); const comment = await storage.createReconciliationComment(commentData); res.status(201).json(comment); } catch (error) { console.error("Error creating reconciliation comment:", error); res.status(500).json({ message: "Failed to create reconciliation comment" }); } }); // PUT - Modifier un commentaire app.put('/api/reconciliation-comments/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const commentId = parseInt(req.params.id); const comment = await storage.getReconciliationCommentById(commentId); if (!comment) { return res.status(404).json({ message: "Comment not found" }); } // Check permissions - only author or admin can edit if (user.role !== 'admin' && comment.authorId !== user.id) { return res.status(403).json({ message: "Only comment author or admin can edit comments" }); } // Check group access if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(comment.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } const updateData = insertReconciliationCommentSchema.partial().parse(req.body); const updatedComment = await storage.updateReconciliationComment(commentId, updateData); res.json(updatedComment); } catch (error) { console.error("Error updating reconciliation comment:", error); res.status(500).json({ message: "Failed to update reconciliation comment" }); } }); // DELETE - Supprimer un commentaire app.delete('/api/reconciliation-comments/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const commentId = parseInt(req.params.id); const comment = await storage.getReconciliationCommentById(commentId); if (!comment) { return res.status(404).json({ message: "Comment not found" }); } // Check permissions - only author or admin can delete if (user.role !== 'admin' && comment.authorId !== user.id) { return res.status(403).json({ message: "Only comment author or admin can delete comments" }); } // Check group access if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(comment.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } await storage.deleteReconciliationComment(commentId); res.json({ message: "Comment deleted successfully" }); } catch (error) { console.error("Error deleting reconciliation comment:", error); res.status(500).json({ message: "Failed to delete reconciliation comment" }); } }); // Route de vérification de facture NocoDB // Envoi au fournisseur de la demande de facture (PDF) ou de BL (Excel) // via le serveur SMTP configuré sur la fiche du magasin de la livraison app.post('/api/deliveries/:id/send-supplier-mail', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const deliveryId = parseInt(req.params.id); const delivery = await storage.getDelivery(deliveryId); if (!delivery) { return res.status(404).json({ message: "Livraison introuvable" }); } if (!hasPermission(user.role, 'deliveries', 'view')) { return res.status(403).json({ message: "Insufficient permissions" }); } // Hors admin, l'utilisateur doit appartenir au magasin de la livraison if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(delivery.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } // Adresse du fournisseur : celle enregistrée sur sa fiche const supplierEmail = delivery.supplier?.email?.trim(); if (!supplierEmail) { return res.status(400).json({ message: `Aucune adresse email renseignée pour ${delivery.supplier?.name || 'ce fournisseur'}` }); } // Configuration SMTP du magasin (mot de passe inclus : usage serveur uniquement) const group = await storage.getGroup(delivery.groupId); if (!group) { return res.status(404).json({ message: "Magasin de la livraison introuvable" }); } if (!(group as any).smtpEnabled) { return res.status(400).json({ message: `L'envoi de mails n'est pas activé pour le magasin ${group.name}. Renseignez la configuration SMTP sur sa fiche.` }); } const missing = getMissingSmtpFields(group as any); if (missing.length > 0) { return res.status(400).json({ message: `Configuration SMTP incomplète pour ${group.name} : ${missing.join(', ')}` }); } // Historisation de la tentative, succès comme échec — un échec de // journalisation ne doit jamais faire échouer (ni annuler) l'envoi const senderName = [user.firstName, user.lastName].filter(Boolean).join(' ').trim() || user.username || user.id; const logAttempt = async (status: 'sent' | 'failed', extra: { messageId?: string; errorMessage?: string }) => { try { await storage.createSupplierMailLog({ deliveryId, groupId: delivery.groupId, supplierId: delivery.supplierId ?? null, supplierName: delivery.supplier?.name || null, sentTo: supplierEmail, subject: buildSupplierMailSubject(delivery as any), status, errorMessage: extra.errorMessage || null, messageId: extra.messageId || null, sentBy: user.id, sentByName: senderName, }); } catch (logError) { console.error('⚠️ Historisation du mail fournisseur impossible:', logError); } }; try { const result = await sendSupplierDocumentRequest(group as any, delivery as any, supplierEmail); await logAttempt('sent', { messageId: result.messageId }); console.log('📧 Mail fournisseur envoyé:', { deliveryId, supplier: delivery.supplier?.name, to: supplierEmail, store: group.name, messageId: result.messageId }); res.json({ success: true, sentTo: supplierEmail, supplierName: delivery.supplier?.name || null, messageId: result.messageId }); } catch (sendError: any) { await logAttempt('failed', { errorMessage: sendError?.message || 'Erreur inconnue' }); throw sendError; } } catch (error: any) { console.error("Erreur envoi mail fournisseur:", error); res.status(500).json({ message: error?.message || "Impossible d'envoyer le mail au fournisseur" }); } }); // Historique des relances fournisseurs, restreint aux magasins de l'utilisateur app.get('/api/supplier-mail-logs', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const deliveryId = req.query.deliveryId ? parseInt(req.query.deliveryId as string) : undefined; let groupIds: number[] | undefined; if (user.role !== 'admin') { groupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (groupIds.length === 0) { return res.json([]); } } else if (req.query.storeId) { groupIds = [parseInt(req.query.storeId as string)]; } const logs = await storage.getSupplierMailLogs(groupIds, deliveryId); res.json(logs); } catch (error) { console.error("Erreur lecture historique mails fournisseurs:", error); res.status(500).json({ message: "Failed to fetch supplier mail logs" }); } }); app.post('/api/deliveries/:id/verify-invoice', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const deliveryId = parseInt(req.params.id); const delivery = await storage.getDelivery(deliveryId); if (!delivery) { return res.status(404).json({ message: "Delivery not found" }); } // Check permissions if (!hasPermission(user.role, 'deliveries', 'view')) { return res.status(403).json({ message: "Insufficient permissions" }); } // Only admin have access to all deliveries, others must be in the same group if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(delivery.groupId)) { console.log('🚫 Access denied - User groups check:', { userId: user.id, userRole: user.role, userGroupIds, deliveryGroupId: delivery.groupId, deliverySupplier: delivery.supplier?.name }); return res.status(403).json({ message: "Access denied to this group" }); } } const { invoiceReference, blNumber, forceRefresh } = req.body; if (!delivery.supplier || !delivery.group) { console.log('❌ Livraison manque informations:', { deliveryId, hasSupplier: !!delivery.supplier, hasGroup: !!delivery.group }); return res.status(400).json({ message: "Delivery missing supplier or group information" }); } // Accepter soit une référence de facture soit un numéro de BL if ((!invoiceReference || !invoiceReference.trim()) && (!blNumber || !blNumber.trim())) { return res.status(400).json({ message: "Référence de facture ou numéro BL requis" }); } let result; if (invoiceReference && invoiceReference.trim()) { // Vérifier par référence de facture result = await invoiceVerificationService.verifyInvoice( invoiceReference, delivery.groupId, forceRefresh || false, delivery.reconciled || false ); } else if (blNumber && blNumber.trim()) { // Vérifier par numéro BL result = await invoiceVerificationService.verifyInvoiceByBL( blNumber, delivery.supplier.name, delivery.groupId, forceRefresh || false, delivery.reconciled || false ); } else { result = { exists: false, matchType: 'none', errorMessage: 'Aucune référence de facture ou numéro BL fourni' }; } // CRITICAL FIX: Sauvegarder les données dans la table deliveries après vérification réussie if (result.exists && (result.invoiceAmount !== undefined || result.invoiceAmountTTC !== undefined || result.dueDate !== undefined || result.invoiceReference !== undefined)) { try { const updateData: any = {}; // Ajouter la référence facture si trouvée if (result.invoiceReference) { updateData.invoiceReference = result.invoiceReference; } // Ajouter le montant facture HT si trouvé if (result.invoiceAmount !== undefined && result.invoiceAmount !== null) { updateData.invoiceAmount = result.invoiceAmount.toString(); } // Ajouter le montant facture TTC si trouvé if (result.invoiceAmountTTC !== undefined && result.invoiceAmountTTC !== null) { updateData.invoiceAmountTTC = result.invoiceAmountTTC.toString(); } // Ajouter l'échéance si trouvée if (result.dueDate) { const normalizedDateString = normalizeDateString(result.dueDate); if (normalizedDateString) { updateData.dueDate = new Date(normalizedDateString); console.log(`✅ Sauvegarde échéance dans deliveries: ${normalizedDateString}`); } } // Mettre à jour la livraison si on a des données if (Object.keys(updateData).length > 0) { await storage.updateDelivery(deliveryId, updateData); console.log(`✅ Livraison #${deliveryId} mise à jour avec:`, updateData); } } catch (error) { console.error('❌ Erreur sauvegarde données vérification:', error); // Ne pas bloquer la réponse, juste logger l'erreur } } res.json(result); } catch (error) { console.error("Error verifying invoice:", error); res.status(500).json({ message: "Failed to verify invoice", error: error instanceof Error ? error.message : 'Unknown error' }); } }); app.post('/api/deliveries/:id/validate', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check validate permissions using the shared permission system if (!hasPermission(user.role, 'deliveries', 'validate')) { return res.status(403).json({ message: "Insufficient permissions to validate deliveries" }); } const id = parseInt(req.params.id); const delivery = await storage.getDelivery(id); if (!delivery) { return res.status(404).json({ message: "Delivery not found" }); } // Check permissions if (user.role === 'manager') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(delivery.groupId)) { return res.status(403).json({ message: "Access denied" }); } } const { blNumber, blAmount } = req.body; // Le numéro de BL est maintenant obligatoire pour valider une livraison if (!blNumber || !blNumber.trim()) { return res.status(400).json({ message: "Le numéro de bon de livraison est obligatoire pour valider une livraison" }); } let blData: any = { blNumber: blNumber.trim() }; if (blAmount !== undefined && blAmount !== null && blAmount !== '') { blData.blAmount = blAmount; } await storage.validateDelivery(id, blData); // AUTO-VALIDATION RAPPROCHEMENT : fournisseur en mode automatique → rapprochement validé directement try { // Fournisseur déjà joint à la livraison const supplier = delivery.supplier; if (supplier?.automaticReconciliation) { await storage.updateDelivery(id, { reconciled: true, validatedAt: new Date() }); console.log(`✅ Auto-reconciliation: Delivery #${id} automatically validated for supplier ${supplier.name}`); } } catch (error) { console.error(`❌ Auto-reconciliation failed for delivery #${id}:`, error); } // MISE À JOUR DU CACHE : Marquer le cache comme permanent pour cette livraison validée try { if (delivery.invoiceReference && delivery.invoiceReference.trim()) { console.log('🔄 [CACHE] Mise à jour cache permanent après validation livraison'); await invoiceVerificationService.updateCacheAsReconciled(delivery.invoiceReference, delivery.groupId); } if (delivery.blNumber && delivery.blNumber.trim()) { console.log('🔄 [CACHE] Mise à jour cache permanent BL après validation livraison'); await invoiceVerificationService.updateCacheAsReconciled(delivery.blNumber, delivery.groupId); } } catch (error) { console.error('❌ Erreur mise à jour cache après validation:', error); } // SYNCHRONISATION AUTOMATIQUE : Quand validation, marquer la commande associée comme "delivered" if (delivery.orderId) { try { console.log(`🔄 Auto-sync: Delivery #${id} validated, updating order #${delivery.orderId} to delivered`); await storage.updateOrder(delivery.orderId, { status: 'delivered' }); console.log(`✅ Auto-sync: Order #${delivery.orderId} automatically marked as delivered`); } catch (error) { console.error(`❌ Auto-sync failed for order #${delivery.orderId}:`, error); } } res.json({ message: "Delivery validated successfully" }); } catch (error) { console.error("Error validating delivery:", error); res.status(500).json({ message: "Failed to validate delivery" }); } }); // Marquer le contrôle d'une livraison comme effectué app.put('/api/deliveries/:id/control', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); if (isNaN(id)) { return res.status(400).json({ message: "Invalid delivery ID" }); } // Vérifier que la livraison existe const delivery = await storage.getDelivery(id); if (!delivery) { return res.status(404).json({ message: "Delivery not found" }); } // Vérifier les permissions if (!hasPermission(user.role, 'deliveries', 'edit')) { return res.status(403).json({ message: "Insufficient permissions" }); } // Mettre à jour le contrôle await storage.markDeliveryControlValidated(id, user.id); res.json({ message: "Delivery control validated successfully" }); } catch (error) { console.error("Error validating delivery control:", error); res.status(500).json({ message: "Failed to validate delivery control" }); } }); // Route pour diagnostiquer le cache des livraisons app.get('/api/cache/diagnosis', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Vérifier que c'est un admin if (user.role !== 'admin') { return res.status(403).json({ message: "Seuls les admins peuvent exécuter cette opération" }); } // Récupérer des statistiques sur le cache const deliveries = await storage.getDeliveries(); const reconciledCount = deliveries.filter(d => d.reconciled).length; const totalCount = deliveries.length; // Vérifier quelques caches const sampleCaches = []; const reconciledDeliveries = deliveries.filter(d => d.reconciled).slice(0, 5); // Prendre 5 exemples for (const delivery of reconciledDeliveries) { if (delivery.invoiceReference) { const cacheKey = `${delivery.invoiceReference.toLowerCase()}_${delivery.groupId}`; const cached = await storage.getInvoiceVerificationCache(cacheKey); sampleCaches.push({ deliveryId: delivery.id, invoiceRef: delivery.invoiceReference, groupId: delivery.groupId, reconciled: delivery.reconciled, cacheExists: !!cached, cacheReconciled: cached?.isReconciled || false, cacheExpires: cached?.expiresAt }); } } res.json({ statistics: { totalDeliveries: totalCount, reconciledDeliveries: reconciledCount, percentageReconciled: Math.round((reconciledCount / totalCount) * 100) }, sampleCaches, message: "Diagnostic du cache terminé" }); } catch (error) { console.error("Erreur diagnostic cache:", error); res.status(500).json({ message: "Erreur lors du diagnostic", error: error instanceof Error ? error.message : 'Unknown error' }); } }); // Route pour mettre à jour les caches existants des livraisons validées app.post('/api/cache/update-reconciled', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Vérifier que c'est un admin if (user.role !== 'admin') { return res.status(403).json({ message: "Seuls les admins peuvent exécuter cette opération" }); } console.log('🔧 [ADMIN] Exécution mise à jour des caches permanents...'); await invoiceVerificationService.updateExistingReconciledCaches(); res.json({ message: "Mise à jour des caches permanents terminée avec succès", success: true }); } catch (error) { console.error("Erreur mise à jour caches:", error); res.status(500).json({ message: "Erreur lors de la mise à jour des caches", error: error instanceof Error ? error.message : 'Unknown error' }); } }); // DLC Products routes app.get('/api/dlc-products', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { storeId, status, supplierId, search } = req.query; let groupIds: number[] | undefined; if (user.role === 'admin') { groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (storeId && userGroupIds.includes(parseInt(storeId as string))) { groupIds = [parseInt(storeId as string)]; } else { groupIds = userGroupIds; } } const filters: any = {}; if (status && status !== 'all') filters.status = status as string; if (supplierId && supplierId !== 'all') filters.supplierId = parseInt(supplierId as string); if (search) filters.search = search as string; const dlcProducts = await storage.getDlcProducts(groupIds, filters); res.json(dlcProducts); } catch (error) { console.error("Error fetching DLC products:", error); res.status(500).json([]); } }); app.get('/api/dlc-products/stats', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { storeId } = req.query; let groupIds: number[] | undefined; if (user.role === 'admin') { groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (storeId && userGroupIds.includes(parseInt(storeId as string))) { groupIds = [parseInt(storeId as string)]; } else { groupIds = userGroupIds; } } const stats = await storage.getDlcStats(groupIds); res.json(stats); } catch (error) { console.error("Error fetching DLC stats:", error); res.status(500).json({ active: 0, expiringSoon: 0, expired: 0 }); } }); app.get('/api/dlc-products/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const dlcProduct = await storage.getDlcProduct(id); if (!dlcProduct) { return res.status(404).json({ message: "DLC Product not found" }); } // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(dlcProduct.groupId)) { return res.status(403).json({ message: "Access denied" }); } } res.json(dlcProduct); } catch (error) { console.error("Error fetching DLC product:", error); res.status(500).json({ message: "Failed to fetch DLC product" }); } }); app.post('/api/dlc-products', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const data = { ...req.body, createdBy: user.id, }; // Check if user has access to the group if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(data.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } const dlcProduct = await storage.createDlcProduct(data); res.json(dlcProduct); } catch (error) { console.error("Error creating DLC product:", error); res.status(500).json({ message: "Failed to create DLC product" }); } }); app.put('/api/dlc-products/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const dlcProduct = await storage.getDlcProduct(id); if (!dlcProduct) { return res.status(404).json({ message: "DLC Product not found" }); } // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(dlcProduct.groupId)) { return res.status(403).json({ message: "Access denied" }); } } const updatedProduct = await storage.updateDlcProduct(id, req.body); res.json(updatedProduct); } catch (error) { console.error("Error updating DLC product:", error); res.status(500).json({ message: "Failed to update DLC product" }); } }); app.delete('/api/dlc-products/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const dlcProduct = await storage.getDlcProduct(id); if (!dlcProduct) { return res.status(404).json({ message: "DLC Product not found" }); } // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(dlcProduct.groupId)) { return res.status(403).json({ message: "Access denied" }); } } await storage.deleteDlcProduct(id); res.json({ message: "DLC Product deleted successfully" }); } catch (error) { console.error("Error deleting DLC product:", error); res.status(500).json({ message: "Failed to delete DLC product" }); } }); app.post('/api/dlc-products/:id/validate', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check if user has permission to validate DLC products (admin, directeur, manager) if (!['admin', 'directeur', 'manager'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions to validate DLC products" }); } const id = parseInt(req.params.id); const dlcProduct = await storage.getDlcProduct(id); if (!dlcProduct) { return res.status(404).json({ message: "DLC Product not found" }); } // For non-admin users, check if they have access to the product's group if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(dlcProduct.groupId)) { return res.status(403).json({ message: "Access denied to this group's DLC products" }); } } console.log('🔍 DLC Validation attempt:', { userId: user.id, userRole: user.role, dlcProductId: id, dlcGroupId: dlcProduct.groupId, userGroups: user.role !== 'admin' ? user.userGroups.map(ug => ug.groupId) : 'all' }); const validatedProduct = await storage.validateDlcProduct(id, user.id); console.log('✅ DLC Product validated successfully by:', user.role, user.id); res.json(validatedProduct); } catch (error) { console.error("Error validating DLC product:", error); res.status(500).json({ message: "Failed to validate DLC product" }); } }); // Route pour marquer un produit DLC comme stock épuisé - accessible à tous app.put('/api/dlc-products/:id/stock-epuise', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const dlcProduct = await storage.getDlcProduct(id); if (!dlcProduct) { return res.status(404).json({ message: "DLC Product not found" }); } // For non-admin users, check if they have access to the product's group if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(dlcProduct.groupId)) { return res.status(403).json({ message: "Access denied to this group's DLC products" }); } } console.log('🔍 DLC Stock épuisé attempt:', { userId: user.id, userRole: user.role, dlcProductId: id, dlcGroupId: dlcProduct.groupId }); const markedProduct = await storage.markDlcProductStockEpuise(id, user.id); console.log('✅ DLC Product marked as stock épuisé by:', user.role, user.id); res.json(markedProduct); } catch (error) { console.error("Error marking DLC product as stock épuisé:", error); res.status(500).json({ message: "Failed to mark DLC product as stock épuisé" }); } }); // Route pour restaurer le stock d'un produit DLC - réservé aux admins, directeurs et managers app.put('/api/dlc-products/:id/restore-stock', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check if user has permission to restore stock (admin, directeur, manager) if (!['admin', 'directeur', 'manager'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions to restore DLC product stock" }); } const id = parseInt(req.params.id); const dlcProduct = await storage.getDlcProduct(id); if (!dlcProduct) { return res.status(404).json({ message: "DLC Product not found" }); } // For non-admin users, check if they have access to the product's group if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(dlcProduct.groupId)) { return res.status(403).json({ message: "Access denied to this group's DLC products" }); } } console.log('🔍 DLC Stock restore attempt:', { userId: user.id, userRole: user.role, dlcProductId: id, dlcGroupId: dlcProduct.groupId }); const restoredProduct = await storage.restoreDlcProductStock(id); console.log('✅ DLC Product stock restored by:', user.role, user.id); res.json(restoredProduct); } catch (error) { console.error("Error restoring DLC product stock:", error); res.status(500).json({ message: "Failed to restore DLC product stock" }); } }); // Route pour marquer un produit DLC comme traité temporairement (expire bientôt) - accessible à tous app.put('/api/dlc-products/:id/mark-processed', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const dlcProduct = await storage.getDlcProduct(id); if (!dlcProduct) { return res.status(404).json({ message: "DLC Product not found" }); } // For non-admin users, check if they have access to the product's group if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(dlcProduct.groupId)) { return res.status(403).json({ message: "Access denied to this group's DLC products" }); } } console.log('🔍 DLC Product mark as processed attempt:', { userId: user.id, userRole: user.role, dlcProductId: id, dlcGroupId: dlcProduct.groupId }); const processedProduct = await storage.markDlcProductAsProcessed(id, user.id); console.log('✅ DLC Product marked as processed by:', user.role, user.id); res.json(processedProduct); } catch (error) { console.error("Error marking DLC product as processed:", error); res.status(500).json({ message: "Failed to mark DLC product as processed" }); } }); // Route pour annuler le traitement temporaire d'un produit DLC - réservé aux admins, directeurs et managers app.put('/api/dlc-products/:id/unmark-processed', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check if user has permission to unmark processed (admin, directeur, manager) if (!['admin', 'directeur', 'manager'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions to unmark DLC product as processed" }); } const id = parseInt(req.params.id); const dlcProduct = await storage.getDlcProduct(id); if (!dlcProduct) { return res.status(404).json({ message: "DLC Product not found" }); } // For non-admin users, check if they have access to the product's group if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(dlcProduct.groupId)) { return res.status(403).json({ message: "Access denied to this group's DLC products" }); } } console.log('🔍 DLC Product unmark processed attempt:', { userId: user.id, userRole: user.role, dlcProductId: id, dlcGroupId: dlcProduct.groupId }); const unprocessedProduct = await storage.unmarkDlcProductAsProcessed(id); console.log('✅ DLC Product unmarked as processed by:', user.role, user.id); res.json(unprocessedProduct); } catch (error) { console.error("Error unmarking DLC product as processed:", error); res.status(500).json({ message: "Failed to unmark DLC product as processed" }); } }); // Tasks routes app.get('/api/tasks', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { storeId } = req.query; let groupIds: number[] | undefined; if (user.role === 'admin') { // Admin can see all tasks or filter by specific store groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { // For directeur and other non-admin users: always restrict to their assigned groups const userGroupIds = user.userGroups.map(ug => ug.groupId); if (storeId) { // If a specific store is requested, verify user has access to it const requestedStoreId = parseInt(storeId as string); if (userGroupIds.includes(requestedStoreId)) { groupIds = [requestedStoreId]; } else { // User doesn't have access to this store, return empty array return res.json([]); } } else { // IMPORTANT FIX: For directeur/manager roles, when no specific store is selected, // we should NOT show data from all their groups. This was causing the issue // where after page refresh, data from multiple groups was displayed. // Return empty result to force explicit store selection for non-admin users. return res.json([]); } } const tasks = await storage.getTasks(groupIds, user.role); res.json(tasks); } catch (error) { console.error("Error fetching tasks:", error); res.status(500).json([]); } }); app.post('/api/tasks', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check permissions for creating tasks if (!hasPermission(user.role, 'tasks', 'create')) { return res.status(403).json({ message: "Permission denied: cannot create tasks" }); } const data = { ...req.body, createdBy: user.id, }; // Assign a default groupId if not provided if (!data.groupId) { if (user.role === 'admin') { data.groupId = 1; // Default for admin } else { const userGroupIds = user.userGroups?.map(ug => ug.groupId) || []; data.groupId = userGroupIds.length > 0 ? userGroupIds[0] : 1; } } // Check if user has access to the group if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map(ug => ug.groupId) || []; if (userGroupIds.length > 0 && !userGroupIds.includes(data.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } const task = await storage.createTask(data); res.json(task); } catch (error) { console.error("Error creating task:", error); res.status(500).json({ message: "Failed to create task" }); } }); app.put('/api/tasks/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const task = await storage.getTask(id); if (!task) { return res.status(404).json({ message: "Task not found" }); } // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(task.groupId)) { return res.status(403).json({ message: "Access denied" }); } } // Nettoyer les données reçues pour éviter les problèmes de types const cleanData: any = {}; // Ne pas utiliser de conditions qui ignorent les valeurs falsy légitimes if (req.body.title !== undefined) cleanData.title = req.body.title; if (req.body.description !== undefined) cleanData.description = req.body.description; if (req.body.priority !== undefined) cleanData.priority = req.body.priority; if (req.body.status !== undefined) cleanData.status = req.body.status; if (req.body.assignedTo !== undefined) cleanData.assignedTo = req.body.assignedTo; if (req.body.startDate !== undefined) { cleanData.startDate = req.body.startDate === '' ? null : req.body.startDate; } if (req.body.dueDate !== undefined) { cleanData.dueDate = req.body.dueDate === '' ? null : req.body.dueDate; } const updatedTask = await storage.updateTask(id, cleanData); res.json(updatedTask); } catch (error) { const taskId = parseInt(req.params.id); console.error("❌ Error updating task in route:", { taskId: taskId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, cleanData: req.body ? { title: req.body.title, description: req.body.description, priority: req.body.priority, status: req.body.status, assignedTo: req.body.assignedTo, startDate: req.body.startDate, dueDate: req.body.dueDate, } : null }); res.status(500).json({ message: "Failed to update task", details: error instanceof Error ? error.message : String(error) }); } }); app.delete('/api/tasks/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const task = await storage.getTask(id); if (!task) { return res.status(404).json({ message: "Task not found" }); } // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(task.groupId)) { return res.status(403).json({ message: "Access denied" }); } } await storage.deleteTask(id); res.json({ message: "Task deleted successfully" }); } catch (error) { console.error("Error deleting task:", error); res.status(500).json({ message: "Failed to delete task" }); } }); app.post('/api/tasks/:id/complete', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const task = await storage.getTask(id); if (!task) { return res.status(404).json({ message: "Task not found" }); } // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(task.groupId)) { return res.status(403).json({ message: "Access denied" }); } } await storage.completeTask(id, user.id); res.json({ message: "Task completed successfully" }); } catch (error) { console.error("Error completing task:", error); res.status(500).json({ message: "Failed to complete task" }); } }); // Customer Orders routes app.get('/api/customer-orders', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { storeId } = req.query; let groupIds: number[] | undefined; if (user.role === 'admin') { groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (storeId && userGroupIds.includes(parseInt(storeId as string))) { groupIds = [parseInt(storeId as string)]; } else { groupIds = userGroupIds; } } const customerOrders = await storage.getCustomerOrders(groupIds); res.json(customerOrders); } catch (error) { console.error("Error fetching customer orders:", error); res.status(500).json([]); } }); app.post('/api/customer-orders', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const data = { ...req.body, createdBy: user.id, }; // Check if user has access to the group if (user.role !== 'admin') { const userGroupIds = user.userGroups ? user.userGroups.map(ug => ug.groupId) : []; // Convert data.groupId to number if it's a string const requestedGroupId = typeof data.groupId === 'string' ? parseInt(data.groupId) : data.groupId; // Allow managers, directeurs, and employees to create orders in their assigned groups if (!['manager', 'directeur', 'employee'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions to create customer orders" }); } if (!userGroupIds.includes(requestedGroupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } const customerOrder = await storage.createCustomerOrder(data); res.json(customerOrder); } catch (error) { console.error("Error creating customer order:", error); res.status(500).json({ message: "Failed to create customer order" }); } }); app.put('/api/customer-orders/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const customerOrder = await storage.getCustomerOrder(id); if (!customerOrder) { return res.status(404).json({ message: "Customer order not found" }); } // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(customerOrder.groupId)) { return res.status(403).json({ message: "Access denied" }); } } const updatedOrder = await storage.updateCustomerOrder(id, req.body); res.json(updatedOrder); } catch (error) { console.error("Error updating customer order:", error); res.status(500).json({ message: "Failed to update customer order" }); } }); app.delete('/api/customer-orders/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const customerOrder = await storage.getCustomerOrder(id); if (!customerOrder) { return res.status(404).json({ message: "Customer order not found" }); } // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(customerOrder.groupId)) { return res.status(403).json({ message: "Access denied" }); } } await storage.deleteCustomerOrder(id); res.json({ message: "Customer order deleted successfully" }); } catch (error) { console.error("Error deleting customer order:", error); res.status(500).json({ message: "Failed to delete customer order" }); } }); // Client call tracking routes app.get('/api/customer-orders/pending-calls', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } let groupIds: number[] | undefined; // Only admin, directeur, and manager can view pending calls if (user.role === 'employee') { return res.status(403).json({ message: "Insufficient permissions" }); } if (user.role === 'admin') { // Admin can optionally filter by store const { storeId } = req.query; if (storeId) { groupIds = [parseInt(storeId as string)]; } } else { // For directeur and manager, filter by their assigned groups const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || []; groupIds = userGroupIds; } const pendingCalls = await storage.getPendingClientCalls(groupIds); res.json(pendingCalls); } catch (error) { console.error("Error fetching pending client calls:", error); res.status(500).json({ message: "Failed to fetch pending client calls" }); } }); app.patch('/api/customer-orders/:id/mark-called', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); // Only admin, directeur, and manager can mark calls if (user.role === 'employee') { return res.status(403).json({ message: "Insufficient permissions" }); } // Check if user has access to this customer order const customerOrder = await storage.getCustomerOrder(id); if (!customerOrder) { return res.status(404).json({ message: "Customer order not found" }); } if (user.role !== 'admin') { const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(customerOrder.groupId)) { return res.status(403).json({ message: "Access denied to this customer order" }); } } const { comment } = req.body; const updatedOrder = await storage.markClientCalled(id, user.id, comment); res.json(updatedOrder); } catch (error) { console.error("Error marking client as called:", error); res.status(500).json({ message: "Failed to mark client as called" }); } }); // Avoir routes app.get('/api/avoirs', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { storeId } = req.query; let groupIds: number[] | undefined; if (user.role === 'admin' || user.role === 'directeur') { groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { // Managers can only see their group's avoirs const userGroupIds = user.userGroups.map(ug => ug.groupId); if (storeId && userGroupIds.includes(parseInt(storeId as string))) { groupIds = [parseInt(storeId as string)]; } else if (!storeId) { groupIds = userGroupIds; } else { return res.json([]); } } const avoirs = await storage.getAvoirs(groupIds); res.json(avoirs); } catch (error) { console.error("Error fetching avoirs:", error); res.status(500).json({ message: "Failed to fetch avoirs" }); } }); app.get('/api/avoirs/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const avoir = await storage.getAvoir(id); if (!avoir) { return res.status(404).json({ message: "Avoir not found" }); } // Check if user has access to this avoir's group if (user.role !== 'admin' && user.role !== 'directeur') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(avoir.groupId)) { return res.status(403).json({ message: "Access denied" }); } } res.json(avoir); } catch (error) { console.error("Error fetching avoir:", error); res.status(500).json({ message: "Failed to fetch avoir" }); } }); app.post('/api/avoirs', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Validate data with Zod schema const validatedData = insertAvoirSchema.parse({ ...req.body, createdBy: user.id, }); // Check if user has access to the specified group if (user.role !== 'admin' && user.role !== 'directeur') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(validatedData.groupId)) { return res.status(403).json({ message: "Access denied to this group" }); } } const avoir = await storage.createAvoir(validatedData); console.log('✅ Avoir created:', avoir.id, 'by user:', user.id); // Send webhook after avoir creation try { const group = await storage.getGroup(avoir.groupId); if (group && group.webhookUrl) { const webhookData = { type: "Avoir", avoirId: avoir.id, invoiceReference: avoir.invoiceReference, amount: avoir.amount, supplierName: "Unknown", // Will be fetched from relations groupName: group.name, comment: avoir.comment || "", commercialProcessed: avoir.commercialProcessed, createdBy: user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : user.username, createdAt: avoir.createdAt }; const webhookResponse = await fetch(group.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(webhookData) }); if (webhookResponse.ok) { await storage.updateAvoirWebhookStatus(avoir.id, true); console.log('✅ Avoir webhook sent successfully:', avoir.id); } else { console.error('❌ Failed to send avoir webhook:', webhookResponse.status); } } } catch (webhookError) { console.error('❌ Error sending avoir webhook:', webhookError); } res.json(avoir); } catch (error) { if (error instanceof z.ZodError) { console.error('❌ [POST AVOIR] ERREURS VALIDATION ZOD:', JSON.stringify(error.errors, null, 2)); return res.status(400).json({ message: "Validation error", errors: error.errors }); } console.error("❌ [POST AVOIR] Erreur générale:", error); res.status(500).json({ message: "Failed to create avoir" }); } }); app.put('/api/avoirs/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const existingAvoir = await storage.getAvoir(id); if (!existingAvoir) { return res.status(404).json({ message: "Avoir not found" }); } // Check permissions if (user.role !== 'admin' && user.role !== 'directeur') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(existingAvoir.groupId)) { return res.status(403).json({ message: "Access denied" }); } } // ✅ CRITICAL FIX: Validate data with Zod schema (partial) const validatedData = insertAvoirSchema.partial().parse(req.body); // ✅ FIX: Convertir undefined en null pour les champs optionnels (important pour PostgreSQL) const dataForDb: any = { ...validatedData, amount: validatedData.amount === undefined ? null : validatedData.amount, invoiceReference: validatedData.invoiceReference === undefined ? null : validatedData.invoiceReference, comment: validatedData.comment === undefined ? null : validatedData.comment, }; const updatedAvoir = await storage.updateAvoir(id, dataForDb); console.log('✅ Avoir updated:', id, 'by user:', user.id); // 🎯 WEBHOOK QUAND STATUT PASSE À "Reçu" if (validatedData.status === 'Reçu' && existingAvoir.status !== 'Reçu') { try { // Utiliser groupe par défaut (1) pour admin si pas de groupe sélectionné const groupId = updatedAvoir.groupId || (user.role === 'admin' ? 1 : updatedAvoir.groupId); const group = await storage.getGroup(groupId); if (group && group.webhookUrl) { const webhookData = { type: "Avoir", avoirId: updatedAvoir.id, invoiceReference: updatedAvoir.invoiceReference, amount: updatedAvoir.amount, supplierName: "Fournisseur", // Sera enrichi avec relations groupName: group.name, comment: updatedAvoir.comment || "", commercialProcessed: updatedAvoir.commercialProcessed, status: "Reçu", createdBy: user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : user.username, processedAt: new Date().toISOString() }; console.log('🌐 Envoi webhook avoir reçu:', { groupId, webhookUrl: group.webhookUrl }); const webhookResponse = await fetch(group.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(webhookData) }); if (webhookResponse.ok) { await storage.updateAvoirWebhookStatus(updatedAvoir.id, true); console.log('✅ Webhook avoir reçu envoyé:', updatedAvoir.id); } else { console.error('❌ Échec envoi webhook avoir:', webhookResponse.status); } } } catch (webhookError) { console.error('❌ Erreur webhook avoir reçu:', webhookError); } } res.json(updatedAvoir); } catch (error) { if (error instanceof z.ZodError) { console.error('❌ [PUT AVOIR] ERREURS VALIDATION ZOD:', JSON.stringify(error.errors, null, 2)); return res.status(400).json({ message: "Validation error", errors: error.errors }); } console.error("Error updating avoir:", error); res.status(500).json({ message: "Failed to update avoir" }); } }); app.delete('/api/avoirs/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const avoir = await storage.getAvoir(id); if (!avoir) { return res.status(404).json({ message: "Avoir not found" }); } // Check permissions (only admin and directeur can delete) if (user.role !== 'admin' && user.role !== 'directeur') { return res.status(403).json({ message: "Insufficient permissions to delete avoirs" }); } // For directeur, check group access if (user.role === 'directeur') { const userGroupIds = user.userGroups.map(ug => ug.groupId); if (!userGroupIds.includes(avoir.groupId)) { return res.status(403).json({ message: "Access denied" }); } } await storage.deleteAvoir(id); console.log('✅ Avoir deleted:', id, 'by user:', user.id); res.json({ message: "Avoir deleted successfully" }); } catch (error) { console.error("Error deleting avoir:", error); res.status(500).json({ message: "Failed to delete avoir" }); } }); // Route de vérification de facture NocoDB pour les avoirs app.post('/api/avoirs/:id/verify-invoice', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const avoirId = parseInt(req.params.id); const avoir = await storage.getAvoir(avoirId); if (!avoir) { return res.status(404).json({ message: "Avoir not found" }); } // Check permissions (using 'deliveries' module for similar logic) if (!hasPermission(user.role, 'deliveries', 'view')) { return res.status(403).json({ message: "Insufficient permissions" }); } // Only admin have access to all avoirs, others must be in the same group if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; if (!userGroupIds.includes(avoir.groupId)) { console.log('🚫 Access denied - User groups check:', { userId: user.id, userRole: user.role, userGroupIds, avoirGroupId: avoir.groupId, avoirSupplier: avoir.supplier?.name }); return res.status(403).json({ message: "Access denied to this group" }); } } const { invoiceReference, forceRefresh } = req.body; if (!avoir.supplier || !avoir.group) { console.log('❌ Avoir manque informations:', { avoirId, hasSupplier: !!avoir.supplier, hasGroup: !!avoir.group }); return res.status(400).json({ message: "Avoir missing supplier or group information" }); } // Vérifier que la référence facture est présente if (!invoiceReference || !invoiceReference.trim()) { return res.status(400).json({ message: "Référence de facture requise" }); } // Vérifier par référence de facture uniquement const result = await invoiceVerificationService.verifyInvoice( invoiceReference, avoir.groupId, forceRefresh || false, false // Les avoirs ne sont pas "réconciliés" comme les livraisons ); res.json(result); } catch (error) { console.error("Error verifying avoir invoice:", error); res.status(500).json({ message: "Failed to verify avoir invoice", error: error instanceof Error ? error.message : 'Unknown error' }); } }); // Avoir status update routes app.put('/api/avoirs/:id/webhook-status', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const { webhookSent } = req.body; // Only allow admin and directeur to update webhook status if (user.role !== 'admin' && user.role !== 'directeur') { return res.status(403).json({ message: "Insufficient permissions" }); } await storage.updateAvoirWebhookStatus(id, webhookSent); res.json({ message: "Webhook status updated successfully" }); } catch (error) { console.error("Error updating avoir webhook status:", error); res.status(500).json({ message: "Failed to update webhook status" }); } }); app.put('/api/avoirs/:id/nocodb-verification', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const { verified } = req.body; // Only allow admin and directeur to update verification status if (user.role !== 'admin' && user.role !== 'directeur') { return res.status(403).json({ message: "Insufficient permissions" }); } await storage.updateAvoirNocodbVerification(id, verified); // Si validé, marquer le cache comme réconcilié (permanent) if (verified) { try { const avoir = await storage.getAvoir(id); if (avoir?.invoiceReference?.trim()) { await invoiceVerificationService.updateCacheAsReconciled( avoir.invoiceReference, avoir.groupId ); console.log('✅ Cache marqué comme réconcilié pour avoir:', id); } } catch (cacheError) { console.error('❌ Erreur marquage cache réconcilié:', cacheError); // Ne pas faire échouer la validation si le cache échoue } } console.log('✅ Avoir NocoDB verification updated:', id, 'verified:', verified, 'by user:', user.id); res.json({ message: "NocoDB verification status updated successfully" }); } catch (error) { console.error("Error updating avoir NocoDB verification:", error); res.status(500).json({ message: "Failed to update verification status" }); } }); // Route pour marquer explicitement le cache comme réconcilié app.post('/api/cache/mark-reconciled', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { invoiceReference, groupId } = req.body; if (!invoiceReference || !groupId) { return res.status(400).json({ message: "Invoice reference and group ID required" }); } // Seuls admin et directeur peuvent marquer comme réconcilié if (user.role !== 'admin' && user.role !== 'directeur') { return res.status(403).json({ message: "Insufficient permissions" }); } await invoiceVerificationService.updateCacheAsReconciled(invoiceReference, groupId); console.log('✅ Cache marqué comme réconcilié:', { invoiceReference, groupId, user: user.id }); res.json({ message: "Cache marked as reconciled successfully" }); } catch (error) { console.error("Error marking cache as reconciled:", error); res.status(500).json({ message: "Failed to mark cache as reconciled" }); } }); // Statistics routes app.get('/api/stats/monthly', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { year, month, storeId } = req.query; const currentYear = year ? parseInt(year as string) : new Date().getFullYear(); const currentMonth = month ? parseInt(month as string) : new Date().getMonth() + 1; let groupIds: number[] | undefined; if (user.role === 'admin') { // Admin can view all stores or filter by selected store groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { // Non-admin users: filter by their assigned groups const userGroupIds = user.userGroups.map(ug => ug.groupId); // If a specific store is selected and user has access, filter by it if (storeId && userGroupIds.includes(parseInt(storeId as string))) { groupIds = [parseInt(storeId as string)]; } else { groupIds = userGroupIds; } } const stats = await storage.getMonthlyStats(currentYear, currentMonth, groupIds); res.json(stats); } catch (error) { console.error("Error fetching stats:", error); res.status(500).json({ message: "Failed to fetch statistics" }); } }); // Route pour les statistiques annuelles app.get('/api/stats/yearly', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { year, storeId } = req.query; const currentYear = year ? parseInt(year as string) : new Date().getFullYear(); let groupIds: number[] | undefined; if (user.role === 'admin') { // Admin can view all stores or filter by selected store groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { // Non-admin users: filter by their assigned groups const userGroupIds = user.userGroups.map(ug => ug.groupId); // If a specific store is selected and user has access, filter by it if (storeId && userGroupIds.includes(parseInt(storeId as string))) { groupIds = [parseInt(storeId as string)]; } else { groupIds = userGroupIds; } } const stats = await storage.getYearlyStats(currentYear, groupIds); res.json(stats); } catch (error) { console.error("Error fetching yearly stats:", error); res.status(500).json({ message: "Failed to fetch yearly statistics" }); } }); // User-Group management routes (admin only) app.post('/api/users/:userId/groups', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Insufficient permissions" }); } const userId = req.params.userId; const data = insertUserGroupSchema.parse({ userId, groupId: req.body.groupId, }); const userGroup = await storage.assignUserToGroup(data); res.json(userGroup); } catch (error) { console.error("Error assigning user to group:", error); res.status(500).json({ message: "Failed to assign user to group" }); } }); app.delete('/api/users/:userId/groups/:groupId', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Insufficient permissions" }); } const userId = req.params.userId; const groupId = parseInt(req.params.groupId); await storage.removeUserFromGroup(userId, groupId); res.json({ message: "User removed from group successfully" }); } catch (error) { console.error("Error removing user from group:", error); res.status(500).json({ message: "Failed to remove user from group" }); } }); // Users management routes app.get('/api/users', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || !['admin', 'directeur', 'manager'].includes(user.role)) { return res.status(403).json({ message: "Access denied" }); } // Tous les utilisateurs avec leurs magasins en une fois (sans le mot de // passe), au lieu d'une lecture par utilisateur const allUsers = await storage.getUsersWithGroups(); const usersWithData = allUsers.map((baseUser) => ({ ...baseUser, userGroups: baseUser.userGroups || [], userRoles: [] // Keep roles simple for now since we're using hardcoded permissions })); res.json(usersWithData); } catch (error) { console.error("❌ Critical error fetching users:", error); console.error("❌ Error stack:", (error as any).stack); // En cas d'erreur, retourner un array vide pour éviter React Error #310 res.status(500).json([]); } }); app.post('/api/users', isAuthenticated, async (req: any, res) => { try { const currentUser = getCurrentUser(req); if (!currentUser || currentUser.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } // Schema création utilisateur SANS champs obligatoires pour résoudre le problème de production const createUserSchema = z.object({ id: z.string().optional(), username: z.string().min(1, "L'identifiant est obligatoire"), email: z.union([z.string().email(), z.literal(""), z.null()]).optional(), firstName: z.string().optional(), lastName: z.string().optional(), password: z.string().min(1, "Le mot de passe est obligatoire"), role: z.enum(['admin', 'directeur', 'manager', 'employee']).optional(), }); const userData = createUserSchema.parse(req.body); // Hash password with improved error handling let hashedPassword = userData.password; if (userData.password) { try { hashedPassword = await hashPasswordSimple(userData.password); } catch (hashError) { console.error('❌ Password hashing failed:', hashError); return res.status(500).json({ message: "Failed to secure password" }); } } // Generate unique ID const newUserId = userData.id || `manual_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const userToCreate = { id: newUserId, username: userData.username, email: userData.email && userData.email.trim() !== '' ? userData.email : undefined, firstName: userData.firstName || '', lastName: userData.lastName || '', password: hashedPassword, role: userData.role || 'employee', }; const newUser = await storage.createUser(userToCreate); res.json(newUser); } catch (error) { console.error("❌ Error creating user:", error); console.error("❌ Error type:", (error as any).constructor?.name); console.error("❌ Error code:", (error as any).code); console.error("❌ Error constraint:", (error as any).constraint); console.error("❌ Error stack:", (error as any).stack); if (error instanceof z.ZodError) { console.log('❌ Validation error:', error.errors); return res.status(400).json({ message: "Invalid user data", errors: error.errors }); } // Handle specific database constraint errors if ((error as any).code === '23505') { if ((error as any).constraint === 'users_username_key') { return res.status(409).json({ message: "Un utilisateur avec ce nom d'utilisateur existe déjà. Veuillez choisir un autre nom d'utilisateur." }); } if ((error as any).constraint === 'users_email_key') { return res.status(409).json({ message: "Un utilisateur avec cette adresse email existe déjà." }); } } // Handle connection errors if ((error as any).code === 'ECONNREFUSED' || (error as any).code === 'ENOTFOUND') { return res.status(503).json({ message: "Database connection error" }); } res.status(500).json({ message: "Failed to create user" }); } }); app.put('/api/users/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } // Schema utilisateur SANS champs obligatoires pour résoudre le problème de production const updateUserSchema = z.object({ username: z.string().optional(), role: z.enum(['admin', 'directeur', 'manager', 'employee']).optional(), firstName: z.union([z.string(), z.literal(""), z.null()]).optional(), lastName: z.union([z.string(), z.literal(""), z.null()]).optional(), email: z.union([z.string().email(), z.literal(""), z.null()]).optional(), password: z.string().optional(), }); const userData = updateUserSchema.parse(req.body); // Clean up the data - handle empty emails properly const cleanUserData: any = { ...userData }; // Handle email field - convert empty string to undefined if (cleanUserData.email !== undefined) { cleanUserData.email = cleanUserData.email && cleanUserData.email.trim() !== '' ? cleanUserData.email : undefined; } // Hash password if provided if (cleanUserData.password) { try { cleanUserData.password = await hashPasswordSimple(cleanUserData.password); cleanUserData.passwordChanged = true; } catch (hashError) { console.error('❌ Password hashing failed:', hashError); return res.status(500).json({ message: "Failed to secure password" }); } } const updatedUser = await storage.updateUser(req.params.id, cleanUserData); res.json(updatedUser); } catch (error: any) { console.error("❌ Error updating user:", error); console.error("❌ Error type:", (error as any).constructor?.name); console.error("❌ Error code:", (error as any).code); console.error("❌ Error constraint:", (error as any).constraint); console.error("❌ Error stack:", (error as any).stack); if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid user data", errors: error.errors }); } // Handle specific database constraint errors if ((error as any).code === '23505') { if ((error as any).constraint === 'users_username_key') { return res.status(409).json({ message: "Un utilisateur avec ce nom d'utilisateur existe déjà. Veuillez choisir un autre nom d'utilisateur." }); } if ((error as any).constraint === 'users_email_key') { return res.status(409).json({ message: "Un utilisateur avec cette adresse email existe déjà. Veuillez utiliser une autre adresse email ou laisser le champ vide." }); } } res.status(500).json({ message: "Failed to update user" }); } }); app.post('/api/users/:id/groups', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const { groupId } = req.body; await storage.assignUserToGroup({ userId: req.params.id, groupId: parseInt(groupId), }); res.json({ message: "User assigned to group successfully" }); } catch (error) { console.error("Error assigning user to group:", error); res.status(500).json({ message: "Failed to assign user to group" }); } }); app.delete('/api/users/:id/groups/:groupId', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } await storage.removeUserFromGroup(req.params.id, parseInt(req.params.groupId)); res.json({ message: "User removed from group successfully" }); } catch (error) { console.error("Error removing user from group:", error); res.status(500).json({ message: "Failed to remove user from group" }); } }); // Delete user route app.delete('/api/users/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const userToDelete = req.params.id; // Prevent admin from deleting themselves if (userToDelete === user.id) { return res.status(400).json({ message: "Vous ne pouvez pas supprimer votre propre compte" }); } // Remove user from all groups first await storage.removeUserFromAllGroups(userToDelete); // Delete the user await storage.deleteUser(userToDelete); res.json({ message: "Utilisateur supprimé avec succès" }); } catch (error) { console.error("Error deleting user:", error); res.status(500).json({ message: "Failed to delete user" }); } }); // Publicity routes (renamed to ad-campaigns to avoid adblocker issues) app.get('/api/ad-campaigns/debug', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { year, storeId } = req.query; const filterYear = year ? parseInt(year as string) : undefined; // DEBUG: Log pour identifier le problème avec 2025 console.log(`📋 API AD-CAMPAIGNS DEBUG REQUEST:`, { year, filterYear, storeId, userRole: user.role }); let groupIds: number[] | undefined; if (user.role === 'admin') { // Admin can view all publicities or filter by selected store groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { // Non-admin users: filter by their assigned groups groupIds = user.userGroups.map(ug => ug.groupId); } console.log(`📋 CALLING storage.getPublicities:`, { filterYear, groupIds }); const publicities = await storage.getPublicities(filterYear, groupIds); console.log(`📋 PUBLICITIES RETURNED:`, { count: publicities.length }); res.json(publicities); } catch (error) { console.error("❌ ERROR fetching publicities:", error); res.status(500).json({ message: "Failed to fetch publicities", error: (error as Error).message }); } }); app.get('/api/ad-campaigns/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const id = parseInt(req.params.id); const publicity = await storage.getPublicity(id); if (!publicity) { return res.status(404).json({ message: "Publicity not found" }); } // Check access permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); const hasAccess = publicity.participations.some((p: any) => userGroupIds.includes(p.groupId)); if (!hasAccess) { return res.status(403).json({ message: "Access denied" }); } } res.json(publicity); } catch (error) { console.error("Error fetching publicity:", error); res.status(500).json({ message: "Failed to fetch publicity" }); } }); // Get all publicities (with optional year and store filtering) app.get('/api/ad-campaigns', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { year, storeId } = req.query; let groupIds: number[] | undefined; // Determine which groups to filter by if (user.role === 'admin') { // Admins can filter by specific store or see all groupIds = storeId ? [parseInt(storeId as string)] : undefined; } else { // Non-admins see only their assigned groups const userGroupIds = user.userGroups.map(ug => ug.groupId); if (storeId && userGroupIds.includes(parseInt(storeId as string))) { groupIds = [parseInt(storeId as string)]; } else { groupIds = userGroupIds; } } const yearNum = year ? parseInt(year as string) : undefined; const publicities = await storage.getPublicities(yearNum, groupIds); res.json(publicities); } catch (error) { console.error("Error fetching publicities:", error); res.status(500).json([]); } }); app.post('/api/ad-campaigns', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check permissions using the shared permission system if (!hasPermission(user.role, 'publicity', 'create')) { return res.status(403).json({ message: "Insufficient permissions" }); } const data = insertPublicitySchema.parse({ ...req.body, createdBy: req.user.claims ? req.user.claims.sub : req.user.id }); const { participatingGroups, ...publicityData } = req.body; // Create publicity const newPublicity = await storage.createPublicity(data); // Set participations if (participatingGroups && participatingGroups.length > 0) { await storage.setPublicityParticipations(newPublicity.id, participatingGroups); } // Get the complete publicity with relations const completePublicity = await storage.getPublicity(newPublicity.id); res.json(completePublicity); } catch (error) { console.error("Error creating publicity:", error); res.status(500).json({ message: "Failed to create publicity" }); } }); app.put('/api/ad-campaigns/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check permissions using the shared permission system if (!hasPermission(user.role, 'publicity', 'edit')) { return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(req.params.id); const { participatingGroups, ...publicityData } = req.body; // Update publicity const updatedPublicity = await storage.updatePublicity(id, publicityData); // Update participations if (participatingGroups !== undefined) { await storage.setPublicityParticipations(id, participatingGroups); } // Get the complete publicity with relations const completePublicity = await storage.getPublicity(id); res.json(completePublicity); } catch (error) { console.error("Error updating publicity:", error); res.status(500).json({ message: "Failed to update publicity" }); } }); app.delete('/api/ad-campaigns/:id', isAuthenticated, async (req: any, res) => { const publicityId = req.params.id; console.log(`🗑️ [API] DELETE request received for publicity ID: ${publicityId} (ad-campaigns)`); console.log(`🗑️ [API] User info:`, { hasUser: !!req.user, userId: req.user?.id || req.user?.claims?.sub, method: req.method, url: req.url, headers: { 'content-type': req.headers['content-type'] } }); try { const user = getCurrentUser(req); if (!user) { console.log(`❌ [API] User not found for publicity deletion: ${publicityId}`); return res.status(404).json({ message: "User not found" }); } console.log(`🗑️ [API] User found:`, { id: user.id, role: user.role, name: user.name }); // Check permissions (admin only for deletion) if (user.role !== 'admin') { console.log(`❌ [API] Insufficient permissions for publicity deletion: ${publicityId}, user role: ${user.role}`); return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(publicityId); console.log(`🗑️ [API] Admin ${user.name} (${user.id}) attempting to delete publicity ${id}`); await storage.deletePublicity(id); console.log(`✅ [API] Successfully deleted publicity ${id} by admin ${user.name}`); res.json({ message: "Publicity deleted successfully" }); } catch (error) { console.error(`❌ [API] Error deleting publicity ${publicityId}:`, error); res.status(500).json({ message: "Failed to delete publicity", error: (error as Error).message }); } }); // Alternative DELETE route using POST (for production environments that block DELETE) app.post('/api/ad-campaigns/:id/delete', isAuthenticated, async (req: any, res) => { const publicityId = req.params.id; console.log(`🗑️ [API-POST] DELETE via POST request received for publicity ID: ${publicityId} (ad-campaigns)`); console.log(`🗑️ [API-POST] User info:`, { hasUser: !!req.user, userId: req.user?.id || req.user?.claims?.sub, method: req.method, url: req.url }); try { const user = getCurrentUser(req); if (!user) { console.log(`❌ [API-POST] User not found for publicity deletion: ${publicityId}`); return res.status(404).json({ message: "User not found" }); } console.log(`🗑️ [API-POST] User found:`, { id: user.id, role: user.role, name: user.name }); // Check permissions (admin only for deletion) if (user.role !== 'admin') { console.log(`❌ [API-POST] Insufficient permissions for publicity deletion: ${publicityId}, user role: ${user.role}`); return res.status(403).json({ message: "Insufficient permissions" }); } const id = parseInt(publicityId); console.log(`🗑️ [API-POST] Admin ${user.name} (${user.id}) attempting to delete publicity ${id} via POST`); await storage.deletePublicity(id); console.log(`✅ [API-POST] Successfully deleted publicity ${id} by admin ${user.name} via POST`); res.json({ message: "Publicity deleted successfully" }); } catch (error) { console.error(`❌ [API-POST] Error deleting publicity ${publicityId} via POST:`, error); res.status(500).json({ message: "Failed to delete publicity", error: (error as Error).message }); } }); // Schema logging route for production debugging app.get('/api/debug/log-schema', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: 'Accès refusé. Seuls les administrateurs peuvent utiliser cette route de debug.' }); } const isProduction = process.env.NODE_ENV === 'production'; if (!isProduction) { return res.status(400).json({ error: 'Cette route fonctionne uniquement en production sur votre serveur privé' }); } console.log('\n🔍 ===== DÉBUT SCAN SCHÉMA BASE DE DONNÉES ====='); console.log('⏰ Timestamp:', new Date().toISOString()); // Vérifier que pool est disponible if (!pool) { throw new Error('Pool de base de données non disponible en production'); } // Récupérer les tables const tablesQuery = ` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name `; const tablesResult = await pool.query(tablesQuery); console.log(`\n📊 TOTAL DES TABLES TROUVÉES: ${tablesResult.rows.length}`); console.log('=========================================='); // Pour chaque table, récupérer les colonnes for (const tableRow of tablesResult.rows) { const tableName = tableRow.table_name; console.log(`\n🔸 TABLE: ${tableName.toUpperCase()}`); console.log(`${'='.repeat(tableName.length + 8)}`); const columnsQuery = ` SELECT column_name, data_type, character_maximum_length, is_nullable, column_default, ordinal_position FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position `; const columnsResult = await pool.query(columnsQuery, [tableName]); if (columnsResult.rows.length > 0) { columnsResult.rows.forEach((col: any, index: number) => { const nullable = col.is_nullable === 'YES' ? 'NULL' : 'NOT NULL'; const maxLength = col.character_maximum_length ? `(${col.character_maximum_length})` : ''; const defaultVal = col.column_default ? ` DEFAULT ${col.column_default}` : ''; console.log(` ${(index + 1).toString().padStart(2, '0')}. ${col.column_name.padEnd(25)} : ${col.data_type}${maxLength} ${nullable}${defaultVal}`); }); // Compter les enregistrements dans la table try { const countQuery = `SELECT COUNT(*) as total FROM "${tableName}"`; const countResult = await pool.query(countQuery); console.log(` 📈 Nombre d'enregistrements: ${countResult.rows[0].total}`); } catch (countError) { console.log(` ⚠️ Impossible de compter les enregistrements: ${(countError as Error).message}`); } } } // Récupérer les contraintes de clés étrangères console.log('\n🔗 CONTRAINTES DE CLÉS ÉTRANGÈRES:'); console.log('==================================='); const fkQuery = ` SELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name, tc.constraint_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' ORDER BY tc.table_name, kcu.column_name `; const fkResult = await pool.query(fkQuery); if (fkResult.rows.length > 0) { fkResult.rows.forEach((fk: any) => { console.log(` ${fk.table_name}.${fk.column_name} → ${fk.foreign_table_name}.${fk.foreign_column_name}`); }); } else { console.log(' Aucune contrainte de clé étrangère trouvée'); } console.log('\n🏁 ===== FIN SCAN SCHÉMA BASE DE DONNÉES =====\n'); res.json({ success: true, message: 'Schéma loggé avec succès dans les logs du serveur', totalTables: tablesResult.rows.length, totalForeignKeys: fkResult.rows.length, timestamp: new Date().toISOString(), downloadUrl: '/api/debug/download-schema' }); } catch (error) { console.error('❌ ERREUR lors du scan du schéma:', error); res.status(500).json({ error: (error as Error).message }); } }); // Download database schema report app.get('/api/debug/download-schema', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: 'Accès refusé. Seuls les administrateurs peuvent télécharger le rapport de schéma.' }); } const isProduction = process.env.NODE_ENV === 'production'; if (!isProduction) { return res.status(400).json({ error: 'Cette route fonctionne uniquement en production sur votre serveur privé' }); } // Vérifier que pool est disponible if (!pool) { throw new Error('Pool de base de données non disponible en production'); } const timestamp = new Date().toISOString(); let reportContent = `LOGIFLOW - RAPPORT SCHÉMA BASE DE DONNÉES PRODUCTION =============================================== GÉNÉRÉ LE : ${timestamp} SERVEUR : Production privé LogiFlow ENVIRONNEMENT : PostgreSQL Docker UTILISATEUR : ${user.username} =============================================== RÉSUMÉ DU SCAN =============================================== `; // Récupérer les tables const tablesQuery = ` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name `; const tablesResult = await pool.query(tablesQuery); reportContent += `TOTAL DES TABLES TROUVÉES: ${tablesResult.rows.length}\n`; reportContent += `==========================================\n\n`; // Pour chaque table, récupérer les colonnes for (const tableRow of tablesResult.rows) { const tableName = tableRow.table_name; reportContent += `TABLE: ${tableName.toUpperCase()}\n`; reportContent += `${'='.repeat(tableName.length + 6)}\n\n`; const columnsQuery = ` SELECT column_name, data_type, character_maximum_length, is_nullable, column_default, ordinal_position FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position `; const columnsResult = await pool.query(columnsQuery, [tableName]); if (columnsResult.rows.length > 0) { columnsResult.rows.forEach((col: any, index: number) => { const nullable = col.is_nullable === 'YES' ? 'NULL' : 'NOT NULL'; const maxLength = col.character_maximum_length ? `(${col.character_maximum_length})` : ''; const defaultVal = col.column_default ? ` DEFAULT ${col.column_default}` : ''; reportContent += ` ${(index + 1).toString().padStart(2, '0')}. ${col.column_name.padEnd(25)} : ${col.data_type}${maxLength} ${nullable}${defaultVal}\n`; }); // Compter les enregistrements dans la table try { const countQuery = `SELECT COUNT(*) as total FROM "${tableName}"`; const countResult = await pool.query(countQuery); reportContent += ` 📈 Nombre d'enregistrements: ${countResult.rows[0].total}\n\n`; } catch (countError) { reportContent += ` ⚠️ Impossible de compter les enregistrements: ${(countError as Error).message}\n\n`; } } } // Récupérer les contraintes de clés étrangères reportContent += `CONTRAINTES DE CLÉS ÉTRANGÈRES:\n`; reportContent += `===================================\n\n`; const fkQuery = ` SELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name, tc.constraint_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' ORDER BY tc.table_name, kcu.column_name `; const fkResult = await pool.query(fkQuery); if (fkResult.rows.length > 0) { fkResult.rows.forEach((fk: any) => { reportContent += ` ${fk.table_name}.${fk.column_name} → ${fk.foreign_table_name}.${fk.foreign_column_name}\n`; }); } else { reportContent += ' Aucune contrainte de clé étrangère trouvée\n'; } reportContent += `\n===============================================\n`; reportContent += `FIN DU RAPPORT - ${timestamp}\n`; reportContent += `===============================================`; // Définir les headers pour le téléchargement const filename = `logiflow-schema-${new Date().toISOString().split('T')[0]}.txt`; res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.send(reportContent); } catch (error) { console.error('❌ ERREUR lors de la génération du rapport de schéma:', error); res.status(500).json({ error: (error as Error).message }); } }); // Invoice verification routes app.post('/api/verify-invoice', isAuthenticated, async (req: any, res) => { try { const { groupId, invoiceReference } = req.body; if (!groupId || !invoiceReference) { return res.status(400).json({ message: "groupId and invoiceReference are required" }); } console.log('🔍 Vérification facture demandée:', { groupId, invoiceReference }); const { InvoiceVerificationService } = await import('./invoiceVerification.js'); const verificationService = new InvoiceVerificationService(); const result = await verificationService.verifyInvoice(invoiceReference, groupId); console.log('✅ Résultat vérification:', result); res.json(result); } catch (error) { console.error("❌ Error verifying invoice:", error); res.status(500).json({ message: "Failed to verify invoice" }); } }); app.post('/api/verify-invoices', isAuthenticated, async (req: any, res) => { try { const { invoiceReferences } = req.body; if (!Array.isArray(invoiceReferences)) { return res.status(400).json({ message: "invoiceReferences must be an array" }); } // Add supplier name to invoice references for verification const enrichedReferences = invoiceReferences.map((ref: any) => ({ ...ref, supplierName: ref.supplierName // Include supplier name for matching })); const { verifyMultipleInvoiceReferences } = await import('./nocodbService.js'); const results = await verifyMultipleInvoiceReferences(enrichedReferences); res.json(results); } catch (error) { console.error("Error verifying invoices:", error); res.status(500).json({ message: "Failed to verify invoices" }); } }); // NocoDB Configuration routes app.get('/api/nocodb-config', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: 'Accès refusé. Seuls les administrateurs peuvent gérer les configurations NocoDB.' }); } const configs = await storage.getNocodbConfigs(); res.json(configs); } catch (error) { console.error('Error fetching NocoDB configs:', error); res.status(500).json({ error: 'Failed to fetch configurations' }); } }); app.post('/api/nocodb-config', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: 'Accès refusé. Seuls les administrateurs peuvent gérer les configurations NocoDB.' }); } const configData = insertNocodbConfigSchema.parse({ ...req.body, createdBy: user.id }); const config = await storage.createNocodbConfig(configData); res.status(201).json(config); } catch (error) { console.error('Error creating NocoDB config:', error); res.status(500).json({ error: 'Failed to create configuration' }); } }); app.put('/api/nocodb-config/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: 'Accès refusé. Seuls les administrateurs peuvent gérer les configurations NocoDB.' }); } const id = parseInt(req.params.id); const configData = insertNocodbConfigSchema.partial().parse(req.body); const config = await storage.updateNocodbConfig(id, configData); res.json(config); } catch (error) { console.error('Error updating NocoDB config:', error); res.status(500).json({ error: 'Failed to update configuration' }); } }); app.delete('/api/nocodb-config/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: 'Accès refusé. Seuls les administrateurs peuvent gérer les configurations NocoDB.' }); } const id = parseInt(req.params.id); await storage.deleteNocodbConfig(id); res.status(204).send(); } catch (error) { console.error('Error deleting NocoDB config:', error); res.status(500).json({ error: 'Failed to delete configuration' }); } }); app.get('/api/nocodb-config/active', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(403).json({ message: 'Accès refusé.' }); } const activeConfig = await storage.getActiveNocodbConfig(); res.json(activeConfig || null); } catch (error) { console.error('Error fetching active NocoDB config:', error); res.status(500).json({ error: 'Failed to fetch active configuration' }); } }); // Backup management routes (Admin only) app.get('/api/backups', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const backups = await backupService.getBackupList(); res.json(backups); } catch (error) { console.error("Error fetching backups:", error); res.status(500).json({ message: "Failed to fetch backups" }); } }); app.post('/api/backups', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const backup = await backupService.createBackup('manual', user.id); res.json(backup); } catch (error) { console.error("Error creating backup:", error); res.status(500).json({ message: "Failed to create backup" }); } }); app.get('/api/backups/:filename/download', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const { filename } = req.params; const filepath = await backupService.downloadBackup(filename); res.download(filepath, filename, (err: any) => { if (err) { console.error("Error downloading backup:", err); res.status(404).json({ message: "Backup file not found" }); } }); } catch (error) { console.error("Error downloading backup:", error); res.status(500).json({ message: "Failed to download backup" }); } }); app.delete('/api/backups/:filename', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const { filename } = req.params; await backupService.deleteBackup(filename); res.json({ message: "Backup deleted successfully" }); } catch (error) { console.error("Error deleting backup:", error); res.status(500).json({ message: "Failed to delete backup" }); } }); // SAV (Service Après-Vente) routes app.get('/api/sav/tickets', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Get user's group IDs for filtering let groupIds: number[] = []; if (user.role === 'admin') { // Admin can see all tickets if no specific store is selected // But if a store is selected in the UI, filter by that store const selectedGroupId = req.query.groupId ? parseInt(req.query.groupId) : null; if (selectedGroupId) { groupIds = [selectedGroupId]; } else { groupIds = []; // See all tickets } } else { // Other roles see only their assigned groups const userGroups = (user as any).userGroups; groupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; } // Parse query filters const filters = { groupIds: groupIds.length > 0 ? groupIds : undefined, status: req.query.status as string, supplierId: req.query.supplierId ? parseInt(req.query.supplierId) : undefined, priority: req.query.priority as string, startDate: req.query.startDate as string, endDate: req.query.endDate as string, }; const tickets = await storage.getSavTickets(filters); res.json(tickets); } catch (error) { console.error("Error fetching SAV tickets:", error); res.status(500).json({ message: "Failed to fetch tickets" }); } }); app.get('/api/sav/tickets/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const ticketId = parseInt(req.params.id); const ticket = await storage.getSavTicket(ticketId); if (!ticket) { return res.status(404).json({ message: "Ticket not found" }); } // Check if user can access this ticket if (user.role !== 'admin') { const userGroups = (user as any).userGroups; const userGroupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; if (!userGroupIds.includes(ticket.groupId)) { return res.status(403).json({ message: "Access denied to this ticket" }); } } res.json(ticket); } catch (error) { console.error("Error fetching SAV ticket:", error); res.status(500).json({ message: "Failed to fetch ticket" }); } }); app.post('/api/sav/tickets', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check permissions: admin, manager, directeur can create (employee can only view) if (!['admin', 'manager', 'directeur'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions to create tickets" }); } // Get user's groups for automatic assignment let availableGroupIds: number[] = []; if (user.role === 'admin') { const allGroups = await storage.getGroups(); availableGroupIds = allGroups.map(g => g.id); } else { const userGroups = (user as any).userGroups; availableGroupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; } if (availableGroupIds.length === 0) { return res.status(400).json({ message: "No groups available for ticket creation" }); } // Generate unique ticket number const now = new Date(); const ticketNumber = `SAV-${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}-${Date.now().toString().slice(-6)}`; // Parse and validate request body const assignedGroupId = req.body.groupId || availableGroupIds[0]; const ticketData = insertSavTicketSchema.parse({ ...req.body, groupId: assignedGroupId, // Use first available group if not specified createdBy: user.id, }); // Add the generated ticket number const ticketDataWithNumber = { ...ticketData, ticketNumber, }; // Validate that the specified group is accessible to the user if (!availableGroupIds.includes(ticketData.groupId)) { return res.status(403).json({ message: "Access denied to the specified group" }); } const ticket = await storage.createSavTicket(ticketDataWithNumber); res.status(201).json(ticket); } catch (error) { console.error("Error creating SAV ticket:", error); if (error instanceof z.ZodError) { return res.status(400).json({ message: "Validation error", errors: error.errors }); } res.status(500).json({ message: "Failed to create ticket" }); } }); app.patch('/api/sav/tickets/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check permissions: admin, manager, directeur can modify if (!['admin', 'manager', 'directeur'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions to modify tickets" }); } const ticketId = parseInt(req.params.id); const existingTicket = await storage.getSavTicket(ticketId); if (!existingTicket) { return res.status(404).json({ message: "Ticket not found" }); } // Check if user can access this ticket if (user.role !== 'admin') { const userGroups = (user as any).userGroups; const userGroupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; if (!userGroupIds.includes(existingTicket.groupId)) { return res.status(403).json({ message: "Access denied to modify this ticket" }); } } // Parse and validate partial update data const updateData = insertSavTicketSchema.partial().parse(req.body); const updatedTicket = await storage.updateSavTicket(ticketId, updateData); // Add history entry for status change if status was modified if (req.body.status && req.body.status !== existingTicket.status) { await storage.addSavTicketHistory({ ticketId: ticketId, action: 'status_change', description: `Statut changé de "${existingTicket.status}" vers "${req.body.status}"`, createdBy: user.id, }); } res.json(updatedTicket); } catch (error) { console.error("Error updating SAV ticket:", error); if (error instanceof z.ZodError) { return res.status(400).json({ message: "Validation error", errors: error.errors }); } res.status(500).json({ message: "Failed to update ticket" }); } }); app.delete('/api/sav/tickets/:id', isAuthenticated, async (req: any, res) => { try { const userId = req.user.claims ? req.user.claims.sub : req.user.id; const user = await storage.getUser(userId); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check permissions: only admin and directeur can delete if (!['admin', 'directeur'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions to delete tickets" }); } const ticketId = parseInt(req.params.id); const existingTicket = await storage.getSavTicket(ticketId); if (!existingTicket) { return res.status(404).json({ message: "Ticket not found" }); } await storage.deleteSavTicket(ticketId); res.status(204).send(); } catch (error) { console.error("Error deleting SAV ticket:", error); res.status(500).json({ message: "Failed to delete ticket" }); } }); app.post('/api/sav/tickets/:id/history', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Check permissions: admin, manager, directeur can add comments if (!['admin', 'manager', 'directeur'].includes(user.role)) { return res.status(403).json({ message: "Insufficient permissions to add comments" }); } const ticketId = parseInt(req.params.id); const ticket = await storage.getSavTicket(ticketId); if (!ticket) { return res.status(404).json({ message: "Ticket not found" }); } // Check if user can access this ticket if (user.role !== 'admin') { const userGroups = (user as any).userGroups; const userGroupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; if (!userGroupIds.includes(ticket.groupId)) { return res.status(403).json({ message: "Access denied to add comments to this ticket" }); } } // Parse and validate history entry // const historyData = insertSavTicketHistorySchema.parse({ // ticketId: ticketId, // action: 'comment', // description: req.body.description, // createdBy: user.id, // }); const history = await storage.addSavTicketHistory({ ticketId: ticketId, action: 'comment', description: req.body.description, createdBy: user.id, }); res.status(201).json(history); } catch (error) { console.error("Error adding SAV ticket history:", error); if (error instanceof z.ZodError) { return res.status(400).json({ message: "Validation error", errors: error.errors }); } res.status(500).json({ message: "Failed to add comment" }); } }); app.get('/api/sav/stats', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Get user's group IDs for filtering let groupIds: number[] = []; if (user.role === 'admin') { // Admin can see all stats if no specific store is selected // But if a store is selected in the UI, filter by that store const selectedGroupId = req.query.groupId ? parseInt(req.query.groupId) : null; if (selectedGroupId) { groupIds = [selectedGroupId]; } else { groupIds = []; // See all stats } } else { // Other roles see only their assigned groups const userGroups = (user as any).userGroups; groupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; } const stats = await storage.getSavTicketStats(groupIds.length > 0 ? groupIds : undefined); res.json(stats); } catch (error) { console.error("Error fetching SAV stats:", error); res.status(500).json({ message: "Failed to fetch stats" }); } }); // Emergency migration route for SAV priority column // SÉCURITÉ : réservée aux administrateurs authentifiés (était accessible sans login) app.post('/api/admin/emergency-migration', isAuthenticated, async (req: any, res) => { try { const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Insufficient permissions" }); } console.log('🚨 EMERGENCY: Forcing SAV migration execution...'); // Import migration function const { runProductionMigrations } = await import('./migrations.production.js'); // Force run the migration await runProductionMigrations(); console.log('✅ EMERGENCY: Migration executed successfully'); res.json({ success: true, message: 'Emergency migration executed successfully', timestamp: new Date().toISOString() }); } catch (error) { console.error('❌ EMERGENCY: Migration failed:', error); res.status(500).json({ success: false, error: 'Emergency migration failed', details: (error as Error).message, timestamp: new Date().toISOString() }); } }); // Weather routes app.get('/api/weather/settings', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const settings = await storage.getWeatherSettings(); res.json(settings); } catch (error) { console.error("Error fetching weather settings:", error); res.status(500).json({ message: "Failed to fetch weather settings" }); } }); app.post('/api/weather/settings', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const data = insertWeatherSettingsSchema.parse(req.body); const settings = await storage.createWeatherSettings(data); res.json(settings); } catch (error) { console.error("Error creating weather settings:", error); if (error instanceof z.ZodError) { return res.status(400).json({ message: "Validation error", errors: error.errors }); } res.status(500).json({ message: "Failed to create weather settings" }); } }); app.put('/api/weather/settings/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const id = parseInt(req.params.id); const data = insertWeatherSettingsSchema.partial().parse(req.body); const settings = await storage.updateWeatherSettings(id, data); res.json(settings); } catch (error) { console.error("Error updating weather settings:", error); if (error instanceof z.ZodError) { return res.status(400).json({ message: "Validation error", errors: error.errors }); } res.status(500).json({ message: "Failed to update weather settings" }); } }); app.post('/api/weather/test-connection', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } const { apiKey, location } = req.body; if (!apiKey || !location) { return res.status(400).json({ message: "API key and location are required" }); } const result = await weatherService.testApiConnection(apiKey, location); res.json(result); } catch (error) { console.error("Error testing weather API connection:", error); res.status(500).json({ message: "Failed to test API connection" }); } }); app.get('/api/weather/current', isAuthenticated, async (req: any, res) => { try { const settings = await storage.getWeatherSettings(); if (!settings || !settings.isActive) { return res.status(404).json({ message: "Weather service not configured or disabled" }); } const today = new Date().toISOString().split('T')[0]; const previousYearDate = weatherService.getPreviousYearDate(); // Lectures du cache indépendantes : en parallèle let [currentYearData, previousYearData] = await Promise.all([ storage.getWeatherData(today, true), storage.getWeatherData(previousYearDate, false), ]); // Les appels à l'API météo restent séquentiels : lancés en parallèle, // Visual Crossing peut refuser la seconde requête d'une même clé (limite // de requêtes simultanées) et la comparaison avec l'an dernier disparaît. // Fetch current year data if not in cache if (!currentYearData) { console.log("🌤️ [FETCH] Fetching current weather data from API"); const apiData = await weatherService.fetchCurrentWeather(settings); if (apiData) { const weatherData = weatherService.convertApiDataToWeatherData(apiData, settings.location, true); if (weatherData) { try { currentYearData = await storage.createWeatherData(weatherData); console.log("✅ [CACHE] Current year data saved to cache"); } catch (error: any) { console.warn("⚠️ [CACHE] Could not save current year data (may already exist):", error.message); // Récupérer les données existantes au lieu de créer ; à défaut, // afficher quand même la réponse de l'API currentYearData = (await storage.getWeatherData(today, true)) ?? (weatherData as any); } } } } // Fetch previous year data if not in cache if (!previousYearData) { console.log("🌤️ [FETCH] Fetching previous year weather data from API"); const apiData = await weatherService.fetchPreviousYearWeather(settings, previousYearDate); if (apiData) { const weatherData = weatherService.convertApiDataToWeatherData(apiData, settings.location, false); if (weatherData) { try { previousYearData = await storage.createWeatherData(weatherData); console.log("✅ [CACHE] Previous year data saved to cache"); } catch (error: any) { console.warn("⚠️ [CACHE] Could not save previous year data (may already exist):", error.message); // Récupérer les données existantes au lieu de créer ; à défaut, // afficher quand même la réponse de l'API plutôt que de la perdre previousYearData = (await storage.getWeatherData(previousYearDate, false)) ?? (weatherData as any); } } else { console.warn(`⚠️ [HISTORY] Empty historical response for ${previousYearDate}`); } } else { console.warn("⚠️ [HISTORY] Could not fetch historical data - continuing with current year only"); } } // Repli : si l'API historique a échoué (quota épuisé, panne, plan sans // accès à l'historique), reprendre la ligne en cache la plus proche de // la date cible plutôt que de faire disparaître la comparaison N-1 if (!previousYearData) { previousYearData = await storage.getNearestWeatherData(previousYearDate, false, 7); if (previousYearData) { console.log(`🌤️ [FALLBACK] Using nearest cached previous-year data (${previousYearData.date}) for target ${previousYearDate}`); } } // Second repli : la météo relevée l'an dernier, au jour le jour, quand ces // dates étaient « aujourd'hui » (lignes is_current_year = true datées de // l'année précédente). Elle reste disponible même si l'API refuse // durablement l'historique (plan, quota). if (!previousYearData) { previousYearData = await storage.getNearestWeatherData(previousYearDate, true, 7); if (previousYearData) { console.log(`🌤️ [FALLBACK] Using weather recorded last year (${previousYearData.date}) for target ${previousYearDate}`); } else { console.warn(`⚠️ [FALLBACK] No previous-year weather available around ${previousYearDate}`); } } // Toujours retourner une réponse même si seule l'année actuelle est disponible const response = { currentYear: currentYearData ? { ...currentYearData, maxTemperature: parseFloat(currentYearData.tempMax), minTemperature: parseFloat(currentYearData.tempMin), condition: currentYearData.conditions, icon: weatherService.getWeatherIcon(currentYearData.icon) } : null, previousYear: previousYearData ? { ...previousYearData, maxTemperature: parseFloat(previousYearData.tempMax), minTemperature: parseFloat(previousYearData.tempMin), condition: previousYearData.conditions, icon: weatherService.getWeatherIcon(previousYearData.icon) } : null, location: settings.location }; res.json(response); } catch (error) { console.error("Error fetching weather data:", error); res.status(500).json({ message: "Failed to fetch weather data" }); } }); // Route de géolocalisation météo // Announcement routes - PostgreSQL en production, mémoire en développement app.get('/api/announcements', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } if (environment === 'production') { // PRODUCTION: Utiliser PostgreSQL avec DASHBOARD_MESSAGES try { // Auteur et magasin joints dans la même requête, au lieu de deux // lectures par message (username et id de magasin sont uniques) let query = db.select({ id: dashboardMessages.id, title: dashboardMessages.title, content: dashboardMessages.content, type: dashboardMessages.type, storeId: dashboardMessages.storeId, createdBy: dashboardMessages.createdBy, createdAt: dashboardMessages.createdAt, authorId: users.id, authorUsername: users.username, authorFirstName: users.firstName, authorLastName: users.lastName, authorName: users.name, groupId: groups.id, groupName: groups.name, }) .from(dashboardMessages) // En production, createdBy est varchar, donc jointure par username .leftJoin(users, eq(users.username, dashboardMessages.createdBy)) .leftJoin(groups, eq(groups.id, dashboardMessages.storeId)); // Filtrage par magasin pour admin : inclure les annonces globales + annonces du magasin if (user.role === 'admin' && req.query.storeId) { const storeId = parseInt(req.query.storeId as string); // Inclure les annonces globales (storeId = null) ET les annonces du magasin sélectionné query = query.where( or( eq(dashboardMessages.storeId, storeId), isNull(dashboardMessages.storeId) ) ); } const rows = await query.orderBy(desc(dashboardMessages.createdAt)).limit(5); const announcements = rows.map((row: any) => { const { authorId, authorUsername, authorFirstName, authorLastName, authorName, groupId, groupName, ...message } = row; // Auteur : repli sur "Utilisateur Inconnu" si l'utilisateur n'existe plus let author = { id: message.createdBy, firstName: 'Utilisateur', lastName: 'Inconnu', username: message.createdBy }; if (authorId) { // Utiliser name s'il existe, sinon firstName + lastName, sinon username const displayName = authorName || (authorFirstName && authorLastName ? `${authorFirstName} ${authorLastName}` : '') || authorUsername; author = { id: authorId, firstName: authorFirstName || displayName.split(' ')[0] || authorUsername, lastName: authorLastName || displayName.split(' ').slice(1).join(' ') || '', username: authorUsername }; } else { console.warn('❌ [PRODUCTION] User not found with username:', message.createdBy); } // Magasin uniquement si storeId est défini et existe const group = message.storeId && groupId ? { id: groupId, name: groupName } : null; return { ...message, author, group }; }); res.json(announcements); } catch (dbError) { console.error('🎯 [PRODUCTION] Database error:', dbError); // Fallback au stockage mémoire en cas d'erreur DB console.log('🧠 [FALLBACK] Using memory storage due to DB error'); const announcements = await storage.getAnnouncements(); res.json(announcements); } } else { // DÉVELOPPEMENT: Utiliser stockage mémoire const groupIds = user.role === 'admin' && req.query.storeId ? [parseInt(req.query.storeId as string)] : undefined; const announcements = await storage.getAnnouncements(groupIds); res.json(announcements); } } catch (error) { console.error("📢 [SERVER] Error fetching announcements:", error); res.status(500).json({ message: "Failed to fetch announcements" }); } }); app.post('/api/announcements', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Only admin can create announcements if (user.role !== 'admin') { console.error('🎯 [SERVER] Access denied - user role:', user.role); return res.status(403).json({ message: "Only administrators can create announcements" }); } const announcementData = insertAnnouncementSchema.parse({ title: req.body.title, content: req.body.content, type: req.body.type || 'info', storeId: req.body.storeId || null, // Permettre les annonces par magasin OU globales createdBy: user.username, // Utiliser username pour PostgreSQL }); if (environment === 'production') { // PRODUCTION: Créer dans PostgreSQL DASHBOARD_MESSAGES try { const [newMessage] = await db.insert(dashboardMessages).values({ title: announcementData.title, content: announcementData.content, type: announcementData.type, storeId: announcementData.storeId, createdBy: user.username, // Utiliser username plutôt que ID pour PostgreSQL }).returning(); const announcement = { ...newMessage, author: { id: user.id, firstName: user.firstName, lastName: user.lastName }, group: null }; res.status(201).json(announcement); } catch (dbError) { console.error('🎯 [PRODUCTION] DB error, fallback to memory:', dbError); const announcement = await storage.createAnnouncement(announcementData); res.status(201).json(announcement); } } else { // DÉVELOPPEMENT: Créer en mémoire const announcement = await storage.createAnnouncement(announcementData); res.status(201).json(announcement); } } catch (error) { console.error('🎯 [SERVER] Error creating announcement:', error); if (error instanceof z.ZodError) { console.error('🎯 [SERVER] Validation errors:', error.errors); return res.status(400).json({ message: "Validation error", errors: error.errors }); } res.status(500).json({ message: "Failed to create announcement" }); } }); // PUT /api/announcements/:id - Update announcement (admin only) app.put('/api/announcements/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Only admin can edit announcements if (user.role !== 'admin') { console.error('📝 [SERVER] Access denied - user role:', user.role); return res.status(403).json({ message: "Only administrators can edit announcements" }); } const id = parseInt(req.params.id); // Verify announcement exists const existingAnnouncement = await storage.getAnnouncement(id); if (!existingAnnouncement) { console.error('📝 [SERVER] Announcement not found:', id); return res.status(404).json({ message: "Announcement not found" }); } const announcementData = insertAnnouncementSchema.partial().parse(req.body); const updatedAnnouncement = await storage.updateAnnouncement(id, announcementData); res.json(updatedAnnouncement); } catch (error) { console.error('📝 [SERVER] Error updating announcement:', error); if (error instanceof z.ZodError) { console.error('📝 [SERVER] Validation errors:', error.errors); return res.status(400).json({ message: "Validation error", errors: error.errors }); } res.status(500).json({ message: "Failed to update announcement" }); } }); app.delete('/api/announcements/:id', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Only admin can delete announcements if (user.role !== 'admin') { console.error('🗑️ [SERVER] Access denied - user role:', user.role); return res.status(403).json({ message: "Only administrators can delete announcements" }); } const id = parseInt(req.params.id); console.log('🗑️ [SERVER] Parsed announcement ID:', id); if (environment === 'production') { // PRODUCTION: Supprimer dans PostgreSQL DASHBOARD_MESSAGES console.log('🗑️ [PRODUCTION] Deleting from PostgreSQL DASHBOARD_MESSAGES table'); try { const deletedRows = await db.delete(dashboardMessages).where(eq(dashboardMessages.id, id)).returning(); if (deletedRows.length === 0) { console.error('🗑️ [PRODUCTION] Announcement not found:', id); return res.status(404).json({ message: "Announcement not found" }); } console.log('🗑️ [PRODUCTION] Announcement deleted successfully from DB:', deletedRows[0]); res.json({ message: "Announcement deleted successfully" }); } catch (dbError) { console.error('🗑️ [PRODUCTION] DB error, fallback to memory:', dbError); const success = await storage.deleteAnnouncement(id); if (!success) { return res.status(404).json({ message: "Announcement not found" }); } res.json({ message: "Announcement deleted successfully" }); } } else { // DÉVELOPPEMENT: Supprimer en mémoire console.log('🗑️ [DEV] Deleting from memory storage'); const success = await storage.deleteAnnouncement(id); if (!success) { console.error('🗑️ [DEV] Announcement not found:', id); return res.status(404).json({ message: "Announcement not found" }); } console.log('🗑️ [DEV] Announcement deleted from memory'); res.json({ message: "Announcement deleted successfully" }); } } catch (error) { console.error("🗑️ [SERVER] Error deleting announcement:", error); res.status(500).json({ message: "Failed to delete announcement" }); } }); app.post('/api/weather/geolocation', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const { latitude, longitude } = req.body; if (!latitude || !longitude) { return res.status(400).json({ message: "Latitude et longitude requises" }); } // Récupérer les paramètres météo actuels pour obtenir la clé API const settings = await storage.getWeatherSettings(); if (!settings || !settings.apiKey) { return res.status(500).json({ message: "Configuration météo manquante" }); } // Convertir les coordonnées en nom de ville const locationData = await weatherService.getCityFromCoordinates( parseFloat(latitude), parseFloat(longitude), settings.apiKey ); if (!locationData) { return res.status(500).json({ message: "Impossible de déterminer la ville à partir des coordonnées" }); } // Mettre à jour automatiquement la configuration météo avec la nouvelle localisation await storage.updateWeatherSettings(settings.id, { location: locationData.fullLocation }); // Vider le cache météo car la localisation a changé await storage.clearWeatherCache(); console.log('🌍 Localisation mise à jour automatiquement:', { from: settings.location, to: locationData.fullLocation, coordinates: { latitude, longitude } }); res.json({ success: true, location: locationData, message: `Localisation mise à jour vers ${locationData.city}, ${locationData.country}` }); } catch (error) { console.error("Erreur géolocalisation météo:", error); res.status(500).json({ message: "Erreur lors de la géolocalisation" }); } }); // API d'exécution SQL pour admin uniquement app.post('/api/admin/execute-sql', isAuthenticated, async (req: any, res) => { try { // Vérifier que l'utilisateur est admin const userId = req.user?.claims?.sub || req.user?.id; if (!userId) { return res.status(401).json({ error: 'Utilisateur non authentifié' }); } const user = await storage.getUser(userId); if (!user || user.role !== 'admin') { return res.status(403).json({ error: 'Accès refusé - Admin uniquement' }); } const { sql: sqlQuery } = req.body; if (!sqlQuery || typeof sqlQuery !== 'string') { return res.status(400).json({ error: 'SQL query requis' }); } console.log('🔧 [SQL-EXECUTOR] Début exécution SQL pour admin:', userId); console.log('🔧 [SQL-EXECUTOR] Query:', sqlQuery.substring(0, 200) + '...'); const logs = [`🔄 Exécution SQL démarrée...`]; try { // Utilisation de drizzle-orm pour l'exécution const { sql } = await import('drizzle-orm'); const result = await db.execute(sql.raw(sqlQuery)); logs.push(`✅ SQL exécuté avec succès`); logs.push(`📊 Nombre de lignes affectées: ${result.rowCount || 0}`); if (result.rows && result.rows.length > 0) { logs.push(`📋 Nombre de lignes retournées: ${result.rows.length}`); if (result.rows.length <= 10) { logs.push(`📋 Résultats: ${JSON.stringify(result.rows, null, 2)}`); } else { logs.push(`📋 Échantillon (10 premières lignes): ${JSON.stringify(result.rows.slice(0, 10), null, 2)}`); } } console.log('✅ [SQL-EXECUTOR] Exécution réussie'); res.json({ success: true, logs, results: result.rows, rowCount: result.rowCount }); } catch (sqlError: any) { console.error('❌ [SQL-EXECUTOR] Erreur SQL:', sqlError); logs.push(`❌ Erreur SQL: ${sqlError.message}`); return res.status(500).json({ error: `Erreur SQL: ${sqlError.message}`, logs }); } } catch (error: any) { console.error('❌ [SQL-EXECUTOR] Erreur générale:', error); res.status(500).json({ error: 'Erreur serveur', details: error.message }); } }); // Analytics routes app.get('/api/analytics/summary', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } // Parse query parameters const filters = { startDate: req.query.startDate ? new Date(req.query.startDate) : undefined, endDate: req.query.endDate ? new Date(req.query.endDate) : undefined, supplierIds: req.query.supplierIds ? req.query.supplierIds.split(',').map(Number) : undefined, groupIds: req.query.groupIds ? req.query.groupIds.split(',').map(Number) : undefined, status: req.query.status ? req.query.status.split(',') : undefined }; // Apply role-based filtering if (user.role !== 'admin' && user.role !== 'directeur') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; filters.groupIds = filters.groupIds ? filters.groupIds.filter((id: number) => userGroupIds.includes(id)) : userGroupIds; } const summary = await storage.getAnalyticsSummary(filters); res.json(summary); } catch (error) { console.error('Error fetching analytics summary:', error); res.status(500).json({ message: 'Failed to fetch analytics summary' }); } }); app.get('/api/analytics/timeseries', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const filters = { startDate: req.query.startDate ? new Date(req.query.startDate) : undefined, endDate: req.query.endDate ? new Date(req.query.endDate) : undefined, supplierIds: req.query.supplierIds ? req.query.supplierIds.split(',').map(Number) : undefined, groupIds: req.query.groupIds ? req.query.groupIds.split(',').map(Number) : undefined, granularity: (req.query.granularity as 'day' | 'week' | 'month') || 'day' }; // Apply role-based filtering if (user.role !== 'admin' && user.role !== 'directeur') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; filters.groupIds = filters.groupIds ? filters.groupIds.filter((id: number) => userGroupIds.includes(id)) : userGroupIds; } const timeseries = await storage.getAnalyticsTimeseries(filters); res.json(timeseries); } catch (error) { console.error('Error fetching analytics timeseries:', error); res.status(500).json({ message: 'Failed to fetch analytics timeseries' }); } }); app.get('/api/analytics/by-supplier', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const filters = { startDate: req.query.startDate ? new Date(req.query.startDate) : undefined, endDate: req.query.endDate ? new Date(req.query.endDate) : undefined, groupIds: req.query.groupIds ? req.query.groupIds.split(',').map(Number) : undefined }; // Apply role-based filtering if (user.role !== 'admin' && user.role !== 'directeur') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; filters.groupIds = filters.groupIds ? filters.groupIds.filter((id: number) => userGroupIds.includes(id)) : userGroupIds; } const bySupplier = await storage.getAnalyticsBySupplier(filters); res.json(bySupplier); } catch (error) { console.error('Error fetching analytics by supplier:', error); res.status(500).json({ message: 'Failed to fetch analytics by supplier' }); } }); app.get('/api/analytics/by-store', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const filters = { startDate: req.query.startDate ? new Date(req.query.startDate) : undefined, endDate: req.query.endDate ? new Date(req.query.endDate) : undefined, supplierIds: req.query.supplierIds ? req.query.supplierIds.split(',').map(Number) : undefined }; const byStore = await storage.getAnalyticsByStore(filters); res.json(byStore); } catch (error) { console.error('Error fetching analytics by store:', error); res.status(500).json({ message: 'Failed to fetch analytics by store' }); } }); app.get('/api/analytics/export', isAuthenticated, async (req: any, res) => { try { const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } const type = req.query.type || 'summary'; const filters = { startDate: req.query.startDate ? new Date(req.query.startDate) : undefined, endDate: req.query.endDate ? new Date(req.query.endDate) : undefined, supplierIds: req.query.supplierIds ? req.query.supplierIds.split(',').map(Number) : undefined, groupIds: req.query.groupIds ? req.query.groupIds.split(',').map(Number) : undefined, status: req.query.status ? req.query.status.split(',') : undefined }; // Apply role-based filtering if (user.role !== 'admin' && user.role !== 'directeur') { const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; filters.groupIds = filters.groupIds ? filters.groupIds.filter((id: number) => userGroupIds.includes(id)) : userGroupIds; } let data: any; let filename = `analytics_${type}_${new Date().toISOString().split('T')[0]}.csv`; switch (type) { case 'timeseries': data = await storage.getAnalyticsTimeseries({ ...filters, granularity: 'day' }); break; case 'suppliers': data = await storage.getAnalyticsBySupplier(filters); break; case 'stores': data = await storage.getAnalyticsByStore(filters); break; default: data = [await storage.getAnalyticsSummary(filters)]; } // Convert to CSV if (data && data.length > 0) { const headers = Object.keys(data[0]).join(','); const rows = data.map((item: any) => Object.values(item).map((val: any) => typeof val === 'object' ? JSON.stringify(val) : val ).join(',') ).join('\n'); const csv = `${headers}\n${rows}`; res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.send(csv); } else { res.status(404).json({ message: 'No data to export' }); } } catch (error) { console.error('Error exporting analytics:', error); res.status(500).json({ message: 'Failed to export analytics' }); } }); // Proxy API ffnancy - évite les problèmes CORS côté navigateur app.get('/api/ffnancy/articles', requireAuth, async (req: Request, res: Response) => { try { const params = new URLSearchParams(); const allowed = ['search', 'codein', 'ean', 'codefou', 'actif', 'page', 'limit']; for (const key of allowed) { if (req.query[key]) params.append(key, req.query[key] as string); } const response = await fetch(`https://api.ffnancy.fr/api/articles?${params}`); if (!response.ok) return res.status(response.status).json({ error: 'API ffnancy error' }); res.json(await response.json()); } catch { res.status(500).json({ error: 'Failed to reach API ffnancy' }); } }); app.get('/api/ffnancy/mouvements/entrees', requireAuth, async (req: Request, res: Response) => { try { const params = new URLSearchParams(); const allowed = ['artNoId', 'dateDebut', 'dateFin', 'site', 'page', 'limit']; for (const key of allowed) { if (req.query[key]) params.append(key, req.query[key] as string); } const response = await fetch(`https://api.ffnancy.fr/api/mouvements/entrees?${params}`); if (!response.ok) return res.status(response.status).json({ error: 'API ffnancy error' }); res.json(await response.json()); } catch { res.status(500).json({ error: 'Failed to reach API ffnancy' }); } }); // Create server instance const httpServer = createServer(app); // Server startup return httpServer; }