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