console.log('🚀 1. Démarrage du serveur...');

import express from 'express';
import cors from 'cors';
import sql from 'mssql';
import jwt from 'jsonwebtoken';
import { ConfidentialClientApplication } from '@azure/msal-node';
import axios from 'axios';
import dotenv from 'dotenv';
import crypto from 'crypto';
import multer from 'multer';
import { Client } from '@microsoft/microsoft-graph-client';
import 'isomorphic-fetch';
import PDFDocument from 'pdfkit';
import { PDFDocument as PDFLib } from 'pdf-lib';

import { generateFicheSignee, preparerLignesPDF }
    from './ndfPdfGenerator.js';

console.log('✅ 2. Modules de base chargés');

dotenv.config();
console.log('✅ 3. Dotenv chargé');

const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });

const proxyCache = new Map();
const PROXY_TTL = 30 * 60 * 1000; // 10 → 30 min
const PROXY_MAX_SIZE = 200;        // 50 → 200 entrées

function getCached(url) {
    const entry = proxyCache.get(url);
    if (!entry) return null;
    if (Date.now() - entry.at > PROXY_TTL) { proxyCache.delete(url); return null; }
    return entry;
}

function setCache(url, buffer, contentType) {
    if (proxyCache.size >= PROXY_MAX_SIZE) {
        const oldest = [...proxyCache.entries()].sort((a, b) => a[1].at - b[1].at)[0];
        proxyCache.delete(oldest[0]);
    }
    proxyCache.set(url, { buffer, contentType, at: Date.now() });
}

const SHAREPOINT_CONFIG = {
    siteId: process.env.SHAREPOINT_SITE_ID,
    driveId: process.env.SHAREPOINT_DRIVE_ID,
    basePath: 'Notes de Frais',
};

process.on('uncaughtException', (error) => {
    console.error('\n❌❌❌ ERREUR NON CAPTURÉE ❌❌❌');
    console.error(error);
    console.error(error.stack);
});

process.on('unhandledRejection', (reason, promise) => {
    console.error('\n❌❌❌ PROMESSE REJETÉE ❌❌❌');
    console.error('Raison:', reason);
});

process.on('exit', (code) => {
    console.log(`\n⚠️ PROCESSUS EN COURS DE TERMINAISON - CODE: ${code}\n`);
});

console.log('✅ 4. Handlers d\'erreurs installés');

const app = express();
console.log('✅ 5. Express initialisé');

const PORT = process.env.PORT || 3024;
console.log(`✅ 6. Port configuré: ${PORT}`);

app.use(cors({
    origin: [
        'http://myndf.ensup-adm.net',
        'https://myndf.ensup-adm.net',
        'http://localhost:3025',
        'http://localhost:81'
    ],
    credentials: true,
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
console.log('✅ 7. Middlewares installés');

const dbConfig = {
    server: process.env.DB_SERVER || '192.168.0.3',
    user: process.env.DB_USER || 'ndf_app',
    password: process.env.DB_PASSWORD || 'P@ssw0rd2026!',
    database: process.env.DB_NAME || 'NDF',
    port: parseInt(process.env.DB_PORT) || 1433,
    options: {
        encrypt: true,
        trustServerCertificate: true,
        enableArithAbort: true,
        connectTimeout: 60000,
        requestTimeout: 60000,
        useUTC: false
    },
    pool: { max: 10, min: 0, idleTimeoutMillis: 30000 }
};

console.log('🔄 8. Test connexion SQL Server...');
let pool;

const AZURE_CONFIG = {
    tenantId: process.env.AZURE_TENANT_ID,
    clientId: process.env.AZURE_CLIENT_ID,
    clientSecret: process.env.AZURE_CLIENT_SECRET,
    groupId: process.env.AZURE_GROUP_ID || 'c1ea877c-6bca-4f47-bfad-f223640813a0'
};

async function initializeDatabase() {
    try {
        pool = await sql.connect(dbConfig);
        console.log('✅ 9. Connexion SQL Server réussie');
        console.log('   Server:', dbConfig.server);
        console.log('   User:', dbConfig.user);
        console.log('   Database:', dbConfig.database);
        console.log('   Port:', dbConfig.port);
    } catch (err) {
        console.error('❌ 9. ERREUR CONNEXION SQL SERVER:', err.message);
        throw err;
    }
}

initializeDatabase().catch(err => {
    console.error('❌ Impossible de démarrer le serveur:', err);
    process.exit(1);
});

const msalConfig = {
    auth: {
        clientId: process.env.AZURE_CLIENT_ID,
        authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
        clientSecret: process.env.AZURE_CLIENT_SECRET
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message) {
                if (process.env.NODE_ENV === 'development') console.log('[MSAL]', message);
            },
            piiLoggingEnabled: false,
            logLevel: 'Info',
        },
    }
};

const BAREME_KM_SERVER = {
    3: { t1: 0.529, t2_a: 0.316, t2_b: 1061, t3: 0.369 },
    4: { t1: 0.606, t2_a: 0.340, t2_b: 1330, t3: 0.408 },
    5: { t1: 0.636, t2_a: 0.356, t2_b: 1391, t3: 0.427 },
    6: { t1: 0.665, t2_a: 0.374, t2_b: 1457, t3: 0.448 },
    7: { t1: 0.697, t2_a: 0.394, t2_b: 1512, t3: 0.470 },
};

function getIndemniteKmServer(kmTotal, chevaux) {
    const cv = Math.min(Math.max(parseInt(chevaux) || 7, 3), 7);
    const b = BAREME_KM_SERVER[cv];
    if (!b || kmTotal <= 0) return 0;
    if (kmTotal <= 5000) return parseFloat((kmTotal * b.t1).toFixed(2));
    if (kmTotal <= 20000) return parseFloat((kmTotal * b.t2_a + b.t2_b).toFixed(2));
    return parseFloat((kmTotal * b.t3).toFixed(2));
}

function normalizeCampus(campus) {
    if (!campus) return null;
    const c = campus.toUpperCase();
    if (c.includes('SQY') || c.includes('SAINT') || c.includes('SQUY')) return 'SQY';
    if (c.includes('CGY') || c.includes('CERGY')) return 'CGY';
    if (c.includes('MRS') || c.includes('MARSEILLE')) return 'MRS';
    if (c.includes('NTE') || c.includes('NANTES')) return 'NTE';
    return null;
}

// ── Helper getTarifKm ────────────────────────────────────────────
async function getTarifKm() {
    try {
        const annee = new Date().getFullYear();
        const result = await pool.request()
            .input('annee', sql.Int, annee)
            .query(`
                SELECT TOP 1 tarifParKm
                FROM ParametresKm
                WHERE annee = @annee AND actif = 1
                ORDER BY DateCreation DESC
            `);
        if (result.recordset[0]?.tarifParKm) {
            return parseFloat(result.recordset[0].tarifParKm);
        }
        return 0.697; // Fallback 7 CV+
    } catch (e) {
        console.warn('⚠️ getTarifKm fallback 0.697:', e.message);
        return 0.697;
    }
}

async function getConfigDebiteur(campus = null) {
    try {
        const request = pool.request();
        let campusWhere = '';
        
        if (campus) {
            const campusCode = normalizeCampus(campus) || campus;
            request.input('campus', sql.NVarChar, campusCode);
            campusWhere = `AND (campus = @campus OR campus IS NULL)`;
        }

        const result = await request.query(`
            SELECT TOP 1 companyName, companyIban, companyBic,
                         companyAddress, companyCp, companyVille, companyPays, campus
            FROM ConfigDebiteurXML
            WHERE actif = 1 ${campusWhere}
            ORDER BY 
                CASE WHEN campus IS NOT NULL AND campus != '' THEN 0 ELSE 1 END ASC,
                DateModification DESC
        `);
        if (result.recordset.length) return result.recordset[0];
    } catch (e) {
        console.warn('⚠️ getConfigDebiteur fallback .env:', e.message);
    }
    return {
        companyName: process.env.COMPANY_NAME || 'ENSUP GROUP',
        companyIban: process.env.COMPANY_IBAN || 'FR0000000000000000000000000',
        companyBic: process.env.COMPANY_BIC || 'BNPAFRPP',
        companyAddress: process.env.COMPANY_ADDRESS || '',
        companyCp: process.env.COMPANY_CP || '',
        companyVille: process.env.COMPANY_VILLE || '',
        companyPays: process.env.COMPANY_PAYS || 'FR',
    };
}
// ── Helpers IBAN ─────────────────────────────────────────────────
// ── Helpers IBAN ─────────────────────────────────────────────────
function getIbanKey() {
    const key = process.env.IBAN_ENCRYPTION_KEY;
    if (!key) throw new Error('IBAN_ENCRYPTION_KEY manquante');
    const buf = Buffer.from(key, 'hex');
    if (buf.length !== 32) throw new Error(`IBAN_ENCRYPTION_KEY invalide: ${buf.length} octets (attendu: 32)`);
    return buf;
}

function encryptIban(iban) {
    const iv = crypto.randomBytes(12);
    const cipher = crypto.createCipheriv('aes-256-gcm', getIbanKey(), iv);
    const encrypted = Buffer.concat([cipher.update(iban, 'utf8'), cipher.final()]);
    const tag = cipher.getAuthTag();
    return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`;
}

function decryptIban(stored) {
    const [ivHex, tagHex, encHex] = stored.split(':');
    const decipher = crypto.createDecipheriv('aes-256-gcm', getIbanKey(), Buffer.from(ivHex, 'hex'));
    decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
    return decipher.update(Buffer.from(encHex, 'hex')) + decipher.final('utf8');
}

function validateIban(iban) {
    const lengths = { FR: 27, BE: 16, DE: 22, ES: 24, IT: 27 };
    const country = iban.slice(0, 2);
    if (lengths[country] && iban.length !== lengths[country]) return false;
    const rearranged = iban.slice(4) + iban.slice(0, 4);
    const numeric = rearranged.split('').map(c => isNaN(c) ? (c.charCodeAt(0) - 55).toString() : c).join('');
    let remainder = 0;
    for (const chunk of numeric.match(/.{1,9}/g)) {
        remainder = parseInt(remainder + chunk) % 97;
    }
    return remainder === 1;
}

function maskIban(iban) {
    if (!iban || iban.length < 8) return iban;
    return iban.slice(0, 4) + ' ' +
        iban.slice(4, -4).replace(/./g, '*').match(/.{1,4}/g).join(' ') +
        ' ' + iban.slice(-4);
}

function hashIban(iban) {
    return crypto.createHash('sha256').update(iban + process.env.IBAN_HASH_SALT).digest('hex');
}
// ✅ NOUVEAU — Charge les rôles depuis UtilisateurRoles (remplace CollaborateurAD.role)
async function getRolesForUser(collaborateurId) {
    try {
        const result = await pool.request()
            .input('id', sql.Int, collaborateurId)
            .query(`
                SELECT role
                FROM UtilisateurRoles
                WHERE collaborateur_id = @id AND actif = 1
            `);
        return result.recordset.map(r => r.role);
        // Retourne ex: ['Collaboratrice', 'Finance'] ou ['superUtilisateur']
    } catch (e) {
        console.warn('⚠️ getRolesForUser erreur:', e.message);
        return [];
    }
}

function formatDateParis(date) {
    if (!date) return '—';
    const d = new Date(date);
    return d.toLocaleString('fr-FR', {
        timeZone: 'Europe/Paris',
        day: '2-digit', month: '2-digit', year: 'numeric',
        hour: '2-digit', minute: '2-digit'
    });
}

const cca = new ConfidentialClientApplication(msalConfig);
console.log('✅ 10. MSAL configuré');

// ================================================
// 🔑 TOKEN MICROSOFT GRAPH
// ================================================
// Cache du token Graph (évite 1 appel HTTP Azure par opération)
let _graphTokenCache = null;
let _sharePointTokenCache = null;

async function getGraphToken() {
    const now = Date.now();
    if (_graphTokenCache && now < _graphTokenCache.expiresAt) {
        return _graphTokenCache.token;
    }

    try {
        console.log('🔑 Obtention nouveau token Graph...');

        if (!AZURE_CONFIG.tenantId || !AZURE_CONFIG.clientId || !AZURE_CONFIG.clientSecret) {
            throw new Error('Configuration Azure incomplète');
        }

        const params = new URLSearchParams({
            grant_type: 'client_credentials',
            client_id: AZURE_CONFIG.clientId,
            client_secret: AZURE_CONFIG.clientSecret,
            scope: 'https://graph.microsoft.com/.default'
        });

        const response = await axios.post(
            `https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`,
            params.toString(),
            { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
        );

        const token = response.data.access_token;
        _graphTokenCache = { token, expiresAt: now + 55 * 60 * 1000 }; // 55 min
        console.log('✅ Token Graph obtenu et mis en cache (55 min)');
        return token;

    } catch (error) {
        console.error('❌ Erreur obtention token:', error.message);
        return null;
    }
}

// ================================================
// 🔄 SYNCHRONISATION ENTRA ID
// ================================================
async function syncEntraIdUsers() {
    const syncResults = { processed: 0, inserted: 0, updated: 0, deactivated: 0, errors: [] };

    try {
        console.log('\n🔄 === DÉBUT SYNCHRONISATION ENTRA ID ===');

        const accessToken = await getGraphToken();
        if (!accessToken) { console.error('❌ Impossible d\'obtenir le token'); return syncResults; }
        console.log('✅ Token obtenu');

        const groupResponse = await axios.get(
            `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`,
            { headers: { Authorization: `Bearer ${accessToken}` } }
        );
        const groupName = groupResponse.data.displayName;
        console.log(`📋 Groupe : ${groupName}`);

        let allAzureMembers = [];
        let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle,officeLocation,accountEnabled&$top=999`;

        console.log('📥 Récupération des membres...');
        while (nextLink) {
            const membersResponse = await axios.get(nextLink, { headers: { Authorization: `Bearer ${accessToken}` } });
            allAzureMembers = allAzureMembers.concat(membersResponse.data.value);
            nextLink = membersResponse.data['@odata.nextLink'];
            if (nextLink) console.log(`   📄 ${allAzureMembers.length} membres récupérés...`);
        }

        console.log(`✅ ${allAzureMembers.length} membres trouvés`);

        const validMembers = allAzureMembers.filter(m => {
            if (!m.mail || m.mail.trim() === '') return false;
            if (m.accountEnabled === false) return false;
            return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(m.mail);
        });

        console.log(`✅ ${validMembers.length} membres valides`);

        const transaction = new sql.Transaction(pool);
        await transaction.begin();

        try {
            const azureEmails = new Set();
            validMembers.forEach(m => azureEmails.add(m.mail.toLowerCase().trim()));

            console.log('\n📝 Traitement des utilisateurs...');

            for (const m of validMembers) {
                try {
                    const emailClean = m.mail.toLowerCase().trim();
                    syncResults.processed++;

                    const request = new sql.Request(transaction);
                    request.input('email', sql.NVarChar, emailClean);
                    const result = await request.query(`
                        SELECT id, email, entraUserId, Actif FROM CollaborateurAD WHERE LOWER(email) = LOWER(@email)
                    `);

                    if (result.recordset.length > 0) {
                        const updateRequest = new sql.Request(transaction);
                        updateRequest.input('entraUserId', sql.NVarChar, m.id);
                        updateRequest.input('prenom', sql.NVarChar, m.givenName || '');
                        updateRequest.input('nom', sql.NVarChar, m.surname || '');
                        updateRequest.input('departement', sql.NVarChar, m.department || '');
                        updateRequest.input('fonction', sql.NVarChar, m.jobTitle || '');
                        updateRequest.input('campus', sql.NVarChar, m.officeLocation || '');
                        updateRequest.input('email', sql.NVarChar, emailClean);
                        await updateRequest.query(`
                            UPDATE CollaborateurAD SET
                                entraUserId = @entraUserId, prenom = @prenom, nom = @nom,
                                departement = @departement, fonction = @fonction, campus = @campus,
                                Actif = 1, dateMiseAJour = GETDATE(), DateModification = GETDATE()
                            WHERE LOWER(email) = LOWER(@email)
                        `);
                        syncResults.updated++;
                        console.log(`   ✓ Mis à jour : ${emailClean}`);
                    } else {
                        const insertRequest = new sql.Request(transaction);
                        insertRequest.input('entraUserId', sql.NVarChar, m.id);
                        insertRequest.input('prenom', sql.NVarChar, m.givenName || '');
                        insertRequest.input('nom', sql.NVarChar, m.surname || '');
                        insertRequest.input('email', sql.NVarChar, emailClean);
                        insertRequest.input('departement', sql.NVarChar, m.department || '');
                        insertRequest.input('fonction', sql.NVarChar, m.jobTitle || '');
                        insertRequest.input('campus', sql.NVarChar, m.officeLocation || '');
                        await insertRequest.query(`
                            INSERT INTO CollaborateurAD
                            (entraUserId, prenom, nom, email, departement, fonction, campus, role, service, Actif, DateCreation, DateModification, dateMiseAJour)
                            VALUES (@entraUserId, @prenom, @nom, @email, @departement, @fonction, @campus, 'Collaborateur', NULL, 1, GETDATE(), GETDATE(), GETDATE())
                        `);
                        syncResults.inserted++;
                        console.log(`   ✓ Créé : ${emailClean}`);
                    }
                } catch (userError) {
                    syncResults.errors.push({ email: m.mail, error: userError.message });
                    console.error(`   ❌ Erreur ${m.mail}:`, userError.message);
                }
            }

            console.log('\n🔍 Désactivation des comptes obsolètes...');
            if (azureEmails.size > 0) {
                const activeEmailsList = Array.from(azureEmails).map(e => `'${e}'`).join(',');
                const deactivateRequest = new sql.Request(transaction);
                const deactivateResult = await deactivateRequest.query(`
                    UPDATE CollaborateurAD SET Actif = 0, DateModification = GETDATE()
                    WHERE email IS NOT NULL AND email != ''
                      AND LOWER(email) NOT IN (${activeEmailsList})
                      AND (Actif = 1 OR Actif IS NULL)
                `);
                syncResults.deactivated = deactivateResult.rowsAffected[0];
                console.log(`   ✓ ${syncResults.deactivated} compte(s) désactivé(s)`);
            }

            await transaction.commit();

            console.log('\n📊 === RÉSUMÉ ===');
            console.log(`   Groupe: ${groupName}`);
            console.log(`   Total Entra: ${allAzureMembers.length}`);
            console.log(`   Valides: ${validMembers.length}`);
            console.log(`   Traités: ${syncResults.processed}`);
            console.log(`   Créés: ${syncResults.inserted}`);
            console.log(`   Mis à jour: ${syncResults.updated}`);
            console.log(`   Désactivés: ${syncResults.deactivated}`);
            console.log(`   Erreurs: ${syncResults.errors.length}`);

        } catch (error) {
            await transaction.rollback();
            throw error;
        }

    } catch (error) {
        console.error('\n❌ ERREUR SYNCHRONISATION:', error.message);
    }

    return syncResults;
}

// ================================================
// MIDDLEWARE JWT
// ================================================
const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    if (!token) return res.status(401).json({ error: 'Token manquant' });
    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;
        next();
    } catch (err) {
        return res.status(403).json({ error: 'Token invalide ou expiré' });
    }
};

// requireRole gère les rôles multiples (tableau req.user.roles)
const requireRole = (...rolesRequis) => (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: 'Non authentifié' });
    const hasRole = req.user.roles && req.user.roles.some(r => rolesRequis.includes(r));
    if (!hasRole) {
        return res.status(403).json({ error: `Accès refusé — rôle(s) requis : ${rolesRequis.join(' ou ')}` });
    }
    next();
};

// Helper : vérifie si l'utilisateur possède un rôle parmi une liste
const hasAnyRole = (user, ...roles) => user.roles && user.roles.some(r => roles.includes(r));

// ================================================
// ROUTES DE BASE
// ================================================
app.get('/', (req, res) => {
    res.json({ message: 'API Gestion des Notes de Frais', version: '1.0.0', status: 'OK' });
});

app.get('/users-dev', async (req, res) => {
    try {
        const result = await pool.request().query(`
            SELECT id, email, nom, prenom, role, Actif FROM CollaborateurAD
            WHERE Actif = 1 OR Actif IS NULL ORDER BY nom, prenom
        `);
        res.json(result.recordset);
    } catch (error) {
        console.error('❌ Erreur /users-dev:', error);
        res.status(500).json({ error: error.message });
    }
});

// ✅ MODIFIÉ — login-dev avec rôles depuis UtilisateurRoles
app.post('/login-dev', async (req, res) => {
    try {
        const { accessToken } = req.body;
        if (!accessToken) return res.status(400).json({ error: 'Token d\'accès manquant' });

        let userInfo;
        try {
            const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
                headers: { 'Authorization': `Bearer ${accessToken}` }
            });
            if (!graphResponse.ok) throw new Error('Token invalide');
            userInfo = await graphResponse.json();
        } catch (graphError) {
            return res.status(401).json({ error: 'Token d\'accès invalide', details: graphError.message });
        }

        const userEmail = userInfo.mail || userInfo.userPrincipalName;
        const result = await pool.request()
            .input('email', sql.VarChar, userEmail)
            .query(`SELECT * FROM CollaborateurAD WHERE email = @email AND (Actif = 1 OR Actif IS NULL)`);

        if (!result.recordset.length)
            return res.status(404).json({ error: 'Utilisateur non trouvé ou compte désactivé' });

        const user = result.recordset[0];

        // ✅ Rôles depuis UtilisateurRoles au lieu de CollaborateurAD.role
        const userRoles = await getRolesForUser(user.id);
        if (!userRoles.length)
            return res.status(403).json({ error: 'Aucun rôle assigné dans UtilisateurRoles' });

        const rolesAutorises = [
            'Collaborateur', 'Collaboratrice',
            'Validateur', 'Validatrice',
            'Finance', 'VerificateurFinance', 'ValidateurFinance',
            'superUtilisateur'
        ];
        const hasAccess = userRoles.some(r => rolesAutorises.includes(r));
        if (!hasAccess)
            return res.status(403).json({ error: `Vos rôles n'ont pas accès à cette application` });

        const token = jwt.sign(
            {
                id: user.id,
                email: user.email,
                roles: userRoles,
                nom: user.nom,
                prenom: user.prenom,
                societe: user.societe,
                campus: normalizeCampus(user.campus),
            },
            process.env.JWT_SECRET,
            { expiresIn: '8h' }
        );

        res.json({
            token,
            user: {
                id: user.id,
                nom: user.nom,
                prenom: user.prenom,
                email: user.email,
                roles: userRoles,
                societe: user.societe,
                campus: user.campus,
            }
        });
    } catch (error) {
        res.status(500).json({ error: 'Erreur serveur inattendue', details: error.message });
    }
});

app.get('/auth/verify', authenticateToken, (req, res) => {
    res.json({ valid: true, user: req.user });
});

app.post('/auth/logout', authenticateToken, (req, res) => {
    res.json({ success: true, message: 'Déconnexion réussie' });
});

const pkceStore = new Map();

app.get('/api/auth/microsoft', (req, res) => {
    const tenantId = process.env.AZURE_TENANT_ID;
    const clientId = process.env.AZURE_CLIENT_ID;
    const state = Math.random().toString(36).substring(2, 15);
    const codeVerifier = crypto.randomBytes(32).toString('base64url');
    const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
    pkceStore.set(state, codeVerifier);
    setTimeout(() => pkceStore.delete(state), 10 * 60 * 1000);

    // ✅ Détecte automatiquement l'URL d'origine
    const redirectUri = process.env.OAUTH_REDIRECT_URI || 'https://myndf.ensup-adm.net/api/auth/callback';

    console.log('🔗 Redirect URI:', redirectUri);

    const params = new URLSearchParams({
        client_id: clientId,
        response_type: 'code',
        redirect_uri: redirectUri,
        response_mode: 'query',
        scope: 'openid profile email User.Read',
        state,
        prompt: 'select_account',
        code_challenge: codeChallenge,
        code_challenge_method: 'S256'
    });

    res.redirect(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`);
});


// ✅ MODIFIÉ — auth/callback avec rôles depuis UtilisateurRoles
app.get('/api/auth/callback', async (req, res) => {
    const { code, error, state } = req.query;
    if (error) return res.redirect(`/login?error=${error}&desc=${encodeURIComponent(req.query.error_description || '')}`);
    if (!code) return res.redirect(`/login?error=no_code`);

    const codeVerifier = pkceStore.get(state);
    if (!codeVerifier) return res.redirect(`/login?error=invalid_state`);
    pkceStore.delete(state);

    try {
        // ✅ Détecte automatiquement l'URL d'origine (même logique que /api/auth/microsoft)
        const redirectUri = process.env.OAUTH_REDIRECT_URI || 'https://myndf.ensup-adm.net/api/auth/callback';

        console.log('🔗 Callback Redirect URI:', redirectUri);

        const tokenResponse = await axios.post(
            `https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`,
            new URLSearchParams({
                client_id: AZURE_CONFIG.clientId,
                client_secret: AZURE_CONFIG.clientSecret,
                code,
                redirect_uri: redirectUri,  // ✅ URI détectée automatiquement
                grant_type: 'authorization_code',
                code_verifier: codeVerifier
            }),
            { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
        );

        const accessToken = tokenResponse.data.access_token;
        const userResponse = await axios.get('https://graph.microsoft.com/v1.0/me', {
            headers: { Authorization: `Bearer ${accessToken}` }
        });

        const userEmail = userResponse.data.mail || userResponse.data.userPrincipalName;
        const result = await pool.request()
            .input('email', sql.VarChar, userEmail)
            .query(`SELECT * FROM CollaborateurAD WHERE email = @email AND (Actif = 1 OR Actif IS NULL)`);

        if (!result.recordset.length) return res.redirect(`/login?error=user_not_found`);

        const user = result.recordset[0];

        // ✅ Rôles depuis UtilisateurRoles
        const userRoles = await getRolesForUser(user.id);
        if (!userRoles.length)
            return res.redirect(`/login?error=no_role_assigned`);

        const rolesAutorises = ['Collaborateur', 'Collaboratrice', 'Validateur', 'Validatrice', 'Finance', 'VerificateurFinance', 'ValidateurFinance', , 'superUtilisateur'];
        const hasAccess = userRoles.some(r => rolesAutorises.includes(r));
        if (!hasAccess) return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3025'}/login?error=unauthorized_role`);


        const jwtToken = jwt.sign(
            {
                id: user.id,
                email: user.email,
                roles: userRoles,
                nom: user.nom,
                prenom: user.prenom,
                societe: user.societe,
                campus: normalizeCampus(user.campus),
            },
            process.env.JWT_SECRET,
            { expiresIn: '8h' }
        );

        // ✅ Redirection vers le frontend avec le token
        res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3025'}/auth/callback?token=${jwtToken}`);

    } catch (error) {
        console.error('❌ Erreur callback OAuth:', error.message);
        if (error.response?.data) {
            console.error('Détails:', JSON.stringify(error.response.data, null, 2));
        }
        res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3025'}/login?error=auth_failed&details=${encodeURIComponent(error.message)}`);

    }
});

