Files
web.start-it.nl/themes/agico-hugo/layouts/shortcodes/quote.html
2025-12-02 16:34:42 +01:00

703 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{/* Drop-in shortcode: quote.html offerteformulier met items, optional, quantities, en herhaalbare secties (Websites/Cloud/Netwerk) */}}
{{/* Bronproducten uit page front matter of fallback site data */}}
{{ $products := slice }}
{{ if .Page.Params.products }}
{{ $products = .Page.Params.products }}
{{ else if site.Data.products }}
{{ $products = site.Data.products }}
{{ end }}
{{/* Optioneel: filteren op categorie via page param `productFilter` (string of array) */}}
{{ $filter := .Page.Params.productFilter }}
{{ if $filter }}
{{ $typeStr := printf "%T" $filter }}
{{ $filters := cond (eq $typeStr "[]interface {}") $filter (slice $filter) }}
{{ $products = where $products "categorie" "in" $filters }}
{{ end }}
<section class="section pt-0 mt-0">
<div class="container col-lg-10 bg-light p-4 rounded-sm">
<div class="row">
<div class="col-lg-12 px-lg-6 px-md-5 px-sm-4 mx-auto">
<form id="quote-form" class="row" novalidate>
<!-- Product-secties -->
<div class="col-lg-12 mb-3">
<h2>Stel je diensten samen</h2>
<p>Maak hieronder je keuze per dienst die wij aanbieden.</p>
{{ range $i, $p := $products }}
{{ $idx := printf "%d" $i }}
{{ $items := cond (isset $p "items") $p.items (slice) }}
{{ $optList := cond (isset $p "optional") $p.optional (slice) }}
{{ $qtyList := cond (isset $p "quantity") $p.quantity (slice) }}
<div class="product-wrap mb-4" data-wrap-idx="{{ $idx }}" data-key="{{ $p.key }}" data-base="{{ with $p.prijs }}{{ . }}{{ else }}0{{ end }}" data-categorie="{{ $p.categorie }}">
<div class="d-flex align-items-center justify-content-between">
<div class="form-check">
<input class="form-check-input me-2 prod-checkbox" type="checkbox" id="prod-{{ $idx }}" name="prod-{{ $p.key }}" data-item="item-{{ $idx }}" data-base="{{ with $p.prijs }}{{ . }}{{ else }}0{{ end }}">
<label class="form-check-label" for="prod-{{ $idx }}"><b>{{ $p.naam }}</b></label>
</div>
</div>
<div class="product-sections d-none">
<div class="row g-3">
<div class="col-lg-8 pt-4">
{{ if $p.omschrijving }}<p class="text-muted small mb-2">{{ $p.omschrijving | markdownify }}</p>{{ end }}
{{ if gt (len $items) 0 }}
<!-- <h4 class="form-label mt-1" for="item-{{ $idx }}">Stel je <span style="text-transform: lowercase;">{{ $p.naam }}</span> samen</h4> -->
<select id="item-{{ $idx }}" name="item-{{ $p.key }}" class="form-select mt-1 item-select" data-idx="{{ $idx }}">
<option class="text-light" value="" disabled selected>{{ i18n "form-select-option" }}</option>
{{ range $j, $d := $items }}
<option value="{{ with $d.prijs }}{{ . }}{{ else }}0{{ end }}"
data-code="{{ $d.code }}"
data-service="{{ with $d.prijsService }}{{ . }}{{ else }}0{{ end }}"
data-periode="{{ with $d.prijsServicePeriode }}{{ . }}{{ else }}maand{{ end }}">
{{ if and (isset $d "prijs") (ne $d.prijs 0) }}€{{ $d.prijs }} — {{ end }}{{ $d.label }}
</option>
{{ end }}
</select>
{{ end }}
{{ if gt (len $optList) 0 }}
<div class="mt-3 optional-block">
<h6 class="d-block my-2">Optioneel:</h6>
{{ range $k, $opt := $optList }}
<div class="form-check">
<input class="form-check-input calc-extra optional-item" type="checkbox" id="opt-{{ $idx }}-{{ $k }}" name="opt-{{ $p.key }}-{{ $k }}" value="{{ with $opt.prijs }}{{ . }}{{ else }}0{{ end }}" data-service="{{ with $opt.prijsService }}{{ . }}{{ else }}0{{ end }}" data-periode="{{ with $opt.prijsServicePeriode }}{{ . }}{{ else }}maand{{ end }}" data-required-for='{{ if isset $opt "requiredFor" }}{{ $opt.requiredFor | jsonify }}{{ else }}[]{{ end }}'>
<input type="hidden" id="opt-{{ $idx }}-{{ $k }}-hidden" name="opt-{{ $p.key }}-{{ $k }}-hidden" value="0">
<label class="form-check-label pb-2" for="opt-{{ $idx }}-{{ $k }}"><b>{{ $opt.label }}</b> - {{ if and (isset $opt "prijs") (ne $opt.prijs 0) }} €{{ $opt.prijs }}{{ end }}{{ if $opt.omschrijving }} <br><small class="text-muted">{{ $opt.omschrijving | markdownify }}</small>{{ end }}</label>
</div>
{{ end }}
</div>
{{ end }}
{{ if gt (len $qtyList) 0 }}
<div class="mt-3 qty-block" data-idx="{{ $idx }}">
{{ range $q, $qty := $qtyList }}
<label class="form-label d-block my-2" for="qty-{{ $idx }}-{{ $q }}">
<b>{{ $qty.label }} {{ if and (isset $qty "prijs") (ne $qty.prijs 0) }} €{{ $qty.prijs }}{{ end }}</b>{{ if $qty.omschrijving }} <br><small class="text-muted">{{ $qty.omschrijving | markdownify }}</small>{{ end }}
</label>
<input
type="number"
min="0"
step="1"
value="0"
class="form-control qty-item mb-2"
data-unit="{{ with $qty.prijs }}{{ . }}{{ else }}0{{ end }}"
data-unit-service="{{ with $qty.prijsService }}{{ . }}{{ else }}0{{ end }}"
data-periode="{{ with $qty.prijsServicePeriode }}{{ . }}{{ else }}maand{{ end }}"
data-idx="{{ $idx }}"
id="qty-{{ $idx }}-{{ $q }}"
name="qty-{{ $p.key }}-{{ $q }}"
>
{{ end }}
</div>
{{ end }}
<hr class="mt-4 mb-0">
</div>
<div class="col-lg-4">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title mb-2">{{ or $p.infoTitle (printf "%s info" $p.naam) }}</h6>
{{ if isset $p "infobox" }}
<span class="text-muted small mb-0">{{ $p.infobox | markdownify }}</span>
{{ else if $p.omschrijving }}
<span class="text-muted small mb-0">{{ $p.omschrijving | markdownify }}</span>
{{ else }}
<span class="text-muted small mb-0">Selecteer opties en vul aantallen in.</span>
{{ end }}
<!-- SUBTOTAAL: sticky onderin de infobox -->
<div class="mt-3 pt-2 border-top d-flex justify-content-between align-items-center subtot-sticky">
<div class="w-100">
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">Kosten eenmalig</span>
<strong><span class="product-subtotal-once" data-idx="{{ $idx }}">0</span></strong>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">Kosten per maand</span>
<strong><span class="product-subtotal-service" data-idx="{{ $idx }}">0</span></strong>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}
</div>
<div class="col-12 my-3 d-flex align-items-center">
<p class="mb-0 h4">
Totaal eenmalig: <strong><span id="total-once">0</span></strong><br>
<small>Kosten per maand: <strong><span id="total-service">0</span></strong></small>
</p>
</div>
<hr>
<h4>Ontvang berekening en een persoonlijke offerte</h4>
<p>Laat jouw gegevens achter om de berekening met aanvullende informatie te ontvangen per mail. Ook zullen wij in contact met je komen over jouw nieuwe IT project. Wij nemen binnen 3 werkdagen contact op om alles door te nemen en een offerte op maat te maken.</p>
<p><b>In de tussentijd nog vragen?</b><br>Bel dan naar <a href="tel:+31657969491">+31 6 5796 9491</a> of stuur een email naar <a href="mailto:info@start-it.nl">info@start-it.nl</a></p>
<input type="hidden" name="estimatedTotal" id="estimatedTotal" value="0">
<!-- Honeypot tegen bots -->
<input type="text" id="company" name="company" class="d-none" tabindex="-1" autocomplete="off">
<!-- Contact -->
<div class="col-lg-6"><input type="text" name="firstName" class="form-control mb-4" placeholder="Voornaam"></div>
<div class="col-lg-6"><input type="text" name="lastName" class="form-control mb-4" placeholder="Achternaam *" required></div>
<div class="col-lg-6"><input type="email" name="email" class="form-control mb-4" placeholder="E-mail" required></div>
<div class="col-lg-6"><input type="tel" name="phone" class="form-control mb-4" placeholder="Telefoon *" required></div>
<div class="col-lg-12"><textarea name="description" class="form-control mb-4" placeholder="Aanvullende informatie over jouw project..."></textarea></div>
<div class="col-lg-12"><input type="checkbox" name="agree"><label style="padding-left: 4px;" for="agree" required>Akkoord met voorwaarden & privacybeleid</label></div>
<div class="col-12 mt-4"><button type="submit" class="btn btn-primary">Ontvang offerte</button></div>
</form>
</div>
</div>
</div>
</section>
<script>
(function () {
// === ESPOCRM LeadCapture endpoint ===
const ESPO_ENDPOINT = 'https://crm.start-it.nl/api/v1/LeadCapture/231344ae2852d65f41e98d99da418af8';
// --- ELEMENTS ---
const totalEl = document.getElementById('total'); // bestaand totaal (blijft bestaan)
const hiddenTotalEl = document.getElementById('estimatedTotal');
const calculateBtn = document.getElementById('calculate');
const form = document.getElementById('quote-form');
const prodCheckboxes = document.querySelectorAll('.prod-checkbox');
const extrasEls = document.querySelectorAll('.calc-extra:not(.optional-item)');
const optionalEls = document.querySelectorAll('.optional-item');
// === DECIMAAL-VEILIG PARSEN ===
function toNumberSafe(value) {
if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
let s = String(value || '').trim();
if (!s) return 0;
s = s.replace(/\s/g, '');
const hasComma = s.includes(',');
const hasDot = s.includes('.');
if (hasComma && hasDot) {
const lastComma = s.lastIndexOf(',');
const lastDot = s.lastIndexOf('.');
if (lastComma > lastDot) { s = s.replace(/\./g, ''); s = s.replace(',', '.'); }
else { s = s.replace(/,/g, ''); }
} else if (hasComma) { s = s.replace(/\./g, ''); s = s.replace(',', '.'); }
else { s = s.replace(/,/g, ''); }
const n = Number(s);
return Number.isFinite(n) ? n : 0;
}
function toQtySafe(value) { return Math.max(0, Math.floor(toNumberSafe(value))); }
// === HELPERS ===
function toIntSafe(value) { const s = String(value).replace(/[^\d\-]/g, ''); const n = parseInt(s, 10); return Number.isNaN(n) ? 0 : n; }
function getIdxFromCb(cb) { const id = cb.dataset.item; if (!id) return null; const m = id.match(/^item-(\d+)$/); return m ? m[1] : null; }
// === UI UPGRADE: voeg service-totaal bovenaan toe als die ontbreekt ===
// Bestaand totaal (id="total") blijft de "eenmalig" tonen; we voegen "per maand" eronder.
(function ensureHeaderTotals(){
if (!totalEl) return;
const parent = totalEl.closest('p') || totalEl.parentElement;
if (!parent) return;
if (!document.getElementById('total-once')) {
// Wrap het bestaande getal in een ID 'total-once' zonder de layout te breken
totalEl.id = 'total-once';
}
if (!document.getElementById('total-service')) {
const small = document.createElement('small');
small.innerHTML = 'Kosten per maand: <strong>€<span id="total-service">0</span></strong>';
parent.appendChild(document.createElement('br'));
parent.appendChild(small);
}
})();
const totalOnceEl = document.getElementById('total-once');
const totalServiceEl = document.getElementById('total-service');
// === UI UPGRADE: vervang subtotaalblok in infobox ON-THE-FLY (niet in template) ===
// We laten de bestaande container staan, maar tonen daarbinnen 2 regels.
function upgradeSubtotalsDOM() {
document.querySelectorAll('.subtot-sticky').forEach(box => {
// al geüpgraded?
if (box.querySelector('.product-subtotal-once') || box.querySelector('.product-subtotal-service')) return;
// Haal idx via een van de bestaande elementen in de buurt
const idxEl = box.closest('.product-wrap');
const idx = idxEl ? idxEl.getAttribute('data-wrap-idx') : null;
if (!idx) return;
// Maak twee regels (eenmalig + per maand)
const wrapper = document.createElement('div');
wrapper.className = 'w-100';
const row1 = document.createElement('div');
row1.className = 'd-flex justify-content-between align-items-center';
row1.innerHTML = '<span class="text-muted small">Kosten eenmalig</span><strong>€<span class="product-subtotal-once" data-idx="'+idx+'">0</span></strong>';
const row2 = document.createElement('div');
row2.className = 'd-flex justify-content-between align-items-center';
row2.innerHTML = '<span class="text-muted small">Kosten per maand</span><strong>€<span class="product-subtotal-service" data-idx="'+idx+'">0</span></strong>';
// Opruimen oude single-subtotal, maar laat de rest van de layout intact
box.innerHTML = '';
box.appendChild(wrapper);
wrapper.appendChild(row1);
wrapper.appendChild(row2);
});
}
upgradeSubtotalsDOM();
// === COLLAPSIBLE ANIMATIE: zonder d-none races ===
function prepareCollapsibles() {
document.querySelectorAll('.product-sections').forEach(sec => {
sec.classList.remove('d-none'); // niet met display:none werken
sec.classList.remove('open'); // start dicht
sec.style.maxHeight = '0px';
sec.dataset.animating = '0';
});
}
prepareCollapsibles();
function toggleSection(checkbox) {
const idx = getIdxFromCb(checkbox);
if (!idx) return;
const wrap = document.querySelector(`.product-wrap[data-wrap-idx="${idx}"]`);
const sections = wrap ? wrap.querySelector('.product-sections') : null;
const selectEl = document.getElementById(`item-${idx}`);
const qtyInputs = wrap ? wrap.querySelectorAll('.qty-item') : [];
const optChecks = wrap ? wrap.querySelectorAll('.optional-item') : [];
if (!sections) return;
if (sections.dataset.animating === '1') return;
const forceReflow = el => el.offsetHeight;
if (checkbox.checked) {
if (sections.classList.contains('open')) return;
sections.dataset.animating = '1';
sections.style.maxHeight = '0px';
sections.style.removeProperty('opacity');
forceReflow(sections);
const target = sections.scrollHeight;
sections.classList.add('open'); // opacity -> 1 via CSS
sections.style.maxHeight = target + 'px';
const onEndOpen = (e) => {
if (e.propertyName !== 'max-height') return;
sections.style.maxHeight = 'auto';
sections.dataset.animating = '0';
sections.removeEventListener('transitionend', onEndOpen);
};
sections.addEventListener('transitionend', onEndOpen);
} else {
const isClosedNow = (!sections.classList.contains('open') && (sections.style.maxHeight === '0px' || !sections.style.maxHeight));
if (isClosedNow) return;
sections.dataset.animating = '1';
const current = sections.scrollHeight;
sections.style.maxHeight = current + 'px';
sections.style.removeProperty('opacity');
forceReflow(sections);
sections.classList.remove('open');
sections.style.maxHeight = '0px';
const onEndClose = (e) => {
if (e.propertyName !== 'max-height') return;
sections.dataset.animating = '0';
sections.removeEventListener('transitionend', onEndClose);
sections.style.maxHeight = '0px';
// Reset velden
if (selectEl) selectEl.value = '0';
qtyInputs.forEach(inp => inp.value = '0');
optChecks.forEach(opt => {
opt.checked = false;
const hidden = document.getElementById(opt.id + '-hidden');
if (hidden) hidden.value = '0';
});
updateUI(); // na reset opnieuw rekenen
};
sections.addEventListener('transitionend', onEndClose);
}
}
function computeTotals() {
let once = 0; // eenmalig
let service = 0; // per maand
// extras (globaal) → eenmalig
extrasEls.forEach(el => {
if (!el.checked) return;
// globale extras tellen we als eenmalig (pas aan als je hier ook service wilt)
once += toNumberSafe(el.value);
});
// producten
prodCheckboxes.forEach(cb => {
if (!cb.checked) return;
const idx = getIdxFromCb(cb);
const baseVal = toNumberSafe(cb.dataset.base || 0);
let itemOnce = 0;
let itemSvc = 0;
if (idx) {
const selectEl = document.getElementById(`item-${idx}`);
if (selectEl) {
const sel = selectEl.options[selectEl.selectedIndex];
itemOnce = toNumberSafe(selectEl.value || 0);
itemSvc = sel ? toNumberSafe(sel.dataset.service || 0) : 0;
}
// Base en item zijn *alternatief*: als er een itemprijs is, laat base weg.
once += (itemOnce > 0 ? itemOnce : baseVal);
service += itemSvc;
const wrap = document.querySelector(`.product-wrap[data-wrap-idx="${idx}"]`);
if (wrap) {
// quantities → eenmalig + (optioneel) per maand
wrap.querySelectorAll('.qty-item').forEach(inp => {
const qty = toQtySafe(inp.value);
if (!qty) return;
const unitOnce = toNumberSafe(inp.dataset.unit || 0);
const unitSvc = toNumberSafe(inp.dataset.unitService || 0);
once += qty * unitOnce;
service += qty * unitSvc;
});
// optionals → eenmalig (uitgezonderd inbegrepen)
// optionals (eenmalig + service)
wrap.querySelectorAll('.optional-item').forEach(opt => {
if (!opt.checked) return;
const sel = selectEl ? selectEl.options[selectEl.selectedIndex] : null;
const selectedCode = sel ? sel.dataset.code : '';
let requiredFor = [];
try { requiredFor = JSON.parse(opt.dataset.requiredFor || '[]'); } catch (e) { requiredFor = []; }
if (requiredFor.indexOf(selectedCode) !== -1) return; // inbegrepen → niet rekenen
const onceVal = toNumberSafe(opt.value || 0);
const svcVal = toNumberSafe(opt.dataset.service || 0);
once += onceVal;
service += svcVal;
});
}
} else {
// Geen idx/select → alleen base
once += baseVal;
}
});
return { once, service };
}
function computeProductSubtotalPair(idx) {
let once = 0;
let service = 0;
const wrap = document.querySelector(`.product-wrap[data-wrap-idx="${idx}"]`);
if (!wrap) return { once: 0, service: 0 };
const cb = wrap.querySelector(`#prod-${idx}`);
if (!cb || !cb.checked) return { once: 0, service: 0 };
const baseVal = toNumberSafe(cb.dataset.base || 0);
// item-select
const selectEl = document.getElementById(`item-${idx}`);
let itemOnce = 0;
let itemSvc = 0;
if (selectEl) {
const sel = selectEl.options[selectEl.selectedIndex];
itemOnce = toNumberSafe(selectEl.value || 0);
itemSvc = sel ? toNumberSafe(sel.dataset.service || 0) : 0;
}
// Base en item zijn *alternatief*: als er een itemprijs is, geen base tellen.
once += (itemOnce > 0 ? itemOnce : baseVal);
service += itemSvc;
// optionals (eenmalig, behalve inbegrepen)
// optionals (eenmalig + service)
wrap.querySelectorAll('.optional-item').forEach(opt => {
if (!opt.checked) return;
const sel = selectEl ? selectEl.options[selectEl.selectedIndex] : null;
const selectedCode = sel ? sel.dataset.code : '';
let requiredFor = [];
try { requiredFor = JSON.parse(opt.dataset.requiredFor || '[]'); } catch (e) { requiredFor = []; }
if (requiredFor.indexOf(selectedCode) !== -1) return; // inbegrepen → niet rekenen
const onceVal = toNumberSafe(opt.value || 0);
const svcVal = toNumberSafe(opt.dataset.service || 0);
once += onceVal;
service += svcVal;
});
// quantities (eenmalig + periodiek)
wrap.querySelectorAll('.qty-item').forEach(inp => {
const qty = toQtySafe(inp.value);
if (!qty) return;
const unitOnce = toNumberSafe(inp.dataset.unit || 0);
const unitSvc = toNumberSafe(inp.dataset.unitService || 0);
once += qty * unitOnce;
service += qty * unitSvc;
});
return { once, service };
}
function updateProductSubtotals(idx) {
const pair = computeProductSubtotalPair(idx);
const onceSpan = document.querySelector(`.product-subtotal-once[data-idx="${idx}"]`);
const svcSpan = document.querySelector(`.product-subtotal-service[data-idx="${idx}"]`);
if (onceSpan) onceSpan.textContent = pair.once.toLocaleString('nl-NL');
if (svcSpan) svcSpan.textContent = pair.service.toLocaleString('nl-NL');
}
function updateAllProductSubtotals() {
document.querySelectorAll('.product-subtotal-once').forEach(span => {
const idx = span.getAttribute('data-idx');
updateProductSubtotals(idx);
});
}
// === UI UPDATE ===
function updateUI() {
// header totals
const { once, service } = computeTotals();
if (totalOnceEl) totalOnceEl.textContent = once.toLocaleString('nl-NL');
if (totalServiceEl) totalServiceEl.textContent = service.toLocaleString('nl-NL');
if (hiddenTotalEl) hiddenTotalEl.value = once; // hidden bewaart eenmalig (compatibel met bestaande verwerkingen)
// per product
updateAllProductSubtotals();
}
// === BINDINGS ===
prodCheckboxes.forEach(cb => {
toggleSection(cb); // initial state
cb.addEventListener('change', () => { toggleSection(cb); updateUI(); });
const idx = getIdxFromCb(cb);
if (idx) {
const selectEl = document.getElementById(`item-${idx}`);
if (selectEl) selectEl.addEventListener('change', () => updateUI());
}
});
extrasEls.forEach(el => {
const hidden = document.getElementById(el.id + '-hidden');
const sync = () => {
if (hidden) hidden.value = el.checked ? el.value : '0';
updateUI();
};
el.addEventListener('change', sync);
sync(); // init
});
optionalEls.forEach(el => {
const hidden = document.getElementById(el.id + '-hidden');
const sync = () => {
if (hidden) hidden.value = el.checked ? el.value : '0';
updateUI(); // triggert totals + subtotals
};
el.addEventListener('change', sync);
sync(); // init
});
document.querySelectorAll('.qty-item').forEach(inp => {
inp.addEventListener('input', () => {
updateUI();
const w = inp.closest('.product-wrap');
if (w) updateProductSubtotals(w.getAttribute('data-wrap-idx'));
});
});
if (calculateBtn) calculateBtn.addEventListener('click', () => updateUI());
document.querySelectorAll('.item-select').forEach(select => {
const idx = select.dataset.idx || (select.id.match(/^item-(\d+)$/) || [])[1];
if (!idx) return;
const wrap = document.querySelector(`.product-wrap[data-wrap-idx="${idx}"]`);
const optionals = wrap ? Array.from(wrap.querySelectorAll('.optional-item')) : [];
const applyState = () => {
const selectedOption = select.options[select.selectedIndex];
const selectedCode = selectedOption ? selectedOption.dataset.code : '';
optionals.forEach(opt => {
const raw = opt.dataset.requiredFor || '[]'; let requiredFor = [];
try { requiredFor = JSON.parse(raw); } catch (e) { requiredFor = []; }
const must = requiredFor.indexOf(selectedCode) !== -1;
const hidden = document.getElementById(opt.id + '-hidden');
if (must) { opt.checked = true; opt.disabled = true; if (hidden) hidden.value = '0'; }
else { opt.disabled = false; if (hidden) hidden.value = opt.checked ? opt.value : '0'; }
});
updateUI();
};
select.addEventListener('change', applyState);
applyState();
});
// Init (UI + subtotals)
updateUI();
// === SUBMIT ===
form.addEventListener('submit', async function (ev) {
ev.preventDefault();
ev.stopPropagation();
if (typeof ev.stopImmediatePropagation === 'function') ev.stopImmediatePropagation();
if (!form.checkValidity()) { form.reportValidity(); return; }
const honeypot = document.getElementById('company');
if (honeypot && honeypot.value && honeypot.value.trim() !== '') { alert('Bot detectie: formulier niet verzonden.'); return; }
const totals = computeTotals();
hiddenTotalEl.value = totals.once; // compatibel
const firstName = (form.querySelector('[name="firstName"]')?.value || '').trim();
const lastName = (form.querySelector('[name="lastName"]')?.value || '').trim();
const email = (form.querySelector('[name="email"]')?.value || '').trim();
const phone = (form.querySelector('[name="phone"]')?.value || '').trim();
const descFree = (form.querySelector('[name="description"]')?.value || '').trim();
const emailOk = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
if (!emailOk) {
const emailEl = form.querySelector('[name="email"]');
if (emailEl) { emailEl.setCustomValidity('Vul een geldig e-mailadres in.'); form.reportValidity(); emailEl.setCustomValidity(''); }
return;
}
// Beschrijving multiline
const lines = [];
lines.push('Ingediend via offerteformulier');
lines.push('Voornaam: ' + firstName);
lines.push('Achternaam: ' + lastName);
lines.push('E-mail: ' + email);
lines.push('Telefoon: ' + phone);
if (descFree) lines.push('Aanvullende info: ' + descFree);
document.querySelectorAll('.product-wrap').forEach(wrap => {
const idx = wrap.getAttribute('data-wrap-idx');
const cb = wrap.querySelector('#prod-' + idx);
if (!cb || !cb.checked) return;
const titleLabel = wrap.querySelector('label[for="prod-' + idx + '"]');
const title = titleLabel ? titleLabel.textContent.trim() : ('Onderdeel #' + idx);
lines.push('');
lines.push('[' + title + ']');
// basis (eenmalig)
const base = toNumberSafe(cb.dataset.base || 0);
if (base) lines.push(' Basis: €' + base.toLocaleString('nl-NL'));
// gekozen item: toon label + service
const selectEl = document.getElementById('item-' + idx);
let serviceForThis = 0;
if (selectEl && selectEl.options && selectEl.selectedIndex >= 0) {
const opt = selectEl.options[selectEl.selectedIndex];
const txt = (opt && opt.textContent ? opt.textContent : '').trim();
if (txt) lines.push(' Keuze: ' + txt);
serviceForThis = toNumberSafe(opt?.dataset?.service || 0);
}
// optionals (eenmalig; inbegrepen overslaan)
wrap.querySelectorAll('.optional-item').forEach(optEl => {
if (!optEl.checked) return;
const lbl = wrap.querySelector('label[for="' + optEl.id + '"]');
const optTxt = lbl ? lbl.textContent.replace(/\s+/g,' ').trim() : 'Optie';
const sel = (selectEl && selectEl.options && selectEl.selectedIndex >= 0) ? selectEl.options[selectEl.selectedIndex] : null;
const selectedCode = sel && sel.dataset ? sel.dataset.code : '';
let requiredFor = [];
try { requiredFor = JSON.parse(optEl.dataset.requiredFor || '[]'); } catch(e) { requiredFor = []; }
const isIncluded = requiredFor.indexOf(selectedCode) !== -1;
const priceShown = toNumberSafe(optEl.value || 0);
lines.push(' Optioneel: ' + optTxt + (isIncluded ? ' (inbegrepen)' : (priceShown ? ' (€' + priceShown.toLocaleString('nl-NL') + ')' : '')));
});
// quantities (eenmalig)
wrap.querySelectorAll('.qty-item').forEach(q => {
const qty = toQtySafe(q.value);
if (!qty) return;
const qLab = wrap.querySelector('label[for="' + q.id + '"]');
const qTxt = qLab ? qLab.textContent.replace(/\s+/g,' ').trim() : 'Aantal-item';
const unit = toNumberSafe(q.dataset.unit || 0);
lines.push(' ' + qTxt + ': ' + qty + ' × €' + unit.toLocaleString('nl-NL') + ' = €' + (qty * unit).toLocaleString('nl-NL'));
});
// per-onderdeel subtotals
const pair = computeProductSubtotalPair(idx);
lines.push(' Kosten eenmalig: €' + pair.once.toLocaleString('nl-NL'));
lines.push(' Kosten per maand: €' + pair.service.toLocaleString('nl-NL'));
});
lines.push('');
lines.push('Totaal eenmalig: €' + totals.once.toLocaleString('nl-NL'));
lines.push('Totaal per maand: €' + totals.service.toLocaleString('nl-NL'));
const payload = {
salutationName: '',
firstName,
lastName,
middleName: '',
emailAddress: email,
phoneNumber: phone,
description: lines.join('\n')
};
try {
const res = await fetch(ESPO_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(payload)
});
const text = await res.text().catch(() => '');
if (!res.ok) {
if (res.status === 409 || res.status === 422 || /duplicate/i.test(text)) {
alert('We hebben dit e-mailadres al in het systeem. We koppelen je inzending aan het bestaande contact en nemen contact op.');
return;
}
throw new Error(`LeadCapture HTTP ${res.status} ${res.statusText}${text}`);
}
alert('Bedankt! Je aanvraag is ontvangen. We nemen zo spoedig mogelijk contact op.');
form.reset();
updateUI();
} catch (e) {
console.error('[LeadCapture] Fout:', e);
alert('Er ging iets mis bij het versturen. Probeer het later opnieuw of neem contact op.');
}
});
})();
</script>