
----------------------------------------------------
server/server.js

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const dotenv = require('dotenv');
const path = require('path');
const fs = require('fs');

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

// ================================================================
// MIDDLEWARES
// ================================================================
app.set('trust proxy', true);
app.use(helmet({ contentSecurityPolicy: false }));
app.use(cors({ origin: true, credentials: true }));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Service des fichiers statiques (pour Angular)
app.use(express.static(path.join(__dirname, '../public')));

// Logger
app.use((req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
    next();
});

// ================================================================
// CHARGER LES CONFIGS
// ================================================================
let appConfigs = {};
try {
    appConfigs.countries = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/countries.json'), 'utf8'));
    appConfigs.currencies = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/currencies.json'), 'utf8'));
    appConfigs.taresRules = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/tares_codes.json'), 'utf8'));

    // [FLAG] Vérification de la configuration critique
    const openRouterApiKey = process.env.OPENROUTER_API_KEY;
    if (!openRouterApiKey || openRouterApiKey.length < 10) {
        console.warn(`[FLAG-INIT-WARN] OPENROUTER_API_KEY semble manquant ou trop court.`);
    } else {
        console.log(`[FLAG-INIT-OK] OPENROUTER_API_KEY chargée (${openRouterApiKey.substring(0, 10)}...).`);
    }
		
    // On attache les configs à l'objet 'app' pour qu'il soit accessible dans les routes
    app.appConfigs = appConfigs;
    console.log('✓ Fichiers de configuration chargés');
} catch (error) {
    console.error('⚠ Erreur chargement config:', error.message);
}

// ================================================================
// ROUTES DE L'API
// ================================================================
const apiRoutes = require('./routes/api');
app.use('/api', apiRoutes);

// ================================================================
// ROUTE FALLBACK POUR ANGULAR (Single Page Application)
// ================================================================
app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname, '../public/index.html'));
});

// ================================================================
// GESTION D'ERREURS
// ================================================================
app.use((err, req, res, next) => {
    console.error('Erreur:', err.stack);
    res.status(err.status || 500).json({
        error: process.env.NODE_ENV === 'production'
            ? 'Erreur interne du serveur'
            : err.message
    });
});
app.use((req, res) => {
    res.status(404).json({ error: 'Route API non trouvée: ' + req.path });
});

// ================================================================
// DÉMARRAGE
// ================================================================
app.listen(PORT, '127.0.0.1', () => {
    console.log(`╔════════════════════════════════════════════════════════════╗`);
    console.log(`║  Backend e-dec démarré sur http://127.0.0.1:${PORT}        ║`);
    console.log(`╚════════════════════════════════════════════════════════════╝`);
});

process.on('SIGTERM', () => {
    console.log('SIGTERM reçu, arrêt gracieux...');
    process.exit(0);
});
----------------------------------------------------
server/routes/api.js

const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { body, validationResult } = require('express-validator');
const { pool } = require('../db');
const { v4: uuidv4 } = require('uuid');

// Services
const ocrService = require('../services/ocrService');
const taresClassifier = require('../services/taresClassifier');
const edecGenerator = require('../services/edecGenerator');
const exchangeRateService = require('../services/exchangeRateService');
const declarationService = require('../services/declarationService');
const edecSubmissionService = require('../services/edecSubmissionService'); // Import direct pour SSE

const DOWNLOAD_DIR = path.join(__dirname, '../../edecpdf');
const EXPIRATION_DAYS = 30;

// Configuration Multer pour l'upload de fichiers
const upload = multer({
    dest: path.join(__dirname, '../../public/uploads'),
    limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
    fileFilter: (req, file, cb) => {
        const allowed = ['image/jpeg', 'image/png', 'application/pdf'];
        if (allowed.includes(file.mimetype)) {
            cb(null, true);
        } else {
            cb(new Error('Formats supportés: JPG, PNG, PDF'));
        }
    }
});

// =========================================================================
// ROUTES DE SERVICE ET DE DONNÉES DE RÉFÉRENCE
// =========================================================================

// Health check
router.get('/health', (req, res) => {
    res.json({
        status: 'OK',
        timestamp: new Date().toISOString(),
        service: 'e-dec Node.js Backend',
        version: '1.2.0' // Version incrémentée
    });
});

// Données de référence (pays, devises)
router.get('/countries', (req, res) => res.json(req.app.appConfigs.countries || {}));
router.get('/currencies', (req, res) => res.json(req.app.appConfigs.currencies || {}));

// Taux de change
router.get('/exchange-rates', async (req, res) => {
    try {
        const rates = await exchangeRateService.getCurrentRates();
        res.json(rates);
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

// Conversion de devise
router.post('/convert-currency', [
    body('amount').isFloat({ min: 0 }),
    body('from').isString().isLength({ min: 3, max: 3 }),
    body('to').optional().isString().isLength({ min: 3, max: 3 })
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ error: 'Paramètres invalides', details: errors.array() });
    }
    try {
        const { amount, from, to = 'CHF' } = req.body;
        const converted = await exchangeRateService.convert(Number(amount), from, to);
        res.json({ converted });
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

// =========================================================================
// ROUTES MÉTIER (OCR, VIN, CLASSIFICATION)
// =========================================================================

// OCR
router.post('/ocr', upload.single('registration_card'), async (req, res) => {
    const filePath = req.file?.path;
    if (!filePath) {
        return res.status(400).json({ error: 'Aucun fichier fourni' });
    }
    try {
        const extractedData = await ocrService.extractFromFile(filePath, req.file.mimetype);
        res.json(extractedData);
    } catch (error) {
        console.error('Erreur OCR complète:', error);
        res.status(500).json({
            error: 'Erreur OCR',
            details: error.message,
            hint: 'Vérifiez que votre clé API OpenRouter est valide et que le fichier est lisible.'
        });
    } finally {
        if (filePath && fs.existsSync(filePath)) {
            try { fs.unlinkSync(filePath); } catch (_) {}
        }
    }
});

// Validation VIN
router.post('/validate-vin', [
    body('vin').isString().isLength({ min: 17, max: 17 })
], async (req, res) => {
    const { vin } = req.body;
    const valid = /^[A-HJ-NPR-Z0-9]{17}$/.test((vin || '').toUpperCase());
    let country = null;
    try {
        if (valid) {
            country = await taresClassifier.getCountryFromVIN_DB(vin);
        }
        res.json({ valid, country });
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

// Classification TARES
router.post('/classify-vehicle', async (req, res) => {
    try {
        const classification = await taresClassifier.classify(req.body, req.app.appConfigs.taresRules);
        res.json(classification);
    } catch (error) {
        console.error('Erreur classification:', error);
        res.status(500).json({ error: error.message });
    }
});

// Recherche de Brand Code
router.post('/brand-codes/lookup', [
    body('brand').trim().notEmpty().withMessage("Le nom de la marque est requis.")
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ error: 'Champs invalides', details: errors.array() });
    }
    try {
        const { brand } = req.body;
        const brandCode = await declarationService.getBrandCodeByBrandName(brand);
        if (brandCode) {
            return res.json({ brandCode });
        }
        const options = await declarationService.getAllBrandCodes();
        if (options.length === 0) {
            return res.status(404).json({ message: "Liste des codes de marque non disponible." });
        }
        return res.json({ options });
    } catch (error) {
        console.error('Erreur lors de la recherche du code de marque:', error);
        return res.status(500).json({ error: "Erreur interne du serveur lors de la recherche." });
    }
});


// =========================================================================
// ROUTE DE GÉNÉRATION D'IMPORTATION
// =========================================================================
router.post('/generate-import', [
    // ... validateurs (inchangés)
    body('user_name').isString().notEmpty(),
    body('user_zip').isString().notEmpty(),
    body('user_city').isString().notEmpty(),
    body('dispatch_country').isString().isLength({ min: 2, max: 2 }),
    body('transport_mode').isString().isIn(['9', '2', '3', '4', '7', '8']),
    body('purchase_price').isFloat({ min: 0 }),
    body('purchase_currency').isString().isLength({ min: 3, max: 3 }),
    body('vin').isString().isLength({ min: 17, max: 17 }),
    body('brand').isString().notEmpty(),
    body('brand_code').optional().isString().isLength({ min: 3, max: 3 }).isNumeric(),
    body('model').isString().notEmpty(),
    body('weight_empty').isInt({ min: 1 }),
    body('weight_total').isInt({ min: 1 }),
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ error: 'Champs invalides', details: errors.array() });
    }

    const txId = uuidv4();
    const conn = await pool.getConnection();

    try {
        await conn.beginTransaction();
        console.log(`[GENERATE-IMPORT] Début génération pour UUID: ${txId}`);

        const data = { ...req.body };

        // Logique métier (classification, conversion, etc.)
        const classification = await taresClassifier.classify(data, req.app.appConfigs.taresRules);
        data.commodity_code = data.commodity_code || classification.commodity_code;
        data.statistical_key = data.statistical_key || classification.statistical_key || '911';
        data.origin_country = await taresClassifier.getCountryFromVIN_DB(data.vin) || data.dispatch_country || 'FR';
        data.brand_code = data.brand_code || await declarationService.getBrandCodeByBrandName(data.brand);

        const sum = Number(data.purchase_price || 0) + Number(data.transport_cost || 0) + Number(data.other_costs || 0);
        data.statistical_value_chf = await exchangeRateService.convert(Number(data.purchase_price || 0), data.purchase_currency, 'CHF');
        data.vat_value_chf = await exchangeRateService.convert(sum, data.purchase_currency, 'CHF');

        // Persistance via le service
        const declarationId = await declarationService.createFullImportDeclaration(conn, txId, data);
        console.log(`[GENERATE-IMPORT] Déclaration créée ID: ${declarationId}`);

        // Génération et sauvegarde XML
        const xml = await edecGenerator.generateImport(data);
        const xmlFilePath = await declarationService.saveXmlFile(conn, txId, xml, declarationId);
        console.log(`[GENERATE-IMPORT] XML sauvegardé: ${xmlFilePath}`);

        await conn.commit();

        // Lancement de la soumission en arrière-plan
        console.log(`[GENERATE-IMPORT] Lancement soumission automatique pour: ${txId}`);
        setTimeout(() => {
            edecSubmissionService.submitDeclaration(xmlFilePath, txId).catch(err => {
                console.error(`[SUBMISSION-BACKGROUND] Erreur fatale non gérée pour ${txId}:`, err);
            });
        }, 100);

        res.json({
            success: true,
            sessionToken: txId,
            message: 'Déclaration générée et soumission automatique démarrée.'
        });

    } catch (e) {
        await conn.rollback();
        console.error('[GENERATE-IMPORT] Erreur:', e);
        res.status(500).json({ error: e.message });
    } finally {
        conn.release();
    }
});

// =========================================================================
// ROUTE DE GÉNÉRATION D'EXPORTATION
// =========================================================================
router.post('/generate-export', [
    // ... validateurs
    body('user_name').isString().notEmpty(),
    body('user_zip').isString().notEmpty(),
    body('user_city').isString().notEmpty(),
    body('destination_country').isString().isLength({ min: 2, max: 2 }),
    body('vin').isString().isLength({ min: 17, max: 17 }),
    body('brand').isString().notEmpty(),
    body('model').isString().notEmpty(),
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ error: 'Champs invalides', details: errors.array() });
    }

    const txId = uuidv4();
    const conn = await pool.getConnection();

    try {
        await conn.beginTransaction();
        console.log(`[GENERATE-EXPORT] Début génération pour UUID: ${txId}`);

        const data = { ...req.body };
        
        // Persistance via le service
        const declarationId = await declarationService.createFullExportDeclaration(conn, txId, data);
        console.log(`[GENERATE-EXPORT] Déclaration créée ID: ${declarationId}`);

        // Génération et sauvegarde XML
        const xml = await edecGenerator.generateExport(data);
        await declarationService.saveXmlFile(conn, txId, xml, declarationId);

        await conn.commit();

        res.set('Content-Type', 'application/xml');
        res.set('Content-Disposition', `attachment; filename="edec-export-${txId}.xml"`);
        res.send(xml);

    } catch (e) {
        await conn.rollback();
        console.error('Erreur génération export:', e);
        res.status(500).json({ error: e.message });
    } finally {
        conn.release();
    }
});


// =========================================================================
// ROUTES DE SUIVI ET TÉLÉCHARGEMENT
// =========================================================================

// [NOUVEAU] Endpoint SSE pour le suivi en temps réel (Push du serveur)
router.get('/submission-status-sse/:sessionToken', async (req, res) => {
    const { sessionToken } = req.params;
    
    console.log(`[SSE] Nouvelle connexion pour ${sessionToken}`);
    
    // Configuration des headers SSE
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.setHeader('Access-Control-Allow-Origin', '*');
    
    // Fonction de callback pour envoyer des données au client
    const sendStatusUpdate = (data) => {
        try {
            // Assure la fermeture si l'état est final (redondant avec la gestion du service mais sécurisant)
            if (res.finished) return; 
            
            res.write(`data: ${JSON.stringify(data)}\n\n`);
            
            if (data.status === 'submitted' || data.status === 'submission_error') {
                 // Délai pour s'assurer que le dernier message est envoyé avant de fermer la connexion
                 setTimeout(() => {
                     if (!res.finished) res.end();
                 }, 100); 
            }
        } catch (error) {
            console.error(`[SSE] Erreur envoi données pour ${sessionToken}:`, error);
        }
    };
    
    // Enregistrer le listener dans le service
    edecSubmissionService.registerListener(sessionToken, sendStatusUpdate);
    
    // Vérifier l'état initial (TRÈS IMPORTANT pour les recharges de page)
    try {
        const [rows] = await pool.execute(`
            SELECT status, declaration_number, liste_importation_path, bulletin_delivrance_path,
                   COALESCE(error_message, '') as error_message, submission_date
            FROM declarations WHERE uuid = ?`, [sessionToken]);
        
        if (rows.length > 0) {
            const declaration = rows[0];
            const { progress, step } = declarationService.getSubmissionProgress(declaration.status);
            
            // Envoi de l'état actuel au client
            sendStatusUpdate({
                status: declaration.status,
                progress,
                step,
                declarationNumber: declaration.declaration_number,
                listePath: declaration.liste_importation_path,
                bulletinPath: declaration.bulletin_delivrance_path,
                submissionDate: declaration.submission_date,
                error: declaration.error_message
            });
        }
    } catch (error) {
        console.error(`[SSE] Erreur vérification état initial ${sessionToken}:`, error);
    }
    
    // Gérer la fermeture de la connexion (très important pour le nettoyage du service)
    const cleanup = () => {
        if (!res.finished) {
            console.log(`[SSE] Connexion fermée pour ${sessionToken} - Nettoyage`);
            edecSubmissionService.removeListener(sessionToken);
            res.end();
        }
    };
    
    // Événements de fermeture
    req.on('close', cleanup);
    req.on('error', cleanup);
});

// Suivi de la soumission (Legacy - Route de Polling conservée mais désormais redondante)
router.get('/submission-status/:sessionToken', async (req, res) => {
    const { sessionToken } = req.params;
    try {
        const [rows] = await pool.execute(`
            SELECT status, declaration_number, liste_importation_path, bulletin_delivrance_path,
                   COALESCE(error_message, '') as error_message, submission_date
            FROM declarations
            WHERE uuid = ?`, [sessionToken]);

        if (rows.length === 0) {
            return res.status(404).json({ error: 'Session non trouvée' });
        }
        const declaration = rows[0];
        const { progress, step } = declarationService.getSubmissionProgress(declaration.status);

        res.json({
            status: declaration.status,
            progress,
            step,
            declarationNumber: declaration.declaration_number,
            listePath: declaration.liste_importation_path,
            bulletinPath: declaration.bulletin_delivrance_path,
            submissionDate: declaration.submission_date,
            error: declaration.error_message
        });
    } catch (error) {
        console.error('[SUBMISSION-STATUS] Erreur:', error);
        res.status(500).json({ error: 'Erreur serveur' });
    }
});

