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
+10 -2
View File
@@ -1,12 +1,20 @@
{# E-mail opstellen (nieuw, beantwoorden, vanaf een contact). m = {to, cc, subject, body, contact_id, reply_to, next,
template}. Een template zet zijn tekst boven het bericht; velden als {voornaam} vult static/compose.js meteen in met
template, mailbox_id}; mailboxes = waarmee de gebruiker mag versturen (Van). Een template zet zijn tekst boven het bericht; velden als {voornaam} vult static/compose.js meteen in met
vars, wat overblijft vult mail_send bij het versturen in. #}
{% macro compose(m, templates, vars, id='m', datalist='', button='Versturen') %}
{% macro compose(m, templates, vars, mailboxes, id='m', datalist='', button='Versturen') %}
<form method="post" action="{{ url_for('mail_send') }}" enctype="multipart/form-data" class="compose" data-vars='{{ vars|tojson }}'>
{% if m.reply_to %}<input type="hidden" name="reply_to" value="{{ m.reply_to }}">{% endif %}
{% if m.contact_id %}<input type="hidden" name="contact_id" value="{{ m.contact_id }}">{% endif %}
{% if m.next %}<input type="hidden" name="next" value="{{ m.next }}">{% endif %}
<div class="row g-3">
{% if not mailboxes %}
<div class="col-12"><div class="flash error mb-0">Er is geen mailbox waarmee je mag versturen. Een beheerder stelt die in bij Instellingen › Mailboxen.</div></div>
{% elif mailboxes|length == 1 %}
<input type="hidden" name="mailbox_id" value="{{ mailboxes[0].id }}">
{% else %}
<div class="col-12"><label class="form-label" for="{{ id }}-from">Van</label>
<select class="form-select" id="{{ id }}-from" name="mailbox_id">{% for b in mailboxes %}<option value="{{ b.id }}" {{ 'selected' if m.mailbox_id == b.id }}>{{ b.name }}{% if b.smtp_from or b.smtp_user or b.imap_user %} · {{ b.smtp_from or b.smtp_user or b.imap_user }}{% endif %}</option>{% endfor %}</select></div>
{% endif %}
{% if templates %}
<div class="col-12"><label class="form-label" for="{{ id }}-template">Template</label>
<select class="form-select" id="{{ id }}-template" name="template" autocomplete="off">
+22 -2
View File
@@ -20,7 +20,8 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js" defer></script>
<script src="{{ url_for('static', filename='fx.js') }}" defer></script>
{% if g.user %}<script src="{{ url_for('static', filename='search.js') }}" defer></script>{% endif %}
{% if g.user %}<script src="{{ url_for('static', filename='search.js') }}" defer></script>
<script src="{{ url_for('static', filename='notify.js') }}" defer></script>{% endif %}
{% block styles %}{% endblock %}
</head>
<body>
@@ -46,12 +47,14 @@
<a href="{{ url_for('calendar_view') }}" class="{{ 'active' if ep.startswith('calendar') }}">Agenda</a>
<a href="{{ url_for('inbox') }}" class="{{ 'active' if ep.startswith('inbox') or ep in ('mail_new', 'mail_send') }}">Inbox {% if unread %}<span class="count">{{ unread }}</span>{% endif %}</a>
<a href="{{ url_for('mail_templates') }}" class="{{ 'active' if ep.startswith('mail_template') }}">E-mailtemplates</a>
<a href="{{ url_for('campaigns') }}" class="{{ 'active' if ep.startswith('campaign') or ep.startswith('segment') }}">Nieuwsbrieven</a>
<a href="{{ url_for('products') }}" class="{{ 'active' if ep.startswith('product') }}">Producten</a>
<a href="{{ url_for('quotes') }}" class="{{ 'active' if ep.startswith('quote') }}">Offertes</a>
<a href="{{ url_for('import_start') }}" class="{{ 'active' if ep.startswith('import') }}">Importeren</a>
{% if g.user.role == 'admin' %}
<a href="{{ url_for('users') }}" class="{{ 'active' if ep.startswith('user') }}">Gebruikers</a>
<a href="{{ url_for('settings') }}" class="{{ 'active' if ep == 'settings' }}">Instellingen</a>
<a href="{{ url_for('routines') }}" class="{{ 'active' if ep.startswith('routine') }}">Routines</a>
<a href="{{ url_for('settings') }}" class="{{ 'active' if ep == 'settings' or ep.startswith('mailbox') }}">Instellingen</a>
{% endif %}
</nav>
<a href="{{ url_for('account') }}" class="logout">{{ g.user.name }}</a>
@@ -82,6 +85,22 @@
<div class="dropdown-menu dropdown-menu-end tsearch-pop">{{ search_form('q-pop', sq) }}</div>
</div>
{% if unread %}<a href="{{ url_for('inbox') }}" class="count d-lg-none" aria-label="{{ unread }} ongelezen e-mail">{{ unread }}</a>{% endif %}
<div class="dropdown bell" data-poll="{{ url_for('notifications_poll') }}" data-last="{{ bell[0].id if bell else 0 }}">
<button class="icon-btn" type="button" data-bs-toggle="dropdown" aria-expanded="false" aria-label="Meldingen{% if bell_unread %}: {{ bell_unread }} ongelezen{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
<span class="bell-count" {{ 'hidden' if not bell_unread }}>{{ bell_unread }}</span>
</button>
<div class="dropdown-menu dropdown-menu-end bell-menu">
<div class="bell-head"><b>Meldingen</b>
{% if bell_unread %}<form method="post" action="{{ url_for('notifications_read_all') }}" class="inline"><button class="btn btn-link btn-sm p-0">Alles gelezen</button></form>{% endif %}</div>
{% for n in bell %}
<a class="dropdown-item {{ 'unread' if not n.read_at }}" href="{{ url_for('notification_open', nid=n.id) }}"><b>{{ n.title }}</b>{% if n.body %}<span>{{ n.body }}</span>{% endif %}<small>{{ n.created_at|dt }}</small></a>
{% else %}
<p class="empty px-3">Geen meldingen.</p>
{% endfor %}
<div class="bell-foot"><a href="{{ url_for('notifications') }}">Alle meldingen</a><button type="button" class="btn btn-link btn-sm p-0 bell-desktop" hidden>Bureaubladmeldingen aanzetten</button></div>
</div>
</div>
<div class="dropdown">
<button class="btn btn-primary btn-sm new-btn" type="button" data-bs-toggle="dropdown" aria-expanded="false" aria-label="Nieuw">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span class="d-none d-sm-inline">Nieuw</span>
@@ -96,6 +115,7 @@
<li><a class="dropdown-item" href="{{ url_for('product_edit') }}">Product</a></li>
<li><a class="dropdown-item" href="{{ url_for('calendar_edit') }}">Afspraak</a></li>
<li><a class="dropdown-item" href="{{ url_for('mail_new') }}">E-mail</a></li>
<li><form method="post" action="{{ url_for('campaign_new') }}"><button class="dropdown-item">Nieuwsbrief</button></form></li>
</ul>
</div>
</div>
+95
View File
@@ -0,0 +1,95 @@
{% extends 'base.html' %}
{% block title %}{{ c.name }}{% endblock %}
{% block content %}
{% set editable = c.status == 'concept' %}
<div class="head">
<div><a class="label" href="{{ url_for('campaigns') }}">← Nieuwsbrieven</a><h1>{{ c.name }}</h1>
<div class="muted"><span class="chip campaign-{{ c.status }}">{{ CSTATUS.get(c.status, c.status) }}</span>
{% if c.scheduled_at and not editable %} · vanaf {{ c.scheduled_at|dt }}{% endif %}{% if c.finished_at %} · klaar {{ c.finished_at|dt }}{% endif %}</div></div>
<div class="actions">
<form method="post" class="inline">
{% if c.status in ('gepland', 'bezig') %}<button class="btn btn-outline-primary" name="action" value="pause">Pauzeren</button>{% endif %}
{% if c.status == 'gepauzeerd' %}<button class="btn btn-primary" name="action" value="resume">Hervatten</button>{% endif %}
{% if c.status in ('gepland', 'bezig', 'gepauzeerd') %}<button class="btn btn-outline-secondary" name="action" value="stop" onclick="return confirm('Stoppen? Wie hem nog niet kreeg, krijgt hem niet meer.')">Stoppen</button>{% endif %}
<button class="btn btn-outline-secondary" name="action" value="copy">Kopie maken</button>
</form>
</div>
</div>
{% if not editable %}
<div class="card"><div class="card-body">
<h2>Voortgang</h2>
{% set pct = (100 * (stats.totaal - stats.wacht) / stats.totaal)|round|int if stats.totaal else 0 %}
<div class="progress mb-3" role="progressbar" aria-label="Voortgang" aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width: {{ pct }}%"></div></div>
<div class="d-flex flex-wrap gap-4">
{% for k, l in RSTATUS.items() %}<div><div class="muted">{{ l }}</div><b>{{ stats[k] }}</b></div>{% endfor %}
</div>
</div></div>
{% endif %}
<div class="row g-4">
<div class="col-lg-8">
<form method="post" class="card compose" data-vars='{}'><div class="card-body">
<fieldset {{ 'disabled' if not editable }}>
<div class="row g-3">
<div class="col-12"><label class="form-label" for="name">Naam (intern)</label><input class="form-control" id="name" name="name" value="{{ c.name }}" required></div>
<div class="col-md-6"><label class="form-label" for="mailbox_id">Van</label>
<select class="form-select" id="mailbox_id" name="mailbox_id">
{% if not mailboxes %}<option value="">Geen mailbox om mee te versturen</option>{% endif %}
{% for m in mailboxes %}<option value="{{ m.id }}" {{ 'selected' if c.mailbox_id == m.id }}>{{ m.name }}{% if m.smtp_from or m.smtp_user or m.imap_user %} · {{ m.smtp_from or m.smtp_user or m.imap_user }}{% endif %}</option>{% endfor %}
{% if mailbox and mailbox.id not in mailboxes|map(attribute='id')|list %}<option value="{{ mailbox.id }}" selected>{{ mailbox.name }}</option>{% endif %}
</select></div>
<div class="col-md-6"><label class="form-label" for="segment_id">Aan (segment)</label>
<select class="form-select" id="segment_id" name="segment_id" aria-describedby="seg-hint">
<option value="">Kies een segment…</option>
{% for s in segments %}<option value="{{ s.id }}" {{ 'selected' if c.segment_id == s.id }}>{{ s.name }}</option>{% endfor %}
</select>
<p class="hint" id="seg-hint">{% if c.segment_id and editable %}Nu {{ seg_count }} ontvanger(s). {% endif %}<a href="{{ url_for('segments') }}">Segmenten beheren</a></p></div>
{% if templates and editable %}
<div class="col-12"><label class="form-label" for="template">Template</label>
<select class="form-select" id="template" name="template" autocomplete="off">
<option value="">Geen template</option>
{% for t in templates %}<option value="{{ t.id }}" data-subject="{{ t.subject }}" data-body="{{ t.body }}">{{ t.name }}</option>{% endfor %}
</select></div>
{% endif %}
<div class="col-12"><label class="form-label" for="subject">Onderwerp</label><input class="form-control" id="subject" name="subject" value="{{ c.subject or '' }}"></div>
<div class="col-12"><label class="form-label" for="body">Tekst</label><textarea class="form-control" id="body" name="body" rows="16">
{{ c.body or '' }}</textarea>
<p class="hint">Velden worden per ontvanger ingevuld: {% for k, l in FIELDS %}<code title="{{ l }}">{{ '{' ~ k ~ '}' }}</code> {% endfor %}. Zonder {afmeldlink} komt de afmeldlink onderaan.</p></div>
</div>
</fieldset>
<div class="actions">
{% if editable %}
<button class="btn btn-primary" name="action" value="save">Opslaan</button>
<button class="btn btn-outline-primary" name="action" value="test">Testmail naar mij</button>
{% endif %}
</div>
{% if editable %}
<div class="schedule">
<h2>Versturen</h2>
<p class="hint">Bij inplannen worden de ontvangers uit het segment vastgelegd. Daarna verstuurt de routine "Nieuwsbrieven versturen" ze per batch.</p>
<div class="row g-3 align-items-end">
<div class="col-md-6"><label class="form-label" for="scheduled_at">Vanaf (leeg = zo snel mogelijk)</label><input class="form-control" id="scheduled_at" type="datetime-local" name="scheduled_at"></div>
<div class="col-md-6"><button class="btn btn-primary" name="action" value="schedule" onclick="return confirm('Nieuwsbrief inplannen? Daarna kun je de tekst niet meer wijzigen.')">Opslaan en inplannen</button></div>
</div>
</div>
{% endif %}
</div></form>
{% if editable %}
<form method="post" onsubmit="return confirm('Nieuwsbrief verwijderen?')"><button class="btn btn-outline-secondary btn-sm" name="action" value="delete">Nieuwsbrief verwijderen</button></form>
{% endif %}
</div>
{% if recipients %}
<div class="col-lg-4">
<div class="card"><div class="card-body">
<h2>Ontvangers</h2>
<ul class="timeline">{% for r in recipients %}<li>{% if r.contact_id %}<a href="{{ url_for('contact_view', cid=r.contact_id) }}">{{ r.name or r.email }}</a>{% else %}{{ r.email }}{% endif %}
<span class="chip {{ 'verloren' if r.status in ('mislukt', 'overgeslagen') else '' }}">{{ RSTATUS.get(r.status, r.status) }}</span><br>
<span class="muted">{{ r.email }}{% if r.sent_at %} · {{ r.sent_at|dt }}{% endif %}{% if r.error %} · {{ r.error }}{% endif %}</span></li>{% endfor %}</ul>
{% if stats.totaal > recipients|length %}<p class="hint">De eerste {{ recipients|length }} van {{ stats.totaal }}.</p>{% endif %}
</div></div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}{% if editable %}<script src="{{ url_for('static', filename='compose.js') }}" defer></script>{% endif %}{% endblock %}
+33
View File
@@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% block title %}Nieuwsbrieven{% endblock %}
{% block content %}
<div class="head">
<div><div class="kicker">E-mail</div><h1>Nieuwsbrieven</h1></div>
<div class="actions">
<a class="btn btn-outline-primary" href="{{ url_for('segments') }}">Segmenten</a>
<a class="btn btn-outline-primary" href="{{ url_for('mail_templates') }}">Templates</a>
<form method="post" action="{{ url_for('campaign_new') }}" class="inline"><button class="btn btn-primary">Nieuwe nieuwsbrief</button></form>
</div>
</div>
<div class="card"><div class="card-body">
{% if rows %}
<div class="table-responsive"><table class="table table-hover table-stack">
<thead><tr><th>Naam</th><th>Segment</th><th>Van</th><th>Status</th><th class="num">Verzonden</th><th>Gepland</th></tr></thead>
<tbody>
{% for c in rows %}
<tr class="link" onclick="location='{{ url_for('campaign_edit', cid=c.id) }}'">
<td class="stack-title"><a href="{{ url_for('campaign_edit', cid=c.id) }}">{{ c.name }}</a></td>
<td data-label="Segment">{{ c.segment_name or '—' }}</td>
<td data-label="Van">{{ c.mailbox_name or '—' }}</td>
<td data-label="Status"><span class="chip campaign-{{ c.status }}">{{ CSTATUS.get(c.status, c.status) }}</span></td>
<td data-label="Verzonden" class="num">{% if c.stats.totaal %}{{ c.stats.verzonden }} / {{ c.stats.totaal }}{% else %}—{% endif %}</td>
<td data-label="Gepland" class="muted">{{ c.scheduled_at|dt if c.scheduled_at else '' }}</td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% else %}<p class="empty">Nog geen nieuwsbrieven. Maak eerst een segment (wie krijgt hem) en eventueel een template.</p>{% endif %}
</div></div>
<p class="muted">Nieuwsbrieven gaan per batch de deur uit via de routine "Nieuwsbrieven versturen". Elke mail krijgt een afmeldlink;
wie zich afmeldt of onbestelbaar blijkt, valt voortaan buiten elk segment.</p>
{% endblock %}
+7 -1
View File
@@ -122,11 +122,17 @@
</form>
</div></div>
{% if c.unsubscribed_at or c.bounced_at %}
<div class="flash error d-flex justify-content-between align-items-center gap-3 flex-wrap">
<span>{% if c.bounced_at %}Het e-mailadres is onbestelbaar (sinds {{ c.bounced_at|dt }}){% else %}Afgemeld voor nieuwsbrieven op {{ c.unsubscribed_at|dt }}{% endif %}; dit contact krijgt geen nieuwsbrieven.</span>
<form method="post" action="{{ url_for('contact_mailstatus', cid=c.id) }}" class="inline" onsubmit="return confirm('Dit contact weer nieuwsbrieven sturen?')"><button class="btn btn-outline-secondary btn-sm">Weer aanmelden</button></form>
</div>
{% endif %}
{% if c.email %}
<div class="card"><div class="card-body">
<details>
<summary>E-mail sturen</summary>
{{ compose({'to': c.email, 'contact_id': c.id}, mail_templates, mail_vars) }}
{{ compose({'to': c.email, 'contact_id': c.id}, mail_templates, mail_vars, mailboxes) }}
</details>
</div></div>
{% endif %}
+3 -3
View File
@@ -5,12 +5,12 @@
{% set incoming = e.direction == 'in' %}
<div class="head">
<div>
<a class="label" href="{{ url_for('inbox', f=None if incoming and not e.archived else ('archived' if incoming else 'sent')) }}">← {{ 'Verzonden' if not incoming else ('Archief' if e.archived else 'Inbox') }}</a>
<a class="label" href="{{ url_for('inbox', mb=e.mailbox_id, f=None if incoming and not e.archived else ('archived' if incoming else 'sent')) }}">← {{ 'Verzonden' if not incoming else ('Archief' if e.archived else 'Inbox') }}</a>
<h1>{{ e.subject }}</h1>
<div class="muted" style="overflow-wrap:anywhere">
{% if incoming %}Van {{ e.from_name }} &lt;{{ e.from_addr }}&gt;{% else %}Aan {{ e.to_addr }}{% endif %}
{% if e.cc %}· CC {{ e.cc }}{% endif %}
· {{ e.date|dt }}
· {{ e.date|dt }}{% if e.mailbox_name %} · {{ e.mailbox_name }}{% endif %}
</div>
{% if e.attachments %}<div class="muted" style="overflow-wrap:anywhere">Bijlagen: {{ e.attachments }}</div>{% endif %}
</div>
@@ -34,7 +34,7 @@
<div class="card"><div class="card-body">
<h2>Beantwoorden</h2>
{{ compose({'to': e.from_addr, 'subject': e.subject if e.subject.lower().startswith('re:') else 'Re: ' ~ e.subject,
'reply_to': e.id, 'contact_id': contact.id if contact else None}, templates, vars) }}
'reply_to': e.id, 'contact_id': contact.id if contact else None, 'mailbox_id': e.mailbox_id}, templates, vars, mailboxes) }}
</div></div>
{% endif %}
{% endblock %}
+24 -10
View File
@@ -3,41 +3,55 @@
{% block content %}
{% set sent = f == 'sent' %}
<div class="head">
<div><div class="kicker">E-mail</div><h1>{{ {'sent': 'Verzonden', 'archived': 'Archief'}.get(f, 'Inbox') }}</h1>{% if last_sync %}<div class="muted">Laatst opgehaald: {{ last_sync|dt }}</div>{% endif %}</div>
<div><div class="kicker">E-mail{% if current %} · {{ current.name }}{% endif %}</div><h1>{{ {'sent': 'Verzonden', 'archived': 'Archief'}.get(f, 'Inbox') }}</h1>
{% if current and current.last_sync %}<div class="muted">Laatst opgehaald: {{ current.last_sync|dt }}</div>{% endif %}</div>
<div class="actions">
<form class="search" method="get">
<input type="hidden" name="f" value="{{ f }}">
{% if mb %}<input type="hidden" name="mb" value="{{ mb }}">{% endif %}
<input class="form-control" type="search" name="q" value="{{ term }}" placeholder="Zoek op afzender, onderwerp, tekst…" aria-label="Zoeken in e-mail">
</form>
<a class="btn btn-outline-primary" href="{{ url_for('mail_templates') }}">Templates</a>
<form method="post" action="{{ url_for('inbox_sync') }}" class="inline"><button class="btn btn-outline-primary">Nieuwe e-mail ophalen</button></form>
<a class="btn btn-primary" href="{{ url_for('mail_new') }}">Nieuwe e-mail</a>
<form method="post" action="{{ url_for('inbox_sync') }}" class="inline">{% if mb %}<input type="hidden" name="mb" value="{{ mb }}">{% endif %}<button class="btn btn-outline-primary">Nieuwe e-mail ophalen</button></form>
<a class="btn btn-primary" href="{{ url_for('mail_new', mb=mb) }}">Nieuwe e-mail</a>
</div>
</div>
{% if not configured %}
<div class="flash">Koppel eerst je mailbox bij <a href="{{ url_for('settings') }}#mail">Instellingen</a> (IMAP voor ontvangen, SMTP voor versturen).</div>
{% if not mailboxes %}
<div class="flash">Er is nog geen mailbox voor jou. {% if g.user.role == 'admin' %}Voeg er een toe bij <a href="{{ url_for('settings') }}#mail">Instellingen</a>.{% else %}Vraag een beheerder om er een in te stellen.{% endif %}</div>
{% elif current and current.last_error %}
<div class="flash error">{{ current.name }}: laatste ophaalpoging mislukt: {{ current.last_error }}</div>
{% endif %}
{% if mailboxes|length > 1 %}
<div class="filters" aria-label="Mailboxen">
<a href="{{ url_for('inbox', f=f if f != 'all' else None, q=term or None) }}" class="{{ 'on' if not mb }}">Alle mailboxen{% if unread %} <span class="muted">{{ unread }}</span>{% endif %}</a>
{% for m in mailboxes %}
<a href="{{ url_for('inbox', mb=m.id, f=f if f != 'all' else None, q=term or None) }}" class="{{ 'on' if mb == m.id }}">{{ m.name }}{% if unread_by.get(m.id) %} <span class="muted">{{ unread_by[m.id] }}</span>{% endif %}</a>
{% endfor %}
</div>
{% endif %}
<div class="filters">
{% for k, l in [('all', 'Inbox'), ('unread', 'Ongelezen'), ('unlinked', 'Niet gekoppeld aan contact'), ('sent', 'Verzonden'), ('archived', 'Archief')] %}
<a href="{{ url_for('inbox', f=k if k != 'all' else None, q=term or None) }}" class="{{ 'on' if f == k }}">{{ l }}{% if k == 'unread' and unread %} <span class="muted">{{ unread }}</span>{% endif %}</a>
<a href="{{ url_for('inbox', f=k if k != 'all' else None, mb=mb, q=term or None) }}" class="{{ 'on' if f == k }}">{{ l }}</a>
{% endfor %}
{% if unread and not sent %}
<form method="post" action="{{ url_for('inbox_read_all') }}" class="inline"><button class="btn btn-link btn-sm">Alles als gelezen markeren</button></form>
{% if (unread_by.get(mb) if mb else unread) and not sent %}
<form method="post" action="{{ url_for('inbox_read_all') }}" class="inline">{% if mb %}<input type="hidden" name="mb" value="{{ mb }}">{% endif %}<button class="btn btn-link btn-sm">Alles als gelezen markeren</button></form>
{% endif %}
</div>
<div class="card"><div class="card-body">
{% if rows %}
<div class="table-responsive"><table class="table table-hover table-stack">
<thead><tr><th>{{ 'Aan' if sent else 'Van' }}</th><th>Onderwerp</th><th>Contact</th><th class="num">Datum</th></tr></thead>
<thead><tr><th>{{ 'Aan' if sent else 'Van' }}</th><th>Onderwerp</th><th>Contact</th>{% if not mb and mailboxes|length > 1 %}<th>Mailbox</th>{% endif %}<th class="num">Datum</th></tr></thead>
<tbody>
{% for e in rows %}
<tr class="link {{ 'unread' if not e.is_read }}" onclick="location='{{ url_for('inbox_view', eid=e.id) }}'">
<td class="stack-title">{% if not e.is_read %}<span class="dot" aria-label="Ongelezen"></span>{% endif %}{{ e.to_addr if sent else e.from_name or e.from_addr }}<a class="d-block d-md-none" href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a></td>
<td class="d-none d-md-table-cell"><a href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a>{% if e.attachments %} <span class="muted" title="{{ e.attachments }}">· bijlage</span>{% endif %}</td>
<td data-label="Contact">{{ e.contact_name or '' }}</td>
{% if not mb and mailboxes|length > 1 %}<td data-label="Mailbox" class="muted">{{ e.mailbox_name or '' }}</td>{% endif %}
<td data-label="Datum" class="muted num">{{ e.date|dt }}</td>
</tr>
{% endfor %}
@@ -45,5 +59,5 @@
</table></div>
{% else %}<p class="empty">{{ 'Niets gevonden.' if term else 'Geen berichten.' }}</p>{% endif %}
</div></div>
{% if f == 'archived' %}<p class="muted">Archiveren haalt een bericht alleen uit de inbox van de CRM; in je mailbox blijft het staan.</p>{% endif %}
{% if f == 'archived' %}<p class="muted">Archiveren haalt een bericht alleen uit de inbox van de CRM; in de mailbox op de server blijft het staan.</p>{% endif %}
{% endblock %}
+1 -1
View File
@@ -8,7 +8,7 @@
<div class="actions"><a class="btn btn-outline-primary" href="{{ url_for('mail_templates') }}">Templates</a></div>
</div>
<div class="card"><div class="card-body">
{{ compose(m, templates, vars, datalist='mail-contacts') }}
{{ compose(m, templates, vars, mailboxes, datalist='mail-contacts') }}
{% if not templates %}<p class="hint">Nog geen templates. <a href="{{ url_for('mail_template_edit') }}">Maak een template</a> voor berichten die je vaker stuurt.</p>{% endif %}
</div></div>
<datalist id="mail-contacts">{% for c in contacts %}<option value="{{ c.email }}">{{ c.name }}</option>{% endfor %}</datalist>
+91
View File
@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% block title %}{{ mb.name or 'Nieuwe mailbox' }}{% endblock %}
{% macro field(key, label, type='text', placeholder='', full=false) %}
<div class="{{ 'col-12' if full else 'col-md-6 col-xl-4' }}"><label class="form-label" for="{{ key }}">{{ label }}</label><input class="form-control" id="{{ key }}" type="{{ type }}" name="{{ key }}" value="{{ mb[key] or '' if mb[key] is defined else '' }}" placeholder="{{ placeholder }}"></div>
{% endmacro %}
{% macro security(key) %}
<div class="col-md-6 col-xl-4"><label class="form-label" for="{{ key }}">Beveiliging</label>
<select class="form-select" id="{{ key }}" name="{{ key }}">{% for v, label in SECURITY %}<option value="{{ v }}" {{ 'selected' if (mb[key] or '') == v }}>{{ label }}</option>{% endfor %}</select></div>
{% endmacro %}
{% macro secret(key, label, empty='') %}
<div class="col-md-6 col-xl-4"><label class="form-label" for="{{ key }}">{{ label }}</label><input class="form-control" id="{{ key }}" type="password" name="{{ key }}" placeholder="{{ '•••••••• (ingesteld)' if mb[key] else empty }}" autocomplete="new-password"></div>
{% endmacro %}
{% block content %}
<div class="head">
<div><a class="label" href="{{ url_for('settings', _anchor='mail') }}">← Instellingen</a><h1>{{ mb.name if mb.id else 'Nieuwe mailbox' }}</h1>
{% if mb.last_error %}<div class="flash error mt-2">Laatste fout bij ophalen: {{ mb.last_error }}</div>{% endif %}</div>
</div>
<form method="post">
<div class="card"><div class="card-body">
<h2>Algemeen</h2>
<div class="row g-3">
<div class="col-md-6 col-xl-4"><label class="form-label" for="name">Naam *</label><input class="form-control" id="name" name="name" value="{{ mb.name or '' }}" placeholder="Info" required></div>
<div class="col-md-6 col-xl-4"><label class="form-label" for="purpose">Rol</label>
<select class="form-select" id="purpose" name="purpose" aria-describedby="purpose-hint">{% for k, l in PURPOSES %}<option value="{{ k }}" {{ 'selected' if mb.purpose == k }}>{{ l }}</option>{% endfor %}</select>
<p class="hint" id="purpose-hint">Nieuwsbrieven kiezen standaard een nieuwsbriefmailbox.</p></div>
{{ field('smtp_from', 'Afzender', placeholder='Van Emous <info@vanemouswoningen.nl>') }}
<div class="col-12">
<div class="form-check"><input class="form-check-input" type="checkbox" id="active" name="active" value="1" {{ 'checked' if mb.active }}><label class="form-check-label" for="active">Actief (ophalen en versturen)</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="notify" name="notify" value="1" {{ 'checked' if mb.notify }}><label class="form-check-label" for="notify">Melding bij nieuwe e-mail voor wie toegang heeft</label></div>
</div>
</div>
<h3>Toegang</h3>
<p class="hint">Niemand aangevinkt = gedeeld met iedereen. Vink alleen jezelf aan voor een persoonlijke mailbox.</p>
<div class="d-flex flex-wrap gap-3">
{% for u in USERS %}<div class="form-check"><input class="form-check-input" type="checkbox" id="user-{{ u.id }}" name="users" value="{{ u.id }}" {{ 'checked' if u.id in access }}><label class="form-check-label" for="user-{{ u.id }}">{{ u.name }}</label></div>{% endfor %}
</div>
</div></div>
<div class="card"><div class="card-body">
<h2>Verbinding</h2>
<p class="hint" style="margin:-.4rem 0 1rem">Bij OVHcloud (Zimbra of MX Plan) is de gebruikersnaam het volledige e-mailadres en heeft elke mailbox een eigen wachtwoord.
Gmail werkt met een app-wachtwoord. Wachtwoorden staan in de database: houd de map <code>data/</code> privé.</p>
<div class="row g-3">
<div class="col-md-6 col-xl-4"><label class="form-label" for="mail-provider">Aanbieder</label>
<select class="form-select" id="mail-provider" aria-describedby="mail-provider-hint">
<option value="">Kies om de servers in te vullen…</option>
{% for key, p in PROVIDERS.items() %}<option value="{{ key }}" data-preset="{{ p|tojson|forceescape }}">{{ p.label }}</option>{% endfor %}
</select>
<p class="hint" id="mail-provider-hint">Vult server, poort en beveiliging in; daarna kun je alles nog aanpassen.</p>
</div>
</div>
<h3>Ontvangen (IMAP)</h3>
<div class="row g-3">
{{ field('imap_host', 'Server', placeholder='imap.mail.ovh.net') }}
{{ field('imap_port', 'Poort', 'number', '993') }}
{{ security('imap_security') }}
{{ field('imap_user', 'Gebruikersnaam', placeholder='info@vanemouswoningen.nl') }}
{{ secret('imap_pass', 'Wachtwoord') }}
{{ field('imap_folder', 'Map', placeholder='INBOX') }}
{{ field('imap_days', 'Aantal dagen terug ophalen', 'number', '30') }}
</div>
<h3>Versturen (SMTP)</h3>
<div class="row g-3">
{{ field('smtp_host', 'Server', placeholder='smtp.mail.ovh.net') }}
{{ field('smtp_port', 'Poort', 'number', '465') }}
{{ security('smtp_security') }}
{{ field('smtp_user', 'Gebruikersnaam', placeholder='leeg = zelfde als IMAP') }}
{{ secret('smtp_pass', 'Wachtwoord', 'leeg = zelfde als IMAP') }}
<div class="col-md-6 col-xl-4"><label class="form-label" for="smtp_sent_folder">Kopie in map</label><input class="form-control" id="smtp_sent_folder" name="smtp_sent_folder" value="{{ mb.smtp_sent_folder or '' }}" placeholder="leeg = geen kopie" aria-describedby="sent-hint"><p class="hint" id="sent-hint">Bij Zimbra/MX Plan <code>Sent</code>, zodat verstuurde mail ook in de webmail staat. Gmail doet dit zelf. Nieuwsbrieven komen er niet in.</p></div>
</div>
</div></div>
<div class="actions">
<button class="btn btn-primary" name="action" value="save">Opslaan</button>
<button class="btn btn-outline-primary" name="action" value="test">Opslaan en verbinding testen</button>
<a class="btn btn-outline-secondary" href="{{ url_for('settings', _anchor='mail') }}">Annuleren</a>
</div>
</form>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('mail-provider').addEventListener('change', (e) => {
const opt = e.target.selectedOptions[0];
if (!opt || !opt.dataset.preset) return;
Object.entries(JSON.parse(opt.dataset.preset)).forEach(([key, value]) => {
const el = document.getElementById(key);
if (el) el.value = value;
});
});
</script>
{% endblock %}
+24
View File
@@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block title %}Meldingen{% endblock %}
{% block content %}
<div class="head">
<div><div class="kicker">Voor jou</div><h1>Meldingen</h1></div>
{% if bell_unread %}<div class="actions"><form method="post" action="{{ url_for('notifications_read_all') }}" class="inline"><button class="btn btn-outline-primary">Alles als gelezen markeren</button></form></div>{% endif %}
</div>
<div class="card"><div class="card-body">
{% if rows %}
<div class="table-responsive"><table class="table table-hover table-stack">
<thead><tr><th>Melding</th><th>Details</th><th class="num">Wanneer</th></tr></thead>
<tbody>
{% for n in rows %}
<tr class="link {{ 'unread' if not n.read_at }}" onclick="location='{{ url_for('notification_open', nid=n.id) }}'">
<td class="stack-title">{% if not n.read_at %}<span class="dot" aria-label="Ongelezen"></span>{% endif %}<a href="{{ url_for('notification_open', nid=n.id) }}">{{ n.title }}</a></td>
<td data-label="Details" class="muted">{{ n.body }}</td>
<td data-label="Wanneer" class="muted num">{{ n.created_at|dt }}</td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% else %}<p class="empty">Geen meldingen.</p>{% endif %}
</div></div>
{% endblock %}
+55
View File
@@ -0,0 +1,55 @@
{% extends 'base.html' %}
{% block title %}{{ job.name if job else jt.label }}{% endblock %}
{% block content %}
<div class="head">
<div><a class="label" href="{{ url_for('routines') }}">← Routines</a><h1>{{ job.name if job else 'Nieuwe routine' }}</h1><div class="muted">{{ jt.label }}</div></div>
{% if job %}<div class="actions"><form method="post" action="{{ url_for('routine_run', jid=job.id) }}" class="inline"><button class="btn btn-outline-primary">Nu uitvoeren</button></form></div>{% endif %}
</div>
<div class="row g-4">
<div class="col-lg-7">
<form method="post" class="card"><div class="card-body">
{% if jt.description %}<p class="hint" style="margin:0 0 1rem">{{ jt.description }}</p>{% endif %}
<div class="row g-3">
<div class="col-12"><label class="form-label" for="name">Naam</label><input class="form-control" id="name" name="name" value="{{ job.name if job else jt.label }}"></div>
<div class="col-6 col-md-4"><label class="form-label" for="every">Elke</label><input class="form-control" id="every" type="number" min="1" name="every" value="{{ every }}" required></div>
<div class="col-6 col-md-4"><label class="form-label" for="unit">Eenheid</label>
<select class="form-select" id="unit" name="unit">{% for size, l in UNITS %}<option value="{{ size }}" {{ 'selected' if unit == size }}>{{ l }}</option>{% endfor %}</select></div>
{% for f in jt.fields %}
{% if f.kind == 'bool' %}
<div class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" id="f-{{ f.key }}" name="{{ f.key }}" value="1" {{ 'checked' if config[f.key] }}><label class="form-check-label" for="f-{{ f.key }}">{{ f.label }}</label></div>{% if f.hint %}<p class="hint">{{ f.hint }}</p>{% endif %}</div>
{% elif f.kind == 'mailboxes' %}
<div class="col-12"><span class="form-label d-block">{{ f.label }}</span>
<div class="d-flex flex-wrap gap-3">{% for m in mailboxes %}<div class="form-check"><input class="form-check-input" type="checkbox" id="f-{{ f.key }}-{{ m.id }}" name="{{ f.key }}" value="{{ m.id }}" {{ 'checked' if m.id in config[f.key] }}><label class="form-check-label" for="f-{{ f.key }}-{{ m.id }}">{{ m.name }}</label></div>{% else %}<span class="muted">Nog geen mailboxen.</span>{% endfor %}</div>
{% if f.hint %}<p class="hint">{{ f.hint }}</p>{% endif %}</div>
{% else %}
<div class="col-md-8"><label class="form-label" for="f-{{ f.key }}">{{ f.label }}</label><input class="form-control" id="f-{{ f.key }}" type="{{ 'number' if f.kind == 'number' else f.kind }}" name="{{ f.key }}" value="{{ config[f.key] }}">{% if f.hint %}<p class="hint">{{ f.hint }}</p>{% endif %}</div>
{% endif %}
{% endfor %}
<div class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" id="active" name="active" value="1" {{ 'checked' if not job or job.active }}><label class="form-check-label" for="active">Actief</label></div></div>
</div>
<div class="actions">
<button class="btn btn-primary">Opslaan</button>
<a class="btn btn-outline-secondary" href="{{ url_for('routines') }}">Annuleren</a>
</div>
</div></form>
{% if job %}
<form method="post" action="{{ url_for('routine_delete', jid=job.id) }}" onsubmit="return confirm('Routine verwijderen?')">
<button class="btn btn-outline-secondary btn-sm">Routine verwijderen</button>
</form>
{% endif %}
</div>
{% if job %}
<div class="col-lg-5">
<div class="card"><div class="card-body">
<h2>Logboek</h2>
<p class="hint">Volgende keer: {{ (job.next_run|dt if job.next_run else 'zo snel mogelijk') if job.active else 'staat uit' }}</p>
{% if runs %}
<ul class="timeline">
{% for r in runs %}<li><b>{{ r.started_at|dt }}</b> {% if r.status == 'fout' %}<span class="chip nieuw">Fout</span>{% endif %}{% if r.manual %} <span class="muted">(handmatig)</span>{% endif %}<br><span class="muted">{{ r.message }}</span></li>{% endfor %}
</ul>
{% else %}<p class="empty">Nog niet uitgevoerd.</p>{% endif %}
</div></div>
</div>
{% endif %}
</div>
{% endblock %}
+35
View File
@@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block title %}Routines{% endblock %}
{% block content %}
<div class="head">
<div><div class="kicker">Beheer</div><h1>Routines</h1></div>
<div class="actions">
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Nieuwe routine</button>
<ul class="dropdown-menu dropdown-menu-end">{% for k, t in TYPES.items() %}<li><a class="dropdown-item" href="{{ url_for('routine_edit', kind=k) }}">{{ t.label }}</a></li>{% endfor %}</ul>
</div>
</div>
</div>
<div class="card"><div class="card-body">
{% if rows %}
<div class="table-responsive"><table class="table table-hover table-stack">
<thead><tr><th>Naam</th><th>Soort</th><th>Elke</th><th>Laatst</th><th>Volgende</th><th>Resultaat</th><th></th></tr></thead>
<tbody>
{% for j in rows %}
<tr class="link" onclick="location='{{ url_for('routine_edit', jid=j.id) }}'">
<td class="stack-title"><a href="{{ url_for('routine_edit', jid=j.id) }}">{{ j.name }}</a>{% if not j.active %} <span class="chip verloren">Uit</span>{% endif %}{% if j.running_since %} <span class="chip nieuw">Bezig</span>{% endif %}</td>
<td data-label="Soort">{{ TYPES[j.kind].label if j.kind in TYPES else j.kind }}</td>
<td data-label="Elke">{{ interval_text(j.interval_minutes) }}</td>
<td data-label="Laatst" class="muted">{{ j.last_run|dt if j.last_run else '—' }}</td>
<td data-label="Volgende" class="muted">{{ (j.next_run|dt if j.next_run else 'zo snel mogelijk') if j.active else '—' }}</td>
<td data-label="Resultaat">{% if j.last_status == 'fout' %}<span class="chip nieuw">Fout</span> {% endif %}<span class="muted">{{ j.last_message|truncate(80) }}</span></td>
<td class="num"><form method="post" action="{{ url_for('routine_run', jid=j.id) }}" class="inline" onclick="event.stopPropagation()"><button class="btn btn-outline-primary btn-sm">Nu uitvoeren</button></form></td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% else %}<p class="empty">Nog geen routines.</p>{% endif %}
</div></div>
<p class="muted">Routines draaien op de achtergrond (Docker: container <code>crm-routines</code>; zonder Docker: cron elke minuut
<code>python app.py tick</code>). Elke halve minuut wordt gekeken welke aan de beurt zijn; een routine draait nooit twee keer tegelijk.</p>
{% endblock %}
+46
View File
@@ -0,0 +1,46 @@
{% extends 'base.html' %}
{% block title %}{{ name or 'Nieuw segment' }}{% endblock %}
{% macro checks(key, options, selected) %}
<div class="d-flex flex-wrap gap-3">{% for v, l in options %}<div class="form-check"><input class="form-check-input" type="checkbox" id="{{ key }}-{{ loop.index }}" name="{{ key }}" value="{{ v }}" {{ 'checked' if v in selected }}><label class="form-check-label" for="{{ key }}-{{ loop.index }}">{{ l }}</label></div>{% endfor %}</div>
{% endmacro %}
{% block content %}
<div class="head">
<div><a class="label" href="{{ url_for('segments') }}">← Segmenten</a><h1>{{ name or 'Nieuw segment' }}</h1></div>
</div>
<div class="row g-4">
<div class="col-lg-7">
<form method="post" class="card"><div class="card-body">
<div class="row g-3">
<div class="col-12"><label class="form-label" for="name">Naam *</label><input class="form-control" id="name" name="name" value="{{ name }}" placeholder="Bijvoorbeeld: Leads met een ontwerp"></div>
<div class="col-12"><span class="form-label d-block">Soort</span>{{ checks('types', TYPES, filters.types or []) }}</div>
<div class="col-12"><span class="form-label d-block">Status</span>{{ checks('statuses', STATUSES, filters.statuses or []) }}</div>
{% if sources %}<div class="col-12"><span class="form-label d-block">Bron</span>{{ checks('sources', sources, filters.sources or []) }}</div>{% endif %}
<div class="col-12"><span class="form-label d-block">Toegewezen aan</span>{{ checks('users', users, filters.users or []) }}</div>
<div class="col-md-4"><label class="form-label" for="city">Plaats bevat</label><input class="form-control" id="city" name="city" value="{{ filters.city or '' }}"></div>
<div class="col-6 col-md-4"><label class="form-label" for="created_from">Aangemaakt vanaf</label><input class="form-control" id="created_from" type="date" name="created_from" value="{{ filters.created_from or '' }}"></div>
<div class="col-6 col-md-4"><label class="form-label" for="created_to">tot en met</label><input class="form-control" id="created_to" type="date" name="created_to" value="{{ filters.created_to or '' }}"></div>
<div class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" id="has_design" name="has_design" value="1" {{ 'checked' if filters.has_design }}><label class="form-check-label" for="has_design">Alleen met een gekozen woningontwerp</label></div></div>
</div>
<p class="hint">Niets aangevinkt bij een onderdeel = geen beperking daarop.</p>
<div class="actions">
<button class="btn btn-primary" name="action" value="save">Opslaan</button>
<button class="btn btn-outline-primary" name="action" value="preview">Ontvangers tellen</button>
</div>
</div></form>
{% if seg %}
<form method="post" action="{{ url_for('segment_delete', sid=seg.id) }}" onsubmit="return confirm('Segment verwijderen? Nieuwsbrieven die het gebruiken houden hun vastgelegde ontvangers.')">
<button class="btn btn-outline-secondary btn-sm">Segment verwijderen</button>
</form>
{% endif %}
</div>
<div class="col-lg-5">
<div class="card"><div class="card-body">
<h2>{{ count }} ontvanger{{ '' if count == 1 else 's' }}</h2>
{% if sample %}
<ul class="timeline">{% for c in sample %}<li><a href="{{ url_for('contact_view', cid=c.id) }}">{{ c.name or c.email }}</a><br><span class="muted">{{ c.email }}{% if c.city %} · {{ c.city }}{% endif %}</span></li>{% endfor %}</ul>
{% if count > sample|length %}<p class="hint">En nog {{ count - sample|length }}.</p>{% endif %}
{% else %}<p class="empty">Geen contacten met deze filters.</p>{% endif %}
</div></div>
</div>
</div>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block title %}Segmenten{% endblock %}
{% block content %}
<div class="head">
<div><a class="label" href="{{ url_for('campaigns') }}">← Nieuwsbrieven</a><h1>Segmenten</h1></div>
<div class="actions"><a class="btn btn-primary" href="{{ url_for('segment_edit') }}">Nieuw segment</a></div>
</div>
<div class="card"><div class="card-body">
{% if rows %}
<div class="table-responsive"><table class="table table-hover table-stack">
<thead><tr><th>Naam</th><th class="num">Ontvangers nu</th><th>Bijgewerkt</th></tr></thead>
<tbody>
{% for s in rows %}
<tr class="link" onclick="location='{{ url_for('segment_edit', sid=s.id) }}'">
<td class="stack-title"><a href="{{ url_for('segment_edit', sid=s.id) }}">{{ s.name }}</a></td>
<td data-label="Ontvangers nu" class="num">{{ s.n }}</td>
<td data-label="Bijgewerkt" class="muted">{{ s.updated_at|dt }}</td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% else %}<p class="empty">Nog geen segmenten.</p>{% endif %}
</div></div>
<p class="muted">Een segment is een selectie van contacten met een e-mailadres. Afgemelde en onbestelbare adressen tellen nooit mee;
per e-mailadres gaat er één nieuwsbrief uit.</p>
{% endblock %}
+26 -51
View File
@@ -3,13 +3,35 @@
{% macro field(key, label, type='text', placeholder='', full=false) %}
<div class="{{ 'col-12' if full else 'col-md-6 col-xl-4' }}"><label class="form-label" for="{{ key }}">{{ label }}</label><input class="form-control" id="{{ key }}" type="{{ type }}" name="{{ key }}" value="{{ s.get(key, '') }}" placeholder="{{ placeholder }}"></div>
{% endmacro %}
{% macro security(key) %}
<div class="col-md-6 col-xl-4"><label class="form-label" for="{{ key }}">Beveiliging</label>
<select class="form-select" id="{{ key }}" name="{{ key }}">{% for v, label in SECURITY %}<option value="{{ v }}" {{ 'selected' if s.get(key, '') == v }}>{{ label }}</option>{% endfor %}</select></div>
{% endmacro %}
{% block content %}
<div class="head"><div><div class="kicker">Beheer</div><h1>Instellingen</h1></div></div>
<div class="card" id="mail"><div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div><h2>Mailboxen</h2>
<p class="hint" style="margin:-.4rem 0 1rem">Elke mailbox (info@, facturen@, nieuwsbrief@ of een persoonlijke) heeft een eigen account. Een mailbox
zonder gekozen gebruikers is gedeeld met iedereen. Ophalen gaat via de routine "E-mail ophalen" (<a href="{{ url_for('routines') }}">Routines</a>).</p></div>
<a class="btn btn-primary" href="{{ url_for('mailbox_edit') }}">Mailbox toevoegen</a>
</div>
{% if mailboxes %}
<div class="table-responsive"><table class="table table-hover table-stack">
<thead><tr><th>Naam</th><th>Adres</th><th>Rol</th><th>Toegang</th><th>Laatst opgehaald</th><th>Status</th></tr></thead>
<tbody>
{% for m in mailboxes %}
<tr class="link" onclick="location='{{ url_for('mailbox_edit', mbid=m.id) }}'">
<td class="stack-title"><a href="{{ url_for('mailbox_edit', mbid=m.id) }}">{{ m.name }}</a></td>
<td data-label="Adres">{{ m.imap_user or m.smtp_user }}</td>
<td data-label="Rol">{{ PURPOSE.get(m.purpose, m.purpose) }}</td>
<td data-label="Toegang">{{ m.access or 'Iedereen' }}</td>
<td data-label="Laatst opgehaald" class="muted">{{ m.last_sync|dt if m.last_sync else '—' }}</td>
<td data-label="Status">{% if not m.active %}<span class="chip verloren">Inactief</span>{% elif m.last_error %}<span class="chip nieuw" title="{{ m.last_error }}">Fout</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% else %}<p class="empty">Nog geen mailbox.</p>{% endif %}
</div></div>
<form method="post">
<div class="card"><div class="card-body">
<h2>Bedrijfsgegevens</h2>
@@ -37,43 +59,6 @@
</div>
</div></div>
<div class="card" id="mail"><div class="card-body">
<h2>E-mail koppelen</h2>
<p class="hint" style="margin:-.4rem 0 1rem">Eén gedeelde mailbox (bijv. info@) voor de inbox en het versturen vanuit de CRM. Bij OVHcloud
(Zimbra of MX Plan) is de gebruikersnaam het volledige e-mailadres; de afzender moet dat adres (of een alias ervan) zijn.
Gmail werkt met een app-wachtwoord. Wachtwoorden staan in de database: houd de map <code>data/</code> privé.</p>
<div class="row g-3">
<div class="col-md-6 col-xl-4"><label class="form-label" for="mail-provider">Aanbieder</label>
<select class="form-select" id="mail-provider" aria-describedby="mail-provider-hint">
<option value="">Kies om de servers in te vullen…</option>
{% for key, p in PROVIDERS.items() %}<option value="{{ key }}" data-preset="{{ p|tojson|forceescape }}">{{ p.label }}</option>{% endfor %}
</select>
<p class="hint" id="mail-provider-hint">Vult server, poort en beveiliging in; daarna kun je alles nog aanpassen.</p>
</div>
</div>
<h3>Ontvangen (IMAP)</h3>
<div class="row g-3">
{{ field('imap_host', 'Server', placeholder='imap.mail.ovh.net') }}
{{ field('imap_port', 'Poort', 'number', '993') }}
{{ security('imap_security') }}
{{ field('imap_user', 'Gebruikersnaam', placeholder='info@vanemouswoningen.nl') }}
<div class="col-md-6 col-xl-4"><label class="form-label" for="imap_pass">Wachtwoord</label><input class="form-control" id="imap_pass" type="password" name="imap_pass" placeholder="{{ '•••••••• (ingesteld)' if s.imap_pass else '' }}" autocomplete="new-password"></div>
{{ field('imap_folder', 'Map', placeholder='INBOX') }}
{{ field('imap_days', 'Aantal dagen terug ophalen', 'number', '30') }}
</div>
<h3>Versturen (SMTP)</h3>
<div class="row g-3">
{{ field('smtp_host', 'Server', placeholder='smtp.mail.ovh.net') }}
{{ field('smtp_port', 'Poort', 'number', '465') }}
{{ security('smtp_security') }}
{{ field('smtp_user', 'Gebruikersnaam', placeholder='leeg = zelfde als IMAP') }}
<div class="col-md-6 col-xl-4"><label class="form-label" for="smtp_pass">Wachtwoord</label><input class="form-control" id="smtp_pass" type="password" name="smtp_pass" placeholder="{{ '•••••••• (ingesteld)' if s.smtp_pass else 'leeg = zelfde als IMAP' }}" autocomplete="new-password"></div>
<div class="col-md-6 col-xl-4"><label class="form-label" for="smtp_sent_folder">Kopie in map</label><input class="form-control" id="smtp_sent_folder" name="smtp_sent_folder" value="{{ s.smtp_sent_folder or '' }}" placeholder="leeg = geen kopie" aria-describedby="sent-hint"><p class="hint" id="sent-hint">Bij Zimbra/MX Plan <code>Sent</code>, zodat verstuurde mail ook in de webmail staat. Gmail doet dit zelf.</p></div>
{{ field('smtp_from', 'Afzender', placeholder='Van Emous <info@vanemouswoningen.nl>', full=true) }}
</div>
<div class="actions"><button class="btn btn-outline-primary" name="action" value="mailtest">Opslaan en verbinding testen</button></div>
</div></div>
<div class="card"><div class="card-body">
<h2>Formulieren en API</h2>
<div class="row g-3">
@@ -145,14 +130,4 @@ const { ok, id } = await res.json();{% endfilter %}</pre>
<button class="btn btn-outline-secondary btn-sm">Nieuwe API-sleutel maken</button>
</form>
</div></div>
<script>
document.getElementById('mail-provider').addEventListener('change', (e) => {
const opt = e.target.selectedOptions[0];
if (!opt || !opt.dataset.preset) return;
Object.entries(JSON.parse(opt.dataset.preset)).forEach(([key, value]) => {
const el = document.getElementById(key);
if (el) el.value = value;
});
});
</script>
{% endblock %}
+20
View File
@@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% block title %}Afmelden{% endblock %}
{% block body %}
<div class="auth-wrap">
<div class="auth">
<img src="{{ url_for('static', filename='logo.svg') }}" alt="{{ company }}">
<div class="card"><div class="card-body">
<h1>Afmelden</h1>
{% if not valid %}
<p>Deze afmeldlink is niet geldig. Neem contact met ons op als u geen nieuwsbrieven meer wilt ontvangen.</p>
{% elif done %}
<p>U bent afgemeld. U ontvangt geen nieuwsbrieven meer van {{ company }}.</p>
{% else %}
<p>Wilt u geen nieuwsbrieven meer ontvangen van {{ company }}?</p>
<form method="post"><button class="btn btn-primary">Afmelden</button></form>
{% endif %}
</div></div>
</div>
</div>
{% endblock %}