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:
co-authored by
Claude Opus 5.5
parent
81fc27fa86
commit
366a3f9d17
@@ -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();
|
||||
});
|
||||
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user