103 lines
5.0 KiB
JavaScript
103 lines
5.0 KiB
JavaScript
/*!
|
||
* 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=<id> 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 = `<h3>Woningen van leads en klanten (CRM)</h3>
|
||
<input type="search" class="new-crm-q" placeholder="Zoek op woning, naam of plaats" aria-label="Zoek in de CRM">
|
||
<ul class="new-crm-list" role="list"></ul>`;
|
||
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 => `<li><button type="button" class="new-crm-item" data-id="${w.id}">
|
||
<i class="bi bi-house"></i><span><strong>${UI.esc(w.titel)}</strong>
|
||
<small>${UI.esc([w.contact && w.contact.naam, w.relatie, w.contact && w.contact.plaats].filter(Boolean).join(' · '))}
|
||
· versie ${w.versie} · ${date(w.datum)}</small></span></button></li>`).join('')
|
||
: '<li class="new-crm-empty">Geen woningen gevonden.</li>';
|
||
};
|
||
const search = () => {
|
||
const id = ++req;
|
||
list(q.value.trim()).then(rows => { if (id === req) render(rows); }, err => { if (id === req) ul.innerHTML = `<li class="new-crm-empty">${UI.esc(err.message)}</li>`; });
|
||
};
|
||
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 };
|
||
})();
|