// Téléchargement sécurisé des PDF
router.get('/download-pdf', async (req, res) => {
    const rawFilePath = req.query.path;
    if (!rawFilePath) {
        return res.status(400).json({ error: 'Paramètre de chemin manquant' });
    }

    const safeFilePath = path.resolve(rawFilePath);
    if (!safeFilePath.startsWith(DOWNLOAD_DIR)) {
        console.warn(`[DOWNLOAD-SECURITY] Tentative d'accès illégal : ${rawFilePath}`);
        return res.status(403).json({ error: 'Accès refusé. Chemin non autorisé.' });
    }

    if (!fs.existsSync(safeFilePath)) {
        return res.status(404).json({ error: 'Fichier non trouvé sur le serveur' });
    }

    const sessionToken = path.basename(safeFilePath).split('_')[0];
    try {
        const [rows] = await pool.execute(
            `SELECT submission_date FROM declarations WHERE uuid = ? AND status = 'submitted'`,
            [sessionToken]
        );
        if (rows.length === 0 || !rows[0].submission_date) {
            return res.status(404).json({ error: 'Déclaration non trouvée ou non finalisée.' });
        }

        const submissionDate = new Date(rows[0].submission_date);
        const expirationDate = new Date(submissionDate.getTime() + EXPIRATION_DAYS * 24 * 60 * 60 * 1000);

        if (new Date() > expirationDate) {
            return res.status(410).json({ error: `Le lien de téléchargement a expiré (limite de ${EXPIRATION_DAYS} jours).` });
        }

        res.setHeader('Content-Type', 'application/pdf');
        res.setHeader('Content-Disposition', `attachment; filename="${path.basename(safeFilePath)}"`);
        fs.createReadStream(safeFilePath).pipe(res);

    } catch (error) {
        console.error('[DOWNLOAD-PDF] Erreur:', error);
        res.status(500).json({ error: 'Erreur serveur.' });
    }
});

// NOUVELLE ROUTE : Signaler une erreur et enregistrer l'email
router.post('/submission-error/report', [
    body('sessionToken').isUUID(),
    body('email').isEmail()
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ error: 'Données invalides' });
    }
    try {
        const { sessionToken, email } = req.body;
        await pool.execute(
            `UPDATE declarations SET user_email = ? WHERE uuid = ? AND status = 'submission_error'`,
            [email, sessionToken]
        );
        console.log(`[ERROR-REPORT] Email ${email} enregistré pour la session ${sessionToken}`);
        res.json({ success: true, message: 'Votre email a été enregistré. Notre équipe vous contactera.' });
    } catch (error) {
        console.error('[ERROR-REPORT] Erreur:', error);
        res.status(500).json({ error: 'Erreur lors de l`enregistrement de l`email.' });
    }
});


module.exports = router;
----------------------------------------------------
server/services/edecGenerator.js

const edecGeneratorImport = require('./edecGeneratorImport');
const edecGeneratorExport = require('./edecGeneratorExport');

class EdecGenerator {
  generateImport(data) {
    return edecGeneratorImport.generateImport(data);
  }
  generateExport(data) {
    return edecGeneratorExport.generateExport(data);
  }
}

module.exports = new EdecGenerator();
----------------------------------------------------
server/services/edecSubmissionService.js

const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const { pool } = require('../db');

class EdecSubmissionService {
    constructor() {
        this.baseUrl = 'https://e-dec-web.ezv.admin.ch/webdec/main.xhtml';
        this.downloadDir = path.join(__dirname, '../../edecpdf');
        this.screenshotDir = path.join(__dirname, '../../screenshots');
        this.statusListeners = new Map();

        [this.downloadDir, this.screenshotDir].forEach(dir => {
            if (!fs.existsSync(dir)) {
                fs.mkdirSync(dir, { recursive: true });
            }
        });
    }

    // ============= GESTION SSE & STATUS =============
    registerListener(sessionToken, listener) {
        this.statusListeners.set(sessionToken, listener);
        console.log(`[SSE] Listener enregistré pour ${sessionToken}`);
    }

    removeListener(sessionToken) {
        this.statusListeners.delete(sessionToken);
        console.log(`[SSE] Listener supprimé pour ${sessionToken}`);
    }

    async updateStatus(sessionToken, status, step = null, progress = null) {
        const conn = await pool.getConnection();
        try {
            console.log(`[STATUS] ${sessionToken} -> ${status} (${progress}%)`);
            
            const errorMessage = status.includes('error') ? String(status) : null;
            await conn.execute(
                `UPDATE declarations SET status = ?, error_message = ? WHERE uuid = ?`,
                [status, errorMessage, sessionToken]
            );

            const listener = this.statusListeners.get(sessionToken);
            if (listener) {
                listener({
                    status,
                    step: step || 'En cours...',
                    progress: progress || 0
                });
            }
        } catch (error) {
            console.error(`[STATUS-ERROR] ${sessionToken}:`, error);
            throw error;
        } finally {
            conn.release();
        }
    }

    // ============= UTILITAIRES =============
    async takeScreenshot(page, name) {
        const screenshotPath = path.join(this.screenshotDir, `${name}_${Date.now()}.png`);
        await page.screenshot({ path: screenshotPath, fullPage: true });
        console.log(`[SCREENSHOT] ${screenshotPath}`);
        return screenshotPath;
    }

    async clickById(page, elementId) {
        console.log(`[CLICK] Tentative de clic sur #${elementId}`);
        const clicked = await page.evaluate((id) => {
            const element = document.getElementById(id);
            if (element) {
                element.click();
                return true;
            }
            return false;
        }, elementId);
        
        if (!clicked) {
            throw new Error(`Élément #${elementId} introuvable`);
        }
        console.log(`[CLICK] ✓ Clic réussi sur #${elementId}`);
    }

    async checkCheckbox(page, checkboxId) {
        console.log(`[CHECKBOX] Tentative de cocher #${checkboxId}`);
        const result = await page.evaluate((id) => {
            const checkbox = document.getElementById(id);
            if (checkbox && checkbox.type === 'checkbox') {
                checkbox.checked = true;
                return checkbox.checked;
            }
            return false;
        }, checkboxId);

        if (!result) {
            throw new Error(`Impossible de cocher la checkbox #${checkboxId}`);
        }
        console.log(`[CHECKBOX] ✓ Checkbox cochée #${checkboxId}`);
    }

    async isPopupVisible(page, popupId) {
        return await page.evaluate((id) => {
            const popup = document.getElementById(id);
            return popup && popup.style.visibility === 'visible';
        }, popupId);
    }

    async waitForUrlChange(page, originalUrl, timeout = 10000) {
        try {
            await page.waitForFunction(
                (expectedUrl) => window.location.href !== expectedUrl,
                originalUrl,
                { timeout }
            );
            console.log(`[URL] ✓ Changement détecté: ${page.url()}`);
            return true;
        } catch {
            console.log(`[URL] ✗ Aucun changement après ${timeout}ms`);
            return false;
        }
    }

    // ============= ÉTAPE 1: UPLOAD XML =============
    async uploadXML(page, xmlFilePath, sessionToken) {
        console.log('[ÉTAPE 1] Début upload XML');
        await this.updateStatus(sessionToken, 'submitting', 'Ouverture formulaire...', 15);

        // Clic sur "Zollanmeldung laden"
        console.log('[EDEC Submit] Clic sur "Zollanmeldung laden"...');
        await page.click('#mainform\\:loadDeclaration');
        await page.waitForTimeout(2000);

        console.log('[EDEC Submit] Attente de la popup de chargement...');
        await page.waitForSelector('#mainform\\:declarationLoadPopup', { 
            state: 'visible',
            timeout: 10000 
        });

        console.log('[EDEC Submit] Recherche de l\'iframe d\'upload...');
        const iframeElement = await page.waitForSelector(
            'iframe#mainform\\:inputFileComponent\\:uploadFrame',
            { state: 'attached', timeout: 10000 }
        );

        const frame = await iframeElement.contentFrame();
        if (!frame) {
            throw new Error('Impossible d\'accéder au contenu de l\'iframe');
        }

        // Upload du XML
        await this.updateStatus(sessionToken, 'submitting', 'Upload du fichier XML...', 30);
        console.log('[EDEC Submit] Upload du fichier XML...');
        const fileInput = await frame.waitForSelector('input[type="file"]', {
            state: 'attached',
            timeout: 10000
        });

        await fileInput.setInputFiles(xmlFilePath);
        
        // Sauvegarde de l'URL avant confirmation
        const urlBeforeConfirm = page.url();
        console.log(`[URL-BEFORE-CONFIRM] ${urlBeforeConfirm}`);

        console.log('[EDEC Submit] Clic sur le bouton OK...');
        await page.click('a#mainform\\:confirmButton');
        
        // Vérification que l'upload a réussi (changement d'URL ou élément visible)
        console.log('[EDEC Submit] Vérification succès upload...');
        
        const uploadSuccess = await Promise.race([
            this.waitForUrlChange(page, urlBeforeConfirm, 10000),
            page.waitForSelector('a#j_id49\\:j_id69', { timeout: 10000 }).then(() => 'button_found'),
            page.waitForSelector('span.iceOutFrmt', { timeout: 10000 }).then(() => 'confirmation_found')
        ]);

        if (!uploadSuccess) {
            await this.takeScreenshot(page, `upload_failed_${sessionToken}`);
            const currentUrl = page.url();
            console.log(`[UPLOAD-ERROR] Aucun changement détecté. URL actuelle: ${currentUrl}`);
            throw new Error('Échec de l\'upload du XML - aucune confirmation détectée');
        }

        console.log('[EDEC Submit] ✓ Upload XML réussi');
        console.log(`[LANDING PAGE 2] URL actuelle: ${page.url()}`);
    }

    // ============= ÉTAPE 2: ENVOI & GESTION ERREURS =============
    async submitAndHandleErrors(page, sessionToken) {
        console.log('[ÉTAPE 2] Début envoi et gestion erreurs');
        await this.updateStatus(sessionToken, 'processing', 'Envoi de la déclaration...', 50);

        let maxAttempts = 5;
        let attempt = 0;

        while (attempt < maxAttempts) {
            attempt++;
            console.log(`[ENVOI] Tentative ${attempt}/${maxAttempts}`);

            // Clic sur le bouton Senden
            const urlBeforeSend = page.url();
            await this.clickById(page, 'j_id49:j_id69');
            
            // Attendre 4 secondes pour laisser le temps à la page de réagir
            await page.waitForTimeout(4000);

            // IMPORTANT: Vérifier d'abord si l'URL a changé (succès direct)
            const currentUrl = page.url();
            if (currentUrl !== urlBeforeSend) {
                console.log('[ENVOI] ✓ URL changée immédiatement après Senden - Succès!');
                console.log(`[ENVOI] Ancienne URL: ${urlBeforeSend}`);
                console.log(`[ENVOI] Nouvelle URL: ${currentUrl}`);
                return true;
            }

            // Si pas de changement d'URL, vérifier si popup d'invraisemblance apparaît
            const inplausibleVisible = await this.isPopupVisible(page, 'mainform:inplausibleDeclarationPopup');
            
            if (inplausibleVisible) {
                console.log('[ENVOI] Popup d\'invraisemblance détectée, fermeture...');
                await this.clickById(page, 'mainform:cancelButton');
                await page.waitForTimeout(2000);

                // Vérifier les erreurs de validation
                await this.updateStatus(sessionToken, 'processing', 'Correction des erreurs...', 60 + (attempt * 5));
                const errorResult = await this.checkAndFixValidationErrors(page, sessionToken);
                
                if (!errorResult.fixed) {
                    throw new Error('Erreurs de validation non résolues');
                }

                // Si l'URL a changé pendant la correction = SUCCÈS!
                if (errorResult.urlChanged) {
                    console.log('[ÉTAPE 2] ✓ URL changée pendant la correction, passage à l\'étape 3');
                    return true;
                }

                // Réessayer l'envoi (continue = retour au début de la boucle while)
                console.log('[ENVOI] Erreurs corrigées, retour au début de la boucle...');
                continue;
            }

            // Si on arrive ici sans popup et sans changement d'URL, réessayer
            console.log('[ENVOI] Pas de popup ni de changement d\'URL, nouvelle tentative...');
        }

        throw new Error(`Échec après ${maxAttempts} tentatives d'envoi`);
    }

    // ============= GESTION DES ERREURS DE VALIDATION =============
    async checkAndFixValidationErrors(page, sessionToken) {
		console.log('[ERREURS] Vérification erreurs de validation...');
		
		const errorIconSelector = '#mainform\\:tree-d-1-0-c img[src*="exclamation.gif"][title*="Maske nicht korrekt ausgefüllt"]';
		await page.waitForTimeout(3000);

		const errorIcons = await page.$$(errorIconSelector);
		
		if (!errorIcons || errorIcons.length === 0) {
			console.log('[ERREURS] ✓ Aucune erreur détectée');
			return { fixed: true, urlChanged: false };
		}

		console.log(`[ERREURS] ${errorIcons.length} erreur(s) détectée(s)`);
		await this.takeScreenshot(page, `errors_detected_${sessionToken}`);

		// FIX ICI: Utiliser une boucle au lieu de .map()
		const errorIds = [];
		for (const icon of errorIcons) {
			const id = await icon.getAttribute('id');
			if (id) errorIds.push(id);
		}

        // Traiter chaque erreur
        for (let i = 0; i < errorIds.length; i++) {
            const errorId = errorIds[i];
            console.log(`[ERREURS] Traitement erreur ${i + 1}/${errorIds.length}: ${errorId}`);
            
            const result = await this.fixErrorById(page, errorId, sessionToken, i);
            
            // Si l'URL a changé pendant la correction, c'est un succès direct!
            if (result === 'SUCCESS_URL_CHANGED') {
                console.log('[ERREURS] ✓ Correction réussie avec changement d\'URL!');
                return { fixed: true, urlChanged: true };
            }
            
            if (!result) {
                console.log(`[ERREURS] ✗ Impossible de corriger ${errorId}`);
                return { fixed: false, urlChanged: false };
            }

            console.log(`[ERREURS] ✓ Correction de ${errorId} terminée`);
            await page.waitForTimeout(1000);
        }

        console.log('[ERREURS] ✓ Toutes les erreurs traitées');
        return { fixed: true, urlChanged: false };
    }

    async fixErrorById(page, errorId, sessionToken, errorIndex) {
        // Switch case basé sur l'ID de l'erreur
        switch (errorId) {
            case 'mainform:tree:n-1-0-1:j_id121':
                return await this.fixR125bError(page, sessionToken, errorIndex);
            
            // Ajouter d'autres cas ici pour d'autres types d'erreurs
            
            default:
                console.log(`[ERREURS] Type d'erreur non géré: ${errorId}`);
                return false;
        }
    }