// GET /api/verificateur/notes — notes approuvées à vérifier
app.get('/api/verificateur/notes', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé VerificateurFinance' });
 
    try {
        const request = pool.request();
        let campusWhere = '';
        if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) {
            const campusCode = normalizeCampus(req.user.campus);
            if (campusCode) {
                request.input('campus', sql.NVarChar, `%${campusCode}%`);
                campusWhere = `AND c.campus LIKE @campus`;
            }
        }
 
        const result = await request.query(`
            SELECT n.*,
                   c.nom + ' ' + c.prenom AS collaborateur,
                   c.email AS collaborateurEmail,
                   c.departement, c.campus, c.societe,
                   v1.nom + ' ' + v1.prenom AS nomN1
                   
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id
           
            WHERE n.statut = 'approuve'
            ${campusWhere}
            ORDER BY n.DateCreation DESC
        `);
 
        const notes = result.recordset;
        if (!notes.length) return res.json([]);
 
        // Charger les lignes refusées actives en batch (utile si une note "approuve"
        // a déjà eu un refus archivé qu'on veut afficher en historique côté UI)
        const noteIds = notes.map(n => n.id).join(',');
        const refusedRows = await pool.request().query(`
            SELECT noteDeFraisId, ligneIndex, motif, statut, dateRefus
            FROM LignesRefusees
            WHERE noteDeFraisId IN (${noteIds}) AND statut = 'active'
        `);
 
        const refusedByNote = {};
        for (const r of refusedRows.recordset) {
            if (!refusedByNote[r.noteDeFraisId]) refusedByNote[r.noteDeFraisId] = [];
            refusedByNote[r.noteDeFraisId].push({ index: r.ligneIndex, motif: r.motif });
        }
 
        for (const note of notes) {
            note.lignesRefusees = refusedByNote[note.id] || [];
        }
 
        res.json(notes);
    } catch (error) {
        console.error('GET /api/verificateur/notes:', error.message);
        res.status(500).json({ error: error.message });
    }
});
// PUT /api/verificateur/notes/:id/verifier
app.put('/api/verificateur/notes/:id/verifier', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé VerificateurFinance' });

    const { commentaire, montantsModifies } = req.body;
    const noteId = parseInt(req.params.id);

    try {
        const noteResult = await pool.request()
            .input('id', sql.Int, noteId)
            .query(`
                SELECT n.*, c.prenom, c.nom, c.email, c.campus
                FROM NoteDeFrais n
                JOIN CollaborateurAD c ON c.id = n.collaborateurId
                WHERE n.id = @id AND n.statut = 'approuve'
            `);

        if (!noteResult.recordset.length)
            return res.status(404).json({ error: 'Note introuvable ou statut incompatible' });

        const note = noteResult.recordset[0];

        let lignesParsed = [];
        try { lignesParsed = JSON.parse(note.lignesJson || '[]'); } catch { }

        let montantFinalAjuste = parseFloat(note.montant);
        let lignesJsonModifie = note.lignesJson;

        if (Array.isArray(montantsModifies) && montantsModifies.length > 0) {
            for (const mod of montantsModifies) {
                const { ligneIndex, montantRetenu } = mod;
                if (
                    typeof ligneIndex === 'number' &&
                    ligneIndex >= 0 &&
                    ligneIndex < lignesParsed.length &&
                    typeof montantRetenu === 'number' &&
                    montantRetenu > 0
                ) {
                    const ligneOriginale = lignesParsed[ligneIndex];
                    const montantOriginalVal = parseFloat(ligneOriginale.montant) || montantRetenu;
                    const ratio = montantOriginalVal > 0 ? montantRetenu / montantOriginalVal : 1;

                    let tvaItemsMisAJour = ligneOriginale.tvaItems;
                    if (Array.isArray(ligneOriginale.tvaItems) && ligneOriginale.tvaItems.length > 0) {
                        tvaItemsMisAJour = ligneOriginale.tvaItems.map(item => {
                            const itemTTC = parseFloat((parseFloat(item.montantTTC) * ratio).toFixed(2));
                            const itemTau = parseFloat(item.taux) || 0;
                            const itemHT = itemTau > 0
                                ? parseFloat((itemTTC / (1 + itemTau / 100)).toFixed(2))
                                : itemTTC;
                            return { ...item, montantTTC: itemTTC.toFixed(2), montantHT: itemHT.toFixed(2) };
                        });
                    }

                    lignesParsed[ligneIndex] = {
                        ...ligneOriginale,
                        montant: montantRetenu.toFixed(2),
                        montantOriginal: ligneOriginale.montant,
                        montantAjuste: true,
                        tvaItems: tvaItemsMisAJour,
                    };
                }
            }

            const tarifKm = await getTarifKm();
            montantFinalAjuste = lignesParsed.reduce((total, l) => {
                const isKm = (l.categorie || '').toLowerCase().includes('kilom');
                if (isKm) {
                    const km = parseFloat(l.km) || 0;
                    const cv = parseInt(l.chevaux) || 7;
                    return total + getIndemniteKmServer(km, cv);
                }
                return total + (parseFloat(l.montant) || 0);
            }, 0);
            montantFinalAjuste = parseFloat(montantFinalAjuste.toFixed(2));
            lignesJsonModifie = JSON.stringify(lignesParsed);
        }

        const nbLignes = lignesParsed.length;
        const commentaireVerif = montantsModifies?.length > 0
            ? ``
            : commentaire || null;

        await pool.request()
            .input('id', sql.Int, noteId)
            .input('verificateurId', sql.Int, req.user.id)
            .input('commentaire', sql.NVarChar, commentaireVerif)
            .input('montant', sql.Decimal, montantFinalAjuste)
            .input('lignesJson', sql.NVarChar, lignesJsonModifie)
            .query(`
                UPDATE NoteDeFrais SET
                    statut = 'verifie',
                    verificateurFinanceId = @verificateurId,
                    dateVerification = GETDATE(),
                    commentaireVerification = @commentaire,
                    montant = @montant,
                    lignesJson = @lignesJson,
                    DateModification = GETDATE()
                WHERE id = @id
            `);

        if (Array.isArray(montantsModifies) && montantsModifies.length > 0) {
            for (const mod of montantsModifies) {
                const { ligneIndex, montantRetenu } = mod;
                if (typeof ligneIndex !== 'number' || montantRetenu <= 0) continue;
                const numPiece = ligneIndex + 1;
                const l = lignesParsed[ligneIndex] || {};
                const taux = parseFloat(l.tauxTVA) || 0;
                const ht = taux > 0 ? montantRetenu / (1 + taux / 100) : montantRetenu;

                await pool.request()
                    .input('noteId', sql.Int, noteId)
                    .input('numPiece', sql.Int, numPiece)
                    .input('montantTTC', sql.Decimal, montantRetenu)
                    .input('montantHT', sql.Decimal, parseFloat(ht.toFixed(2)))
                    .query(`
                        UPDATE LigneNoteDeFrais
                        SET montantTTC = @montantTTC, montantHT = @montantHT
                        WHERE noteDeFraisId = @noteId AND numPiece = @numPiece
                    `);
            }
        }

        const commentaireHisto = `${nbLignes} ligne${nbLignes > 1 ? 's' : ''} validée${nbLignes > 1 ? 's' : ''}${montantsModifies?.length > 0 ? ` — ${montantsModifies.length} montant(s) proratisé(s)` : ''}${commentaire ? ' — ' + commentaire : ''}`;
        await pool.request()
            .input('noteId', sql.Int, noteId)
            .input('validateurId', sql.Int, req.user.id)
            .input('action', sql.NVarChar, 'verifier')
            .input('commentaire', sql.NVarChar, commentaireHisto)
            .input('statut', sql.NVarChar, 'verifie')
            .query(`
                INSERT INTO HistoriqueValidation
                    (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, NouveauStatut, DateAction)
                VALUES
                    (@noteId, @validateurId, 'VERIF', @action, @commentaire, @statut, GETDATE())
            `);

        res.json({
            success: true,
            statut: 'verifie',
            montantAjuste: montantFinalAjuste,
            nbMontantsModifies: Array.isArray(montantsModifies) ? montantsModifies.length : 0,
        });

        setImmediate(async () => {
            try {
                // ── Récupérer historique des signatures ───────────────────
                const histResult = await pool.request()
                    .input('noteId', sql.Int, noteId)
                    .query(`
                        SELECT h.Niveau, h.Action, h.Commentaire, h.DateAction,
                               c.prenom + ' ' + c.nom AS nomPrenom
                        FROM HistoriqueValidation h
                        JOIN CollaborateurAD c ON c.id = h.ValidateurId
                        WHERE h.NoteDeFraisId = @noteId
                        ORDER BY h.DateAction ASC
                    `);

                const noteComplete = await pool.request()
                    .input('id', sql.Int, noteId)
                    .query(`
                        SELECT n.reference, n.libelle, n.montant, n.date,
                               n.categorie, n.lignesJson, n.fichiers, n.DateCreation,
                               c.prenom + ' ' + c.nom AS nomPrenom,
                               c.prenom AS collabPrenom, c.nom AS collabNom,
                               c.departement
                        FROM NoteDeFrais n
                        JOIN CollaborateurAD c ON c.id = n.collaborateurId
                        WHERE n.id = @id
                    `);

                const nd = noteComplete.recordset[0];
                if (!nd) return;

                const moisStr = (() => {
                    const d = new Date(nd.date);
                    const m = d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
                    return m.charAt(0).toUpperCase() + m.slice(1);
                })();

                const nomPrenom = nd.nomPrenom;
                const verificateurNom = `${req.user.prenom} ${req.user.nom}`;

                // ── Construire les signatures COLLAB + N1/N2 + VERIF ──────
                const signatures = [];
                signatures.push({
                    niveau: 'COLLAB', nomPrenom,
                    date: nd.DateCreation, action: 'soumettre', commentaire: null
                });
                for (const h of histResult.recordset) {
                    if (h.Niveau !== 'VERIF') {
                        signatures.push({
                            niveau: h.Niveau, nomPrenom: h.nomPrenom,
                            date: h.DateAction, action: h.Action, commentaire: h.Commentaire || null
                        });
                    }
                }
                signatures.push({
                    niveau: 'VERIF', nomPrenom: verificateurNom,
                    date: new Date(), action: 'verifier', commentaire: commentaireVerif || null
                });

                const noteDataPDF = {
                    reference: nd.reference,
                    nomPrenom,
                    mois: moisStr,
                    departement: nd.departement,
                    lignesJson: lignesJsonModifie,
                    tarifKm: await getTarifKm(),
                    statut: 'verifie',
                    montant: montantFinalAjuste,
                };

                let fichiersExistants = [];
                try { fichiersExistants = JSON.parse(nd.fichiers || '[]'); } catch { }

                const existingFolder = fichiersExistants[0]?.folderPath;
                const nomDossier = existingFolder
                    ? existingFolder.split('/')[1]
                    : `${nd.collabNom}_${nd.collabPrenom}`
                        .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
                        .replace(/[^a-zA-Z0-9_]/g, '_');
                const moisDossier = existingFolder
                    ? existingFolder.split('/')[2]
                    : `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;

                // ── Générer PDF fiche vérifiée (fiche seule) ──────────────
                try {
                    const pdfSigne = await generateFicheSignee(noteDataPDF, signatures);
                    const suffixe = montantsModifies?.length > 0 ? 'verifie-proratise' : 'verifie';

                    const signedResult = await uploadToSharePointHierarchique(
                        {
                            buffer: pdfSigne,
                            originalname: `${nd.reference}-${suffixe}.pdf`,
                            mimetype: 'application/pdf',
                            size: pdfSigne.length
                        },
                        nd.reference, nomDossier, moisDossier
                    );
                    fichiersExistants.push(signedResult);

                    await pool.request()
                        .input('id', sql.Int, noteId)
                        .input('fichiers', sql.NVarChar, JSON.stringify(fichiersExistants))
                        .query('UPDATE NoteDeFrais SET fichiers = @fichiers WHERE id = @id');

                    console.log(`✅ [ASYNC] PDF vérification ${suffixe} généré: ${signedResult.fileName}`);
                } catch (e) {
                    console.error('❌ [ASYNC] PDF vérification:', e.message);
                }

                // ── Régénérer le recap complet (fiche + justifs + 3 signatures) ──
                try {
                    const EXCLUS_RECAP = ['soumission', 'resoumission', 'signe', 'verifie', 'recap'];
                    const justifFiles = [];

                    for (const f of fichiersExistants) {
                        const fname = (f.fileName || '').toLowerCase();
                        if (EXCLUS_RECAP.some(kw => fname.includes(kw))) continue;
                        try {
                            const buf = await downloadFromSharePoint(f.uploadUrl);
                            const mimetype = fname.endsWith('.pdf') ? 'application/pdf'
                                : fname.endsWith('.png') ? 'image/png' : 'image/jpeg';
                            justifFiles.push({ buffer: buf, originalname: f.fileName, mimetype, size: buf.length });
                        } catch (e) { console.warn(`⚠️ Justif recap non récupérable: ${f.fileName}`, e.message); }
                    }

                    const recapBuffer = await generateRecapWithJustifs(noteDataPDF, justifFiles, signatures);
                    const recapResult = await uploadToSharePointHierarchique(
                        {
                            buffer: recapBuffer,
                            originalname: `${nd.reference}_recap.pdf`,
                            mimetype: 'application/pdf',
                            size: recapBuffer.length
                        },
                        nd.reference, nomDossier, moisDossier
                    );

                    // Récupérer la liste de fichiers à jour après upload du PDF verifie
                    const noteUpdated = await pool.request()
                        .input('id', sql.Int, noteId)
                        .query('SELECT fichiers FROM NoteDeFrais WHERE id = @id');

                    let fichiersAJour = [];
                    try { fichiersAJour = JSON.parse(noteUpdated.recordset[0]?.fichiers || '[]'); } catch { }

                    // Remplacer l'ancien _recap.pdf (garder recap-paiement intact)
                    const fichiersFinaux = fichiersAJour.filter(f => {
                        const fname = (f.fileName || '').toLowerCase();
                        return !fname.endsWith('_recap.pdf') || fname.includes('recap-paiement');
                    });
                    fichiersFinaux.push(recapResult);

                    await pool.request()
                        .input('id', sql.Int, noteId)
                        .input('fichiers', sql.NVarChar, JSON.stringify(fichiersFinaux))
                        .query('UPDATE NoteDeFrais SET fichiers = @fichiers WHERE id = @id');

                    console.log(`✅ [ASYNC] Recap PDF régénéré avec ${signatures.length} signature(s) (proratisé: ${Array.isArray(montantsModifies) && montantsModifies.length > 0}): ${recapResult.fileName}`);
                } catch (recapError) {
                    console.error('❌ [ASYNC] Régénération recap vérification:', recapError.message);
                }

                // ── Notifier les ValidateurFinance ────────────────────────
                try {
                    const campusNorm = normalizeCampus(note.campus);
                    const validateurs = await pool.request()
                        .input('campus', sql.NVarChar, campusNorm ? `%${campusNorm}%` : '%')
                        .query(`
                            SELECT c.id, c.email, c.prenom, c.nom
                            FROM CollaborateurAD c
                            JOIN UtilisateurRoles r ON r.collaborateur_id = c.id
                            WHERE r.role = 'ValidateurFinance' AND r.actif = 1
                              AND c.campus LIKE @campus AND c.Actif = 1
                        `);

                    const montantFormate = montantFinalAjuste.toFixed(2);
                    const montantOriginal = parseFloat(note.montant).toFixed(2);
                    const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net';
                    const aProratise = Array.isArray(montantsModifies) && montantsModifies.length > 0;

                    for (const val of validateurs.recordset) {
                        try {
                            await creerNotification({
                                destinataireId: val.id,
                                destinataireEmail: val.email,
                                type: 'paiement',
                                titre: `✅ Note vérifiée à valider — ${note.reference}`,
                                message: `${verificateurNom} a vérifié la note ${note.reference} (${montantFormate} €${aProratise ? ` — montant ajusté de ${montantOriginal} €` : ''}) de ${note.prenom} ${note.nom}. En attente de votre validation de paiement.`,
                                noteId
                            });
                        } catch (e) { console.error('Notif ValidateurFinance:', e.message); }

                        try {
                            await sendMailGraph(
                                val.email,
                                `✅ Note vérifiée — validation paiement requise : ${note.reference}`,
                                `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                                    <div style="background:linear-gradient(135deg,#7c3aed,#6d28d9);color:white;padding:24px;border-radius:12px 12px 0 0">
                                        <h2 style="margin:0">✅ Note vérifiée — paiement à valider</h2>
                                    </div>
                                    <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                                        <p>Bonjour <strong>${val.prenom} ${val.nom}</strong>,</p>
                                        <p>La note <strong>${note.reference}</strong> de <strong>${note.prenom} ${note.nom}</strong> a été vérifiée par <strong>${verificateurNom}</strong> et est prête pour le paiement.</p>
                                        ${aProratise ? `<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:12px;margin:14px 0">
                                            <strong style="color:#92400e">⚠️ Montants proratisés</strong><br>
                                            <span style="color:#78350f;font-size:13px">Montant original : ${montantOriginal} € → Montant retenu : <strong>${montantFormate} €</strong> (${montantsModifies.length} repas plafonné${montantsModifies.length > 1 ? 's' : ''} à 25 €/pers.)</span>
                                        </div>` : ''}
                                        ${commentaire ? `<p style="background:#f1f5f9;padding:12px;border-radius:8px;border-left:4px solid #7c3aed">💬 ${commentaire}</p>` : ''}
                                        <div style="text-align:center;margin-top:24px">
                                            <a href="${frontendUrl}" style="background:linear-gradient(135deg,#7c3aed,#6d28d9);color:white;padding:14px 32px;text-decoration:none;border-radius:8px;font-weight:700;display:inline-block">Valider le paiement →</a>
                                        </div>
                                    </div>
                                </div>`
                            );
                        } catch (e) { console.error('Email ValidateurFinance:', e.message); }
                    }

                    // Notifier le collaborateur
                    const montantAjusteMsg = aProratise
                        ? `Votre note ${note.reference} a été vérifiée. Montant retenu : ${montantFormate} € (ajusté depuis ${montantOriginal} € — plafonnement repas à 25 €/pers.).`
                        : `Votre note ${note.reference} (${montantFormate} €) a été vérifiée et est en attente de validation du paiement.`;

                    try {
                        await creerNotification({
                            destinataireId: note.collaborateurId,
                            destinataireEmail: note.email,
                            type: 'paiement',
                            titre: `Note ${note.reference} ${aProratise ? 'vérifiée — montant ajusté' : 'en cours de traitement'}`,
                            message: montantAjusteMsg,
                            noteId
                        });

                        if (aProratise) {
                            await sendMailGraph(
                                note.email,
                                `ℹ️ Montant ajusté — Note ${note.reference}`,
                                `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                                    <div style="background:linear-gradient(135deg,#f59e0b,#d97706);color:white;padding:24px;border-radius:12px 12px 0 0">
                                        <h2 style="margin:0">ℹ️ Montant de votre note ajusté</h2>
                                    </div>
                                    <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                                        <p>Bonjour <strong>${note.prenom} ${note.nom}</strong>,</p>
                                        <p>Votre note <strong>${note.reference}</strong> a été vérifiée par la Finance. Certains frais de repas ont été plafonnés à 25 €/personne conformément à la politique de l'entreprise.</p>
                                        <div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:16px 0">
                                            <table style="width:100%;border-collapse:collapse;font-size:13px">
                                                <tr><td style="color:#64748b;padding:4px 0;width:160px">Montant soumis</td><td style="font-weight:700;text-decoration:line-through;color:#6b7280">${montantOriginal} €</td></tr>
                                                <tr><td style="color:#64748b;padding:4px 0">Montant retenu</td><td style="font-weight:800;color:#15803d;font-size:15px">${montantFormate} €</td></tr>
                                                <tr><td style="color:#64748b;padding:4px 0">Ajustements</td><td style="color:#92400e">${montantsModifies.length} ligne${montantsModifies.length > 1 ? 's' : ''} de repas plafonnée${montantsModifies.length > 1 ? 's' : ''}</td></tr>
                                            </table>
                                        </div>
                                        <p style="font-size:13px;color:#64748b">Le plafond légal pour les frais de repas est de 25 € par personne. Les montants ont été ajustés en conséquence.</p>
                                    </div>
                                </div>`
                            );
                        }
                    } catch (e) { console.error('Notif collab vérification:', e.message); }

                } catch (e) {
                    console.error('❌ [ASYNC] Notifications vérification:', e.message);
                }

            } catch (e) {
                console.error('❌ [ASYNC] PDF vérification général:', e.message);
            }
        });

    } catch (error) {
        console.error('Erreur PUT /verificateur/notes/:id/verifier:', error.message);
        res.status(500).json({ error: error.message });
    }
});

app.post('/api/verificateur/notes/:id/refuser', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé VerificateurFinance' });
 
    const noteId = parseInt(req.params.id);
    const { lignesRefusees, notifier = true } = req.body;
 
    if (!Array.isArray(lignesRefusees) || lignesRefusees.length === 0)
        return res.status(400).json({ error: 'Au moins une ligne refusée est requise' });
 
    // Validation : chaque entrée doit avoir index (number) + motif (string non vide)
    for (const r of lignesRefusees) {
        if (typeof r.index !== 'number' || r.index < 0)
            return res.status(400).json({ error: 'Chaque ligne refusée doit avoir un index (number) >= 0' });
        if (!r.motif || typeof r.motif !== 'string' || !r.motif.trim())
            return res.status(400).json({ error: `Motif manquant pour la ligne d'index ${r.index}` });
    }
 
    const transaction = new sql.Transaction(pool);
 
    try {
        // Récupérer la note + collab + N1 + N2 (avant transaction pour validation)
        const noteResult = await pool.request()
            .input('id', sql.Int, noteId)
            .query(`
                SELECT n.id, n.reference, n.libelle, n.montant, n.statut,
                       n.collaborateurId, n.lignesJson,
                       c.prenom, c.nom, c.email, c.campus,
                       v1.id AS n1Id, v1.email AS emailN1, v1.prenom AS prenomN1, v1.nom AS nomN1
                   
                FROM NoteDeFrais n
                JOIN CollaborateurAD c ON c.id = n.collaborateurId
                LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id
              
                WHERE n.id = @id AND n.statut = 'approuve'
            `);
 
        if (!noteResult.recordset.length)
            return res.status(404).json({ error: 'Note introuvable ou statut incompatible (doit être "approuve")' });
 
        const note = noteResult.recordset[0];
 
        // Parser les lignes pour récupérer libellé + catégorie au moment du refus
        let lignesData = [];
        try { lignesData = JSON.parse(note.lignesJson || '[]'); } catch { }
 
        // Vérifier que les index sont valides
        for (const r of lignesRefusees) {
            if (r.index >= lignesData.length)
                return res.status(400).json({ error: `Index ${r.index} hors limites (note a ${lignesData.length} lignes)` });
        }
 
        await transaction.begin();
 
        // ── 1. Archiver les anciens refus actifs (si re-refus après correction partielle)
        await new sql.Request(transaction)
            .input('noteId', sql.Int, noteId)
            .query(`
                UPDATE LignesRefusees
                SET statut = 'archive'
                WHERE noteDeFraisId = @noteId AND statut = 'active'
            `);
 
        // ── 2. Insérer les nouveaux refus
        for (const r of lignesRefusees) {
            const ligne = lignesData[r.index] || {};
            await new sql.Request(transaction)
                .input('noteId', sql.Int, noteId)
                .input('ligneIndex', sql.Int, r.index)
                .input('ligneLibelle', sql.NVarChar, ligne.libelle || ligne.categorie || `Ligne ${r.index + 1}`)
                .input('ligneCategorie', sql.NVarChar, ligne.categorie || null)
                .input('motif', sql.NVarChar, r.motif.trim())
                .input('verificateurId', sql.Int, req.user.id)
                .query(`
                    INSERT INTO LignesRefusees
                        (noteDeFraisId, ligneIndex, ligneLibelle, ligneCategorie, motif, verificateurId, dateRefus, statut)
                    VALUES
                        (@noteId, @ligneIndex, @ligneLibelle, @ligneCategorie, @motif, @verificateurId, GETDATE(), 'active')
                `);
        }
 
        // ── 3. Passer la note en 'refuse_verif'
        const commentaireSynth = lignesRefusees.map(r => {
            const ligne = lignesData[r.index] || {};
            const label = ligne.libelle || ligne.categorie || `Ligne ${r.index + 1}`;
            return `• ${label} — ${r.motif.trim()}`;
        }).join(' | ');
 
        await new sql.Request(transaction)
            .input('id', sql.Int, noteId)
            .input('verificateurId', sql.Int, req.user.id)
            .input('commentaire', sql.NVarChar, `${lignesRefusees.length} ligne${lignesRefusees.length > 1 ? 's' : ''} refusée${lignesRefusees.length > 1 ? 's' : ''} : ${commentaireSynth}`)
            .query(`
                UPDATE NoteDeFrais SET
                    statut = 'refuse_verif',
                    verificateurFinanceId = @verificateurId,
                    dateVerification = GETDATE(),
                    commentaireVerification = @commentaire,
                    DateModification = GETDATE()
                WHERE id = @id
            `);
 
        // ── 4. Historique
        await new sql.Request(transaction)
            .input('noteId', sql.Int, noteId)
            .input('validateurId', sql.Int, req.user.id)
            .input('commentaire', sql.NVarChar, commentaireSynth)
            .input('statut', sql.NVarChar, 'refuse_verif')
            .query(`
                INSERT INTO HistoriqueValidation
                    (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, NouveauStatut, DateAction)
                VALUES
                    (@noteId, @validateurId, 'VERIF', 'refuser', @commentaire, @statut, GETDATE())
            `);
 
        await transaction.commit();
 
        // ── 5. Notifications (en dehors de la transaction)
        const verificateurNom = `${req.user.prenom} ${req.user.nom}`;
        const montantFormate = parseFloat(note.montant).toFixed(2);
        const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net';
 
        // HTML : tableau récap des lignes refusées
        const tableLignesHtml = lignesRefusees.map(r => {
            const ligne = lignesData[r.index] || {};
            const label = ligne.libelle || ligne.categorie || `Ligne ${r.index + 1}`;
            const cat = ligne.categorie || '—';
            return `
                <tr style="border-bottom:1px solid #fecaca">
                    <td style="padding:10px 12px;font-size:12px;color:#64748b;width:60px;font-weight:700">${r.index + 1}</td>
                    <td style="padding:10px 12px;font-size:13px">
                        <div style="font-weight:700;color:#111827">${label}</div>
                        <div style="font-size:11px;color:#6b7280;margin-top:2px">${cat}</div>
                    </td>
                    <td style="padding:10px 12px;font-size:12px;color:#dc2626;font-style:italic">${r.motif.trim()}</td>
                </tr>`;
        }).join('');
 
        let notifiedCollab = false, notifiedN1 = false;
 
        if (notifier) {
            // Notif collaborateur
            try {
                await creerNotification({
                    destinataireId: note.collaborateurId,
                    destinataireEmail: note.email,
                    type: 'refus',
                    titre: `❌ Note ${note.reference} refusée par la Finance`,
                    message: `${lignesRefusees.length} ligne${lignesRefusees.length > 1 ? 's' : ''} refusée${lignesRefusees.length > 1 ? 's' : ''} sur votre note ${note.reference}. Vous devez corriger uniquement ces lignes et resoumettre.`,
                    noteId
                });
                notifiedCollab = true;
            } catch (e) { console.error('Notif BDD collab refus:', e.message); }
 
            try {
                await sendMailGraph(
                    note.email,
                    `❌ Note refusée — corrections demandées : ${note.reference}`,
                    `<div style="font-family:Arial,sans-serif;max-width:640px;margin:0 auto">
                        <div style="background:linear-gradient(135deg,#ef4444,#dc2626);color:white;padding:24px;border-radius:12px 12px 0 0">
                            <h2 style="margin:0;font-size:18px">❌ Votre note a été refusée par la Finance</h2>
                            <p style="margin:8px 0 0;opacity:.9;font-size:13px">${lignesRefusees.length} ligne${lignesRefusees.length > 1 ? 's' : ''} à corriger</p>
                        </div>
                        <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                            <p>Bonjour <strong>${note.prenom} ${note.nom}</strong>,</p>
                            <p>Votre note <strong>${note.reference}</strong> (${montantFormate} €) a été refusée par <strong>${verificateurNom}</strong> (Vérificateur Finance).</p>
 
                            <div style="background:#fff;border:1.5px solid #fecaca;border-radius:8px;overflow:hidden;margin:20px 0">
                                <div style="background:#fef2f2;padding:10px 14px;border-bottom:1px solid #fecaca">
                                    <span style="font-size:12px;font-weight:700;color:#991b1b;text-transform:uppercase;letter-spacing:.5px">
                                        Lignes à corriger (${lignesRefusees.length})
                                    </span>
                                </div>
                                <table style="width:100%;border-collapse:collapse">
                                    <thead>
                                        <tr style="background:#fafafa">
                                            <th style="padding:8px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">N°</th>
                                            <th style="padding:8px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">Ligne</th>
                                            <th style="padding:8px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">Motif</th>
                                        </tr>
                                    </thead>
                                    <tbody>${tableLignesHtml}</tbody>
                                </table>
                            </div>
 
                            <div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:16px;margin:16px 0">
                                <div style="font-size:13px;font-weight:700;color:#1e40af;margin-bottom:8px">📝 Que faire maintenant ?</div>
                                <ol style="margin:0;padding-left:18px;font-size:13px;color:#1d4ed8;line-height:2">
                                    <li>Connectez-vous à la plateforme NDF</li>
                                    <li>Ouvrez la note <strong>${note.reference}</strong></li>
                                    <li>Corrigez <strong>uniquement les lignes listées ci-dessus</strong></li>
                                    <li>Resoumettez la note (les lignes validées sont conservées)</li>
                                </ol>
                            </div>
 
                            <div style="text-align:center;margin-top:28px">
                                <a href="${frontendUrl}" style="background:linear-gradient(135deg,#6366f1,#4f46e5);color:white;padding:14px 36px;text-decoration:none;border-radius:8px;font-weight:700;display:inline-block">
                                    ✏️ Corriger ma note →
                                </a>
                            </div>
                        </div>
                    </div>`
                );
            } catch (e) { console.error('Email collab refus:', e.message); }
 
            // Notif N1
            if (note.n1Id && note.emailN1) {
                try {
                    await creerNotification({
                        destinataireId: note.n1Id,
                        destinataireEmail: note.emailN1,
                        type: 'refus',
                        titre: `⚠️ Note ${note.reference} refusée par la Finance`,
                        message: `La note ${note.reference} de ${note.prenom} ${note.nom} a été refusée par ${verificateurNom} (${lignesRefusees.length} ligne${lignesRefusees.length > 1 ? 's' : ''} à corriger).`,
                        noteId
                    });
                    notifiedN1 = true;
                } catch (e) { console.error('Notif BDD N1 refus:', e.message); }
 
                try {
                    await sendMailGraph(
                        note.emailN1,
                        `⚠️ Note ${note.reference} refusée par la Finance`,
                        `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                            <div style="background:linear-gradient(135deg,#f59e0b,#d97706);color:white;padding:20px;border-radius:12px 12px 0 0">
                                <h2 style="margin:0">⚠️ Note refusée par la Finance</h2>
                            </div>
                            <div style="padding:20px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                                <p>Bonjour <strong>${note.prenomN1} ${note.nomN1}</strong>,</p>
                                <p>La note <strong>${note.reference}</strong> de <strong>${note.prenom} ${note.nom}</strong> que vous aviez validée a été refusée par <strong>${verificateurNom}</strong> (Vérificateur Finance).</p>
                                <div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:14px;margin:16px 0">
                                    <div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:6px">${lignesRefusees.length} ligne${lignesRefusees.length > 1 ? 's' : ''} à corriger</div>
                                    <table style="width:100%;border-collapse:collapse;font-size:12px;color:#78350f">
                                        ${lignesRefusees.map(r => {
                            const ligne = lignesData[r.index] || {};
                            const label = ligne.libelle || ligne.categorie || `Ligne ${r.index + 1}`;
                            return `<tr><td style="padding:4px 0;font-weight:600">${label}</td><td style="padding:4px 0 4px 12px;font-style:italic">${r.motif.trim()}</td></tr>`;
                        }).join('')}
                                    </table>
                                </div>
                                <p style="font-size:13px;color:#64748b">${note.prenom} ${note.nom} a été notifié et doit corriger uniquement les lignes listées.</p>
                            </div>
                        </div>`
                    );
                } catch (e) { console.error('Email N1 refus:', e.message); }
            }
        }
 
        res.json({
            success: true,
            statut: 'refuse_verif',
            nbLignesRefusees: lignesRefusees.length,
            notifiedCollab,
            notifiedN1
        });
 
    } catch (error) {
        try { await transaction.rollback(); } catch { }
        console.error('Erreur POST /verificateur/notes/:id/refuser:', error.message);
        res.status(500).json({ error: error.message });
    }
});

app.get('/api/verificateur/historique', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès refusé' });
 
    try {
        const request = pool.request().input('verificateurId', sql.Int, req.user.id);
 
        let campusWhere = '';
        if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) {
            const campusCode = normalizeCampus(req.user.campus);
            if (campusCode) {
                request.input('campus', sql.NVarChar, `%${campusCode}%`);
                campusWhere = 'AND c.campus LIKE @campus';
            }
        }
 
        const result = await request.query(`
            SELECT 
                n.id, n.reference, n.libelle, n.montant, n.statut,
                n.dateVerification, n.commentaireVerification,
                n.lignesJson, n.fichiers,
                c.nom + ' ' + c.prenom AS collaborateur,
                c.campus, c.departement
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            WHERE n.verificateurFinanceId = @verificateurId
              AND n.statut IN ('verifie', 'paiementenattente', 'payee', 'refuse_verif',
                               'refuse_verif_archive', 'non_conforme_verif', 'non_conforme_archive')
              ${campusWhere}
            ORDER BY n.dateVerification DESC
        `);
 
        const notes = result.recordset;
        if (!notes.length) return res.json([]);
 
        // Récupérer toutes les lignes refusées en un seul appel
        const noteIds = notes.map(n => n.id).join(',');
        const refusedRows = await pool.request().query(`
            SELECT noteDeFraisId, ligneIndex, ligneLibelle, ligneCategorie, motif, dateRefus
            FROM LignesRefusees
            WHERE noteDeFraisId IN (${noteIds})
            ORDER BY noteDeFraisId, ligneIndex ASC
        `);
 
        const refusedByNote = {};
        for (const r of refusedRows.recordset) {
            if (!refusedByNote[r.noteDeFraisId]) refusedByNote[r.noteDeFraisId] = [];
            refusedByNote[r.noteDeFraisId].push({
                index: r.ligneIndex,
                motif: r.motif,
                ligneLibelle: r.ligneLibelle,
                ligneCategorie: r.ligneCategorie,
                dateRefus: r.dateRefus
            });
        }
 
        const enriched = notes.map(row => {
            let nbLignes = 0;
            try { nbLignes = (JSON.parse(row.lignesJson || '[]')).length; } catch { }
            const lignesRefusees = refusedByNote[row.id] || [];
            const nbLignesRefusees = lignesRefusees.length;
            const nbLignesOk = Math.max(0, nbLignes - nbLignesRefusees);
            const isRefusee = row.statut === 'refuse_verif' || row.statut === 'refuse_verif_archive'
                || row.statut === 'non_conforme_verif' || row.statut === 'non_conforme_archive';
 
            return {
                ...row,
                statut: isRefusee ? 'REFUSEE' : 'VERIFIEE',
                nbLignes,
                nbLignesOk: isRefusee ? nbLignesOk : nbLignes,
                nbLignesRefusees,
                lignesRefusees,
                commentaire: row.commentaireVerification,
            };
        });
 
        res.json(enriched);
 
    } catch (error) {
        console.error('GET /api/verificateur/historique:', error.message);
        res.status(500).json({ error: error.message });
    }
});
// ================================================
// SYNC ENTRA — réservé Finance
// ================================================
app.post('/api/sync-entra', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ success: false, message: 'Accès refusé - réservé à la Finance' });
    const results = await syncEntraIdUsers();
    res.json({ success: true, message: 'Synchronisation terminée', stats: results });
});

app.get('/api/sync-status', authenticateToken, async (req, res) => {
    try {
        const totalDB = await pool.request().query(`
            SELECT COUNT(*) as total,
                SUM(CASE WHEN Actif = 1 THEN 1 ELSE 0 END) as actifs,
                SUM(CASE WHEN Actif = 0 THEN 1 ELSE 0 END) as inactifs
            FROM CollaborateurAD
        `);
        const derniers = await pool.request().query(`
            SELECT TOP 10 id, prenom, nom, email, role, Actif, dateCreation, dateMiseAJour
            FROM CollaborateurAD ORDER BY dateMiseAJour DESC
        `);
        let entraStatus = { connected: false };
        try {
            const token = await getGraphToken();
            if (token) {
                const groupResponse = await axios.get(
                    `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`,
                    { headers: { Authorization: `Bearer ${token}` } }
                );
                entraStatus = { connected: true, groupName: groupResponse.data.displayName, groupId: AZURE_CONFIG.groupId };
            }
        } catch (err) { entraStatus.error = err.message; }

        res.json({
            success: true,
            database: totalDB.recordset[0],
            entraId: entraStatus,
            derniers_utilisateurs: derniers.recordset
        });
    } catch (error) {
        res.status(500).json({ success: false, error: error.message });
    }
});

