v0.0.6: massa-acties en CSV-export in de lijsten van contacten, relaties, producten en offertes

Rijen aanvinken (shift-klik voor een reeks, of alles op de pagina) en in de balk onderin een actie kiezen
(templates/_bulk.html, static/bulk.js). Contacten: status wijzigen, toewijzen, omzetten naar klant, exporteren,
verwijderen; relaties: type wijzigen, exporteren, verwijderen; producten: actief/inactief, exporteren, verwijderen
(in offertes gebruikt → inactief); offertes: status wijzigen (met de gevolgen voor het contact), exporteren,
verwijderen. Exporteer CSV neemt de filters van de lijst mee en levert alle velden; bij contacten ook labels,
toegewezen gebruiker, relatie, model, portaaltoegang en de extra formuliervelden als eigen kolom. Nieuwe
leadstatus Vervallen (telt als afgesloten).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-16 19:12:39 +02:00
co-authored by Claude Opus 5.5
parent 3cf53bc248
commit db3e26d258
9 changed files with 471 additions and 74 deletions
+57
View File
@@ -0,0 +1,57 @@
/* Massa-acties in lijsten (templates/_bulk.html): rijen aanvinken, shift-klik voor een reeks,
dan in de balk onderin een actie kiezen. Een klik op het vinkje opent de rij niet. */
(() => {
const form = document.getElementById('bulk');
if (!form) return;
const boxes = Array.from(document.querySelectorAll('input[name="ids"][form="bulk"]'));
const all = document.querySelector('[data-bulk-all]');
const count = form.querySelector('[data-bulk-count]');
const action = form.elements.action;
let last = null;
const update = () => {
const n = boxes.filter(b => b.checked).length;
count.textContent = n;
form.hidden = !n;
document.body.classList.toggle('bulk-open', n > 0);
if (all) {
all.checked = n > 0 && n === boxes.length;
all.indeterminate = n > 0 && n < boxes.length;
}
boxes.forEach(b => b.closest('tr')?.classList.toggle('selected', b.checked));
};
const showFields = () => form.querySelectorAll('[data-show]').forEach(el => {
const on = el.dataset.show === action.value;
el.hidden = !on;
el.disabled = !on;
});
boxes.forEach((box, i) => box.closest('td').addEventListener('click', e => {
e.stopPropagation();
if (e.target !== box) box.checked = !box.checked;
if (e.shiftKey && last !== null) {
boxes.slice(Math.min(last, i), Math.max(last, i) + 1).forEach(b => { b.checked = box.checked; });
}
last = i;
update();
}));
all?.closest('th').addEventListener('click', e => {
if (e.target !== all) all.checked = !all.checked;
boxes.forEach(b => { b.checked = all.checked; });
update();
});
form.querySelector('[data-bulk-clear]').addEventListener('click', () => {
boxes.forEach(b => { b.checked = false; });
update();
});
action.addEventListener('change', showFields);
form.addEventListener('submit', e => {
const ask = action.selectedOptions[0]?.dataset.confirm;
if (ask && !confirm(ask.replace('{n}', count.textContent))) return e.preventDefault();
// een export is een download: de pagina blijft staan, dus de keuze terugzetten
if (action.value === 'export') setTimeout(() => { action.value = ''; showFields(); }, 0);
});
addEventListener('pageshow', update);
showFields();
update();
})();
+21
View File
@@ -390,3 +390,24 @@ tr.link td { transition: background-color 0.2s; }
.chip.campaign-gestopt, .chip.campaign-gepauzeerd { background: var(--light); color: var(--ink-muted); }
.schedule { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
.progress-bar { background: var(--orange); }
/* ---------- massa-acties (templates/_bulk.html, static/bulk.js) ---------- */
.chip.vervallen { background: var(--light); color: var(--ink-muted); }
.table > :not(caption) > * > .bulk-col { width: 2.25rem; padding-right: 0; cursor: pointer; }
.bulk-col .form-check-input { margin: 0.2rem 0 0; cursor: pointer; }
.table > tbody > tr.selected > * { --bs-table-bg-state: var(--orange-tint); }
.bulk-bar { position: fixed; z-index: 1030; left: 50%; bottom: 1rem; transform: translateX(-50%); display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; max-width: calc(100% - 2rem); padding: 0.6rem 0.75rem 0.6rem 1.25rem; background: var(--white); border: 1px solid var(--orange-line); border-radius: var(--radius-pill); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14); }
.bulk-bar[hidden] { display: none; }
.bulk-bar .form-select { width: auto; max-width: 16rem; }
.bulk-count { white-space: nowrap; }
body.bulk-open main.content { padding-bottom: 7rem; }
@media (max-width: 767.98px) {
.bulk-bar { left: 0.75rem; right: 0.75rem; bottom: 0.75rem; transform: none; max-width: none; border-radius: var(--radius-md); padding: 0.75rem; }
.bulk-bar .form-select { flex: 1 1 100%; max-width: none; }
.bulk-bar .btn { flex: 1 1 auto; }
.bulk-count { flex-basis: 100%; }
.table-stack > tbody > tr { position: relative; }
.table-stack > tbody > tr > td.bulk-col { position: absolute; top: 0.8rem; right: 0; padding: 0; width: auto; }
.table-stack > tbody > tr > td.bulk-col::before { content: none; }
.table-stack > tbody > tr > td.bulk-col ~ td.stack-title { padding-right: 2.25rem; }
}