const d = devisList.find(x => x.id === id); if (d) { d.client[field] = val; renderSidebar(); } }, updateVehicule(id, field, val) { const d = devisList.find(x => x.id === id); if (d) d.vehicule[field] = val; }, updateField(id, field, val) { const d = devisList.find(x => x.id === id); if (d) { d[field] = val; renderSidebar(); } }, updateLigne(id, i, field, val) { const d = devisList.find(x => x.id === id); if (d && d.lignes[i]) { d.lignes[i][field] = val; const el = document.getElementById(`total-${id}-${i}`); if (el) el.textContent = ((d.lignes[i].quantite||0)*(d.lignes[i].prix_unitaire||0)).toFixed(2)+' €'; } }, addFromGrille(id) { const sel = document.getElementById(`grille-${id}`); const idx = parseInt(sel.value); if (isNaN(idx)) return; const g = GRILLE[idx]; const d = devisList.find(x => x.id === id); if (!d) return; d.lignes.push({ label:g.label, categorie:g.cat, type_ligne:g.type, quantite:1, prix_unitaire:g.prix, inclus:g.inclus||'', part_variable:'', notes:'' }); sel.value = ''; renderForm(d); }, addLigneLibre(id) { const d = devisList.find(x => x.id === id); if (!d) return; d.lignes.push({ label:'', categorie:'Autre', type_ligne:'variable', quantite:1, prix_unitaire:0, inclus:'', part_variable:'', notes:'' }); renderForm(d); }, removeLigne(id, i) { const d = devisList.find(x => x.id === id); if (!d) return; d.lignes.splice(i, 1); renderForm(d); }, async newVersion(id) { const d = devisList.find(x => x.id === id); if (!d) return; const snap = { version: d.version, date: new Date().toISOString(), lignes: JSON.parse(JSON.stringify(d.lignes)), notes: d.notes_internes }; d.versions = d.versions || []; d.versions.push(snap); d.version++; try { await archiverVersion(d.id, d.version-1, snap); } catch(e) {} renderForm(d); renderSidebar(); notify(`📋 Version ${d.version} créée`); }, switchTab(btn, formId, tab) { btn.closest('.tabs').querySelectorAll('.tab').forEach(t => t.classList.remove('active')); btn.classList.add('active'); document.getElementById(formId).querySelectorAll('[data-tab]').forEach(el => { el.style.display = el.dataset.tab === tab ? 'block' : 'none'; }); if (tab === 'tab-client') refreshClientView(id); }, exportPDF(id, mode) { const d = devisList.find(x => x.id === id); if (!d) return; exportPDF(d, mode, prestataire); } }; function genRef() { const now = new Date(); const yy = String(now.getFullYear()).slice(2); const mm = String(now.getMonth()+1).padStart(2,'0'); const rand = Math.floor(Math.random()*900)+100; return `D${yy}${mm}-${rand}`; } function totalDevis(d) { let mo = 0, pieces = 0, forfait = 0; (d.lignes||[]).forEach(l => { const tot = (l.quantite||0) * (l.prix_unitaire||0); if (l.type_ligne === 'forfait') forfait += tot; else if (l.categorie === 'Pièce') pieces += tot; else mo += tot; }); return { mo, pieces, forfait, total: mo+pieces+forfait }; } function renderSidebar() { const el = document.getElementById('devisList'); if (!devisList.length) { el.innerHTML = '
Aucun devis
'; return; } const badges = { brouillon:'badge-brouillon', a_valider:'badge-a_valider', envoye:'badge-envoye', accepte:'badge-accepte', refuse:'badge-refuse' }; const labels = { brouillon:'Brouillon', a_valider:'À valider', envoye:'Envoyé', accepte:'Accepté', refuse:'Refusé' }; el.innerHTML = devisList.map(d => { const active = d.id === currentId ? 'active' : ''; const clientBadge = d.source === 'configurateur_client' ? 'Demande client' : ''; return `
${d.ref} v${d.version||1}
${d.client?.nom||d.clients?.nom||'Client non renseigné'} ${d.client?.prenom||d.clients?.prenom||''}
${d.date_devis||d.created_at?.slice(0,10)||''}
${labels[d.statut]||'Brouillon'} ${clientBadge}
`; }).join(''); } function renderForm(d) { const mc = document.getElementById('mainContent'); const id = d.id; mc.innerHTML = `
Version ${d.version||1} Réf. ${d.ref} · ${d.date_devis||''}
${(d.versions||[]).length?``:''}
${renderSaisie(d)}
${renderDetail(d)}
${renderClientView(d)}
${(d.versions||[]).length?`
${renderHistory(d)}
`:''}
`; } function renderSaisie(d) { const id = d.id; return `
👤 Client
🚲 Vélo
🔧 Prestations & matériels
${(d.lignes||[]).map((l,i) => renderLigne(id,l,i)).join('')}
PrestationType Inclus / DétailQté P.U. €Total Part variable
📝 Notes internes
${renderTotal(d)} `; } function renderLigne(id, l, i) { const tot = ((l.quantite||0)*(l.prix_unitaire||0)).toFixed(2); return ` ${tot} € `; } function renderTotal(d) { const t = totalDevis(d); return `
Main d'œuvre${t.mo.toFixed(2)} €
Forfaits${t.forfait.toFixed(2)} €
Pièces${t.pieces.toFixed(2)} €
TOTAL TTC${t.total.toFixed(2)} €
TVA non applicable – art. 293 B CGI
`; } function renderDetail(d) { const t = totalDevis(d); return `
📊 Vue détaillée — usage interne
${(d.lignes||[]).map(l => ``).join('')}
CatégoriePrestationTypeInclusQtéP.U.TotalPart variable
${l.categorie||'—'} ${l.label||''} ${l.type_ligne==='forfait'?'Forfait':'Variable'} ${l.inclus||'—'} ${l.quantite||0} ${(l.prix_unitaire||0).toFixed(2)} € ${((l.quantite||0)*(l.prix_unitaire||0)).toFixed(2)} € ${l.part_variable||'—'}
Main d'œuvre
${t.mo.toFixed(2)} €
Forfaits
${t.forfait.toFixed(2)} €
Pièces
${t.pieces.toFixed(2)} €
Total
${t.total.toFixed(2)} €
${d.notes_internes?`
Notes : ${d.notes_internes}
`:''}
`; } function renderClientView(d) { const t = totalDevis(d); const validDate = new Date(new Date(d.date_devis).getTime() + (d.validite||30)*86400000).toISOString().slice(0,10); return `
Devis
Réf. ${d.ref} · v${d.version||1} · ${d.date_devis}
${prestataire.artisan_activite||'Atelier VAE'}
${prestataire.artisan_adresse||'Le Vésinet (78)'}
${prestataire.artisan_tel||''}
${prestataire.artisan_siret?`
SIRET : ${prestataire.artisan_siret}
`:''}
Prestataire
${prestataire.artisan_nom||'—'}
${prestataire.artisan_email||''}
Client
${d.client?.nom||'—'} ${d.client?.prenom||''}
${d.client?.adresse||''}
${d.client?.tel||''} ${d.client?.email?'· '+d.client.email:''}
${d.vehicule?.marque?`
Vélo : ${d.vehicule.marque} ${d.vehicule.modele||''} ${d.vehicule.annee?'('+d.vehicule.annee+')':''} ${d.vehicule.numero?'· N° '+d.vehicule.numero:''}
`:''} ${(d.lignes||[]).map(l => { const tot = ((l.quantite||0)*(l.prix_unitaire||0)); return ``; }).join('')}
PrestationQtéP.U.Total
${l.label||''} ${l.type_ligne==='forfait'?'Forfait':''}
${l.inclus?`
Inclus : ${l.inclus}
`:''} ${l.part_variable?`
+ Part variable : ${l.part_variable} (non inclus)
`:''}
${l.quantite||0} ${(l.prix_unitaire||0).toFixed(2)} € ${tot.toFixed(2)} €
Sous-total : ${t.total.toFixed(2)} €
Total TTC : ${t.total.toFixed(2)} €
Envoyer au client :
`; } function renderHistory(d) { return `
🕐 Historique des versions
${(d.versions||[]).map(v => `
Version ${v.version} ${new Date(v.date).toLocaleDateString('fr-FR')}
${(v.lignes||[]).map(l => ``).join('')}
${l.label||'—'}${l.quantite||0} × ${(l.prix_unitaire||0).toFixed(2)} € = ${((l.quantite||0)*(l.prix_unitaire||0)).toFixed(2)} €
`).join('')}
`; } function exportPDF(d, mode, prest) { const { jsPDF } = window.jspdf; const doc = new jsPDF({ unit:'mm', format:'a4' }); const W=210, M=18, CW=W-2*M; const t = totalDevis(d); const validDate = new Date(new Date(d.date_devis).getTime() + (d.validite||30)*86400000).toISOString().slice(0,10); doc.setFillColor(15,39,68); doc.rect(0,0,W,28,'F'); doc.setTextColor(255,255,255); doc.setFontSize(18); doc.setFont('helvetica','bold'); doc.text('DEVIS', M, 13); doc.setFontSize(10); doc.setFont('helvetica','normal'); doc.text(`Réf. ${d.ref} · v${d.version||1} · ${d.date_devis}`, M, 21); doc.setTextColor(201,168,76); doc.setFontSize(10); doc.setFont('helvetica','bold'); doc.text(prest.artisan_activite||'Atelier VAE', W-M, 13, {align:'right'}); doc.setTextColor(200,220,255); doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.text(prest.artisan_adresse||'Le Vésinet (78)', W-M, 21, {align:'right'}); let y = 36; doc.setFillColor(232,240,251); doc.roundedRect(M,y,CW/2-4,22,2,2,'F'); doc.roundedRect(M+CW/2+4,y,CW/2-4,22,2,2,'F'); doc.setFontSize(7); doc.setFont('helvetica','bold'); doc.setTextColor(100,120,150); doc.text('PRESTATAIRE', M+4, y+6); doc.text('CLIENT', M+CW/2+8, y+6); doc.setFontSize(9); doc.setFont('helvetica','bold'); doc.setTextColor(15,39,68); doc.text(prest.artisan_nom||'—', M+4, y+12); doc.text(`${d.client?.nom||'—'} ${d.client?.prenom||''}`, M+CW/2+8, y+12); doc.setFont('helvetica','normal'); doc.setFontSize(8); doc.setTextColor(90,110,130); doc.text(prest.artisan_adresse||'', M+4, y+17); doc.text(d.client?.adresse||'', M+CW/2+8, y+17, {maxWidth:CW/2-12}); y += 28; if (d.vehicule?.marque) { doc.setFillColor(244,246,250); doc.roundedRect(M,y,CW,8,1,1,'F'); doc.setFontSize(8); doc.setFont('helvetica','bold'); doc.setTextColor(15,39,68); doc.text('Vélo :', M+3, y+5.5); doc.setFont('helvetica','normal'); doc.text(`${d.vehicule.marque} ${d.vehicule.modele||''} ${d.vehicule.annee?'('+d.vehicule.annee+')':''}`, M+18, y+5.5); y += 12; } doc.setFillColor(15,39,68); doc.rect(M,y,CW,8,'F'); doc.setTextColor(255,255,255); doc.setFontSize(7.5); doc.setFont('helvetica','bold'); const cols=[M+3,M+80,M+105,M+120,M+140,M+162]; ['Prestation','Type','Qté','P.U.','Total','Note'].forEach((h,i) => doc.text(h,cols[i],y+5.5)); y += 8; (d.lignes||[]).forEach((l,idx) => { if (y>255) { doc.addPage(); y=20; } const tot=((l.quantite||0)*(l.prix_unitaire||0)); const fill = idx%2===0?[248,251,255]:[255,255,255]; doc.setFillColor(...fill); doc.rect(M,y,CW,8,'F'); doc.setTextColor(20,40,70); doc.setFontSize(8); doc.setFont('helvetica','bold'); doc.text(l.label||'', M+3, y+5.5, {maxWidth:74}); doc.setFont('helvetica','normal'); doc.setTextColor(l.type_ligne==='forfait'?[26,74,138]:[133,100,4]); doc.text(l.type_ligne==='forfait'?'Forfait':'Variable', cols[1], y+5.5); doc.setTextColor(20,40,70); doc.text(String(l.quantite||0), cols[2], y+5.5); doc.text(`${(l.prix_unitaire||0).toFixed(2)} €`, cols[3], y+5.5); doc.setFont('helvetica','bold'); doc.setTextColor(26,74,138); doc.text(`${tot.toFixed(2)} €`, cols[4], y+5.5); doc.setFont('helvetica','normal'); doc.setTextColor(120,140,160); doc.setFontSize(7); if (l.part_variable) doc.text(`+${l.part_variable}`, cols[5], y+5.5, {maxWidth:28}); y += 8; if (l.inclus) { doc.setFillColor(...fill); doc.rect(M,y,CW,6,'F'); doc.setTextColor(100,130,160); doc.setFontSize(7); doc.setFont('helvetica','italic'); doc.text(` Inclus : ${l.inclus}`, M+3, y+4); y+=6; } }); doc.setDrawColor(200,210,230); doc.line(M,y+2,W-M,y+2); y+=8; [['Main d\'œuvre',t.mo],['Forfaits',t.forfait],['Pièces',t.pieces]].forEach(([label,val]) => { doc.setFontSize(9); doc.setFont('helvetica','normal'); doc.setTextColor(80,100,120); doc.text(label, W-M-50, y); doc.text(`${val.toFixed(2)} €`, W-M, y, {align:'right'}); y+=7; }); doc.setFillColor(15,39,68); doc.roundedRect(W-M-80,y-3,80,12,2,2,'F'); doc.setTextColor(255,255,255); doc.setFont('helvetica','bold'); doc.setFontSize(11); doc.text('TOTAL TTC', W-M-76, y+5.5); doc.setTextColor(201,168,76); doc.setFontSize(13); doc.text(`${t.total.toFixed(2)} €`, W-M-3, y+5.5, {align:'right'}); y+=18; doc.setFontSize(7.5); doc.setFont('helvetica','italic'); doc.setTextColor(140,150,160); doc.text(`Devis valable jusqu'au ${validDate} · TVA non applicable – art. 293 B du CGI`, M, y, {maxWidth:CW}); y+=10; doc.setDrawColor(200,210,230); doc.rect(M,y,CW/2-8,20); doc.rect(M+CW/2+8,y,CW/2-8,20); doc.setFontSize(8); doc.setFont('helvetica','normal'); doc.setTextColor(15,39,68); doc.text('Signature prestataire :', M+3, y+5); doc.text('Bon pour accord — Signature client :', M+CW/2+11, y+5); const filename=`Devis_${d.ref}_v${d.version||1}.pdf`; const pdfBlob=doc.output('blob'); const pdfUrl=URL.createObjectURL(pdfBlob); if (mode==='print') { const iframe=document.createElement('iframe'); iframe.style.display='none'; iframe.src=pdfUrl; document.body.appendChild(iframe); iframe.onload=()=>{ iframe.contentWindow.print(); }; } else if (mode==='email') { doc.save(filename); const subject=encodeURIComponent(`Devis ${d.ref} - Réparation VAE`); const body=encodeURIComponent(`Bonjour ${d.client?.prenom||d.client?.nom||''},\n\nVeuillez trouver ci-joint votre devis réf. ${d.ref} (v${d.version||1}) d'un montant de ${t.total.toFixed(2)} €.\n\nValable jusqu'au ${validDate}.\n\nCordialement,\n${prest.artisan_nom||'Atelier VAE'}`); window.location.href=`mailto:${encodeURIComponent(d.client?.email||'')}?subject=${subject}&body=${body}`; } else if (mode==='sms') { doc.save(filename); const msg=encodeURIComponent(`Bonjour ${d.client?.prenom||d.client?.nom||''}, votre devis VAE réf. ${d.ref} est prêt : ${t.total.toFixed(2)} €. Valable jusqu'au ${validDate}. ${prest.artisan_nom||'Atelier VAE'}`); window.location.href=`sms:${(d.client?.tel||'').replace(/\s/g,'')}?body=${msg}`; } } init();