// ── GET tarif km actif ─────────────────────────────────────────────
app.get('/api/parametres/km', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request().query(`
            SELECT TOP 1 tarifParKm 
            FROM ParametresKm 
            WHERE actif = 1 
            ORDER BY annee DESC
        `);
        const tarif = result.recordset[0]?.tarifParKm ?? 0.697;
        res.json({ tarifKm: tarif });
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

// ── GET taux TVA actifs ────────────────────────────────────────────
app.get('/api/parametres/tva', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request().query(`
            SELECT taux, libelle, categorie
            FROM ParametresTVA
            WHERE actif = 1
              AND dateDebut <= GETDATE()
              AND (dateFin IS NULL OR dateFin >= GETDATE())
            ORDER BY taux ASC
        `);
        res.json(result.recordset);
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

// ── PUT tarif km — réservé Finance ────────────────────────────────
app.put('/api/parametres/km', authenticateToken, requireRole('Finance'), async (req, res) => {
    const { tarifParKm, annee } = req.body;
    if (!tarifParKm || isNaN(tarifParKm))
        return res.status(400).json({ error: 'Tarif invalide' });
    try {
        await pool.request().query(`UPDATE ParametresKm SET actif = 0 WHERE actif = 1`);
        await pool.request()
            .input('tarif', sql.Decimal(10, 4), parseFloat(tarifParKm))
            .input('annee', sql.Int, annee || new Date().getFullYear())
            .query(`INSERT INTO ParametresKm (tarifParKm, annee, actif, DateCreation) VALUES (@tarif, @annee, 1, GETDATE())`);
        res.json({ success: true, tarifParKm: parseFloat(tarifParKm) });
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

app.get('/api/parametres/bareme-km', authenticateToken, (req, res) => {
    res.json({
        baremes: [
            { chevaux: 3, label: '3 CV et moins', t1: 0.529, t2_a: 0.316, t2_b: 1061, t3: 0.369 },
            { chevaux: 4, label: '4 CV', t1: 0.606, t2_a: 0.340, t2_b: 1330, t3: 0.408 },
            { chevaux: 5, label: '5 CV', t1: 0.636, t2_a: 0.356, t2_b: 1391, t3: 0.427 },
            { chevaux: 6, label: '6 CV', t1: 0.665, t2_a: 0.374, t2_b: 1457, t3: 0.448 },
            { chevaux: 7, label: '7 CV et plus', t1: 0.697, t2_a: 0.394, t2_b: 1512, t3: 0.470 },
        ]
    });
});

// ✅ MODIFIÉ — profil expose les rôles depuis UtilisateurRoles
app.get('/api/profile', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('id', sql.Int, req.user.id)
            .query(`
              SELECT id, nom, prenom, email, role, fonction as poste,
           TypeContrat, DateEntree, campus, departement, societe,
           adresse_rue, adresse_cp, adresse_ville, adresse_pays
    FROM CollaborateurAD WHERE id = @id
            `);
        if (!result.recordset.length) return res.status(404).json({ error: 'Profil non trouvé' });
        const userProfile = result.recordset[0];

        // ✅ Rôles depuis UtilisateurRoles au lieu de CollaborateurAD.role
        const rolesDB = await getRolesForUser(req.user.id);
        userProfile.roles = rolesDB.length > 0 ? rolesDB : ['Collaborateur'];

        res.json(userProfile);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

app.put('/api/profile/adresse', authenticateToken, async (req, res) => {
    try {
        const { adresse_rue, adresse_cp, adresse_ville, adresse_pays, societe } = req.body;

        if (!adresse_rue || !adresse_cp || !adresse_ville || !adresse_pays)
            return res.status(400).json({ error: 'Tous les champs adresse sont obligatoires' });

        await pool.request()
            .input('id', sql.Int, req.user.id)
            .input('adresse_rue', sql.NVarChar, adresse_rue.trim())
            .input('adresse_cp', sql.NVarChar, adresse_cp.trim())
            .input('adresse_ville', sql.NVarChar, adresse_ville.trim())
            .input('adresse_pays', sql.NVarChar, adresse_pays.trim())
            .input('societe', sql.NVarChar, societe ? societe.trim() : null)
            .query(`
                UPDATE CollaborateurAD SET
                    adresse_rue   = @adresse_rue,
                    adresse_cp    = @adresse_cp,
                    adresse_ville = @adresse_ville,
                    adresse_pays  = @adresse_pays,
                    societe       = COALESCE(@societe, societe),
                    DateModification = GETDATE()
                WHERE id = @id
            `);

        res.json({ success: true });
    } catch (error) {
        console.error('PUT /api/profile/adresse :', error.message);
        res.status(500).json({ error: error.message });
    }
});

// ================================================
// HELPERS
// ================================================
async function genererReference(campus, nom, prenom) {
    const now = new Date();
    const jour = String(now.getDate()).padStart(2, '0');
    const mois = String(now.getMonth() + 1).padStart(2, '0');
    const annee = now.getFullYear();

    const campusCode = normalizeCampus(campus) || 'XXX';

    const nomClean = (nom || '').toUpperCase()
        .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
        .replace(/[^A-Z]/g, '');
    const prenomInitiale = (prenom || '').charAt(0).toUpperCase()
        .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
        .replace(/[^A-Z]/g, '');

    const nomPart = `${nomClean}.${prenomInitiale}`;

    const tx = new sql.Transaction(pool);
    await tx.begin();
    try {
        await new sql.Request(tx).query(`
            IF NOT EXISTS (SELECT 1 FROM NDFSequence WHERE annee = ${annee})
                INSERT INTO NDFSequence (annee, compteur) VALUES (${annee}, 0)
        `);
        const result = await new sql.Request(tx).query(`
            UPDATE NDFSequence SET compteur = compteur + 1
            OUTPUT INSERTED.compteur
            WHERE annee = ${annee}
        `);
        await tx.commit();
        const num = String(result.recordset[0].compteur).padStart(2, '0');
        // Format : NDF01-SQY-IMER.O-05-05-2026
        return `NDF${num}-${campusCode}-${nomPart}-${jour}-${mois}-${annee}`;
    } catch (e) {
        await tx.rollback();
        throw e;
    }
}
async function uploadToSharePoint(file, ndfReference, collaborateurNomPrenom) {
    const accessToken = await getGraphToken();
    if (!accessToken) throw new Error('Token Graph indisponible');
    const safeName = file.originalname.replace(/[^a-zA-Z0-9._\-]/g, '_');
    const fileName = `${ndfReference}_${safeName}`;
    const folderPath = `${SHAREPOINT_CONFIG.basePath}/${collaborateurNomPrenom}`;
    const uploadPath = `${folderPath}/${fileName}`;

    const res = await axios.put(
        `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${uploadPath}:/content`,
        file.buffer,
        { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': file.mimetype } }
    );
    return { fileName, uploadUrl: res.data.webUrl };
}
const downloadUrlCache = new Map(); // webUrl SP → { url, expiresAt }
async function downloadFromSharePoint(webUrl) {
    const accessToken = await getGraphToken();
    
    // ✅ Cache de l'URL de téléchargement direct (valable ~1h)
    const cached = downloadUrlCache.get(webUrl);
    if (cached && Date.now() < cached.expiresAt) {
        try {
            const fileRes = await axios.get(cached.url, {
                responseType: 'arraybuffer',
                timeout: 15000
            });
            return Buffer.from(fileRes.data);
        } catch {
            downloadUrlCache.delete(webUrl); // URL expirée, on refait
        }
    }

    const urlObj = new URL(webUrl);
    const fullPath = decodeURIComponent(urlObj.pathname);
    const marker = '/Shared Documents/';
    const markerAlt = '/Documents/';
    let relativePath = '';
    if (fullPath.includes(marker)) relativePath = fullPath.split(marker)[1];
    else if (fullPath.includes(markerAlt)) relativePath = fullPath.split(markerAlt)[1];
    else {
        const parts = fullPath.split('/sites/')[1]?.split('/');
        relativePath = parts?.slice(2).join('/') || '';
    }

    const metaRes = await axios.get(
        `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${relativePath}`,
        { headers: { Authorization: `Bearer ${accessToken}` }, timeout: 5000 }
    );
    
    const downloadUrl = metaRes.data['@microsoft.graph.downloadUrl'];
    if (!downloadUrl) throw new Error('downloadUrl absent de la réponse Graph');

    // Mettre en cache 50 min (les URLs pré-signées expirent vers 1h)
    downloadUrlCache.set(webUrl, { url: downloadUrl, expiresAt: Date.now() + 50 * 60 * 1000 });

    const fileRes = await axios.get(downloadUrl, {
        responseType: 'arraybuffer',
        timeout: 15000
    });
    return Buffer.from(fileRes.data);
}

async function uploadToSharePointHierarchique(file, noteRef, nomDossier, moisDossier) {
    const accessToken = await getGraphToken();
    if (!accessToken) throw new Error('Token Graph indisponible');

    const safeName = (file.originalname || 'fichier').replace(/[^a-zA-Z0-9._\-]/g, '_');
    const fileName = safeName.startsWith(noteRef) ? safeName : `${noteRef}_${safeName}`;

    // moisDossier format "2026-05" → annee="2026", mois="05"
    const [annee, mois] = (moisDossier || '').split('-');

    // Structure : Notes de Frais / Nom_Prenom / 2026 / 05 / NDF01-SQY-IMER.O-05-05-2026 /
    const folderPath = `${SHAREPOINT_CONFIG.basePath}/${nomDossier}/${annee}/${mois}/${noteRef}`;
    const uploadPath = `${folderPath}/${fileName}`;

    const res = await axios.put(
        `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${uploadPath}:/content`,
        file.buffer,
        {
            headers: {
                Authorization: `Bearer ${accessToken}`,
                'Content-Type': file.mimetype || 'application/octet-stream'
            },
            maxBodyLength: Infinity,
            maxContentLength: Infinity
        }
    );
    return { fileName, uploadUrl: res.data.webUrl, folderPath };
}

// ══════════════════════════════════════════════════════
// 📎 RÉCAP COMPLET (fiche PDF/A + justificatifs fusionnés)
// ══════════════════════════════════════════════════════
async function generateRecapWithJustifs(noteData, justifFiles, signaturesOpt) {
    const signatures = signaturesOpt || [
        { niveau: 'COLLAB', nomPrenom: noteData.nomPrenom || '', date: new Date(), action: 'soumettre', commentaire: null }
    ];
    const ficheBuffer = await generateFicheSignee(noteData, signatures);

    const finalPdf = await PDFLib.create();
    const fichePdf = await PDFLib.load(ficheBuffer);
    const fichePages = await finalPdf.copyPages(fichePdf, fichePdf.getPageIndices());
    fichePages.forEach(p => finalPdf.addPage(p));

    for (const file of justifFiles) {
        const mimetype = file.mimetype || '';
        if (mimetype === 'application/pdf') {
            try {
                const justifPdf = await PDFLib.load(file.buffer);
                const pages = await finalPdf.copyPages(justifPdf, justifPdf.getPageIndices());
                pages.forEach(p => finalPdf.addPage(p));
            } catch (e) { console.warn(`⚠️ PDF non intégrable: ${file.originalname} — ${e.message}`); }
        } else if (mimetype.startsWith('image/')) {
            try {
                const page = finalPdf.addPage([595, 842]);
                const img = mimetype === 'image/png' ? await finalPdf.embedPng(file.buffer) : await finalPdf.embedJpg(file.buffer);
                const maxW = 495, maxH = 742;
                const ratio = Math.min(maxW / img.width, maxH / img.height);
                const w = img.width * ratio, h = img.height * ratio;
                page.drawImage(img, { x: (595 - w) / 2, y: (842 - h) / 2, width: w, height: h });
            } catch (e) { console.warn(`⚠️ Image non intégrable: ${file.originalname} — ${e.message}`); }
        }
    }

    return Buffer.from(await finalPdf.save());
}

// GET /api/notes/:id/download-urls — préchargement des URLs directes
app.get('/api/notes/:id/download-urls', authenticateToken, async (req, res) => {
    try {
        const noteId = parseInt(req.params.id);
        
        // Récupérer les fichiers de la note
        const noteResult = await pool.request()
            .input('id', sql.Int, noteId)
            .query(`SELECT fichiers, lignesJson FROM NoteDeFrais WHERE id = @id`);
        
        if (!noteResult.recordset.length) return res.json({});
        
        const note = noteResult.recordset[0];
        let allUrls = [];
        
        // Fichiers globaux
        try {
            const fichiers = JSON.parse(note.fichiers || '[]');
            fichiers.forEach(f => { if (f.uploadUrl) allUrls.push(f.uploadUrl); });
        } catch { }
        
        // Fichiers des lignes
        try {
            const lignes = JSON.parse(note.lignesJson || '[]');
            lignes.forEach(l => {
                (l.qrFiles || []).forEach(f => { if (f.uploadUrl) allUrls.push(f.uploadUrl); });
            });
        } catch { }
        
        if (!allUrls.length) return res.json({});
        
        const accessToken = await getGraphToken();
        const result = {};
        
        // ✅ Graph Batch — résout toutes les URLs en UNE SEULE requête HTTP
        const batchRequests = allUrls.slice(0, 20).map((url, i) => {
            const urlObj = new URL(url);
            const fullPath = decodeURIComponent(urlObj.pathname);
            const marker = '/Shared Documents/';
            const markerAlt = '/Documents/';
            let relativePath = '';
            if (fullPath.includes(marker)) relativePath = fullPath.split(marker)[1];
            else if (fullPath.includes(markerAlt)) relativePath = fullPath.split(markerAlt)[1];
            else {
                const parts = fullPath.split('/sites/')[1]?.split('/');
                relativePath = parts?.slice(2).join('/') || '';
            }
            return {
                id: String(i),
                method: 'GET',
                url: `/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${relativePath}`
            };
        });
        
        const batchRes = await axios.post(
            'https://graph.microsoft.com/v1.0/$batch',
            { requests: batchRequests },
            { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }
        );
        
        for (const response of batchRes.data.responses) {
            const i = parseInt(response.id);
            const downloadUrl = response.body?.['@microsoft.graph.downloadUrl'];
            if (downloadUrl && allUrls[i]) {
                result[allUrls[i]] = downloadUrl;
                // Mettre en cache côté serveur aussi
                downloadUrlCache.set(allUrls[i], { 
                    url: downloadUrl, 
                    expiresAt: Date.now() + 50 * 60 * 1000 
                });
            }
        }
        
        res.json(result);
    } catch (error) {
        console.error('GET /api/notes/:id/download-urls:', error.message);
        res.json({}); // Fail silencieux — le client tombera sur le proxy normal
    }
});

// ══════════════════════════════════════════════════════
// POST /api/notes — Créer une note de frais (multi-lignes)
// ══════════════════════════════════════════════════════
app.post('/api/notes', authenticateToken, upload.any(), async (req, res) => {
    try {
        const { libelle, date, description, participants, nombreParticipants, lignes, qrNoteRef } = req.body;

        if (!libelle || !date || !lignes)
            return res.status(400).json({ error: 'Libellé, date et lignes sont obligatoires' });

        let lignesParsed;
        try {
            lignesParsed = typeof lignes === 'string' ? JSON.parse(lignes) : lignes;
        } catch {
            return res.status(400).json({ error: 'Format des lignes invalide' });
        }
        if (!Array.isArray(lignesParsed) || lignesParsed.length === 0)
            return res.status(400).json({ error: 'Au moins une ligne est obligatoire' });

        // ── Tarif KM ─────────────────────────────────────────────────────
        let tarifKm = await getTarifKm();
        try {
            const annee = new Date().getFullYear();
            const kmParam = await pool.request()
                .input('annee', sql.Int, annee)
                .query(`SELECT TOP 1 tarifParKm FROM ParametresKm WHERE annee = @annee AND actif = 1 ORDER BY DateCreation DESC`);
            if (kmParam.recordset[0]?.tarifParKm) tarifKm = parseFloat(kmParam.recordset[0].tarifParKm);
        } catch { }

        const lignesPDF = preparerLignesPDF(lignesParsed, tarifKm);
        const montantTTC = lignesPDF.reduce((s, l) => s + l.montantTTC, 0);
        const indemKm = lignesPDF.reduce((s, l) => s + l.indemniteKm, 0);
        const kmTotal = lignesPDF.reduce((s, l) => s + (l.km || 0), 0);
        const montantFinal = parseFloat((montantTTC + indemKm).toFixed(2));
        const montantFormate = montantFinal.toFixed(2);
        const isKmOnly = lignesPDF.every(l => l.montantTTC === 0 && l.indemniteKm > 0);
        const categorieNote = isKmOnly ? 'Kilométrique pur' : 'Multiple';

        // ── Collaborateur + hiérarchie ────────────────────────────────────
        const collabResult = await pool.request()
            .input('id', sql.Int, req.user.id)
            .query(`SELECT id, email, prenom, nom, departement, campus FROM CollaborateurAD WHERE id = @id`);
        if (!collabResult.recordset.length)
            return res.status(404).json({ error: 'Collaborateur non trouvé' });
        const collaborateur = collabResult.recordset[0];
        const nomPrenom = `${collaborateur.nom.toUpperCase()} ${collaborateur.prenom}`;

        const dateObj = new Date(date);
        const moisStr = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
        const moisCapitalized = moisStr.charAt(0).toUpperCase() + moisStr.slice(1);

        const hierarchie = await pool.request()
            .input('collabId', sql.Int, req.user.id)
            .query(`
                SELECT h.SuperieurId,
                    s1.email AS emailN1, s1.prenom AS prenomN1, s1.nom AS nomN1
                FROM HierarchieValidationNDF h
                LEFT JOIN CollaborateurAD s1 ON s1.id = h.SuperieurId
                WHERE h.CollaborateurId = @collabId
            `);
        const n1Id = hierarchie.recordset[0]?.SuperieurId ?? null;
        const emailN1 = hierarchie.recordset[0]?.emailN1 ?? null;
        const prenomN1 = hierarchie.recordset[0]?.prenomN1 ?? null;
        const nomN1 = hierarchie.recordset[0]?.nomN1 ?? null;

      const reference = await genererReference(collaborateur.campus, collaborateur.nom, collaborateur.prenom);
                const nomDossier = `${collaborateur.nom}_${collaborateur.prenom}`
                    .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
                    .replace(/[^a-zA-Z0-9_]/g, '_');
                const now = new Date();
        const moisDossier = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;

        // ── Collecte des fichiers (QR global) ────────────────────────────
        const allFiles = [...(req.files || [])];
        if (qrNoteRef) {
            const qrToken = await pool.request()
                .input('noteRef', sql.NVarChar, qrNoteRef)
                .query(`SELECT TOP 1 fichiers FROM UploadTokens WHERE noteRef = @noteRef AND used = 1 ORDER BY expiresAt DESC`);
            if (qrToken.recordset.length && qrToken.recordset[0].fichiers) {
                const qrFichiers = JSON.parse(qrToken.recordset[0].fichiers);
                const qrDownloads = await Promise.all(
                    qrFichiers.map(f =>
                        downloadFromSharePoint(f.uploadUrl)
                            .then(buf => ({
                                buffer: buf,
                                originalname: f.fileName,
                                mimetype: f.fileName?.endsWith('.pdf') ? 'application/pdf' : 'image/jpeg',
                                size: buf.length
                            }))
                            .catch(e => { console.warn('⚠️ QR global download fail:', e.message); return null; })
                    )
                );
                qrDownloads.filter(Boolean).forEach(f => allFiles.push(f));
            }
        }

        // ── QR par ligne : téléchargement + upload en parallèle ──────────
        await Promise.all(
            lignesParsed.map(async (ligne, i) => {
                const ligneQrRef = ligne.qrNoteRef;
                if (!ligneQrRef) return;
                try {
                    const qrLigne = await pool.request()
                        .input('noteRef', sql.NVarChar, ligneQrRef)
                        .query(`SELECT TOP 1 fichiers FROM UploadTokens WHERE noteRef = @noteRef AND used = 1 ORDER BY expiresAt DESC`);
                    if (!qrLigne.recordset.length || !qrLigne.recordset[0].fichiers) {
                        console.warn(`⚠️ QR ligne ${i} (${ligneQrRef}) : token introuvable ou non utilisé`);
                        return;
                    }
                    const qrFichiers = JSON.parse(qrLigne.recordset[0].fichiers);
                    if (!ligne.qrFiles) ligne.qrFiles = [];

                    await Promise.all(
                        qrFichiers.map(async f => {
                            try {
                                const buf = await downloadFromSharePoint(f.uploadUrl);
                                const fileObj = {
                                    buffer: buf,
                                    originalname: f.fileName,
                                    mimetype: f.fileName?.endsWith('.pdf') ? 'application/pdf' : 'image/jpeg',
                                    size: buf.length
                                };
                                allFiles.push(fileObj);

                                const uploaded = await uploadToSharePointHierarchique(fileObj, reference, nomDossier, moisDossier);
                                const dejaSauve = ligne.qrFiles.some(x => x.fileName === uploaded.fileName);
                                if (!dejaSauve) {
                                    ligne.qrFiles.push({ fileName: uploaded.fileName, uploadUrl: uploaded.uploadUrl });
                                }
                                console.log(`✅ QR ligne ${i} stocké: ${uploaded.fileName}`);
                            } catch (e) {
                                console.warn(`⚠️ QR ligne ${i} download/upload fail:`, e.message);
                            }
                        })
                    );
                } catch (e) {
                    console.warn(`⚠️ QR ligne ${i} erreur DB:`, e.message);
                }
            })
        );

        const lignesJsonFinal = JSON.stringify(lignesParsed);

        // ── Upload justificatifs en parallèle ────────────────────────────
        const fichiersUploades = (await Promise.all(
            allFiles.map(file =>
                uploadToSharePointHierarchique(file, reference, nomDossier, moisDossier)
                    .catch(e => { console.error(`❌ Upload justif ${file.originalname}:`, e.message); return null; })
            )
        )).filter(Boolean);

        const noteDataPDF = {
            reference,
            nomPrenom,
            mois: moisCapitalized,
            date,
            categorie: categorieNote,
            libelle,
            montant: montantFinal,
            lignes: lignesParsed,
            lignesJson: lignesJsonFinal,
            tarifKm,
            statut: 'enattente',
            departement: collaborateur.departement,
            participants: participants || null,
        };

        // ── Insérer la note en BDD ───────────────────────────────────────
        const insertResult = await pool.request()
            .input('reference', sql.NVarChar, reference)
            .input('collaborateurId', sql.Int, req.user.id)
            .input('libelle', sql.NVarChar, libelle)
            .input('montant', sql.Decimal, montantFinal)
            .input('date', sql.Date, new Date(date))
            .input('categorie', sql.NVarChar, categorieNote)
            .input('description', sql.NVarChar, description || null)
            .input('participants', sql.NVarChar, participants ? String(participants) : null)
            .input('nombreParticipants', sql.Int, nombreParticipants ? parseInt(nombreParticipants) : null)
            .input('sharepointUrl', sql.NVarChar, fichiersUploades[0]?.uploadUrl || null)
            .input('fichiers', sql.NVarChar, JSON.stringify(fichiersUploades))
            .input('statut', sql.NVarChar, 'enattente')
            .input('validateurN1Id', sql.Int, n1Id)
            .input('montantHT', sql.Decimal, null)
            .input('tauxTVA', sql.Decimal, null)
            .input('montantTVA21', sql.Decimal, null)
            .input('montantTVA55', sql.Decimal, null)
            .input('montantTVA10', sql.Decimal, null)
            .input('montantTVA20', sql.Decimal, null)
            .input('km', sql.Decimal, kmTotal || null)
            .input('indemniteKm', sql.Decimal, indemKm || null)
            .input('lignesJson', sql.NVarChar, lignesJsonFinal)
            .query(`
                INSERT INTO NoteDeFrais
                    (reference, collaborateurId, libelle, montant, date, categorie,
                     description, participants, nombreParticipants, sharepointUrl, fichiers,
                     statut, validateurN1Id,
                     montantHT, tauxTVA, montantTVA21, montantTVA55, montantTVA10, montantTVA20,
                     km, indemniteKm, lignesJson)
                OUTPUT INSERTED.id, INSERTED.reference
                VALUES
                    (@reference, @collaborateurId, @libelle, @montant, @date, @categorie,
                     @description, @participants, @nombreParticipants, @sharepointUrl, @fichiers,
                     @statut, @validateurN1Id,
                     @montantHT, @tauxTVA, @montantTVA21, @montantTVA55, @montantTVA10, @montantTVA20,
                     @km, @indemniteKm, @lignesJson)
            `);

        const noteCreee = insertResult.recordset[0];

        // ── Insérer les lignes ────────────────────────────────────────────
        for (let i = 0; i < lignesParsed.length; i++) {
            const l = lignesParsed[i];
            const pdf = lignesPDF[i];
            try {
                await pool.request()
                    .input('noteId', sql.Int, noteCreee.id)
                    .input('numPiece', sql.Int, i + 1)
                    .input('date', sql.Date, new Date(l.date))
                    .input('nature', sql.NVarChar, l.categorie || '')
                    .input('libelle', sql.NVarChar, l.libelle || '')
                    .input('km', sql.Decimal, pdf.km || null)
                    .input('montantTTC', sql.Decimal, pdf.montantTTC || null)
                    .input('tva21', sql.Decimal, pdf.tva21 || null)
                    .input('tva55', sql.Decimal, pdf.tva55 || null)
                    .input('tva10', sql.Decimal, pdf.tva10 || null)
                    .input('tva20', sql.Decimal, pdf.tva20 || null)
                    .input('montantHT', sql.Decimal, pdf.montantHT || null)
                    .input('tauxTVA', sql.Decimal, parseFloat(l.tauxTVA) || null)
                    .input('indemniteKm', sql.Decimal, pdf.indemniteKm || null)
                    .query(`
                        INSERT INTO LigneNoteDeFrais
                            (noteDeFraisId, numPiece, date, nature, libelle,
                             km, montantTTC, tva21, tva55, tva10, tva20,
                             montantHT, tauxTVA, indemniteKm)
                        VALUES
                            (@noteId, @numPiece, @date, @nature, @libelle,
                             @km, @montantTTC, @tva21, @tva55, @tva10, @tva20,
                             @montantHT, @tauxTVA, @indemniteKm)
                    `);
            } catch (e) { console.error(`❌ Insertion ligne ${i + 1}:`, e.message); }
        }

        console.log(`✅ Note créée: ${reference} (${lignesParsed.length} lignes) par ${collaborateur.email}`);

        res.status(201).json({
            success: true,
            id: noteCreee.id,
            reference: noteCreee.reference,
            fichiers: fichiersUploades,
            recapUrl: null,
            pending: true,
        });

        // ── Traitement lourd en arrière-plan ─────────────────────────────
        setImmediate(async () => {
            const fichiersAsync = [...fichiersUploades];
            try {
                console.log(`🔄 [ASYNC] PDF + emails pour ${reference}...`);
                const frontendUrl = process.env.FRONTEND_URL || 'myndf.ensup-adm.net';

                let ficheResult = null;
                try {
                    const fichePDF = await generateFicheSignee(
                        noteDataPDF,
                        [{ niveau: 'COLLAB', nomPrenom, date: new Date(), action: 'soumettre', commentaire: null }]
                    );
                    ficheResult = await uploadToSharePointHierarchique(
                        { buffer: fichePDF, originalname: `${reference}_soumission.pdf`, mimetype: 'application/pdf', size: fichePDF.length },
                        reference, nomDossier, moisDossier
                    );
                    fichiersAsync.push(ficheResult);
                    console.log(`✅ [ASYNC] Fiche soumission: ${ficheResult.fileName}`);
                } catch (e) { console.error('❌ [ASYNC] Fiche PDF:', e.message); }

                let recapUrl = null;
                try {
                    const recapBuffer = await generateRecapWithJustifs(noteDataPDF, allFiles);
                    const recapResult = await uploadToSharePointHierarchique(
                        { buffer: recapBuffer, originalname: `${reference}_recap.pdf`, mimetype: 'application/pdf', size: recapBuffer.length },
                        reference, nomDossier, moisDossier
                    );
                    fichiersAsync.push(recapResult);
                    recapUrl = recapResult.uploadUrl;
                    console.log(`✅ [ASYNC] Récap PDF: ${recapResult.fileName}`);
                } catch (e) { console.error('❌ [ASYNC] Récap PDF:', e.message); }

                try {
                    await pool.request()
                        .input('id', sql.Int, noteCreee.id)
                        .input('sharepointUrl', sql.NVarChar, recapUrl || ficheResult?.uploadUrl || null)
                        .input('fichiers', sql.NVarChar, JSON.stringify(fichiersAsync))
                        .query(`
                            UPDATE NoteDeFrais SET
                                sharepointUrl    = @sharepointUrl,
                                fichiers         = @fichiers,
                                DateModification = GETDATE()
                            WHERE id = @id
                        `);
                } catch (e) { console.error('❌ [ASYNC] UPDATE BDD fichiers:', e.message); }

                await Promise.all([
                    creerNotification({
                        destinataireId: collaborateur.id, destinataireEmail: null, type: 'soumission',
                        titre: `✅ Note ${reference} soumise avec succès`,
                        message: `Votre note ${reference} — ${libelle} (${montantFormate} €) a bien été reçue.`,
                        noteId: noteCreee.id
                    }).catch(e => console.error('❌ [ASYNC] Notif BDD collab:', e.message)),

                    n1Id ? creerNotification({
                        destinataireId: n1Id, destinataireEmail: emailN1, type: 'validation',
                        titre: `📋 Note à valider — ${reference}`,
                        message: `${collaborateur.prenom} ${collaborateur.nom} a soumis une note de ${montantFormate} € en attente de votre validation.`,
                        noteId: noteCreee.id
                    }).catch(e => console.error('❌ [ASYNC] Notif BDD N1:', e.message)) : Promise.resolve(),
                ]);

                await Promise.all([
                    sendMailGraph(
                        collaborateur.email,
                        `✅ Accusé de réception — Note ${reference}`,
                        `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                            <div style="background:linear-gradient(135deg,#10b981,#059669);color:white;padding:24px;border-radius:12px 12px 0 0">
                                <h2 style="margin:0">✅ Note de frais bien reçue</h2>
                            </div>
                            <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                                <p>Bonjour <strong>${collaborateur.prenom} ${collaborateur.nom}</strong>,</p>
                                <p>Votre note <strong>${reference}</strong> — ${libelle} (${montantFormate} €) a bien été enregistrée.</p>
                                ${nomN1 ? `<p>Validateur : <strong>${prenomN1} ${nomN1}</strong></p>` : ''}
                                ${recapUrl ? `<p><a href="${recapUrl}" style="color:#6366f1">📎 Voir le récapitulatif PDF</a></p>` : ''}
                                <div style="text-align:center;margin-top:24px">
                                    <a href="${frontendUrl}" style="background:#6366f1;color:white;padding:13px 28px;text-decoration:none;border-radius:8px;font-weight:700">Suivre mes notes →</a>
                                </div>
                            </div>
                        </div>`
                    ).catch(e => console.error('❌ [ASYNC] Email collab:', e.message)),

                    (n1Id && emailN1) ? sendMailGraph(
                        emailN1,
                        `📋 Note de frais à valider — ${reference}`,
                        `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                            <div style="background:linear-gradient(135deg,#6366f1,#4f46e5);color:white;padding:24px;border-radius:12px 12px 0 0">
                                <h2 style="margin:0">📋 Note de frais à valider</h2>
                            </div>
                            <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                                <p>Bonjour <strong>${prenomN1} ${nomN1}</strong>,</p>
                                <p><strong>${collaborateur.prenom} ${collaborateur.nom}</strong> a soumis la note <strong>${reference}</strong> — ${libelle} (<strong>${montantFormate} €</strong>).</p>
                                ${recapUrl ? `<p><a href="${recapUrl}" style="color:#6366f1">📎 Récapitulatif + justificatifs</a></p>` : ''}
                                <div style="text-align:center;margin-top:24px">
                                    <a href="${frontendUrl}" style="background:#6366f1;color:white;padding:14px 32px;text-decoration:none;border-radius:8px;font-weight:700">Valider sur la plateforme →</a>
                                </div>
                            </div>
                        </div>`
                    ).catch(e => console.error('❌ [ASYNC] Email N1:', e.message)) : Promise.resolve(),
                ]);

                console.log(`✅ [ASYNC] Traitement terminé pour ${reference}`);
            } catch (e) {
                console.error(`❌ [ASYNC] Erreur générale ${reference}:`, e.message);
            }
        });

    } catch (error) {
        console.error('❌ Erreur POST /api/notes:', error.message);
        res.status(500).json({ error: error.message });
    }
});

app.put('/api/notes/:id', authenticateToken, upload.any(), async (req, res) => {
    try {
        const noteId = parseInt(req.params.id);
        const userId = req.user.id;

        const noteCheck = await pool.request()
            .input('id', sql.Int, noteId)
            .input('collabId', sql.Int, userId)
            .query(`
                SELECT * FROM NoteDeFrais 
                WHERE id = @id AND collaborateurId = @collabId
                AND statut IN ('enattente', 'refuse', 'refuse_verif', 'non_conforme_verif', 'brouillon')

            `);

        if (!noteCheck.recordset.length)
            return res.status(403).json({ error: 'Note introuvable ou non modifiable (statut incompatible)' });

        const noteExist = noteCheck.recordset[0];

        // ✅ Détecter si correction (refusée ou non-conforme) → nouvelle note
        const estCorrection = noteExist.statut === 'refuse' 
            || noteExist.statut === 'refuse_verif' 
            || noteExist.statut === 'non_conforme_verif';

        const { libelle, date, description, participants, nombreParticipants, lignes } = req.body;

        if (!libelle || !date || !lignes)
            return res.status(400).json({ error: 'Libellé, date et lignes sont obligatoires' });

        let lignesParsed;
        try { lignesParsed = typeof lignes === 'string' ? JSON.parse(lignes) : lignes; }
        catch { return res.status(400).json({ error: 'Format des lignes invalide' }); }

        // ── Calculs communs ───────────────────────────────────────────────────
        const montantFinal = lignesParsed.reduce((total, l) => {
            const isKm = (l.categorie || '').toLowerCase().includes('kilom');
            if (isKm) {
                const km = parseFloat(l.km) || 0;
                const cv = parseInt(l.chevaux) || 7;
                return total + getIndemniteKmServer(km, cv);
            }
            const items = l.tvaItems || [{ montantTTC: l.montant, taux: l.tauxTVA }];
            return total + items.reduce((s, i) => s + (parseFloat(i.montantTTC) || 0), 0);
        }, 0);

        const indemKm = lignesParsed.reduce((s, l) => {
            if ((l.categorie || '').toLowerCase().includes('kilom'))
                return s + getIndemniteKmServer(parseFloat(l.km) || 0, parseInt(l.chevaux) || 7);
            return s;
        }, 0);

        const kmTotal = lignesParsed.reduce((s, l) =>
            s + ((l.categorie || '').toLowerCase().includes('kilom') ? (parseFloat(l.km) || 0) : 0), 0
        );

        const isKmOnly = lignesParsed.every(l => (l.categorie || '').toLowerCase().includes('kilom'));
        const categorieNote = isKmOnly ? 'Kilométrique pur' : 'Multiple';

        const collabResult = await pool.request()
            .input('id', sql.Int, userId)
            .query(`SELECT id, email, prenom, nom, departement, campus FROM CollaborateurAD WHERE id = @id`);
        const collaborateur = collabResult.recordset[0];
        const nomPrenom = `${collaborateur.nom.toUpperCase()} ${collaborateur.prenom}`;

        const dateObj = new Date(date);
        const moisStr = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
        const moisCapitalized = moisStr.charAt(0).toUpperCase() + moisStr.slice(1);
    const nomDossier = `${collaborateur.nom}_${collaborateur.prenom}`
                        .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
                        .replace(/[^a-zA-Z0-9_]/g, '_');
                    const now = new Date();
                    const moisDossier = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
                    const tarifKmVal = await getTarifKm();

        // ════════════════════════════════════════════════════════════════════
        // CAS 1 — Note REFUSÉE ou NON_CONFORME_VERIF → créer une NOUVELLE note
        // ════════════════════════════════════════════════════════════════════
        if (estCorrection) {

            // 1. Archiver l'ancienne note
            const statutArchive = noteExist.statut === 'non_conforme_verif'
                ? 'non_conforme_archive'
                : noteExist.statut === 'refuse_verif'
                    ? 'refuse_verif_archive'
                    : 'refuse_archive';

            await pool.request()
                .input('id', sql.Int, noteId)
                .input('statutArchive', sql.NVarChar, statutArchive)
                .query(`
                    UPDATE NoteDeFrais SET
                        statut = @statutArchive,
                        DateModification = GETDATE()
                    WHERE id = @id
                `);

            // 2. Nouvelle référence
            const nouvelleReference = await genererReference(collaborateur.campus, collaborateur.nom, collaborateur.prenom);

            // 3. Upload fichiers joints
            const allFiles = [...(req.files || [])];
            const fichiersUploades = [];
            for (const file of allFiles) {
                try {
                    const r = await uploadToSharePointHierarchique(file, nouvelleReference, nomDossier, moisDossier);
                    fichiersUploades.push(r);
                } catch (e) { console.error(`❌ Upload justif correction ${file.originalname}:`, e.message); }
            }

            // 4. Récupérer fichiers QR par ligne
            for (let i = 0; i < lignesParsed.length; i++) {
                const ligneQrRef = lignesParsed[i].qrNoteRef;
                if (!ligneQrRef) continue;
                try {
                    const qrLigne = await pool.request()
                        .input('noteRef', sql.NVarChar, ligneQrRef)
                        .query(`SELECT TOP 1 fichiers FROM UploadTokens WHERE noteRef = @noteRef AND used = 1 ORDER BY expiresAt DESC`);
                    if (qrLigne.recordset.length && qrLigne.recordset[0].fichiers) {
                        const qrFichiers = JSON.parse(qrLigne.recordset[0].fichiers);
                        for (const f of qrFichiers) {
                            try {
                                const buf = await downloadFromSharePoint(f.uploadUrl);
                                const r = await uploadToSharePointHierarchique(
                                    { buffer: buf, originalname: f.fileName, mimetype: f.fileName?.endsWith('.pdf') ? 'application/pdf' : 'image/jpeg', size: buf.length },
                                    nouvelleReference, nomDossier, moisDossier
                                );
                                fichiersUploades.push(r);
                            } catch (e) { console.warn(`⚠️ QR ligne ${i} download fail:`, e.message); }
                        }
                    }
                } catch (e) { console.warn(`⚠️ QR ligne ${i} erreur DB:`, e.message); }
            }

            // 5. Générer fiche PDF
            const noteDataPDF = {
                reference: nouvelleReference, nomPrenom, mois: moisCapitalized, date,
                categorie: categorieNote, libelle,
                montant: parseFloat(montantFinal.toFixed(2)),
                lignes: lignesParsed, lignesJson: JSON.stringify(lignesParsed),
                tarifKm: tarifKmVal, statut: 'enattente',
                departement: collaborateur.departement,
            };

            try {
                const commentairePDF = noteExist.statut === 'non_conforme_verif'
                    ? `Correction suite à non-conformité signalée sur ${noteExist.reference}`
                    : `Correction suite au refus de ${noteExist.reference}`;
                const fichePDF = await generateFicheSignee(noteDataPDF, [{
                    niveau: 'COLLAB', nomPrenom, date: new Date(),
                    action: 'soumettre', commentaire: commentairePDF
                }]);
                const ficheResult = await uploadToSharePointHierarchique(
                    { buffer: fichePDF, originalname: `${nouvelleReference}_soumission.pdf`, mimetype: 'application/pdf', size: fichePDF.length },
                    nouvelleReference, nomDossier, moisDossier
                );
                fichiersUploades.push(ficheResult);
            } catch (e) { console.error('❌ Fiche PDF correction:', e.message); }

            // 6. Hiérarchie
            const hierarchie = await pool.request()
                .input('collabId', sql.Int, userId)
                .query(`
                    SELECT h.SuperieurId,
                        s1.email AS emailN1, s1.prenom AS prenomN1, s1.nom AS nomN1
                    
                    FROM HierarchieValidationNDF h
                    LEFT JOIN CollaborateurAD s1 ON s1.id = h.SuperieurId
                   
                    WHERE h.CollaborateurId = @collabId
                `);
            const n1Id = hierarchie.recordset[0]?.SuperieurId ?? null;
          
            const emailN1 = hierarchie.recordset[0]?.emailN1 ?? null;
            const prenomN1 = hierarchie.recordset[0]?.prenomN1 ?? null;
            const nomN1 = hierarchie.recordset[0]?.nomN1 ?? null;

            // 7. Insérer nouvelle note
            const insertResult = await pool.request()
                .input('reference', sql.NVarChar, nouvelleReference)
                .input('collaborateurId', sql.Int, userId)
                .input('libelle', sql.NVarChar, libelle)
                .input('montant', sql.Decimal, parseFloat(montantFinal.toFixed(2)))
                .input('date', sql.Date, new Date(date))
                .input('categorie', sql.NVarChar, categorieNote)
                .input('description', sql.NVarChar, description || null)
                .input('participants', sql.NVarChar, participants ? String(participants) : null)
                .input('nombreParticipants', sql.Int, nombreParticipants ? parseInt(nombreParticipants) : null)
                .input('fichiers', sql.NVarChar, JSON.stringify(fichiersUploades))
                .input('statut', sql.NVarChar, 'enattente')
                .input('validateurN1Id', sql.Int, n1Id)
               
                .input('km', sql.Decimal, kmTotal || null)
                .input('indemniteKm', sql.Decimal, indemKm || null)
                .input('lignesJson', sql.NVarChar, JSON.stringify(lignesParsed))
                .input('noteRefuseeId', sql.Int, noteId)
                .query(`
                    INSERT INTO NoteDeFrais
                        (reference, collaborateurId, libelle, montant, date, categorie,
                         description, participants, nombreParticipants,
                         fichiers, statut, validateurN1Id,
                         km, indemniteKm, lignesJson, noteRefuseeId,
                         DateCreation, DateModification)
                    OUTPUT INSERTED.id, INSERTED.reference
                    VALUES
                        (@reference, @collaborateurId, @libelle, @montant, @date, @categorie,
                         @description, @participants, @nombreParticipants,
                         @fichiers, @statut, @validateurN1Id, 
                         @km, @indemniteKm, @lignesJson, @noteRefuseeId,
                         GETDATE(), GETDATE())
                `);

            const nouvelleNote = insertResult.recordset[0];

            // 8. Insérer lignes
            const lignesPDF = preparerLignesPDF(lignesParsed, tarifKmVal);
            for (let i = 0; i < lignesParsed.length; i++) {
                const l = lignesParsed[i];
                const pdf = lignesPDF[i] || {};
                const isKmLine = (l.categorie || '').toLowerCase().includes('kilom');
                const kmLine = parseFloat(l.km) || 0;
                const cvLine = parseInt(l.chevaux) || 7;
                const indemLine = isKmLine ? getIndemniteKmServer(kmLine, cvLine) : 0;
                const items = l.tvaItems || [{ montantTTC: l.montant, taux: l.tauxTVA }];
                const ttcLine = isKmLine ? indemLine : items.reduce((s, item) => s + (parseFloat(item.montantTTC) || 0), 0);
                try {
                    await pool.request()
                        .input('noteId', sql.Int, nouvelleNote.id)
                        .input('numPiece', sql.Int, i + 1)
                        .input('date', sql.Date, new Date(l.date))
                        .input('nature', sql.NVarChar, l.categorie || '')
                        .input('libelle', sql.NVarChar, l.libelle || '')
                        .input('km', sql.Decimal, isKmLine ? kmLine : null)
                        .input('montantTTC', sql.Decimal, ttcLine || null)
                        .input('tva21', sql.Decimal, pdf.tva21 || null)
                        .input('tva55', sql.Decimal, pdf.tva55 || null)
                        .input('tva10', sql.Decimal, pdf.tva10 || null)
                        .input('tva20', sql.Decimal, pdf.tva20 || null)
                        .input('montantHT', sql.Decimal, pdf.montantHT || null)
                        .input('tauxTVA', sql.Decimal, parseFloat(l.tauxTVA) || null)
                        .input('indemniteKm', sql.Decimal, indemLine || null)
                        .query(`
                            INSERT INTO LigneNoteDeFrais
                                (noteDeFraisId, numPiece, date, nature, libelle,
                                 km, montantTTC, tva21, tva55, tva10, tva20,
                                 montantHT, tauxTVA, indemniteKm)
                            VALUES
                                (@noteId, @numPiece, @date, @nature, @libelle,
                                 @km, @montantTTC, @tva21, @tva55, @tva10, @tva20,
                                 @montantHT, @tauxTVA, @indemniteKm)
                        `);
                } catch (e) { console.error(`❌ Insertion ligne correction ${i + 1}:`, e.message); }
            }

            // 9. Notifier N1
            if (n1Id && emailN1) {
                try {
                    const titreNotif = noteExist.statut === 'non_conforme_verif'
                        ? `📋 Note corrigée à valider — ${nouvelleReference}`
                        : `📋 Note corrigée à valider — ${nouvelleReference}`;
                    // Dans le PUT /api/notes/:id — CAS 1 correction, section "Notifier N1"
                    const msgNotif = `${collaborateur.prenom} ${collaborateur.nom} a resoumis une note corrigée.
Ancienne référence : ${noteExist.reference} (${statutArchive})
Nouvelle référence : ${nouvelleReference}
Montant : ${parseFloat(montantFinal.toFixed(2))} €`;

                    await creerNotification({
                        destinataireId: n1Id, destinataireEmail: emailN1,
                        type: 'validation', titre: titreNotif, message: msgNotif,
                        noteId: nouvelleNote.id
                    });

                    const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net';
                    const couleurHeader = noteExist.statut === 'non_conforme_verif'
                        ? 'linear-gradient(135deg,#f97316,#ea580c)'
                        : 'linear-gradient(135deg,#f59e0b,#d97706)';
                    const contexte = noteExist.statut === 'non_conforme_verif'
                        ? `suite à la non-conformité signalée sur <strong>${noteExist.reference}</strong>`
                        : `suite au refus de <strong>${noteExist.reference}</strong>`;

                    await sendMailGraph(emailN1, `📋 Note corrigée à valider — ${nouvelleReference}`,
                        `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                            <div style="background:${couleurHeader};color:white;padding:20px;border-radius:12px 12px 0 0">
                                <h2 style="margin:0">📋 Note corrigée — à valider</h2>
                            </div>
                            <div style="padding:20px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                                <p>Bonjour <strong>${prenomN1} ${nomN1}</strong>,</p>
                                <p><strong>${collaborateur.prenom} ${collaborateur.nom}</strong> a resoumis une note corrigée ${contexte}.</p>
                                <p>Nouvelle référence : <strong>${nouvelleReference}</strong> — <strong>${parseFloat(montantFinal.toFixed(2))} €</strong></p>
                                <div style="text-align:center;margin-top:24px">
                                    <a href="${frontendUrl}" style="background:#6366f1;color:white;padding:14px 32px;text-decoration:none;border-radius:8px;font-weight:700">Valider sur la plateforme →</a>
                                </div>
                            </div>
                        </div>`
                    );
                } catch (e) { console.error('❌ Notif N1 correction:', e.message); }
            }

            console.log(`✅ Note corrigée ${nouvelleReference} créée (remplace ${noteExist.reference} → ${statutArchive}) par ${collaborateur.email}`);
            return res.json({
                success: true,
                id: nouvelleNote.id,
                reference: nouvelleReference,
                statut: 'enattente',
                isCorrection: true,
                ancienneReference: noteExist.reference
            });
        }

        // ════════════════════════════════════════════════════════════════════
        // CAS 2 — Note EN ATTENTE → modifier sur place (comportement original)
        // ════════════════════════════════════════════════════════════════════

        const reference = noteExist.reference;
        const allFiles = [...(req.files || [])];
        let fichiersExistants = [];
        try { fichiersExistants = JSON.parse(noteExist.fichiers || '[]'); } catch { }

        for (const file of allFiles) {
            try {
                const r = await uploadToSharePointHierarchique(file, reference, nomDossier, moisDossier);
                fichiersExistants.push(r);
            } catch (e) { console.error(`❌ Upload justif modif ${file.originalname}:`, e.message); }
        }

        const noteDataPDF = {
            reference, nomPrenom, mois: moisCapitalized, date,
            categorie: categorieNote, libelle,
            montant: parseFloat(montantFinal.toFixed(2)),
            lignes: lignesParsed, lignesJson: JSON.stringify(lignesParsed),
            tarifKm: tarifKmVal, statut: 'enattente',
            departement: collaborateur.departement,
        };

        try {
            const fichePDF = await generateFicheSignee(noteDataPDF, [
                { niveau: 'COLLAB', nomPrenom, date: new Date(), action: 'soumettre', commentaire: 'Note modifiée et resoumise' }
            ]);
            const ficheResult = await uploadToSharePointHierarchique(
                { buffer: fichePDF, originalname: `${reference}_resoumission.pdf`, mimetype: 'application/pdf', size: fichePDF.length },
                reference, nomDossier, moisDossier
            );
            fichiersExistants.push(ficheResult);
        } catch (e) { console.error('❌ Fiche PDF re-soumission:', e.message); }

        await pool.request()
            .input('id', sql.Int, noteId)
            .input('libelle', sql.NVarChar, libelle)
            .input('montant', sql.Decimal, parseFloat(montantFinal.toFixed(2)))
            .input('date', sql.Date, new Date(date))
            .input('categorie', sql.NVarChar, categorieNote)
            .input('description', sql.NVarChar, description || null)
            .input('fichiers', sql.NVarChar, JSON.stringify(fichiersExistants))
            .input('statut', sql.NVarChar, 'enattente')
            .input('km', sql.Decimal, kmTotal || null)
            .input('indemniteKm', sql.Decimal, indemKm || null)
            .input('lignesJson', sql.NVarChar, JSON.stringify(lignesParsed))
            .query(`
                UPDATE NoteDeFrais SET
                    libelle = @libelle, montant = @montant, date = @date,
                    categorie = @categorie, description = @description,
                    fichiers = @fichiers, statut = @statut,
                    km = @km, indemniteKm = @indemniteKm,
                    lignesJson = @lignesJson,
                    motifRefus = NULL, commentaireN1 = NULL, commentaireN2 = NULL,
                    dateValidationN1 = NULL, dateValidationN2 = NULL,
                    DateModification = GETDATE()
                WHERE id = @id
            `);

        await pool.request()
            .input('noteId', sql.Int, noteId)
            .query(`DELETE FROM LigneNoteDeFrais WHERE noteDeFraisId = @noteId`);

        const lignesPDF = preparerLignesPDF(lignesParsed, tarifKmVal);
        for (let i = 0; i < lignesParsed.length; i++) {
            const l = lignesParsed[i];
            const pdf = lignesPDF[i] || {};
            const isKmLine = (l.categorie || '').toLowerCase().includes('kilom');
            const kmLine = parseFloat(l.km) || 0;
            const cvLine = parseInt(l.chevaux) || 7;
            const indemLine = isKmLine ? getIndemniteKmServer(kmLine, cvLine) : 0;
            const items = l.tvaItems || [{ montantTTC: l.montant, taux: l.tauxTVA }];
            const ttcLine = isKmLine ? indemLine : items.reduce((s, item) => s + (parseFloat(item.montantTTC) || 0), 0);
            try {
                await pool.request()
                    .input('noteId', sql.Int, noteId)
                    .input('numPiece', sql.Int, i + 1)
                    .input('date', sql.Date, new Date(l.date))
                    .input('nature', sql.NVarChar, l.categorie || '')
                    .input('libelle', sql.NVarChar, l.libelle || '')
                    .input('km', sql.Decimal, isKmLine ? kmLine : null)
                    .input('montantTTC', sql.Decimal, ttcLine || null)
                    .input('tva21', sql.Decimal, pdf.tva21 || null)
                    .input('tva55', sql.Decimal, pdf.tva55 || null)
                    .input('tva10', sql.Decimal, pdf.tva10 || null)
                    .input('tva20', sql.Decimal, pdf.tva20 || null)
                    .input('montantHT', sql.Decimal, pdf.montantHT || null)
                    .input('tauxTVA', sql.Decimal, parseFloat(l.tauxTVA) || null)
                    .input('indemniteKm', sql.Decimal, indemLine || null)
                    .query(`
                        INSERT INTO LigneNoteDeFrais
                            (noteDeFraisId, numPiece, date, nature, libelle,
                             km, montantTTC, tva21, tva55, tva10, tva20,
                             montantHT, tauxTVA, indemniteKm)
                        VALUES
                            (@noteId, @numPiece, @date, @nature, @libelle,
                             @km, @montantTTC, @tva21, @tva55, @tva10, @tva20,
                             @montantHT, @tauxTVA, @indemniteKm)
                    `);
            } catch (e) { console.error(`❌ Insertion ligne modif ${i + 1}:`, e.message); }
        }

        const hierarchie = await pool.request()
            .input('collabId', sql.Int, userId)
            .query(`
                SELECT h.SuperieurId, s1.email AS emailN1, s1.prenom AS prenomN1, s1.nom AS nomN1
                FROM HierarchieValidationNDF h
                LEFT JOIN CollaborateurAD s1 ON s1.id = h.SuperieurId
                WHERE h.CollaborateurId = @collabId
            `);
        const n1 = hierarchie.recordset[0];
        if (n1?.SuperieurId && n1?.emailN1) {
            try {
                await creerNotification({
                    destinataireId: n1.SuperieurId, destinataireEmail: n1.emailN1,
                    type: 'validation',
                    titre: `📋 Note modifiée à valider — ${reference}`,
                    message: `${collaborateur.prenom} ${collaborateur.nom} a modifié et resoumis la note ${reference} (${parseFloat(montantFinal.toFixed(2))} €).`,
                    noteId
                });
                const frontendUrl = process.env.FRONTEND_URL || 'myndf.ensup-adm.net';
                await sendMailGraph(n1.emailN1, `📋 Note modifiée à valider — ${reference}`,
                    `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                        <div style="background:linear-gradient(135deg,#f59e0b,#d97706);color:white;padding:20px;border-radius:12px 12px 0 0">
                            <h2 style="margin:0">📋 Note modifiée — à valider</h2>
                        </div>
                        <div style="padding:20px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                            <p>Bonjour <strong>${n1.prenomN1} ${n1.nomN1}</strong>,</p>
                            <p><strong>${collaborateur.prenom} ${collaborateur.nom}</strong> a modifié et resoumis la note <strong>${reference}</strong> — ${libelle} (<strong>${parseFloat(montantFinal.toFixed(2))} €</strong>).</p>
                            <div style="text-align:center;margin-top:24px">
                                <a href="${frontendUrl}" style="background:#6366f1;color:white;padding:14px 32px;text-decoration:none;border-radius:8px;font-weight:700">Valider sur la plateforme →</a>
                            </div>
                        </div>
                    </div>`
                );
            } catch (e) { console.error('❌ Notif N1 modif:', e.message); }
        }

        console.log(`✅ Note ${reference} modifiée par ${collaborateur.email}`);
        res.json({ success: true, id: noteId, reference, statut: 'enattente', isCorrection: false });

    } catch (error) {
        console.error('❌ Erreur PUT /api/notes/:id:', error.message);
        res.status(500).json({ error: error.message });
    }
});
// ================================================
// GET /api/notes
// ================================================
app.get('/api/notes', authenticateToken, async (req, res) => {
    try {
        const { statut, mois, annee } = req.query;
        const request = pool.request()
            .input('collaborateurId', sql.Int, req.user.id);

        let where = 'WHERE n.collaborateurId = @collaborateurId';
        if (statut) {
            request.input('statut', sql.NVarChar, statut);
            where += ' AND n.statut = @statut';
        }
        if (mois && annee) {
            request.input('mois', sql.Int, parseInt(mois));
            request.input('annee', sql.Int, parseInt(annee));
            where += ' AND MONTH(n.date) = @mois AND YEAR(n.date) = @annee';
        }

        const result = await request.query(`
            SELECT n.*,
                   v1.nom + ' ' + v1.prenom as nomValidateurN1
                  
            FROM NoteDeFrais n
            LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id
           
            ${where}
            ORDER BY n.DateCreation DESC
        `);

        const notes = result.recordset;
        if (!notes.length) return res.json([]);

        // ── 1. Une seule requête pour tous les non-conformes ─────────────
        const noteIds = notes.map(n => n.id).join(',');
        let ncByNote = {};
        try {
            const ncAll = await pool.request().query(`
                SELECT noteDeFraisId, fileName, motif, statut, dateSignalement
                FROM JustificatifsNonConformes
                WHERE noteDeFraisId IN (${noteIds})
                ORDER BY dateSignalement DESC
            `);
            for (const row of ncAll.recordset) {
                if (!ncByNote[row.noteDeFraisId]) ncByNote[row.noteDeFraisId] = [];
                ncByNote[row.noteDeFraisId].push(row);
            }
        } catch (e) { console.warn('⚠️ NC batch fetch:', e.message); }

        // ── 2. Collecter tous les qrNoteRef qui manquent encore de qrFiles
        const allQrRefs = new Set();
        for (const note of notes) {
            if (!note.lignesJson) continue;
            try {
                const lignes = JSON.parse(note.lignesJson);
                for (const l of lignes) {
                    if (l.qrNoteRef && !(l.qrFiles?.length)) allQrRefs.add(l.qrNoteRef);
                }
            } catch { }
        }

        // ── 3. Une seule requête pour tous les tokens QR manquants ───────
        let qrByRef = {};
        if (allQrRefs.size > 0) {
            const refsStr = [...allQrRefs]
                .map(r => `'${r.replace(/'/g, "''")}'`)
                .join(',');
            try {
                const qrAll = await pool.request().query(`
                    SELECT noteRef, fichiers
                    FROM UploadTokens
                    WHERE noteRef IN (${refsStr}) AND used = 1
                `);
                for (const row of qrAll.recordset) {
                    try { qrByRef[row.noteRef] = JSON.parse(row.fichiers || '[]'); } catch { }
                }
            } catch (e) { console.warn('⚠️ QR batch fetch:', e.message); }
        }

        // ── 4. Enrichissement en mémoire — zéro requête SQL ──────────────
        for (const note of notes) {
            // Parser fichiers → sharepointFiles
            try { note.sharepointFiles = JSON.parse(note.fichiers || '[]'); } catch { note.sharepointFiles = []; }

            // Non-conformes depuis le batch
            if (note.statut === 'non_conforme_verif') {
                note.nonConformes = ncByNote[note.id] || [];
            }

            // Enrichissement QR en mémoire
            if (note.lignesJson) {
                try {
                    const lignes = JSON.parse(note.lignesJson);
                    let enrichi = false;
                    const lignesEnrichies = lignes.map(l => {
                        if (l.qrFiles?.length > 0) return l;
                        if (!l.qrNoteRef) return l;
                        const fichiers = qrByRef[l.qrNoteRef];
                        if (fichiers?.length) { enrichi = true; return { ...l, qrFiles: fichiers }; }
                        return l;
                    });
                    if (enrichi) note.lignesJson = JSON.stringify(lignesEnrichies);
                } catch (e) { console.warn('⚠️ Enrichissement QR fail:', note.id, e.message); }
            }
        }

        res.json(notes);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// ================================================
// 🔑 TOKEN SHAREPOINT (scope différent de Graph)
// ================================================
async function getSharePointToken() {
    const now = Date.now();
    if (_sharePointTokenCache && now < _sharePointTokenCache.expiresAt) {
        return _sharePointTokenCache.token;
    }
    try {
        const params = new URLSearchParams({
            grant_type: 'client_credentials',
            client_id: AZURE_CONFIG.clientId,
            client_secret: AZURE_CONFIG.clientSecret,
            scope: 'https://ensup.sharepoint.com/.default'
        });
        const response = await axios.post(
            `https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`,
            params.toString(),
            { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
        );
        const token = response.data.access_token;
        _sharePointTokenCache = { token, expiresAt: now + 55 * 60 * 1000 };
        console.log('✅ Token SharePoint mis en cache (55 min)');
        return token;
    } catch (error) {
        console.error('❌ Erreur token SharePoint:', error.response?.data || error.message);
        return null;
    }
}

// routes/sharepoint.js
// ✅ route proxy-file — force les bons headers
app.get('/api/proxy-pdf', async (req, res) => {
    let url = req.query.url;
    if (!url) return res.status(400).send('URL manquante');
    try { url = decodeURIComponent(url); } catch { }
    if (url.includes('/api/proxy-pdf?url=')) {
        url = url.split('/api/proxy-pdf?url=')[1];
        try { url = decodeURIComponent(url); } catch { }
    }
    if (!url.startsWith('http')) return res.status(400).send('URL invalide');

    // ✅ Cache HIT → redirection instantanée vers CDN
    const cached = downloadUrlCache.get(url);
    if (cached && Date.now() < cached.expiresAt) {
        res.setHeader('Cache-Control', 'private, max-age=3600');
        return res.redirect(302, cached.url);
    }

    try {
        const accessToken = await getGraphToken();
        const urlObj = new URL(url);
        const fullPath = decodeURIComponent(urlObj.pathname);
        const marker = '/Shared Documents/';
        const markerAlt = '/Documents/';
        let relativePath = '';
        if (fullPath.includes(marker)) relativePath = fullPath.split(marker)[1];
        else if (fullPath.includes(markerAlt)) relativePath = fullPath.split(markerAlt)[1];
        else {
            const parts = fullPath.split('/sites/')[1]?.split('/');
            relativePath = parts?.slice(2).join('/') || '';
        }

        const metaRes = await axios.get(
            `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${relativePath}`,
            { 
                headers: { Authorization: `Bearer ${accessToken}` }, 
                timeout: 15000  // ← 5000 → 15000ms
            }
        );
        const downloadUrl = metaRes.data['@microsoft.graph.downloadUrl'];
        if (!downloadUrl) throw new Error('downloadUrl absent');

        downloadUrlCache.set(url, { url: downloadUrl, expiresAt: Date.now() + 50 * 60 * 1000 });

        res.setHeader('Cache-Control', 'private, max-age=3600');
        return res.redirect(302, downloadUrl);

    } catch (err) {
        console.error('proxy-pdf erreur:', err.message);
        res.status(500).json({ error: err.message });
    }
});

// ================================================
// GET /api/notes/pending
// ================================================
app.get('/api/notes/pending', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('userId', sql.Int, req.user.id)
            .query(`
        SELECT n.id, n.reference, n.libelle, n.montant, n.date, n.categorie, n.statut,
               n.description, n.participants, n.nombreParticipants,
               n.montantHT, n.tauxTVA, n.km, n.indemniteKm,
               n.commentaireN1, n.commentaireN2, n.sharepointUrl, n.fichiers,
               n.lignesJson,
               n.noteRefuseeId,
               ancienne.reference AS ancienneReference,
               ancienne.motifRefus AS ancienMotifRefus,
               c.prenom + ' ' + c.nom AS collaborateur, c.departement, c.campus
        FROM NoteDeFrais n
        JOIN CollaborateurAD c ON c.id = n.collaborateurId
        LEFT JOIN NoteDeFrais ancienne ON ancienne.id = n.noteRefuseeId
        WHERE (n.validateurN1Id = @userId AND n.statut = 'enattente')
        ORDER BY n.date DESC
    `);

        // Pour chaque note, récupérer les lignes depuis LigneNoteDeFrais
        // si lignesJson est vide
        const notes = result.recordset;
        for (const note of notes) {
            // ✅ Parser fichiers → sharepointFiles pour le frontend
            if (note.fichiers) {
                try { note.sharepointFiles = JSON.parse(note.fichiers); } catch { note.sharepointFiles = []; }
            } else { note.sharepointFiles = []; }

            // ✅ Toujours re-parser lignesJson depuis la BDD pour avoir les qrFiles à jour
            // Ne reconstruire depuis LigneNoteDeFrais qu'en dernier recours
            // ✅ Enrichir chaque ligne avec ses fichiers QR depuis UploadTokens
            if (note.lignesJson) {
                try {
                    const lignes = JSON.parse(note.lignesJson);
                    let enrichi = false;

                    const lignesEnrichies = await Promise.all(lignes.map(async (l) => {
                        if (l.qrFiles && l.qrFiles.length > 0) return l;

                        const qrRef = l.qrNoteRef || '';
                        if (!qrRef) return l;

                        try {
                            const qrResult = await pool.request()
                                .input('noteRef', sql.NVarChar, qrRef)
                                .query(`SELECT TOP 1 fichiers FROM UploadTokens 
                            WHERE noteRef = @noteRef AND used = 1 
                            ORDER BY expiresAt DESC`);

                            if (qrResult.recordset.length && qrResult.recordset[0].fichiers) {
                                const fichiers = JSON.parse(qrResult.recordset[0].fichiers);
                                if (fichiers.length > 0) {
                                    enrichi = true;
                                    return { ...l, qrFiles: fichiers };
                                }
                            }
                        } catch (e) { }
                        return l;
                    }));

                    if (enrichi) {
                        note.lignesJson = JSON.stringify(lignesEnrichies);
                    }
                } catch (e) {
                    console.warn('⚠️ Enrichissement QR fail:', note.id, e.message);
                }
            }
        }

        res.json(notes);
    } catch (e) {
        console.error('Erreur /api/notes/pending:', e.message);
        res.status(500).json({ error: e.message });
    }
});

// ================================================
// GET /api/notes/:id/lignes
// ================================================
app.get('/api/notes/:id/lignes', authenticateToken, async (req, res) => {
    try {
        const isValidator = hasAnyRole(req.user, 'Finance', 'Validateur', 'Validatrice', 'superUtilisateur') ? 1 : 0;
        const result = await pool.request()
            .input('noteId', sql.Int, req.params.id)
            .input('userId', sql.Int, req.user.id)
            .input('isVal', sql.Int, isValidator)
            .query(`
                SELECT l.*
                FROM LigneNoteDeFrais l
                JOIN NoteDeFrais n ON n.id = l.noteDeFraisId
                WHERE l.noteDeFraisId = @noteId
                  AND (n.collaborateurId = @userId OR @isVal = 1)
                ORDER BY l.numPiece ASC
            `);
        res.json(result.recordset);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// GET /api/notes/:id/detail — récupère une note par ID (pour validateur + collaborateur)
app.get('/api/notes/:id/detail', authenticateToken, async (req, res) => {
    try {
        const noteId = parseInt(req.params.id);
        const userId = req.user.id;

        const result = await pool.request()
            .input('id', sql.Int, noteId)
            .query(`
                SELECT n.*,
                       c.prenom + ' ' + c.nom AS collaborateur,
                       c.departement, c.campus,
                       ancienne.reference AS ancienneReference,
                       ancienne.motifRefus AS ancienMotifRefus
                FROM NoteDeFrais n
                JOIN CollaborateurAD c ON c.id = n.collaborateurId
                LEFT JOIN NoteDeFrais ancienne ON ancienne.id = n.noteRefuseeId
                WHERE n.id = @id
                  AND (
                      n.collaborateurId = ${userId}
                      OR n.validateurN1Id = ${userId}
                     
                      OR EXISTS (
                          SELECT 1 FROM UtilisateurRoles r
                          WHERE r.collaborateur_id = ${userId}
                          AND r.role IN ('Finance','superUtilisateur','VerificateurFinance','ValidateurFinance')
                          AND r.actif = 1
                      )
                  )
            `);

        if (!result.recordset.length)
            return res.status(404).json({ error: 'Note introuvable ou accès refusé' });

        const note = result.recordset[0];
        if (note.fichiers) {
            try { note.sharepointFiles = JSON.parse(note.fichiers); } catch { note.sharepointFiles = []; }
        } else { note.sharepointFiles = []; }

        res.json(note);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// ================================================
// PUT /api/notes/:id/statut — Valider ou refuser
// ================================================
app.put('/api/notes/:id/statut', authenticateToken, async (req, res) => {
    try {
        const { id } = req.params;
        const { action, commentaire, motifRefus } = req.body;
        const userId = Number(req.user.id);

        const noteResult = await pool.request()
            .input('id', sql.Int, id)
            .query('SELECT * FROM NoteDeFrais WHERE id = @id');
        if (!noteResult.recordset.length)
            return res.status(404).json({ error: 'Note non trouvée' });

        const note = noteResult.recordset[0];
        const n1Id = Number(note.validateurN1Id);
        const statutNote = note.statut?.trim();

        let nouveauStatut = null, niveauValidation = null;

        if (n1Id === userId && statutNote === 'enattente') {
            niveauValidation = 'N1';
            nouveauStatut = action === 'valider' ? 'approuve' : 'refuse';
        } else {
            return res.status(403).json({ error: 'Non autorisé à valider cette note' });
        }

        await pool.request()
            .input('id', sql.Int, id)
            .input('statut', sql.NVarChar, nouveauStatut)
            .input('commentaire', sql.NVarChar, commentaire ?? null)
            .input('motifRefus', sql.NVarChar, motifRefus ?? null)
            .query(`
                UPDATE NoteDeFrais
                SET statut = @statut,
                    dateValidationN1 = GETDATE(),
                    commentaireN1 = @commentaire,
                    motifRefus = CASE WHEN @motifRefus IS NOT NULL THEN @motifRefus ELSE motifRefus END,
                    DateModification = GETDATE()
                WHERE id = @id
            `);

        await pool.request()
            .input('noteId', sql.Int, id)
            .input('validateurId', sql.Int, userId)
            .input('niveau', sql.NVarChar, 'N1')
            .input('action', sql.NVarChar, action)
            .input('commentaire', sql.NVarChar, commentaire ?? null)
            .input('motifRefus', sql.NVarChar, motifRefus ?? null)
            .input('statut', sql.NVarChar, nouveauStatut)
            .query(`
                INSERT INTO HistoriqueValidation
                    (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, MotifRefus, NouveauStatut, DateAction)
                VALUES
                    (@noteId, @validateurId, @niveau, @action, @commentaire, @motifRefus, @statut, GETDATE())
            `);

        res.json({ success: true, statut: nouveauStatut, niveau: 'N1' });

        setImmediate(async () => {
            try {
                console.log(`🔄 [ASYNC] PDF + emails validation ${id} → ${nouveauStatut}`);

                const frontendUrl = process.env.FRONTEND_URL || 'myndf.ensup-adm.net';
                const montantFormate = parseFloat(note.montant).toFixed(2);

                const [collabResult, validateurResult, noteCompleteResult] = await Promise.all([
                    pool.request()
                        .input('id', sql.Int, note.collaborateurId)
                        .query('SELECT id, email, prenom, nom FROM CollaborateurAD WHERE id = @id'),
                    pool.request()
                        .input('id', sql.Int, userId)
                        .query('SELECT id, email, prenom, nom FROM CollaborateurAD WHERE id = @id'),
                    pool.request()
                        .input('id', sql.Int, id)
                        .query(`
                            SELECT n.reference, n.libelle, n.montant, n.date, n.categorie,
                                   n.fichiers, n.DateCreation, n.collaborateurId, n.lignesJson,
                                   n.commentaireN1, n.dateValidationN1,
                                   c.prenom + ' ' + c.nom AS nomPrenom,
                                   c.prenom AS collabPrenom, c.nom AS collabNom, c.departement,
                                   v1.prenom + ' ' + v1.nom AS nomValidateurN1
                            FROM NoteDeFrais n
                            JOIN CollaborateurAD c ON c.id = n.collaborateurId
                            LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id
                            WHERE n.id = @id
                        `)
                ]);

                const c = collabResult.recordset[0];
                const v = validateurResult.recordset[0];
                const nd = noteCompleteResult.recordset[0];

                if (!c || !nd) {
                    console.error(`❌ [ASYNC] Données manquantes pour note ${id}`);
                    return;
                }

                const nomValidateurActuel = v
                    ? `${v.prenom} ${v.nom}`.trim()
                    : `${req.user.prenom} ${req.user.nom}`.trim();

                // ── Construire les signatures ──────────────────────────────
                const signatures = [
                    { niveau: 'COLLAB', nomPrenom: nd.nomPrenom, date: nd.DateCreation, action: 'soumettre', commentaire: null },
                    { niveau: 'N1', nomPrenom: nomValidateurActuel, date: new Date(), action, commentaire: commentaire ?? null }
                ];

                const moisStr = (() => {
                    if (!nd.date) return '';
                    const d = new Date(nd.date);
                    const m = d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
                    return m.charAt(0).toUpperCase() + m.slice(1);
                })();

                const noteDataPDF = {
                    reference: nd.reference,
                    nomPrenom: nd.nomPrenom,
                    mois: moisStr,
                    departement: nd.departement,
                    lignesJson: nd.lignesJson,
                    tarifKm: await getTarifKm(),
                    statut: nouveauStatut
                };

                let fichiersExistants = [];
                try { fichiersExistants = JSON.parse(nd.fichiers || '[]'); } catch { }

                const existingFolder = fichiersExistants[0]?.folderPath;
            // APRÈS
            let nomDossier = existingFolder
                ? existingFolder.split('/')[1]
                : `${nd.collabNom}_${nd.collabPrenom}`
                    .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
                    .replace(/[^a-zA-Z0-9_]/g, '_');
            let moisDossier = existingFolder
                ? existingFolder.split('/')[2]
                : `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;

                // ── Génération PDF signé (fiche seule) ────────────────────
                try {
                    const pdfSigne = await generateFicheSignee(noteDataPDF, signatures);
                    const suffixe = nouveauStatut === 'approuve' ? 'signe-approuve' : 'signe-refuse';

                    const signedResult = await uploadToSharePointHierarchique(
                        { buffer: pdfSigne, originalname: `${nd.reference}-${suffixe}.pdf`, mimetype: 'application/pdf', size: pdfSigne.length },
                        nd.reference, nomDossier, moisDossier
                    );
                    fichiersExistants.push(signedResult);

                    await pool.request()
                        .input('id', sql.Int, id)
                        .input('fichiers', sql.NVarChar, JSON.stringify(fichiersExistants))
                        .query('UPDATE NoteDeFrais SET fichiers = @fichiers WHERE id = @id');

                    console.log(`✅ [ASYNC] PDF signé uploadé: ${signedResult.fileName}`);
                } catch (pdfError) {
                    console.error('❌ [ASYNC] Génération PDF signé:', pdfError.message);
                }

                // ── Régénérer le recap complet ────────────────────────────
                try {
                    const EXCLUS_RECAP = ['soumission', 'resoumission', 'signe', 'verifie', 'recap'];
                    const justifFiles = [];

                    for (const f of fichiersExistants) {
                        const fname = (f.fileName || '').toLowerCase();
                        if (EXCLUS_RECAP.some(kw => fname.includes(kw))) continue;
                        try {
                            const buf = await downloadFromSharePoint(f.uploadUrl);
                            const mimetype = fname.endsWith('.pdf') ? 'application/pdf'
                                : fname.endsWith('.png') ? 'image/png' : 'image/jpeg';
                            justifFiles.push({ buffer: buf, originalname: f.fileName, mimetype, size: buf.length });
                        } catch (e) { console.warn(`⚠️ Justif recap non récupérable: ${f.fileName}`, e.message); }
                    }

                    const recapBuffer = await generateRecapWithJustifs(noteDataPDF, justifFiles, signatures);
                    const recapResult = await uploadToSharePointHierarchique(
                        {
                            buffer: recapBuffer,
                            originalname: `${nd.reference}_recap.pdf`,
                            mimetype: 'application/pdf',
                            size: recapBuffer.length
                        },
                        nd.reference, nomDossier, moisDossier
                    );

                    const fichiersAvecRecap = fichiersExistants.filter(f => {
                        const fname = (f.fileName || '').toLowerCase();
                        return !fname.endsWith('_recap.pdf') || fname.includes('recap-paiement');
                    });
                    fichiersAvecRecap.push(recapResult);

                    await pool.request()
                        .input('id', sql.Int, id)
                        .input('fichiers', sql.NVarChar, JSON.stringify(fichiersAvecRecap))
                        .query('UPDATE NoteDeFrais SET fichiers = @fichiers WHERE id = @id');

                    console.log(`✅ [ASYNC] Recap PDF régénéré avec ${signatures.length} signature(s): ${recapResult.fileName}`);
                } catch (recapError) {
                    console.error('❌ [ASYNC] Régénération recap:', recapError.message);
                }

                // ── Notifications + emails ────────────────────────────────
                const isApprouve = nouveauStatut === 'approuve';
                const isRefus = nouveauStatut === 'refuse';
                const titreCollab = isApprouve
                    ? `Note ${note.reference} approuvée`
                    : `Note ${note.reference} refusée`;
                const msgCollab = isApprouve
                    ? `Votre note ${note.reference} (${montantFormate}€) a été approuvée.`
                    : `Votre note ${note.reference} a été refusée. Motif : ${motifRefus || commentaire || 'Non précisé'}`;

                const motifAffiche = motifRefus || commentaire || 'Non précisé';

                const emailCollabHtml = isRefus
                    ? `<div style="font-family:Arial,sans-serif;max-width:620px;margin:0 auto">
                        <div style="background:linear-gradient(135deg,#ef4444,#dc2626);color:white;padding:24px;border-radius:12px 12px 0 0">
                            <h2 style="margin:0;font-size:18px">❌ Votre note de frais a été refusée</h2>
                            <p style="margin:8px 0 0;opacity:.85;font-size:13px">Une action de votre part est nécessaire</p>
                        </div>
                        <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                            <p>Bonjour <strong>${c.prenom} ${c.nom}</strong>,</p>
                            <p>Votre note <strong>${note.reference}</strong> a été refusée par <strong>${nomValidateurActuel}</strong>.</p>
                            <div style="background:#fef2f2;border:1.5px solid #fecaca;border-left:4px solid #ef4444;border-radius:0 8px 8px 0;padding:16px;margin:20px 0">
                                <div style="font-size:12px;font-weight:700;color:#991b1b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">Motif du refus</div>
                                <div style="font-size:14px;color:#dc2626;font-weight:600">${motifAffiche}</div>
                            </div>
                            <div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:16px 0">
                                <table style="width:100%;border-collapse:collapse;font-size:13px">
                                    <tr><td style="color:#64748b;padding:4px 0;width:140px">Référence</td><td style="font-weight:700;font-family:monospace;color:#6366f1">${note.reference}</td></tr>
                                    <tr><td style="color:#64748b;padding:4px 0">Libellé</td><td style="font-weight:600">${note.libelle}</td></tr>
                                    <tr><td style="color:#64748b;padding:4px 0">Montant</td><td style="font-weight:700">${montantFormate} €</td></tr>
                                    <tr><td style="color:#64748b;padding:4px 0">Refusé par</td><td>${nomValidateurActuel}</td></tr>
                                    <tr><td style="color:#64748b;padding:4px 0">Date</td><td>${new Date().toLocaleDateString('fr-FR', { day: '2-digit', month: 'long', year: 'numeric' })}</td></tr>
                                </table>
                            </div>
                            <div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:16px;margin:16px 0">
                                <div style="font-size:13px;font-weight:700;color:#1e40af;margin-bottom:8px">📝 Que faire maintenant ?</div>
                                <ol style="margin:0;padding-left:18px;font-size:13px;color:#1d4ed8;line-height:2">
                                    <li>Connectez-vous à la plateforme NDF</li>
                                    <li>Rendez-vous dans <strong>Mes notes</strong></li>
                                    <li>Cliquez sur la note <strong>${note.reference}</strong></li>
                                    <li>Corrigez les informations demandées</li>
                                    <li>Resoumettez la note</li>
                                </ol>
                            </div>
                            <div style="text-align:center;margin-top:28px">
                                <a href="${frontendUrl}" style="background:linear-gradient(135deg,#6366f1,#4f46e5);color:white;padding:14px 36px;text-decoration:none;border-radius:8px;font-weight:700;font-size:14px;display:inline-block">
                                    ✏️ Modifier ma note →
                                </a>
                            </div>
                        </div>
                    </div>`
                    : `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                        <div style="background:${isApprouve ? '#10b981' : '#6366f1'};color:white;padding:20px;border-radius:12px 12px 0 0">
                            <h2 style="margin:0">${titreCollab}</h2>
                        </div>
                        <div style="padding:20px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                            <p>Bonjour <strong>${c.prenom} ${c.nom}</strong>,</p>
                            <p>${msgCollab}</p>
                            <div style="text-align:center;margin-top:24px">
                                <a href="${frontendUrl}" style="background:#6366f1;color:white;padding:13px 28px;text-decoration:none;border-radius:8px;font-weight:700">Voir mes notes</a>
                            </div>
                        </div>
                    </div>`;

                await Promise.all([
                    creerNotification({
                        destinataireId: c.id,
                        destinataireEmail: c.email,
                        type: isRefus ? 'refus' : 'validation',
                        titre: titreCollab,
                        message: msgCollab,
                        noteId: parseInt(id)
                    }).catch(e => console.error('❌ [ASYNC] Notif collab:', e.message)),

                    sendMailGraph(
                        c.email,
                        isRefus ? `❌ Note refusée — action requise : ${note.reference}` : titreCollab,
                        emailCollabHtml
                    ).catch(e => console.error('❌ [ASYNC] Email collab:', e.message)),
                ]);

                console.log(`✅ [ASYNC] Validation terminée pour note ${id} → ${nouveauStatut}`);

            } catch (e) {
                console.error(`❌ [ASYNC] Erreur générale validation note ${id}:`, e.message);
            }
        });

    } catch (error) {
        console.error('Erreur validation:', error.message);
        res.status(500).json({ error: error.message });
    }
});
// ================================================
// GET /api/notes/:id/historique
// ================================================
app.get('/api/notes/:id/historique', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('noteId', sql.Int, req.params.id)
            .query(`
                SELECT h.id, h.Niveau, h.Action, h.Commentaire, h.MotifRefus,
                       h.NouveauStatut, h.DateAction,
                       v.prenom + ' ' + v.nom AS validateur, v.role AS roleValidateur
                FROM HistoriqueValidation h
                JOIN CollaborateurAD v ON v.id = h.ValidateurId
                WHERE h.NoteDeFraisId = @noteId ORDER BY h.DateAction ASC
            `);
        res.json(result.recordset);
    } catch (error) { res.status(500).json({ error: error.message }); }
});

// ================================================
// GET /api/validateur/historique
// ================================================
app.get('/api/validateur/historique', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('validateurId', sql.Int, req.user.id)
            .query(`
                SELECT h.id, h.Niveau, h.Action, h.Commentaire, h.MotifRefus,
                       h.NouveauStatut, h.DateAction,
                       n.reference, n.libelle, n.montant, n.categorie,
                       c.prenom + ' ' + c.nom AS collaborateur, c.departement, c.campus
                FROM HistoriqueValidation h
                JOIN NoteDeFrais n     ON n.id = h.NoteDeFraisId
                JOIN CollaborateurAD c ON c.id = n.collaborateurId
                WHERE h.ValidateurId = @validateurId ORDER BY h.DateAction DESC
            `);
        res.json(result.recordset);
    } catch (error) { res.status(500).json({ error: error.message }); }
});

// ================================================
// GET /api/notes/all — réservé Finance
// ================================================
app.get('/api/notes/all', authenticateToken, async (req, res) => {
    try {
        if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ error: 'Accès réservé Finance' });
        const result = await pool.request().query(`
            SELECT n.*, c.nom + ' ' + c.prenom as collaborateur, c.departement, c.campus,
                   v1.nom + ' ' + v1.prenom as nomN1
            FROM NoteDeFrais n
            JOIN CollaborateurAD c  ON c.id  = n.collaborateurId
            LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id
           
            ORDER BY n.DateCreation DESC
        `);
        res.json(result.recordset);
    } catch (error) { res.status(500).json({ error: error.message }); }
});

// ================================================
// GET /api/admin/notes — réservé superUtilisateur
// ================================================
// GET /api/admin/notes — superUtilisateur, filtré sur ENSUP SOLUTION ET SUPPORT
app.get('/api/admin/notes', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé superUtilisateur' });

    try {
        const { mois, annee, statut } = req.query;
        let query = `
      SELECT n.id, n.reference, n.libelle, n.montant, n.date, n.categorie, n.statut,
             n.DateCreation, n.montantHT, n.tauxTVA, n.km, n.indemniteKm,
             n.sharepointUrl, n.fichiers, n.lignesJson,
             c.prenom + ' ' + c.nom AS collaborateur, c.email AS collaborateurEmail,
             c.departement, c.campus, c.societe
      FROM NoteDeFrais n
      JOIN CollaborateurAD c ON c.id = n.collaborateurId
      WHERE c.societe = 'ENSUP SOLUTION ET SUPPORT'
    `;
        const req2 = pool.request();
        if (mois && annee) {
            query += ` AND MONTH(n.date) = @mois AND YEAR(n.date) = @annee`;
            req2.input('mois', sql.Int, parseInt(mois));
            req2.input('annee', sql.Int, parseInt(annee));
        }
        if (statut) {
            query += ` AND n.statut = @statut`;
            req2.input('statut', sql.NVarChar, statut);
        }
        query += ` ORDER BY c.nom, n.date DESC`;
        const result = await req2.query(query);
        res.json(result.recordset);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// ================================================
// PAIEMENTS
// ================================================
app.get('/api/paiements/prochain', authenticateToken, async (req, res) => {
    try {
        const config = await pool.request().query(`SELECT TOP 1 JourPaiement FROM ConfigPaiement WHERE Actif = 1 ORDER BY Id DESC`);
        const jourPaiement = config.recordset[0]?.JourPaiement ?? 20;
        const today = new Date();
        const jour = today.getDate(), mois = today.getMonth(), annee = today.getFullYear();
        const datePaiement = jour <= jourPaiement ? new Date(annee, mois, jourPaiement) : new Date(annee, mois + 1, jourPaiement);
        res.json({ jourPaiement, datePaiement: datePaiement.toISOString().split('T')[0], libelle: `Paiement le ${datePaiement.toLocaleDateString('fr-FR')}` });
    } catch (error) { res.status(500).json({ error: error.message }); }
});

app.post('/api/paiements/marquer-payees', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ error: 'Accès réservé Finance' });
    try {
        const config = await pool.request().query(`SELECT TOP 1 JourPaiement FROM ConfigPaiement WHERE Actif = 1 ORDER BY Id DESC`);
        const jourPaiement = config.recordset[0]?.JourPaiement ?? 20;
        const today = new Date();
        const mois = today.getMonth() + 1, annee = today.getFullYear();
        const datePaiementExacte = new Date(annee, today.getMonth(), jourPaiement);

        const notes = await pool.request().query(`
            SELECT n.id, n.reference, n.montant, c.nom + ' ' + c.prenom AS collaborateur, c.email
            FROM NoteDeFrais n JOIN CollaborateurAD c ON c.id = n.collaborateurId
            WHERE n.statut = 'approuve' AND n.datePaiement IS NULL
        `);

        if (!notes.recordset.length) return res.json({ success: true, message: 'Aucune note approuvée à payer', count: 0 });

        await pool.request()
            .input('datePaiement', sql.DateTime, datePaiementExacte)
            .input('moisPaiement', sql.Int, mois)
            .input('anneePaiement', sql.Int, annee)
            .query(`
                UPDATE NoteDeFrais SET statut = 'payee', datePaiement = @datePaiement,
                    moisPaiement = @moisPaiement, anneePaiement = @anneePaiement, DateModification = GETDATE()
                WHERE statut = 'approuve' AND datePaiement IS NULL
            `);

        res.json({ success: true, count: notes.recordset.length, datePaiement: datePaiementExacte.toISOString().split('T')[0], notes: notes.recordset });
    } catch (error) { res.status(500).json({ error: error.message }); }
});

app.get('/api/paiements/historique', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'superUtilisateur', 'ValidateurFinance', 'VerificateurFinance'))
        return res.status(403).json({ error: 'Accès réservé Finance' });

    try {
        const mois = req.query.mois ? parseInt(req.query.mois) : null;
        const annee = req.query.annee ? parseInt(req.query.annee) : null;

        const request = pool.request();
        request.input('mois', sql.Int, mois);
        request.input('annee', sql.Int, annee);

        let campusFilter = '';
        if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) {
            const campusCode = normalizeCampus(req.user.campus);
            if (campusCode) {
                request.input('campus', sql.NVarChar, `%${campusCode}%`);
                campusFilter = 'AND c.campus LIKE @campus';
            }
        }

        const result = await request.query(`
    SELECT 
        n.id, n.reference, n.libelle, n.montant,
        n.datePaiement, n.moisPaiement, n.anneePaiement,
        n.categorie, n.lignesJson, n.indemniteKm, n.km,
        n.statut,
        c.nom + ' ' + c.prenom AS collaborateur,
        c.campus,
        c.societe
    FROM NoteDeFrais n
    JOIN CollaborateurAD c ON c.id = n.collaborateurId
    WHERE n.statut = 'payee'
      AND (@mois IS NULL OR n.moisPaiement = @mois)
      AND (@annee IS NULL OR n.anneePaiement = @annee)
      ${campusFilter}
    ORDER BY n.datePaiement DESC, c.nom
`);

        const data = result.recordset.map(row => {
            let montantRepas = 0, montantHebergement = 0;
            let montantKilometrique = row.indemniteKm || 0;
            let montantTransport = 0, montantAutres = 0;

            const cat = (row.categorie || '').toLowerCase();
            if (cat !== 'multiple') {
                if (cat.includes('repas') || cat.includes('restaurant')) montantRepas = row.montant;
                else if (cat.includes('hebergement') || cat.includes('hotel')) montantHebergement = row.montant;
                else if (cat.includes('kilom') || cat.includes('km')) montantKilometrique = row.indemniteKm || row.montant;
                else if (cat.includes('transport') || cat.includes('avion') || cat.includes('train') || cat.includes('taxi')) montantTransport = row.montant;
                else montantAutres = row.montant;
            } else {
                try {
                    const lignes = JSON.parse(row.lignesJson || '[]');
                    lignes.forEach(ligne => {
                        const lcat = (ligne.categorie || ligne.nature || '').toLowerCase();
                        const montantLigne = parseFloat(ligne.montantTTC || ligne.montant || 0);
                        if (lcat.includes('repas') || lcat.includes('restaurant')) montantRepas += montantLigne;
                        else if (lcat.includes('hebergement') || lcat.includes('hotel')) montantHebergement += montantLigne;
                        else if (lcat.includes('kilom') || lcat.includes('km')) montantKilometrique += parseFloat(ligne.indemniteKm || montantLigne);
                        else if (lcat.includes('transport') || lcat.includes('avion') || lcat.includes('train') || lcat.includes('taxi')) montantTransport += montantLigne;
                        else montantAutres += montantLigne;
                    });
                } catch { montantAutres = row.montant; }
            }

            return {
                ...row,
                montantRepas: Math.round(montantRepas * 100) / 100,
                montantHebergement: Math.round(montantHebergement * 100) / 100,
                montantKilometrique: Math.round(montantKilometrique * 100) / 100,
                montantTransport: Math.round(montantTransport * 100) / 100,
                montantAutres: Math.round(montantAutres * 100) / 100,
            };
        });

        res.json(data);
    } catch (error) {
        console.error('Erreur historique:', error.message);
        res.status(500).json({ error: error.message });
    }
});
app.get('/api/paiements/config', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request().query(`SELECT TOP 1 Id, JourPaiement, Actif, DateModif FROM ConfigPaiement WHERE Actif = 1 ORDER BY Id DESC`);
        res.json(result.recordset[0] ?? { JourPaiement: 20 });
    } catch (error) { res.status(500).json({ error: error.message }); }
});

app.put('/api/paiements/config', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ error: 'Accès réservé Finance' });
    try {
        const { jourPaiement } = req.body;
        if (!jourPaiement || jourPaiement < 1 || jourPaiement > 28) return res.status(400).json({ error: 'Jour de paiement invalide (1-28)' });
        await pool.request().query(`UPDATE ConfigPaiement SET Actif = 0 WHERE Actif = 1`);
        await pool.request().input('jour', sql.Int, jourPaiement).query(`INSERT INTO ConfigPaiement (JourPaiement, Actif, DateModif) VALUES (@jour, 1, GETDATE())`);
        res.json({ success: true, jourPaiement });
    } catch (error) { res.status(500).json({ error: error.message }); }
});

// ================================================
// HELPERS EMAIL + NOTIFICATION
// ================================================
async function sendMailGraph(to, subject, htmlBody) {
    try {
        const accessToken = await getGraphToken();
        if (!accessToken) throw new Error('Token Graph indisponible');
        const senderEmail = process.env.MAIL_SENDER || process.env.MAIL_FROM;
        if (!senderEmail) { console.error('MAIL_SENDER non défini'); return; }

        await axios.post(
            `https://graph.microsoft.com/v1.0/users/${senderEmail}/sendMail`,
            { message: { subject, body: { contentType: 'HTML', content: htmlBody }, from: { emailAddress: { address: senderEmail } }, toRecipients: [{ emailAddress: { address: to } }] }, saveToSentItems: false },
            { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }
        );
        console.log(`✅ Email envoyé à ${to} via ${senderEmail}`);
    } catch (error) {
        console.error('Erreur sendMailGraph:', error.response?.data || error.message);
    }
}

async function creerNotification({ destinataireId, destinataireEmail, type, titre, message, noteId }) {
    if (!destinataireId) {
        console.error('❌ creerNotification annulée : destinataireId manquant', { destinataireEmail, type, titre });
        return;
    }
    try {
        await pool.request()
            .input('destinataireId', sql.Int, destinataireId)
            .input('type', sql.NVarChar, type)
            .input('titre', sql.NVarChar, titre)
            .input('message', sql.NVarChar, message)
            .input('noteId', sql.Int, noteId || null)
            .query(`
                INSERT INTO Notifications (CollaborateurId, Type, Titre, Message, NoteDeFraisId, Lu, DateCreation)
                VALUES (@destinataireId, @type, @titre, @message, @noteId, 0, GETDATE())
            `);
        console.log(`🔔 Notification insérée pour ${destinataireEmail} (id: ${destinataireId})`);
    } catch (err) { console.error('❌ Erreur insertion notification:', err.message); }
}

// ================================================
// PAIEMENTS — NOTIFIER
// ================================================
app.post('/api/paiements/notifier', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ error: 'Accès réservé Finance' });
    try {
        const notes = await pool.request().query(`
            SELECT n.id, n.reference, n.montant, n.libelle, c.nom + ' ' + c.prenom AS collaborateur
            FROM NoteDeFrais n JOIN CollaborateurAD c ON c.id = n.collaborateurId
            WHERE n.statut = 'approuve' AND n.datePaiement IS NULL
        `);
        if (!notes.recordset.length) return res.json({ success: true, message: 'Aucune note en attente de paiement' });

        const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0);
        const alexandre = await pool.request()
            .input('email', sql.NVarChar, process.env.RESPONSABLE_PAIEMENT_EMAIL)
            .query(`SELECT TOP 1 id, email, prenom, nom FROM CollaborateurAD WHERE email = @email AND Actif = 1`);

        if (!alexandre.recordset.length) return res.status(404).json({ error: 'Responsable paiement introuvable' });
        const responsable = alexandre.recordset[0];

        const config = await pool.request().query(`SELECT TOP 1 JourPaiement FROM ConfigPaiement WHERE Actif = 1 ORDER BY Id DESC`);
        const jourPaiement = config.recordset[0]?.JourPaiement ?? 20;
        const titre = `⚠️ ${notes.recordset.length} note(s) à payer — ${total.toFixed(2)} €`;

        await creerNotification({ destinataireId: responsable.id, destinataireEmail: responsable.email, type: 'paiement', titre, message: `${notes.recordset.length} note(s) en attente avant le ${jourPaiement}. Total : ${total.toFixed(2)} €`, noteId: null });
        await sendMailGraph(responsable.email, titre, `<p>Bonjour ${responsable.prenom},</p><p>${notes.recordset.length} note(s) en attente de paiement avant le <strong>${jourPaiement}</strong>. Total : <strong>${total.toFixed(2)} €</strong></p>`);

        res.json({ success: true, count: notes.recordset.length, total: parseFloat(total.toFixed(2)) });
    } catch (error) { res.status(500).json({ error: error.message }); }
});

// ================================================
// NOTIFICATIONS
// ================================================
app.get('/api/notifications', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request().input('userId', sql.Int, req.user.id).query(`
            SELECT TOP 50 id, Type, Titre, Message, NoteDeFraisId, Lu, DateCreation
            FROM Notifications WHERE CollaborateurId = @userId ORDER BY DateCreation DESC
        `);
        res.json(result.recordset);
    } catch (error) { res.status(500).json({ error: error.message }); }
});

app.put('/api/notifications/:id/lu', authenticateToken, async (req, res) => {
    try {
        await pool.request().input('id', sql.Int, req.params.id).input('userId', sql.Int, req.user.id)
            .query(`UPDATE Notifications SET Lu = 1 WHERE id = @id AND CollaborateurId = @userId`);
        res.json({ success: true });
    } catch (error) { res.status(500).json({ error: error.message }); }
});

// ================================================
// CATÉGORIES / PARAMÈTRES TVA / KM
// ================================================
app.get('/api/categories', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request().query(`
            SELECT c.*, p.PlafondParPersonne, p.DescriptionPlafond
            FROM CategorieNDF c LEFT JOIN PlafondRepas p ON p.CategorieId = c.id AND p.Actif = 1
            WHERE c.Actif = 1 ORDER BY c.Ordre, c.Nom
        `);
        res.json(result.recordset);
    } catch (error) { res.status(500).json({ error: error.message }); }
});

app.get('/api/parametres/tva', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request().query(`
            SELECT id, taux, libelle, categorie FROM ParametresTVA
            WHERE actif = 1 AND dateDebut <= GETDATE() AND (dateFin IS NULL OR dateFin >= GETDATE())
            ORDER BY taux ASC
        `);
        res.json(result.recordset);
    } catch (err) { res.status(500).json({ error: err.message }); }
});

app.get('/api/parametres/km', authenticateToken, async (req, res) => {
    try {
        const annee = new Date().getFullYear();
        const result = await pool.request().input('annee', sql.Int, annee).query(`
            SELECT TOP 1 tarifParKm FROM ParametresKm WHERE annee = @annee AND actif = 1 ORDER BY DateCreation DESC
        `);
        res.json({ tarifKm: result.recordset[0]?.tarifParKm ?? await getTarifKm() });
    } catch (err) { res.status(500).json({ error: err.message }); }
});

// ================================================
// QR CODE UPLOAD MOBILE
// ================================================
app.post('/api/upload/generate-link', authenticateToken, async (req, res) => {
    try {
        const { noteRef } = req.body;
        if (!noteRef) return res.status(400).json({ error: 'noteRef obligatoire' });

        const collabResult = await pool.request().input('id', sql.Int, req.user.id).query('SELECT nom, prenom FROM CollaborateurAD WHERE id = @id');
        if (!collabResult.recordset.length) return res.status(404).json({ error: 'Collaborateur introuvable' });

        const { nom, prenom } = collabResult.recordset[0];
        const token = crypto.randomBytes(32).toString('hex');

        await pool.request()
            .input('token', sql.VarChar, token)
            .input('nomPrenom', sql.NVarChar, `${nom.toUpperCase()}_${prenom}`)
            .input('noteRef', sql.NVarChar, noteRef)
            .query(`INSERT INTO UploadTokens (token, nomPrenom, noteRef, expiresAt) VALUES (@token, @nomPrenom, @noteRef, DATEADD(MINUTE, 120, GETDATE()))`);

        res.json({ uploadLink: `${process.env.FRONTEND_URL}/upload/${token}`, expiresAt: new Date(Date.now() + 120 * 60 * 1000), token });
    } catch (e) { res.status(500).json({ error: e.message }); }
});

app.get('/api/upload/check/:token', async (req, res) => {
    try {
        const valid = await pool.request().input('token', sql.VarChar, req.params.token)
            .query(`SELECT * FROM UploadTokens WHERE token = @token AND used = 0 AND expiresAt > GETDATE()`);
        if (!valid.recordset.length) return res.status(410).json({ error: 'Lien expiré ou déjà utilisé' });
        const t = valid.recordset[0];
        res.json({ valid: true, noteRef: t.noteRef, nomPrenom: t.nomPrenom });
    } catch (e) { res.status(500).json({ error: e.message }); }
});

app.post('/api/upload/submit/:token', upload.array('files', 10), async (req, res) => {
    try {
        const result = await pool.request().input('token', sql.VarChar, req.params.token)
            .query(`SELECT * FROM UploadTokens WHERE token = @token AND used = 0 AND expiresAt > GETDATE()`);
        if (!result.recordset.length) return res.status(410).json({ error: 'Lien expiré ou déjà utilisé' });

        const { nomPrenom, noteRef } = result.recordset[0];
        const files = req.files;
        if (!files || !files.length) return res.status(400).json({ error: 'Aucun fichier reçu' });

        const uploaded = [];
        for (const file of files) {
            const r = await uploadToSharePoint(file, noteRef, nomPrenom);
            uploaded.push(r);
        }

        await pool.request()
            .input('token', sql.VarChar, req.params.token)
            .input('firstUrl', sql.NVarChar, uploaded[0].uploadUrl)
            .input('allFiles', sql.NVarChar, JSON.stringify(uploaded))
            .query(`UPDATE UploadTokens SET used = 1, sharepointUrl = @firstUrl, fichiers = @allFiles WHERE token = @token`);

        res.json({ success: true, uploaded });
    } catch (e) { res.status(500).json({ error: e.message }); }
});

app.get('/api/upload/status/:noteRef', async (req, res) => {
    try {
        const result = await pool.request().input('noteRef', sql.NVarChar, req.params.noteRef)
            .query(`SELECT TOP 1 used, fichiers, sharepointUrl FROM UploadTokens WHERE noteRef = @noteRef ORDER BY expiresAt DESC`);
        if (!result.recordset.length) return res.json({ uploaded: false });
        const row = result.recordset[0];
        const fichiers = row.fichiers ? JSON.parse(row.fichiers) : [];
        res.json({
            uploaded: row.used === true || row.used === 1,
            files: fichiers,      // ← ajout pour le frontend
            fichiers: fichiers,   // ← conservé pour rétrocompat
            sharepointUrl: row.sharepointUrl
        });
    } catch (e) { res.status(500).json({ error: e.message }); }
});


// GET /api/notes-all — réservé Finance (filtré par campus) + superUtilisateur (ENSUP SOLUTION ET SUPPORT)

app.get('/api/notes-all', authenticateToken, async (req, res) => {
    try {
        if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance'))
            return res.status(403).json({ error: 'Accès réservé Finance' });

        const request = pool.request();
        let campusWhere = '';

        if (req.user.campus) {
            const campusCode = normalizeCampus(req.user.campus);
            
            // Variantes de recherche par campus normalisé
            const campusVariants = {
                'SQY': ['%SQY%', '%SAINT%'],
                'CGY': ['%CGY%', '%CERGY%'],
                'MRS': ['%MRS%', '%MARSEILLE%'],
                'NTE': ['%NTE%', '%NANTES%'],
            };

            const variants = campusVariants[campusCode] || [`%${campusCode}%`];
            
            // Construire les conditions OR pour chaque variante
            const conditions = variants.map((v, i) => {
                request.input(`campus${i}`, sql.NVarChar, v);
                return `c.campus LIKE @campus${i}`;
            });
            campusWhere = `AND (${conditions.join(' OR ')})`;
        }

        const statutFilter = hasAnyRole(req.user, 'ValidateurFinance') && !hasAnyRole(req.user, 'Finance')
            ? `AND LOWER(n.statut) IN ('verifie', 'en_attente_president', 'paiementenattente', 'payee')`
            : `AND LOWER(REPLACE(n.statut COLLATE Latin1_General_CI_AI, ' ', '')) IN (
                'approuve', 'approuv', 'verifie', 'paiementenattente', 'paiement_en_attente', 'payee'
               )`;

        const result = await request.query(`
            SELECT n.*,
                   c.nom + ' ' + c.prenom AS collaborateur,
                   c.departement, c.campus, c.societe,
                   v1.nom + ' ' + v1.prenom AS nomN1,
                
                   vf.nom + ' ' + vf.prenom AS nomVerificateur,
                   n.dateVerification, n.commentaireVerification
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id
          
            LEFT JOIN CollaborateurAD vf ON vf.id = n.verificateurFinanceId
            WHERE 1=1 ${statutFilter} ${campusWhere}
            ORDER BY n.DateCreation DESC
        `);

        res.json(result.recordset);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});


// POST /api/paiements/generer-xml
// Body: { noteIds: number[] }
// POST /api/paiements/generer-xml — Génère le XML PAIN.001 et passe statut à 'paiementenattente'
app.post('/api/paiements/generer-xml', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé Finance' });

    const { noteIds } = req.body;
    if (!Array.isArray(noteIds) || noteIds.length === 0)
        return res.status(400).json({ error: 'Aucune note sélectionnée' });

    try {
        const idList = noteIds.map(id => parseInt(id)).filter(Boolean).join(',');

        const notes = await pool.request().query(`
            SELECT n.id, n.reference, n.montant, n.libelle, n.date,
                   n.fichiers, n.lignesJson, n.categorie, n.DateCreation,
                   c.nom, c.prenom, c.iban, c.bic, c.campus, c.societe,
                   c.adresse_rue, c.adresse_cp, c.adresse_ville, c.adresse_pays,
                   c.id AS collabId, c.email
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            WHERE n.id IN (${idList})
              AND n.statut IN ('approuve', 'approuvé', 'verifie')
        `);

        if (!notes.recordset.length)
            return res.status(404).json({ error: 'Aucune note approuvée/vérifiée trouvée' });

        // ── Validation des données AVANT de générer quoi que ce soit ─────
        // On fait toutes les vérifications en une seule requête SQL (batch)
        const checksResult = await pool.request().query(`
            SELECT id AS collabId, IBAN, BIC, adresse_rue, adresse_cp, adresse_ville, adresse_pays,
                   nom, prenom
            FROM CollaborateurAD
            WHERE id IN (${notes.recordset.map(n => n.collabId).join(',')})
        `);
        const checksMap = {};
        for (const c of checksResult.recordset) checksMap[c.collabId] = c;

        const erreurs = [];
        for (const n of notes.recordset) {
            const c = checksMap[n.collabId];
            if (!c) { erreurs.push(`${n.reference} : collaborateur introuvable`); continue; }
            if (!c.IBAN) erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : IBAN manquant`);
            if (!c.BIC) erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : BIC manquant`);
            if (!c.adresse_rue || !c.adresse_cp || !c.adresse_ville || !c.adresse_pays)
                erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : adresse postale incomplète`);
        }
        if (erreurs.length > 0)
            return res.status(422).json({ error: 'Données manquantes — XML non généré', details: erreurs });

        const now = new Date();
        const annee = now.getFullYear();
        const mois = String(now.getMonth() + 1).padStart(2, '0');
        const todayISO = now.toISOString().split('T')[0];
        const creDtTm = now.toISOString().slice(0, 19);
        const msgId = `NDF-${annee}${mois}-${Date.now().toString().slice(-7)}`;
        const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0);
        const totalFormate = total.toFixed(2);

        // Détecter le campus dominant des notes sélectionnées
        const campusDominant = (() => {
            const campusCounts = {};
            for (const n of notes.recordset) {
                const code = normalizeCampus(n.campus || '') || n.campus || '';
                if (code) campusCounts[code] = (campusCounts[code] || 0) + 1;
            }
            // Campus le plus fréquent parmi les notes
            return Object.entries(campusCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || null;
        })();

        const cfg = await getConfigDebiteur(campusDominant);
        console.log(`🏦 Config débiteur utilisée : ${cfg.companyName} (campus: ${campusDominant || 'global'})`);
        const { companyName: dbtrNom, companyIban: dbtrIban, companyBic: dbtrBic,
            companyAddress: dbtrAdrLine, companyCp: dbtrCp,
            companyVille: dbtrVille, companyPays: dbtrPays } = cfg;

        // ── Générer les transactions XML ──────────────────────────────────
        let transactions = '';
        for (const n of notes.recordset) {
            let ibanClair = 'FR0000000000000000000000000';
            try {
                if (n.iban && n.iban.includes(':')) ibanClair = decryptIban(n.iban);
                else if (n.iban) ibanClair = n.iban;
            } catch (e) {
                console.warn(`⚠️ Déchiffrement IBAN impossible pour ${n.reference}:`, e.message);
            }

            const benefNom = `${n.nom.toUpperCase()} ${n.prenom}`;
            const adrLine = (n.adresse_rue || '').toUpperCase();
            const cp = n.adresse_cp || '';
            const ville = (n.adresse_ville || '').toUpperCase();
            const pays = (n.adresse_pays || 'FR').slice(0, 2).toUpperCase();
            const benefBicBlock = n.bic
                ? `<FinInstnId><BIC>${n.bic}</BIC></FinInstnId>`
                : `<FinInstnId><Othr><Id>NOTPROVIDED</Id></Othr></FinInstnId>`;

            transactions += `
    <CdtTrfTxInf>
      <PmtId>
        <InstrId>VIREMENT NUM:${n.reference}</InstrId>
        <EndToEndId>${n.reference}</EndToEndId>
      </PmtId>
      <Amt>
        <InstdAmt Ccy="EUR">${parseFloat(n.montant).toFixed(2)}</InstdAmt>
      </Amt>
      <CdtrAgt>
        ${benefBicBlock}
      </CdtrAgt>
      <Cdtr>
        <Nm>${benefNom}</Nm>${adrLine || cp || ville ? `
        <PstlAdr>${cp ? `
          <PstCd>${cp}</PstCd>` : ''}${ville ? `
          <TwnNm>${ville}</TwnNm>` : ''}
          <Ctry>${pays}</Ctry>${adrLine ? `
          <AdrLine>${adrLine}</AdrLine>` : ''}
        </PstlAdr>` : ''}
        <CtryOfRes>${pays}</CtryOfRes>
      </Cdtr>
      <CdtrAcct>
        <Id>
          <IBAN>${ibanClair}</IBAN>
        </Id>
      </CdtrAcct>
    </CdtTrfTxInf>`;
        }

        const xml = `<?xml version="1.0" encoding="iso-8859-1"?>
<Document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03">
  <CstmrCdtTrfInitn>
    <GrpHdr>
      <MsgId>${msgId}</MsgId>
      <CreDtTm>${creDtTm}</CreDtTm>
      <NbOfTxs>${notes.recordset.length}</NbOfTxs>
      <CtrlSum>${totalFormate}</CtrlSum>
      <InitgPty>
        <Nm>${dbtrNom}</Nm>
      </InitgPty>
    </GrpHdr>
    <PmtInf>
      <PmtInfId>${msgId}</PmtInfId>
      <PmtMtd>TRF</PmtMtd>
      <BtchBookg>true</BtchBookg>
      <NbOfTxs>${notes.recordset.length}</NbOfTxs>
      <CtrlSum>${totalFormate}</CtrlSum>
      <PmtTpInf>
        <SvcLvl>
          <Cd>SEPA</Cd>
        </SvcLvl>
      </PmtTpInf>
      <ReqdExctnDt>${todayISO}</ReqdExctnDt>
      <Dbtr>
        <Nm>${dbtrNom}</Nm>
        <PstlAdr>
          <PstCd>${dbtrCp}</PstCd>
          <TwnNm>${dbtrVille}</TwnNm>
          <Ctry>${dbtrPays}</Ctry>
          <AdrLine>${dbtrAdrLine}</AdrLine>
        </PstlAdr>
      </Dbtr>
      <DbtrAcct>
        <Id>
          <IBAN>${dbtrIban}</IBAN>
        </Id>
      </DbtrAcct>
      <DbtrAgt>
        <FinInstnId>
          <Othr>
            <Id>NOTPROVIDED</Id>
          </Othr>
        </FinInstnId>
      </DbtrAgt>
      <ChrgBr>SLEV</ChrgBr>${transactions}
    </PmtInf>
  </CstmrCdtTrfInitn>
</Document>`;

        // ── Passer en 'paiementenattente' + enregistrer date XML ─────────
        // Fait AVANT res.send pour que le statut soit correct immédiatement
        await pool.request()
            .input('dateXml', sql.DateTime, now)
            .query(`
                UPDATE NoteDeFrais
                SET statut = 'paiementenattente',
                    dateXml = @dateXml,
                    DateModification = GETDATE()
                WHERE id IN (${idList})
                  AND statut IN ('approuve', 'approuvé', 'verifie')
            `);

        // ── Réponse immédiate — le client reçoit le XML sans attendre ────
        const xmlFileName = `virements-ndf-${annee}-${mois}-${todayISO}.xml`;
        res.setHeader('Content-Type', 'application/xml; charset=iso-8859-1');
        res.setHeader('Content-Disposition', `attachment; filename=${xmlFileName}`);
        res.send(xml);

        // ── Tout le reste en arrière-plan (non bloquant) ─────────────────
        setImmediate(async () => {
            console.log(`🔄 [ASYNC] Post-XML : SharePoint + PDFs + notifs pour ${notes.recordset.length} note(s)...`);

            // 1. Upload XML sur SharePoint
            try {
                const spXmlFileName = `virements-ndf-${annee}-${mois}-${Date.now().toString().slice(-5)}.xml`;
                const xmlUploadPath = `Virements/${annee}/${mois}/${spXmlFileName}`;
                const accessToken = await getGraphToken();
                if (accessToken) {
                    await axios.put(
                        `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${xmlUploadPath}:/content`,
                        Buffer.from(xml, 'utf-8'),
                        { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/xml' }, maxBodyLength: Infinity }
                    );
                    console.log(`✅ [ASYNC] XML uploadé sur SharePoint : ${xmlUploadPath}`);
                }
            } catch (spErr) {
                console.error('⚠️ [ASYNC] Upload XML SharePoint échoué :', spErr.message);
            }

            // 2. Générer les PDFs récap + notifier en parallèle par note
            const tarifKm = await getTarifKm();

            await Promise.allSettled(notes.recordset.map(async (note) => {
                try {
                    // PDFs récap
                    let fichiersExistants = [];
                    try { fichiersExistants = JSON.parse(note.fichiers || '[]'); } catch { }

                    const justifFiles = [];
                    for (const f of fichiersExistants) {
                        const name = (f.fileName || '').toLowerCase();
                        if (name.includes('soumission') || name.includes('resoumission') || name.includes('recap')) continue;
                        try {
                            const buf = await downloadFromSharePoint(f.uploadUrl);
                            const mimetype = name.endsWith('.pdf') ? 'application/pdf'
                                : name.endsWith('.png') ? 'image/png' : 'image/jpeg';
                            justifFiles.push({ buffer: buf, originalname: f.fileName, mimetype, size: buf.length });
                        } catch (e) { console.warn(`⚠️ [ASYNC] Justif non récupérable: ${f.fileName}`, e.message); }
                    }

                    const dateObj = new Date(note.date);
                    const moisStr = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
                    const moisCapitalized = moisStr.charAt(0).toUpperCase() + moisStr.slice(1);
                    const nomPrenom = `${note.nom.toUpperCase()} ${note.prenom}`;

                    const histResult = await pool.request()
                        .input('noteId', sql.Int, note.id)
                        .query(`
                            SELECT h.Niveau, h.Action, h.Commentaire, h.DateAction,
                                   c.prenom + ' ' + c.nom AS nomPrenom
                            FROM HistoriqueValidation h
                            JOIN CollaborateurAD c ON c.id = h.ValidateurId
                            WHERE h.NoteDeFraisId = @noteId
                            ORDER BY h.DateAction ASC
                        `);

                    const signatures = [
                        { niveau: 'COLLAB', nomPrenom, date: note.DateCreation || new Date(), action: 'soumettre', commentaire: null },
                        ...histResult.recordset.map(h => ({
                            niveau: h.Niveau, nomPrenom: h.nomPrenom,
                            date: h.DateAction, action: h.Action, commentaire: h.Commentaire || null
                        }))
                    ];

                    const noteDataPDF = {
                        reference: note.reference, nomPrenom, mois: moisCapitalized,
                        date: note.date, categorie: note.categorie || 'Multiple',
                        libelle: note.libelle, montant: parseFloat(note.montant),
                        lignesJson: note.lignesJson, tarifKm,
                        statut: note.statut, departement: note.departement,
                    };

                    const recapBuffer = await generateRecapWithJustifs(noteDataPDF, justifFiles, signatures);

                    const existingFolder = fichiersExistants[0]?.folderPath;
                    const nomDossier = existingFolder
                        ? existingFolder.split('/')[1]
                        : `${note.prenom}_${note.nom}`.replace(/[^a-zA-Z0-9]/g, '_');
                    const moisDossier = existingFolder
                        ? existingFolder.split('/')[2]
                        : `${annee}-${mois}`;

                    const recapResult = await uploadToSharePointHierarchique(
                        { buffer: recapBuffer, originalname: `${note.reference}_recap-paiement.pdf`, mimetype: 'application/pdf', size: recapBuffer.length },
                        note.reference, nomDossier, moisDossier
                    );

                    fichiersExistants.push(recapResult);

                    await pool.request()
                        .input('id', sql.Int, note.id)
                        .input('fichiers', sql.NVarChar, JSON.stringify(fichiersExistants))
                        .input('recapUrl', sql.NVarChar, recapResult.uploadUrl)
                        .query(`
                            UPDATE NoteDeFrais
                            SET fichiers = @fichiers, sharepointUrl = @recapUrl, DateModification = GETDATE()
                            WHERE id = @id
                        `);

                    console.log(`✅ [ASYNC] PDF récap-paiement généré : ${note.reference}`);
                } catch (pdfErr) {
                    console.error(`❌ [ASYNC] PDF récap ${note.reference}:`, pdfErr.message);
                }

                // Notification collaborateur (indépendante du PDF)
                try {
                    await creerNotification({
                        destinataireId: note.collabId,
                        destinataireEmail: note.email,
                        type: 'paiement',
                        titre: `Paiement en cours de traitement : ${note.reference}`,
                        message: `Votre note ${note.reference} de ${parseFloat(note.montant).toFixed(2)} € est en cours de traitement bancaire.`,
                        noteId: note.id
                    });
                } catch (e) { console.error(`❌ [ASYNC] Notif ${note.reference}:`, e.message); }
            }));

            console.log(`✅ [ASYNC] Traitement post-XML terminé`);
        });

    } catch (error) {
        console.error('Erreur génération XML:', error.message);
        res.status(500).json({ error: error.message });
    }
});
// GET /api/paiements/xml-historique — liste les XML générés
app.get('/api/paiements/xml-historique', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur', 'President'))
        return res.status(403).json({ error: 'Accès réservé Finance' });
 
    try {
        const { annee, mois } = req.query;
 
        const request = pool.request();
        let where = `WHERE n.dateXml IS NOT NULL AND n.statut IN ('paiementenattente', 'payee')`;
 
        if (annee) { request.input('annee', sql.Int, parseInt(annee)); where += ` AND YEAR(n.dateXml) = @annee`; }
        if (mois) { request.input('mois', sql.Int, parseInt(mois)); where += ` AND MONTH(n.dateXml) = @mois`; }
 
        let campusWhere = '';
        if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur', 'President')) {
            const campusCode = normalizeCampus(req.user.campus);
            if (campusCode) {
                request.input('campus', sql.NVarChar, `%${campusCode}%`);
                campusWhere = `AND c.campus LIKE @campus`;
            }
        }
 
        const result = await request.query(`
            SELECT
            CONVERT(NVARCHAR(16), n.dateXml, 120) AS dateXmlJour, -- "2026-05-22 14:32"
            MIN(n.dateXml) AS dateXmlExacte,
            COUNT(*) AS nbNotes,
            SUM(n.montant) AS totalMontant,
            STRING_AGG(CAST(n.id AS NVARCHAR(20)), ',') AS noteIds,
            STRING_AGG(n.reference, ', ') AS listeReferences,
            MAX(CASE WHEN n.presidentId IS NOT NULL
            THEN p.prenom + ' ' + p.nom ELSE NULL END) AS presidentNom,
            MAX(n.dateValidationPresident) AS dateValidationPresident,
            MAX(n.commentairePresident) AS commentairePresident
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            LEFT JOIN CollaborateurAD p ON p.id = n.presidentId
            ${where} ${campusWhere}
            GROUP BY CONVERT(NVARCHAR(16), n.dateXml, 120)
            ORDER BY CONVERT(NVARCHAR(16), n.dateXml, 120) DESC
        `);
 
        res.json(result.recordset);
    } catch (error) {
        console.error('GET /api/paiements/xml-historique:', error.message);
        res.status(500).json({ error: error.message });
    }
});
// POST /api/paiements/regenerer-xml — régénère le XML pour un batch
app.post('/api/paiements/regenerer-xml', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé Finance' });

    const { noteIds } = req.body;
    if (!Array.isArray(noteIds) || noteIds.length === 0)
        return res.status(400).json({ error: 'Aucune note sélectionnée' });

    try {
        const idList = noteIds.map(id => parseInt(id)).filter(Boolean).join(',');

        const notes = await pool.request().query(`
            SELECT n.id, n.reference, n.montant, n.libelle, n.dateXml,
                   c.nom, c.prenom, c.iban, c.bic
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            WHERE n.id IN (${idList})
              AND n.statut IN ('paiementenattente', 'payee')
        `);

        if (!notes.recordset.length)
            return res.status(404).json({ error: 'Notes introuvables' });

        const now = new Date();
        const annee = now.getFullYear();
        const mois = String(now.getMonth() + 1).padStart(2, '0');
        const todayISO = now.toISOString().split('T')[0];
        const creDtTm = now.toISOString().slice(0, 19);
        const msgId = `NDF-REGEN-${annee}${mois}-${Date.now().toString().slice(-7)}`;
        const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0).toFixed(2);
        const campusDominant = (() => {
            const campusCounts = {};
            for (const n of notes.recordset) {
                const code = normalizeCampus(n.campus || '') || n.campus || '';
                if (code) campusCounts[code] = (campusCounts[code] || 0) + 1;
            }
            return Object.entries(campusCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || null;
        })();
        const cfg = await getConfigDebiteur(campusDominant);
        const dbtrNom = cfg.companyName;
        const dbtrIban = cfg.companyIban;
        const dbtrBic = cfg.companyBic;
        const dbtrAdrLine = cfg.companyAddress;
        const dbtrCp = cfg.companyCp;
        const dbtrVille = cfg.companyVille;
        const dbtrPays = cfg.companyPays;

        let transactions = '';
        for (const n of notes.recordset) {
            let ibanClair = 'FR0000000000000000000000000';
            try {
                if (n.iban && n.iban.includes(':')) ibanClair = decryptIban(n.iban);
                else if (n.iban) ibanClair = n.iban;
            } catch { }

            const benefNom = `${n.nom.toUpperCase()} ${n.prenom}`;
            const benefBicBlock = n.bic
                ? `<FinInstnId><BIC>${n.bic}</BIC></FinInstnId>`
                : `<FinInstnId><Othr><Id>NOTPROVIDED</Id></Othr></FinInstnId>`;

            transactions += `
    <CdtTrfTxInf>
      <PmtId>
        <InstrId>VIREMENT NUM:${n.reference}</InstrId>
        <EndToEndId>${n.reference}</EndToEndId>
      </PmtId>
      <Amt>
        <InstdAmt Ccy="EUR">${parseFloat(n.montant).toFixed(2)}</InstdAmt>
      </Amt>
      <CdtrAgt>${benefBicBlock}</CdtrAgt>
      <Cdtr><Nm>${benefNom}</Nm></Cdtr>
      <CdtrAcct><Id><IBAN>${ibanClair}</IBAN></Id></CdtrAcct>
    </CdtTrfTxInf>`;
        }

        const xml = `<?xml version="1.0" encoding="iso-8859-1"?>
<Document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03">
  <CstmrCdtTrfInitn>
    <GrpHdr>
      <MsgId>${msgId}</MsgId>
      <CreDtTm>${creDtTm}</CreDtTm>
      <NbOfTxs>${notes.recordset.length}</NbOfTxs>
      <CtrlSum>${total}</CtrlSum>
      <InitgPty><Nm>${dbtrNom}</Nm></InitgPty>
    </GrpHdr>
    <PmtInf>
      <PmtInfId>${msgId}</PmtInfId>
      <PmtMtd>TRF</PmtMtd>
      <BtchBookg>true</BtchBookg>
      <NbOfTxs>${notes.recordset.length}</NbOfTxs>
      <CtrlSum>${total}</CtrlSum>
      <PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl></PmtTpInf>
      <ReqdExctnDt>${todayISO}</ReqdExctnDt>
      <Dbtr>
        <Nm>${dbtrNom}</Nm>
        <PstlAdr>
          <PstCd>${dbtrCp}</PstCd>
          <TwnNm>${dbtrVille}</TwnNm>
          <Ctry>${dbtrPays}</Ctry>
          <AdrLine>${dbtrAdrLine}</AdrLine>
        </PstlAdr>
      </Dbtr>
      <DbtrAcct><Id><IBAN>${dbtrIban}</IBAN></Id></DbtrAcct>
      <DbtrAgt><FinInstnId><Othr><Id>NOTPROVIDED</Id></Othr></FinInstnId></DbtrAgt>
      <ChrgBr>SLEV</ChrgBr>${transactions}
    </PmtInf>
  </CstmrCdtTrfInitn>
</Document>`;

        const xmlFileName = `virements-ndf-REGEN-${todayISO}-${Date.now().toString().slice(-5)}.xml`;
        res.setHeader('Content-Type', 'application/xml; charset=iso-8859-1');
        res.setHeader('Content-Disposition', `attachment; filename=${xmlFileName}`);
        res.send(xml);

    } catch (error) {
        console.error('Erreur régénération XML:', error.message);
        res.status(500).json({ error: error.message });
    }
});

// GET /api/paiements/config-debiteur
// GET — récupérer toutes les configs actives (une par campus)
app.get('/api/paiements/config-debiteur', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé Finance' });
    try {
        const result = await pool.request().query(`
            SELECT id, companyName, companyIban, companyBic,
                   companyAddress, companyCp, companyVille, companyPays,
                   campus, DateModification
            FROM ConfigDebiteurXML WHERE actif = 1
            ORDER BY CASE WHEN campus IS NULL THEN 1 ELSE 0 END, campus
        `);
        res.json(result.recordset);
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

// PUT — créer/remplacer la config pour un campus donné
app.put('/api/paiements/config-debiteur', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé Finance' });

    const { companyName, companyIban, companyBic, companyAddress,
        companyCp, companyVille, companyPays, campus } = req.body;

    if (!companyName || !companyIban || !companyBic)
        return res.status(400).json({ error: 'Nom, IBAN et BIC sont obligatoires' });

    const ibanClean = companyIban.replace(/\s+/g, '').toUpperCase();
    const bicClean = companyBic.replace(/\s+/g, '').toUpperCase();
    const campusCode = campus ? (normalizeCampus(campus) || campus) : null;

    try {
        // Désactiver uniquement la config du même campus
        if (campusCode) {
            await pool.request()
                .input('campus', sql.NVarChar, campusCode)
                .query(`UPDATE ConfigDebiteurXML SET actif = 0 WHERE actif = 1 AND campus = @campus`);
        } else {
            await pool.request()
                .query(`UPDATE ConfigDebiteurXML SET actif = 0 WHERE actif = 1 AND campus IS NULL`);
        }

        await pool.request()
            .input('companyName', sql.NVarChar, companyName.trim())
            .input('companyIban', sql.NVarChar, ibanClean)
            .input('companyBic', sql.NVarChar, bicClean)
            .input('companyAddress', sql.NVarChar, (companyAddress || '').trim())
            .input('companyCp', sql.NVarChar, (companyCp || '').trim())
            .input('companyVille', sql.NVarChar, (companyVille || '').trim())
            .input('companyPays', sql.NVarChar, (companyPays || 'FR').trim().slice(0, 2).toUpperCase())
            .input('campus', sql.NVarChar, campusCode)
            .input('modifiePar', sql.Int, req.user.id)
            .query(`
                INSERT INTO ConfigDebiteurXML
                    (companyName, companyIban, companyBic, companyAddress,
                     companyCp, companyVille, companyPays, campus, actif, modifiePar,
                     DateCreation, DateModification)
                VALUES
                    (@companyName, @companyIban, @companyBic, @companyAddress,
                     @companyCp, @companyVille, @companyPays, @campus, 1, @modifiePar,
                     GETDATE(), GETDATE())
            `);

        res.json({ success: true, campus: campusCode, companyName, companyIban: ibanClean });
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

// PUT /api/paiements/config-debiteur

// POST /api/paiements/confirmer-paiement
// Body: { noteIds: number[], datePaiement: string (ISO) }
// POST /api/paiements/confirmer-paiement — Confirme paiement et passe statut à 'payee'
app.post('/api/paiements/confirmer-paiement', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé Finance' });

    const { noteIds, datePaiement } = req.body;
    if (!Array.isArray(noteIds) || noteIds.length === 0)
        return res.status(400).json({ error: 'Aucune note sélectionnée' });
    if (!datePaiement)
        return res.status(400).json({ error: 'Date de paiement obligatoire' });

    try {
        const idList = noteIds.map(id => parseInt(id)).filter(Boolean).join(',');
        const dateObj = new Date(datePaiement);
        const mois = dateObj.getMonth() + 1;
        const annee = dateObj.getFullYear();

        // Récupérer les notes pour notifications
        const notes = await pool.request().query(`
      SELECT n.id, n.reference, n.montant, n.libelle,
             c.id AS collabId, c.email, c.prenom, c.nom
      FROM NoteDeFrais n
      JOIN CollaborateurAD c ON c.id = n.collaborateurId
      WHERE n.id IN (${idList})
        AND n.statut = 'paiementenattente'
    `);

        if (!notes.recordset.length)
            return res.status(404).json({ error: 'Aucune note en attente de paiement trouvée' });

        // Mettre à jour statut → 'payee'
        await pool.request()
            .input('datePaiement', sql.DateTime, dateObj)
            .input('moisPaiement', sql.Int, mois)
            .input('anneePaiement', sql.Int, annee)
            .query(`
        UPDATE NoteDeFrais
        SET statut = 'payee',
            datePaiement = @datePaiement,
            moisPaiement = @moisPaiement,
            anneePaiement = @anneePaiement,
            DateModification = GETDATE()
        WHERE id IN (${idList})
          AND statut = 'paiementenattente'
      `);

        // Notifier chaque collaborateur
        for (const n of notes.recordset) {
            try {
                await creerNotification({
                    destinataireId: n.collabId,
                    destinataireEmail: n.email,
                    type: 'paiement',
                    titre: `Paiement effectué : ${n.reference}`,
                    message: `Votre note ${n.reference} de ${parseFloat(n.montant).toFixed(2)}€ a été payée le ${dateObj.toLocaleDateString('fr-FR')}.`,
                    noteId: n.id
                });
                await sendMailGraph(n.email, `Paiement effectué : ${n.reference}`, `
          <div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
            <div style="background:linear-gradient(135deg,#10b981,#059669);color:white;padding:24px;border-radius:12px 12px 0 0">
              <h2 style="margin:0">Paiement effectué</h2>
            </div>
            <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
              <p>Bonjour <strong>${n.prenom} ${n.nom}</strong>,</p>
              <p>Votre note <strong>${n.reference}</strong> — ${n.libelle} d'un montant de <strong>${parseFloat(n.montant).toFixed(2)}€</strong> a été payée le <strong>${dateObj.toLocaleDateString('fr-FR')}</strong>.</p>
              <div style="text-align:center;margin-top:24px">
                <a href="${process.env.FRONTEND_URL || 'myndf.ensup-adm.net'}" style="background:#6366f1;color:white;padding:13px 28px;text-decoration:none;border-radius:8px;font-weight:700">Voir mes notes</a>
              </div>
            </div>
          </div>`);
            } catch (e) { console.error('Notif paiement confirmé:', e.message); }
        }

        const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0);
        res.json({
            success: true,
            count: notes.recordset.length,
            total: parseFloat(total.toFixed(2)),
            datePaiement
        });
    } catch (error) {
        console.error('Erreur confirmer-paiement:', error.message);
        res.status(500).json({ error: error.message });
    }
});



// ================================================
// ROUTES DE TEST
// ================================================
app.get('/api/test-get-drive', async (req, res) => {
    try {
        const token = await getGraphToken();
        const siteId = 'ensup.sharepoint.com,d94abc08-28eb-47ce-8e12-fbbd6f16b9ea,a052c325-d33a-40e3-9e7b-7896a2ea7ab7';
        const r = await axios.get(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, { headers: { Authorization: `Bearer ${token}` } });
        res.json({ driveId: r.data.id, name: r.data.name });
    } catch (e) { res.status(500).json({ error: e.message, details: e.response?.data }); }
});

app.get('/api/test-upload', async (req, res) => {
    try {
        const token = await getGraphToken();
        const testContent = Buffer.from('Test upload NDF - ' + new Date().toISOString());
        const path = `Notes de Frais/TEST_Upload/test_${Date.now()}.txt`;
        const r = await axios.put(
            `https://graph.microsoft.com/v1.0/sites/${process.env.SHAREPOINT_SITE_ID}/drives/${process.env.SHAREPOINT_DRIVE_ID}/root:/${path}:/content`,
            testContent,
            { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'text/plain' } }
        );
        res.json({ ok: true, url: r.data.webUrl });
    } catch (e) { res.status(500).json({ error: e.message, details: e.response?.data }); }
});




// ══════════════════════════════════════════════════════════════════
// BROUILLONS — Sauvegarde serveur (statut = 'brouillon')
// ══════════════════════════════════════════════════════════════════

// GET /api/notes/brouillons — récupère les brouillons du collaborateur connecté
app.get('/api/notes/brouillons', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('collaborateurId', sql.Int, req.user.id)
            .query(`
                SELECT id, libelle, date, description, montant, categorie,
                       lignesJson, DateCreation, DateModification
                FROM NoteDeFrais
                WHERE collaborateurId = @collaborateurId
                  AND statut = 'brouillon'
                ORDER BY DateModification DESC
            `);
        res.json(result.recordset);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// POST /api/notes/brouillons — crée un nouveau brouillon
app.post('/api/notes/brouillons', authenticateToken, async (req, res) => {
    try {
        const { libelle, date, description, lignes } = req.body;

        let lignesJson = '[]';
        if (lignes) {
            lignesJson = typeof lignes === 'string' ? lignes : JSON.stringify(lignes);
        }

        // Calcul du montant total estimé
        let montantEstime = 0;
        try {
            const tarifKm = await getTarifKm();
            const lignesParsed = JSON.parse(lignesJson);
            montantEstime = lignesParsed.reduce((acc, l) => {
                const isKm = (l.categorie || '').toLowerCase().includes('kilom');
                const km = parseFloat(l.km) || 0;
                const ttc = isKm ? parseFloat((km * tarifKm).toFixed(2)) : (parseFloat(l.montant) || 0);
                return acc + ttc;
            }, 0);
        } catch (e) { /* montant reste 0 */ }

        // Référence temporaire pour les brouillons (colonne NOT NULL)
        const refBrouillon = `BRO-${req.user.id}-${Date.now().toString().slice(-6)}`;

        const insertResult = await pool.request()
            .input('collaborateurId', sql.Int, req.user.id)
            .input('reference', sql.NVarChar, refBrouillon)
            .input('libelle', sql.NVarChar, libelle || 'Brouillon sans titre')
            .input('date', sql.Date, date ? new Date(date) : new Date())
            .input('description', sql.NVarChar, description || null)
            .input('montant', sql.Decimal, montantEstime)
            .input('categorie', sql.NVarChar, 'Multiple')
            .input('lignesJson', sql.NVarChar, lignesJson)
            .input('statut', sql.NVarChar, 'brouillon')
            .query(`
                INSERT INTO NoteDeFrais
                    (reference, collaborateurId, libelle, date, description, montant,
                     categorie, lignesJson, statut, DateCreation, DateModification)
                OUTPUT INSERTED.id, INSERTED.DateCreation
                VALUES
                    (@reference, @collaborateurId, @libelle, @date, @description, @montant,
                     @categorie, @lignesJson, @statut, GETDATE(), GETDATE())
            `);

        const created = insertResult.recordset[0];
        res.status(201).json({ success: true, id: created.id, createdAt: created.DateCreation });
    } catch (error) {
        console.error('Erreur POST brouillon:', error.message);
        res.status(500).json({ error: error.message });
    }
});

// PUT /api/notes/brouillons/:id — met à jour un brouillon existant
app.put('/api/notes/brouillons/:id', authenticateToken, upload.any(), async (req, res) => {
    try {
        const { libelle, date, description, lignes } = req.body;
        const brouillonId = parseInt(req.params.id);

        // Vérifier que ce brouillon appartient bien à ce collaborateur
        const check = await pool.request()
            .input('id', sql.Int, brouillonId)
            .input('collaborateurId', sql.Int, req.user.id)
            .query(`SELECT id FROM NoteDeFrais
                    WHERE id = @id AND collaborateurId = @collaborateurId AND statut = 'brouillon'`);

        if (!check.recordset.length) {
            return res.status(404).json({ error: 'Brouillon introuvable ou accès refusé' });
        }

        // Récupérer infos collaborateur pour le dossier SharePoint
        const collabResult = await pool.request()
            .input('id', sql.Int, req.user.id)
            .query(`SELECT prenom, nom FROM CollaborateurAD WHERE id = @id`);
        const collaborateur = collabResult.recordset[0];
        const nomDossier = `${collaborateur.prenom}${collaborateur.nom}`.replace(/[^a-zA-Z0-9]/g, '_');
        const now = new Date();
        const moisDossier = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
        const noteRef = `BRO-${brouillonId}`;

        let lignesParsed = [];
        if (lignes) {
            try {
                lignesParsed = typeof lignes === 'string' ? JSON.parse(lignes) : lignes;
            } catch (e) { lignesParsed = []; }
        }

        const uploadedFiles = {}; 
        // ✅ Upload des nouveaux fichiers vers SharePoint et injection dans lignesParsed
        const allFiles = req.files || [];
        for (const file of allFiles) {
            const match = file.fieldname.match(/^files_(.+)$/);
            if (!match) continue;
            const depId = String(match[1]);
            try {
                const uploaded = await uploadToSharePointHierarchique(file, noteRef, nomDossier, moisDossier);
                const ligne = lignesParsed.find(l => String(l.id) === depId);
                if (ligne) {
                    if (!Array.isArray(ligne.qrFiles)) ligne.qrFiles = [];
                    const dejaSauve = ligne.qrFiles.some(f => f.fileName === uploaded.fileName);
                    if (!dejaSauve) {
                        // ✅ Stocker avec origin='upload'
                        ligne.qrFiles.push({
                            fileName: uploaded.fileName,
                            uploadUrl: uploaded.uploadUrl,
                            origin: 'upload' // ✅ AJOUT
                        });
                        // ✅ Tracker pour le retour
                        if (!uploadedFiles[depId]) uploadedFiles[depId] = [];
                        uploadedFiles[depId].push({
                            fileName: uploaded.fileName,
                            uploadUrl: uploaded.uploadUrl,
                            origin: 'upload'
                        });
                    }
                }
            } catch (e) {
                console.error(`Upload brouillon fichier ${file.originalname}:`, e.message);
            }
        }


        // Recalcul montant
        let montantEstime = 0;
        try {
            const tarifKm = await getTarifKm();
            montantEstime = lignesParsed.reduce((acc, l) => {
                const isKm = (l.categorie || '').toLowerCase().includes('kilom');
                const km = parseFloat(l.km) || 0;
                const ttc = isKm
                    ? parseFloat((km * tarifKm).toFixed(2))
                    : (parseFloat(l.montant) || parseFloat(l.tvaItems?.[0]?.montantTTC) || 0);
                return acc + ttc;
            }, 0);
        } catch (e) { /* montant reste 0 */ }

        const lignesJson = JSON.stringify(lignesParsed);

        await pool.request()
            .input('id', sql.Int, brouillonId)
            .input('libelle', sql.NVarChar, libelle || 'Brouillon sans titre')
            .input('date', sql.Date, date ? new Date(date) : new Date())
            .input('description', sql.NVarChar, description || null)
            .input('montant', sql.Decimal, montantEstime)
            .input('lignesJson', sql.NVarChar, lignesJson)
            .query(`
                UPDATE NoteDeFrais
                SET libelle          = @libelle,
                    date             = @date,
                    description      = @description,
                    montant          = @montant,
                    lignesJson       = @lignesJson,
                    DateModification = GETDATE()
                WHERE id = @id
            `);

        res.json({ success: true, updatedAt: new Date().toISOString(), uploadedFiles });
    } catch (error) {
        console.error('Erreur PUT brouillon:', error.message);
        res.status(500).json({ error: error.message });
    }
});

// DELETE /api/notes/brouillons/:id — supprime un brouillon
app.delete('/api/notes/brouillons/:id', authenticateToken, async (req, res) => {
    try {
        const brouillonId = parseInt(req.params.id);

        const check = await pool.request()
            .input('id', sql.Int, brouillonId)
            .input('collaborateurId', sql.Int, req.user.id)
            .query(`SELECT id FROM NoteDeFrais
                    WHERE id = @id AND collaborateurId = @collaborateurId AND statut = 'brouillon'`);

        if (!check.recordset.length) {
            return res.status(404).json({ error: 'Brouillon introuvable ou accès refusé' });
        }

        await pool.request()
            .input('id', sql.Int, brouillonId)
            .query(`DELETE FROM NoteDeFrais WHERE id = @id`);

        res.json({ success: true });
    } catch (error) {
        console.error('Erreur DELETE brouillon:', error.message);
        res.status(500).json({ error: error.message });
    }
});


// ══════════════════════════════════════════════════════════════════
// DOCUMENTS COLLABORATEUR — RIB / Carte grise / Permis
// Coller dans server.js après les routes /api/profil
// ══════════════════════════════════════════════════════════════════



// ============================================================
// DOCUMENTS COLLABORATEUR — RIB / Carte grise / Permis
// ============================================================
const DOCTYPES = ['rib', 'cartegrise', 'permis'];

// GET /api/profil/documents
app.get('/api/profil/documents', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('collabId', sql.Int, req.user.id)
            .query(`
                SELECT type, fileName, sharepointUrl, dateUpload, DateModification, statut, commentaire
                FROM DocumentsCollaborateur
                WHERE collaborateurId = @collabId
                AND type != 'rib'  -- ← exclure le rib de cette table
            `);

        // Vérifier si IBAN saisi directement dans CollaborateurAD
        const ibanResult = await pool.request()
            .input('collabId', sql.Int, req.user.id)
            .query(`SELECT IBAN FROM CollaborateurAD WHERE id = @collabId`);

        const ibanSaisi = !!(ibanResult.recordset[0]?.IBAN);

        const docs = {
            rib: ibanSaisi
                ? { fileName: 'IBAN_saisi', sharepointUrl: '', updatedAt: new Date().toISOString(), statut: 'valide', commentaire: null }
                : null,
            carte_grise: null,
            permis: null
        };

        for (const row of result.recordset) {
            docs[row.type] = {
                fileName: row.fileName,
                sharepointUrl: row.sharepointUrl,
                updatedAt: row.DateModification,
                statut: row.statut ?? 'en_attente',
                commentaire: row.commentaire ?? null
            };
        }

        res.json(docs);
    } catch (error) {
        console.error('GET /api/profil/documents', error.message);
        res.status(500).json({ error: error.message });
    }
});

// POST /api/profil/documents/:type  — upload ou remplacement
app.post('/api/profil/documents/:type', authenticateToken, upload.single('file'), async (req, res) => {
    try {
        const type = req.params.type;
        if (!DOCTYPES.includes(type))
            return res.status(400).json({ error: 'Type invalide. Valeurs : rib, cartegrise, permis' });
        if (!req.file)
            return res.status(400).json({ error: 'Aucun fichier fourni' });

        // Infos collaborateur (campus pour notifier la bonne Finance)
        const collabResult = await pool.request()
            .input('id', sql.Int, req.user.id)
            .query(`SELECT prenom, nom, campus FROM CollaborateurAD WHERE id = @id`);
        if (!collabResult.recordset.length)
            return res.status(404).json({ error: 'Collaborateur introuvable' });

        const { prenom, nom, campus } = collabResult.recordset[0];
        const nomDossier = `${prenom}${nom}`.replace(/[^a-zA-Z0-9]/g, '_');
        const safeFileName = `${type}_${req.file.originalname.replace(/[^a-zA-Z0-9.\-]/g, '_')}`;
        const folderPath = `Documents-Profil/${nomDossier}`;
        const uploadPath = `${folderPath}/${safeFileName}`;

        // Upload SharePoint
        const accessToken = await getGraphToken();
        if (!accessToken) throw new Error('Token Graph indisponible');

        const spRes = await axios.put(
            `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${uploadPath}:/content`,
            req.file.buffer,
            {
                headers: {
                    Authorization: `Bearer ${accessToken}`,
                    'Content-Type': req.file.mimetype || 'application/octet-stream'
                },
                maxBodyLength: Infinity
            }
        );
        const sharepointUrl = spRes.data.webUrl;

        // UPSERT — remet en attente si document remplacé
        await pool.request()
            .input('collabId', sql.Int, req.user.id)
            .input('type', sql.NVarChar, type)
            .input('fileName', sql.NVarChar, safeFileName)
            .input('sharepointUrl', sql.NVarChar, sharepointUrl)
            .query(`
        IF EXISTS (SELECT 1 FROM DocumentsCollaborateur WHERE collaborateurId = @collabId AND type = @type)
          UPDATE DocumentsCollaborateur
          SET fileName = @fileName, sharepointUrl = @sharepointUrl,
              DateModification = GETDATE(), dateUpload = GETDATE(),
              statut = 'en_attente', validePar = NULL, dateValidation = NULL, commentaire = NULL
          WHERE collaborateurId = @collabId AND type = @type
        ELSE
          INSERT INTO DocumentsCollaborateur (collaborateurId, type, fileName, sharepointUrl, dateUpload, DateModification, statut)
          VALUES (@collabId, @type, @fileName, @sharepointUrl, GETDATE(), GETDATE(), 'en_attente')
      `);

        // Notifier les Finance du même campus
        const typeLabels = { rib: 'RIB', cartegrise: 'Carte grise', permis: 'Permis de conduire' };
        const campusNorm = normalizeCampus(campus);
   const financeResult = await pool.request()
                        .input('campus', sql.NVarChar, campusNorm ? `%${campusNorm}%` : '%')
                        .query(`
SELECT DISTINCT c.id, c.email, c.prenom, c.nom
FROM CollaborateurAD c
JOIN UtilisateurRoles r ON r.collaborateur_id = c.id
WHERE r.role IN ('Finance', 'VerificateurFinance') AND r.actif = 1
  AND c.campus LIKE @campus AND c.Actif = 1
`);

        const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net';
        for (const finance of financeResult.recordset) {
            try {
                await creerNotification({
                    destinataireId: finance.id,
                    destinataireEmail: finance.email,
                    type: 'validationdoc',
                    titre: `Document à valider — ${prenom} ${nom}`,
                    message: `${prenom} ${nom} (${campus}) a soumis son ${typeLabels[type]} pour validation.`,
                    noteId: null
                });
            } catch (e) { console.error('Notif BDD Finance doc', e.message); }
            try {
                await sendMailGraph(
                    finance.email,
                    `Document à valider — ${typeLabels[type]} de ${prenom} ${nom}`,
                    `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
            <div style="background:linear-gradient(135deg,#6366f1,#4f46e5);color:white;padding:24px;border-radius:12px 12px 0 0">
              <h2 style="margin:0">Document à valider</h2>
            </div>
            <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
              <p>Bonjour <strong>${finance.prenom} ${finance.nom}</strong>,</p>
              <p><strong>${prenom} ${nom}</strong> (${campus}) a soumis son <strong>${typeLabels[type]}</strong> en attente de votre validation.</p>
              <div style="text-align:center;margin-top:24px">
                <a href="${frontendUrl}" style="background:#6366f1;color:white;padding:13px 28px;text-decoration:none;border-radius:8px;font-weight:700">
                  Valider les documents
                </a>
              </div>
            </div>
          </div>`
                );
            } catch (e) { console.error('Email Finance doc', e.message); }
        }

        res.json({ success: true, type, fileName: safeFileName, sharepointUrl, statut: 'en_attente', updatedAt: new Date().toISOString() });
    } catch (error) {
        console.error(`POST /api/profil/documents/${req.params.type}`, error.message);
        res.status(500).json({ error: error.message });
    }
});

// DELETE /api/profil/documents/:type
app.delete('/api/profil/documents/:type', authenticateToken, async (req, res) => {
    try {
        const type = req.params.type;
        if (!DOCTYPES.includes(type))
            return res.status(400).json({ error: 'Type invalide' });
        await pool.request()
            .input('collabId', sql.Int, req.user.id)
            .input('type', sql.NVarChar, type)
            .query(`DELETE FROM DocumentsCollaborateur WHERE collaborateurId = @collabId AND type = @type`);
        res.json({ success: true });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});


// GET /api/finance/documents-a-valider — Finance voit les docs en_attente de son campus
app.get('/api/finance/documents-a-valider', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'VerificateurFinance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé Finance' });
    try {
        const request = pool.request();
        let campusFilter = '';
        if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) {
            const campusCode = normalizeCampus(req.user.campus);
            if (campusCode) {
                request.input('campus', sql.NVarChar, `%${campusCode}%`);
                campusFilter = 'AND c.campus LIKE @campus';
            }
        }
        const result = await request.query(`
            SELECT d.id, d.collaborateurId, d.type, d.fileName, d.sharepointUrl,
                   d.dateUpload, d.DateModification, d.statut, d.commentaire,
                   c.prenom, c.nom, c.email, c.campus, c.departement
            FROM DocumentsCollaborateur d
            JOIN CollaborateurAD c ON c.id = d.collaborateurId
            WHERE d.statut = 'en_attente' ${campusFilter}
            ORDER BY d.DateModification ASC
        `);
        res.json(result.recordset);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// PUT /api/finance/documents/:id/valider — Finance valide ou refuse un document
// PUT /api/finance/documents/:id/valider
app.put('/api/finance/documents/:id/valider', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'VerificateurFinance', 'superUtilisateur')) 
        return res.status(403).json({ error: 'Accès réservé Finance' });
    try {
        const docId = parseInt(req.params.id);
        const { action, commentaire } = req.body;
        if (!['valider', 'refuser'].includes(action)) return res.status(400).json({ error: 'Action invalide' });

        const newStatut = action === 'valider' ? 'valide' : 'refuse';

        const docResult = await pool.request()
            .input('id', sql.Int, docId)
            .query(`SELECT d.*, c.prenom, c.nom, c.email, c.campus FROM DocumentsCollaborateur d JOIN CollaborateurAD c ON c.id = d.collaborateurId WHERE d.id = @id`);
        if (!docResult.recordset.length) return res.status(404).json({ error: 'Document introuvable' });

        const doc = docResult.recordset[0];

        await pool.request()
            .input('id', sql.Int, docId)
            .input('statut', sql.NVarChar, newStatut)
            .input('validePar', sql.Int, req.user.id)
            .input('commentaire', sql.NVarChar, commentaire || null)
            .query(`
        UPDATE DocumentsCollaborateur
        SET statut = @statut, validePar = @validePar,
            dateValidation = GETDATE(), commentaire = @commentaire, DateModification = GETDATE()
        WHERE id = @id
      `);

        const typeLabels = { rib: 'RIB', carte_grise: 'Carte grise', cartegrise: 'Carte grise', permis: 'Permis de conduire' };
        const isValide = newStatut === 'valide';
        const titre = isValide ? `${typeLabels[doc.type] || doc.type} validé ✅` : `${typeLabels[doc.type] || doc.type} refusé ❌`;
        const message = isValide
            ? `Votre ${typeLabels[doc.type] || doc.type} a été validé par la Finance.`
            : `Votre ${typeLabels[doc.type] || doc.type} a été refusé.${commentaire ? ' Motif : ' + commentaire : ''}`;

        try {
            await creerNotification({ destinataireId: doc.collaborateurId, destinataireEmail: doc.email, type: isValide ? 'doc_valide' : 'doc_refuse', titre, message, noteId: null });
        } catch (e) { console.error('Notif doc validé', e.message); }

        try {
            const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net';
            await sendMailGraph(doc.email, titre,
                `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
          <div style="background:linear-gradient(135deg,${isValide ? '#10b981,#059669' : '#ef4444,#dc2626'});color:white;padding:24px;border-radius:12px 12px 0 0">
            <h2 style="margin:0">${titre}</h2>
          </div>
          <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
            <p>Bonjour <strong>${doc.prenom} ${doc.nom}</strong>,</p>
            <p>${message}</p>
            ${!isValide ? '<p>Veuillez soumettre un nouveau document corrigé depuis votre profil.</p>' : ''}
            <div style="text-align:center;margin-top:24px">
              <a href="${frontendUrl}" style="background:#6366f1;color:white;padding:13px 28px;text-decoration:none;border-radius:8px;font-weight:700">Accéder à mon profil</a>
            </div>
          </div>
        </div>`
            );
        } catch (e) { console.error('Email doc validé', e.message); }

        res.json({ success: true, statut: newStatut });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// GET /api/profil/docs-statut — vérifie si le collaborateur peut soumettre (docs validés)
app.get('/api/profil/docs-statut', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('collaborateurId', sql.Int, req.user.id)
            .query(`SELECT type, statut FROM DocumentsCollaborateur WHERE collaborateurId = @collaborateurId`);
        const map = {};
        for (const row of result.recordset) map[row.type] = row.statut;
        res.json({
            rib: map['rib'] || 'absent',
            carte_grise: map['carte_grise'] || 'absent',
            permis: map['permis'] || 'absent',
        });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// ── PUT /api/profil/iban ─────────────────────────────────────────
app.put('/api/profil/iban', authenticateToken, async (req, res) => {
    try {
        let { iban, bic } = req.body;
        if (!iban) return res.status(400).json({ error: 'IBAN obligatoire' });
        if (!bic) return res.status(400).json({ error: 'BIC obligatoire' });

        iban = iban.replace(/\s+/g, '').toUpperCase();
        bic = bic.replace(/\s+/g, '').toUpperCase();

        if (!validateIban(iban))
            return res.status(400).json({ error: 'IBAN invalide (format ou checksum incorrect)' });

        const ibanChiffre = encryptIban(iban);
        const ibanHash = hashIban(iban);

        await pool.request()
            .input('id', sql.Int, req.user.id)
            .input('iban', sql.NVarChar, ibanChiffre)
            .input('ibanHash', sql.NVarChar, ibanHash)
            .input('bic', sql.NVarChar, bic)
            .query(`
                UPDATE CollaborateurAD
                SET IBAN = @iban, IBAN_HASH = @ibanHash, BIC = @bic, DateModification = GETDATE()
                WHERE id = @id
            `);

        // ← Le second bloc DocumentsCollaborateur est supprimé

        res.json({ success: true, iban: maskIban(iban), bic, statut: 'valide' });
    } catch (error) {
        console.error('PUT /api/profil/iban :', error.message);
        res.status(500).json({ error: 'Erreur serveur' });
    }
});

// ── GET /api/profil/iban ─────────────────────────────────────────
app.get('/api/profil/iban', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('id', sql.Int, req.user.id)
            .query(`SELECT IBAN, BIC FROM CollaborateurAD WHERE id = @id`);

        const row = result.recordset[0];
        if (!row) return res.status(404).json({ error: 'Utilisateur introuvable' });

        let ibanMasque = null;
        if (row.IBAN) {
            try {
                const ibanClair = decryptIban(row.IBAN);
                ibanMasque = maskIban(ibanClair);
            } catch {
                // IBAN encore en clair en base (avant migration) — on masque directement
                ibanMasque = maskIban(row.IBAN);
            }
        }

        res.json({
            iban: ibanMasque,
            ibanSaisi: !!row.IBAN,
            bic: row.BIC || null,
        });
    } catch (error) {
        console.error('GET /api/profil/iban :', error.message);
        res.status(500).json({ error: 'Erreur serveur' });
    }
});

app.post('/api/verificateur/notes/:id/non-conforme', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès refusé' });

    const { fileName, motif } = req.body;
    const noteId = parseInt(req.params.id);

    try {
        const noteResult = await pool.request()
            .input('id', sql.Int, noteId)
            .query(`
                SELECT 
                    n.id, n.reference, n.libelle, n.montant,
                    n.collaborateurId,
                    c.prenom, c.nom, c.email,
                    v1.id       AS n1Id,
                    v1.email    AS emailN1,
                    v1.prenom   AS prenomN1,
                    v1.nom      AS nomN1
                FROM NoteDeFrais n
                JOIN CollaborateurAD c  ON c.id = n.collaborateurId
                LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id
                WHERE n.id = @id
            `);

        if (!noteResult.recordset.length)
            return res.status(404).json({ error: 'Note introuvable' });

        const note = noteResult.recordset[0];

        // ✅ NOUVEAU — sauvegarder en BDD
        await pool.request()
            .input('noteId', sql.Int, noteId)
            .input('verificateurId', sql.Int, req.user.id)
            .input('fileName', sql.NVarChar, fileName)
            .input('motif', sql.NVarChar, motif)
            .query(`
                INSERT INTO JustificatifsNonConformes
                    (noteDeFraisId, verificateurId, fileName, motif, statut, dateSignalement)
                VALUES
                    (@noteId, @verificateurId, @fileName, @motif, 'non_conforme', GETDATE())
            `);
        await pool.request()
            .input('id', sql.Int, noteId)
            .query(`
        UPDATE NoteDeFrais SET
            statut = 'non_conforme_verif',
            DateModification = GETDATE()
        WHERE id = @id AND statut = 'approuve'
    `);

        // ✅ NOUVEAU — tracer dans HistoriqueValidation
        await pool.request()
            .input('noteId', sql.Int, noteId)
            .input('validateurId', sql.Int, req.user.id)
            .input('commentaire', sql.NVarChar, `Justificatif non conforme : "${fileName}" — ${motif}`)
            .input('statut', sql.NVarChar, 'approuve')
            .query(`
                INSERT INTO HistoriqueValidation
                    (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, NouveauStatut, DateAction)
                VALUES
                    (@noteId, @validateurId, 'VERIF', 'non_conforme', @commentaire, @statut, GETDATE())
            `);

        const verificateurNom = `${req.user.prenom} ${req.user.nom}`;
        const montantFormate = parseFloat(note.montant).toFixed(2);
        const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net';

        // Notifications (votre code existant inchangé)
        try {
            await creerNotification({
                destinataireId: note.collaborateurId,
                destinataireEmail: note.email,
                type: 'refus',
                titre: `⚠️ Justificatif non conforme — ${note.reference}`,
                message: `Le justificatif "${fileName}" de votre note ${note.reference} est non conforme. Motif : ${motif}`,
                noteId
            });
        } catch (e) { console.error('Notif BDD collab non-conforme:', e.message); }


        try {
            await sendMailGraph(
                note.email,
                `⚠️ Justificatif non conforme — ${note.reference}`,
                `<div style="font-family:Arial,sans-serif;max-width:620px;margin:0 auto">
                    <div style="background:linear-gradient(135deg,#ef4444,#dc2626);color:white;padding:24px;border-radius:12px 12px 0 0">
                        <h2 style="margin:0;font-size:18px">⚠️ Justificatif non conforme</h2>
                        <p style="margin:8px 0 0;opacity:.85;font-size:13px">Une correction est nécessaire</p>
                    </div>
                    <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                        <p>Bonjour <strong>${note.prenom} ${note.nom}</strong>,</p>
                        <p>Le vérificateur Finance <strong>${verificateurNom}</strong> a signalé 
                        un justificatif non conforme sur votre note <strong>${note.reference}</strong>.</p>

                        <div style="background:#fef2f2;border:1.5px solid #fecaca;border-left:4px solid #ef4444;
                                    border-radius:0 8px 8px 0;padding:16px;margin:20px 0">
                            <div style="font-size:12px;font-weight:700;color:#991b1b;
                                        text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">
                                Justificatif concerné
                            </div>
                            <div style="font-size:13px;color:#dc2626;font-weight:600;margin-bottom:10px">
                                📄 ${fileName}
                            </div>
                            <div style="font-size:12px;font-weight:700;color:#991b1b;
                                        text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">
                                Motif
                            </div>
                            <div style="font-size:14px;color:#dc2626;font-weight:600">${motif}</div>
                        </div>

                        <div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;
                                    padding:16px;margin:16px 0">
                            <table style="width:100%;border-collapse:collapse;font-size:13px">
                                <tr><td style="color:#64748b;padding:4px 0;width:140px">Référence</td>
                                    <td style="font-weight:700;font-family:monospace;color:#6366f1">${note.reference}</td></tr>
                                <tr><td style="color:#64748b;padding:4px 0">Libellé</td>
                                    <td style="font-weight:600">${note.libelle}</td></tr>
                                <tr><td style="color:#64748b;padding:4px 0">Montant</td>
                                    <td style="font-weight:700">${montantFormate} €</td></tr>
                            </table>
                        </div>

                        <div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;
                                    padding:16px;margin:16px 0">
                            <div style="font-size:13px;font-weight:700;color:#1e40af;margin-bottom:8px">
                                📝 Que faire ?
                            </div>
                            <ol style="margin:0;padding-left:18px;font-size:13px;color:#1d4ed8;line-height:2">
                                <li>Retrouvez le justificatif original corrigé</li>
                                <li>Contactez votre responsable ou la Finance</li>
                                <li>Soumettez un nouveau justificatif via la plateforme</li>
                            </ol>
                        </div>

                        <div style="text-align:center;margin-top:28px">
                            <a href="${frontendUrl}"
                               style="background:linear-gradient(135deg,#6366f1,#4f46e5);color:white;
                                      padding:14px 36px;text-decoration:none;border-radius:8px;
                                      font-weight:700;font-size:14px;display:inline-block">
                                Accéder à ma note →
                            </a>
                        </div>
                    </div>
                </div>`
            );
        } catch (e) { console.error('Email collab non-conforme:', e.message); }

        // ── Notifier le validateur N1 ──
        if (note.n1Id && note.emailN1) {
            try {
                await creerNotification({
                    destinataireId: note.n1Id,
                    destinataireEmail: note.emailN1,
                    type: 'refus',
                    titre: `⚠️ Justificatif non conforme — ${note.reference}`,
                    message: `La note ${note.reference} de ${note.prenom} ${note.nom} comporte un justificatif non conforme : "${fileName}". Motif : ${motif}`,
                    noteId
                });
            } catch (e) { console.error('Notif BDD N1 non-conforme:', e.message); }

            try {
                await sendMailGraph(
                    note.emailN1,
                    `⚠️ Note ${note.reference} — justificatif non conforme`,
                    `<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
                        <div style="background:linear-gradient(135deg,#f59e0b,#d97706);color:white;
                                    padding:20px;border-radius:12px 12px 0 0">
                            <h2 style="margin:0">⚠️ Justificatif non conforme signalé</h2>
                        </div>
                        <div style="padding:20px;background:#f8fafc;border:1px solid #e2e8f0;
                                    border-top:none;border-radius:0 0 12px 12px">
                            <p>Bonjour <strong>${note.prenomN1} ${note.nomN1}</strong>,</p>
                            <p>Le vérificateur Finance <strong>${verificateurNom}</strong> a signalé 
                            un justificatif non conforme sur la note 
                            <strong>${note.reference}</strong> de 
                            <strong>${note.prenom} ${note.nom}</strong>.</p>
                            <div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;
                                        padding:14px;margin:16px 0">
                                <div style="font-size:13px;font-weight:700;color:#92400e">
                                    📄 ${fileName}
                                </div>
                                <div style="font-size:13px;color:#b45309;margin-top:6px">
                                    Motif : ${motif}
                                </div>
                            </div>
                            <p style="font-size:13px;color:#64748b">
                                Le collaborateur a été notifié et doit fournir un justificatif corrigé.
                            </p>
                        </div>
                    </div>`
                );
            } catch (e) { console.error('Email N1 non-conforme:', e.message); }
        }

        res.json({
            success: true,
            notifiedCollab: true,
            notifiedN1: !!(note.n1Id && note.emailN1)
        });

    } catch (error) {
        console.error('Erreur POST non-conforme:', error.message);
        res.status(500).json({ error: error.message });
    }
});

app.get('/api/paiements/filtres-disponibles', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé Finance' });

    try {
        const result = await pool.request().query(`
            SELECT DISTINCT
                c.campus,
                c.societe
            FROM CollaborateurAD c
            WHERE c.Actif = 1
              AND c.campus IS NOT NULL
              AND c.campus != ''
            ORDER BY c.campus, c.societe
        `);

        const campusSet = new Set();
        // Map : campusCode → Set de sociétés
        const societeParCampus = {};
        const societeSet = new Set();

        for (const row of result.recordset) {
            if (row.campus) {
                const c = row.campus.toUpperCase();
                const code = 
                    c.includes('SQY') || c.includes('SAINT') ? 'SQY' :
                        c.includes('CGY') || c.includes('CERGY') ? 'CGY' :
                            c.includes('MRS') || c.includes('MARSEILLE') ? 'MRS' :
                                c.includes('NTE') || c.includes('NANTES') ? 'NTE' :
                                    row.campus;

                campusSet.add(code);

                // Grouper les sociétés par campus normalisé
                if (!societeParCampus[code]) societeParCampus[code] = new Set();

                if (row.societe && row.societe.trim()) {
                    societeParCampus[code].add(row.societe.trim());
                    societeSet.add(row.societe.trim());
                }
            }
        }

        // Convertir les Sets en tableaux triés
        const societeParCampusFinal = {};
        for (const [campus, set] of Object.entries(societeParCampus)) {
            societeParCampusFinal[campus] = [...set].sort();
        }

        res.json({
            campus: [...campusSet].sort(),
            societes: [...societeSet].sort(),          // toutes sociétés (fallback)
            societeParCampus: societeParCampusFinal,   // sociétés par campus ← nouveau
        });

    } catch (error) {
        console.error('GET /api/paiements/filtres-disponibles:', error.message);
        res.status(500).json({ error: error.message });
    }
});
app.get('/api/verificateur/notes/:id/non-conformes', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès refusé' });

    try {
        const result = await pool.request()
            .input('noteId', sql.Int, parseInt(req.params.id))
            .query(`
                SELECT j.id, j.fileName, j.motif, j.statut, j.dateSignalement,
                       c.prenom + ' ' + c.nom AS verificateur
                FROM JustificatifsNonConformes j
                JOIN CollaborateurAD c ON c.id = j.verificateurId
                WHERE j.noteDeFraisId = @noteId
                ORDER BY j.dateSignalement DESC
            `);
        res.json(result.recordset);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// =========================================================
// ROUTES À AJOUTER DANS server.js
// Coller après les routes /api/profil/iban existantes
// =========================================================

// ── GET /api/profil/vehicule ─────────────────────────────
app.get('/api/profil/vehicule', authenticateToken, async (req, res) => {
    try {
        const result = await pool.request()
            .input('id', sql.Int, req.user.id)
            .query(`
                SELECT chevauxFiscaux, vehiculeMarque, vehiculeModele,
                       vehiculeImmat, vehiculeDateMaj
                FROM CollaborateurAD
                WHERE id = @id
            `);

        const row = result.recordset[0];
        if (!row) return res.status(404).json({ error: 'Utilisateur introuvable' });

        // Retourner null si pas encore configuré
        if (!row.chevauxFiscaux) {
            return res.json({ configured: false, vehicule: null });
        }

        res.json({
            configured: true,
            vehicule: {
                chevaux: row.chevauxFiscaux,
                marque: row.vehiculeMarque || '',
                modele: row.vehiculeModele || '',
                immatriculation: row.vehiculeImmat || '',
                dateMaj: row.vehiculeDateMaj,
            }
        });
    } catch (error) {
        console.error('GET /api/profil/vehicule :', error.message);
        res.status(500).json({ error: 'Erreur serveur' });
    }
});

// ── PUT /api/profil/vehicule ─────────────────────────────
app.put('/api/profil/vehicule', authenticateToken, async (req, res) => {
    try {
        const { chevaux, marque, modele, immatriculation } = req.body;

        // Validation
        const cv = parseInt(chevaux);
        if (!cv || cv < 3 || cv > 7) {
            return res.status(400).json({
                error: 'Cheval fiscal invalide (valeurs acceptées : 3, 4, 5, 6, 7)'
            });
        }

        const immatClean = (immatriculation || '')
            .replace(/\s+/g, '-')
            .toUpperCase()
            .slice(0, 20);

        await pool.request()
            .input('id', sql.Int, req.user.id)
            .input('chevaux', sql.Int, cv)
            .input('marque', sql.NVarChar, (marque || '').trim().slice(0, 100))
            .input('modele', sql.NVarChar, (modele || '').trim().slice(0, 100))
            .input('immat', sql.NVarChar, immatClean)
            .query(`
                UPDATE CollaborateurAD SET
                    chevauxFiscaux    = @chevaux,
                    vehiculeMarque    = @marque,
                    vehiculeModele    = @modele,
                    vehiculeImmat     = @immat,
                    vehiculeDateMaj   = GETDATE(),
                    DateModification  = GETDATE()
                WHERE id = @id
            `);

        console.log(`✅ Profil véhicule mis à jour — user ${req.user.email} : ${cv} CV`);

        res.json({
            success: true,
            vehicule: { chevaux: cv, marque: marque || '', modele: modele || '', immatriculation: immatClean }
        });
    } catch (error) {
        console.error('PUT /api/profil/vehicule :', error.message);
        res.status(500).json({ error: 'Erreur serveur' });
    }
});

// ── DELETE /api/profil/vehicule ──────────────────────────
app.delete('/api/profil/vehicule', authenticateToken, async (req, res) => {
    try {
        await pool.request()
            .input('id', sql.Int, req.user.id)
            .query(`
                UPDATE CollaborateurAD SET
                    chevauxFiscaux   = NULL,
                    vehiculeMarque   = NULL,
                    vehiculeModele   = NULL,
                    vehiculeImmat    = NULL,
                    vehiculeDateMaj  = NULL,
                    DateModification = GETDATE()
                WHERE id = @id
            `);
        res.json({ success: true });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});


app.post('/api/paiements/soumettre-president', authenticateToken, async (req, res) => {
    if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur'))
        return res.status(403).json({ error: 'Accès réservé Finance / ValidateurFinance' });
 
    const { noteIds } = req.body;
    if (!Array.isArray(noteIds) || noteIds.length === 0)
        return res.status(400).json({ error: 'Aucune note sélectionnée' });
 
    try {
        const idList = noteIds.map(id => parseInt(id)).filter(Boolean).join(',');
 
        // Vérifier que toutes les notes sont bien au statut 'verifie'
        const notes = await pool.request().query(`
            SELECT n.id, n.reference, n.montant, n.libelle, n.statut,
                   c.prenom, c.nom, c.email, c.campus, c.id AS collabId
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            WHERE n.id IN (${idList})
              AND n.statut IN ('approuve', 'verifie')
        `);
 
        if (!notes.recordset.length)
            return res.status(404).json({ error: 'Aucune note approuvée/vérifiée trouvée' });
 
        // Passer en 'en_attente_president'
        await pool.request().query(`
            UPDATE NoteDeFrais
            SET presidentValidation = 'en_attente_president',
                statut = 'en_attente_president',
                DateModification = GETDATE()
            WHERE id IN (${idList})
              AND statut IN ('approuve', 'verifie')
        `);
 
        // Historique
        for (const note of notes.recordset) {
            try {
                await pool.request()
                    .input('noteId', sql.Int, note.id)
                    .input('validateurId', sql.Int, req.user.id)
                    .input('commentaire', sql.NVarChar, `Soumis au Président par ${req.user.prenom} ${req.user.nom} (${notes.recordset.length} note(s))`)
                    .input('statut', sql.NVarChar, 'en_attente_president')
                    .query(`
                        INSERT INTO HistoriqueValidation
                            (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, NouveauStatut, DateAction)
                        VALUES
                            (@noteId, @validateurId, 'PRESIDENT', 'soumettre', @commentaire, @statut, GETDATE())
                    `);
            } catch (e) { console.error('Histo President soumettre:', e.message); }
        }
 
        // Trouver le(s) Président(s) → notifier
        const presidents = await pool.request().query(`
            SELECT c.id, c.email, c.prenom, c.nom
            FROM CollaborateurAD c
            JOIN UtilisateurRoles r ON r.collaborateur_id = c.id
            WHERE r.role = 'President' AND r.actif = 1 AND c.Actif = 1
        `);
 
        const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0).toFixed(2);
        const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net';
        const validateurNom = `${req.user.prenom} ${req.user.nom}`;
 
        for (const president of presidents.recordset) {
            try {
                await creerNotification({
                    destinataireId: president.id,
                    destinataireEmail: president.email,
                    type: 'validation',
                    titre: `💼 ${notes.recordset.length} note(s) en attente de votre validation`,
                    message: `${validateurNom} vous soumet ${notes.recordset.length} note(s) de frais pour un total de ${total} € en attente de votre validation avant virement.`,
                    noteId: null
                });
            } catch (e) { console.error('Notif President BDD:', e.message); }
 
            try {
                const lignesNotes = notes.recordset.map(n =>
                    `<tr style="border-bottom:1px solid #e2e8f0">
                        <td style="padding:8px 12px;font-family:monospace;font-size:12px;color:#6366f1;font-weight:700">${n.reference}</td>
                        <td style="padding:8px 12px;font-size:13px">${n.prenom} ${n.nom}</td>
                        <td style="padding:8px 12px;font-size:13px;font-weight:700">${parseFloat(n.montant).toFixed(2)} €</td>
                    </tr>`
                ).join('');
 
                await sendMailGraph(
                    president.email,
                    `💼 ${notes.recordset.length} note(s) de frais en attente de votre validation`,
                    `<div style="font-family:Arial,sans-serif;max-width:640px;margin:0 auto">
                        <div style="background:linear-gradient(135deg,#1e3a5f,#1d4ed8);color:white;padding:28px;border-radius:12px 12px 0 0">
                            <h2 style="margin:0;font-size:20px">💼 Notes de frais — Validation Président</h2>
                            <p style="margin:10px 0 0;opacity:.85;font-size:13px">${notes.recordset.length} note(s) soumises par ${validateurNom}</p>
                        </div>
                        <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                            <p>Bonjour <strong>${president.prenom} ${president.nom}</strong>,</p>
                            <p>${validateurNom} vous soumet les notes de frais suivantes pour validation avant génération du virement bancaire :</p>
                            <div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;margin:20px 0">
                                <table style="width:100%;border-collapse:collapse">
                                    <thead>
                                        <tr style="background:#f1f5f9">
                                            <th style="padding:10px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">Référence</th>
                                            <th style="padding:10px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">Collaborateur</th>
                                            <th style="padding:10px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">Montant</th>
                                        </tr>
                                    </thead>
                                    <tbody>${lignesNotes}</tbody>
                                    <tfoot>
                                        <tr style="background:#f0fdf4;border-top:2px solid #86efac">
                                            <td colspan="2" style="padding:10px 12px;font-weight:700;color:#15803d;font-size:13px">Total à virer</td>
                                            <td style="padding:10px 12px;font-weight:900;color:#15803d;font-size:15px">${total} €</td>
                                        </tr>
                                    </tfoot>
                                </table>
                            </div>
                            <div style="text-align:center;margin-top:28px">
                                <a href="${frontendUrl}" style="background:linear-gradient(135deg,#1d4ed8,#1e40af);color:white;padding:14px 36px;text-decoration:none;border-radius:8px;font-weight:700;display:inline-block">
                                    ✅ Valider et générer le virement →
                                </a>
                            </div>
                            <p style="font-size:12px;color:#64748b;margin-top:20px;text-align:center">
                                Cette validation est définitive. Le fichier XML de virement sera généré automatiquement.
                            </p>
                        </div>
                    </div>`
                );
            } catch (e) { console.error('Email President:', e.message); }
        }
 
        res.json({
            success: true,
            nbNotes: notes.recordset.length,
            total: parseFloat(total),
            presidentsNotifies: presidents.recordset.length
        });
 
    } catch (error) {
        console.error('Erreur POST /api/paiements/soumettre-president:', error.message);
        res.status(500).json({ error: error.message });
    }
});
 
    // ══════════════════════════════════════════════════════════════════════════
    // 2. Président → Récupérer les notes en attente de sa validation
    //    GET /api/president/notes
    // ══════════════════════════════════════════════════════════════════════════
    app.get('/api/president/notes', authenticateToken, async (req, res) => {
        if (!hasAnyRole(req.user, 'President', 'superUtilisateur'))
            return res.status(403).json({ error: 'Accès réservé au Président' });
 
        try {
            const result = await pool.request().query(`
            SELECT n.*,
                   c.nom + ' ' + c.prenom AS collaborateur,
                   c.email AS collaborateurEmail,
                   c.departement, c.campus, c.societe,
                   v1.nom + ' ' + v1.prenom AS nomN1,
                   vf.nom + ' ' + vf.prenom AS nomVerificateur,
                   n.dateVerification, n.commentaireVerification
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id
            LEFT JOIN CollaborateurAD vf ON vf.id = n.verificateurFinanceId
            WHERE n.statut = 'en_attente_president'
              AND n.presidentValidation = 'en_attente_president'
            ORDER BY n.DateModification DESC
        `);
 
            res.json(result.recordset);
        } catch (error) {
            console.error('GET /api/president/notes:', error.message);
            res.status(500).json({ error: error.message });
        }
    });
 
    // ══════════════════════════════════════════════════════════════════════════
    // 3. Président → Générer le XML + valider les notes
    //    POST /api/president/generer-xml
    //    Body: { noteIds: number[], commentaire?: string }
    // ══════════════════════════════════════════════════════════════════════════
    app.post('/api/president/generer-xml', authenticateToken, async (req, res) => {
        if (!hasAnyRole(req.user, 'President', 'superUtilisateur'))
            return res.status(403).json({ error: 'Accès réservé au Président' });
 
        const { noteIds, commentaire } = req.body;
        if (!Array.isArray(noteIds) || noteIds.length === 0)
            return res.status(400).json({ error: 'Aucune note sélectionnée' });
 
        try {
            const idList = noteIds.map(id => parseInt(id)).filter(Boolean).join(',');
 
            const notes = await pool.request().query(`
            SELECT n.id, n.reference, n.montant, n.libelle, n.date,
                   n.fichiers, n.lignesJson, n.categorie, n.DateCreation,
                   c.nom, c.prenom, c.iban, c.bic, c.campus, c.societe,
                   c.adresse_rue, c.adresse_cp, c.adresse_ville, c.adresse_pays,
                   c.id AS collabId, c.email
            FROM NoteDeFrais n
            JOIN CollaborateurAD c ON c.id = n.collaborateurId
            WHERE n.id IN (${idList})
              AND n.statut = 'en_attente_president'
        `);
 
            if (!notes.recordset.length)
                return res.status(404).json({ error: 'Aucune note en attente de validation Président trouvée' });
 
            // ── Validation IBAN / adresse ─────────────────────────────────────
            const erreurs = [];
            for (const n of notes.recordset) {
                if (!n.iban) erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : IBAN manquant`);
                if (!n.bic) erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : BIC manquant`);
                if (!n.adresse_rue || !n.adresse_cp || !n.adresse_ville || !n.adresse_pays)
                    erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : adresse postale incomplète`);
            }
            if (erreurs.length > 0)
                return res.status(422).json({ error: 'Données manquantes — XML non généré', details: erreurs });
 
            // ── Construction XML PAIN.001 ─────────────────────────────────────
            const now = new Date();
            const annee = now.getFullYear();
            const mois = String(now.getMonth() + 1).padStart(2, '0');
            const todayISO = now.toISOString().split('T')[0];
            const creDtTm = now.toISOString().slice(0, 19);
            const presidentNom = `${req.user.prenom} ${req.user.nom}`;
            const msgId = `NDF-PRES-${annee}${mois}-${Date.now().toString().slice(-7)}`;
            const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0);
            const totalFormate = total.toFixed(2);
 
            // Campus dominant pour le compte débiteur
            const campusDominant = (() => {
                const campusCounts = {};
                for (const n of notes.recordset) {
                    const code = normalizeCampus(n.campus || '') || n.campus || '';
                    if (code) campusCounts[code] = (campusCounts[code] || 0) + 1;
                }
                return Object.entries(campusCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || null;
            })();
 
            const cfg = await getConfigDebiteur(campusDominant);
            const { companyName: dbtrNom, companyIban: dbtrIban, companyBic: dbtrBic,
                companyAddress: dbtrAdrLine, companyCp: dbtrCp,
                companyVille: dbtrVille, companyPays: dbtrPays } = cfg;
 
            let transactions = '';
            for (const n of notes.recordset) {
                let ibanClair = 'FR0000000000000000000000000';
                try {
                    if (n.iban && n.iban.includes(':')) ibanClair = decryptIban(n.iban);
                    else if (n.iban) ibanClair = n.iban;
                } catch (e) {
                    console.warn(`⚠️ Déchiffrement IBAN impossible pour ${n.reference}:`, e.message);
                }
 
                const benefNom = `${n.nom.toUpperCase()} ${n.prenom}`;
                const adrLine = (n.adresse_rue || '').toUpperCase();
                const cp = n.adresse_cp || '';
                const ville = (n.adresse_ville || '').toUpperCase();
                const pays = (n.adresse_pays || 'FR').slice(0, 2).toUpperCase();
                const benefBicBlock = n.bic
                    ? `<FinInstnId><BIC>${n.bic}</BIC></FinInstnId>`
                    : `<FinInstnId><Othr><Id>NOTPROVIDED</Id></Othr></FinInstnId>`;
 
                transactions += `
    <CdtTrfTxInf>
      <PmtId>
        <InstrId>VIREMENT NUM:${n.reference}</InstrId>
        <EndToEndId>${n.reference}</EndToEndId>
      </PmtId>
      <Amt>
        <InstdAmt Ccy="EUR">${parseFloat(n.montant).toFixed(2)}</InstdAmt>
      </Amt>
      <CdtrAgt>
        ${benefBicBlock}
      </CdtrAgt>
      <Cdtr>
        <Nm>${benefNom}</Nm>${adrLine || cp || ville ? `
        <PstlAdr>${cp ? `
          <PstCd>${cp}</PstCd>` : ''}${ville ? `
          <TwnNm>${ville}</TwnNm>` : ''}
          <Ctry>${pays}</Ctry>${adrLine ? `
          <AdrLine>${adrLine}</AdrLine>` : ''}
        </PstlAdr>` : ''}
        <CtryOfRes>${pays}</CtryOfRes>
      </Cdtr>
      <CdtrAcct>
        <Id>
          <IBAN>${ibanClair}</IBAN>
        </Id>
      </CdtrAcct>
    </CdtTrfTxInf>`;
            }
 
            // Mention Président dans le message du fichier
            const xml = `<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Validé par le Président ${presidentNom} le ${now.toLocaleDateString('fr-FR')} à ${now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })} -->
<Document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03">
  <CstmrCdtTrfInitn>
    <GrpHdr>
      <MsgId>${msgId}</MsgId>
      <CreDtTm>${creDtTm}</CreDtTm>
      <NbOfTxs>${notes.recordset.length}</NbOfTxs>
      <CtrlSum>${totalFormate}</CtrlSum>
      <InitgPty>
        <Nm>${dbtrNom}</Nm>
      </InitgPty>
    </GrpHdr>
    <PmtInf>
      <PmtInfId>${msgId}</PmtInfId>
      <PmtMtd>TRF</PmtMtd>
      <BtchBookg>true</BtchBookg>
      <NbOfTxs>${notes.recordset.length}</NbOfTxs>
      <CtrlSum>${totalFormate}</CtrlSum>
      <PmtTpInf>
        <SvcLvl>
          <Cd>SEPA</Cd>
        </SvcLvl>
      </PmtTpInf>
      <ReqdExctnDt>${todayISO}</ReqdExctnDt>
      <Dbtr>
        <Nm>${dbtrNom}</Nm>
        <PstlAdr>
          <PstCd>${dbtrCp}</PstCd>
          <TwnNm>${dbtrVille}</TwnNm>
          <Ctry>${dbtrPays}</Ctry>
          <AdrLine>${dbtrAdrLine}</AdrLine>
        </PstlAdr>
      </Dbtr>
      <DbtrAcct>
        <Id>
          <IBAN>${dbtrIban}</IBAN>
        </Id>
      </DbtrAcct>
      <DbtrAgt>
        <FinInstnId>
          <Othr>
            <Id>NOTPROVIDED</Id>
          </Othr>
        </FinInstnId>
      </DbtrAgt>
      <ChrgBr>SLEV</ChrgBr>${transactions}
    </PmtInf>
  </CstmrCdtTrfInitn>
</Document>`;
 
            // ── Mettre à jour les notes : 'paiementenattente' + validation Président ──
            await pool.request()
                .input('presidentId', sql.Int, req.user.id)
                .input('commentaire', sql.NVarChar, commentaire || null)
                .input('dateXml', sql.DateTime, now)
                .query(`
                UPDATE NoteDeFrais
                SET statut                    = 'paiementenattente',
                    presidentValidation        = 'valide_president',
                    presidentId                = @presidentId,
                    dateValidationPresident    = GETDATE(),
                    commentairePresident       = @commentaire,
                    dateXml                    = @dateXml,
                    DateModification           = GETDATE()
                WHERE id IN (${idList})
                  AND statut = 'en_attente_president'
            `);
 
            // ── Historique ────────────────────────────────────────────────────
            for (const note of notes.recordset) {
                try {
                    await pool.request()
                        .input('noteId', sql.Int, note.id)
                        .input('presidentId', sql.Int, req.user.id)
                        .input('commentaire', sql.NVarChar, `Validé par le Président ${presidentNom}${commentaire ? ' — ' + commentaire : ''}`)
                        .input('statut', sql.NVarChar, 'paiementenattente')
                        .query(`
                        INSERT INTO HistoriqueValidation
                            (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, NouveauStatut, DateAction)
                        VALUES
                            (@noteId, @presidentId, 'PRESIDENT', 'valider_xml', @commentaire, @statut, GETDATE())
                    `);
                } catch (e) { console.error('Histo President valider:', e.message); }
            }
 
            // ── Envoyer le XML immédiatement ──────────────────────────────────
            const xmlFileName = `virements-ndf-PRESIDENT-${annee}-${mois}-${todayISO}.xml`;
            res.setHeader('Content-Type', 'application/xml; charset=iso-8859-1');
            res.setHeader('Content-Disposition', `attachment; filename=${xmlFileName}`);
            res.send(xml);
 
            // ── Traitement asynchrone : SharePoint + emails ValidateurFinance ──
          setImmediate(async () => {
              console.log(`🔄 [ASYNC PRESIDENT] Post-XML pour ${notes.recordset.length} note(s)...`);

              // Upload XML sur SharePoint
              try {
                  const spXmlFileName = `virements-ndf-PRESIDENT-${annee}-${mois}-${Date.now().toString().slice(-5)}.xml`;
                  const xmlUploadPath = `Virements/President/${annee}/${mois}/${spXmlFileName}`;
                  const accessToken = await getGraphToken();
                  if (accessToken) {
                      await require('axios').put(
                          `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${xmlUploadPath}:/content`,
                          Buffer.from(xml, 'utf-8'),
                          { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/xml' }, maxBodyLength: Infinity }
                      );
                      console.log(`✅ [ASYNC PRESIDENT] XML uploadé : ${xmlUploadPath}`);
                  }
              } catch (e) {
                  console.error('⚠️ [ASYNC PRESIDENT] Upload XML SharePoint:', e.message);
              }

              // ── Retrouver le ValidateurFinance qui a soumis au président ──────────
              try {
                  // On prend la première note du batch pour retrouver qui a soumis
                // APRÈS (cherche sur TOUTES les notes du batch)
                  const allNoteIds = notes.recordset.map(n => n.id).join(',');
                          const histResult = await pool.request()
                              .query(`
        SELECT TOP 1 h.ValidateurId, c.email, c.prenom, c.nom
        FROM HistoriqueValidation h
        JOIN CollaborateurAD c ON c.id = h.ValidateurId
        WHERE h.NoteDeFraisId IN (${allNoteIds})
          AND h.Niveau = 'PRESIDENT'
          AND h.Action = 'soumettre'
        ORDER BY h.DateAction DESC
    `);

                  const soumetteur = histResult.recordset[0];

                  if (soumetteur) {
                      const dateLabel = now.toLocaleDateString('fr-FR', {
                          weekday: 'long', day: '2-digit', month: 'long', year: 'numeric'
                      });
                      const heureLabel = now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });

                      const lignesNotes = notes.recordset.map(n =>
                          `<tr style="border-bottom:1px solid #e2e8f0">
                    <td style="padding:8px 12px;font-family:monospace;font-size:12px;color:#6366f1;font-weight:700">${n.reference}</td>
                    <td style="padding:8px 12px;font-size:13px">${n.prenom} ${n.nom}</td>
                    <td style="padding:8px 12px;font-size:13px;font-weight:700">${parseFloat(n.montant).toFixed(2)} €</td>
                </tr>`
                      ).join('');

                      // Notification BDD
                      await creerNotification({
                          destinataireId: soumetteur.ValidateurId,
                          destinataireEmail: soumetteur.email,
                          type: 'paiement',
                          titre: `✅ XML virement validé par le Président — ${notes.recordset.length} note(s)`,
                          message: `Le Président ${presidentNom} a validé et généré le XML de virement le ${dateLabel} à ${heureLabel} pour ${notes.recordset.length} note(s) — ${totalFormate} €.`,
                          noteId: null
                      });

                      // Email
                      await sendMailGraph(
                          soumetteur.email,
                          `✅ XML virement validé par le Président ${presidentNom}`,
                          `<div style="font-family:Arial,sans-serif;max-width:640px;margin:0 auto">
                    <div style="background:linear-gradient(135deg,#1e3a5f,#1d4ed8);color:white;padding:28px;border-radius:12px 12px 0 0">
                        <h2 style="margin:0;font-size:20px">✅ Virement validé par le Président</h2>
                        <p style="margin:10px 0 0;opacity:.85;font-size:13px">Le fichier XML a été généré et est prêt pour votre banque</p>
                    </div>
                    <div style="padding:24px;background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
                        <p>Bonjour <strong>${soumetteur.prenom} ${soumetteur.nom}</strong>,</p>
                        <p>Le Président <strong>${presidentNom}</strong> a validé et généré le fichier XML de virement bancaire pour les notes que vous lui avez soumises.</p>
                        
                        <div style="background:#fff;border:1.5px solid #bfdbfe;border-left:4px solid #1d4ed8;border-radius:0 8px 8px 0;padding:16px;margin:20px 0">
                            <div style="font-size:13px;color:#1e40af;margin-bottom:8px">
                                <strong>Date :</strong> ${dateLabel} à ${heureLabel}<br/>
                                <strong>Validé par :</strong> ${presidentNom}<br/>
                                <strong>Nombre de virements :</strong> ${notes.recordset.length}<br/>
                                <strong>Montant total :</strong> <span style="font-weight:900;color:#15803d;font-size:15px">${totalFormate} €</span>
                            </div>
                            ${commentaire ? `<div style="margin-top:10px;padding:10px;background:#eff6ff;border-radius:6px;font-size:12px;color:#1d4ed8">💬 <em>${commentaire}</em></div>` : ''}
                        </div>

                        <div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;margin:20px 0">
                            <div style="background:#f1f5f9;padding:10px 14px;border-bottom:1px solid #e2e8f0">
                                <span style="font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px">
                                    Détail des virements (${notes.recordset.length})
                                </span>
                            </div>
                            <table style="width:100%;border-collapse:collapse">
                                <thead>
                                    <tr style="background:#fafafa">
                                        <th style="padding:8px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">Référence</th>
                                        <th style="padding:8px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">Collaborateur</th>
                                        <th style="padding:8px 12px;text-align:left;font-size:11px;color:#64748b;text-transform:uppercase">Montant</th>
                                    </tr>
                                </thead>
                                <tbody>${lignesNotes}</tbody>
                                <tfoot>
                                    <tr style="background:#f0fdf4;border-top:2px solid #86efac">
                                        <td colspan="2" style="padding:10px 12px;font-weight:700;color:#15803d;font-size:13px">Total</td>
                                        <td style="padding:10px 12px;font-weight:900;color:#15803d;font-size:15px">${totalFormate} €</td>
                                    </tr>
                                </tfoot>
                            </table>
                        </div>

                        <p style="font-size:13px;color:#374151">
                            Vous pouvez re-télécharger ce fichier XML depuis la rubrique <strong>"XML virements"</strong> de la plateforme.
                        </p>
                        <div style="text-align:center;margin-top:28px">
                            <a href="${process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net'}" style="background:linear-gradient(135deg,#1d4ed8,#1e40af);color:white;padding:14px 36px;text-decoration:none;border-radius:8px;font-weight:700;display:inline-block">
                                🏦 Voir les XML virements →
                            </a>
                        </div>
                    </div>
                </div>`
                      );

                      console.log(`✅ [ASYNC PRESIDENT] ValidateurFinance notifié : ${soumetteur.email}`);
                  } else {
                      console.warn('⚠️ [ASYNC PRESIDENT] Soumetteur introuvable dans HistoriqueValidation');
                  }
              } catch (e) {
                  console.error('❌ [ASYNC PRESIDENT] Notification soumetteur:', e.message);
              }

              console.log(`✅ [ASYNC PRESIDENT] Traitement terminé pour ${notes.recordset.length} note(s)`);
          });
 
        } catch (error) {
            console.error('Erreur POST /api/president/generer-xml:', error.message);
            res.status(500).json({ error: error.message });
        }
    });
 
    // ══════════════════════════════════════════════════════════════════════════
    // 4. Président → Historique de ses validations
    //    GET /api/president/historique
    // ══════════════════════════════════════════════════════════════════════════
    app.get('/api/president/historique', authenticateToken, async (req, res) => {
        if (!hasAnyRole(req.user, 'President', 'superUtilisateur'))
            return res.status(403).json({ error: 'Accès réservé au Président' });
 
        try {
            const result = await pool.request()
                .input('presidentId', sql.Int, req.user.id)
                .query(`
                SELECT
                    CAST(n.dateXml AS DATE)       AS dateXmlJour,
                    MIN(n.dateXml)                AS dateXmlExacte,
                    n.dateValidationPresident,
                    n.commentairePresident,
                    COUNT(*)                      AS nbNotes,
                    SUM(n.montant)                AS totalMontant,
                    STRING_AGG(CAST(n.id AS NVARCHAR(20)), ',')  AS noteIds,
                    STRING_AGG(n.reference, ', ')                AS listeReferences
                FROM NoteDeFrais n
                WHERE n.presidentId = @presidentId
                  AND n.presidentValidation = 'valide_president'
                GROUP BY CAST(n.dateXml AS DATE), n.dateValidationPresident, n.commentairePresident
                ORDER BY CAST(n.dateXml AS DATE) DESC
            `);
 
            res.json(result.recordset);
        } catch (error) {
            console.error('GET /api/president/historique:', error.message);
            res.status(500).json({ error: error.message });
        }
    });
 

// ================================================
// GESTION DES ERREURS
// ================================================
app.use((err, req, res, next) => {
    console.error('❌ Erreur middleware:', err.stack);
    res.status(500).json({ error: 'Une erreur est survenue', details: process.env.NODE_ENV === 'development' ? err.message : undefined });
});

// ================================================
// DÉMARRAGE DU SERVEUR
// ================================================
const server = app.listen(PORT, '0.0.0.0', () => {
    console.log('\n================================================');
    console.log(`✅ SERVEUR DÉMARRÉ sur http://0.0.0.0:${PORT}`);
    console.log('================================================');

    setTimeout(async () => {
        console.log('\n🚀 Lancement synchronisation automatique Entra ID...');
        await syncEntraIdUsers();
        setInterval(async () => {
            console.log('\n🔁 Synchronisation périodique Entra ID...');
            await syncEntraIdUsers();
        }, 6 * 60 * 60 * 1000);
    }, 5000);
});

server.on('error', (error) => {
    console.error('\n❌ ERREUR SERVEUR:', error);
    if (error.code === 'EADDRINUSE') console.error(`⚠️ Le port ${PORT} est déjà utilisé`);
    process.exit(1);
});

setInterval(() => { }, 60000);