    async fixR125bError(page, sessionToken, errorIndex) {
        try {
            console.log('[R125b] Début correction erreur R125b');
            
            // Cliquer sur le lien de l'erreur
            await this.clickById(page, 'mainform:tree:n-1-0-1:link');
            await page.waitForTimeout(2000);

            // Ouvrir le popup de détail
            await this.clickById(page, 'mainform:j_id224');
            await page.waitForTimeout(2000);

            // Cocher la checkbox de correction
            await this.checkCheckbox(page, 'mainform:j_id367');
            await page.waitForTimeout(1000);

            // Vérifier que la checkbox est bien cochée
            const isChecked = await page.evaluate(() => {
                return document.getElementById('mainform:j_id367')?.checked || false;
            });

            if (!isChecked) {
                await this.takeScreenshot(page, `r125b_checkbox_fail_${sessionToken}_${errorIndex}`);
                throw new Error('Checkbox R125b non cochée');
            }

            console.log('[R125b] ✓ Checkbox cochée');

            // Fermer le popup
            await this.clickById(page, 'mainform:plausiPopupCloseButton');
            await page.waitForTimeout(2000);

            await this.takeScreenshot(page, `r125b_fixed_${sessionToken}_${errorIndex}`);
            
            // NOUVEAU: Clic sur Senden après la correction
            const urlBeforeClick = page.url();
            console.log('[R125b] Clic sur Senden après correction...');
            await this.clickById(page, 'j_id49:j_id69');
            await page.waitForTimeout(3000);

            // Vérifier si l'URL a changé (= succès, passage à l'étape 3)
            const currentUrl = page.url();
            if (currentUrl !== urlBeforeClick) {
                console.log('[R125b] ✓ URL changée après correction! Passage direct à l\'étape 3');
                console.log(`[R125b] Ancienne URL: ${urlBeforeClick}`);
                console.log(`[R125b] Nouvelle URL: ${currentUrl}`);
                return 'SUCCESS_URL_CHANGED'; // Code spécial pour indiquer le succès
            }

            console.log('[R125b] ✓ Erreur R125b corrigée, pas de changement d\'URL');
            return true;

        } catch (error) {
            console.error(`[R125b] ✗ Échec correction: ${error.message}`);
            await this.takeScreenshot(page, `r125b_error_${sessionToken}_${errorIndex}`);
            return false;
        }
    }

    // ============= ÉTAPE 3: RÉCUPÉRATION DES PDF =============
    async downloadPDFs(page, sessionToken) {
        console.log('[ÉTAPE 3] Récupération des PDF');
        await this.updateStatus(sessionToken, 'processing', 'Téléchargement des PDF...', 90);

        // Extraction du numéro de déclaration
        let declarationNumber = null;
        try {
            const confirmationSpan = await page.waitForSelector('span.iceOutFrmt', { timeout: 10000 });
            const spanText = await confirmationSpan.textContent();
            console.log(`[PDF] Texte confirmation: ${spanText}`);

            const currentYear = new Date().getFullYear().toString().slice(-2);
            const match = spanText.match(new RegExp(`(${currentYear}CHWI[^\\s]+)`)) ||
                             spanText.match(/(\d{2}CHWI[^\\s]+)/);
            
            if (match) {
                declarationNumber = match[1];
                console.log(`[PDF] Numéro de déclaration: ${declarationNumber}`);
            }
        } catch (error) {
            console.warn(`[PDF] Impossible d'extraire le numéro: ${error.message}`);
        }

        // Téléchargement des PDF
        await page.waitForSelector('a[href*=".pdf"]', { timeout: 15000 });
        const pdfLinks = await page.$$('a.downloadLink[href*=".pdf"]');

        if (pdfLinks.length < 2) {
            await this.takeScreenshot(page, `insufficient_pdfs_${sessionToken}`);
            throw new Error(`Seulement ${pdfLinks.length} PDF trouvé(s), 2 attendus`);
        }

        const listePath = path.join(this.downloadDir, `${sessionToken}_liste_importation.pdf`);
        const bulletinPath = path.join(this.downloadDir, `${sessionToken}_bulletin_delivrance.pdf`);

        // PDF 1
        console.log('[PDF] Téléchargement PDF 1...');
        const downloadPromise1 = page.waitForEvent('download', { timeout: 30000 });
        await pdfLinks[0].click();
        const download1 = await downloadPromise1;
        await download1.saveAs(listePath);
        console.log(`[PDF] ✓ PDF 1: ${listePath}`);

        // PDF 2
        console.log('[PDF] Téléchargement PDF 2...');
        await page.waitForTimeout(1000);
        const downloadPromise2 = page.waitForEvent('download', { timeout: 30000 });
        await pdfLinks[1].click();
        const download2 = await downloadPromise2;
        await download2.saveAs(bulletinPath);
        console.log(`[PDF] ✓ PDF 2: ${bulletinPath}`);

        // Sauvegarde en base de données
        await this.saveToDatabase(sessionToken, declarationNumber || 'UNKNOWN', listePath, bulletinPath);

        console.log('[ÉTAPE 3] ✓ PDF récupérés avec succès');

        return {
            success: true,
            declarationNumber: declarationNumber || 'UNKNOWN',
            listePath,
            bulletinPath
        };
    }

    async saveToDatabase(sessionToken, declarationNumber, listePath, bulletinPath) {
        const conn = await pool.getConnection();
        try {
            await conn.execute(`
                UPDATE declarations 
                SET 
                    declaration_number = ?,
                    liste_importation_path = ?,
                    bulletin_delivrance_path = ?,
                    status = 'submitted',
                    submission_date = NOW(),
                    error_message = NULL
                WHERE uuid = ?
            `, [declarationNumber, listePath, bulletinPath, sessionToken]);

            const listener = this.statusListeners.get(sessionToken);
            if (listener) {
                listener({
                    status: 'submitted',
                    step: 'Déclaration soumise avec succès',
                    progress: 100,
                    declarationNumber,
                    listePath,
                    bulletinPath
                });
            }
        } finally {
            conn.release();
        }
    }

    // ============= MÉTHODE PRINCIPALE =============
    async submitDeclaration(xmlFilePath, sessionToken) {
        let browser = null;
        let page = null;

        try {
            console.log(`[DÉBUT] Soumission pour ${sessionToken}`);
            await this.updateStatus(sessionToken, 'submitting', 'Initialisation...', 5);

            // Lancement du navigateur
            browser = await chromium.launch({
                headless: true,
                args: [
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                    '--disable-dev-shm-usage',
                    '--disable-gpu'
                ]
            });

            const context = await browser.newContext({
                acceptDownloads: true,
                userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
                viewport: { width: 1280, height: 720 }
            });

            page = await context.newPage();
            page.on('console', msg => console.log('[BROWSER]', msg.text()));
            page.on('pageerror', error => console.log('[BROWSER-ERROR]', error));

            // Navigation initiale
            console.log('[NAVIGATION] Accès au portail...');
            await page.goto(this.baseUrl, { waitUntil: 'networkidle', timeout: 60000 });
            await page.waitForTimeout(2000);

            // ÉTAPE 1: Upload XML
            await this.uploadXML(page, xmlFilePath, sessionToken);

            // ÉTAPE 2: Envoi avec gestion des erreurs
            await this.submitAndHandleErrors(page, sessionToken);

            // ÉTAPE 3: Récupération des PDF
            const result = await this.downloadPDFs(page, sessionToken);

            await this.takeScreenshot(page, `success_${sessionToken}`);
            console.log('[FIN] ✓ Soumission terminée avec succès');

            return result;

        } catch (error) {
            console.error('[ERREUR FATALE]', error);

            if (page) {
                await this.takeScreenshot(page, `fatal_error_${sessionToken}`);
            }

            await this.updateStatus(
                sessionToken,
                'submission_error',
                `Échec: ${error.message}`,
                100
            );

            throw error;

        } finally {
            if (browser) {
                await browser.close();
            }
        }
    }
}

module.exports = new EdecSubmissionService();
----------------------------------------------------
server/services/edecGeneratorImport.js

