/*! * Tectum * Copyright © 2026 Machiel Heinen. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ // Koppeling met de Van Emous CRM (alleen in de Docker-stack): woningen van leads en klanten laden (Nieuw › Uit de // CRM, of ?woning= in de URL) en een gewijzigde woning als nieuwe versie terugzetten (Opslaan in CRM). // De API staat op api/crm/ naast deze pagina: Caddy stuurt dat na het Tectum-wachtwoord door naar de CRM, met de // sleutel erbij. Zonder stack (start.sh) geeft api/crm/ een 404 en blijft alles van de CRM verborgen. window.TectumCrm = (function () { const API = 'api/crm/woningen'; let available = null; async function call(url, opts) { const res = await fetch(url, { credentials: 'same-origin', headers: { Accept: 'application/json', ...(opts && opts.body ? { 'Content-Type': 'application/json' } : {}) }, ...opts }); let data = null; try { data = await res.json(); } catch (e) { /* geen JSON: geen CRM */ } if (!data) throw new Error(`CRM niet bereikbaar (HTTP ${res.status})`); return { status: res.status, data }; } // Eén keer kijken of de CRM bereikbaar is (de lijst ophalen); onthoudt de uitkomst function isAvailable() { if (!available) available = list('').then(() => true, () => false); return available; } async function list(q) { const { data } = await call(`${API}?q=${encodeURIComponent(q || '')}`); if (!data.ok) throw new Error(data.error || 'CRM-fout'); return data.woningen; } // Ontwerp van een woning (nieuwste versie, of `versie`), klaar voor de editor: gecontroleerd en zonder de // bestandsvelden (type, version, exportedAt), zoals bij Openen. async function load(id, versie) { const { data } = await call(`${API}/${encodeURIComponent(id)}${versie ? `?versie=${versie}` : ''}`); if (!data.ok) throw new Error(data.error || 'Woning niet gevonden'); HouseModel.validateState(data.ontwerp); const { type, version, exportedAt, ...state } = data.ontwerp; return { woning: data.woning, state }; } // Nieuwe versie bewaren. basis = de versie die geladen was; is er intussen een nieuwere, dan vraagt dit eerst. async function saveVersion(id, state, basis, opmerking) { const ontwerp = { type: 'house-design', version: 1, exportedAt: new Date().toISOString(), configurator: APP_VERSION, ...state }; const send = (forceer) => call(`${API}/${encodeURIComponent(id)}/versies`, { method: 'POST', body: JSON.stringify({ ontwerp, opmerking, basis, forceer }) }); let { status, data } = await send(false); if (status === 409) { if (!window.confirm(`In de CRM staat intussen versie ${data.versie} van deze woning (u bewerkt versie ${basis}). Toch opslaan als nieuwe versie?`)) return null; ({ data } = await send(true)); } if (!data.ok) throw new Error(data.error || 'Opslaan mislukt'); return data.versie; } function toast(msg, cls) { let el = document.getElementById('crm-toast'); if (!el) { el = document.createElement('div'); el.id = 'crm-toast'; el.setAttribute('role', 'status'); document.body.appendChild(el); } el.textContent = msg; el.className = 'open ' + (cls || ''); clearTimeout(toast._t); toast._t = setTimeout(() => { el.className = ''; }, 5000); } const date = (s) => (s || '').slice(0, 10).split('-').reverse().join('-'); // Sectie "Woningen uit de CRM" in het venster Nieuw: zoeken en kiezen; verborgen zolang de CRM er niet is function mountPicker(host, onPick) { host.innerHTML = `

Woningen van leads en klanten (CRM)

`; const q = host.querySelector('.new-crm-q'), ul = host.querySelector('.new-crm-list'); let req = 0, timer = null; const render = (rows) => { ul.innerHTML = rows.length ? rows.map(w => `
  • `).join('') : '
  • Geen woningen gevonden.
  • '; }; const search = () => { const id = ++req; list(q.value.trim()).then(rows => { if (id === req) render(rows); }, err => { if (id === req) ul.innerHTML = `
  • ${UI.esc(err.message)}
  • `; }); }; q.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(search, 250); }); ul.addEventListener('click', (e) => { const b = e.target.closest('.new-crm-item'); if (b) onPick(Number(b.dataset.id)); }); isAvailable().then(ok => { host.hidden = !ok; if (ok) search(); }); return { refresh: () => { if (!host.hidden) search(); } }; } return { isAvailable, list, load, saveVersion, mountPicker, toast }; })();