diff --git a/Dockerfile b/Dockerfile index aa61863..f781505 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,7 +62,8 @@ RUN npx esbuild server/index.production.ts \ --external:memoizee \ --external:nanoid \ --external:date-fns \ - --external:nodemailer + --external:nodemailer \ + --external:compression # Production stage FROM node:20-alpine AS production @@ -99,6 +100,8 @@ USER nextjs # Expose port EXPOSE 3000 ENV PORT=3000 +# Fixé ici car db.ts lit NODE_ENV dès l'import, avant le forçage de index.production.ts +ENV NODE_ENV=production # Install wget for health check and postgresql-client for pg_dump USER root diff --git a/package-lock.json b/package-lock.json index c5b13be..d86353d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "compression": "^1.8.2", "connect-pg-simple": "^10.0.0", "cookie-parser": "^1.4.7", "csrf-csrf": "^4.0.3", @@ -106,6 +107,7 @@ "@replit/vite-plugin-runtime-error-modal": "^0.0.3", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.1.3", + "@types/compression": "^1.8.1", "@types/connect-pg-simple": "^7.0.3", "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", @@ -3643,6 +3645,17 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -4532,6 +4545,65 @@ "node": ">= 6" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "destroy": "1.2.0", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", diff --git a/package.json b/package.json index a96a57f..8276aaa 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "compression": "^1.8.2", "connect-pg-simple": "^10.0.0", "cookie-parser": "^1.4.7", "csrf-csrf": "^4.0.3", @@ -108,6 +109,7 @@ "@replit/vite-plugin-runtime-error-modal": "^0.0.3", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.1.3", + "@types/compression": "^1.8.1", "@types/connect-pg-simple": "^7.0.3", "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", diff --git a/server/backupService.ts b/server/backupService.ts index 20a1c9a..f02838a 100644 --- a/server/backupService.ts +++ b/server/backupService.ts @@ -3,7 +3,7 @@ import { promisify } from 'util'; import fs from 'fs'; import path from 'path'; import { nanoid } from 'nanoid'; -import { eq, desc } from "drizzle-orm"; +import { eq, desc, sql } from "drizzle-orm"; import { db } from "./db"; import { databaseBackups, utilities, users } from "@shared/schema"; import type { DatabaseBackup, InsertDatabaseBackup } from "@shared/schema"; @@ -14,6 +14,8 @@ export class BackupService { private backupDir: string; private maxBackups = 10; private lastAutomaticBackupDate: string | null = null; + // Vérification quotidienne en cours, partagée par les connexions simultanées + private dailyBackupCheck: Promise<{ backupPerformed: boolean; message: string }> | null = null; constructor() { // Use /app/backups in production (with proper permissions), or use env variable @@ -130,9 +132,10 @@ export class BackupService { await execAsync(command, { env }); // Get file stats and count tables - const stats = fs.statSync(filepath); - const sqlContent = fs.readFileSync(filepath, 'utf8'); - const tablesCount = (sqlContent.match(/CREATE TABLE/g) || []).length; + // Tables comptées en base plutôt qu'en relisant tout le dump en mémoire : + // la lecture synchrone et la regex gelaient le serveur sur les grosses bases + const stats = await fs.promises.stat(filepath); + const tablesCount = await this.countDatabaseTables(); // Update database record with completion details const [updatedBackup] = await db.update(databaseBackups) @@ -156,6 +159,18 @@ export class BackupService { } } + // Nombre de tables utilisateur (hors schémas système), proche de ce que + // pg_dump inclut dans la sauvegarde + private async countDatabaseTables(): Promise { + const result = await db.execute(sql` + SELECT count(*)::int AS count + FROM information_schema.tables + WHERE table_type = 'BASE TABLE' + AND table_schema NOT IN ('pg_catalog', 'information_schema') + `); + return Number(result.rows[0]?.count ?? 0); + } + async getBackupList(): Promise { try { const backups = await db.select() @@ -238,6 +253,17 @@ export class BackupService { // Nouvelle méthode : Vérifier et effectuer une sauvegarde quotidienne si nécessaire async checkAndPerformDailyBackup(userId: string = 'system'): Promise<{ backupPerformed: boolean; message: string }> { + // Plusieurs connexions simultanées (ouverture des magasins) partagent la + // même vérification : un seul pg_dump est lancé + if (!this.dailyBackupCheck) { + this.dailyBackupCheck = this.performDailyBackupCheck(userId).finally(() => { + this.dailyBackupCheck = null; + }); + } + return this.dailyBackupCheck; + } + + private async performDailyBackupCheck(userId: string): Promise<{ backupPerformed: boolean; message: string }> { try { // Vérifier si les backups automatiques sont activés const [config] = await db.select() diff --git a/server/cache.ts b/server/cache.ts deleted file mode 100644 index 36e2ef8..0000000 --- a/server/cache.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; - -// Cache en mémoire simple pour les données fréquemment utilisées -class MemoryCache { - private cache: Map = new Map(); - private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes - - set(key: string, data: any, ttl: number = this.DEFAULT_TTL): void { - this.cache.set(key, { - data, - timestamp: Date.now(), - ttl - }); - } - - get(key: string): any | null { - const item = this.cache.get(key); - if (!item) return null; - - if (Date.now() - item.timestamp > item.ttl) { - this.cache.delete(key); - return null; - } - - return item.data; - } - - delete(key: string): void { - this.cache.delete(key); - } - - clear(): void { - this.cache.clear(); - } - - // Nettoyer les éléments expirés - cleanup(): void { - const now = Date.now(); - for (const [key, item] of this.cache.entries()) { - if (now - item.timestamp > item.ttl) { - this.cache.delete(key); - } - } - } -} - -export const cache = new MemoryCache(); - -// Nettoyage automatique toutes les 10 minutes -setInterval(() => cache.cleanup(), 10 * 60 * 1000); - -// Middleware de mise en cache pour les réponses API -export function cacheMiddleware(ttl: number = 5 * 60 * 1000) { - return (req: Request, res: Response, next: NextFunction) => { - // Ne pas mettre en cache les requêtes POST/PUT/DELETE - if (req.method !== 'GET') { - return next(); - } - - const key = `${req.originalUrl}:${req.user?.id || 'anonymous'}`; - const cachedData = cache.get(key); - - if (cachedData) { - res.setHeader('X-Cache', 'HIT'); - return res.json(cachedData); - } - - res.setHeader('X-Cache', 'MISS'); - - // Intercepter la réponse pour la mettre en cache - const originalSend = res.json; - res.json = function(data: any) { - if (res.statusCode === 200) { - cache.set(key, data, ttl); - } - return originalSend.call(this, data); - }; - - next(); - }; -} - -// Invalidation de cache par pattern -export function invalidateCache(pattern: string): void { - for (const key of cache['cache'].keys()) { - if (key.includes(pattern)) { - cache.delete(key); - } - } -} - -// Middleware de compression des réponses -export function setupCompression(app: any) { - app.use((req: Request, res: Response, next: NextFunction) => { - // Activer la compression pour les réponses JSON - if (req.accepts('gzip') && String(res.getHeader('Content-Type') ?? '').includes('json')) { - res.setHeader('Content-Encoding', 'gzip'); - } - next(); - }); -} - -// Optimisation des requêtes base de données -export function createOptimizedQuery(baseQuery: string, params: any[]): { query: string; params: any[] } { - // Optimiser les requêtes en évitant les SELECT * inutiles - let optimizedQuery = baseQuery; - - // Limiter les résultats pour éviter les grosses requêtes - if (!optimizedQuery.includes('LIMIT') && !optimizedQuery.includes('COUNT')) { - optimizedQuery += ' LIMIT 1000'; - } - - // Ajouter des index hints si nécessaire - if (optimizedQuery.includes('ORDER BY created_at')) { - optimizedQuery = optimizedQuery.replace('ORDER BY created_at', 'ORDER BY created_at DESC'); - } - - return { query: optimizedQuery, params }; -} \ No newline at end of file diff --git a/server/index.production.ts b/server/index.production.ts index 1127e1f..6b3ce25 100644 --- a/server/index.production.ts +++ b/server/index.production.ts @@ -1,4 +1,5 @@ import express, { type Express, type Request, type Response, type NextFunction } from "express"; +import compression from "compression"; import { createServer, type Server } from "http"; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; @@ -24,6 +25,10 @@ console.log('🐳 Environment:', { const app = express(); +// Compression gzip des réponses (assets JS/CSS, index.html et JSON de l'API). +// En premier pour couvrir toutes les réponses. +app.use(compression()); + app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: false, limit: '10mb' })); @@ -126,48 +131,62 @@ async function registerProductionRoutes(app: Express): Promise { }); } -await registerProductionRoutes(app); - -app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { - const status = err.status || err.statusCode || 500; - const message = err.message || "Internal Server Error"; - console.error('Server error:', { status, message, error: err }); - res.status(status).json({ message }); - throw err; -}); - // Serve static files directly in production (no Vite) +// Déclarés AVANT les routes : les fichiers statiques et l'index.html ne passent +// ni par la session ni par passport (aucune requête SQL par fichier). const publicPath = join(__dirname, 'public'); console.log('🐳 Serving static files from:', publicPath); -app.use('/assets', express.static(join(publicPath, 'assets'))); -app.use('/', express.static(publicPath)); - -// Add explicit root route handler -app.get('/', (req, res) => { - console.log('🏠 ROOT: Serving index.html for root request'); - res.sendFile(join(publicPath, 'index.html'), (err) => { - if (err) { - console.error('❌ ROOT: Error serving index.html:', err); - res.status(500).send('Error loading application'); +// Les fichiers de /assets ont un nom haché par Vite : cache navigateur d'un an +app.use('/assets', express.static(join(publicPath, 'assets'), { + maxAge: '1y', + immutable: true, + index: false, +})); +// Autres fichiers publics (favicon, manifest...). L'index.html est toujours +// revalidé pour qu'un nouveau déploiement soit pris en compte immédiatement. +app.use(express.static(publicPath, { + index: false, + setHeaders: (res, filePath) => { + if (filePath.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache'); } - }); -}); + }, +})); -// SPA fallback - serve index.html for all non-API routes +// SPA fallback - serve index.html for all non-API routes (y compris "/") app.get('*', (req, res, next) => { if (req.path.startsWith('/api/')) { return next(); } - console.log(`📄 SPA: Serving index.html for ${req.path}`); + res.set('Cache-Control', 'no-cache'); res.sendFile(join(publicPath, 'index.html'), (err) => { if (err) { console.error(`❌ SPA: Error serving index.html for ${req.path}:`, err); - res.status(500).send('Error loading application'); + if (!res.headersSent) { + res.status(500).send('Error loading application'); + } } }); }); +await registerProductionRoutes(app); + +// Tâches de maintenance périodiques (purge du cache factures expiré) +const { startMaintenanceJobs } = await import('./maintenance.js'); +startMaintenanceJobs(); + +app.use((err: any, _req: Request, res: Response, next: NextFunction) => { + // Réponse déjà commencée : on laisse Express clore la connexion + if (res.headersSent) { + return next(err); + } + const status = err.status || err.statusCode || 500; + const message = err.message || "Internal Server Error"; + console.error('Server error:', { status, message, error: err }); + res.status(status).json({ message }); +}); + const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; const server = createServer(app); server.listen(port, "0.0.0.0", () => { diff --git a/server/index.ts b/server/index.ts index 6829e00..db325e4 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,4 +1,5 @@ import express, { type Request, Response, NextFunction } from "express"; +import compression from "compression"; import cookieParser from "cookie-parser"; import { registerRoutes } from "./routes.js"; import { setupVite, serveStatic } from "./vite.js"; @@ -24,6 +25,9 @@ console.log('✅ [STARTUP] Weather system initialized'); const app = express(); +// Compression gzip des réponses, en premier pour couvrir toutes les réponses +app.use(compression()); + // Parse cookies (required for CSRF) app.use(cookieParser()); @@ -63,11 +67,15 @@ const server = await registerRoutes(app); const { startMaintenanceJobs } = await import('./maintenance.js'); startMaintenanceJobs(); -app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { +app.use((err: any, _req: Request, res: Response, next: NextFunction) => { + // Réponse déjà commencée : on laisse Express clore la connexion + if (res.headersSent) { + return next(err); + } const status = err.status || err.statusCode || 500; const message = err.message || "Internal Server Error"; + console.error('Server error:', { status, message, error: err }); res.status(status).json({ message }); - throw err; }); // Setup Vite in development diff --git a/server/invoiceVerification.ts b/server/invoiceVerification.ts index 57e550d..1b94869 100644 --- a/server/invoiceVerification.ts +++ b/server/invoiceVerification.ts @@ -1,5 +1,10 @@ +import { sql } from "drizzle-orm"; +import { db } from "./db.js"; import { storage } from "./storage.js"; +// Blancs ASCII retirés par String.prototype.trim(), pour reproduire en SQL les clés de cache +const ASCII_WHITESPACE = ' \t\n\v\f\r'; + /** * Service de vérification des factures avec NocoDB * Gère la vérification automatique des références de factures @@ -19,26 +24,10 @@ export class InvoiceVerificationService { async checkCache(invoiceReference: string, groupId: number): Promise { try { const cacheKey = this.generateCacheKey(invoiceReference, groupId); - console.log('🔍 [CACHE] Recherche cache pour:', { invoiceReference, groupId, cacheKey }); - const cached = await storage.getInvoiceVerificationCache(cacheKey); - console.log('🔍 [CACHE] Résultat cache:', { - found: !!cached, - isReconciled: cached?.isReconciled, - expired: cached ? new Date() >= new Date(cached.expiresAt) : 'N/A', - expiresAt: cached?.expiresAt, - currentTime: new Date().toISOString() - }); - + // ✅ CACHE PERMANENT : Facture validée avec coche verte = JAMAIS re-vérifier if (cached && cached.isReconciled && cached.exists) { - console.log('🛡️ [CACHE] PERMANENT - Facture validée avec coche verte, AUCUNE vérification:', { - invoiceReference, - groupId, - exists: cached.exists, - invoiceAmount: cached.invoiceAmount, - dueDate: cached.dueDate - }); return { exists: cached.exists, matchType: cached.matchType, @@ -55,7 +44,6 @@ export class InvoiceVerificationService { // Cache temporaire non expiré if (cached && new Date() < new Date(cached.expiresAt)) { - console.log('✅ [CACHE] Cache temporaire hit pour:', { invoiceReference, groupId, exists: cached.exists }); return { exists: cached.exists, matchType: cached.matchType, @@ -69,13 +57,7 @@ export class InvoiceVerificationService { permanent: false }; } - - if (cached && new Date() >= new Date(cached.expiresAt)) { - console.log('⏰ [CACHE] Cache temporaire expiré pour:', { invoiceReference, groupId }); - } else { - console.log('❌ [CACHE] Cache miss pour:', { invoiceReference, groupId }); - } - + return null; } catch (error) { console.error('❌ [CACHE] Erreur lecture cache:', error); @@ -92,32 +74,18 @@ export class InvoiceVerificationService { // Durée de cache adaptative selon les cas const expiresAt = new Date(); - let cacheDescription = ''; - + if (isReconciled) { // Cache PERMANENT pour factures validées - expire dans 50 ans expiresAt.setFullYear(expiresAt.getFullYear() + 50); - cacheDescription = 'PERMANENT (validé)'; } else if (result.exists) { // Facture trouvée mais non validée - cache 6h pour permettre corrections expiresAt.setHours(expiresAt.getHours() + 6); - cacheDescription = 'temporaire 6h (trouvé)'; } else { // Facture non trouvée - cache 12h pour éviter spam expiresAt.setHours(expiresAt.getHours() + 12); - cacheDescription = 'temporaire 12h (pas trouvé)'; } - - console.log('💾 [CACHE] Tentative sauvegarde:', { - invoiceReference, - groupId, - cacheKey, - exists: result.exists, - isReconciled, - cacheType: cacheDescription, - expiresAt: expiresAt.toISOString() - }); - + const cacheData = { cacheKey, groupId, @@ -135,17 +103,7 @@ export class InvoiceVerificationService { expiresAt }; - const savedCache = await storage.saveInvoiceVerificationCache(cacheData); - - console.log('✅ [CACHE] Résultat sauvé en cache:', { - id: savedCache.id, - invoiceReference, - groupId, - exists: result.exists, - isReconciled, - cacheType: cacheDescription, - cacheKey - }); + await storage.saveInvoiceVerificationCache(cacheData); } catch (error) { // Gérer spécifiquement les erreurs de contrainte unique (duplicate key) if (error && typeof error === 'object' && 'code' in error && error.code === '23505') { @@ -211,7 +169,16 @@ export class InvoiceVerificationService { async updateExistingReconciledCaches(): Promise { try { console.log('🔄 [CACHE] Mise à jour en lot des caches pour livraisons validées...'); - + + // Base PostgreSQL : une seule requête ensembliste au lieu de charger toutes + // les livraisons puis 2 requêtes (lecture + upsert) par référence + if (db) { + const updatedCount = await this.markReconciledCachesInDatabase(); + console.log(`✅ [CACHE] Mise à jour terminée: ${updatedCount} caches mis à jour comme permanents`); + return; + } + + // Stockage mémoire (développement sans base) : parcours livraison par livraison // Récupérer toutes les livraisons validées (reconciled = true) const deliveries = await storage.getDeliveries(); const reconciledDeliveries = deliveries.filter(d => d.reconciled); @@ -243,6 +210,36 @@ export class InvoiceVerificationService { } } + /** + * Équivalent SQL de updateCacheAsReconciled appliqué à toutes les livraisons validées. + * Mêmes clés que generateCacheKey (référence facture et numéro de BL non vides après + * trim), mêmes conditions (entrée présente, non expirée, pas encore réconciliée) et + * mêmes colonnes modifiées que l'upsert de DatabaseStorage : is_reconciled et + * updated_at (expires_at n'y est pas mis à jour). Écart accepté : seuls les blancs + * ASCII sont retirés, et lower() suit la locale de la base. + * Retourne le nombre d'entrées de cache mises à jour. + */ + private async markReconciledCachesInDatabase(): Promise { + // Heure UTC, comme drizzle écrit et relit les colonnes timestamp + const now = new Date().toISOString(); + const result = await db.execute(sql` + UPDATE invoice_verification_cache + SET is_reconciled = true, updated_at = ${now}::timestamp + WHERE is_reconciled IS NOT TRUE + AND expires_at > ${now}::timestamp + AND cache_key IN ( + SELECT group_id::text || '_' || lower(btrim(invoice_reference, ${ASCII_WHITESPACE})) + FROM deliveries + WHERE reconciled = true AND btrim(invoice_reference, ${ASCII_WHITESPACE}) <> '' + UNION + SELECT group_id::text || '_' || lower(btrim(bl_number, ${ASCII_WHITESPACE})) + FROM deliveries + WHERE reconciled = true AND btrim(bl_number, ${ASCII_WHITESPACE}) <> '' + ) + `); + return result?.rowCount ?? 0; + } + /** * Vérifie une référence de facture pour un groupe donné */ @@ -258,8 +255,6 @@ export class InvoiceVerificationService { fromCache?: boolean; }> { try { - console.log('🔍 [INVOICE] Début vérification facture:', { invoiceReference, groupId, forceRefresh }); - if (!invoiceReference || !invoiceReference.trim()) { return { exists: false, @@ -270,15 +265,10 @@ export class InvoiceVerificationService { // Vérifier le cache d'abord (sauf si refresh forcé) if (!forceRefresh) { - console.log('🔍 [INVOICE] Vérification cache...'); const cachedResult = await this.checkCache(invoiceReference, groupId); if (cachedResult) { - console.log('✅ [INVOICE] Résultat depuis cache:', cachedResult); return cachedResult; } - console.log('🔍 [INVOICE] Pas de cache, requête API...'); - } else { - console.log('🔄 [INVOICE] Refresh forcé, ignorant le cache'); } // Récupérer la configuration du groupe @@ -291,16 +281,8 @@ export class InvoiceVerificationService { }; } - console.log('🔧 Configuration groupe:', { - groupName: group.name, - hasNocodbConfig: !!group.nocodbConfigId, - hasTableName: !!group.nocodbTableName, - hasWebhook: !!group.webhookUrl - }); - // Si pas de configuration NocoDB, retourner un résultat par défaut if (!group.nocodbConfigId && !group.nocodbTableName && !group.webhookUrl) { - console.log('⚠️ Pas de configuration NocoDB pour ce groupe'); return { exists: false, matchType: 'none', @@ -353,8 +335,6 @@ export class InvoiceVerificationService { } // En production, faire l'appel réel à NocoDB - console.log('🔍 Vérification NocoDB en production...'); - try { // Récupérer la configuration NocoDB active const nocodbConfig = await storage.getActiveNocodbConfig(); @@ -367,20 +347,8 @@ export class InvoiceVerificationService { }; } - console.log('🔧 Configuration NocoDB trouvée:', { - configName: nocodbConfig.name, - baseUrl: nocodbConfig.baseUrl, - projectId: nocodbConfig.projectId, - hasToken: !!nocodbConfig.apiToken - }); - // Utiliser l'ID de table configuré dans le groupe const tableId = group.nocodbTableId || 'mrr733dfb8wtt9b'; // Fallback par défaut - console.log('🔧 Utilisation table ID:', { - groupTable: group.nocodbTableName, - configuredId: group.nocodbTableId, - resolvedId: tableId - }); // Vérifier d'abord par référence de facture let matchResult = await this.searchInNocoDB( @@ -621,8 +589,6 @@ export class InvoiceVerificationService { fromCache?: boolean; }> { try { - console.log('🔍 Début vérification facture par BL:', { blNumber, supplierName, groupId, forceRefresh }); - if (!blNumber || !blNumber.trim()) { return { exists: false, @@ -639,7 +605,6 @@ export class InvoiceVerificationService { try { const cached = await storage.getInvoiceVerificationCache(cacheKey); if (cached && new Date() < new Date(cached.expiresAt)) { - console.log('💾 Cache hit pour BL:', { blNumber, groupId }); return { exists: cached.exists, matchType: cached.matchType as 'invoice_reference' | 'bl_number' | 'none', @@ -664,17 +629,8 @@ export class InvoiceVerificationService { }; } - console.log('🔧 Configuration groupe pour BL:', { - groupName: group.name, - hasNocodbConfig: !!group.nocodbConfigId, - hasTableName: !!group.nocodbTableName, - hasWebhook: !!group.webhookUrl, - blColumnName: group.nocodbBlColumnName - }); - // Si pas de configuration NocoDB, retourner un résultat par défaut if (!group.nocodbConfigId && !group.nocodbTableName && !group.webhookUrl) { - console.log('⚠️ Pas de configuration NocoDB pour ce groupe'); return { exists: false, matchType: 'none', @@ -727,8 +683,6 @@ export class InvoiceVerificationService { } // En production, faire l'appel réel à NocoDB - console.log('🔍 Vérification BL NocoDB en production...'); - try { // Récupérer la configuration NocoDB active const nocodbConfig = await storage.getActiveNocodbConfig(); @@ -743,11 +697,6 @@ export class InvoiceVerificationService { // Utiliser l'ID de table configuré dans le groupe const tableId = group.nocodbTableId || 'mrr733dfb8wtt9b'; - console.log('🔧 Utilisation table ID pour BL:', { - groupTable: group.nocodbTableName, - configuredId: group.nocodbTableId, - resolvedId: tableId - }); // Rechercher par numéro de BL const blColumnName = group.nocodbBlColumnName || 'Numero_BL'; diff --git a/server/localAuth.ts b/server/localAuth.ts index ca83906..450482f 100644 --- a/server/localAuth.ts +++ b/server/localAuth.ts @@ -5,7 +5,7 @@ import session from "express-session"; import { scrypt, randomBytes, timingSafeEqual } from "crypto"; import { promisify } from "util"; import { storage } from "./storage"; -import { User as SelectUser } from "@shared/schema"; +import { User as SelectUser, type UserWithGroups } from "@shared/schema"; import connectPg from "connect-pg-simple"; import { backupService } from "./backupService"; @@ -141,7 +141,9 @@ export function setupLocalAuth(app: Express) { passport.deserializeUser(async (id: string, done) => { try { const user = await storage.getUserWithGroups(id); - done(null, user); + // Utilisateur supprimé depuis la connexion : false vide la session (401) + // au lieu de l'erreur « Failed to deserialize user out of session » (500) + done(null, user || false); } catch (error) { done(error); } @@ -157,20 +159,9 @@ export function setupLocalAuth(app: Express) { return res.status(400).json({ message: info?.message || "Invalid credentials" }); } - req.login(user, async (err) => { + req.login(user, (err) => { if (err) return next(err); - // Vérifier et effectuer une sauvegarde quotidienne si nécessaire - try { - const backupResult = await backupService.checkAndPerformDailyBackup(user.id); - if (backupResult.backupPerformed) { - console.log('💾 Sauvegarde quotidienne effectuée lors de la connexion de', user.username); - } - } catch (error) { - console.error('⚠️ Erreur lors de la vérification de sauvegarde:', error); - // Ne pas faire échouer la connexion si la sauvegarde échoue - } - res.json({ id: user.id, username: user.username, @@ -180,6 +171,21 @@ export function setupLocalAuth(app: Express) { role: user.role, passwordChanged: user.passwordChanged }); + + // Vérifier et effectuer une sauvegarde quotidienne si nécessaire, après + // la réponse : la connexion n'attend plus le pg_dump + setImmediate(() => { + backupService.checkAndPerformDailyBackup(user.id) + .then((backupResult) => { + if (backupResult.backupPerformed) { + console.log('💾 Sauvegarde quotidienne effectuée lors de la connexion de', user.username); + } + }) + .catch((error) => { + // Ne pas faire échouer la connexion si la sauvegarde échoue + console.error('⚠️ Erreur lors de la vérification de sauvegarde:', error); + }); + }); }); })(req, res, next); }); @@ -201,33 +207,25 @@ export function setupLocalAuth(app: Express) { app.get("/api/logout", logoutHandler); // Get current user - app.get("/api/user", async (req, res) => { + app.get("/api/user", (req, res) => { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Non authentifié" }); } - try { - const userId = (req.user as SelectUser).id; - const userWithGroups = await storage.getUserWithGroups(userId); - - if (!userWithGroups) { - return res.status(404).json({ message: "Utilisateur non trouvé" }); - } - - res.json({ - id: userWithGroups.id, - username: userWithGroups.username, - email: userWithGroups.email, - firstName: userWithGroups.firstName, - lastName: userWithGroups.lastName, - role: userWithGroups.role, - passwordChanged: userWithGroups.passwordChanged, - userGroups: userWithGroups.userGroups || [] - }); - } catch (error) { - console.error("Error fetching user with groups:", error); - res.status(500).json({ message: "Erreur lors de la récupération des données utilisateur" }); - } + // req.user est déjà rechargé avec ses magasins par deserializeUser à chaque + // requête : inutile de relire l'utilisateur en base + const userWithGroups = req.user as UserWithGroups; + + res.json({ + id: userWithGroups.id, + username: userWithGroups.username, + email: userWithGroups.email, + firstName: userWithGroups.firstName, + lastName: userWithGroups.lastName, + role: userWithGroups.role, + passwordChanged: userWithGroups.passwordChanged, + userGroups: userWithGroups.userGroups || [] + }); }); // Check if default credentials should be shown diff --git a/server/migrations.production.ts b/server/migrations.production.ts index 4ae7615..12543d9 100644 --- a/server/migrations.production.ts +++ b/server/migrations.production.ts @@ -1,5 +1,283 @@ import { Client } from 'pg'; import { encryptSecret, isEncryptedSecret } from './crypto.js'; +import { pool } from './db.js'; + +// Index de performance garantis à chaque démarrage. +// Union de migrations/20260814_add_performance_indexes.sql, des index d'init.sql +// et des index composites issus de l'audit base de données (DB-14, DB-15, DB-34, +// DB-35, TASKS-29, SAV-27). Tables et colonnes conformes à shared/schema.ts. +// Volontairement absents : IDX_session_expire (table sessions inutilisée), +// pg_trgm, et l'index unique de user_groups (dédoublonnage préalable requis). +type PerformanceIndex = { name: string; table: string; columns: string; where?: string }; + +const PERFORMANCE_INDEXES: PerformanceIndex[] = [ + // user_groups : lu à chaque requête authentifiée via getUserWithGroups() + { name: 'idx_user_groups_user_id', table: 'user_groups', columns: 'user_id' }, + { name: 'idx_user_groups_group_id', table: 'user_groups', columns: 'group_id' }, + + // orders + { name: 'idx_orders_group_id', table: 'orders', columns: 'group_id' }, + { name: 'idx_orders_supplier_id', table: 'orders', columns: 'supplier_id' }, + { name: 'idx_orders_created_by', table: 'orders', columns: 'created_by' }, + { name: 'idx_orders_planned_date', table: 'orders', columns: 'planned_date' }, + { name: 'idx_orders_group_planned_date', table: 'orders', columns: 'group_id, planned_date' }, + { name: 'idx_orders_created_at', table: 'orders', columns: 'created_at DESC' }, + { name: 'idx_orders_status', table: 'orders', columns: 'status' }, + // Comptage des commandes en attente par magasin (statistiques) + { name: 'idx_orders_group_status', table: 'orders', columns: 'group_id, status' }, + + // deliveries : order_id est la clé de jointure la plus sollicitée + { name: 'idx_deliveries_order_id', table: 'deliveries', columns: 'order_id' }, + { name: 'idx_deliveries_group_id', table: 'deliveries', columns: 'group_id' }, + { name: 'idx_deliveries_supplier_id', table: 'deliveries', columns: 'supplier_id' }, + { name: 'idx_deliveries_created_by', table: 'deliveries', columns: 'created_by' }, + { name: 'idx_deliveries_scheduled_date', table: 'deliveries', columns: 'scheduled_date' }, + { name: 'idx_deliveries_group_scheduled_date', table: 'deliveries', columns: 'group_id, scheduled_date' }, + { name: 'idx_deliveries_created_at', table: 'deliveries', columns: 'created_at DESC' }, + { name: 'idx_deliveries_invoice_reference', table: 'deliveries', columns: 'invoice_reference', where: 'invoice_reference IS NOT NULL' }, + { name: 'idx_deliveries_status', table: 'deliveries', columns: 'status' }, + { name: 'idx_deliveries_bl_number', table: 'deliveries', columns: 'bl_number' }, + { name: 'idx_deliveries_due_date', table: 'deliveries', columns: 'due_date' }, + // Livraisons livrées par magasin (rapprochement, statistiques) + { name: 'idx_deliveries_group_status', table: 'deliveries', columns: 'group_id, status' }, + { name: 'idx_deliveries_group_delivered_date', table: 'deliveries', columns: 'group_id, delivered_date' }, + + // reconciliation_comments + { name: 'idx_reconciliation_comments_delivery_id', table: 'reconciliation_comments', columns: 'delivery_id' }, + { name: 'idx_reconciliation_comments_group_id', table: 'reconciliation_comments', columns: 'group_id' }, + + // contacts + { name: 'idx_contacts_group_id', table: 'contacts', columns: 'group_id' }, + + // customer_orders + { name: 'idx_customer_orders_group_id', table: 'customer_orders', columns: 'group_id' }, + { name: 'idx_customer_orders_supplier_id', table: 'customer_orders', columns: 'supplier_id' }, + { name: 'idx_customer_orders_created_at', table: 'customer_orders', columns: 'created_at DESC' }, + { name: 'idx_customer_orders_group_status', table: 'customer_orders', columns: 'group_id, status' }, + // Appels clients en attente : même prédicat que getPendingClientCalls() + { name: 'idx_customer_orders_pending_calls', table: 'customer_orders', columns: 'group_id, created_at DESC', where: 'customer_notified = false' }, + + // dlc_products + { name: 'idx_dlc_products_group_id', table: 'dlc_products', columns: 'group_id' }, + { name: 'idx_dlc_products_supplier_id', table: 'dlc_products', columns: 'supplier_id' }, + { name: 'idx_dlc_products_expiry_date', table: 'dlc_products', columns: 'expiry_date' }, + { name: 'idx_dlc_products_status', table: 'dlc_products', columns: 'status' }, + { name: 'idx_dlc_products_group_status', table: 'dlc_products', columns: 'group_id, status' }, + { name: 'idx_dlc_products_group_expiry', table: 'dlc_products', columns: 'group_id, expiry_date' }, + + // tasks + { name: 'idx_tasks_group_id', table: 'tasks', columns: 'group_id' }, + { name: 'idx_tasks_created_by', table: 'tasks', columns: 'created_by' }, + { name: 'idx_tasks_due_date', table: 'tasks', columns: 'due_date' }, + { name: 'idx_tasks_group_status', table: 'tasks', columns: 'group_id, status' }, + { name: 'idx_tasks_group_created', table: 'tasks', columns: 'group_id, created_at DESC' }, + + // avoirs + { name: 'idx_avoirs_group_id', table: 'avoirs', columns: 'group_id' }, + { name: 'idx_avoirs_supplier_id', table: 'avoirs', columns: 'supplier_id' }, + { name: 'idx_avoirs_created_at', table: 'avoirs', columns: 'created_at DESC' }, + { name: 'idx_avoirs_group_status', table: 'avoirs', columns: 'group_id, status' }, + + // sav_tickets + { name: 'idx_sav_tickets_group_id', table: 'sav_tickets', columns: 'group_id' }, + { name: 'idx_sav_tickets_supplier_id', table: 'sav_tickets', columns: 'supplier_id' }, + { name: 'idx_sav_tickets_created_at', table: 'sav_tickets', columns: 'created_at DESC' }, + { name: 'idx_sav_tickets_status', table: 'sav_tickets', columns: 'status' }, + { name: 'idx_sav_tickets_priority', table: 'sav_tickets', columns: 'priority' }, + { name: 'idx_sav_tickets_group_status', table: 'sav_tickets', columns: 'group_id, status' }, + { name: 'idx_sav_tickets_group_created', table: 'sav_tickets', columns: 'group_id, created_at DESC' }, + + // invoice_verification_cache : expires_at sert la purge périodique + { name: 'idx_invoice_cache_expires_at', table: 'invoice_verification_cache', columns: 'expires_at' }, + { name: 'idx_invoice_cache_group_id', table: 'invoice_verification_cache', columns: 'group_id' }, + + // publicity_participations : la clé primaire (publicity_id, group_id) ne couvre pas group_id seul + { name: 'idx_publicity_participations_group_id', table: 'publicity_participations', columns: 'group_id' }, + + // publicities + { name: 'idx_publicities_year', table: 'publicities', columns: 'year' }, + { name: 'idx_publicities_start_date', table: 'publicities', columns: 'start_date' }, + + // dashboard_messages + { name: 'idx_dashboard_messages_store_id', table: 'dashboard_messages', columns: 'store_id' }, + { name: 'idx_dashboard_messages_created_at', table: 'dashboard_messages', columns: 'created_at DESC' }, + + // weather_data + { name: 'idx_weather_data_date_location', table: 'weather_data', columns: 'date, location' }, + { name: 'idx_weather_data_date_year', table: 'weather_data', columns: 'date, is_current_year' }, + + // supplier_mail_logs : historique récent par magasin + { name: 'idx_supplier_mail_logs_delivery', table: 'supplier_mail_logs', columns: 'delivery_id' }, + { name: 'idx_supplier_mail_logs_group', table: 'supplier_mail_logs', columns: 'group_id' }, + { name: 'idx_supplier_mail_logs_group_created', table: 'supplier_mail_logs', columns: 'group_id, created_at DESC' }, +]; + +let performanceIndexesRunning = false; + +/** + * Crée les index de performance manquants sans verrouiller les tables en écriture. + * + * CREATE INDEX CONCURRENTLY est interdit dans une transaction, et pg en ouvre une + * implicite pour une chaîne de plusieurs instructions : chaque index part donc + * dans sa propre requête, avec son propre try/catch pour qu'un échec (table + * absente, etc.) n'empêche pas les suivants. + */ +export async function ensurePerformanceIndexes(): Promise { + if (!pool) { + console.warn('⚠️ MIGRATION: No database pool, performance indexes not checked'); + return; + } + + const startedAt = Date.now(); + console.log('🔄 MIGRATION: Checking performance indexes in background...'); + + // 1. Une création CONCURRENTLY interrompue laisse un index invalide, que + // IF NOT EXISTS considère ensuite comme présent : on purge ces reliquats. + // Un index en cours de construction (par exemple par le processus d'avant un + // redémarrage) est lui aussi invalide : on l'écarte via + // pg_stat_progress_create_index. DROP INDEX CONCURRENTLY ne prend pas de + // verrou exclusif, donc la purge ne bloque jamais les lectures de l'app. + try { + const invalid = await pool.query(` + SELECT c.relname + FROM pg_index i + JOIN pg_class c ON c.oid = i.indexrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE NOT i.indisvalid AND n.nspname = 'public' AND c.relname LIKE 'idx_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_stat_progress_create_index p WHERE p.index_relid = i.indexrelid + ) + `); + for (const row of invalid.rows) { + const indexName = String(row.relname).replace(/"/g, '""'); + try { + await pool.query(`DROP INDEX CONCURRENTLY IF EXISTS public."${indexName}"`); + console.log(`🧹 MIGRATION: Invalid index ${row.relname} dropped`); + } catch (error) { + console.warn(`⚠️ MIGRATION: Invalid index ${row.relname} could not be dropped:`, (error as any)?.message); + } + } + } catch (error) { + console.warn('⚠️ MIGRATION: Invalid indexes purge failed:', (error as any)?.message); + } + + // 2. État actuel : tables présentes, index par nom (valides ou non) et colonnes + // déjà indexées sous un autre nom (ex. idx_deliveries_invoice_ref d'init.sql), + // pour ne pas créer de doublon. En cas d'échec, chaque index est tenté. + let existingTables: Set | null = null; + const existingIndexNames = new Set(); + const invalidIndexNames = new Set(); + const indexedColumns = new Set(); + try { + const tables = await pool.query(` + SELECT c.relname + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' AND c.relkind IN ('r', 'p') + `); + const indexes = await pool.query(` + SELECT t.relname AS table_name, + c.relname AS index_name, + i.indisvalid AS valid, + (i.indpred IS NULL AND i.indexprs IS NULL) AS plain, + array_to_string(ARRAY( + SELECT a.attname + FROM unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord) + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum + WHERE k.ord <= i.indnkeyatts + ORDER BY k.ord + ), ',') AS columns + FROM pg_index i + JOIN pg_class c ON c.oid = i.indexrelid + JOIN pg_class t ON t.oid = i.indrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + `); + existingTables = new Set(tables.rows.map((row: any) => row.relname)); + for (const row of indexes.rows) { + if (!row.valid) { + invalidIndexNames.add(row.index_name); + continue; + } + existingIndexNames.add(row.index_name); + if (row.plain) indexedColumns.add(`${row.table_name}(${row.columns})`); + } + } catch (error) { + console.warn('⚠️ MIGRATION: Could not read existing indexes:', (error as any)?.message); + } + + // 3. Création des index manquants, une requête par index + let created = 0; + let present = 0; + let skipped = 0; + const failed: string[] = []; + const tablesToAnalyze = new Set(); + + for (const index of PERFORMANCE_INDEXES) { + // Le sens de tri n'importe pas : un btree se parcourt dans les deux sens + const keyColumns = index.columns + .split(',') + .map((column) => column.trim().split(/\s+/)[0]) + .join(','); + if (existingIndexNames.has(index.name) || indexedColumns.has(`${index.table}(${keyColumns})`)) { + present++; + continue; + } + if (existingTables && !existingTables.has(index.table)) { + skipped++; + continue; + } + // Reliquat invalide que la purge n'a pas pu supprimer : IF NOT EXISTS l'ignorerait + if (invalidIndexNames.has(index.name)) { + failed.push(index.name); + console.warn(`⚠️ MIGRATION: Index ${index.name} is invalid and could not be purged`); + continue; + } + + try { + await pool.query( + `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${index.name} ON ${index.table} (${index.columns})` + + (index.where ? ` WHERE ${index.where}` : '') + ); + created++; + tablesToAnalyze.add(index.table); + console.log(`✅ MIGRATION: Index ${index.name} created`); + } catch (error) { + failed.push(index.name); + console.warn(`⚠️ MIGRATION: Index ${index.name} failed:`, (error as any)?.message); + } + } + + // 4. Statistiques à jour pour que le planificateur utilise les nouveaux index + // sans attendre le prochain autovacuum + for (const table of tablesToAnalyze) { + try { + await pool.query(`ANALYZE ${table}`); + } catch (error) { + console.warn(`⚠️ MIGRATION: ANALYZE ${table} failed:`, (error as any)?.message); + } + } + + const seconds = ((Date.now() - startedAt) / 1000).toFixed(1); + console.log( + `📊 MIGRATION: Performance indexes: ${created} created, ${present} already present, ` + + `${skipped} skipped (missing table), ${failed.length} failed in ${seconds}s` + + (failed.length > 0 ? ` (${failed.join(', ')})` : '') + ); +} + +// Lance la vérification des index en tâche de fond : ni le démarrage ni le +// healthcheck n'attendent la fin des créations. La route d'urgence pouvant +// relancer les migrations, un seul passage à la fois. +function startPerformanceIndexes(): void { + if (performanceIndexesRunning) return; + performanceIndexesRunning = true; + ensurePerformanceIndexes() + .catch((error) => console.error('❌ MIGRATION ERROR: Performance indexes check failed:', error)) + .finally(() => { + performanceIndexesRunning = false; + }); +} export async function runProductionMigrations() { const databaseUrl = process.env.DATABASE_URL; @@ -145,4 +423,7 @@ export async function runProductionMigrations() { await client.end(); console.log('🔌 MIGRATION: Database connection closed'); } + + // Après les migrations de schéma ci-dessus (colonne priority, supplier_mail_logs) + startPerformanceIndexes(); } \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 5cd8c5b..8dd549d 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -142,7 +142,8 @@ import { users, groups, userGroups, suppliers, orders, deliveries, publicities, publicityParticipations, customerOrders, nocodbConfig, dlcProducts, tasks, invoiceVerificationCache, dashboardMessages, webhookBapConfig, utilities, - avoirs + avoirs, + type UserWithGroups } from "@shared/schema"; import { hasPermission } from "@shared/permissions"; import { z } from "zod"; @@ -152,6 +153,13 @@ 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'; @@ -167,6 +175,10 @@ export async function registerRoutes(app: Express): Promise { 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(); }); @@ -424,18 +436,14 @@ export async function registerRoutes(app: Express): Promise { return res.json({ schedules: [], message: 'Groupe non trouvé' }); } - console.log('📅 Récupération échéances depuis deliveries:', { groupId, groupName: group.name }); - - // Récupérer toutes les livraisons du groupe - const allDeliveries = await storage.getDeliveries(); + // 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); - console.log(`📅 Livraisons avec échéance: ${deliveriesWithDueDate.length}, sans échéance: ${deliveriesWithoutDueDate.length}`); - // FALLBACK : Pour les livraisons sans dueDate ou sans TTC, interroger NocoDB const { InvoiceVerificationService } = await import('./invoiceVerification.js'); const verificationService = new InvoiceVerificationService(); @@ -486,8 +494,6 @@ export async function registerRoutes(app: Express): Promise { // 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); - console.log(`💰 Livraisons nécessitant récupération TTC: ${deliveriesNeedingTTC.length}`); - for (const delivery of deliveriesNeedingTTC) { try { const result = await verificationService.verifyInvoice( @@ -536,7 +542,6 @@ export async function registerRoutes(app: Express): Promise { }; }); - console.log(`📅 Total échéances retournées: ${schedules.length}`); res.json({ schedules }); } catch (error: any) { @@ -592,8 +597,8 @@ export async function registerRoutes(app: Express): Promise { return res.status(404).json({ error: 'Groupe non trouvé' }); } - // Récupérer toutes les livraisons du groupe avec échéance - const allDeliveries = await storage.getDeliveries(); + // 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 && @@ -887,7 +892,7 @@ export async function registerRoutes(app: Express): Promise { return res.status(401).json({ error: 'Non authentifié' }); } - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user || (user.role !== 'admin' && user.role !== 'directeur')) { return res.status(403).json({ error: 'Accès refusé' }); } @@ -969,8 +974,7 @@ export async function registerRoutes(app: Express): Promise { // Groups routes app.get('/api/groups', isAuthenticated, async (req: any, res) => { try { - const userId = req.user.claims ? req.user.claims ? req.user.claims.sub : req.user.id : req.user.id; - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1272,7 +1276,7 @@ export async function registerRoutes(app: Express): Promise { // Contacts routes app.get('/api/contacts', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) return res.status(404).json({ message: "User not found" }); let groupIds: number[] | undefined; @@ -1342,7 +1346,7 @@ export async function registerRoutes(app: Express): Promise { // Orders routes app.get('/api/orders', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1350,25 +1354,18 @@ export async function registerRoutes(app: Express): Promise { const { startDate, endDate, storeId } = req.query; let orders; - console.log('Orders API called with:', { startDate, endDate, storeId, userRole: user.role }); - if (user.role === 'admin') { let groupIds: number[] | undefined; // If admin selected a specific store, filter by it if (storeId) { groupIds = [parseInt(storeId as string)]; - console.log('🔍 Admin orders filtering by store:', { storeId, groupIds, role: user.role }); - } else { - console.log('🔍 Admin orders - showing all stores', { role: user.role }); } // Only filter by date if both startDate and endDate are provided if (startDate && endDate) { - console.log('Fetching orders by date range:', startDate, 'to', endDate); orders = await storage.getOrdersByDateRange(startDate as string, endDate as string, groupIds); } else { - console.log('Fetching all orders'); orders = await storage.getOrders(groupIds); } } else { @@ -1381,19 +1378,8 @@ export async function registerRoutes(app: Express): Promise { const requestedStoreId = parseInt(storeId as string); if (userGroupIds.includes(requestedStoreId)) { groupIds = [requestedStoreId]; - console.log('🔍 Non-admin orders - filtering by accessible store:', { - userId: user.id, - role: user.role, - requestedStoreId - }); } else { // User doesn't have access to this store, return empty array - console.log('🚫 Non-admin orders - user has no access to requested store:', { - userId: user.id, - role: user.role, - requestedStoreId, - userGroups: userGroupIds - }); return res.json([]); } } else { @@ -1401,17 +1387,7 @@ export async function registerRoutes(app: Express): Promise { if (user.role === 'directeur') { if (userGroupIds.length > 0) { groupIds = [userGroupIds[0]]; // Use first assigned store automatically - console.log('🔍 Directeur orders - using assigned store automatically:', { - userId: user.id, - role: user.role, - assignedStore: userGroupIds[0], - allUserGroups: userGroupIds - }); } else { - console.log('🚫 Directeur has no assigned stores:', { - userId: user.id, - role: user.role - }); return res.json([]); } } @@ -1419,26 +1395,11 @@ export async function registerRoutes(app: Express): Promise { else if (user.role === 'manager') { if (userGroupIds.length > 0) { groupIds = [userGroupIds[0]]; // Use first assigned store automatically - console.log('🔍 Manager orders - using assigned store automatically:', { - userId: user.id, - role: user.role, - assignedStore: userGroupIds[0], - allUserGroups: userGroupIds - }); } else { - console.log('🚫 Manager has no assigned stores:', { - userId: user.id, - role: user.role - }); return res.json([]); } } else { // For employee role, require explicit store selection - console.log('🔍 Employee orders - no store selection, returning empty:', { - userId: user.id, - role: user.role, - userGroups: userGroupIds - }); return res.json([]); } } @@ -1451,8 +1412,6 @@ export async function registerRoutes(app: Express): Promise { } } - console.log('Orders returned:', orders.length, 'items'); - res.json(orders); } catch (error) { console.error("Error fetching orders:", error); @@ -1484,7 +1443,7 @@ export async function registerRoutes(app: Express): Promise { app.get('/api/stats/by-group', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1503,7 +1462,7 @@ export async function registerRoutes(app: Express): Promise { app.get('/api/stats/by-supplier', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1522,7 +1481,7 @@ export async function registerRoutes(app: Express): Promise { app.get('/api/orders/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1557,7 +1516,7 @@ export async function registerRoutes(app: Express): Promise { environment: process.env.NODE_ENV }); - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { console.log('❌ User not found in order creation'); return res.status(404).json({ message: "User not found" }); @@ -1619,7 +1578,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/orders/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1654,7 +1613,7 @@ export async function registerRoutes(app: Express): Promise { app.delete('/api/orders/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1689,7 +1648,7 @@ export async function registerRoutes(app: Express): Promise { // Route pour diagnostiquer et synchroniser les statuts commandes/livraisons app.post('/api/sync-order-delivery-status', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -1751,7 +1710,7 @@ export async function registerRoutes(app: Express): Promise { // Deliveries routes app.get('/api/deliveries', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1759,7 +1718,12 @@ export async function registerRoutes(app: Express): Promise { const { startDate, endDate, storeId, withBL } = req.query; let deliveries; - console.log('Deliveries API called with:', { startDate, endDate, storeId, withBL, userRole: user.role }); + // 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; @@ -1767,18 +1731,13 @@ export async function registerRoutes(app: Express): Promise { // If admin selected a specific store, filter by it if (storeId) { groupIds = [parseInt(storeId as string)]; - console.log('🔍 Admin deliveries filtering by store:', { storeId, groupIds, role: user.role }); - } else { - console.log('🔍 Admin deliveries - showing all stores', { role: user.role }); } // Only filter by date if both startDate and endDate are provided if (startDate && endDate) { - console.log('Fetching deliveries by date range:', startDate, 'to', endDate); deliveries = await storage.getDeliveriesByDateRange(startDate as string, endDate as string, groupIds); } else { - console.log('Fetching all deliveries'); - deliveries = await storage.getDeliveries(groupIds); + deliveries = await storage.getDeliveries(groupIds, { status }); } } else { // For manager and employee roles, filter by their assigned groups @@ -1790,19 +1749,8 @@ export async function registerRoutes(app: Express): Promise { const requestedStoreId = parseInt(storeId as string); if (userGroupIds.includes(requestedStoreId)) { groupIds = [requestedStoreId]; - console.log('🔍 Non-admin deliveries - filtering by accessible store:', { - userId: user.id, - role: user.role, - requestedStoreId - }); } else { // User doesn't have access to this store, return empty array - console.log('🚫 Non-admin deliveries - user has no access to requested store:', { - userId: user.id, - role: user.role, - requestedStoreId, - userGroups: userGroupIds - }); return res.json([]); } } else { @@ -1810,17 +1758,7 @@ export async function registerRoutes(app: Express): Promise { if (user.role === 'directeur') { if (userGroupIds.length > 0) { groupIds = [userGroupIds[0]]; // Use first assigned store automatically - console.log('🔍 Directeur deliveries - using assigned store automatically:', { - userId: user.id, - role: user.role, - assignedStore: userGroupIds[0], - allUserGroups: userGroupIds - }); } else { - console.log('🚫 Directeur has no assigned stores:', { - userId: user.id, - role: user.role - }); return res.json([]); } } @@ -1828,26 +1766,11 @@ export async function registerRoutes(app: Express): Promise { else if (user.role === 'manager') { if (userGroupIds.length > 0) { groupIds = [userGroupIds[0]]; // Use first assigned store automatically - console.log('🔍 Manager deliveries - using assigned store automatically:', { - userId: user.id, - role: user.role, - assignedStore: userGroupIds[0], - allUserGroups: userGroupIds - }); } else { - console.log('🚫 Manager has no assigned stores:', { - userId: user.id, - role: user.role - }); return res.json([]); } } else { // For employee role, require explicit store selection - console.log('🔍 Employee deliveries - no store selection, returning empty:', { - userId: user.id, - role: user.role, - userGroups: userGroupIds - }); return res.json([]); } } @@ -1856,17 +1779,20 @@ export async function registerRoutes(app: Express): Promise { if (startDate && endDate) { deliveries = await storage.getDeliveriesByDateRange(startDate as string, endDate as string, groupIds); } else { - deliveries = await storage.getDeliveries(groupIds); + 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'); } - console.log('Deliveries returned:', deliveries.length, 'items'); - res.json(deliveries); } catch (error) { console.error("Error fetching deliveries:", error); @@ -1876,7 +1802,7 @@ export async function registerRoutes(app: Express): Promise { app.get('/api/deliveries/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1905,7 +1831,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/deliveries/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -1929,8 +1855,6 @@ export async function registerRoutes(app: Express): Promise { } } - console.log('🔄 Updating delivery:', { id, data: req.body, user: user.id }); - // Transform data types before validation const transformedData = { ...req.body }; @@ -2030,7 +1954,6 @@ export async function registerRoutes(app: Express): Promise { } const updatedDelivery = await storage.updateDelivery(id, data); - console.log('✅ Delivery updated successfully:', { id, updatedDelivery }); // SYNCHRONISATION AUTOMATIQUE : Si livraison devient "delivered", marquer la commande associée comme "delivered" // MAIS seulement après validation explicite (pas juste mise à jour status) @@ -2039,9 +1962,11 @@ export async function registerRoutes(app: Express): Promise { // 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 - const suppliers = await storage.getSuppliers(); - const supplier = suppliers.find((s: any) => s.id === updatedDelivery.supplierId); + // 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' && @@ -2071,7 +1996,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/deliveries', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2135,7 +2060,7 @@ export async function registerRoutes(app: Express): Promise { app.delete('/api/deliveries/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2174,7 +2099,7 @@ export async function registerRoutes(app: Express): Promise { // GET - Récupérer les commentaires d'une livraison app.get('/api/deliveries/:id/reconciliation-comments', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2210,7 +2135,7 @@ export async function registerRoutes(app: Express): Promise { // POST - Créer un nouveau commentaire app.post('/api/deliveries/:id/reconciliation-comments', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2253,7 +2178,7 @@ export async function registerRoutes(app: Express): Promise { // PUT - Modifier un commentaire app.put('/api/reconciliation-comments/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2291,7 +2216,7 @@ export async function registerRoutes(app: Express): Promise { // DELETE - Supprimer un commentaire app.delete('/api/reconciliation-comments/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2329,7 +2254,7 @@ export async function registerRoutes(app: Express): Promise { // 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 = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2438,7 +2363,7 @@ export async function registerRoutes(app: Express): Promise { // Historique des relances fournisseurs, restreint aux magasins de l'utilisateur app.get('/api/supplier-mail-logs', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2465,7 +2390,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/deliveries/:id/verify-invoice', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2513,15 +2438,6 @@ export async function registerRoutes(app: Express): Promise { return res.status(400).json({ message: "Référence de facture ou numéro BL requis" }); } - console.log('🔍 Vérification facture:', { - deliveryId, - invoiceReference, - blNumber, - supplier: delivery.supplier?.name, - group: delivery.group?.name, - groupId: delivery.groupId - }); - let result; if (invoiceReference && invoiceReference.trim()) { @@ -2549,8 +2465,6 @@ export async function registerRoutes(app: Express): Promise { }; } - console.log('✅ Résultat vérification:', result); - // 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 { @@ -2603,7 +2517,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/deliveries/:id/validate', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2677,7 +2591,7 @@ export async function registerRoutes(app: Express): Promise { // Marquer le contrôle d'une livraison comme effectué app.put('/api/deliveries/:id/control', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2711,7 +2625,7 @@ export async function registerRoutes(app: Express): Promise { // Route pour diagnostiquer le cache des livraisons app.get('/api/cache/diagnosis', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2767,7 +2681,7 @@ export async function registerRoutes(app: Express): Promise { // 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 = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2796,7 +2710,7 @@ export async function registerRoutes(app: Express): Promise { // DLC Products routes app.get('/api/dlc-products', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2830,7 +2744,7 @@ export async function registerRoutes(app: Express): Promise { app.get('/api/dlc-products/stats', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2859,7 +2773,7 @@ export async function registerRoutes(app: Express): Promise { app.get('/api/dlc-products/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2888,7 +2802,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/dlc-products', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2916,7 +2830,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/dlc-products/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2946,7 +2860,7 @@ export async function registerRoutes(app: Express): Promise { app.delete('/api/dlc-products/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -2976,7 +2890,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/dlc-products/:id/validate', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3022,7 +2936,7 @@ export async function registerRoutes(app: Express): Promise { // 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 = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3062,7 +2976,7 @@ export async function registerRoutes(app: Express): Promise { // 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 = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3107,7 +3021,7 @@ export async function registerRoutes(app: Express): Promise { // 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 = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3147,7 +3061,7 @@ export async function registerRoutes(app: Express): Promise { // 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 = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3192,7 +3106,7 @@ export async function registerRoutes(app: Express): Promise { // Tasks routes app.get('/api/tasks', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3203,11 +3117,6 @@ export async function registerRoutes(app: Express): Promise { if (user.role === 'admin') { // Admin can see all tasks or filter by specific store groupIds = storeId ? [parseInt(storeId as string)] : undefined; - console.log('🔍 Admin tasks filtering:', { - storeId, - groupIds, - message: storeId ? `Filtering by store ${storeId}` : 'Showing all stores' - }); } else { // For directeur and other non-admin users: always restrict to their assigned groups const userGroupIds = user.userGroups.map(ug => ug.groupId); @@ -3217,21 +3126,8 @@ export async function registerRoutes(app: Express): Promise { const requestedStoreId = parseInt(storeId as string); if (userGroupIds.includes(requestedStoreId)) { groupIds = [requestedStoreId]; - console.log('🔍 Non-admin user requesting specific accessible store:', { - userId: user.id, - role: user.role, - requestedStoreId, - hasAccess: true - }); } else { // User doesn't have access to this store, return empty array - console.log('🚫 Non-admin user requesting inaccessible store:', { - userId: user.id, - role: user.role, - requestedStoreId, - userGroups: userGroupIds, - hasAccess: false - }); return res.json([]); } } else { @@ -3239,34 +3135,11 @@ export async function registerRoutes(app: Express): Promise { // 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. - console.log('🔍 Non-admin user with no store selection - returning empty result:', { - userId: user.id, - role: user.role, - userGroups: userGroupIds, - message: 'Forcing explicit store selection' - }); return res.json([]); } } - console.log('🔍 Tasks API called with:', { - groupIds, - userRole: user.role, - userId: user.id, - requestedStoreId: storeId, - userGroups: user.role !== 'admin' ? user.userGroups.map(ug => ug.groupId) : 'all', - timestamp: new Date().toISOString() - }); - const tasks = await storage.getTasks(groupIds, user.role); - console.log('📋 Tasks returned:', { - count: tasks.length, - userId: user.id, - userRole: user.role, - requestedStoreId: storeId, - groupIds, - taskGroups: tasks.map(t => ({ id: t.id, title: t.title, groupId: t.groupId })).slice(0, 3) - }); res.json(tasks); } catch (error) { console.error("Error fetching tasks:", error); @@ -3276,7 +3149,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/tasks', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3291,14 +3164,6 @@ export async function registerRoutes(app: Express): Promise { createdBy: user.id, }; - console.log('📝 POST /api/tasks - Received data:', { - originalBody: req.body, - processedData: data, - dueDate: data.dueDate, - dueDateType: typeof data.dueDate, - dueDateValue: data.dueDate - }); - // Assign a default groupId if not provided if (!data.groupId) { if (user.role === 'admin') { @@ -3307,29 +3172,17 @@ export async function registerRoutes(app: Express): Promise { const userGroupIds = user.userGroups?.map(ug => ug.groupId) || []; data.groupId = userGroupIds.length > 0 ? userGroupIds[0] : 1; } - console.log('📝 POST /api/tasks - Assigned default groupId:', data.groupId); } // Check if user has access to the group if (user.role !== 'admin') { const userGroupIds = user.userGroups?.map(ug => ug.groupId) || []; - console.log('📝 POST /api/tasks - User group access check:', { - userGroupIds, - requestedGroupId: data.groupId, - hasUserGroups: !!user.userGroups - }); 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); - console.log('✅ Task created:', { - id: task.id, - title: task.title, - dueDate: task.dueDate, - dueDateType: typeof task.dueDate - }); res.json(task); } catch (error) { console.error("Error creating task:", error); @@ -3339,7 +3192,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/tasks/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3351,19 +3204,6 @@ export async function registerRoutes(app: Express): Promise { return res.status(404).json({ message: "Task not found" }); } - console.log('🔄 PUT /api/tasks/:id - Received data:', { - taskId: id, - originalTask: { - id: task.id, - title: task.title, - dueDate: task.dueDate, - dueDateType: typeof task.dueDate - }, - updateBody: req.body, - newDueDate: req.body.dueDate, - newDueDateType: typeof req.body.dueDate - }); - // Check permissions if (user.role !== 'admin') { const userGroupIds = user.userGroups.map(ug => ug.groupId); @@ -3388,15 +3228,7 @@ export async function registerRoutes(app: Express): Promise { cleanData.dueDate = req.body.dueDate === '' ? null : req.body.dueDate; } - console.log('🧹 Cleaned data for update:', cleanData); - const updatedTask = await storage.updateTask(id, cleanData); - console.log('✅ Task updated:', { - id: updatedTask.id, - title: updatedTask.title, - dueDate: updatedTask.dueDate, - dueDateType: typeof updatedTask.dueDate - }); res.json(updatedTask); } catch (error) { const taskId = parseInt(req.params.id); @@ -3404,7 +3236,6 @@ export async function registerRoutes(app: Express): Promise { taskId: taskId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, - reqBody: req.body, cleanData: req.body ? { title: req.body.title, description: req.body.description, @@ -3424,7 +3255,7 @@ export async function registerRoutes(app: Express): Promise { app.delete('/api/tasks/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3454,7 +3285,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/tasks/:id/complete', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3485,7 +3316,7 @@ export async function registerRoutes(app: Express): Promise { // Customer Orders routes app.get('/api/customer-orders', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3504,9 +3335,7 @@ export async function registerRoutes(app: Express): Promise { } } - console.log('Customer Orders API called with:', { groupIds, userRole: user.role }); const customerOrders = await storage.getCustomerOrders(groupIds); - console.log('Customer Orders returned:', customerOrders.length, 'items'); res.json(customerOrders); } catch (error) { console.error("Error fetching customer orders:", error); @@ -3516,7 +3345,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/customer-orders', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3526,44 +3355,21 @@ export async function registerRoutes(app: Express): Promise { createdBy: user.id, }; - // DEBUG: Log what we received and what we're about to save - console.log('🔍 CUSTOMER ORDER - Received data from frontend:', req.body); - console.log('🔍 CUSTOMER ORDER - Final data for DB:', data); - console.log('🔍 CUSTOMER ORDER - orderTaker value:', data.orderTaker, 'type:', typeof data.orderTaker); - - // Check if user has access to the group - PRODUCTION DEBUG - console.log('🔍 CUSTOMER ORDER PERMISSION DEBUG:', { - userRole: user.role, - userId: user.id, - userGroups: user.userGroups, - requestedGroupId: data.groupId, - requestedGroupIdType: typeof data.groupId - }); - + // Check if user has access to the group if (user.role !== 'admin') { const userGroupIds = user.userGroups ? user.userGroups.map(ug => ug.groupId) : []; - console.log('🔍 CUSTOMER ORDER - User group IDs:', userGroupIds); - console.log('🔍 CUSTOMER ORDER - Requested group ID:', data.groupId); // Convert data.groupId to number if it's a string const requestedGroupId = typeof data.groupId === 'string' ? parseInt(data.groupId) : data.groupId; - console.log('🔍 CUSTOMER ORDER - Converted group ID:', requestedGroupId); // Allow managers, directeurs, and employees to create orders in their assigned groups if (!['manager', 'directeur', 'employee'].includes(user.role)) { - console.log('❌ CUSTOMER ORDER - Access denied: Invalid role for customer orders'); return res.status(403).json({ message: "Insufficient permissions to create customer orders" }); } if (!userGroupIds.includes(requestedGroupId)) { - console.log('❌ CUSTOMER ORDER - Access denied: User not in requested group'); - console.log('🔍 Available groups:', userGroupIds, 'Requested:', requestedGroupId); - console.log('🔍 Type check - userGroupIds types:', userGroupIds.map(id => typeof id)); - console.log('🔍 Type check - requestedGroupId type:', typeof requestedGroupId); return res.status(403).json({ message: "Access denied to this group" }); } - - console.log('✅ CUSTOMER ORDER - Permission granted for user role:', user.role); } const customerOrder = await storage.createCustomerOrder(data); @@ -3576,7 +3382,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/customer-orders/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3606,7 +3412,7 @@ export async function registerRoutes(app: Express): Promise { app.delete('/api/customer-orders/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3637,7 +3443,7 @@ export async function registerRoutes(app: Express): Promise { // Client call tracking routes app.get('/api/customer-orders/pending-calls', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3663,7 +3469,6 @@ export async function registerRoutes(app: Express): Promise { const pendingCalls = await storage.getPendingClientCalls(groupIds); - console.log(`📞 Pending client calls fetched: ${pendingCalls.length} calls for user ${user.role}`); res.json(pendingCalls); } catch (error) { console.error("Error fetching pending client calls:", error); @@ -3673,7 +3478,7 @@ export async function registerRoutes(app: Express): Promise { app.patch('/api/customer-orders/:id/mark-called', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3710,7 +3515,7 @@ export async function registerRoutes(app: Express): Promise { // Avoir routes app.get('/api/avoirs', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3732,9 +3537,7 @@ export async function registerRoutes(app: Express): Promise { } } - console.log('🔍 Avoirs API called with:', { groupIds, userRole: user.role }); const avoirs = await storage.getAvoirs(groupIds); - console.log('📋 Avoirs returned:', avoirs.length, 'items'); res.json(avoirs); } catch (error) { console.error("Error fetching avoirs:", error); @@ -3744,7 +3547,7 @@ export async function registerRoutes(app: Express): Promise { app.get('/api/avoirs/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3773,23 +3576,17 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/avoirs', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } - // DEBUG: Log des données reçues - console.log('🔍 [POST AVOIR] Données reçues:', JSON.stringify(req.body, null, 2)); - console.log('🔍 [POST AVOIR] User:', { id: user.id, role: user.role }); - // Validate data with Zod schema const validatedData = insertAvoirSchema.parse({ ...req.body, createdBy: user.id, }); - console.log('✅ [POST AVOIR] Données validées:', JSON.stringify(validatedData, null, 2)); - // Check if user has access to the specified group if (user.role !== 'admin' && user.role !== 'directeur') { const userGroupIds = user.userGroups.map(ug => ug.groupId); @@ -3850,7 +3647,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/avoirs/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3871,9 +3668,7 @@ export async function registerRoutes(app: Express): Promise { } // ✅ CRITICAL FIX: Validate data with Zod schema (partial) - console.log('💰 PUT Avoir - Raw body received:', JSON.stringify(req.body, null, 2)); const validatedData = insertAvoirSchema.partial().parse(req.body); - console.log('💰 PUT Avoir - Validated data:', JSON.stringify(validatedData, null, 2)); // ✅ FIX: Convertir undefined en null pour les champs optionnels (important pour PostgreSQL) const dataForDb: any = { @@ -3882,7 +3677,6 @@ export async function registerRoutes(app: Express): Promise { invoiceReference: validatedData.invoiceReference === undefined ? null : validatedData.invoiceReference, comment: validatedData.comment === undefined ? null : validatedData.comment, }; - console.log('💰 PUT Avoir - Data for DB (undefined → null):', JSON.stringify(dataForDb, null, 2)); const updatedAvoir = await storage.updateAvoir(id, dataForDb); console.log('✅ Avoir updated:', id, 'by user:', user.id); @@ -3944,7 +3738,7 @@ export async function registerRoutes(app: Express): Promise { app.delete('/api/avoirs/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -3981,7 +3775,7 @@ export async function registerRoutes(app: Express): Promise { // 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 = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4029,14 +3823,6 @@ export async function registerRoutes(app: Express): Promise { return res.status(400).json({ message: "Référence de facture requise" }); } - console.log('🔍 Vérification facture avoir:', { - avoirId, - invoiceReference, - supplier: avoir.supplier?.name, - group: avoir.group?.name, - groupId: avoir.groupId - }); - // Vérifier par référence de facture uniquement const result = await invoiceVerificationService.verifyInvoice( invoiceReference, @@ -4045,7 +3831,6 @@ export async function registerRoutes(app: Express): Promise { false // Les avoirs ne sont pas "réconciliés" comme les livraisons ); - console.log('✅ Résultat vérification avoir:', result); res.json(result); } catch (error) { console.error("Error verifying avoir invoice:", error); @@ -4059,7 +3844,7 @@ export async function registerRoutes(app: Express): Promise { // Avoir status update routes app.put('/api/avoirs/:id/webhook-status', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4082,7 +3867,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/avoirs/:id/nocodb-verification', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4125,7 +3910,7 @@ export async function registerRoutes(app: Express): Promise { // Route pour marquer explicitement le cache comme réconcilié app.post('/api/cache/mark-reconciled', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4154,7 +3939,7 @@ export async function registerRoutes(app: Express): Promise { // Statistics routes app.get('/api/stats/monthly', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4191,7 +3976,7 @@ export async function registerRoutes(app: Express): Promise { // Route pour les statistiques annuelles app.get('/api/stats/yearly', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4267,35 +4052,19 @@ export async function registerRoutes(app: Express): Promise { // Users management routes app.get('/api/users', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || !['admin', 'directeur', 'manager'].includes(user.role)) { return res.status(403).json({ message: "Access denied" }); } - // Get all basic users first - const baseUsers = await storage.getUsers(); - - // Add userGroups and userRoles to each user individually with error handling - const usersWithData = await Promise.all( - baseUsers.map(async (baseUser) => { - try { - const userWithGroups = await storage.getUserWithGroups(baseUser.id); - return { - ...baseUser, - userGroups: userWithGroups?.userGroups || [], - userRoles: [] // Keep roles simple for now since we're using hardcoded permissions - }; - } catch (error) { - console.error(`❌ Error getting groups for user ${baseUser.username}:`, error); - // Return user with empty groups if there's an error - return { - ...baseUser, - userGroups: [], - userRoles: [] - }; - } - }) - ); + // 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) { @@ -4308,8 +4077,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/users', isAuthenticated, async (req: any, res) => { try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - const currentUser = await storage.getUserWithGroups(userId); + const currentUser = getCurrentUser(req); if (!currentUser || currentUser.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -4391,7 +4159,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/users/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -4460,7 +4228,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/users/:id/groups', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -4480,7 +4248,7 @@ export async function registerRoutes(app: Express): Promise { app.delete('/api/users/:id/groups/:groupId', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -4496,7 +4264,7 @@ export async function registerRoutes(app: Express): Promise { // Delete user route app.delete('/api/users/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -4509,12 +4277,7 @@ export async function registerRoutes(app: Express): Promise { } // Remove user from all groups first - const userWithGroups = await storage.getUserWithGroups(userToDelete); - if (userWithGroups) { - for (const userGroup of userWithGroups.userGroups) { - await storage.removeUserFromGroup(userToDelete, userGroup.groupId); - } - } + await storage.removeUserFromAllGroups(userToDelete); // Delete the user await storage.deleteUser(userToDelete); @@ -4528,7 +4291,7 @@ export async function registerRoutes(app: Express): Promise { // Publicity routes (renamed to ad-campaigns to avoid adblocker issues) app.get('/api/ad-campaigns/debug', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4562,7 +4325,7 @@ export async function registerRoutes(app: Express): Promise { app.get('/api/ad-campaigns/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4594,7 +4357,7 @@ export async function registerRoutes(app: Express): Promise { // Get all publicities (with optional year and store filtering) app.get('/api/ad-campaigns', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4628,7 +4391,7 @@ export async function registerRoutes(app: Express): Promise { app.post('/api/ad-campaigns', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4664,7 +4427,7 @@ export async function registerRoutes(app: Express): Promise { app.put('/api/ad-campaigns/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -4706,7 +4469,7 @@ export async function registerRoutes(app: Express): Promise { }); try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + 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" }); @@ -4745,7 +4508,7 @@ export async function registerRoutes(app: Express): Promise { }); try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + 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" }); @@ -4775,7 +4538,7 @@ export async function registerRoutes(app: Express): Promise { // Schema logging route for production debugging app.get('/api/debug/log-schema', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + 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.' }); } @@ -4903,7 +4666,7 @@ export async function registerRoutes(app: Express): Promise { // Download database schema report app.get('/api/debug/download-schema', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + 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.' }); } @@ -5089,7 +4852,7 @@ RÉSUMÉ DU SCAN // NocoDB Configuration routes app.get('/api/nocodb-config', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + 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.' }); } @@ -5104,7 +4867,7 @@ RÉSUMÉ DU SCAN app.post('/api/nocodb-config', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + 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.' }); } @@ -5123,7 +4886,7 @@ RÉSUMÉ DU SCAN app.put('/api/nocodb-config/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + 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.' }); } @@ -5140,7 +4903,7 @@ RÉSUMÉ DU SCAN app.delete('/api/nocodb-config/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + 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.' }); } @@ -5156,7 +4919,7 @@ RÉSUMÉ DU SCAN app.get('/api/nocodb-config/active', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(403).json({ message: 'Accès refusé.' }); } @@ -5172,7 +4935,7 @@ RÉSUMÉ DU SCAN // Backup management routes (Admin only) app.get('/api/backups', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -5187,7 +4950,7 @@ RÉSUMÉ DU SCAN app.post('/api/backups', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -5202,7 +4965,7 @@ RÉSUMÉ DU SCAN app.get('/api/backups/:filename/download', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -5224,7 +4987,7 @@ RÉSUMÉ DU SCAN app.delete('/api/backups/:filename', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -5241,8 +5004,7 @@ RÉSUMÉ DU SCAN // SAV (Service Après-Vente) routes app.get('/api/sav/tickets', isAuthenticated, async (req: any, res) => { try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -5255,16 +5017,13 @@ RÉSUMÉ DU SCAN const selectedGroupId = req.query.groupId ? parseInt(req.query.groupId) : null; if (selectedGroupId) { groupIds = [selectedGroupId]; - console.log(`🎫 [SAV] Admin filtering by selected store: ${selectedGroupId}`); } else { groupIds = []; // See all tickets - console.log(`🎫 [SAV] Admin viewing all tickets`); } } else { // Other roles see only their assigned groups const userGroups = (user as any).userGroups; groupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; - console.log(`🎫 [SAV] User ${user.username} (${user.role}) can see stores:`, groupIds); } // Parse query filters @@ -5287,8 +5046,7 @@ RÉSUMÉ DU SCAN app.get('/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.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -5318,8 +5076,7 @@ RÉSUMÉ DU SCAN app.post('/api/sav/tickets', isAuthenticated, async (req: any, res) => { try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -5334,11 +5091,9 @@ RÉSUMÉ DU SCAN if (user.role === 'admin') { const allGroups = await storage.getGroups(); availableGroupIds = allGroups.map(g => g.id); - console.log(`🎫 [SAV] Admin ${user.username} can access all groups:`, availableGroupIds); } else { const userGroups = (user as any).userGroups; availableGroupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; - console.log(`🎫 [SAV] User ${user.username} (${user.role}) has groups:`, availableGroupIds); } if (availableGroupIds.length === 0) { @@ -5351,7 +5106,6 @@ RÉSUMÉ DU SCAN // Parse and validate request body const assignedGroupId = req.body.groupId || availableGroupIds[0]; - console.log(`🎫 [SAV] Creating ticket for user ${user.username} with groupId: ${assignedGroupId} (requested: ${req.body.groupId}, available: ${availableGroupIds})`); const ticketData = insertSavTicketSchema.parse({ ...req.body, @@ -5371,12 +5125,6 @@ RÉSUMÉ DU SCAN } const ticket = await storage.createSavTicket(ticketDataWithNumber); - console.log(`🎫 [SAV] Ticket created successfully:`, { - ticketNumber: ticket.ticketNumber, - groupId: ticket.groupId, - createdBy: ticket.createdBy, - userRole: user.role - }); res.status(201).json(ticket); } catch (error) { console.error("Error creating SAV ticket:", error); @@ -5389,8 +5137,7 @@ RÉSUMÉ DU SCAN app.patch('/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.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -5470,8 +5217,7 @@ RÉSUMÉ DU SCAN app.post('/api/sav/tickets/:id/history', isAuthenticated, async (req: any, res) => { try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -5523,8 +5269,7 @@ RÉSUMÉ DU SCAN app.get('/api/sav/stats', isAuthenticated, async (req: any, res) => { try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -5537,16 +5282,13 @@ RÉSUMÉ DU SCAN const selectedGroupId = req.query.groupId ? parseInt(req.query.groupId) : null; if (selectedGroupId) { groupIds = [selectedGroupId]; - console.log(`📊 [SAV STATS] Admin filtering by selected store: ${selectedGroupId}`); } else { groupIds = []; // See all stats - console.log(`📊 [SAV STATS] Admin viewing all stats`); } } else { // Other roles see only their assigned groups const userGroups = (user as any).userGroups; groupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : []; - console.log(`📊 [SAV STATS] User ${user.username} (${user.role}) can see stores:`, groupIds); } const stats = await storage.getSavTicketStats(groupIds.length > 0 ? groupIds : undefined); @@ -5595,7 +5337,7 @@ RÉSUMÉ DU SCAN // Weather routes app.get('/api/weather/settings', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -5610,7 +5352,7 @@ RÉSUMÉ DU SCAN app.post('/api/weather/settings', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -5629,7 +5371,7 @@ RÉSUMÉ DU SCAN app.put('/api/weather/settings/:id', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -5649,7 +5391,7 @@ RÉSUMÉ DU SCAN app.post('/api/weather/test-connection', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user || user.role !== 'admin') { return res.status(403).json({ message: "Access denied" }); } @@ -5679,49 +5421,58 @@ RÉSUMÉ DU SCAN const today = new Date().toISOString().split('T')[0]; const previousYearDate = weatherService.getPreviousYearDate(); - // Check if we already have today's data - let currentYearData = await storage.getWeatherData(today, true); - let previousYearData = await storage.getWeatherData(previousYearDate, false); + // Check if we already have today's data (deux lectures indépendantes) + let [currentYearData, previousYearData] = await Promise.all([ + storage.getWeatherData(today, true), + storage.getWeatherData(previousYearDate, false), + ]); - // 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 - currentYearData = await storage.getWeatherData(today, true); + // Les appels à l'API pour l'année en cours et l'année précédente sont + // indépendants : ils sont lancés en parallèle + await Promise.all([ + (async () => { + // 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 + currentYearData = await storage.getWeatherData(today, true); + } + } } } - } - } - - // 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 - previousYearData = await storage.getWeatherData(previousYearDate, false); + })(), + (async () => { + // 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 + previousYearData = await storage.getWeatherData(previousYearDate, false); + } + } + } else { + console.warn("⚠️ [HISTORY] Could not fetch historical data - continuing with current year only"); } } - } 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 @@ -5752,12 +5503,6 @@ RÉSUMÉ DU SCAN location: settings.location }; - console.log('🌤️ [RESPONSE] Weather data prepared:', { - hasCurrentYear: !!response.currentYear, - hasPreviousYear: !!response.previousYear, - location: response.location - }); - res.json(response); } catch (error) { console.error("Error fetching weather data:", error); @@ -5769,21 +5514,16 @@ RÉSUMÉ DU SCAN // Announcement routes - PostgreSQL en production, mémoire en développement app.get('/api/announcements', isAuthenticated, async (req: any, res) => { try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - console.log('📢 [SERVER] Fetching announcements for user:', userId, 'environment:', environment); - - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } - console.log('📢 [SERVER] User found:', { username: user.username, role: user.role }); - if (environment === 'production') { // PRODUCTION: Utiliser PostgreSQL avec DASHBOARD_MESSAGES - console.log('🎯 [PRODUCTION] Using PostgreSQL DASHBOARD_MESSAGES table'); - 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, @@ -5792,7 +5532,18 @@ RÉSUMÉ DU SCAN storeId: dashboardMessages.storeId, createdBy: dashboardMessages.createdBy, createdAt: dashboardMessages.createdAt, - }).from(dashboardMessages); + 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) { @@ -5806,67 +5557,43 @@ RÉSUMÉ DU SCAN ); } - const messages = await query.orderBy(desc(dashboardMessages.createdAt)).limit(5); + const rows = await query.orderBy(desc(dashboardMessages.createdAt)).limit(5); - console.log('🔍 [PRODUCTION] Raw messages from DB:', messages.length, 'items:', messages); + const announcements = rows.map((row: any) => { + const { + authorId, authorUsername, authorFirstName, authorLastName, authorName, + groupId, groupName, + ...message + } = row; - // Ajouter les relations manuellement - const announcements = await Promise.all( - messages.map(async (message: any) => { - // Récupérer l'auteur - let author = { id: message.createdBy, firstName: 'Utilisateur', lastName: 'Inconnu', username: message.createdBy }; - try { - // En production, createdBy est varchar, donc chercher par username - console.log('🔍 [PRODUCTION] Looking for user with username:', message.createdBy); - const [userResult] = await db.select({ - id: users.id, - username: users.username, - firstName: users.firstName, - lastName: users.lastName, - name: users.name - }).from(users).where(eq(users.username, message.createdBy)); - console.log('🔍 [PRODUCTION] User search result:', userResult ? 'Found' : 'Not found'); - if (userResult) { - // Utiliser name s'il existe, sinon firstName + lastName, sinon username - const displayName = userResult.name || - (userResult.firstName && userResult.lastName ? `${userResult.firstName} ${userResult.lastName}` : '') || - userResult.username; + // 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: userResult.id, - firstName: userResult.firstName || displayName.split(' ')[0] || userResult.username, - lastName: userResult.lastName || displayName.split(' ').slice(1).join(' ') || '', - username: userResult.username - }; - } else { - console.warn('❌ [PRODUCTION] User not found with username:', message.createdBy); - } - } catch (e) { - console.warn('❌ [PRODUCTION] Could not fetch author for message:', message.id, e); - } - - // Récupérer le groupe si storeId est défini - let group = null; - if (message.storeId) { - try { - const [groupResult] = await db.select().from(groups).where(eq(groups.id, message.storeId)); - if (groupResult) { - group = { id: groupResult.id, name: groupResult.name }; - } - } catch (e) { - console.warn('Could not fetch group for message:', message.id); - } - } - - return { - ...message, - author, - group + 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 + }; + }); - console.log('🎯 [PRODUCTION] Found announcements:', announcements.length); res.json(announcements); } catch (dbError) { @@ -5878,7 +5605,6 @@ RÉSUMÉ DU SCAN } } else { // DÉVELOPPEMENT: Utiliser stockage mémoire - console.log('🧠 [DEV] Using memory storage for announcements'); const groupIds = user.role === 'admin' && req.query.storeId ? [parseInt(req.query.storeId as string)] : undefined; @@ -5893,34 +5619,18 @@ RÉSUMÉ DU SCAN }); app.post('/api/announcements', isAuthenticated, async (req: any, res) => { - console.log('🎯 [SERVER] POST /api/announcements endpoint hit'); - console.log('🎯 [SERVER] Request body:', JSON.stringify(req.body, null, 2)); - console.log('🎯 [SERVER] User object:', { - hasClaims: !!req.user.claims, - hasId: !!req.user.id, - userId: req.user.claims ? req.user.claims.sub : req.user.id - }); - try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - console.log('🎯 [SERVER] Extracted userId:', userId); - - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { - console.error('🎯 [SERVER] User not found for ID:', userId); return res.status(404).json({ message: "User not found" }); } - console.log('🎯 [SERVER] User found:', { username: user.username, role: user.role, id: user.id }); - // 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" }); } - console.log('🎯 [SERVER] User is admin, proceeding with validation'); - const announcementData = insertAnnouncementSchema.parse({ title: req.body.title, content: req.body.content, @@ -5929,11 +5639,8 @@ RÉSUMÉ DU SCAN createdBy: user.username, // Utiliser username pour PostgreSQL }); - console.log('🎯 [SERVER] Announcement data validated:', announcementData); - if (environment === 'production') { // PRODUCTION: Créer dans PostgreSQL DASHBOARD_MESSAGES - console.log('🎯 [PRODUCTION] Creating in PostgreSQL DASHBOARD_MESSAGES table'); try { const [newMessage] = await db.insert(dashboardMessages).values({ title: announcementData.title, @@ -5949,7 +5656,6 @@ RÉSUMÉ DU SCAN group: null }; - console.log('🎯 [PRODUCTION] Announcement created successfully in DB:', announcement); res.status(201).json(announcement); } catch (dbError) { console.error('🎯 [PRODUCTION] DB error, fallback to memory:', dbError); @@ -5959,7 +5665,6 @@ RÉSUMÉ DU SCAN } else { // DÉVELOPPEMENT: Créer en mémoire const announcement = await storage.createAnnouncement(announcementData); - console.log('🧠 [DEV] Announcement created in memory:', announcement); res.status(201).json(announcement); } } catch (error) { @@ -5974,21 +5679,12 @@ RÉSUMÉ DU SCAN // PUT /api/announcements/:id - Update announcement (admin only) app.put('/api/announcements/:id', isAuthenticated, async (req: any, res) => { - console.log('📝 [SERVER] PUT /api/announcements/:id endpoint hit'); - console.log('📝 [SERVER] Request body:', JSON.stringify(req.body, null, 2)); - try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - console.log('📝 [SERVER] Extracted userId:', userId); - - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { - console.error('📝 [SERVER] User not found for ID:', userId); return res.status(404).json({ message: "User not found" }); } - console.log('📝 [SERVER] User found:', { username: user.username, role: user.role, id: user.id }); - // Only admin can edit announcements if (user.role !== 'admin') { console.error('📝 [SERVER] Access denied - user role:', user.role); @@ -5996,7 +5692,6 @@ RÉSUMÉ DU SCAN } const id = parseInt(req.params.id); - console.log('📝 [SERVER] Announcement ID:', id); // Verify announcement exists const existingAnnouncement = await storage.getAnnouncement(id); @@ -6005,13 +5700,9 @@ RÉSUMÉ DU SCAN return res.status(404).json({ message: "Announcement not found" }); } - console.log('📝 [SERVER] User is admin, proceeding with validation'); - const announcementData = insertAnnouncementSchema.partial().parse(req.body); - console.log('📝 [SERVER] Announcement data validated:', announcementData); const updatedAnnouncement = await storage.updateAnnouncement(id, announcementData); - console.log('📝 [SERVER] Announcement updated successfully:', updatedAnnouncement); res.json(updatedAnnouncement); } catch (error) { @@ -6025,21 +5716,12 @@ RÉSUMÉ DU SCAN }); app.delete('/api/announcements/:id', isAuthenticated, async (req: any, res) => { - console.log('🗑️ [SERVER] DELETE /api/announcements/:id endpoint hit'); - console.log('🗑️ [SERVER] Announcement ID:', req.params.id); - try { - const userId = req.user.claims ? req.user.claims.sub : req.user.id; - console.log('🗑️ [SERVER] Extracted userId:', userId); - - const user = await storage.getUserWithGroups(userId); + const user = getCurrentUser(req); if (!user) { - console.error('🗑️ [SERVER] User not found for ID:', userId); return res.status(404).json({ message: "User not found" }); } - console.log('🗑️ [SERVER] User found:', { username: user.username, role: user.role, id: user.id }); - // Only admin can delete announcements if (user.role !== 'admin') { console.error('🗑️ [SERVER] Access denied - user role:', user.role); @@ -6093,7 +5775,7 @@ RÉSUMÉ DU SCAN app.post('/api/weather/geolocation', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -6217,7 +5899,7 @@ RÉSUMÉ DU SCAN // Analytics routes app.get('/api/analytics/summary', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -6249,7 +5931,7 @@ RÉSUMÉ DU SCAN app.get('/api/analytics/timeseries', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -6280,7 +5962,7 @@ RÉSUMÉ DU SCAN app.get('/api/analytics/by-supplier', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -6309,7 +5991,7 @@ RÉSUMÉ DU SCAN app.get('/api/analytics/by-store', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } @@ -6330,7 +6012,7 @@ RÉSUMÉ DU SCAN app.get('/api/analytics/export', isAuthenticated, async (req: any, res) => { try { - const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + const user = getCurrentUser(req); if (!user) { return res.status(404).json({ message: "User not found" }); } diff --git a/server/storage.ts b/server/storage.ts index 78fb197..c76c6c6 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -90,9 +90,54 @@ import { } from "@shared/schema"; import { encryptSecret, decryptSecret } from "./crypto"; import { db } from "./db"; -import { eq, and, inArray, desc, sql, gte, lte, lt, gt, or, isNull, isNotNull, asc, ne } from "drizzle-orm"; +import { eq, and, inArray, desc, sql, gte, lte, lt, gt, or, isNull, isNotNull, asc, ne, getTableColumns } from "drizzle-orm"; import { getAnnouncementStorage } from "./announcementStorage"; +// Projections des relations jointes dans les listes. +// +// La ligne groups complète contient le logo (data URI de plusieurs dizaines de +// Ko) et la configuration SMTP : la joindre telle quelle la recopiait dans +// chaque ligne de chaque liste. Le client ne lit que ces champs. Le +// rapprochement et les avoirs ont en plus besoin de la config NocoDB et du +// webhook. La fiche complète reste disponible via getGroup/getGroups. +// L'id doit rester en tête : drizzle renvoie null pour la relation quand la +// première colonne d'une jointure externe est nulle. +const groupSummaryColumns = { + id: groups.id, + name: groups.name, + color: groups.color, +}; + +const groupReconciliationColumns = { + ...groupSummaryColumns, + nocodbConfigId: groups.nocodbConfigId, + nocodbTableName: groups.nocodbTableName, + webhookUrl: groups.webhookUrl, +}; + +// Créateur ou auteur joint à une ligne, sans le hash du mot de passe +const userSummaryColumns = { + id: users.id, + firstName: users.firstName, + lastName: users.lastName, + username: users.username, + email: users.email, +}; + +// Toutes les colonnes de users sauf le hash du mot de passe +const { password: _password, ...publicUserColumns } = getTableColumns(users); + +// Magasin joint aux affectations d'un utilisateur (forme de UserWithGroups). +// Les dates restent la valeur brute du driver, comme avec l'ancienne requête SQL +// brute, pour ne pas changer ce que reçoit le client. +const userGroupColumns = { + id: groups.id, + name: groups.name, + color: groups.color, + createdAt: sql`${groups.createdAt}`, + updatedAt: sql`${groups.updatedAt}`, +}; + export interface IStorage { // User operations @@ -101,6 +146,8 @@ export interface IStorage { getUserByUsername(username: string): Promise; upsertUser(user: UpsertUser): Promise; getUserWithGroups(id: string): Promise; + // Tous les utilisateurs avec leurs magasins, sans le champ password + getUsersWithGroups(): Promise; getUsers(): Promise; createUser(user: UpsertUser): Promise; updateUser(id: string, user: Partial): Promise; @@ -134,7 +181,7 @@ export interface IStorage { deleteOrder(id: number): Promise; // Delivery operations - getDeliveries(groupIds?: number[]): Promise; + getDeliveries(groupIds?: number[], options?: { status?: string }): Promise; getDeliveriesByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise; getDelivery(id: number): Promise; createDelivery(delivery: InsertDelivery): Promise; @@ -147,6 +194,7 @@ export interface IStorage { getUserGroups(userId: string): Promise; assignUserToGroup(userGroup: InsertUserGroup): Promise; removeUserFromGroup(userId: string, groupId: number): Promise; + removeUserFromAllGroups(userId: string): Promise; // Statistics getMonthlyStats(year: number, month: number, groupIds?: number[]): Promise<{ @@ -369,43 +417,78 @@ export class DatabaseStorage implements IStorage { } async getUserWithGroups(id: string): Promise { - const user = await this.getUser(id); - if (!user) return undefined; - + // Appelée à chaque requête authentifiée (deserializeUser) : l'utilisateur + // et ses magasins sont lus en une seule requête au lieu de deux successives try { - const result = await db.execute(sql` - SELECT - ug.user_id, - ug.group_id, - g.id as group_id_ref, - g.name as group_name, - g.color as group_color, - g.created_at as group_created_at, - g.updated_at as group_updated_at - FROM user_groups ug - INNER JOIN groups g ON ug.group_id = g.id - WHERE ug.user_id = ${id} - `); + const rows = await db + .select({ + user: users, + groupId: userGroups.groupId, + group: userGroupColumns, + }) + .from(users) + .leftJoin(userGroups, eq(userGroups.userId, users.id)) + .leftJoin(groups, eq(userGroups.groupId, groups.id)) + .where(eq(users.id, id)); - const userGroups = result.rows.map((row: any) => ({ - userId: row.user_id, - groupId: row.group_id, - group: { - id: row.group_id_ref, - name: row.group_name, - color: row.group_color, - createdAt: row.group_created_at, - updatedAt: row.group_updated_at, - } - })); + if (rows.length === 0) return undefined; - return { ...user, userGroups }; + // Les affectations vers un magasin inexistant sont ignorées, comme + // avec la jointure interne d'origine + const userGroupsList = rows + .filter((row: any) => row.group !== null) + .map((row: any) => ({ + userId: id, + groupId: row.groupId, + group: row.group, + })); + + return { ...rows[0].user, userGroups: userGroupsList }; } catch (error) { console.error('Error fetching user with groups:', error); - return { ...user, userGroups: [] }; + const user = await this.getUser(id); + return user ? { ...user, userGroups: [] } : undefined; } } + async getUsersWithGroups(): Promise { + // Deux requêtes en parallèle pour tous les utilisateurs, au lieu d'un + // getUserWithGroups par utilisateur + const [allUsers, memberships] = await Promise.all([ + db.select(publicUserColumns).from(users), + (async () => { + try { + return await db + .select({ + userId: userGroups.userId, + groupId: userGroups.groupId, + group: userGroupColumns, + }) + .from(userGroups) + .innerJoin(groups, eq(userGroups.groupId, groups.id)); + } catch (error) { + console.error('Error fetching user groups:', error); + return []; + } + })(), + ]); + + const groupsByUserId = new Map(); + for (const membership of memberships) { + const list = groupsByUserId.get(membership.userId); + if (list) { + list.push(membership); + } else { + groupsByUserId.set(membership.userId, [membership]); + } + } + + return allUsers.map((user: any) => ({ + ...user, + userGroups: groupsByUserId.get(user.id) ?? [], + })) as UserWithGroups[]; + } + async getUsers(): Promise { return await db.select().from(users); } @@ -550,7 +633,7 @@ export class DatabaseStorage implements IStorage { createdAt: deliveries.createdAt, updatedAt: deliveries.updatedAt, supplier: suppliers, - group: groups, + group: groupReconciliationColumns, creator: { id: users.id, firstName: users.firstName, @@ -596,7 +679,7 @@ export class DatabaseStorage implements IStorage { createdAt: orders.createdAt, updatedAt: orders.updatedAt, supplier: suppliers, - group: groups, + group: groupReconciliationColumns, creator: { id: users.id, firstName: users.firstName, @@ -690,7 +773,7 @@ export class DatabaseStorage implements IStorage { createdAt: orders.createdAt, updatedAt: orders.updatedAt, supplier: suppliers, - group: groups, + group: groupReconciliationColumns, creator: { id: users.id, firstName: users.firstName, @@ -718,7 +801,6 @@ export class DatabaseStorage implements IStorage { deliveries: deliveriesByOrderId.get(order.id) ?? [] })); - console.log(`🔗 PRODUCTION: getOrders() récupéré ${ordersWithDeliveries.length} commandes avec relations`); return ordersWithDeliveries as OrderWithRelations[]; } @@ -737,7 +819,7 @@ export class DatabaseStorage implements IStorage { createdAt: orders.createdAt, updatedAt: orders.updatedAt, supplier: suppliers, - group: groups, + group: groupReconciliationColumns, creator: { id: users.id, firstName: users.firstName, @@ -781,7 +863,9 @@ export class DatabaseStorage implements IStorage { } async getOrder(id: number): Promise { - const [order] = await db + // La commande et ses livraisons ne dépendent que de l'id : les deux + // requêtes partent en parallèle + const orderQuery = db .select({ id: orders.id, supplierId: orders.supplierId, @@ -810,10 +894,8 @@ export class DatabaseStorage implements IStorage { .leftJoin(users, eq(orders.createdBy, users.id)) .where(eq(orders.id, id)); - if (!order) return undefined; - - // Récupérer les livraisons associées à cette commande (PRODUCTION PostgreSQL) - const associatedDeliveries = await db + // Livraisons associées à cette commande + const deliveriesQuery = db .select({ id: deliveries.id, orderId: deliveries.orderId, @@ -840,7 +922,7 @@ export class DatabaseStorage implements IStorage { createdAt: deliveries.createdAt, updatedAt: deliveries.updatedAt, supplier: suppliers, - group: groups, + group: groupReconciliationColumns, creator: { id: users.id, firstName: users.firstName, @@ -855,7 +937,9 @@ export class DatabaseStorage implements IStorage { .leftJoin(users, eq(deliveries.createdBy, users.id)) .where(eq(deliveries.orderId, id)); - console.log(`🔗 PRODUCTION: getOrder #${id} found ${associatedDeliveries.length} associated deliveries`); + const [[order], associatedDeliveries] = await Promise.all([orderQuery, deliveriesQuery]); + + if (!order) return undefined; return { ...order, @@ -882,7 +966,7 @@ export class DatabaseStorage implements IStorage { } // Delivery operations - async getDeliveries(groupIds?: number[]): Promise { + async getDeliveries(groupIds?: number[], options?: { status?: string }): Promise { let query = db .select({ id: deliveries.id, @@ -910,7 +994,7 @@ export class DatabaseStorage implements IStorage { createdAt: deliveries.createdAt, updatedAt: deliveries.updatedAt, supplier: suppliers, - group: groups, + group: groupReconciliationColumns, creator: { id: users.id, firstName: users.firstName, @@ -924,15 +1008,22 @@ export class DatabaseStorage implements IStorage { .leftJoin(groups, eq(deliveries.groupId, groups.id)) .leftJoin(users, eq(deliveries.createdBy, users.id)); + const conditions = []; if (groupIds && groupIds.length > 0) { - query = query.where(inArray(deliveries.groupId, groupIds)); + conditions.push(inArray(deliveries.groupId, groupIds)); + } + // Filtre de statut optionnel (ex. le rapprochement ne lit que les livraisons livrées) + if (options?.status) { + conditions.push(eq(deliveries.status, options.status)); + } + if (conditions.length > 0) { + query = query.where(and(...conditions)); } const baseDeliveries = await query.orderBy(desc(deliveries.createdAt)); const deliveriesWithOrders = await this.attachOrdersAndCommentCounts(baseDeliveries); - console.log(`🔗 PRODUCTION: getDeliveries() récupéré ${deliveriesWithOrders.length} livraisons avec relations`); return deliveriesWithOrders as DeliveryWithRelations[]; } @@ -964,7 +1055,7 @@ export class DatabaseStorage implements IStorage { createdAt: deliveries.createdAt, updatedAt: deliveries.updatedAt, supplier: suppliers, - group: groups, + group: groupReconciliationColumns, creator: { id: users.id, firstName: users.firstName, @@ -1035,56 +1126,64 @@ export class DatabaseStorage implements IStorage { if (!delivery) return undefined; - // Add creator info separately to avoid complex JOIN issues in production - let creator = null; - try { - const user = await this.getUser(delivery.createdBy); - if (user) { - creator = { + // Créateur, commande associée et nombre de commentaires ne dépendent que de + // la livraison : les trois chargements partent en parallèle. Chacun garde + // son try/catch pour qu'un échec n'empêche pas de renvoyer la livraison. + const loadCreator = async () => { + try { + const user = await this.getUser(delivery.createdBy); + if (!user) return null; + return { id: user.id, firstName: user.firstName, lastName: user.lastName, username: user.username, email: user.email }; + } catch (error) { + console.warn('⚠️ Could not load creator info:', error); + return null; } - } catch (error) { - console.log('⚠️ Could not load creator info:', error); - } + }; - // Récupérer la commande associée si elle existe (PRODUCTION PostgreSQL) - let associatedOrder = undefined; - if (delivery.orderId) { + const loadAssociatedOrder = async () => { + if (!delivery.orderId) return undefined; try { - console.log(`🔗 PRODUCTION: getDelivery #${id} retrieving associated order #${delivery.orderId}`); const orderData = await this.getOrder(delivery.orderId); - if (orderData) { - // CRITICAL FIX: Vérifier que la commande appartient au même magasin que la livraison - if (orderData.groupId !== delivery.groupId) { - console.error(`❌ PRODUCTION: getDelivery #${id} (store ${delivery.groupId}) linked to order #${delivery.orderId} (store ${orderData.groupId}) - STORE MISMATCH DETECTED!`); - // Ne pas inclure la commande si elle n'appartient pas au bon magasin - } else { - associatedOrder = orderData; - console.log(`✅ PRODUCTION: getDelivery #${id} found associated order #${delivery.orderId} with status: ${orderData.status}`); - } + if (!orderData) return undefined; + // CRITICAL FIX: Vérifier que la commande appartient au même magasin que la livraison + if (orderData.groupId !== delivery.groupId) { + console.error(`❌ PRODUCTION: getDelivery #${id} (store ${delivery.groupId}) linked to order #${delivery.orderId} (store ${orderData.groupId}) - STORE MISMATCH DETECTED!`); + // Ne pas inclure la commande si elle n'appartient pas au bon magasin + return undefined; } + return orderData; } catch (error) { console.error(`❌ PRODUCTION: Failed to retrieve associated order #${delivery.orderId} for delivery #${id}:`, error); + return undefined; } - } + }; // Compter les commentaires de rapprochement pour cette livraison - let commentsCount = 0; - try { - const [countResult] = await db - .select({ count: sql`count(*)` }) - .from(reconciliationComments) - .where(eq(reconciliationComments.deliveryId, id)); + const countComments = async () => { + try { + const [countResult] = await db + .select({ count: sql`count(*)` }) + .from(reconciliationComments) + .where(eq(reconciliationComments.deliveryId, id)); - commentsCount = Number(countResult?.count || 0); - } catch (error) { - console.error(`Failed to count reconciliation comments for delivery #${id}:`, error); - } + return Number(countResult?.count || 0); + } catch (error) { + console.error(`Failed to count reconciliation comments for delivery #${id}:`, error); + return 0; + } + }; + + const [creator, associatedOrder, commentsCount] = await Promise.all([ + loadCreator(), + loadAssociatedOrder(), + countComments() + ]); return { ...delivery, @@ -1218,6 +1317,10 @@ export class DatabaseStorage implements IStorage { ); } + async removeUserFromAllGroups(userId: string): Promise { + await db.delete(userGroups).where(eq(userGroups.userId, userId)); + } + // Statistics async getMonthlyStats(year: number, month: number, groupIds?: number[]): Promise<{ ordersCount: number; @@ -1229,7 +1332,6 @@ export class DatabaseStorage implements IStorage { }> { // En mode développement avec MemStorage, retourner des statistiques simulées if (process.env.NODE_ENV === 'development') { - console.log('📊 Mode développement - statistiques simulées'); return { ordersCount: 12, deliveriesCount: 8, @@ -1247,8 +1349,6 @@ export class DatabaseStorage implements IStorage { : `${year}-${(month + 1).toString().padStart(2, '0')}-01`; try { - console.log('📊 Calcul statistiques mensuelles:', { year, month, startDate, endDate, groupIds }); - // Construire les conditions pour le filtrage par groupe - UTILISER LE BON CHAMP DE DATE let ordersWhereCondition = and( gte(orders.plannedDate, startDate), @@ -1267,13 +1367,13 @@ export class DatabaseStorage implements IStorage { } // Compter les commandes du mois - const ordersResult = await db + const ordersQuery = db .select({ count: sql`count(*)` }) .from(orders) .where(ordersWhereCondition); // Compter les livraisons du mois - const deliveriesResult = await db + const deliveriesQuery = db .select({ count: sql`count(*)` }) .from(deliveries) .where(deliveriesWhereCondition); @@ -1284,7 +1384,7 @@ export class DatabaseStorage implements IStorage { pendingWhereCondition = and(pendingWhereCondition, inArray(orders.groupId, groupIds)) as any; } - const pendingResult = await db + const pendingQuery = db .select({ count: sql`count(*)` }) .from(orders) .where(pendingWhereCondition); @@ -1305,7 +1405,7 @@ export class DatabaseStorage implements IStorage { // NOUVEAU: Calculer le délai moyen entre la date de commande et la date de livraison // Uniquement pour les livraisons qui ont une commande liée - const deliveriesStatsResult = await db + const deliveriesStatsQuery = db .select({ totalPalettes: sql`COALESCE(SUM(CAST(${deliveries.quantity} as INTEGER)), 0)`, totalPackages: sql`COALESCE(COUNT(*), 0)`, @@ -1321,6 +1421,14 @@ export class DatabaseStorage implements IStorage { ) ); + // Les quatre requêtes sont indépendantes : on les lance en parallèle + const [ordersResult, deliveriesResult, pendingResult, deliveriesStatsResult] = await Promise.all([ + ordersQuery, + deliveriesQuery, + pendingQuery, + deliveriesStatsQuery + ]); + const ordersCount = Number(ordersResult[0]?.count || 0); const deliveriesCount = Number(deliveriesResult[0]?.count || 0); const pendingOrdersCount = Number(pendingResult[0]?.count || 0); @@ -1328,15 +1436,6 @@ export class DatabaseStorage implements IStorage { const totalPackages = Number(deliveriesStatsResult[0]?.totalPackages || 0); const averageDeliveryTime = Number(deliveriesStatsResult[0]?.avgDelay || 0); - console.log('📊 Statistiques calculées:', { - ordersCount, - deliveriesCount, - pendingOrdersCount, - averageDeliveryTime: `${averageDeliveryTime} jours (commande → livraison, livraisons avec commande liée uniquement)`, - totalPalettes, - totalPackages, - }); - return { ordersCount, deliveriesCount, @@ -1371,7 +1470,6 @@ export class DatabaseStorage implements IStorage { }> { // En mode développement avec MemStorage, retourner des statistiques simulées if (process.env.NODE_ENV === 'development') { - console.log('📊 Mode développement - statistiques annuelles simulées'); return { ordersCount: 144, // 12 mois * 12 deliveriesCount: 96, // 12 mois * 8 @@ -1387,8 +1485,6 @@ export class DatabaseStorage implements IStorage { const endDate = `${year + 1}-01-01`; try { - console.log('📊 Calcul statistiques annuelles:', { year, startDate, endDate, groupIds }); - // Construire les conditions pour le filtrage par groupe let ordersWhereCondition = and( gte(orders.plannedDate, startDate), @@ -1406,13 +1502,13 @@ export class DatabaseStorage implements IStorage { } // Compter les commandes de l'année - const ordersResult = await db + const ordersQuery = db .select({ count: sql`count(*)` }) .from(orders) .where(ordersWhereCondition); // Compter les livraisons de l'année - const deliveriesResult = await db + const deliveriesQuery = db .select({ count: sql`count(*)` }) .from(deliveries) .where(deliveriesWhereCondition); @@ -1423,7 +1519,7 @@ export class DatabaseStorage implements IStorage { pendingWhereCondition = and(pendingWhereCondition, inArray(orders.groupId, groupIds)) as any; } - const pendingResult = await db + const pendingQuery = db .select({ count: sql`count(*)` }) .from(orders) .where(pendingWhereCondition); @@ -1443,7 +1539,7 @@ export class DatabaseStorage implements IStorage { // NOUVEAU: Calculer le délai moyen entre la date de commande et la date de livraison // Uniquement pour les livraisons qui ont une commande liée - const deliveriesStatsResult = await db + const deliveriesStatsQuery = db .select({ totalPalettes: sql`COALESCE(SUM(CAST(${deliveries.quantity} as INTEGER)), 0)`, totalPackages: sql`COALESCE(COUNT(*), 0)`, @@ -1459,6 +1555,14 @@ export class DatabaseStorage implements IStorage { ) ); + // Les quatre requêtes sont indépendantes : on les lance en parallèle + const [ordersResult, deliveriesResult, pendingResult, deliveriesStatsResult] = await Promise.all([ + ordersQuery, + deliveriesQuery, + pendingQuery, + deliveriesStatsQuery + ]); + const ordersCount = Number(ordersResult[0]?.count || 0); const deliveriesCount = Number(deliveriesResult[0]?.count || 0); const pendingOrdersCount = Number(pendingResult[0]?.count || 0); @@ -1466,15 +1570,6 @@ export class DatabaseStorage implements IStorage { const totalPackages = Number(deliveriesStatsResult[0]?.totalPackages || 0); const averageDeliveryTime = Number(deliveriesStatsResult[0]?.avgDelay || 0); - console.log('📊 Statistiques annuelles calculées:', { - ordersCount, - deliveriesCount, - pendingOrdersCount, - averageDeliveryTime: `${averageDeliveryTime} jours (commande → livraison, livraisons avec commande liée uniquement)`, - totalPalettes, - totalPackages, - }); - return { ordersCount, deliveriesCount, @@ -1521,19 +1616,13 @@ export class DatabaseStorage implements IStorage { // SAFE: Use simple column ordering instead of SQL CAST which fails in production const results = await query.orderBy(publicities.pubNumber); - // LOG: Debug des publicités récupérées - console.log(`📋 PUBLICITES FETCHED: ${results.length} résultats pour année ${year || 'toutes'}`); - if (results.length > 0) { - console.log('🔍 PREMIERS RESULTATS:', results.slice(0, 3).map((p: any, i: any) => `${i + 1}. N°${p.pubNumber} - ${p.designation}`)); - } - const publicityIds = results.map((p: any) => p.id); const participations = publicityIds.length > 0 ? await db .select({ publicityId: publicityParticipations.publicityId, groupId: publicityParticipations.groupId, - group: groups, + group: groupSummaryColumns, }) .from(publicityParticipations) .leftJoin(groups, eq(publicityParticipations.groupId, groups.id)) @@ -1547,15 +1636,25 @@ export class DatabaseStorage implements IStorage { return numA - numB; }); + // Participations regroupées par publicité en un seul passage + const participationsByPublicityId = new Map(); + for (const p of participations as any[]) { + const participation = { + publicityId: p.publicityId, + groupId: p.groupId, + group: p.group!, + }; + const list = participationsByPublicityId.get(p.publicityId); + if (list) { + list.push(participation); + } else { + participationsByPublicityId.set(p.publicityId, [participation]); + } + } + return sortedResults.map((publicity: any) => ({ ...publicity, - participations: participations - .filter((p: any) => p.publicityId === publicity.id) - .map((p: any) => ({ - publicityId: p.publicityId, - groupId: p.groupId, - group: p.group!, - })), + participations: participationsByPublicityId.get(publicity.id) ?? [], })); } @@ -1567,7 +1666,7 @@ export class DatabaseStorage implements IStorage { .select({ publicityId: publicityParticipations.publicityId, groupId: publicityParticipations.groupId, - group: groups, + group: groupSummaryColumns, }) .from(publicityParticipations) .leftJoin(groups, eq(publicityParticipations.groupId, groups.id)) @@ -1598,22 +1697,16 @@ export class DatabaseStorage implements IStorage { } async deletePublicity(id: number): Promise { - console.log(`🗑️ [DELETION] Starting deletion of publicity ID: ${id}`); - try { // First, delete all participations (defensive approach for production DB constraints) - const deletedParticipations = await db.delete(publicityParticipations).where(eq(publicityParticipations.publicityId, id)).returning(); - console.log(`🗑️ [DELETION] Deleted ${deletedParticipations.length} participations for publicity ${id}`); + await db.delete(publicityParticipations).where(eq(publicityParticipations.publicityId, id)); // Then delete the publicity itself const deletedPublicity = await db.delete(publicities).where(eq(publicities.id, id)).returning(); - console.log(`🗑️ [DELETION] Deleted publicity ${id}, found: ${deletedPublicity.length > 0 ? 'YES' : 'NO'}`); if (deletedPublicity.length === 0) { throw new Error(`Publicity with ID ${id} not found`); } - - console.log(`✅ [DELETION] Successfully deleted publicity ID: ${id}`); } catch (error) { console.error(`❌ [DELETION] Failed to delete publicity ID: ${id}`, error); throw error; @@ -1670,8 +1763,6 @@ export class DatabaseStorage implements IStorage { .where(eq(nocodbConfig.isActive, true)) .limit(1); - // Ne pas tracer le jeton en clair dans les logs - console.log('🔧 Configuration NocoDB active récupérée:', config?.id, config?.name); return this.decryptNocodbConfig(config); } catch (error) { console.error('❌ Erreur récupération config NocoDB:', error); @@ -1715,11 +1806,6 @@ export class DatabaseStorage implements IStorage { const expiresAt = new Date(cache.expiresAt); if (now < expiresAt) { - console.log('✅ [DATABASE-CACHE] Cache hit pour:', { - cacheKey, - expires: cache.expiresAt, - hoursRemaining: Math.round((expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60)) - }); return cache; // Cache valide } else { console.log('⏰ [DATABASE-CACHE] Cache expiré, suppression:', { @@ -1733,7 +1819,6 @@ export class DatabaseStorage implements IStorage { } } - console.log('❌ [DATABASE-CACHE] Cache miss pour:', { cacheKey }); return undefined; // Pas de cache } @@ -1870,7 +1955,7 @@ export class DatabaseStorage implements IStorage { .select({ customerOrder: customerOrders, supplier: suppliers, - group: groups + group: groupSummaryColumns }) .from(customerOrders) .leftJoin(suppliers, eq(customerOrders.supplierId, suppliers.id)) @@ -1942,7 +2027,7 @@ export class DatabaseStorage implements IStorage { .select({ customerOrder: customerOrders, supplier: suppliers, - group: groups + group: groupSummaryColumns }) .from(customerOrders) .leftJoin(suppliers, eq(customerOrders.supplierId, suppliers.id)) @@ -1978,7 +2063,7 @@ export class DatabaseStorage implements IStorage { .select({ dlcProduct: dlcProducts, supplier: suppliers, - group: groups + group: groupSummaryColumns }) .from(dlcProducts) .leftJoin(suppliers, eq(dlcProducts.supplierId, suppliers.id)) @@ -2271,7 +2356,7 @@ export class DatabaseStorage implements IStorage { let query = db .select({ task: tasks, - group: groups + group: groupSummaryColumns }) .from(tasks) .leftJoin(groups, eq(tasks.groupId, groups.id)); @@ -2300,18 +2385,6 @@ export class DatabaseStorage implements IStorage { const results = await query.orderBy(desc(tasks.createdAt)); - console.log('📋 DatabaseStorage.getTasks - Raw results:', { - resultCount: results.length, - userRole, - sampleTasks: results.slice(0, 2).map((r: any) => ({ - taskExists: !!r.task, - taskId: r.task?.id, - title: r.task?.title, - startDate: r.task?.startDate, - groupExists: !!r.group - })) - }); - return results .filter((row: any) => row.task) // Filtrer les tâches nulles .map((row: any) => { @@ -2544,7 +2617,7 @@ export class DatabaseStorage implements IStorage { createdAt: avoirs.createdAt, updatedAt: avoirs.updatedAt, supplier: suppliers, - group: groups, + group: groupReconciliationColumns, creator: { id: users.id, firstName: users.firstName, @@ -2665,8 +2738,8 @@ export class DatabaseStorage implements IStorage { .select({ ticket: savTickets, supplier: suppliers, - group: groups, - creator: users, + group: groupSummaryColumns, + creator: userSummaryColumns, }) .from(savTickets) .leftJoin(suppliers, eq(savTickets.supplierId, suppliers.id)) @@ -2700,21 +2773,16 @@ export class DatabaseStorage implements IStorage { const results = await query.orderBy(desc(savTickets.createdAt)); - // Get history for each ticket - const ticketsWithHistory = await Promise.all( - results.map(async (result: any) => { - const history = await this.getSavTicketHistory(result.ticket.id); - return { - ...result.ticket, - supplier: result.supplier!, - group: result.group!, - creator: result.creator!, - history, - }; - }) - ); - - return ticketsWithHistory; + // L'historique SAV est désactivé (getSavTicketHistory renvoie toujours []) : + // pas d'appel par ticket. Si on le réactive, le charger en une seule + // requête groupée plutôt que ticket par ticket. + return results.map((result: any) => ({ + ...result.ticket, + supplier: result.supplier!, + group: result.group!, + creator: result.creator!, + history: [], + })); } async getSavTicket(id: number): Promise { @@ -2723,7 +2791,7 @@ export class DatabaseStorage implements IStorage { ticket: savTickets, supplier: suppliers, group: groups, - creator: users, + creator: userSummaryColumns, }) .from(savTickets) .leftJoin(suppliers, eq(savTickets.supplierId, suppliers.id)) @@ -2747,10 +2815,11 @@ export class DatabaseStorage implements IStorage { async createSavTicket(ticketData: InsertSavTicket): Promise { // Generate ticket number const currentYear = new Date().getFullYear(); + // Bornes de l'année plutôt qu'EXTRACT(year ...), pour que l'index sur created_at serve const count = await db .select({ count: sql`count(*)` }) .from(savTickets) - .where(sql`EXTRACT(year from created_at) = ${currentYear}`); + .where(sql`${savTickets.createdAt} >= make_date(${currentYear}::int, 1, 1) AND ${savTickets.createdAt} < make_date(${currentYear + 1}::int, 1, 1)`); const ticketNumber = `SAV-${currentYear}-${String((count[0]?.count || 0) + 1).padStart(4, '0')}`; @@ -2822,69 +2891,26 @@ export class DatabaseStorage implements IStorage { resolvedTickets: number; criticalTickets: number; }> { - let baseQuery = db.select().from(savTickets); - - if (groupIds?.length) { - baseQuery = baseQuery.where(inArray(savTickets.groupId, groupIds)); - } - - // Get status counts - const statusResults = await db + // Tous les compteurs en une seule requête. « Critiques » compte tous les + // tickets de priorité critique, quel que soit leur statut. + const [result] = await db .select({ - count: sql`count(*)`, - status: savTickets.status + total: sql`count(*)`, + newCount: sql`count(*) filter (where ${savTickets.status} = 'nouveau')`, + inProgressCount: sql`count(*) filter (where ${savTickets.status} in ('en_cours', 'attente_pieces', 'attente_echange'))`, + resolvedCount: sql`count(*) filter (where ${savTickets.status} in ('resolu', 'ferme'))`, + criticalCount: sql`count(*) filter (where ${savTickets.priority} = 'critique')` }) .from(savTickets) - .where(groupIds?.length ? inArray(savTickets.groupId, groupIds) : undefined) - .groupBy(savTickets.status); - - // Get priority counts - const priorityResults = await db - .select({ - count: sql`count(*)`, - priority: savTickets.priority - }) - .from(savTickets) - .where(groupIds?.length ? inArray(savTickets.groupId, groupIds) : undefined) - .groupBy(savTickets.priority); - - // Get total count - const totalResult = await db - .select({ count: sql`count(*)` }) - .from(savTickets) .where(groupIds?.length ? inArray(savTickets.groupId, groupIds) : undefined); - const stats = { - totalTickets: Number(totalResult[0]?.count || 0), - newTickets: 0, - inProgressTickets: 0, - resolvedTickets: 0, - criticalTickets: 0, + return { + totalTickets: Number(result?.total || 0), + newTickets: Number(result?.newCount || 0), + inProgressTickets: Number(result?.inProgressCount || 0), + resolvedTickets: Number(result?.resolvedCount || 0), + criticalTickets: Number(result?.criticalCount || 0), }; - - // Process status results - statusResults.forEach((result: any) => { - const count = Number(result.count || 0); - - if (result.status === 'nouveau') { - stats.newTickets = count; - } else if (['en_cours', 'attente_pieces', 'attente_echange'].includes(result.status)) { - stats.inProgressTickets += count; - } else if (['resolu', 'ferme'].includes(result.status)) { - stats.resolvedTickets += count; - } - }); - - // Process priority results for critical tickets - priorityResults.forEach((result: any) => { - const count = Number(result.count || 0); - - if (result.priority === 'critique') { - stats.criticalTickets = count; - } - }); - - return stats; } // Weather operations @@ -3043,8 +3069,8 @@ export class DatabaseStorage implements IStorage { authorId: reconciliationComments.authorId, createdAt: reconciliationComments.createdAt, updatedAt: reconciliationComments.updatedAt, - author: users, - group: groups, + author: { ...userSummaryColumns, role: users.role }, + group: groupSummaryColumns, delivery: { id: deliveries.id, orderId: deliveries.orderId, @@ -3096,7 +3122,7 @@ export class DatabaseStorage implements IStorage { authorId: reconciliationComments.authorId, createdAt: reconciliationComments.createdAt, updatedAt: reconciliationComments.updatedAt, - author: users, + author: { ...userSummaryColumns, role: users.role }, group: groups, delivery: { id: deliveries.id, @@ -3210,7 +3236,6 @@ export class DatabaseStorage implements IStorage { // Get total orders const orderQuery = db.select({ count: sql`COUNT(*)` }).from(orders); if (orderConditions.length) orderQuery.where(and(...orderConditions)); - const [{ count: totalOrders }] = await orderQuery; // Get total deliveries and amounts const deliveryQuery = db.select({ @@ -3220,7 +3245,6 @@ export class DatabaseStorage implements IStorage { avgDelay: sql`AVG(EXTRACT(EPOCH FROM (delivered_date - scheduled_date)) / 86400)` // days }).from(deliveries); if (deliveryConditions.length) deliveryQuery.where(and(...deliveryConditions)); - const [deliveryStats] = await deliveryQuery; // Get top suppliers const supplierQuery = db.select({ @@ -3233,29 +3257,46 @@ export class DatabaseStorage implements IStorage { .innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id)); if (deliveryConditions.length) supplierQuery.where(and(...deliveryConditions)); - const topSuppliers = await supplierQuery - .groupBy(suppliers.id, suppliers.name) - .orderBy(desc(sql`COUNT(${deliveries.id})`)) - .limit(5); - // Get top stores - const storeQuery = db.select({ - id: groups.id, - name: groups.name, - orders: sql`COUNT(DISTINCT ${orders.id})`, - deliveries: sql`COUNT(DISTINCT ${deliveries.id})` - }) - .from(groups) - .leftJoin(orders, eq(groups.id, orders.groupId)) - .leftJoin(deliveries, eq(groups.id, deliveries.groupId)); + // Get top stores : commandes et livraisons comptées séparément par magasin + // puis jointes, au lieu d'une jointure orders × deliveries qui multipliait + // les lignes. Comme avant, seul le filtre magasin s'applique ici. + const storeGroupIds = filters.groupIds?.length ? filters.groupIds : null; + const storeQuery = db.execute(sql` + SELECT + g.id AS id, + g.name AS name, + COALESCE(o.count, 0) AS orders, + COALESCE(d.count, 0) AS deliveries + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(*) AS count + FROM orders + ${storeGroupIds ? sql`WHERE group_id IN ${storeGroupIds}` : sql``} + GROUP BY group_id + ) o ON o.group_id = g.id + LEFT JOIN ( + SELECT group_id, COUNT(*) AS count + FROM deliveries + ${storeGroupIds ? sql`WHERE group_id IN ${storeGroupIds}` : sql``} + GROUP BY group_id + ) d ON d.group_id = g.id + ${storeGroupIds ? sql`WHERE g.id IN ${storeGroupIds}` : sql``} + ORDER BY COALESCE(o.count, 0) + COALESCE(d.count, 0) DESC + LIMIT 5 + `); - if (filters.groupIds?.length) { - storeQuery.where(inArray(groups.id, filters.groupIds)); - } - const topStores = await storeQuery - .groupBy(groups.id, groups.name) - .orderBy(desc(sql`COUNT(DISTINCT ${orders.id}) + COUNT(DISTINCT ${deliveries.id})`)) - .limit(5); + // Les quatre requêtes sont indépendantes : on les lance en parallèle + const [[{ count: totalOrders }], [deliveryStats], topSuppliers, storeResult] = await Promise.all([ + orderQuery, + deliveryQuery, + supplierQuery + .groupBy(suppliers.id, suppliers.name) + .orderBy(desc(sql`COUNT(${deliveries.id})`)) + .limit(5), + storeQuery + ]); + const topStores = storeResult.rows; return { totalOrders: Number(totalOrders) || 0, @@ -3330,8 +3371,6 @@ export class DatabaseStorage implements IStorage { ORDER BY TO_CHAR(planned_date, '${dateFormat}') `; - const ordersData = await db.execute(sql.raw(ordersSql)); - // Get deliveries by date using raw SQL const deliveriesSql = ` SELECT TO_CHAR(scheduled_date, '${dateFormat}') as date, COUNT(*) as count @@ -3341,7 +3380,11 @@ export class DatabaseStorage implements IStorage { ORDER BY TO_CHAR(scheduled_date, '${dateFormat}') `; - const deliveriesData = await db.execute(sql.raw(deliveriesSql)); + // Les deux séries sont indépendantes : on les charge en parallèle + const [ordersData, deliveriesData] = await Promise.all([ + db.execute(sql.raw(ordersSql)), + db.execute(sql.raw(deliveriesSql)) + ]); // Merge data const dataMap = new Map(); @@ -3616,6 +3659,19 @@ export class MemStorage implements IStorage { return { ...user, userGroups: userGroupsWithGroups }; } + async getUsersWithGroups(): Promise { + return Array.from(this.users.values()).map(({ password: _password, ...user }) => { + const userGroupsList = this.userGroups.get(user.id) || []; + return { + ...user, + userGroups: userGroupsList.map(ug => ({ + ...ug, + group: this.groups.get(ug.groupId)!, + })), + } as UserWithGroups; + }); + } + async getUsers(): Promise { return Array.from(this.users.values()); } @@ -4029,11 +4085,14 @@ export class MemStorage implements IStorage { this.orders.delete(id); } - async getDeliveries(groupIds?: number[]): Promise { + async getDeliveries(groupIds?: number[], options?: { status?: string }): Promise { let deliveries = Array.from(this.deliveries.values()); if (groupIds && groupIds.length > 0) { deliveries = deliveries.filter(delivery => groupIds.includes(delivery.groupId)); } + if (options?.status) { + deliveries = deliveries.filter(delivery => delivery.status === options.status); + } return deliveries.map(delivery => { // Récupérer la commande associée à cette livraison si elle existe (DEV RELATIONS) let associatedOrder = undefined; @@ -4217,6 +4276,9 @@ export class MemStorage implements IStorage { async getUserGroups(): Promise { return []; } async assignUserToGroup(): Promise { return {} as UserGroup; } async removeUserFromGroup(): Promise { } + async removeUserFromAllGroups(userId: string): Promise { + this.userGroups.delete(userId); + } async getMonthlyStats(): Promise { return { diff --git a/server/vite.ts b/server/vite.ts index 9338c14..29358c5 100644 --- a/server/vite.ts +++ b/server/vite.ts @@ -76,10 +76,24 @@ export function serveStatic(app: Express) { ); } - app.use(express.static(distPath)); + // Fichiers de /assets hachés par Vite : cache navigateur d'un an + app.use("/assets", express.static(path.resolve(distPath, "assets"), { + maxAge: "1y", + immutable: true, + index: false, + })); + // index.html toujours revalidé pour prendre en compte un nouveau déploiement + app.use(express.static(distPath, { + setHeaders: (res, filePath) => { + if (filePath.endsWith(".html")) { + res.setHeader("Cache-Control", "no-cache"); + } + }, + })); // fall through to index.html if the file doesn't exist app.use("*", (_req, res) => { + res.set("Cache-Control", "no-cache"); res.sendFile(path.resolve(distPath, "index.html")); }); }