function escapeXml(value) {
  if (value === null || value === undefined) return '';
  return String(value)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;')
    .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

// invoiceCurrencyType mapping:
// 1=CHF, 2=EUR, 3=USD, 4=Autre monnaie UE, 5=Autre monnaie
function mapInvoiceCurrencyType(currency) {
  if (currency === 'CHF') return 1;
  if (currency === 'EUR') return 2;
  if (currency === 'USD') return 3;

  // Liste des pays de l'UE qui n'utilisent pas l'Euro (BGN, CZK, DKK, HUF, PLN, RON, SEK)
  const euNonEuroCurrencies = ['BGN', 'CZK', 'DKK', 'HUF', 'PLN', 'RON', 'SEK'];
  if (euNonEuroCurrencies.includes(currency)) {
    return 4; // Autre monnaie UE
  }

  return 5; // Autre monnaie
}

class EdecGeneratorImport {
  generateImport(data) {
    // CORRECTION: Remplace 'T' par un espace et 'Z' par ' UTC' pour le format e-dec (YYYY-MM-DD HH:mm:ss.sss UTC)
    const now = new Date().toISOString().replace('T', ' ').replace('Z', ' UTC');
    
    const lang = (data.language || 'fr').toLowerCase();
    const dispatchCountry = data.dispatch_country || 'FR';
    const transportMode = data.transport_mode || '9'; 
    
    // Le nom (raison sociale ou Prénom Nom) est désormais formaté par le controller client
    const importerName = escapeXml(data.user_name || '');
    const importerStreet = escapeXml(data.user_address || '');
    const importerZip = escapeXml(data.user_zip || '');
    const importerCity = escapeXml(data.user_city || '');
    const importerCountry = 'CH';
    const ide = escapeXml(data.user_ide || 'CHE222251936'); // Particulier par défaut

    const consigneeName = escapeXml(data.consignee_name || data.user_name || '');
    const consigneeStreet = escapeXml(data.consignee_address || data.user_address || '');
    const consigneeZip = escapeXml(data.consignee_zip || data.user_zip || '');
    const consigneeCity = escapeXml(data.consignee_city || data.user_city || '');
    const consigneeIde = escapeXml(data.consignee_ide || ide);

    const declarantName = escapeXml(data.declarant_name || data.user_name || '');
    const declarantStreet = escapeXml(data.declarant_address || data.user_address || '');
    const declarantZip = escapeXml(data.declarant_zip || data.user_zip || '');
    const declarantCity = escapeXml(data.declarant_city || data.user_city || '');
    const declarantIde = escapeXml(data.declarant_ide || '');

    const description = escapeXml(`${data.brand || ''} ${data.model || ''} ${data.year || ''}`.trim());
    const commodityCode = escapeXml(data.commodity_code || '8703.9060'); 
    const statisticalCode = escapeXml(data.statistical_key || '911'); 
    
    // Les masses doivent être des nombres entiers (arrondi par précaution)
    const grossMass = Math.round(Number(data.weight_total || 0));
    const netMass = Math.round(Number(data.weight_empty || 0));

    // Correction: La valeur statistique et la base TVA sont arrondies à l'ENTIER.
    // statisticalValue (Prix d'achat hors coûts annexes)
    const statisticalValue = Math.round(Number(data.statistical_value_chf || data.statistical_value || 0)); 
    // vatValue (Prix d'achat + coûts annexes)
    const vatValue = Math.round(Number(data.vat_value_chf || data.vatValue || statisticalValue));

    const vin = escapeXml(data.vin || '');
    const brandCode = escapeXml(data.brand_code || '');

    const originCountry = escapeXml(data.origin_country || data.originCountry || dispatchCountry || 'FR');
    const preference = 0; // pas de tarif préférentiel par défaut

    // TVA code: 1 normal, 3 déménagement
    const vatCode = data.is_relocation ? '3' : '1';

    // Additional tax (IVA) RG 660: key 001 (assujetti) ou 002 (exonéré)
    const additionalTaxKey = data.is_iva_exempt ? '002' : '001';
    // Assure que la quantité pour la taxe additionnelle est un entier
    const additionalTaxQty = Math.round(Number(data.additional_tax_value || data.purchase_price || statisticalValue || 0));

    // Utilisation de data.purchase_currency pour déduire le type de monnaie
    const invoiceCurrencyType = mapInvoiceCurrencyType(data.purchase_currency);
    
    // Construction des détails de l'article de marchandise (VIN et Brand Code)
    let goodsItemDetailsXml = '';
    if (vin || brandCode) {
        goodsItemDetailsXml = `
        <goodsItemDetails>
          <goodsItemDetail>
            <GoodsItemDetail>
              <name>2</name>
              <value>${vin}</value>
            </GoodsItemDetail>
            ${brandCode ? `
            <GoodsItemDetail>
              <name>1</name>
              <value>${brandCode}</value>
            </GoodsItemDetail>` : ''}
          </goodsItemDetail>
        </goodsItemDetails>`;
    }

    // Ajout de l'entête XML et utilisation de la variable now corrigée
    const xml = `<?xml version="1.0" encoding="UTF-8"?>
<EdecWeb version="4.0" createdDate="${now}">
  <goodsDeclarationType>
    <serviceType>1</serviceType>
    <declarationType>1</declarationType>
    <language>${lang}</language>
    <dispatchCountry>${dispatchCountry}</dispatchCountry>
    <transportMeans>
      <transportMode>${transportMode}</transportMode>
      <transportationNumber></transportationNumber>
    </transportMeans>
    <transportInContainer>0</transportInContainer>
    <previousDocument/>
    <importer>
      <name>${importerName}</name>
      ${importerStreet ? `<street>${importerStreet}</street>` : ''}
      <postalCode>${importerZip}</postalCode>
      <city>${importerCity}</city>
      <country>${importerCountry}</country>
      <traderIdentificationNumber>${ide}</traderIdentificationNumber>
    </importer>
    <consignee>
      <name>${consigneeName}</name>
      ${consigneeStreet ? `<street>${consigneeStreet}</street>` : ''}
      <postalCode>${consigneeZip}</postalCode>
      <city>${consigneeCity}</city>
      <country>${importerCountry}</country>
      <traderIdentificationNumber>${consigneeIde}</traderIdentificationNumber>
    </consignee>
    <declarant>
      ${declarantIde ? `<traderIdentificationNumber>${declarantIde}</traderIdentificationNumber>` : '<traderIdentificationNumber></traderIdentificationNumber>'}
      <name>${declarantName}</name>
      ${declarantStreet ? `<street>${declarantStreet}</street>` : ''}
      <postalCode>${declarantZip}</postalCode>
      <city>${declarantCity}</city>
      <country>${importerCountry}</country>
    </declarant>
    <business>
      <customsAccount>0</customsAccount>
      <vatAccount>0</vatAccount>
      <vatSuffix>0</vatSuffix>
      <invoiceCurrencyType>${invoiceCurrencyType}</invoiceCurrencyType>
    </business>
    <goodsItem>
      <GoodsItemType>
        <traderItemID>0</traderItemID>
        <description>${description}</description>
        <commodityCode>${commodityCode}</commodityCode>
        <statisticalCode>${statisticalCode}</statisticalCode>
        <grossMass>${grossMass}</grossMass>
        <netMass>${netMass}</netMass>
        <additionalUnit>1</additionalUnit>
        <permitObligation>0</permitObligation>
        <nonCustomsLawObligation>2</nonCustomsLawObligation>
        <statistic>
          <customsClearanceType>1</customsClearanceType>
          <commercialGood>1</commercialGood>
          <statisticalValue>${statisticalValue}</statisticalValue>
          <repair>0</repair>
        </statistic>
        <origin>
          <originCountry>${originCountry}</originCountry>
          <preference>${preference}</preference>
        </origin>
        ${goodsItemDetailsXml}
        <packaging>
          <PackagingType>
            <packagingType>VN</packagingType>
            <quantity>1</quantity>
            <packagingReferenceNumber>${description}</packagingReferenceNumber>
          </PackagingType>
        </packaging>
        <valuation>
          <netDuty>0</netDuty>
          <vatValue>${vatValue}</vatValue>
          <vatCode>${vatCode}</vatCode>
        </valuation>
        <additionalTax>
          <AdditionalTaxType>
            <type>660</type>
            <key>${additionalTaxKey}</key>
            <quantity>${additionalTaxQty}</quantity>
          </AdditionalTaxType>
        </additionalTax>
      </GoodsItemType>
    </goodsItem>
  </goodsDeclarationType>
</EdecWeb>`;

    return xml;
  }
}

module.exports = new EdecGeneratorImport();
----------------------------------------------------
server/services/declarationService.js

const { pool } = require('../db');
const path = require('path');
const fs = require('fs');

class DeclarationService {

    /**
     * Crée l'enregistrement principal dans la table 'declarations'.
     */
    async createDeclarationRecord(conn, uuid, type, data) {
        const [result] = await conn.execute(
            `INSERT INTO declarations (uuid, declaration_type, user_name, user_firstname, user_address, user_zip, user_city, user_country, user_ide, language, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'CH', ?, ?, 'draft')`,
            [
                uuid,
                type.toUpperCase(),
                data.user_name || '',
                data.user_firstname || null,
                data.user_address || null,
                data.user_zip || '',
                data.user_city || '',
                data.user_ide || null,
                (data.language || 'fr').toLowerCase()
            ]
        );
        return result.insertId;
    }

    /**
     * Crée l'enregistrement des détails d'importation.
     */
    async createImportDetailsRecord(conn, declarationId, data) {
        return conn.execute(
            `INSERT INTO import_details (declaration_id, dispatch_country, transport_mode, is_relocation, purchase_price, purchase_currency, transport_cost, other_costs, statistical_value, vat_value) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
            [
                declarationId,
                data.dispatch_country,
                data.transport_mode,
                data.is_relocation ? 1 : 0,
                Number(data.purchase_price || 0),
                data.purchase_currency,
                Number(data.transport_cost || 0),
                Number(data.other_costs || 0),
                data.statistical_value_chf,
                data.vat_value_chf
            ]
        );
    }

    /**
     * Crée l'enregistrement des détails d'exportation.
     */
    async createExportDetailsRecord(conn, declarationId, data) {
        return conn.execute(
            `INSERT INTO export_details (declaration_id, destination_country, taxation_code) VALUES (?, ?, ?)`,
            [
                declarationId,
                data.destination_country,
                data.taxation_code || null
            ]
        );
    }

    /**
     * Crée l'enregistrement du véhicule.
     */
    async createVehicleRecord(conn, declarationId, data) {
        return conn.execute(
            `INSERT INTO vehicles (declaration_id, vin, brand, model, year, cylinder_capacity, fuel_type, weight_empty, weight_total, tares_code, statistical_key, country_origin, brand_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
            [
                declarationId,
                (data.vin || '').toUpperCase(),
                data.brand,
                data.model,
                data.year || null,
                data.cylinder_capacity || null,
                data.fuel_type || null,
                data.weight_empty || null,
                data.weight_total || null,
                data.commodity_code || (data.declaration_type === 'EXPORT' ? '8703.9060' : null),
                data.statistical_key || '911',
                data.origin_country || null,
                data.brand_code || null
            ]
        );
    }

    /**
     * Orchestre la création d'une déclaration d'importation complète.
     */
    async createFullImportDeclaration(conn, txId, data) {
        const declarationId = await this.createDeclarationRecord(conn, txId, 'IMPORT', data);
        await this.createImportDetailsRecord(conn, declarationId, data);
        await this.createVehicleRecord(conn, declarationId, { ...data, declaration_type: 'IMPORT' });
        return declarationId;
    }

    /**
     * Orchestre la création d'une déclaration d'exportation complète.
     */
    async createFullExportDeclaration(conn, txId, data) {
        const declarationId = await this.createDeclarationRecord(conn, txId, 'EXPORT', data);
        await this.createExportDetailsRecord(conn, declarationId, data);
        await this.createVehicleRecord(conn, declarationId, { ...data, declaration_type: 'EXPORT' });
        return declarationId;
    }

    /**
     * Sauvegarde le fichier XML et met à jour la base de données.
     */
    async saveXmlFile(conn, txId, xml, declarationId) {
        const xmlDir = path.join(__dirname, '../../xml');
        if (!fs.existsSync(xmlDir)) {
            fs.mkdirSync(xmlDir, { recursive: true });
        }
        const xmlFilePath = path.join(xmlDir, `${txId}.xml`);
        fs.writeFileSync(xmlFilePath, xml, 'utf8');

        await conn.execute(
            `UPDATE declarations SET xml_content = ?, xml_file_path = ?, status = 'generated' WHERE id = ?`,
            [xml, xmlFilePath, declarationId]
        );
        return xmlFilePath;
    }
    
    /**
     * Traduit le statut de la base de données en progression et étape pour le client.
     */
    getSubmissionProgress(status) {
        let progress = 0;
        let step = '';
        switch (status) {
            case 'generated':
                progress = 25;
                step = 'Déclaration générée, en attente de soumission';
                break;
            case 'submitting':
                progress = 50;
                step = 'Connexion au portail e-dec';
                break;
            case 'processing':
                progress = 75;
                step = 'Traitement par la douane';
                break;
            case 'submitted':
                progress = 100;
                step = 'Soumission terminée avec succès';
                break;
            case 'submission_error':
                progress = 100; // L'erreur est une finalité
                step = 'Erreur de soumission';
                break;
            default:
                progress = 10;
                step = 'Initialisation';
        }
        return { progress, step };
    }
    
    /**
     * Recherche le brand_code par le nom de la marque.
     */
    async getBrandCodeByBrandName(brandName) {
        if (!brandName) return null;
        const cleanedBrandName = brandName.trim().toUpperCase();
        const query = `SELECT brand_code FROM brand_codes WHERE brand_name = ?`;
        const [rows] = await pool.execute(query, [cleanedBrandName]);
        return rows.length > 0 ? rows[0].brand_code : null;
    }

    /**
     * Récupère la liste complète des brand_codes.
     */
    async getAllBrandCodes() {
        const query = `SELECT brand_code, brand_name FROM brand_codes ORDER BY brand_name`;
        const [rows] = await pool.execute(query);
        return rows;
    }
}

module.exports = new DeclarationService();
----------------------------------------------------
server/services/ocrService.js

const axios = require('axios');
const fs = require('fs');
const sharp = require('sharp');
const path = require('path');

/**
 * Service pour l'OCR (Reconnaissance Optique de Caractères)
 * utilisant l'API OpenRouter (Gemini) pour extraire les données
 * de documents (images ou PDF) de type carte grise/certificat d'immatriculation.
 */
class OcrService {
    constructor() {
        // Initialisation des propriétés de configuration
        this.apiKey = process.env.OPENROUTER_API_KEY;
        this.model = process.env.OPENROUTER_MODEL || 'google/gemini-2.5-flash';
        
        console.log('[OCR Service] Initialisé avec:', {
            hasApiKey: !!this.apiKey,
            apiKeyPreview: this.apiKey ? this.apiKey.substring(0, 10) + '...' : 'MANQUANT',
            model: this.model
        });
    }

    /**
     * Construit le prompt détaillé pour l'extraction de données de carte grise.
     * @returns {string} Le prompt à envoyer au modèle.
     */
    buildExtractionPrompt() {
        return `Tu es un expert en extraction de données de certificats d'immatriculation (cartes grises) européens (Suisse, France, Allemagne, Italie, etc.). 
		Analyse l'image ou le document fourni et retourne UNIQUEMENT un objet JSON strict correspondant au format: 
			{"vin":"VF3LCYHZPFS123456","brand":"Peugeot","model":"308","year":2015,"cylinder_capacity":1560,"fuel_type":"diesel","weight_empty":1245,"weight_total":1870,"type_approval":"e11*2007/46*0009*15"} 
		RÈGLES ET TERMES DE RECHERCHE PAR CHAMP: 
			- vin: 17 caractères alphanumériques. Cherche les champs E, 23, FIN, Chassis N., Fahrgestell-Nr, Telaio n., ou VIN. 
				Supprime les espaces internes pour la valeur finale. 
				IMPORTANT: Les lettres I, O et Q ne sont JAMAIS utilisées dans un VIN (norme ISO 3779).
				Si tu rencontres un I, considère-le comme un 1. Si tu rencontres un O ou un Q, considère-les comme des 0. 
			- brand: D.1, Marke.
			- model: D.3, Modèle, Typ. 
			- year: L'année de la première mise en circulation.
			- Cherche les champs B, 1. Inverkehrsetzung, 1ère mise en circulation, Data di prima immatricolazione. Isole UNIQUEMENT l'année (YYYY). 
			- cylinder_capacity: P.1, Hubraum, Cilindrata, Cylindrée. (Int en cm³) 
			- fuel_type: P.3, type de carburant, Carburant, Kraftstoff, Alimentazione. (essence|diesel|electrique|hybride|hybride_plugin) 
			- weight_empty: G, Leergewicht, Poids à vide, massa a vuoto. (Int en kg) 
			- weight_total: F.2, Gesamtgewicht, Poids total autorisé en charge, Massa massima ammissibile a pieno carico. (Int en kg) 
			- type_approval: K, Réception par type, Typengenehmigung, Approvazione del tipo. 
			Retourne strictement du JSON valide sans commentaire ni texte. Si une valeur est totalement inconnue, utilise: null.`.trim();
    }

    /**
     * Valide et nettoie un numéro VIN.
     * @param {string} vin - Le VIN à valider.
     * @returns {string|null} Le VIN nettoyé ou null si invalide.
     */
    validateVIN(vin) {
        if (!vin) return null;

        // Supprimer tous les espaces et normaliser en majuscules
        let clean = vin.replace(/\s/g, '').toUpperCase().trim();

        // Correction automatique des lettres interdites (ISO 3779: I, O, Q)
        const corrected = clean
            .replace(/I/g, '1')
            .replace(/[OQ]/g, '0');

        if (clean !== corrected) {
            console.warn('[OCR] VIN corrigé automatiquement:', {
                original: vin,
                corrected
            });
        }

        // Vérifie que c'est bien un VIN valide (17 caractères, sans I, O, Q)
        return /^[A-HJ-NPR-Z0-9]{17}$/.test(corrected) ? corrected : null;
    }

    /**
     * Valide l'année.
     * @param {number|string} year - L'année à valider.
     * @returns {number|null} L'année ou null.
     */
    validateYear(year) {
        if (!year) return null;
        const y = parseInt(year, 10);
        return (y >= 1900 && y <= new Date().getFullYear() + 1) ? y : null;
    }

    /**
     * Valide un entier positif.
     * @param {number|string} value - La valeur à valider.
     * @returns {number|null} L'entier ou null.
     */
    validateInteger(value) {
        if (value === null || value === undefined || value === '') return null;
        const int = parseInt(value, 10);
        // Vérifie que c'est un nombre fini et strictement positif
        return Number.isFinite(int) && int > 0 ? int : null;
    }

    /**
     * Normalise le type de carburant.
     * @param {string} fuelType - Le type de carburant brut.
     * @returns {string|null} Le type normalisé ou null.
     */
    normalizeFuelType(fuelType) {
        if (!fuelType) return null;
        const normalized = fuelType.toLowerCase().trim();
        if (normalized.includes('plug')) return 'hybride_plugin';
        if (normalized.includes('hybr')) return 'hybride';
        if (normalized.includes('elec') || normalized.includes('élec')) return 'electrique';
        if (normalized.includes('dies')) return 'diesel';
        if (normalized.includes('ess') || normalized.includes('petrol') || normalized.includes('gasoline')) return 'essence';
        return normalized;
    }

    /**
     * Nettoie et parse la réponse JSON du modèle.
     * @param {string} response - La réponse brute du modèle.
     * @returns {object} L'objet de données parsé et validé.
     */
    parseGeminiJSON(response) {
        console.log('[OCR] ÉTAPE 7a - Parsing JSON, réponse brute:', response);
        // Supprime les balises de code Markdown JSON
        const clean = response.replace(/```json|```/g, '').trim();
        console.log('[OCR] ÉTAPE 7b - Après nettoyage:', clean);

        try {
            const data = JSON.parse(clean);
            console.log('[OCR] ÉTAPE 7c - JSON parsé:', data);

            // Validation et normalisation des champs extraits
            return {
                vin: this.validateVIN(data.vin),
                brand: data.brand || null,
                model: data.model || null,
                year: this.validateYear(data.year),
                cylinder_capacity: this.validateInteger(data.cylinder_capacity),
                fuel_type: this.normalizeFuelType(data.fuel_type),
                weight_empty: this.validateInteger(data.weight_empty),
                weight_total: this.validateInteger(data.weight_total),
                type_approval: data.type_approval || null
            };
        } catch (err) {
            console.error('[OCR] ERREUR ÉTAPE 7 - Parsing JSON:', err);
            throw new Error('Format de réponse OCR invalide: ' + err.message);
        }
    }

    /**
     * Extrait les données d'un fichier (image ou PDF) via l'API OpenRouter.
     * @param {string} filePath - Chemin d'accès au fichier.
     * @param {string} mimeType - Type MIME du fichier.
     * @returns {Promise<object>} Les données extraites.
     */
    async extractFromFile(filePath, mimeType) {
        console.log('[OCR] ÉTAPE 1 - Début extraction:', { filePath, mimeType });

        if (!this.apiKey) {
            throw new Error('OPENROUTER_API_KEY manquante dans .env');
        }

        // Validation simple du type MIME
        const safeMime = ['image/jpeg', 'image/png', 'application/pdf'].includes(mimeType) ? mimeType : 'image/jpeg';
        console.log('[OCR] ÉTAPE 2 - Type MIME validé:', safeMime);

        let base64Data;
        let contentType;
        let dataUrlPreview;

        try {
            if (safeMime === 'application/pdf') {
                console.log('[OCR] ÉTAPE 3a - Traitement PDF');
                const pdfBuffer = fs.readFileSync(filePath);
                console.log('[OCR] PDF lu, taille:', pdfBuffer.length, 'bytes');

                // Conversion PDF en Base64
                base64Data = Buffer.from(pdfBuffer).toString('base64');
                contentType = 'application/pdf';
                console.log('[OCR] PDF converti en base64, longueur:', base64Data.length);
            } else {
                // Traitement d'image (optimisation avec sharp)
                console.log('[OCR] ÉTAPE 3b - Traitement Image');
                const optimized = await sharp(filePath)
                    .rotate()
                    .resize({ width: 1800, withoutEnlargement: true })
                    .jpeg({ quality: 80 })
                    .toBuffer();
                console.log('[OCR] Image optimisée, taille:', optimized.length, 'bytes');
                base64Data = optimized.toString('base64');
                contentType = 'image/jpeg';
                console.log('[OCR] Image convertie en base64, longueur:', base64Data.length);
            }

            dataUrlPreview = `data:${contentType};base64,...${base64Data.substring(base64Data.length - 10)}`;
            console.log('[OCR] ÉTAPE 3c - Data URL construite, aperçu:', dataUrlPreview);

            const dataSizeMB = base64Data.length * 0.75 / (1024 * 1024);
            console.log(`[OCR] ÉTAPE 3d - Taille du contenu à envoyer: ${dataSizeMB.toFixed(2)} MB`);
            if (dataSizeMB > 15) {
                console.warn('[OCR] AVERTISSEMENT: La taille du contenu est supérieure à 15MB, risque d\'échec API.');
            }

        } catch (error) {
            console.error('[OCR] ERREUR ÉTAPE 3 - Lecture/conversion fichier:', error);
            throw new Error('Impossible de lire le fichier: ' + error.message);
        }

        // Début de la construction de la requête API (Étapes 4 et 5 fusionnées)
        const prompt = this.buildExtractionPrompt();
        console.log('[OCR] ÉTAPE 4 - Prompt construit, longueur:', prompt.length);
        console.log('[OCR] ÉTAPE 5 - Envoi requête à OpenRouter');

        const fileContent = {};
        const dataUrl = `data:${contentType};base64,${base64Data}`;

        // Construction dynamique de l'objet pour le contenu multimédia (image_url ou file)
        if (contentType === 'application/pdf') {
            // Utilisation du type 'file' pour les PDFs
            fileContent.type = 'file';
            fileContent.file = {
                file_data: dataUrl,
                filename: path.basename(filePath)
            };
        } else {
            // Utilisation du type 'image_url' pour les images
            fileContent.type = 'image_url';
            fileContent.image_url = {
                url: dataUrl
            };
        }

        try {
            const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
                model: this.model,
                messages: [
                    {
                        role: 'user',
                        content: [
                            { type: 'text', text: prompt },
                            fileContent // L'objet construit (image_url ou file)
                        ]
                    }
                ],
                // Paramètres spécifiques pour l'extraction de données
                temperature: 0.1, 
                max_tokens: 4000 // Remis à 4000 (comme dans le 1er fragment) pour plus de flexibilité
            }, {
                headers: {
                    'Authorization': `Bearer ${this.apiKey}`,
                    'Content-Type': 'application/json',
                    // Utilisez une logique de fallback pour le referer
                    'HTTP-Referer': process.env.REFERER_URL || 'http://localhost:3000', 
                    'X-Title': 'e-dec Vehicles' // Maintenu le titre spécifique
                },
                timeout: 30000
            });

            console.log('[OCR] ÉTAPE 6 - Réponse reçue:', {
                status: response.status,
                hasChoices: !!response.data?.choices,
                choicesLength: response.data?.choices?.length
            });

            const extractedText = response.data.choices?.[0]?.message?.content || '';
            console.log('[OCR] ÉTAPE 7 - Contenu extrait (aperçu):', extractedText.substring(0, 200));

            // Parsing et validation
            const parsed = this.parseGeminiJSON(extractedText);
            console.log('[OCR] ÉTAPE 8 - JSON parsé et validé avec succès:', parsed);

            return parsed;

        } catch (error) {
            // Gestion détaillée des erreurs API (étapes 5/6)
            const status = error.response?.status || 'Aucun';
            const code = error.code || 'Inconnu';

            console.error('[OCR] ERREUR ÉTAPE 5/6 - Requête API:', {
                message: error.message,
                status: status,
                statusText: error.response?.statusText,
                data: error.response?.data
            });

            if (status === 401) {
                throw new Error('Clé API OpenRouter invalide ou expirée (401)');
            }
            if (status === 429) {
                throw new Error('Quota OpenRouter dépassé (429)');
            }
            if (code === 'ECONNABORTED') {
                throw new Error('Timeout: OpenRouter n\'a pas répondu dans les 30 secondes');
            }

            // Erreur générale
            throw new Error('Erreur API OpenRouter inattendue: ' + (error.response?.data?.error?.message || error.message));
        }
    }
}

