diff --git a/ndf/public/backend/server.js b/ndf/public/backend/server.js index 2049050..17a41cc 100644 --- a/ndf/public/backend/server.js +++ b/ndf/public/backend/server.js @@ -2091,50 +2091,63 @@ app.post('/api/notes', authenticateToken, upload.any(), async (req, res) => { } } - // ── 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 = []; + // ── QR par ligne : ne re-télécharge/ré-upload que si le client n'a pas déjà le fichier ── + await Promise.all( + lignesParsed.map(async (ligne, i) => { + const ligneQrRef = ligne.qrNoteRef; + if (!ligneQrRef) return; - 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); + // ✅ Si le client a déjà reçu ce fichier via le polling QR (origin 'qr'), + // il est déjà dans ligne.qrFiles avec son uploadUrl d'origine. + // Pas besoin de re-télécharger/ré-uploader : on évite ainsi le doublon + // causé par un nouveau nom de fichier (basé sur la référence finale de la note). + const dejaRecuParClient = Array.isArray(ligne.qrFiles) && + ligne.qrFiles.some(f => !f.origin || f.origin === 'qr'); - 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); + if (dejaRecuParClient) { + console.log(`✅ QR ligne ${i} (${ligneQrRef}) : fichier déjà reçu côté client, pas de re-upload`); + 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; } - }) - ); - } catch (e) { - console.warn(`⚠️ QR ligne ${i} erreur DB:`, e.message); - } - }) - ); + 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); + } + }) + ); // ── Upload fichiers soumis directement par ligne (files_${depId}) ── // Identique à la logique du PUT /api/notes/brouillons/:id @@ -2534,28 +2547,37 @@ app.put('/api/notes/:id', authenticateToken, upload.any(), async (req, res) => { } // 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); } + // QR par ligne — skip si le client a déjà le fichier (évite doublon) + for (let i = 0; i < lignesParsed.length; i++) { + const ligneQrRef = lignesParsed[i].qrNoteRef; + if (!ligneQrRef) continue; + + const dejaRecuParClient = Array.isArray(lignesParsed[i].qrFiles) && + lignesParsed[i].qrFiles.some(f => !f.origin || f.origin === 'qr'); + if (dejaRecuParClient) { + console.log(`✅ [CAS1] QR ligne ${i} (${ligneQrRef}) : fichier déjà reçu côté client, pas de re-upload`); + 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); } } - } catch (e) { console.warn(`⚠️ QR ligne ${i} erreur DB:`, e.message); } - } // lignesJsonCorrige APRÈS injection const lignesJsonCorrige = JSON.stringify(lignesParsed); diff --git a/ndf/src/pages/NouvelleNote.tsx b/ndf/src/pages/NouvelleNote.tsx index b2be00f..dc63957 100644 --- a/ndf/src/pages/NouvelleNote.tsx +++ b/ndf/src/pages/NouvelleNote.tsx @@ -1945,24 +1945,25 @@ export default function NouvelleNote({ if (!res.ok) { alert("Erreur génération QR"); return; } setDepenses(prev => prev.map(d => d.id === depenseId ? { ...d, qrLink: data.uploadLink, qrNoteRef: tempRef, qrUploaded: false } : d)); if (qrPollingRefs.current[depenseId]) clearInterval(qrPollingRefs.current[depenseId]); - qrPollingRefs.current[depenseId] = setInterval(async () => { - try { - const r = await fetch(`${apiBaseUrl}/api/upload/status/${tempRef}`, { headers: getHeaders() }); - const s = await r.json(); - if (s.uploaded) { - clearInterval(qrPollingRefs.current[depenseId]); - setDepenses(prev => prev.map(d => d.id === depenseId - ? { - ...d, qrUploaded: true, qrLink: null, - qrFiles: [ - ...(d.qrFiles ?? []), - ...(Array.isArray(s.files) ? s.files.map((f: any) => ({ ...f, origin: 'qr' as const })) : []) - ] - } - : d)); - } - } catch { } - }, 3000); + qrPollingRefs.current[depenseId] = setInterval(async () => { + try { + const r = await fetch(`${apiBaseUrl}/api/upload/status/${tempRef}`, { headers: getHeaders() }); + const s = await r.json(); + if (s.uploaded) { + clearInterval(qrPollingRefs.current[depenseId]); + delete qrPollingRefs.current[depenseId]; // ✅ empêche un 2e tick concurrent de re-déclencher la fusion + setDepenses(prev => prev.map(d => d.id === depenseId + ? { + ...d, qrUploaded: true, qrLink: null, + qrFiles: [ + ...(d.qrFiles ?? []), + ...(Array.isArray(s.files) ? s.files.map((f: any) => ({ ...f, origin: 'qr' as const })) : []) + ].filter((f, i, arr) => arr.findIndex(x => x.uploadUrl === f.uploadUrl) === i) // ✅ dédup par URL + } + : d)); + } + } catch { } + }, 3000); } catch { } }, [apiBaseUrl, getHeaders]);