Init
This commit is contained in:
28
themes/agico-hugo/layouts/shortcodes/accordion.html
Normal file
28
themes/agico-hugo/layouts/shortcodes/accordion.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{{ $title:= .Get 0 }}
|
||||
{{ $class:= .Get "class" }}
|
||||
{{ $headerClass:= .Get "header-class" }}
|
||||
{{ $bodyClass:= .Get "body-class" }}
|
||||
|
||||
{{ range $i, $e:= .Params }}
|
||||
{{ if eq $i "title" }}{{ $title = $e }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
|
||||
<div class="accordion {{ $class }}">
|
||||
<button class="accordion-header {{ $headerClass }}" data-accordion>
|
||||
{{ $title | markdownify }}
|
||||
<svg
|
||||
class="accordion-icon"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 512 512"
|
||||
xmlspace="preserve">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M505.755,123.592c-8.341-8.341-21.824-8.341-30.165,0L256.005,343.176L36.421,123.592c-8.341-8.341-21.824-8.341-30.165,0 s-8.341,21.824,0,30.165l234.667,234.667c4.16,4.16,9.621,6.251,15.083,6.251c5.462,0,10.923-2.091,15.083-6.251l234.667-234.667 C514.096,145.416,514.096,131.933,505.755,123.592z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="accordion-content {{ $bodyClass }}">
|
||||
<p>{{ .Inner | markdownify }}</p>
|
||||
</div>
|
||||
</div>
|
||||
6
themes/agico-hugo/layouts/shortcodes/changelog.html
Normal file
6
themes/agico-hugo/layouts/shortcodes/changelog.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{{ $_hugo_config := `{ "version": 1 }` }}
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="badge d-inline-block rounded-1 {{ .Get 0 | lower}}">{{ .Get 0 | title }}</div>
|
||||
{{ .Inner | markdownify }}
|
||||
</div>
|
||||
702
themes/agico-hugo/layouts/shortcodes/quote.html
Normal file
702
themes/agico-hugo/layouts/shortcodes/quote.html
Normal file
@@ -0,0 +1,702 @@
|
||||
{{/* 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>
|
||||
Reference in New Issue
Block a user