// Exportation de l'instance du service pour une utilisation directe
module.exports = new OcrService();
----------------------------------------------------
server/services/edecGeneratorExport.js

function escapeXml(value) {
    if (value === null || value === undefined) return '';
    return String(value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&apos;');
}

class EdecGeneratorExport {
    generateExport(data) {
        const now = new Date().toISOString().replace('T', ' ').replace('Z', ' UTC');
        const lang = (data.language || 'fr').toLowerCase();
        
        const exporterName = escapeXml(data.user_name || '');
        const exporterStreet = escapeXml(data.user_address || '');
        const exporterZip = escapeXml(data.user_zip || '');
        const exporterCity = escapeXml(data.user_city || '');
        const exporterCountry = 'CH'; // L'export se fait depuis la Suisse
        const exporterIde = escapeXml(data.user_ide || '');

        const description = escapeXml(`${data.brand || ''} ${data.model || ''} ${data.year || ''}`.trim());
        const commodityCode = escapeXml(data.commodity_code || '8703.9060');
        const statisticalCode = escapeXml(data.statistical_key || '911');
        const vin = escapeXml(data.vin || '');
        const destinationCountry = escapeXml(data.destination_country || '');

        // Les masses sont optionnelles pour l'export de base mais peuvent être utiles
        const grossMass = Math.round(Number(data.weight_total || 0));

        const xml = `<?xml version="1.0" encoding="UTF-8"?>
<EdecWeb version="4.0" createdDate="${now}">
  <goodsDeclarationType>
    <serviceType>2</serviceType>
    <declarationType>2</declarationType>
    <language>${lang}</language>
    <destinationCountry>${destinationCountry}</destinationCountry>
    <transportMeans>
      <transportMode>9</transportMode>
      <transportationNumber></transportationNumber>
    </transportMeans>
    <transportInContainer>0</transportInContainer>
    <exporter>
      <name>${exporterName}</name>
      ${exporterStreet ? `<street>${exporterStreet}</street>` : ''}
      <postalCode>${exporterZip}</postalCode>
      <city>${exporterCity}</city>
      <country>${exporterCountry}</country>
      ${exporterIde ? `<traderIdentificationNumber>${exporterIde}</traderIdentificationNumber>` : ''}
    </exporter>
    <business>
      <customsAccount>0</customsAccount>
    </business>
    <goodsItem>
      <GoodsItemType>
        <traderItemID>0</traderItemID>
        <description>${description}</description>
        <commodityCode>${commodityCode}</commodityCode>
        <statisticalCode>${statisticalCode}</statisticalCode>
        ${grossMass > 0 ? `<grossMass>${grossMass}</grossMass>` : ''}
        <additionalUnit>1</additionalUnit>
        <permitObligation>0</permitObligation>
        <nonCustomsLawObligation>2</nonCustomsLawObligation>
        <statistic>
            <customsClearanceType>1</customsClearanceType>
            <commercialGood>1</commercialGood>
        </statistic>
        <goodsItemDetails>
          <goodsItemDetail>
            <GoodsItemDetail>
              <name>2</name>
              <value>${vin}</value>
            </GoodsItemDetail>
          </goodsItemDetail>
        </goodsItemDetails>
        <packaging>
          <PackagingType>
            <packagingType>VN</packagingType>
            <quantity>1</quantity>
            <packagingReferenceNumber>${description}</packagingReferenceNumber>
          </PackagingType>
        </packaging>
        <valuation/>
      </GoodsItemType>
    </goodsItem>
  </goodsDeclarationType>
</EdecWeb>`;
        return xml;
    }
}

module.exports = new EdecGeneratorExport();
----------------------------------------------------
server/services/exchangeRateService.js

const mysql = require('mysql2/promise');

class ExchangeRateService {
  constructor() {
    this.dbConfig = {
      host: process.env.DB_HOST,
      user: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_NAME
    };
  }

  async getCurrentRates() {
    const connection = await mysql.createConnection(this.dbConfig);
    try {
      const [rows] = await connection.execute(`
        SELECT currency_code, rate_to_chf, rate_date 
        FROM daily_exchange_rates 
        WHERE rate_date = (
          SELECT MAX(rate_date) FROM daily_exchange_rates
        )
        ORDER BY currency_code
      `);
      const rates = {};
      rows.forEach(r => rates[r.currency_code] = { rate: parseFloat(r.rate_to_chf), date: r.rate_date });
      return rates;
    } finally {
      await connection.end();
    }
  }

  async getRate(currency) {
    if (!currency) return 1.0;
    if (currency === 'CHF') return 1.0;
    const connection = await mysql.createConnection(this.dbConfig);
    try {
      const [rows] = await connection.execute(`
        SELECT rate_to_chf 
        FROM daily_exchange_rates 
        WHERE currency_code = ? 
        ORDER BY rate_date DESC 
        LIMIT 1
      `, [currency]);
      return rows.length > 0 ? parseFloat(rows[0].rate_to_chf) : 1.0;
    } finally {
      await connection.end();
    }
  }

  async convert(amount, fromCurrency, toCurrency = 'CHF') {
    const amt = Number(amount || 0);
    if (!amt) return 0;
    if (fromCurrency === toCurrency) return Math.round(amt * 100) / 100;
    const rateFrom = await this.getRate(fromCurrency);
    const rateTo = await this.getRate(toCurrency);
    const amountInCHF = amt * rateFrom;
    const converted = amountInCHF / rateTo;
    return Math.round(converted * 100) / 100;
  }
}

module.exports = new ExchangeRateService();
----------------------------------------------------
server/services/cleanupService.old

const { pool } = require('../db'); // Ajouter cette ligne

class CleanupService {
    constructor() {
        this.stuckDeclarations = new Set();
    }

    async checkForStuckSubmissions() {
        try {
            const conn = await pool.getConnection();
            try {
                // Trouver les déclarations en processing depuis plus de 5 minutes
                const [rows] = await conn.execute(`
                    SELECT uuid, created_at 
                    FROM declarations 
                    WHERE status = 'processing' 
                    AND created_at < DATE_SUB(NOW(), INTERVAL 5 MINUTE)
                `);

                for (const row of rows) {
                    console.log(`[CLEANUP] Déclaration bloquée détectée: ${row.uuid}`);
                    
                    await conn.execute(
                        `UPDATE declarations SET status = 'timeout', error_message = ? WHERE uuid = ?`,
                        ['Processus interrompu - timeout système', row.uuid]
                    );
                    
                    this.stuckDeclarations.add(row.uuid);
                }

                if (rows.length > 0) {
                    console.log(`[CLEANUP] ${rows.length} déclaration(s) bloquée(s) nettoyée(s)`);
                }
            } finally {
                conn.release();
            }
        } catch (error) {
            console.error('[CLEANUP-ERROR]:', error);
        }
    }

    startCleanupInterval() {
        // Vérifier toutes les 5 minutes
        setInterval(() => {
            this.checkForStuckSubmissions();
        }, 5 * 60 * 1000);
    }
}

module.exports = new CleanupService();
----------------------------------------------------
server/services/taresClassifier.js

const fs = require('fs');
const path = require('path');
// Assurez-vous que le chemin d'accès à votre pool de connexion DB est correct
const { pool } = require('../db'); 

/**
 * Gère la logique de classification des véhicules selon les règles TARES
 * et la validation/lookup des VIN.
 * * NOTE: Les règles TARES (taresRules) et la classification s'attendent à être passées
 * par le contrôleur ou lues à l'initialisation du serveur,
 * car la classe n'est plus chargée statiquement par le constructeur.
 */
class TaresClassifier {

    // Suppression du constructeur qui lisait statiquement les fichiers.

    normalizeFuelType(fuelType) {
        if (!fuelType) return 'essence';
        const normalized = fuelType.toLowerCase().trim();
        
        // La nouvelle logique de normalisation est plus simple et utilise des sous-chaînes
        if (normalized.includes('plug')) return 'hybride_plugin';
        if (normalized.includes('hybr')) return 'hybride';
        if (normalized.includes('elec') || normalized.includes('élec')) return 'electrique';
        if (normalized.includes('dies')) return 'diesel';
        if (normalized.includes('ess') || normalized.includes('petrol') || normalized.includes('gasoline')) return 'essence';
        
        return normalized;
    }

    matchesRule(fuel, cylinder, weightEmpty, rule) {
        if (rule.fuel && rule.fuel !== fuel) return false;
        
        if (rule.cylinder_min !== undefined && cylinder < rule.cylinder_min) return false;
        if (rule.cylinder_max !== undefined && cylinder > rule.cylinder_max) return false;
        
        // Utilisation de weightEmpty (poids à vide) pour la classification
        if (rule.weight_min !== undefined && weightEmpty < rule.weight_min) return false;
        if (rule.weight_max !== undefined && weightEmpty > rule.weight_max) return false;
        
        return true;
    }

    /**
     * Tente de classer le véhicule en utilisant les règles de configuration fournies.
     * Si aucune correspondance n'est trouvée, utilise un fallback détaillé.
     * @param {object} vehicleData - Données du véhicule (fuel_type, cc, weight, etc.)
     * @param {object} taresRules - Les règles TARES lues depuis le fichier de configuration (passées par l'appelant)
     */
    async classify(vehicleData, taresRules = {}) {
        const fuelType = this.normalizeFuelType(vehicleData.fuel_type || 'essence');
        const cc = parseInt(vehicleData.cylinder_capacity || 0, 10);
        const wEmpty = parseInt(vehicleData.weight_empty || 0, 10);
        const wTotal = parseInt(vehicleData.weight_total || 0, 10);

        // 1. Tente d'appliquer les règles de configuration d'abord
        for (const [code, rule] of Object.entries(taresRules || {})) {
            if (this.matchesRule(fuelType, cc, wEmpty, rule)) {
                const { statistical_keys = { '911': 'Automobile' }, description = 'Véhicule' } = rule;
                const { key, needs_user_selection, description: skDesc, available_keys } = this.selectStatisticalKey(statistical_keys, wTotal);
                
                return {
                    commodity_code: code,
                    description,
                    statistical_key: needs_user_selection ? null : key,
                    statistical_key_description: needs_user_selection ? null : (skDesc || statistical_keys[key]),
                    needs_user_selection,
                    available_keys: needs_user_selection ? statistical_keys : null
                };
            }
        }

        // 2. Logique de fallback manuelle pour une classification plus précise (comme les règles 8703.xx)
        if (fuelType === 'diesel') {
            if (cc <= 1500) return { commodity_code: '8703.3100', statistical_key: '911', description: 'Véhicule diesel ≤1500 cm3', needs_user_selection: false };
            if (cc > 1500 && cc <= 2500) {
                if (wEmpty <= 1200) return { commodity_code: '8703.3240', statistical_key: '911', description: 'Diesel >1500 ≤2500 cm3, ≤1200kg', needs_user_selection: false };
                if (wEmpty > 1200 && wEmpty <= 1600) return { commodity_code: '8703.3250', statistical_key: '911', description: 'Diesel >1500 ≤2500 cm3, >1200kg ≤1600kg', needs_user_selection: false };
                return { commodity_code: '8703.3260', statistical_key: '911', description: 'Diesel >1500 ≤2500 cm3, >1600kg', needs_user_selection: false };
            }
            if (cc > 2500) {
                if (wEmpty <= 1600) return { commodity_code: '8703.3330', statistical_key: '911', description: 'Diesel >2500 cm3, ≤1600kg', needs_user_selection: false };
                return { commodity_code: '8703.3340', statistical_key: '911', description: 'Diesel >2500 cm3, >1600kg', needs_user_selection: false };
            }
        }
        
        // 3. Fallback générique si rien ne correspond
        return {
            commodity_code: '8703.9060',
            statistical_key: '911',
            description: 'Autres véhicules',
            needs_user_selection: false
        };
    }

    selectStatisticalKey(keys, weightTotal) {
        if (!keys || Object.keys(keys).length === 0) return { key: '911', description: 'Automobile', needs_user_selection: false, available_keys: null };
        
        // Cas 1: Une seule clé disponible
        if (Object.keys(keys).length === 1) {
            const k = Object.keys(keys)[0];
            return { key: k, description: keys[k], needs_user_selection: false, available_keys: null };
        }
        
        // Cas 2: Sélection automatique basée sur le Poids Total (ex: 921 > 3500kg, 923 <= 3500kg)
        if (keys['921'] && keys['923']) {
            return weightTotal > 3500
                ? { key: '921', description: keys['921'], needs_user_selection: false, available_keys: null }
                : { key: '923', description: keys['923'], needs_user_selection: false, available_keys: null };
        }
        
        // Cas 3: Sélection manuelle requise
        return { key: null, description: null, needs_user_selection: true, available_keys: keys };
    }

    /**
     * Récupère le pays de fabrication (WMI) à partir du VIN en utilisant la base de données.
     * @param {string} vin - Numéro d'identification du véhicule.
     * @returns {Promise<string|null>} - Code pays (ex: 'FR') ou null.
     */
    async getCountryFromVIN_DB(vin) {
        if (!vin || vin.length < 2) return null;
        const wmi = vin.substring(0, 2).toUpperCase();
        
        // Utilisation du pool pour la connexion à la base de données
        const conn = await pool.getConnection();
        try {
            const [rows] = await conn.execute(`
                SELECT country_code FROM vin_wmi_country_codes
                WHERE ? BETWEEN wmi_start AND wmi_end
                LIMIT 1
            `, [wmi]);
            
            // Assurez-vous d'avoir des colonnes 'wmi_start', 'wmi_end' et 'country_code' dans votre table SQL.
            return rows?.[0]?.country_code || null;
        } catch (error) {
            console.error("Erreur DB WMI:", error.message);
            return null;
        } finally {
            if (conn) conn.release();
        }
    }
}

// Exportation en tant qu'instance unique (Singleton)
module.exports = new TaresClassifier();
----------------------------------------------------
server/db.js

const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

module.exports = {
  pool,
  getConnection: () => pool.getConnection()
};
----------------------------------------------------
server/controllers/homeController.js

// Obsolète (SPA), conservé pour compatibilité si utilisé ailleurs
const path = require('path');
const router = require('express').Router();

router.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
});

