1/* DE KLANTLINK (Gerd 2026-09-22 16:1x): "ik wil een bezwaar kunnen maken en dan met een link naar een klant kunnen appen via 2 * WhatsApp; de klant krijgt de keuze voor zelf printen en op de post, of via de postservice, betaalt via Mollie, en Postbode.nu 3 * doet zijn ding." 4 * 5 * DE MAKER (bezwaar-maken.html, eindpagina): "Stuur naar de klant". De browser versleutelt de brief (AES-GCM 256, WebCrypto) 6 * en zet alleen het versleutelde blok op de server (api/klantlink.php). De sleutel staat in de link achter het hekje; die gaat 7 * mee in het WhatsApp-bericht en nooit naar onze server. Naam en adres van de klant zijn daardoor op de server niet te lezen. 8 * 9 * DE KLANT (klant.html): leest zijn bezwaar, tekent met zijn vinger, en kiest: 10 * - zelf printen en posten: de PDF met zijn handtekening; 11 * - de verzendservice: hetzelfde blok als op de eindpagina (assets/verzend-ui.js): e-mailadres, Mollie, daarna Postbode.nu. 12 * De handtekening van de maker gaat niet mee: tekenen doet degene wiens naam onder de brief staat. 13 */ 14(function (root) { 15 'use strict'; 16 17 function doc() { return typeof document !== 'undefined' ? document : null; } 18 function el(tag, a, kids) { 19 var e = doc().createElement(tag); 20 Object.keys(a || {}).forEach(function (k) { 21 if (k === 'text') e.textContent = a[k]; 22 else if (k === 'html') e.innerHTML = a[k]; 23 else if (k === 'onclick') e.addEventListener('click', a[k]); 24 else e.setAttribute(k, a[k]); 25 }); 26 (kids || []).forEach(function (c) { if (c) e.appendChild(c); }); 27 return e; 28 } 29 function apiBasis() { return (typeof root.rzIsNative === 'function' && root.rzIsNative() && root.BZT_API) ? root.BZT_API : ''; } 30 function siteBasis() { 31 var o = (root.location && root.location.origin) || ''; 32 return /^https?:\/\//.test(o) && !/localhost|127\.0\.0\.1/.test(o) ? o : (root.BZT_SITE || 'https://bpmbezwaartje.nl'); 33 } 34 function eindpunt() { return apiBasis() + '/api/klantlink.php'; } 35 36 /* ---------------------------------------------------------------- versleutelen */ 37 38 function b64(buf) { var s = '', a = new Uint8Array(buf); for (var i = 0; i < a.length; i++) s += String.fromCharCode(a[i]); return btoa(s); } 39 function unb64(s) { var b = atob(s), a = new Uint8Array(b.length); for (var i = 0; i < b.length; i++) a[i] = b.charCodeAt(i); return a; } 40 function b64url(buf) { return b64(buf).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } 41 function unb64url(s) { s = String(s).replace(/-/g, '+').replace(/_/g, '/'); while (s.length % 4) s += '='; return unb64(s); } 42 43 function versleutel(obj) { 44 var c = root.crypto.subtle; 45 var iv = root.crypto.getRandomValues(new Uint8Array(12)); 46 var sleutel; 47 return c.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']).then(function (k) { 48 sleutel = k; 49 return c.encrypt({ name: 'AES-GCM', iv: iv }, k, new TextEncoder().encode(JSON.stringify(obj))); 50 }).then(function (ct) { 51 var alles = new Uint8Array(12 + ct.byteLength); 52 alles.set(iv, 0); alles.set(new Uint8Array(ct), 12); 53 return c.exportKey('raw', sleutel).then(function (raw) { return { data: b64(alles), sleutel: b64url(raw) }; }); 54 }); 55 } 56 function ontsleutel(data, sleutelUrl) { 57 var c = root.crypto.subtle; 58 var alles = unb64(data); 59 return c.importKey('raw', unb64url(sleutelUrl), { name: 'AES-GCM' }, false, ['decrypt']).then(function (k) { 60 return c.decrypt({ name: 'AES-GCM', iv: alles.slice(0, 12) }, k, alles.slice(12)); 61 }).then(function (pt) { return JSON.parse(new TextDecoder().decode(pt)); }); 62 } 63 64 /* ---------------------------------------------------------------- de maker */ 65 66 /* opties.pakket(): { tekst, opties (voor bztMakeLetterPDF, zonder handtekening), bestandsnaam, deadlineIso, magGewoon, naam } */ 67 function maker(doosId, opties) { 68 var d = doc(); 69 var doos = d && d.getElementById(doosId); 70 if (!doos || d.getElementById(doosId + '-kl')) return false; 71 var stand = el('p', { class: 'sr-hulp sr-tel-stand', 'aria-live': 'polite' }); 72 var paneel = el('div', { class: 'sr-tel-paneel', hidden: 'hidden' }); 73 var knop = el('button', { type: 'button', class: 'sr-knop sr-knop-2 sr-klein', id: doosId + '-klknop', text: 'Stuur naar de klant (WhatsApp)' }); 74 doos.appendChild(el('div', { class: 'sr-tel', id: doosId + '-kl' }, [ 75 el('p', { class: 'sr-hulp', text: 'Maakt u dit bezwaar voor iemand anders? Stuur het hem of haar als link. Diegene leest het op de telefoon, tekent, en kiest zelf: printen en posten, of laten versturen via de postservice (betalen met iDEAL).' }), 76 knop, paneel, stand])); 77 78 knop.addEventListener('click', function () { 79 var pakket = opties && opties.pakket ? opties.pakket() : null; 80 if (!pakket || !pakket.tekst) { stand.textContent = 'Er is nog geen brief om te versturen.'; return; } 81 if (!(root.crypto && root.crypto.subtle)) { stand.textContent = 'Deze browser kan de link niet veilig maken. Gebruik een recente browser.'; return; } 82 knop.disabled = true; stand.textContent = 'De link wordt gemaakt...'; 83 versleutel(pakket).then(function (v) { 84 return fetch(eindpunt(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ actie: 'zet', data: v.data }), cache: 'no-store' }) 85 .then(function (r) { return r.json(); }) 86 .then(function (j) { return { j: j, sleutel: v.sleutel }; }); 87 }).then(function (u) { 88 knop.disabled = false;
89 if (!u.j || !u.j.ok || !/^[0-9a-f]{32}$/.test(u.j.id || '')) { stand.textContent = 'Dat lukte nu niet. Probeer het zo nog eens.'; return; } 90 var link = (opties.linkBasis || siteBasis()) + '/klant.html#id=' + u.j.id + '&k=' + u.sleutel; 91 while (paneel.firstChild) paneel.removeChild(paneel.firstChild); 92 var bericht = 'Uw bezwaar staat klaar. Lees het, zet uw handtekening en kies hoe het verstuurd wordt: ' + link; 93 var veld = el('input', { type: 'text', class: 'sr-tel-link', readonly: 'readonly', value: link, 'aria-label': 'Link voor de klant' }); 94 var kopie = el('button', { type: 'button', class: 'sr-knop sr-knop-2 sr-klein', text: 'Kopieer de link', onclick: function () { 95 veld.select(); 96 var gelukt = function () { kopie.textContent = 'Gekopieerd'; }; 97 if (root.navigator && root.navigator.clipboard) root.navigator.clipboard.writeText(link).then(gelukt, function () { try { d.execCommand('copy'); gelukt(); } catch (e) { } }); 98 else { try { d.execCommand('copy'); gelukt(); } catch (e) { } } 99 } }); 100 paneel.appendChild(el('div', { class: 'sr-knoppen sr-hdt-knoppen' }, [ 101 el('a', { class: 'sr-knop sr-klein', id: doosId + '-klwa', target: '_blank', rel: 'noopener', href: 'https://wa.me/?text=' + encodeURIComponent(bericht), text: 'Open WhatsApp met de link' }), 102 kopie])); 103 paneel.appendChild(veld); 104 paneel.appendChild(el('p', { class: 'sr-hulp', text: 'De link werkt 14 dagen. Wie de link heeft, kan het bezwaar lezen: stuur hem alleen naar de klant zelf.' })); 105 paneel.hidden = false; 106 stand.textContent = ''; 107 }, function () { knop.disabled = false; stand.textContent = 'Dat lukte nu niet. Probeer het zo nog eens.'; }); 108 }); 109 return true; 110 } 111 112 /* ---------------------------------------------------------------- de klant */ 113 114 function klant(doosId) { 115 var d = doc(); 116 var doos = d && d.getElementById(doosId); 117 if (!doos) return false; 118 var h = (root.location && root.location.hash) || ''; 119 var id = (/(?:^|[#&])id=([0-9a-f]{32})/.exec(h) || [])[1] || ''; 120 var k = (/(?:^|[#&])k=([A-Za-z0-9_-]{43})/.exec(h) || [])[1] || ''; 121 /* Het kenmerk en de sleutel uit de adresbalk (bewaren of doorsturen van de pagina neemt ze niet mee). De pagina onthoudt ze 122 in dit tabblad, zodat herladen blijft werken. */ 123 try { 124 if (id && k) { sessionStorage.setItem('bzt_kl', id + '|' + k); if (root.history && root.history.replaceState) root.history.replaceState(null, '', root.location.pathname); } 125 else { var o = (sessionStorage.getItem('bzt_kl') || '').split('|'); if (o.length === 2) { id = o[0]; k = o[1]; } } 126 } catch (e) { } 127 var melding = function (t, klasse) { while (doos.firstChild) doos.removeChild(doos.firstChild); doos.appendChild(el('p', { class: 'sr-melding ' + (klasse || 'sr-let'), text: t })); }; 128 if (!id || !k) { melding('Deze link is niet compleet. Vraag degene die uw bezwaar maakte om de link opnieuw te sturen.'); return false; } 129 if (!(root.crypto && root.crypto.subtle)) { melding('Deze browser kan uw bezwaar niet veilig openen. Gebruik een recente browser.'); return false; } 130 melding('Uw bezwaar wordt geopend...', 'sr-ok'); 131 132 fetch(eindpunt() + '?actie=haal&id=' + id, { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (j) { 133 if (!j || !j.ok || !j.data) { melding('Deze link is verlopen (een link werkt 14 dagen). Vraag degene die uw bezwaar maakte om een nieuwe.'); return; } 134 return ontsleutel(j.data, k).then(function (p) { toon(p); }, function () { melding('Deze link klopt niet (de sleutel past niet). Vraag om de link opnieuw.'); }); 135 }, function () { melding('Geen verbinding. Probeer het zo nog eens.'); }); 136 137 function pdf(p) { 138 var o = Object.assign({}, p.opties || {}); 139 var hdt = root.bztHandtekening && root.bztHandtekening.waarde(); 140 if (hdt) { 141 o.handtekening = hdt; 142 var m = (root.bztHandtekening.maat && root.bztHandtekening.maat()) || {}; 143 if (m.breedte && m.hoogte) { o.handtekeningBreedte = m.breedte; o.handtekeningHoogte = m.hoogte; } 144 } 145 return root.bztMakeLetterPDF(p.tekst, o); 146 } 147 148 function toon(p) { 149 while (doos.firstChild) doos.removeChild(doos.firstChild); 150 doos.appendChild(el('h2', { text: '1. Lees uw bezwaar' })); 151 doos.appendChild(el('p', { class: 'sr-hulp', text: 'Klopt er iets niet, bel of app dan eerst degene die het voor u maakte. Teken pas als alles klopt.' })); 152 doos.appendChild(el('pre', { class: 'sr-kl-tekst', id: doosId + '-tekst', text: p.tekst })); 153 doos.appendChild(el('h2', { text: '2. Zet uw handtekening' })); 154 var vak = el('div', { id: doosId + '-hdt', class: 'sr-hdt' }); 155 doos.appendChild(vak); 156 if (root.bztHandtekening) root.bztHandtekening.render(doosId + '-hdt', { el: el }); 157 doos.appendChild(el('h2', { text: '3. Versturen' })); 158 var err = el('p', { class: 'sr-hulp', 'aria-live': 'polite' }); 159 doos.appendChild(el('div', { class: 'sr-knoppen' }, [ 160 el('button', { type: 'button', class: 'sr-knop sr-knop-2', id: doosId + '-pdf', text: 'Download de PDF (zelf printen en posten)', onclick: function () { 161 try { 162 var docu = pdf(p); 163 var blob = docu.output('blob'); 164 var a = el('a', { href: URL.createObjectURL(blob), download: p.bestandsnaam || 'bezwaar.pdf' }); 165 d.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000); 166 err.textContent = root.bztHandtekening && root.bztHandtekening.waarde() ? '' : 'Let op: u hebt nog niet getekend. Teken dan op het geprinte stuk.'; 167 } catch (e) { err.textContent = 'De PDF kon niet worden gemaakt. Probeer het nog eens.'; } 168 } })])); 169 doos.appendChild(err); 170 var vz = el('div', { id: doosId + '-verzend', class: 'sr-verzend' }); 171 doos.appendChild(vz); 172 if (typeof root.bztVerzendBlok === 'function') { 173 root.bztVerzendBlok({ 174 container: doosId + '-verzend', 175 getPdfB64: function () { try { return pdf(p).output('datauristring').split(',')[1] || null; } catch (e) { return null; } },
176 getDeadlineIso: function () { return p.deadlineIso || ''; }, 177 handtekeningOntbreekt: function () { return !(root.bztHandtekening && root.bztHandtekening.waarde()); }, 178 magGewoon: !!p.magGewoon, 179 emailVoorstel: function () { return ''; } 180 }); 181 } 182 } 183 return true; 184 } 185 186 var api = { maker: maker, klant: klant, _versleutel: versleutel, _ontsleutel: ontsleutel }; 187 if (typeof module !== 'undefined' && module.exports) module.exports = api; 188 else root.bztKlantlink = api; 189})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.