v0.0.5: gedeelde mailboxen (info@, facturen@, nieuwsbrief@, persoonlijk) met eigen IMAP/SMTP, afzender, rol en toegang per gebruiker; inbox per mailbox en versturen vanuit een gekozen mailbox; routines (jobs.py: types met @jobs.register, interval, instellingen, logboek, slot tegen dubbel draaien; app.py scheduler/tick) voor e-mail ophalen, nieuwsbrieven versturen en opschonen; meldingen (bel, bureaubladmelding); nieuwsbrieven met segmenten, testmail, inplannen en versturen per batch over één verbinding, afmeldlink met List-Unsubscribe en bounceverwerking; oude mailinstellingen worden de eerste mailbox

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-16 18:25:44 +02:00
co-authored by Claude Opus 5.5
parent 8f6e1ab443
commit cad838f1f4
24 changed files with 1710 additions and 185 deletions
+32
View File
@@ -0,0 +1,32 @@
// Bel in de bovenbalk: elke minuut het aantal ongelezen meldingen ophalen (/notifications/poll). Met toestemming
// van de browser verschijnen nieuwe meldingen ook als bureaubladmelding (zolang de CRM ergens open staat).
(() => {
const bell = document.querySelector('.bell[data-poll]');
if (!bell) return;
const count = bell.querySelector('.bell-count');
const desktop = bell.querySelector('.bell-desktop');
let last = Number(bell.dataset.last) || 0;
const canNotify = 'Notification' in window;
if (canNotify && Notification.permission === 'default') {
desktop.hidden = false;
desktop.addEventListener('click', () => Notification.requestPermission().then(() => { desktop.hidden = true; }));
}
async function poll() {
try {
const r = await fetch(`${bell.dataset.poll}?after=${last}`, { headers: { Accept: 'application/json' } });
if (!r.ok) return;
const d = await r.json();
count.textContent = d.count;
count.hidden = !d.count;
d.items.slice().reverse().forEach((n) => {
last = Math.max(last, n.id);
if (canNotify && Notification.permission === 'granted') {
const note = new Notification(n.title, { body: n.body || '', tag: `crm-${n.id}` });
note.onclick = () => { window.focus(); location = n.url; };
}
});
} catch (e) { /* offline of uitgelogd: volgende keer opnieuw */ }
}
setInterval(() => { if (!document.hidden) poll(); }, 60000);
document.addEventListener('visibilitychange', () => { if (!document.hidden) poll(); });
})();
+19
View File
@@ -371,3 +371,22 @@ tr.link td { transition: background-color 0.2s; }
.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; }
/* Meldingen (bel in de bovenbalk) */
.bell .icon-btn { position: relative; }
.bell-count { position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px; background: var(--orange); color: var(--white); font-size: 0.7rem; font-weight: 700; line-height: 18px; text-align: center; }
.bell-menu { width: min(360px, calc(100vw - 2rem)); padding: 0; overflow: hidden; }
.bell-head, .bell-foot { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.6rem 1rem; }
.bell-head { border-bottom: 1px solid var(--line); }
.bell-foot { border-top: 1px solid var(--line); font-size: 0.875rem; }
.bell-menu .dropdown-item { display: flex; flex-direction: column; white-space: normal; padding: 0.6rem 1rem; border-bottom: 1px solid var(--line); }
.bell-menu .dropdown-item span { color: var(--ink-muted); font-size: 0.875rem; overflow-wrap: anywhere; }
.bell-menu .dropdown-item small { color: var(--ink-muted); font-size: 0.75rem; }
.bell-menu .dropdown-item.unread { background: var(--orange-tint-soft); }
/* Nieuwsbrieven */
.chip.campaign-gepland, .chip.campaign-bezig { background: var(--orange-tint); color: var(--orange-dark); }
.chip.campaign-verzonden { background: var(--orange); color: var(--white); }
.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); }