router.get('/:lang', (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
});

module.exports = router;
----------------------------------------------------
server/controllers/importController.js

// Obsolète, les routes actuelles sont dans /server/routes/api.js
const router = require('express').Router();

router.get('/ping', (req, res) => res.json({ ok: true }));

module.exports = router;
----------------------------------------------------
server/controllers/exportController.js

// Obsolète, les routes actuelles sont dans /server/routes/api.js
module.exports = {
  submit: (req, res) => {
    const xml = `<?xml version="1.0" encoding="UTF-8"?>
<EdecWeb version="4.0">
  <goodsDeclarationType>
    <serviceType>2</serviceType>
  </goodsDeclarationType>
</EdecWeb>`;
    res.set('Content-Type', 'application/xml');
    res.set('Content-Disposition', `attachment; filename="edec-export-${Date.now()}.xml"`);
    res.send(xml);
  }
};
----------------------------------------------------
public/js/controllers/homeController.js

angular.module('edecApp')
  .controller('HomeController', function($scope) {
    $scope.features = [
      { icon: '⚡', title: 'Rapide', description: 'Générez votre déclaration en moins de 5 minutes' },
      { icon: '💰', title: 'Économique', description: 'Tarifs transparents, outil personnel' },
      { icon: '✅', title: 'Simple', description: 'Interface intuitive, étapes guidées' },
      { icon: '🤖', title: 'IA intégrée', description: 'Extraction automatique depuis votre carte grise' }
    ];
  });
----------------------------------------------------
public/js/controllers/importController.js

angular.module('edecApp')
    .controller('ImportController', function($scope, $http, $window, $rootScope, $timeout, ConfigService) {

        function initScope() {
            $scope.step = 1;
            $scope.method = null;
            $scope.vehicle = {};
            $scope.parties = {
                importer: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' },
                consignee: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' },
                declarant: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' }
            };
            $scope.declaration = {
                transport_mode: '9',
                purchase_currency: 'EUR',
                purchase_price: 0,
                transport_cost: 0,
                other_costs: 0,
                language: $rootScope.lang || 'fr',
                dispatch_country: ''
            };
            $scope.classification = null;
            $scope.ocrLoading = false;
            $scope.generating = false;
            $scope.ocrAttempted = false;
            $scope.statisticalValueCHF = 0;
            $scope.vatValueCHF = 0;
            $scope.vinValid = null;
            $scope.isCompany = false;
            $scope.sessionToken = null;
            $scope.submissionProgress = 0;
            $scope.submissionStep = '';
            $scope.submissionResult = null;
            $scope.showProgress = false;
            $scope.eventSource = null; // [NOUVEAU] Stocke la connexion SSE
        }

        initScope(); // Initialiser le scope

        // Load reference data using the cached service
        ConfigService.getCountries().then(data => $scope.countries = data);
        ConfigService.getCurrencies().then(data => $scope.currencies = data);

        $scope.$watch('parties.importer.ide', function(newVal) {
            $scope.isCompany = !!newVal && newVal.trim() !== 'CHE222251936';
        });

        $scope.selectMethod = function(method) {
            $scope.method = method;
            $scope.step = 2;
        };

		$scope.copyLinkToClipboard = function(link) {
			// Crée un élément temporaire pour copier le texte
			const tempInput = document.createElement('input');
			tempInput.style.position = 'absolute';
			tempInput.style.left = '-9999px';
			tempInput.value = 'monsite.ch' + link; // Utilisez le domaine complet

			document.body.appendChild(tempInput);
			tempInput.select();
			document.execCommand('copy');
			document.body.removeChild(tempInput);
			
			// Notification visuelle rapide à l'utilisateur
			alert('Lien copié dans le presse-papiers !');
		};

        $scope.triggerFileInput = () => document.getElementById('file-upload').click();

        $scope.handleFileSelect = function(event) {
            const files = event.target.files;
            if (!files || files.length === 0) return;

            $scope.$apply(() => {
                $scope.ocrLoading = true;
                $scope.ocrAttempted = true;
            });
            const formData = new FormData();
            formData.append('registration_card', files[0]);

            $http.post('/api/ocr', formData, {
                transformRequest: angular.identity,
                headers: { 'Content-Type': undefined }
            }).then(function(response) {
                Object.assign($scope.vehicle, response.data);
                if ($scope.vehicle.vin) $scope.validateVIN();
                $scope.updateClassification();
            }).catch(function(error) {
                const details = error.data?.details || error.data?.error || 'Erreur inconnue';
                alert('Erreur OCR: ' + details);
            }).finally(() => {
                $scope.ocrLoading = false;
            });
        };

        $scope.validateVIN = function() {
            if (!$scope.vehicle.vin || $scope.vehicle.vin.length !== 17) {
                $scope.vinValid = false;
                return;
            }
            $http.post('/api/validate-vin', { vin: $scope.vehicle.vin })
                .then(function(response) {
                    $scope.vinValid = response.data.valid;
                    if (response.data.valid && response.data.country) {
                        $scope.declaration.dispatch_country = response.data.country;
                    }
                });
        };

        $scope.updateClassification = function() {
            if (!$scope.vehicle.fuel_type || !$scope.vehicle.weight_empty) return;
            $http.post('/api/classify-vehicle', $scope.vehicle)
                .then(function(response) {
                    $scope.classification = response.data;
                    $scope.vehicle.commodity_code = response.data.commodity_code;
                    if (!response.data.needs_user_selection) {
                        $scope.vehicle.statistical_key = response.data.statistical_key;
                    }
                });
        };
        
        $scope.calculateTotal = function() {
            const price = parseFloat($scope.declaration.purchase_price) || 0;
            const transport = parseFloat($scope.declaration.transport_cost) || 0;
            const other = parseFloat($scope.declaration.other_costs) || 0;
            const currency = $scope.declaration.purchase_currency;

            $http.post('/api/convert-currency', { amount: price, from: currency })
                .then(function(res) {
                    $scope.statisticalValueCHF = Math.round(res.data.converted);
                    return $http.post('/api/convert-currency', { amount: price + transport + other, from: currency });
                })
                .then(function(resVat) {
                    $scope.vatValueCHF = Math.round(resVat.data.converted);
                });
        };
        
        $scope.nextStep = function() {
            if ($scope.step === 2) {
                if (!$scope.vehicle.vin || !$scope.vehicle.brand || !$scope.vehicle.model || !$scope.vehicle.weight_empty || !$scope.vehicle.weight_total) {
                    alert('Veuillez remplir les informations obligatoires du véhicule.');
                    return;
                }
            }
            $scope.step++;
        };

        $scope.prevStep = () => $scope.step--;

        $scope.transportModes = [
            { value: '9', label: 'Autopropulsion' },
            { value: '3', label: 'Trafic routier' },
            { value: '2', label: 'Trafic ferroviaire' },
            { value: '4', label: 'Trafic aérien' },
            { value: '8', label: 'Trafic par eau' },
            { value: '7', label: 'Pipeline, etc.' }
        ];

        const getFullName = (name, firstname, ide) => {
            const isCompany = !!ide && ide.trim() !== 'CHE222251936';
            if (isCompany) return name || '';
            return [firstname, name].filter(p => p && p.trim()).join(' ');
        };
        
        $scope.generateDeclaration = function() {
            if ($scope.generating) return;

            const imp = $scope.parties.importer;
            if (!imp.name || !imp.zip || !imp.city || !$scope.declaration.dispatch_country) {
                alert('Veuillez renseigner les informations de l`importateur et le pays d`expédition.');
                return;
            }

            $scope.generating = true;
            $scope.submissionProgress = 0;
            $scope.submissionStep = 'Initialisation...';
            $scope.showProgress = true;
            $scope.step = 4; // Aller à l'étape de progression/résultat

            const importerFullName = getFullName(imp.name, imp.firstname, imp.ide);
            const payload = { ...$scope.vehicle, ...$scope.declaration,
                user_name: importerFullName,
                user_address: imp.address, user_zip: imp.zip, user_city: imp.city, user_ide: imp.ide,
                language: $rootScope.lang
            };

            $http.post('/api/generate-import', payload)
                .then(function(response) {
                    console.log('[CLIENT] Génération acceptée, session:', response.data.sessionToken);
                    $scope.sessionToken = response.data.sessionToken;
                    $scope.listenForSubmissionStatus(); // Remplacement du polling par SSE
                })
                .catch(function(error) {
                    alert('Erreur: ' + (error.data?.error || 'Erreur de génération'));
                    if ($scope.eventSource) $scope.eventSource.close(); // S'assurer que tout est fermé
                    $scope.generating = false;
                    $scope.showProgress = false;
                    $scope.step = 3; 
                });
        };

        // [NOUVEAU] Écouteur SSE
        $scope.listenForSubmissionStatus = function() {
            if (!$scope.sessionToken) return;

            // Fermer l'ancienne connexion si elle existe
            if ($scope.eventSource) {
                $scope.eventSource.close();
            }
            
            // Crée une connexion EventSource vers le nouveau endpoint SSE
            const source = new EventSource('/api/submission-status-sse/' + $scope.sessionToken);
            $scope.eventSource = source; 

            source.onmessage = function(event) {
                $scope.$apply(() => {
                    try {
                        const status = JSON.parse(event.data);
                        
                        $scope.submissionProgress = status.progress;
                        $scope.submissionStep = status.step;
                        
                        console.log(`[CLIENT-SSE] Statut: ${status.status}, Progression: ${status.progress}%`);

                        if (status.status === 'submitted') {
                            $scope.submissionResult = status;
                            $scope.generating = false;
                            $scope.showProgress = false;
                            source.close(); // Fermer la connexion SSE après le succès final
                        } else if (status.status === 'submission_error') {
                            $scope.generating = false;
                            $scope.showProgress = false;
                            source.close(); // Fermer la connexion SSE après erreur
                            $rootScope.setErrorContext($scope.sessionToken); 
                        }
                    } catch (e) {
                        console.error('[CLIENT-SSE] Erreur de données reçues:', e);
                    }
                });
            };
            
            source.onerror = function(error) {
                console.error('[CLIENT-SSE] Erreur EventSource:', error);
                $scope.$apply(() => {
                    // La reconnexion est gérée par le navigateur
                    $scope.submissionStep = 'Erreur de connexion SSE. Tentative de reconnexion automatique...';
                });
            };

            source.onopen = function() {
                console.log('[CLIENT-SSE] Connexion SSE établie.');
            };
        };

        $scope.downloadPDF = function(path) {
            if (!path) return;
            $window.open('/api/download-pdf?path=' + encodeURIComponent(path), '_blank');
        };

        $scope.resetForm = function() {
            if ($scope.eventSource) $scope.eventSource.close();
            initScope();
        };
    });
----------------------------------------------------
public/js/controllers/submissionErrorController.js

angular.module('edecApp')
    .controller('SubmissionErrorController', function($scope, $http) {
        $scope.sessionToken = sessionStorage.getItem('error_session_token');
        $scope.email = '';
        $scope.submitted = false;
        $scope.errorMessage = '';

        $scope.reportError = function() {
            if (!$scope.email || !$scope.sessionToken) {
                $scope.errorMessage = 'Email invalide.';
                return;
            }
            $scope.errorMessage = '';
            
            $http.post('/api/submission-error/report', {
                sessionToken: $scope.sessionToken,
                email: $scope.email
            }).then(function() {
                $scope.submitted = true;
            }).catch(function(error) {
                $scope.errorMessage = error.data?.error || 'Une erreur est survenue.';
            });
        };
    });
----------------------------------------------------
public/js/controllers/exportController.js

angular.module('edecApp')
    .controller('ExportController', function($scope, $http, $window, $rootScope, ConfigService) {

        function initScope() {
            $scope.vehicle = {};
            $scope.parties = {
                exporter: { name: '', address: '', zip: '', city: '', ide: '' }
            };
            $scope.declaration = {
                destination_country: '',
                language: $rootScope.lang || 'fr'
            };
            $scope.generating = false;
        }

        initScope();

        // Load countries using the cached service
        ConfigService.getCountries().then(data => $scope.countries = data);

        $scope.generateExport = function() {
            if ($scope.generating || $scope.exportForm.$invalid) {
                if ($scope.exportForm.$invalid) {
                    alert('Veuillez remplir tous les champs obligatoires (*).');
                }
                return;
            }

            $scope.generating = true;
            
            const ex = $scope.parties.exporter;
            const payload = {
                ...$scope.vehicle,
                ...$scope.declaration,
                user_name: ex.name,
                user_address: ex.address,
                user_zip: ex.zip,
                user_city: ex.city,
                user_ide: ex.ide
            };

            $http.post('/api/generate-export', payload, { responseType: 'blob' })
                .then(function(response) {
                    const blob = new Blob([response.data], { type: 'application/xml' });
                    const url = $window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `edec-export-${Date.now()}.xml`;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    $window.URL.revokeObjectURL(url);
                    alert('✅ Déclaration d\'exportation générée avec succès !');
                    initScope(); // Réinitialiser le formulaire
                    $scope.exportForm.$setPristine();
                    $scope.exportForm.$setUntouched();
                })
                .catch(function(error) {
                    alert('Erreur: ' + (error.data?.error || 'Erreur de génération'));
                })
                .finally(function() {
                    $scope.generating = false;
                });
        };
    });
----------------------------------------------------
public/js/app.js

