v0.0.4: e-mailtemplates met velden ({voornaam}, {model}, {afzender}…), te kiezen bij nieuwe e-mail, antwoord en contact; inbox met nieuwe e-mail, doorsturen, CC en bijlagen, Verzonden, Archief, zoeken, als ongelezen markeren en alles gelezen; agenda per gebruiker met CalDAV (/dav/, app-wachtwoord in Mijn account); zoeken en Nieuw in de bovenbalk; e-mailinstellingen met voorinstellingen (OVHcloud, Gmail), beveiliging per server, kopie in Verzonden en verbindingstest; offerte in het klantportaal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-16 17:29:47 +02:00
co-authored by Claude Opus 5.5
parent 81fc27fa86
commit 366a3f9d17
26 changed files with 2095 additions and 271 deletions
+27
View File
@@ -0,0 +1,27 @@
// E-mail opstellen (templates/_compose.html): een template zet zijn tekst boven het bericht (of vervangt de tekst die
// de vorige template daar zette) en vult een leeg onderwerp. Velden als {voornaam} worden ingevuld met data-vars; wat
// overblijft, vult de server bij het versturen in.
document.querySelectorAll('form.compose').forEach((form) => {
const pick = form.querySelector('select[name="template"]');
if (!pick) return;
const vars = JSON.parse(form.dataset.vars || '{}');
const fill = (text) => text.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? vars[k] : m));
const subject = form.elements.subject, body = form.elements.body;
let added = '', addedSubject = '';
function apply() {
const o = pick.selectedOptions[0];
const on = o && o.value;
const rest = (added && body.value.startsWith(added) ? body.value.slice(added.length) : body.value).replace(/^\n+/, '');
const text = on ? fill(o.dataset.body || '') : '';
added = text && rest ? `${text}\n\n` : text;
body.value = added + rest;
if (!subject.value.trim() || subject.value === addedSubject) {
addedSubject = on ? fill(o.dataset.subject || '') : '';
subject.value = addedSubject;
}
body.focus();
body.setSelectionRange(text.length, text.length);
}
pick.addEventListener('change', apply);
if (pick.value) apply();
});
+58
View File
@@ -0,0 +1,58 @@
/* Zoeken in de bovenbalk: live resultaten (search_live) onder het veld; Enter opent de volledige zoekpagina.
* Pijltjes lopen door de resultaten, Escape sluit, "/" springt naar het zoekveld. */
(() => {
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
document.querySelectorAll('form.tsearch').forEach((form) => {
const input = form.querySelector('input[name=q]');
const box = form.querySelector('.tsearch-results');
let timer = null, ctrl = null, last = '';
const items = () => [...box.querySelectorAll('a')];
const close = () => { box.classList.remove('open'); input.setAttribute('aria-expanded', 'false'); };
const open = () => { if (box.innerHTML) { box.classList.add('open'); input.setAttribute('aria-expanded', 'true'); } };
const render = (data) => {
box.innerHTML = data.groups.length
? data.groups.map((g) => `<div class="ts-group">${esc(g.label)}</div>` + g.items.map((i) =>
`<a class="ts-item" role="option" href="${esc(i.url)}">${esc(i.title)}${i.sub ? `<small>${esc(i.sub)}</small>` : ''}</a>`).join('')).join('')
+ `<a class="ts-all" href="${esc(data.url)}">Alle resultaten voor “${esc(data.q)}”</a>`
: `<div class="ts-empty">Niets gevonden voor “${esc(data.q)}”.</div>`;
open();
};
input.addEventListener('input', () => {
clearTimeout(timer);
const v = input.value.trim();
if (v.length < 2) { box.innerHTML = ''; close(); return; }
timer = setTimeout(() => {
if (v === last && box.innerHTML) { open(); return; }
if (ctrl) ctrl.abort();
ctrl = new AbortController();
fetch(`${form.dataset.live}?q=${encodeURIComponent(v)}`, { signal: ctrl.signal, headers: { Accept: 'application/json' } })
.then((r) => (r.ok ? r.json() : null))
.then((d) => { if (d && input.value.trim() === v) { last = v; render(d); } })
.catch(() => {});
}, 180);
});
form.addEventListener('keydown', (e) => {
const list = items(), at = list.indexOf(document.activeElement);
if (e.key === 'ArrowDown' && list.length) { e.preventDefault(); open(); list[Math.min(at + 1, list.length - 1)].focus(); }
else if (e.key === 'ArrowUp' && list.length) { e.preventDefault(); (at <= 0 ? input : list[at - 1]).focus(); }
else if (e.key === 'Escape') { close(); input.focus(); }
});
input.addEventListener('focus', () => { if (input.value.trim().length >= 2) open(); });
document.addEventListener('click', (e) => { if (!form.contains(e.target)) close(); });
// mobiel: het veld krijgt de focus zodra het uitklapmenu opengaat
const toggle = form.closest('.dropdown')?.querySelector('[data-bs-toggle="dropdown"]');
if (toggle) toggle.addEventListener('shown.bs.dropdown', () => input.focus());
});
document.addEventListener('keydown', (e) => {
if (e.key !== '/' || e.ctrlKey || e.metaKey || e.altKey || e.target.closest('input, textarea, select, [contenteditable]')) return;
const input = [...document.querySelectorAll('form.tsearch input[name=q]')].find((i) => i.offsetParent);
if (input) { e.preventDefault(); input.focus(); input.select(); }
});
})();
+72 -2
View File
@@ -72,15 +72,41 @@ a:focus-visible, button:focus-visible, .btn:focus-visible { outline: 3px solid v
.active .count { background: var(--white); }
.topbar { position: sticky; top: 0; z-index: 1020; background: var(--white); border-bottom: 1px solid var(--line); padding: 0.6rem 1rem; display: flex; align-items: center; gap: 0.75rem; }
.topbar img { width: 120px; display: block; }
.topbar-logo img { width: 120px; display: block; }
.topbar .menu-btn { border: 1px solid var(--line); background: var(--white); border-radius: var(--radius-pill); padding: 0.45rem 0.9rem; display: inline-flex; align-items: center; gap: 0.5rem; font-weight: 600; color: var(--ink); }
.topbar .menu-btn svg { width: 20px; height: 20px; }
.topbar .count { margin-left: auto; }
.topbar-tools { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }
.icon-btn { width: 40px; height: 40px; border: 1px solid var(--line); background: var(--white); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: var(--ink); padding: 0; }
.icon-btn svg { width: 20px; height: 20px; }
.icon-btn:hover, .icon-btn[aria-expanded="true"] { background: var(--orange-tint-soft); border-color: var(--orange-line); }
.new-btn { --bs-btn-padding-x: 0.9rem; height: 40px; }
.new-btn svg { width: 18px; height: 18px; }
@media (max-width: 575.98px) { .new-btn { width: 40px; padding: 0; } }
.new-menu { --bs-dropdown-link-active-bg: var(--orange); --bs-dropdown-border-color: var(--line); --bs-dropdown-border-radius: var(--radius-md); min-width: 12rem; padding: 0.4rem; }
.new-menu .dropdown-item { border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-weight: 600; }
.new-menu .dropdown-item:hover { background: var(--orange-tint-soft); }
/* zoeken: live resultaten onder het veld (desktop) of in het uitklapmenu (mobiel) */
.tsearch { position: relative; }
.tsearch-icon { position: absolute; left: 0.8rem; top: 50%; width: 18px; height: 18px; transform: translateY(-50%); color: var(--ink-muted); pointer-events: none; }
.tsearch input { padding-left: 2.4rem; border-radius: var(--radius-pill); width: 340px; max-width: 100%; padding-top: 0.45rem; padding-bottom: 0.45rem; }
.tsearch-results { display: none; position: absolute; right: 0; top: calc(100% + 0.4rem); width: min(440px, 90vw); max-height: 70vh; overflow-y: auto; background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12); padding: 0.4rem; z-index: 1030; }
.tsearch-results.open { display: block; }
.ts-group { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-muted); padding: 0.5rem 0.6rem 0.2rem; }
.ts-item { display: block; padding: 0.4rem 0.6rem; border-radius: var(--radius-sm); color: var(--ink); line-height: 1.3; }
.ts-item small { display: block; color: var(--ink-muted); font-weight: 400; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts-item:hover, .ts-item:focus { background: var(--orange-tint-soft); text-decoration: none; outline: none; }
.ts-all { display: block; text-align: center; padding: 0.5rem; border-top: 1px solid var(--line); margin-top: 0.3rem; }
.ts-empty { color: var(--ink-muted); padding: 0.6rem; }
.tsearch-pop { width: calc(100vw - 2rem); padding: 0.75rem; border-radius: var(--radius-md); }
.tsearch-pop .tsearch input { width: 100%; }
.tsearch-pop .tsearch-results { position: static; width: auto; box-shadow: none; border: 0; padding: 0.4rem 0 0; max-height: 60vh; }
.app-main { flex: 1; min-width: 0; }
.content { padding: 1.25rem 1rem 2rem; max-width: 1400px; }
@media (min-width: 992px) {
.topbar { padding: 0.75rem 2.5rem; }
.app { display: flex; min-height: 100vh; }
.side { width: 240px; flex: none; position: sticky; top: 0; height: 100vh; background: var(--white); border-right: 1px solid var(--line); padding: 1.5rem 1rem; display: flex; flex-direction: column; }
.side-panel { display: flex; flex-direction: column; }
@@ -301,3 +327,47 @@ tr.link td { transition: background-color 0.2s; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; animation: none !important; }
}
/* ---------- agenda ---------- */
.u0 { --u: #b95913; } .u1 { --u: #3f6e9e; } .u2 { --u: #5b8a3a; } .u3 { --u: #8a4f9e; } .u4 { --u: #a33f4a; } .u5 { --u: #2f7f7b; }
.cal { width: 100%; table-layout: fixed; border-collapse: collapse; }
.cal th { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-muted); padding: 0 0.4rem 0.4rem; }
.cal td { border: 1px solid var(--line); vertical-align: top; height: 7.5rem; padding: 0.3rem; }
.cal td.out { background: var(--ultra-light); }
.cal td.out .cal-num { color: var(--control); }
.cal-num { display: inline-flex; align-items: center; justify-content: center; min-width: 1.8rem; height: 1.8rem; border-radius: 50%; color: var(--ink); font-weight: 600; font-size: 0.875rem; }
.cal-num:hover { background: var(--orange-tint-soft); text-decoration: none; }
.cal td.today .cal-num { background: var(--orange); color: var(--white); }
.cal-ev { display: block; font-size: 0.8rem; line-height: 1.25; padding: 0.15rem 0.4rem; margin-top: 0.15rem; border-radius: 0.35rem; color: var(--ink); background: color-mix(in srgb, var(--u, #b95913) 12%, white); border-left: 3px solid var(--u, #b95913); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-ev:hover { text-decoration: none; background: color-mix(in srgb, var(--u, #b95913) 22%, white); }
.cal-ev .t { font-weight: 400; color: var(--ink-muted); margin-right: 0.25rem; }
.cal-ev.task { background: var(--white); border: 1px dashed var(--control); border-left-width: 1px; font-weight: 400; }
.cal-ev.allday { background: var(--u, #b95913); color: var(--white); }
.cal-ld { display: flex; gap: 0.9rem; padding: 0.7rem 0; border-bottom: 1px solid var(--line); }
.cal-ld:last-child { border-bottom: 0; }
.cal-ldate { width: 3.2rem; flex: none; text-align: center; line-height: 1.1; }
.cal-ldate b { display: block; font-family: var(--font-heading); font-size: 1.4rem; font-weight: 600; }
.cal-ldate span { font-size: 0.75rem; color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.06em; }
.cal-ld.today .cal-ldate b { color: var(--orange); }
.cal-ld .cal-items { flex: 1; min-width: 0; }
.cal-ld .cal-ev { white-space: normal; font-size: 0.9rem; padding: 0.35rem 0.6rem; margin: 0 0 0.3rem; }
.cal-legend { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; font-size: 0.85rem; color: var(--ink-muted); margin-top: 0.75rem; }
.cal-legend span::before { content: ""; display: inline-block; width: 0.7rem; height: 0.7rem; border-radius: 3px; background: var(--u); margin-right: 0.35rem; vertical-align: -1px; }
.agenda-row { display: flex; gap: 0.75rem; padding: 0.55rem 0; border-bottom: 1px solid var(--line); align-items: baseline; }
.agenda-row:last-child { border-bottom: 0; }
.agenda-row .when { width: 7.5rem; flex: none; color: var(--ink-muted); font-size: 0.875rem; }
.dav-box code { word-break: break-all; }
.dav-pass { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1.15rem; letter-spacing: 0.04em; background: var(--orange-tint-soft); border: 1px solid var(--orange-line); border-radius: var(--radius-sm); padding: 0.5rem 0.8rem; display: inline-block; }
/* ---------- afdrukken: alleen de inhoud (offerte, bestek) ---------- */
@media print {
body { background: #fff; }
.side, .topbar, .flash, .no-print { display: none !important; }
.app { display: block; }
.content { padding: 0; max-width: none; }
}
/* E-mailtemplates: velden om in te voegen */
.mail-fields { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; }
.mail-fields li { display: flex; align-items: center; gap: 0.6rem; font-size: 0.9rem; }
.mail-fields .btn { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; flex: none; }