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||''}
Brouillon
À valider
Envoyé
Accepté
Refusé
📋 Nouvelle version
💾 Sauvegarder
✏️ Saisie
📊 Détail
📄 Client
${(d.versions||[]).length?`🕐 Historique `:''}
`;
}
function renderSaisie(d) {
const id = d.id;
return `
🔧 Prestations & matériels
Prestation Type
Inclus / Détail Qté
P.U. € Total
Part variable
${(d.lignes||[]).map((l,i) => renderLigne(id,l,i)).join('')}
— Choisir dans la grille tarifaire —
${Object.entries(GRILLE.reduce((acc,g)=>{ (acc[g.cat]=acc[g.cat]||[]).push(g); return acc; },{})).map(([cat,items]) =>
`${items.map(g => `${g.label} (${g.prix} €) `).join('')} `
).join('')}
+ Ajouter
+ Ligne libre
${renderTotal(d)}
`;
}
function renderLigne(id, l, i) {
const tot = ((l.quantite||0)*(l.prix_unitaire||0)).toFixed(2);
return `
Variable
Forfait
${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
Catégorie Prestation Type Inclus Qté P.U. Total Part variable
${(d.lignes||[]).map(l => `
${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||'—'}
`).join('')}
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:''}
`:''}
Prestation Qté P.U. Total
${(d.lignes||[]).map(l => {
const tot = ((l.quantite||0)*(l.prix_unitaire||0));
return `
${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)} €
`;
}).join('')}
Sous-total : ${t.total.toFixed(2)} €
Total TTC : ${t.total.toFixed(2)} €
TVA non applicable – art. 293 B du CGI · Devis valable ${d.validite||30} jours · Valable jusqu'au ${validDate} · Accord du client requis avant intervention
Envoyer au client :
🖨️ Imprimer
📧 Email
💬 SMS
`;
}
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 => `${l.label||'—'} ${l.quantite||0} × ${(l.prix_unitaire||0).toFixed(2)} € = ${((l.quantite||0)*(l.prix_unitaire||0)).toFixed(2)} € `).join('')}
`).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();