angular.module('edecApp', ['ngRoute'])
    .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
        $locationProvider.html5Mode({ enabled: true, requireBase: true });

        $routeProvider
            .when('/', { templateUrl: 'views/home.html', controller: 'HomeController' })
            .when('/import', { templateUrl: 'views/import.html', controller: 'ImportController' })
            .when('/export', { templateUrl: 'views/export.html', controller: 'ExportController' })
            .when('/submission-error', { templateUrl: 'views/submission-error.html', controller: 'SubmissionErrorController' })
            .otherwise({ redirectTo: '/' });
    }])
    .controller('MainController', ['$scope', '$location', function($scope, $location) {
        $scope.currentPath = () => $location.path();
    }])
    .factory('ConfigService', ['$http', '$q', function($http, $q) {
        const cache = {};

        function getCached(key, apiUrl) {
            if (cache[key]) {
                return $q.resolve(cache[key]);
            }
            const dataFromSession = sessionStorage.getItem(key);
            if (dataFromSession) {
                cache[key] = JSON.parse(dataFromSession);
                return $q.resolve(cache[key]);
            }
            return $http.get(apiUrl).then(function(response) {
                cache[key] = response.data;
                sessionStorage.setItem(key, JSON.stringify(response.data));
                return response.data;
            });
        }

        return {
            getCountries: () => getCached('countries', '/api/countries'),
            getCurrencies: () => getCached('currencies', '/api/currencies')
        };
    }])
    .run(['$rootScope', '$location', function($rootScope, $location) {
        // Language handling
        const SUPPORTED = ['fr', 'de', 'it', 'en'];
        const stored = localStorage.getItem('edec_lang');
        const defaultLang = 'fr';
        $rootScope.lang = SUPPORTED.includes(stored) ? stored : defaultLang;
        
        $rootScope.setLang = function(l) {
            if (SUPPORTED.includes(l)) {
                $rootScope.lang = l;
                localStorage.setItem('edec_lang', l);
            }
        };
        $rootScope.isLang = l => $rootScope.lang === l;

        // Stocker le sessionToken d'erreur pour la page d'erreur
        $rootScope.setErrorContext = function(token) {
            sessionStorage.setItem('error_session_token', token);
            $location.path('/submission-error');
        };
    }]);
----------------------------------------------------
public/index.html

<!DOCTYPE html>
<html lang="fr" ng-app="edecApp">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>e-dec Véhicules - Déclaration Simplifiée</title>
  <base href="/">
  <link rel="stylesheet" href="/css/style.css">
  
  <!-- AngularJS CDN -->
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular-route.min.js"></script>
</head>
<body ng-controller="MainController">
  <!-- Navigation -->
  <nav class="navbar">
    <div class="container">
      <a href="/" class="logo">🚗 e-dec Véhicules</a>
      <ul class="nav-menu">
        <a href="/import" ng-class="{active: currentPath() == '/import'}">Importer</a>
        <a href="/export" ng-class="{active: currentPath() == '/export'}">Exporter</a>
        <a href="/" ng-class="{active: currentPath() == '/'}">Accueil</a>
      </ul>
      <div class="lang-switcher">
        <button class="btn-lang" ng-class="{active: isLang('fr')}" ng-click="setLang('fr')">FR</button>
        <button class="btn-lang" ng-class="{active: isLang('de')}" ng-click="setLang('de')">DE</button>
        <button class="btn-lang" ng-class="{active: isLang('it')}" ng-click="setLang('it')">IT</button>
        <button class="btn-lang" ng-class="{active: isLang('en')}" ng-click="setLang('en')">EN</button>
      </div>
    </div>
  </nav>
  
  <!-- Contenu principal -->
  <main class="main-content" ng-view></main>
  
  <!-- Footer -->
  <footer class="footer">
    <div class="container">
      <p>&copy; 2025 e-dec Véhicules. Simplifiez vos déclarations douanières.</p>
      <p class="disclaimer">⚠️ Cet outil aide à créer votre déclaration. La responsabilité des informations incombe au déclarant.</p>
    </div>
  </footer>
  
  <!-- Scripts AngularJS -->
    <script src="/js/app.js"></script>
    <script src="/js/controllers/homeController.js"></script>
    <script src="/js/controllers/importController.js"></script>
    <script src="/js/controllers/exportController.js"></script>

</body>
</html>
----------------------------------------------------
public/css/style.css

/* Reset & Base */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    line-height: 1.6;
    color: #333;
    background: #f5f7fa;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

/* Navigation */
.navbar {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    padding: 1rem 0;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.navbar .container {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
}

.logo {
    font-size: 1.5rem;
    font-weight: bold;
    color: white;
    text-decoration: none;
}

.nav-menu {
    display: flex;
    list-style: none;
    gap: 2rem;
}

.nav-menu a {
    color: rgba(255,255,255,0.9);
    text-decoration: none;
    font-weight: 500;
    transition: color 0.3s;
}

.nav-menu a:hover,
.nav-menu a.active {
    color: white;
}

.lang-switcher {
    display: flex;
    gap: 0.5rem;
}

.btn-lang {
    padding: 0.5rem 1rem;
    border: 1px solid rgba(255,255,255,0.3);
    background: rgba(255,255,255,0.1);
    color: white;
    cursor: pointer;
    border-radius: 5px;
    transition: all 0.3s;
}

.btn-lang:hover,
.btn-lang.active {
    background: rgba(255,255,255,0.2);
    border-color: white;
}

/* Hero */
.hero {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 5rem 0;
    text-align: center;
}

.hero h1 {
    font-size: 3rem;
    margin-bottom: 1rem;
}

.subtitle {
    font-size: 1.25rem;
    margin-bottom: 2rem;
    opacity: 0.9;
}

.cta-buttons {
    display: flex;
    gap: 1rem;
    justify-content: center;
    flex-wrap: wrap;
}

/* Buttons */
.btn {
    padding: 0.875rem 2rem;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: all 0.3s;
    display: inline-block;
}

.btn-primary {
    background: #667eea;
    color: white;
}

.btn-primary:hover {
    background: #5568d3;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(102,126,234,0.4);
}

.btn-secondary {
    background: #6c757d;
    color: white;
}

.btn-secondary:hover {
    background: #5a6268;
}

/* Features */
.features {
    padding: 5rem 0;
    background: white;
}

.features h2 {
    text-align: center;
    margin-bottom: 3rem;
    font-size: 2.5rem;
    color: #333;
}

.features-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 2rem;
}

.feature-card {
    text-align: center;
    padding: 2rem;
    border-radius: 12px;
    background: #f8f9fa;
    transition: transform 0.3s;
}

.feature-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 8px 20px rgba(0,0,0,0.1);
}

.feature-icon {
    font-size: 3rem;
    margin-bottom: 1rem;
}

/* How it works */
.how-it-works {
    padding: 5rem 0;
}

.how-it-works h2 {
    text-align: center;
    margin-bottom: 3rem;
    font-size: 2.5rem;
}

.steps {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 2rem;
}

.step-item {
    text-align: center;
}

.step-number {
    width: 60px;
    height: 60px;
    background: #667eea;
    color: white;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    font-weight: bold;
    margin: 0 auto 1rem;
}

/* Progress Bar */
.progress-bar {
    display: flex;
    justify-content: space-between;
    margin: 2rem 0 3rem;
    position: relative;
}

.progress-bar::before {
    content: '';
    position: absolute;
    top: 20px;
    left: 0;
    right: 0;
    height: 2px;
    background: #ddd;
    z-index: 0;
}

.progress-step {
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    z-index: 1;
}

.step-num {
    width: 40px;
    height: 40px;
    background: #ddd;
    color: #666;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
    margin-bottom: 0.5rem;
}

.progress-step.active .step-num {
    background: #667eea;
    color: white;
}

.progress-step.completed .step-num {
    background: #28a745;
    color: white;
}

/* Method Cards */
.method-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 2rem;
    margin: 2rem 0;
}

.method-card {
    text-align: center;
    padding: 3rem 2rem;
    border: 2px solid #ddd;
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.3s;
}

.method-card:hover {
    border-color: #667eea;
    transform: translateY(-5px);
    box-shadow: 0 8px 20px rgba(102,126,234,0.2);
}

.method-icon {
    font-size: 4rem;
    margin-bottom: 1rem;
}

/* Form */
.form {
    max-width: 800px;
    margin: 2rem auto;
    background: white;
    padding: 2rem;
    border-radius: 12px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

fieldset {
    border: 1px solid #ddd;
    padding: 1.5rem;
    margin-bottom: 2rem;
    border-radius: 8px;
}

legend {
    font-weight: bold;
    padding: 0 0.5rem;
    color: #667eea;
}

.form-group {
    margin-bottom: 1.5rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
    color: #333;
}

.form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
}

/* Style des inputs: inclut text, email, number et select */
input[type="text"],
input[type="email"],
input[type="number"],
select {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 1rem;
    transition: border-color 0.3s;
}

input:focus,
select:focus {
    outline: none;
    border-color: #667eea;
    box-shadow: 0 0 0 3px rgba(102,126,234,0.1);
}

.hint {
    display: block;
    font-size: 0.875rem;
    color: #28a745;
    margin-top: 0.25rem;
}

.error {
    display: block;
    font-size: 0.875rem;
    color: #dc3545;
    margin-top: 0.25rem;
}

/* Upload Box */
.upload-box {
    border: 3px dashed #ddd;
    border-radius: 12px;
    padding: 3rem;
    text-align: center;
    margin: 2rem 0;
    cursor: pointer;
    transition: all 0.3s;
}

.upload-box:hover {
    border-color: #667eea;
    background: #f8f9fa;
}

.upload-label {
    font-size: 1.25rem;
    cursor: pointer;
}

/* Classification Info */
.classification-info {
    background: #e7f3ff;
    border-left: 4px solid #667eea;
    padding: 1.5rem;
    margin: 1.5rem 0;
    border-radius: 6px;
}

/* Total Box */
.total-box {
    background: #d4edda;
    border: 2px solid #28a745;
    padding: 1.5rem;
    border-radius: 8px;
    margin: 1.5rem 0;
    text-align: center;
}

.total-box h3 {
    color: #155724;
    margin: 0;
}

/* Checkbox */
.checkbox-label {
    display: flex;
    align-items: center;
    cursor: pointer;
    margin: 1rem 0;
}

.checkbox-label input[type="checkbox"] {
    width: auto;
    margin-right: 0.75rem; /* Mieux */
    height: 1.1em;         /* Mieux */
    width: 1.1em;          /* Mieux */
}

/* Form Actions */
.form-actions {
    display: flex;
    gap: 1rem;
    justify-content: space-between; /* Mieux pour la navigation Précédent/Suivant */
    margin-top: 2rem;
}

/* Success */
.success-content {
    text-align: center;
    padding: 3rem 0;
}

.success-icon {
    font-size: 5rem;
    margin-bottom: 1rem;
    color: #28a745; /* Mieux: couleur de succès */
}

.next-steps-box {
    background: white;
    border: 1px solid #ddd;
    border-radius: 12px;
    padding: 2rem;
    margin: 2rem auto;
    max-width: 600px;
    text-align: left;
}

.next-steps-box ol {
    padding-left: 1.5rem; /* Mieux que margin-left */
}

.next-steps-box li {
    margin-bottom: 0.5rem;
}

/* Barre de progression */
.progress-bar-container {
  width: 100%;
  height: 20px;
  background: #ddd;
  border-radius: 10px;
  margin: 1rem 0;
  overflow: hidden;
}

.progress-bar {
  height: 100%;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  transition: width 0.5s ease;
}

.progress-section {
  text-align: center;
  padding: 2rem 0;
}

/* Footer */
.footer {
    background: #2c3e50;
    color: white;
    padding: 2rem 0;
    text-align: center;
    margin-top: 3rem;
}

.disclaimer {
    font-size: 0.875rem;
    opacity: 0.8;
    margin-top: 0.5rem;
}

/* Main Content */
.main-content {
    min-height: calc(100vh - 300px);
    padding: 2rem 0;
}

/* Responsive */
@media (max-width: 768px) {
    .hero h1 {
        font-size: 2rem;
    }
    
    .navbar .container {
        flex-direction: column;
        gap: 1rem;
    }
    
    .nav-menu {
        flex-direction: column;
        gap: 0.5rem;
        text-align: center;
        width: 100%;
    }
    
    .form-row {
        grid-template-columns: 1fr;
    }
    
    .form-actions {
        flex-direction: column-reverse; /* Mieux: met le bouton principal en bas */
    }
    
    .btn {
        width: 100%;
    }
}
----------------------------------------------------
public/views/submission-error.html

<div class="container">
    <div class="step-content" style="text-align: center; max-width: 700px; margin: 2rem auto;">
        <div class="success-icon" style="color: #dc3545;">⚠️</div>
        <h2>Erreur Technique de Soumission</h2>
        <p style="font-size: 1.1rem; margin: 1rem 0;">
            Nous avons rencontré une erreur inattendue lors de la communication avec le portail de la douane.
            Votre déclaration a bien été générée mais n'a pas pu être soumise automatiquement.
        </p>
        <div class="next-steps-box" ng-if="!submitted">
            <h3>Que faire ?</h3>
            <p>Notre équipe technique a été informée. Pour un suivi personnalisé, veuillez laisser votre adresse e-mail ci-dessous. Nous analyserons le problème et vous recontacterons dans les plus brefs délais avec une solution.</p>
            <form name="errorForm" ng-submit="reportError()" style="margin-top: 1.5rem;" novalidate>
                <div class="form-group">
                    <label for="email" style="text-align: left;">Votre adresse e-mail :</label>
                    <input type="email" id="email" class="form-control" ng-model="email" placeholder="votre.email@exemple.com" required>
                </div>
                <p class="error" ng-if="errorMessage">{{ errorMessage }}</p>
                <div class="form-actions" style="justify-content: center;">
                    <button type="submit" class="btn btn-primary" ng-disabled="errorForm.$invalid">
                        Envoyer et être recontacté
                    </button>
                </div>
            </form>
        </div>
        <div class="next-steps-box" ng-if="submitted">
            <h3>Merci !</h3>
            <p>Nous avons bien reçu votre adresse e-mail. Notre équipe est sur le coup et reviendra vers vous très prochainement.</p>
        </div>
    </div>
</div>
----------------------------------------------------
public/views/home.html

<div class="hero">
    <div class="container">
        <h1>🚗 Déclaration e-dec Simplifiée</h1>
        <p class="subtitle">Importez ou exportez votre véhicule en Suisse en quelques clics</p>
        <div class="cta-buttons">
            <a href="/import" class="btn btn-primary">Importer un véhicule</a>
            <a href="/export" class="btn btn-secondary">Exporter un véhicule</a>
        </div>
    </div>
</div>

<section class="features">
    <div class="container">
        <h2>Pourquoi choisir notre service ?</h2>
        <div class="features-grid">
            <div class="feature-card" ng-repeat="feature in features">
                <div class="feature-icon">{{ feature.icon }}</div>
                <h3>{{ feature.title }}</h3>
                <p>{{ feature.description }}</p>
            </div>
        </div>
    </div>
</section>

<section class="how-it-works">
    <div class="container">
        <h2>Comment ça marche ?</h2>
        <div class="steps">
            <div class="step-item">
                <div class="step-number">1</div>
                <h3>Choisissez votre méthode</h3>
                <p>Scanner la carte grise ou saisie manuelle</p>
            </div>
            <div class="step-item">
                <div class="step-number">2</div>
                <h3>Remplissez les informations</h3>
                <p>Véhicule, finances et coordonnées</p>
            </div>
            <div class="step-item">
                <div class="step-number">3</div>
                <h3>Générez le fichier XML</h3>
                <p>Téléchargez votre déclaration e-dec prête à l'emploi</p>
            </div>
            <div class="step-item">
                <div class="step-number">4</div>
                <h3>Soumettez à la douane</h3>
                <p>Importez le fichier sur le portail officiel e-dec</p>
            </div>
        </div>
    </div>
</section>
----------------------------------------------------
public/views/export.html

