Untitled
3 hours ago in Plain Text
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Inspection Mobile — EXTÉRIEUR (Mobile Direct)</title>
<style>
@font-face { font-family: 'ConstantiaFallback'; src: local('Constantia'), local('Constantia Regular'); }
:root { --red:#ff0000; --ink:#222; --muted:#666; --band:#f7f7f7; --rule:#d9d9d9; --grey:#f4f4f4; }
* { box-sizing: border-box; }
body { font-family: Constantia, ConstantiaFallback, Cambria, Georgia, 'Times New Roman', serif; color: var(--ink); background: #fafafa; margin:0; }
header { background: white; border-bottom: 3px solid var(--red); padding: 12px 16px; position: sticky; top:0; z-index: 10; }
h1 { margin: 0 0 4px; font-size: 20px; letter-spacing: .2px; }
.sub { color: var(--muted); font-size: 12px; }
main { padding: 12px; max-width: 1000px; margin: 0 auto; }
.card { background: #fff; border: 1px solid #e6e6e6; border-radius: 14px; padding: 14px; margin: 10px 0 16px; box-shadow: 0 2px 6px rgba(0,0,0,.04); }
label { font-weight: 600; font-size: 14px; display:block; margin-top: 6px; }
input[type=text], input[type=date], textarea, select { width: 100%; padding: 10px 12px; border: 1px solid #d0d0d0; border-radius: 10px; font-size: 14px; background: #fff; font-family: Constantia, ConstantiaFallback, Cambria, Georgia, 'Times New Roman', serif; }
textarea { min-height: 80px; resize: vertical; }
.row2 { display:grid; grid-template-columns:1fr 1fr; gap:10px }
.actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px }
button { border:0; background:var(--red); color:#fff; padding:10px 14px; border-radius:12px; font-weight:700; cursor:pointer; }
button.ghost { background:#efefef; color:#111; }
.muted { color: var(--muted); font-size: 14px; margin-top: 8px; line-height: 1.35; }
/* ===== TITRE DE SECTION ===== */
.section-block { margin-top: 16px; }
.sec-title { font-weight:900; font-size: 20px; text-transform: uppercase; letter-spacing:.6px; color: var(--ink); text-align:left; }
.sec-underline { height:2px; width:75%; background: var(--red); margin: 2px 0 0 0; border-radius: 2px; }
/* ===== SOUS-SECTIONS ===== */
.subcard { border: 1px solid #e0e0e0; border-radius: 12px; padding: 0; margin: 12px 0; overflow: hidden; background: #fff; }
.subcard .sub-head { background: var(--grey); border-bottom: 1px solid #e0e0e0; padding: 8px 12px; display:flex; align-items:center; gap:10px; }
.subcard .dot { color: var(--red); font-size: 12px; }
.subcard .sub-title { font-weight: 800; text-transform: uppercase; }
.subcard .sub-body { padding: 12px; }
.def-block { border: 1px dashed #cfcfcf; border-radius: 10px; padding: 10px; margin-top: 10px; background: #fff; }
.photo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 6px; }
.photo-slot { border: 1px dashed #cfcfcf; border-radius: 10px; padding: 8px; text-align:center; }
.photo-slot input[type=file]{ width: 100%; padding: 6px; }
.thumb { width: 100%; height: auto; border-radius: 8px; border:1px solid #ddd; display:none; margin-top:6px; }
.ok { color:#0a7a28; font-size:12px; display:none; margin-top:4px; }
.checkgrid { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; margin-top: 8px; }
.checkgrid label { font-weight: 600; display:flex; align-items:center; gap:8px; padding:6px 8px; border:1px solid #e5e5e5; border-radius:10px; }
.preview-line { margin-top: 8px; font-size: 14px; }
/* ===== RAPPORT (PDF) ===== */
#report { display:none; }
.report-page { page-break-after: always; background: #fff; padding: 22px; border: 1px solid #e5e5e5; border-radius: 8px; margin: 16px 0; }
.rpt-headerband { background: var(--band); border-bottom: 1px solid var(--rule); padding: 10px 14px; display:flex; align-items:center; justify-content: space-between; }
.rpt-h-left { font-weight: 900; letter-spacing: .6px; text-transform: uppercase; color: var(--red); }
.report-h2 { font-size: 18px; margin: 0; text-align:left; text-transform: uppercase; font-weight: 900; letter-spacing: .6px; color: var(--ink); }
.report-underline { height:2px; width:75%; background: var(--red); margin: 2px 0 8px 0; border-radius: 2px; }
.report-note { font-size: 12.5px; color: #333; margin: 6px 0 10px 0; }
.report-h3 { font-size: 15px; margin: 12px 0 6px; }
.report-h3 .dot { color: var(--red); margin-right: 8px; }
.report-h3 .text { text-transform: uppercase; font-weight:800; background: var(--grey); padding: 4px 8px; border-radius: 8px; border: 1px solid #e0e0e0; display:inline-block; }
.report-def { margin: 8px 0 10px; }
.report-photos { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 6px; }
.report-photos img { width: 100%; height: auto; border-radius: 8px; border:1px solid #ddd; }
.rpt-footer { border-top: 1px solid var(--rule); margin-top: 14px; padding-top: 6px; font-size: 11.5px; color: #555; display:flex; align-items:center; justify-content: space-between; gap:12px; flex-wrap: wrap; }
.report-line { margin: 6px 0; }
@media print { header, .no-print { display:none !important; } body { background:#fff; } .report-page { border: 0; border-radius: 0; margin: 0; padding: 14mm 12mm; } }
</style>
</head>
<body>
<header>
<h1>Inspection Mobile — EXTÉRIEUR (Mobile Direct)</h1>
<div class="sub">Aucune fenêtre externe • Rapport visible en bas • Impression PDF via partage natif</div>
</header>
<main>
<section class="card">
<div class="row2">
<div><label>Client</label><input id="client" type="text" placeholder="Nom du client"></div>
<div><label>Adresse</label><input id="adresse" type="text" placeholder="Adresse complète"></div>
<div><label>Date</label><input id="dateInspection" type="date"></div>
<div><label>Numéro de dossier</label><input id="dossier" type="text" placeholder="Ex.: JM-2025-001"></div>
</div>
</section>
<!-- Section EXTÉRIEUR -->
<section class="card section-block">
<div class="sec-title">EXTÉRIEUR</div>
<div class="sec-underline"></div>
<div class="muted" id="note-exterieur">Nous avons inspecté les différentes composantes extérieures par le niveau du sol.</div>
<!-- Sous-section: REVÊTEMENT EXTÉRIEUR ET SOLINS -->
<div class="subcard" id="rev-sub">
<div class="sub-head">
<span class="dot">▪</span>
<div class="sub-title">REVÊTEMENT EXTÉRIEUR ET SOLINS</div>
</div>
<div class="sub-body">
<div class="muted" id="rev-note">
Tout endroit susceptible de laisser passer l’eau sur un bâtiment doit être scellé, tel que les bouches de ventilation, les prises électriques, les luminaires, les robinets, les portes, les fenêtres, etc. Afin d’éviter tout dommage causé par l’eau, nous recommandons également d’en faire l’entretien régulièrement.
</div>
<label>Type de revêtement (multi-choix)</label>
<div class="checkgrid" id="parement-grid">
<label><input type="checkbox" value="brique"> Parement de brique</label>
<label><input type="checkbox" value="pierre"> Parement de pierre</label>
<label><input type="checkbox" value="vinyle"> Parement de vinyle</label>
<label><input type="checkbox" value="metal"> Parement de métal</label>
<label><input type="checkbox" value="bois_syn"> Parement de bois synthétique</label>
<label><input type="checkbox" value="acrylique"> Parement d’acrylique</label>
<label><input type="checkbox" value="agregat"> Parement d’agrégat</label>
<label><input type="checkbox" value="stuc"> Parement de stuc</label>
<label><input type="checkbox" value="bardeaux"> Parement de bardeaux d’asphalte</label>
<label><input type="checkbox" value="amiante"> Parement d’amiante</label>
</div>
<div class="preview-line"><strong>Aperçu&nbsp;:</strong> <span id="parement-preview" class="muted">—</span></div>
<div class="def-list" id="list-revetement"></div>
<div class="actions"><button class="ghost" type="button" id="add-revetement">+ Ajouter une déficience</button></div>
</div>
</div>
</section>
<div class="actions no-print">
<button id="generate">Générer le rapport (dans la page)</button>
<button class="ghost" id="printBtn">Imprimer / Enregistrer en PDF</button>
<button class="ghost" id="reset">Tout effacer</button>
</div>
<section id="report"></section>
</main>
<script>
/* === Phrase intelligente pour les parements === */
const PAREMENTS = {
'brique':'brique','pierre':'pierre','vinyle':'vinyle','metal':'métal','bois_syn':'bois synthétique','acrylique':'acrylique','agregat':'agrégat','stuc':'stuc','bardeaux':'bardeaux d’asphalte','amiante':'amiante'
};
const VOWELS = 'aàâäeéèêëiîïoôöuùûüyÿ'.split('');
function joinWithCommasAndEt(items){ if(items.length===0)return ''; if(items.length===1)return items[0]; if(items.length===2)return items[0]+' et '+items[1]; return items.slice(0,-1).join(', ')+' et '+items[items.length-1]; }
function articleDe(word){ const first = word.trim().charAt(0).toLowerCase(); return VOWELS.includes(first) ? "d’" : "de "; }
function formatParementPhrase(keys){ if(!keys.length) return '—'; const parts = keys.map(k=> articleDe(PAREMENTS[k]) + PAREMENTS[k]); return 'Parement ' + joinWithCommasAndEt(parts); }
function updatePreview(){ const keys = Array.from(document.querySelectorAll('#parement-grid input[type=checkbox]:checked')).map(cb => cb.value); document.getElementById('parement-preview').textContent = formatParementPhrase(keys); }
document.getElementById('parement-grid').addEventListener('change', updatePreview);
/* === Défauts prédéfinis === */
const PRESETS = {
"REVÊTEMENT EXTÉRIEUR ET SOLINS": [
"REVÊTEMENT ENDOMMAGÉ/DÉTACHÉ","REVÊTEMENT TROP PRÈS DU SOL","SOLIN ABSENT","SOLIN MAL SCELLÉ","JOINTS DE SCELLANT DÉGRADÉS"
]
};
function fileToJpegDataURL(file){
return new Promise((resolve, reject)=>{
const img = new Image();
img.onload = ()=>{
const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0);
try { resolve(canvas.toDataURL('image/jpeg', 0.92)); }
catch(e){ const fr = new FileReader(); fr.onload = ()=> resolve(fr.result); fr.onerror = reject; fr.readAsDataURL(file); }
};
img.onerror = ()=>{
const fr = new FileReader(); fr.onload = ()=> resolve(fr.result); fr.onerror = reject; fr.readAsDataURL(file);
};
const fr0 = new FileReader(); fr0.onload = ()=> { img.src = fr0.result; }; fr0.onerror = reject; fr0.readAsDataURL(file);
});
}
function makePhotoSlot(){
const slot = document.createElement('div');
slot.className = 'photo-slot';
const inp = document.createElement('input'); inp.type = 'file'; inp.accept = 'image/*'; inp.setAttribute('capture','environment');
const ok = document.createElement('div'); ok.className='ok'; ok.textContent='Photo prise ✔';
const img = document.createElement('img'); img.className = 'thumb';
inp.addEventListener('change', async ()=>{
if (inp.files && inp.files[0]){
const dataUrl = await fileToJpegDataURL(inp.files[0]);
img.src = dataUrl; img.style.display='block'; ok.style.display='block'; inp.dataset.preview = dataUrl;
}
});
slot.appendChild(inp); slot.appendChild(img); slot.appendChild(ok);
return slot;
}
function createDefBlock(key){
const block = document.createElement('div');
block.className = 'def-block';
const select = document.createElement('select');
const empty = document.createElement('option'); empty.value=""; empty.textContent="— Choisir une déficience —"; select.appendChild(empty);
(PRESETS[key]||[]).forEach(txt=>{ const opt = document.createElement('option'); opt.value = txt; opt.textContent = txt; select.appendChild(opt); });
const lbl = document.createElement('label'); lbl.textContent = "Observation / Détails (modifiable)";
const textarea = document.createElement('textarea');
select.addEventListener('change', ()=>{ if (select.value){ textarea.value = textarea.value.trim() ? textarea.value + "\n\n" + select.value : select.value; } });
const photos = document.createElement('div'); photos.className = 'photo-grid'; for (let i=0;i<3;i++){ photos.appendChild(makePhotoSlot()); }
const actions = document.createElement('div'); actions.className='actions';
const rm = document.createElement('button'); rm.className='ghost'; rm.type='button'; rm.textContent='Retirer cette déficience'; rm.addEventListener('click', ()=> block.remove());
actions.appendChild(rm);
block.appendChild(select); block.appendChild(lbl); block.appendChild(textarea); block.appendChild(photos); block.appendChild(actions);
return block;
}
document.getElementById('list-revetement').appendChild(createDefBlock('REVÊTEMENT EXTÉRIEUR ET SOLINS'));
document.getElementById('add-revetement').addEventListener('click', ()=>{
document.getElementById('list-revetement').appendChild(createDefBlock('REVÊTEMENT EXTÉRIEUR ET SOLINS'));
});
function escapeHTML(s){ return (s||"").replace(/[&<>\"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;','\\'':'&#039;'}[m])); }
function buildReportNode(){
const meta = { client: document.getElementById('client').value, adresse: document.getElementById('adresse').value, date: document.getElementById('dateInspection').value, dossier: document.getElementById('dossier').value };
let pageNum = 1;
const page = document.createElement('div'); page.className = 'report-page';
const band = document.createElement('div'); band.className = 'rpt-headerband';
const left = document.createElement('div'); left.className = 'rpt-h-left'; left.textContent = 'INSPECTION PRÉ-ACHAT';
const right = document.createElement('div'); right.textContent=''; right.style.minWidth='36px';
band.appendChild(left); band.appendChild(right); page.appendChild(band);
const metaRow = document.createElement('div'); metaRow.style.cssText='display:flex;gap:24px;justify-content:flex-end;font-size:12px;color:#444;margin-top:8px';
metaRow.innerHTML = `<div><strong>Adresse :</strong> ${escapeHTML(meta.adresse)}</div><div><strong>Dossier :</strong> ${escapeHTML(meta.dossier)}</div><div><strong>Date :</strong> ${escapeHTML(meta.date)}</div>`;
page.appendChild(metaRow);
const h2 = document.createElement('div'); h2.className = 'report-h2'; h2.textContent = 'EXTÉRIEUR'; page.appendChild(h2);
const underline = document.createElement('div'); underline.className = 'report-underline'; page.appendChild(underline);
const note1 = document.createElement('div'); note1.className = 'report-note'; note1.textContent = 'Nous avons inspecté les différentes composantes extérieures par le niveau du sol.'; page.appendChild(note1);
const sub = document.createElement('div'); sub.className = 'report-h3'; sub.innerHTML = `<span class="dot">▪</span><span class="text">REVÊTEMENT EXTÉRIEUR ET SOLINS</span>`; page.appendChild(sub);
const note2 = document.createElement('div'); note2.className = 'report-note'; note2.textContent = "Tout endroit susceptible de laisser passer l’eau sur un bâtiment doit être scellé, tel que les bouches de ventilation, les prises électriques, les luminaires, les robinets, les portes, les fenêtres, etc. Afin d’éviter tout dommage causé par l’eau, nous recommandons également d’en faire l’entretien régulièrement."; page.appendChild(note2);
const keys = Array.from(document.querySelectorAll('#parement-grid input[type=checkbox]:checked')).map(cb => cb.value);
const phrase = (function(){
const map = {'brique':'brique','pierre':'pierre','vinyle':'vinyle','metal':'métal','bois_syn':'bois synthétique','acrylique':'acrylique','agregat':'agrégat','stuc':'stuc','bardeaux':'bardeaux d’asphalte','amiante':'amiante'};
const vowels = ['a','à','â','ä','e','é','è','ê','ë','i','î','ï','o','ô','ö','u','ù','û','ü','y','ÿ'];
const parts = keys.map(k=>{
const w = map[k]||'';
return (vowels.includes(w.charAt(0).toLowerCase()) ? "d’" : "de ") + w;
});
if (parts.length===0) return '—';
if (parts.length===1) return 'Parement ' + parts[0];
if (parts.length===2) return 'Parement ' + parts[0] + ' et ' + parts[1];
return 'Parement ' + parts.slice(0,-1).join(', ') + ' et ' + parts[parts.length-1];
})();
const lineParement = document.createElement('div'); lineParement.className = 'report-line'; lineParement.innerHTML = `<strong>Type de revêtement :</strong> ${phrase}`; page.appendChild(lineParement);
const list = document.getElementById('list-revetement');
Array.from(list.children).forEach(db=>{
const obs = db.querySelector('textarea').value.trim();
const previews = Array.from(db.querySelectorAll('.photo-slot input[type=file]')).map(i => i.dataset.preview).filter(Boolean);
if (!obs && !previews.length) return;
const wrap = document.createElement('div'); wrap.className = 'report-def';
if (obs){ const p = document.createElement('p'); p.textContent = obs; wrap.appendChild(p); }
if (previews.length){
const grid = document.createElement('div'); grid.className = 'report-photos';
previews.forEach(src=>{ const im=document.createElement('img'); im.src=src; grid.appendChild(im); });
wrap.appendChild(grid);
}
page.appendChild(wrap);
});
const footer = document.createElement('div'); footer.className = 'rpt-footer';
const L = document.createElement('div'); L.textContent = 'Jonathan Malette – Inspecteur en Bâtiment';
const C = document.createElement('div'); C.textContent = `Rapport exclusif pour : ${meta.client||''}`; C.style.flex='1'; C.style.textAlign='center';
const R = document.createElement('div'); R.textContent = `Dossier : ${meta.dossier||''} — Page ${pageNum}`;
footer.appendChild(L); footer.appendChild(C); footer.appendChild(R); page.appendChild(footer);
return page;
}
document.getElementById('generate').addEventListener('click', ()=>{
const report = document.getElementById('report'); report.innerHTML = ''; report.style.display='block';
report.appendChild(buildReportNode());
window.scrollTo({ top: report.offsetTop, behavior: 'smooth' });
});
document.getElementById('printBtn').addEventListener('click', ()=> window.print());
document.getElementById('reset').addEventListener('click', ()=> location.reload());
// Init Aperçu
updatePreview();
</script>
</body>
</html>