Compare commits
1
Commits
0da1769f51
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7215b9e62 |
@@ -46,6 +46,7 @@ js/unit/ UnitModel: 1 vloer + 4 wanden, naden, wandstukken
|
||||
js/house/ HouseModel (woning) + woning-PDF + CAD-export (house-dxf.js) + BIM-export (house-ifc.js) + woning-editor
|
||||
js/cost/ CostModel (kostenberekening), posten + noodwaarden, CostCsv (CSV's), venster Kosten & tijden
|
||||
js/app.js menubalk, versienummer, actieve editor
|
||||
js/crm.js koppeling met de Van Emous CRM (alleen in de Docker-stack, zie "CRM")
|
||||
light.html light-versie (modelpagina's), los te openen: light.html?model=amstel
|
||||
js/light/ LightModel (keuzes op een model toepassen, indelingscontrole, kerncijfers), LightPdf (klant-PDF) + light-UI
|
||||
css/light.css stijl light-versie, alles onder .tectum-light
|
||||
@@ -410,6 +411,21 @@ texturen, kleurstalen en `kosten/*.csv` laden via `tectumUrl()` (`js/core/geomet
|
||||
Per ontwerp wordt de GLB één keer gemaakt; `el.tectumLight.glb()` geeft `{ blob, url, stats }` (op
|
||||
`light.html` de testknop GLB).
|
||||
|
||||
## CRM (woningen van leads en klanten)
|
||||
|
||||
Alleen in de Docker-stack (`../docker`), waar Tectum op `tectum.<domein>` achter het wachtwoord staat. Caddy zet
|
||||
`api/crm/*` daar door naar de CRM (`/api/tectum/*`) met de sleutel `TECTUM_CRM_KEY`; de browser ziet die nooit.
|
||||
|
||||
- **Nieuw** toont onder de standaardmodellen de woningen uit de CRM (zoeken op woning, naam, relatie of plaats);
|
||||
kiezen opent de nieuwste versie in een eigen tab (wolkje in de tab, naam "woning · vN").
|
||||
- **`?woning=<id>`** opent die woning direct (de knop "Bewerken in Tectum" bij een woning in de CRM).
|
||||
- **Opslaan in CRM** (bovenbalk, Bestand, Ctrl+S in een CRM-tab) vraagt wat er veranderd is en zet het ontwerp als
|
||||
nieuwe versie bij de woning in de CRM. Staat er intussen een nieuwere versie (iemand anders), dan vraagt Tectum
|
||||
eerst of het toch moet. **Downloaden (JSON + PDF)** doet in een CRM-tab wat Opslaan anders doet.
|
||||
- Sluiten van een CRM-tab met wijzigingen die nog niet in de CRM staan vraagt om bevestiging.
|
||||
|
||||
Zonder stack (`start.sh`) bestaat `api/crm/` niet en is van de CRM niets te zien.
|
||||
|
||||
## Kosten en prijzen (CSV)
|
||||
|
||||
Alle waarden van het kostenmodel staan in twee CSV-bestanden in `kosten/`, die
|
||||
|
||||
+17
@@ -345,6 +345,23 @@ html, body { height: 100%; }
|
||||
.new-option span { font-size: 13px; }
|
||||
.new-option:hover:not(.disabled) { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
|
||||
.new-option.disabled { opacity: .4; cursor: not-allowed; }
|
||||
.new-crm { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border-soft); max-width: 640px; }
|
||||
.new-crm h3 { margin: 0 0 10px; font-size: 13px; color: var(--primary); }
|
||||
.new-crm-q { width: 100%; box-sizing: border-box; font: inherit; font-size: 13px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; }
|
||||
.new-crm-list { list-style: none; margin: 8px 0 0; padding: 0; max-height: 260px; overflow-y: auto; }
|
||||
.new-crm-item { display: flex; gap: 10px; align-items: center; width: 100%; text-align: left; background: none; border: 0; border-radius: 6px; padding: 6px 8px; cursor: pointer; color: var(--text); font: inherit; }
|
||||
.new-crm-item:hover, .new-crm-item:focus-visible { background: var(--primary-soft); color: var(--primary); }
|
||||
.new-crm-item .bi { font-size: 18px; }
|
||||
.new-crm-item strong { display: block; font-size: 13px; }
|
||||
.new-crm-item small { display: block; font-size: 12px; color: var(--text-muted); }
|
||||
.new-crm-empty { font-size: 12px; color: var(--text-muted); padding: 6px 8px; }
|
||||
.doc-tab .bi-cloud { margin-right: 4px; color: var(--primary); }
|
||||
#crm-toast { position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 20px); opacity: 0; pointer-events: none; z-index: 900;
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 10px 16px; font-size: 13px;
|
||||
box-shadow: 0 12px 30px rgba(33, 33, 33, .2); transition: opacity .2s, transform .2s; max-width: 90vw; }
|
||||
#crm-toast.open { opacity: 1; transform: translate(-50%, 0); }
|
||||
#crm-toast.ok { border-color: var(--ok); background: var(--ok-soft); }
|
||||
#crm-toast.err { border-color: var(--danger); background: var(--danger-soft); }
|
||||
|
||||
.hn-title { font-size: 12px; font-weight: bold; color: var(--primary); text-transform: uppercase; letter-spacing: .8px; padding: 2px 6px 6px; }
|
||||
#house-nav-tree { display: flex; flex-direction: column; gap: 2px; }
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<span>Los: vloer</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="new-crm" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -124,6 +125,7 @@
|
||||
<script src="js/house/house-dxf.js"></script>
|
||||
<script src="js/house/house-ifc.js"></script>
|
||||
<script src="js/house/house.js"></script>
|
||||
<script src="js/crm.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
const APP_VERSION = '0.7.8';
|
||||
const APP_VERSION = '0.7.9';
|
||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||
|
||||
let tabs = [];
|
||||
@@ -41,7 +41,9 @@ const MENUS = [
|
||||
{ label: 'Bestand', items: [
|
||||
{ label: 'Nieuw…', icon: 'file-earmark-plus', keys: 'Ctrl+N', run: openNewModal },
|
||||
{ label: 'Openen…', icon: 'folder2-open', keys: 'Ctrl+O', run: () => editorCall('triggerImport'), enabled: () => editorHas('triggerImport') },
|
||||
{ label: 'Opslaan (JSON + PDF)', icon: 'save', keys: 'Ctrl+S', run: () => editorCall('exportDesign'), enabled: () => editorHas('exportDesign') },
|
||||
{ label: 'Opslaan (JSON + PDF)', icon: 'save', keys: 'Ctrl+S', run: () => editorCall('exportDesign'), enabled: () => editorHas('exportDesign') && !activeCrm() },
|
||||
{ label: 'Opslaan in CRM (nieuwe versie)', icon: 'cloud-upload', keys: 'Ctrl+S', run: saveToCrm, enabled: () => !!activeCrm() },
|
||||
{ label: 'Downloaden (JSON + PDF)', icon: 'download', run: () => editorCall('exportDesign'), enabled: () => editorHas('exportDesign') && !!activeCrm() },
|
||||
{ sep: true }, { head: 'Exporteren' },
|
||||
{ label: 'CAD (DXF)', icon: 'vector-pen', run: () => editorCall('exportDxf'), enabled: () => editorHas('exportDxf') },
|
||||
{ label: 'BIM (IFC)', icon: 'boxes', run: () => editorCall('exportIfc'), enabled: () => editorHas('exportIfc') },
|
||||
@@ -71,7 +73,7 @@ const MENUS = [
|
||||
];
|
||||
|
||||
const KEY_HELP = [
|
||||
['Bestand', [['Ctrl+N', 'Nieuw ontwerp'], ['Ctrl+O', 'Openen'], ['Ctrl+S', 'Opslaan (JSON + PDF)']]],
|
||||
['Bestand', [['Ctrl+N', 'Nieuw ontwerp (ook uit de CRM)'], ['Ctrl+O', 'Openen'], ['Ctrl+S', 'Opslaan (JSON + PDF; woning uit de CRM: nieuwe versie in de CRM)']]],
|
||||
['Gereedschap (woning)', [['V', 'Selecteren'], ['W', 'Binnenwand tekenen'], ['R', 'Ruimte vullen'], ['I', 'Inrichting plaatsen'],
|
||||
['Esc', 'Gereedschap stoppen, daarna selectie opheffen'], ['Del', 'Selectie verwijderen'], ['D / Shift+D', 'Inrichting draaien'], ['Dubbelklik', 'Onderdeel bewerken (wand, vloer)']]],
|
||||
['Beeld', [['1 / 2 / 3', '2D + 3D, alleen 2D, alleen 3D'], ['4', 'Rapport aan/uit'], ['X', 'Doorsnede aan/uit'], ['F', 'Plattegrond passend'],
|
||||
@@ -101,11 +103,57 @@ function renderTabBar() {
|
||||
const bar = document.getElementById('doc-tabs');
|
||||
bar.innerHTML = tabs.map(tab => `
|
||||
<div class="doc-tab ${tab.id === activeTabId ? 'active' : ''}" data-tab-id="${tab.id}" role="tab" aria-selected="${tab.id === activeTabId}" title="${UI.esc(tab.name)}">
|
||||
<span class="doc-name">${UI.esc(tab.name)}</span>
|
||||
${tab.crm ? '<i class="bi bi-cloud" title="Woning uit de CRM"></i>' : ''}<span class="doc-name">${UI.esc(tab.name)}</span>
|
||||
<button type="button" class="doc-close" data-close-id="${tab.id}" title="Sluiten" aria-label="Sluiten">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('top-save').disabled = !editorHas('exportDesign');
|
||||
const save = document.getElementById('top-save');
|
||||
save.disabled = !editorHas('exportDesign');
|
||||
save.querySelector('span').textContent = activeCrm() ? 'Opslaan in CRM' : 'Opslaan';
|
||||
save.title = activeCrm() ? 'Opslaan als nieuwe versie in de CRM (Ctrl+S)' : 'Opslaan als JSON + PDF (Ctrl+S)';
|
||||
}
|
||||
|
||||
// ---- CRM (js/crm.js): woning uit de CRM in een tab (tab.crm = { id, versie, titel }), terug als nieuwe versie
|
||||
const activeCrm = () => { const t = tabs.find(x => x.id === activeTabId); return t && t.crm ? t : null; };
|
||||
const crmName = (crm) => `${crm.titel} · v${crm.versie}`;
|
||||
|
||||
function saveActive() {
|
||||
if (activeCrm()) saveToCrm(); else editorCall('exportDesign');
|
||||
}
|
||||
|
||||
async function openCrmWoning(id) {
|
||||
try {
|
||||
const { woning, state } = await TectumCrm.load(id);
|
||||
const open = tabs.find(t => t.crm && t.crm.id === woning.id);
|
||||
if (open && !window.confirm(`${woning.titel} is al geopend. Nog een keer openen (versie ${woning.versie} uit de CRM)?`)) {
|
||||
switchToTab(open.id);
|
||||
return;
|
||||
}
|
||||
const crm = { id: woning.id, versie: woning.versie, titel: woning.titel };
|
||||
openNew('woning', null, { stateJson: state, name: crmName(crm), crm });
|
||||
TectumCrm.toast(`${woning.titel} geladen uit de CRM (versie ${woning.versie}${woning.contact ? ', ' + woning.contact.naam : ''}).`, 'ok');
|
||||
} catch (err) {
|
||||
TectumCrm.toast('Laden uit de CRM mislukt: ' + err.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToCrm() {
|
||||
const tab = activeCrm();
|
||||
if (!tab || !currentEditor) return;
|
||||
const note = window.prompt(`Opslaan als nieuwe versie van ${tab.crm.titel} in de CRM.\nWat is er veranderd?`, '');
|
||||
if (note === null) return;
|
||||
const state = currentEditor.getStateJson();
|
||||
try {
|
||||
const versie = await TectumCrm.saveVersion(tab.crm.id, state, tab.crm.versie, note.trim());
|
||||
if (versie === null) return;
|
||||
tab.crm.versie = versie;
|
||||
tab.baseJson = JSON.stringify(state);
|
||||
tab.name = crmName(tab.crm);
|
||||
renderTabBar();
|
||||
TectumCrm.toast(`Opgeslagen in de CRM als versie ${versie}.`, 'ok');
|
||||
} catch (err) {
|
||||
TectumCrm.toast('Opslaan in de CRM mislukt: ' + err.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
function onTabSaved(tab, filenameWithExt) {
|
||||
@@ -138,7 +186,7 @@ function switchToTab(tabId) {
|
||||
renderTabBar();
|
||||
}
|
||||
|
||||
function openNew(kind, template) {
|
||||
function openNew(kind, template, opts) {
|
||||
if (!editorModuleFor(kind)) return;
|
||||
|
||||
const oldTab = tabs.find(t => t.id === activeTabId);
|
||||
@@ -148,10 +196,13 @@ function openNew(kind, template) {
|
||||
if (currentEditor && typeof currentEditor.unmount === 'function') currentEditor.unmount();
|
||||
currentEditor = null;
|
||||
|
||||
const tab = { id: tabIdCounter++, kind, template: template || null, name: defaultNameFor(kind), savedName: null, stateJson: null };
|
||||
const tab = { id: tabIdCounter++, kind, template: template || null, name: (opts && opts.name) || defaultNameFor(kind), savedName: null,
|
||||
stateJson: (opts && opts.stateJson) || null, crm: (opts && opts.crm) || null };
|
||||
tabs.push(tab);
|
||||
activeTabId = tab.id;
|
||||
mountEditorForTab(tab);
|
||||
// geladen ontwerp (CRM): wijzigingen tellen vanaf hier, niet vanaf het standaardmodel
|
||||
if (tab.stateJson && currentEditor && typeof currentEditor.getStateJson === 'function') tab.baseJson = JSON.stringify(currentEditor.getStateJson());
|
||||
renderTabBar();
|
||||
}
|
||||
|
||||
@@ -162,6 +213,7 @@ function isTabDirty(tab) {
|
||||
? currentEditor.getStateJson()
|
||||
: tab.stateJson;
|
||||
if (!state) return false;
|
||||
if (tab.baseJson) return JSON.stringify(state) !== tab.baseJson;
|
||||
return JSON.stringify(state) !== JSON.stringify(module.getDefaultState(tab.template));
|
||||
}
|
||||
|
||||
@@ -178,7 +230,9 @@ function closeTab(tabId) {
|
||||
switchToTab(tabId);
|
||||
}
|
||||
|
||||
if (isTabDirty(tab) && currentEditor && typeof currentEditor.exportDesign === 'function') {
|
||||
if (tab.crm && isTabDirty(tab)) {
|
||||
if (!window.confirm(`De wijzigingen aan ${tab.crm.titel} staan nog niet in de CRM (Opslaan in CRM). Toch sluiten?`)) return;
|
||||
} else if (isTabDirty(tab) && currentEditor && typeof currentEditor.exportDesign === 'function') {
|
||||
const saved = currentEditor.exportDesign();
|
||||
if (!saved) return;
|
||||
}
|
||||
@@ -233,10 +287,16 @@ function wireNewModal() {
|
||||
overlay.classList.remove('open');
|
||||
});
|
||||
});
|
||||
newCrmPicker = TectumCrm.mountPicker(overlay.querySelector('.new-crm'), (id) => {
|
||||
overlay.classList.remove('open');
|
||||
openCrmWoning(id);
|
||||
});
|
||||
}
|
||||
|
||||
let newCrmPicker = null;
|
||||
function openNewModal() {
|
||||
document.getElementById('new-overlay').classList.add('open');
|
||||
if (newCrmPicker) newCrmPicker.refresh();
|
||||
}
|
||||
|
||||
function toggleBrowserFullscreen() {
|
||||
@@ -267,7 +327,7 @@ function wireHotkeys() {
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
const act = { s: () => editorCall('exportDesign'), n: openNewModal, o: () => editorCall('triggerImport'), '\\': togglePanels }[k];
|
||||
const act = { s: saveActive, n: openNewModal, o: () => editorCall('triggerImport'), '\\': togglePanels }[k];
|
||||
if (act && !e.altKey && !e.shiftKey) { e.preventDefault(); act(); }
|
||||
return;
|
||||
}
|
||||
@@ -288,7 +348,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.app-version-slot').forEach(el => { el.textContent = 'v' + APP_VERSION; });
|
||||
document.querySelectorAll('.app-version-number').forEach(el => { el.textContent = APP_VERSION; });
|
||||
Menubar.init(document.getElementById('menubar'), MENUS);
|
||||
document.getElementById('top-save').addEventListener('click', () => editorCall('exportDesign'));
|
||||
document.getElementById('top-save').addEventListener('click', saveActive);
|
||||
document.getElementById('keys-overlay').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'keys-overlay' || e.target.closest('[data-close]')) document.getElementById('keys-overlay').classList.remove('open');
|
||||
});
|
||||
@@ -302,7 +362,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
note.innerHTML = '<i class="bi bi-exclamation-triangle"></i><span>Tectum is geopend als bestand. Start het via een webserver: <code>./start.sh</code> of <code>python3 -m http.server</code>, en open <code>http://localhost:8000</code>. De omgeving (Dag/Nacht) werkt anders niet.</span>';
|
||||
document.body.appendChild(note);
|
||||
}
|
||||
openNew('woning', 'amstel');
|
||||
// ?woning=<id>: die woning uit de CRM openen (knop "Bewerken in Tectum" in de CRM), anders het standaardmodel
|
||||
const crmId = Number(new URLSearchParams(location.search).get('woning'));
|
||||
if (crmId) openCrmWoning(crmId); else openNew('woning', 'amstel');
|
||||
|
||||
setTimeout(() => {
|
||||
const splash = document.getElementById('splash-screen');
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*!
|
||||
* 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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user