<div class="container">
    <h1>Exporter un Véhicule depuis la Suisse</h1>
    <p>Cette page vous permet de générer une déclaration d'exportation pour un véhicule sortant du territoire suisse.</p>
    
    <form name="exportForm" class="form" ng-submit="generateExport()" novalidate>
        <fieldset>
            <legend>Informations Exportateur</legend>
            <div class="form-row">
                <div class="form-group">
                    <label>Nom ou raison sociale *</label>
                    <input type="text" ng-model="parties.exporter.name" required>
                </div>
                <div class="form-group">
                    <label>Numéro IDE</label>
                    <input type="text" ng-model="parties.exporter.ide" placeholder="Optionnel (ex: CHE-123.456.789)">
                </div>
            </div>
            <div class="form-group">
                <label>Adresse</label>
                <input type="text" ng-model="parties.exporter.address">
            </div>
            <div class="form-row">
                <div class="form-group">
                    <label>NPA *</label>
                    <input type="text" ng-model="parties.exporter.zip" required>
                </div>
                <div class="form-group">
                    <label>Ville *</label>
                    <input type="text" ng-model="parties.exporter.city" required>
                </div>
            </div>
        </fieldset>

        <fieldset>
            <legend>Informations Véhicule</legend>
            <div class="form-group">
                <label>VIN (Numéro de châssis) *</label>
                <input type="text" ng-model="vehicle.vin" maxlength="17" minlength="17" required style="text-transform:uppercase">
            </div>
            <div class="form-row">
                <div class="form-group">
                    <label>Marque *</label>
                    <input type="text" ng-model="vehicle.brand" required>
                </div>
                <div class="form-group">
                    <label>Modèle *</label>
                    <input type="text" ng-model="vehicle.model" required>
                </div>
                <div class="form-group">
                    <label>Année</label>
                    <input type="number" ng-model="vehicle.year" placeholder="Optionnel">
                </div>
            </div>
        </fieldset>
        
        <fieldset>
            <legend>Informations d'Exportation</legend>
            <div class="form-group">
                <label>Pays de destination *</label>
                <select ng-model="declaration.destination_country" required 
                        ng-options="code as name for (code, name) in countries">
                    <option value="">Sélectionner un pays...</option>
                </select>
            </div>
        </fieldset>
        
        <div class="form-actions" style="justify-content: flex-end;">
            <button type="submit" class="btn btn-primary" ng-disabled="exportForm.$invalid || generating">
                <span ng-if="!generating">Générer le Fichier XML ✨</span>
                <span ng-if="generating">Génération en cours...</span>
            </button>
        </div>
    </form>
</div>
----------------------------------------------------
public/views/import.html

<div class="container">
	<h1>Importer un Véhicule en Suisse</h1>
	<div class="progress-bar" ng-show="step > 1">
		<div class="progress-step" ng-class="{active: step >= 1, completed: step > 1}">
			<span class="step-num">1</span>
			<span class="step-label">Méthode</span>
		</div>
		<div class="progress-step" ng-class="{active: step >= 2, completed: step > 2}">
			<span class="step-num">2</span>
			<span class="step-label">Véhicule</span>
		</div>
		<div class="progress-step" ng-class="{active: step >= 3, completed: step > 3}">
			<span class="step-num">3</span>
			<span class="step-label">Déclaration</span>
		</div>
		<div class="progress-step" ng-class="{active: step >= 4, completed: submissionResult}">
			<span class="step-num">4</span>
			<span class="step-label">Résultat</span>
		</div>
	</div>
	<div ng-show="step === 1" class="step-content">
		<h2 style="text-align: center; margin-bottom: 2rem;">Choisissez votre méthode de saisie</h2>
		<div class="method-cards">
			<div class="method-card" ng-click="selectMethod('ocr')">
				<div class="method-icon">📸</div>
				<h3>Scanner la carte grise</h3>
				<p>Extraction automatique des données par IA (JPG, PNG, PDF)</p>
			</div>
			<div class="method-card" ng-click="selectMethod('manual')">
				<div class="method-icon">⌨️</div>
				<h3>Saisie manuelle</h3>
				<p>Remplir le formulaire pas à pas</p>
			</div>
		</div>
	</div>
	<div ng-show="step === 2" class="step-content">
		<h2 style="text-align: center;">Informations du véhicule</h2>
		<div ng-if="method === 'ocr'" class="ocr-section">
			<div class="upload-box" ng-click="triggerFileInput()">
				<label class="upload-label">
					<span ng-if="!ocrLoading">📷 Photographier ou choisir l'image/PDF</span>
					<span ng-if="ocrLoading">⏳ Extraction en cours...</span>
				</label>
				<input id="file-upload" type="file" accept="image/*,application/pdf" style="display:none" onchange="angular.element(this).scope().handleFileSelect(event)">
			</div>
		</div>
		<form name="vehicleForm" class="form" ng-show="method === 'manual' || vehicle.vin || vehicle.brand" novalidate>
			<div class="form-group">
				<label>VIN (Numéro de châssis / 17 caractères) *</label>
				<input type="text" ng-model="vehicle.vin" maxlength="17" minlength="17" ng-change="validateVIN()" required style="text-transform:uppercase">
				<span class="hint" ng-if="vinValid">✅ VIN valide. Pays d'origine détecté: {{declaration.dispatch_country || '...'}}</span>
				<span class="error" ng-if="vinValid === false">❌ Le format du VIN est invalide (17 caractères alphanumériques, sans I, O, Q).</span>
			</div>
			<div class="form-row">
				<div class="form-group">
					<label>Marque *</label>
					<input type="text" ng-model="vehicle.brand" required>
				</div>
				<div class="form-group">
					<label>Modèle *</label>
					<input type="text" ng-model="vehicle.model" required>
				</div>
			</div>
			<div class="form-row">
				<div class="form-group">
					<label>Année de 1ère mise en circulation</label>
					<input type="number" ng-model="vehicle.year" min="1900" max="2026">
				</div>
				<div class="form-group">
					<label>Type de carburant *</label>
					<select ng-model="vehicle.fuel_type" ng-change="updateClassification()" required>
						<option value="">Sélectionner...</option>
						<option value="essence">Essence</option>
						<option value="diesel">Diesel</option>
						<option value="electrique">Électrique</option>
						<option value="hybride">Hybride</option>
						<option value="hybride_plugin">Hybride rechargeable</option>
					</select>
				</div>
			</div>
			<div class="form-row">
				<div class="form-group">
					<label>Cylindrée (cm³)</label>
					<input type="number" ng-model="vehicle.cylinder_capacity" ng-change="updateClassification()">
				</div>
				<div class="form-group">
					<label>Poids à vide (kg) *</label>
					<input type="number" ng-model="vehicle.weight_empty" ng-change="updateClassification()" required>
				</div>
				<div class="form-group">
					<label>Poids total autorisé (kg) *</label>
					<input type="number" ng-model="vehicle.weight_total" ng-change="updateClassification()" required>
				</div>
			</div>
			<div class="classification-info" ng-if="classification">
				<h4>📋 Classification douanière estimée</h4>
				<p>
					<strong>Code TARES:</strong> {{ classification.commodity_code }} - {{ classification.description }}
				</p>
				<div ng-if="classification.needs_user_selection">
					<p>
						<strong>Sélectionnez la clé statistique: *</strong>
					</p>
					<label ng-repeat="(key, desc) in classification.available_keys" class="radio-label">
						<input type="radio" ng-model="vehicle.statistical_key" ng-value="key" required>
						{{ key }} - {{ desc }}
					</label>
				</div>
				<p ng-if="!classification.needs_user_selection">
					<strong>Clé statistique:</strong> {{ vehicle.statistical_key }} - {{ classification.statistical_key_description }}
				</p>
			</div>
			<div class="form-actions">
				<button type="button" class="btn btn-secondary" ng-click="prevStep()">← Retour</button>
				<button type="button" class="btn btn-primary" ng-click="nextStep()" ng-disabled="vehicleForm.$invalid || (classification.needs_user_selection && !vehicle.statistical_key)">Continuer →</button>
			</div>
		</form>
		<div class="error" ng-if="ocrAttempted && !ocrLoading && !vehicle.vin && !vehicle.brand" style="text-align:center; margin-top:1rem;"> Analyse de votre carte grise impossible. Veuillez vérifier le fichier et réessayer, ou passer en saisie manuelle. </div>
	</div>
	<div ng-show="step === 3" class="step-content">
		<h2 style="text-align: center;">Informations pour la déclaration</h2>
		<form name="valuationForm" class="form" novalidate>
			<fieldset>
				<legend>Parties Impliquées</legend>
				<h4>Importateur / Destinataire</h4>
				<p class="disclaimer" style="margin-bottom: 1rem;">La personne ou l'entreprise qui importe le véhicule en Suisse.</p>
				<div class="form-row">
					<div class="form-group">
						<label>Nom ou raison sociale *</label>
						<input type="text" ng-model="parties.importer.name" required>
					</div>
					<div class="form-group">
						<label>Prénom</label>
						<input type="text" ng-model="parties.importer.firstname" ng-disabled="isCompany" placeholder="Pour les particuliers">
					</div>
				</div>
				<div class="form-group">
					<label>Adresse (Rue et numéro)</label>
					<input type="text" ng-model="parties.importer.address">
				</div>
				<div class="form-row">
					<div class="form-group">
						<label>NPA *</label>
						<input type="text" ng-model="parties.importer.zip" required>
					</div>
					<div class="form-group">
						<label>Ville *</label>
						<input type="text" ng-model="parties.importer.city" required>
					</div>
				</div>
				<div class="form-group">
					<label>Numéro IDE</label>
					<input type="text" ng-model="parties.importer.ide" placeholder="CHE222251936 pour particulier (défaut)">
				</div>
				<small>Par défaut, le Destinataire et le Déclarant sont identiques à l'Importateur.</small>
			</fieldset>
			<fieldset>
				<legend>Régime Douanier</legend>
				<div class="form-row">
					<div class="form-group">
						<label>Pays d'expédition *</label>
						<select ng-model="declaration.dispatch_country" ng-options="code as name for (code, name) in countries" required>
							<option value="">Sélectionner...</option>
						</select>
					</div>
					<div class="form-group">
						<label>Mode de transport *</label>
						<select ng-model="declaration.transport_mode" ng-options="mode.value as mode.label for mode in transportModes" required></select>
					</div>
				</div>
				<label class="checkbox-label">
					<input type="checkbox" ng-model="declaration.is_relocation"> Importation pour déménagement (Exemption TVA) </label>
				<label class="checkbox-label">
					<input type="checkbox" ng-model="declaration.is_iva_exempt"> Exonéré de l'impôt sur les véhicules automobiles (IVA / RG 660) </label>
			</fieldset>
			<fieldset>
				<legend>Transactions Financières</legend>
				<div class="form-row">
					<div class="form-group">
						<label>Prix d'achat du véhicule *</label>
						<input type="number" step="0.01" ng-model="declaration.purchase_price" ng-change="calculateTotal()" required>
					</div>
					<div class="form-group">
						<label>Monnaie de la facture *</label>
						<select ng-model="declaration.purchase_currency" ng-change="calculateTotal()" ng-options="code as code for (code, name) in currencies" required></select>
					</div>
				</div>
				<p>
					<strong>Coûts additionnels (dans la même devise)</strong>
				</p>
				<div class="form-row">
					<div class="form-group">
						<label>Frais de transport jusqu'à la frontière</label>
						<input type="number" step="0.01" ng-model="declaration.transport_cost" ng-change="calculateTotal()">
					</div>
					<div class="form-group">
						<label>Autres frais (ex: assurance)</label>
						<input type="number" step="0.01" ng-model="declaration.other_costs" ng-change="calculateTotal()">
					</div>
				</div>
				<div class="total-box" ng-if="vatValueCHF > 0">
					<h3>Valeur totale (base de calcul TVA/IVA): ~ {{ vatValueCHF | number:0 }} CHF</h3>
				</div>
			</fieldset>
			<div class="form-actions">
				<button type="button" class="btn btn-secondary" ng-click="prevStep()">← Retour</button>
				<button type="button" class="btn btn-primary" ng-click="generateDeclaration()" ng-disabled="valuationForm.$invalid || generating">
					<span ng-if="!generating">Soumettre la déclaration ✨</span>
					<span ng-if="generating">Soumission en cours...</span>
				</button>
			</div>
		</form>
	</div>
	<div ng-show="step === 4" class="step-content">
		<div ng-if="showProgress" class="progress-section">
			<div class="progress-bar-container">
				<div class="progress-bar" ng-style="{'width': submissionProgress + '%'}"></div>
			</div>
			<p>{{submissionStep}} ({{submissionProgress}}%)</p>
		</div>
	</div>
	<div ng-if="submissionResult" class="success-content">
		<div class="success-icon">✅</div>
		<h2>Soumission terminée avec succès !</h2>
		<p>Votre déclaration a été soumise au portail e-dec et les documents ont été récupérés.</p>
		<div class="next-steps-box">
			<h3>Numéro de déclaration : <strong>{{ submissionResult.declarationNumber }}</strong>
			</h3>
			<p style="margin-top:1rem;">Veuillez télécharger les documents ci-dessous. Ils vous seront demandés lors du passage en douane.</p>
			<div class="form-actions" style="justify-content: center; margin-top:1.5rem; gap: 10px;">
				<button class="btn btn-primary" ng-click="downloadPDF(submissionResult.listePath)"> 📥 Télécharger la Liste d'Importation </button>
				<button class="btn btn-primary" ng-click="downloadPDF(submissionResult.bulletinPath)"> 📥 Télécharger le Bulletin de Délivrance </button>
			</div>
			<hr style="margin: 2rem 0; border-top: 1px solid #ddd;">
			<h4 style="text-align: center;">Accès futur et partage</h4>
			<p style="text-align: center;"> Utilisez ce lien unique pour accéder à vos documents ultérieurement. </p>
			<div class="unique-link-container" style="text-align: center;">
				<a href="/download/{{ sessionToken }}" target="_blank" class="unique-link" style="word-break: break-all; display: block; margin-bottom: 10px;">
					{{ 'monsite.ch/download/' + sessionToken }}
				</a>
				<button class="btn btn-secondary" ng-click="copyLinkToClipboard('/download/' + sessionToken)"> 📋 Copier le lien d'accès </button>
			</div>
			<p style="margin-top:1.5rem; font-size:0.9rem; text-align:center;"> Les documents et ce lien d'accès **expireront dans 30 jours**. </p>
		</div>
		<div class="form-actions" style="justify-content: center; margin-top: 2rem;">
			<button class="btn btn-secondary" ng-click="resetForm()">Effectuer une nouvelle déclaration</button>
		</div>
	</div>
</div>
</div>
----------------------------------------------------
package.json

{
  "name": "edec-vehicles",
  "version": "1.1.0",
  "description": "Application e-dec Véhicules avec AngularJS et ExpressJS",
  "main": "server/server.js",
  "scripts": {
    "start": "node server/server.js",
    "dev": "nodemon server/server.js"
  },
  "dependencies": {
    "axios": "^1.6.2",
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "express-rate-limit": "^7.4.0",
    "express-validator": "^7.1.0",
    "helmet": "^7.1.0",
    "jsonwebtoken": "^9.0.2",
    "morgan": "^1.10.0",
    "multer": "^1.4.5-lts.1",
    "mysql2": "^3.6.5",
    "node-cron": "^3.0.3",
    "pdf-parse": "^1.1.1",
    "playwright": "^1.40.1",
    "sharp": "^0.33.4",
    "uuid": "^9.0.1",
    "winston": "^3.11.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.2"
  }
}
