Gebruikers: massa-actie actief/inactief maken of verwijderen (jezelf wordt overgeslagen) en een knop Verwijderen op het gebruikersformulier; contacten, taken en activiteiten blijven zonder eigenaar, agenda, meldingen en mailboxkoppelingen gaan mee

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-16 20:14:40 +02:00
co-authored by Claude Opus 5.5
parent 0368b7c1da
commit 5a97f992da
4 changed files with 50 additions and 3 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ Eenvoudige CRM in Python (Flask) met SQLite, in de Van Emous-huisstijl. Versie 0
in `kosten/leveranciers.csv`). Ontbreekt het product, dan wordt het aangemaakt met de prijs per eenheid van de
berekening; bestaande productprijzen worden alleen bijgewerkt als je dat aanvinkt (met producthistorie).
De regels staan in `PRODUCT_RULES` in `static/bestek.js`.
- **Gebruikers** met de rollen beheerder (ook gebruikers en instellingen) en medewerker.
- **Gebruikers** met de rollen beheerder (ook gebruikers en instellingen) en medewerker; massa-actie actief/inactief maken of verwijderen (jezelf wordt overgeslagen). Verwijderen laat contacten, taken en activiteiten zonder eigenaar staan; agenda, meldingen en mailboxkoppelingen gaan mee.
- **Klantportaal** (`/portal`): de klant krijgt een uitnodigingslink (kopiëren of mailen vanaf het
contact), stelt een wachtwoord in en kan dan offertes bekijken, accepteren of afwijzen, bestanden
uploaden en berichten sturen. Een geaccepteerde offerte zet het contact op gewonnen/klant.
+42 -1
View File
@@ -101,7 +101,7 @@ API_SKIP = {'api_key', 'redirect', '_gotcha', 'website_hp', 'g-recaptcha-respons
PUBLIC = {'static', 'login', 'setup', 'api_leads', 'api_products', 'dav', 'dav_wellknown', 'unsubscribe',
'api_tectum_woningen', 'api_tectum_woning', 'api_tectum_versie'}
PORTAL_PUBLIC = {'portal_login', 'portal_invite'}
ADMIN_ONLY = {'settings', 'users', 'user_edit', 'mailbox_edit', 'routines', 'routine_edit', 'routine_run',
ADMIN_ONLY = {'settings', 'users', 'user_edit', 'users_bulk', 'user_delete', 'mailbox_edit', 'routines', 'routine_edit', 'routine_run',
'routine_delete'}
@@ -918,6 +918,47 @@ def user_edit(uid):
return render_template('user_form.html', u=u or {'role': 'user', 'active': 1})
def delete_users(ids):
"""Verwijdert gebruikers: hun contacten, taken en activiteiten blijven (zonder eigenaar), hun agenda,
meldingen en mailboxkoppelingen gaan mee (foreign keys)."""
db().execute(f'DELETE FROM users WHERE id IN {id_list(ids)}', ids)
@app.post('/users/<int:uid>/delete')
def user_delete(uid):
u = get_or_404('users', uid)
if u['id'] == g.user['id']:
flash('Je kunt jezelf niet verwijderen.', 'error')
return redirect(url_for('user_edit', uid=uid))
delete_users([uid])
db().commit()
flash(f"Gebruiker {u['name']} verwijderd.")
return redirect(url_for('users'))
@app.post('/users/bulk')
def users_bulk():
ids, action = bulk_ids(), request.form.get('action')
skipped = g.user['id'] in ids
ids = [i for i in ids if i != g.user['id']] # jezelf niet buitensluiten
if not ids:
flash('Je kunt jezelf niet inactief maken of verwijderen.' if skipped else
'Vink eerst een of meer gebruikers aan.', 'error')
return bulk_back(url_for('users'))
if action in ('activate', 'deactivate'):
db().execute(f'UPDATE users SET active=? WHERE id IN {id_list(ids)}', [int(action == 'activate'), *ids])
msg = f"{len(ids)} gebruiker(s) op {'actief' if action == 'activate' else 'inactief'} gezet."
elif action == 'delete':
delete_users(ids)
msg = f'{len(ids)} gebruiker(s) verwijderd.'
else:
flash('Kies een actie.', 'error')
return bulk_back(url_for('users'))
db().commit()
flash(msg + (' Jezelf heb ik overgeslagen.' if skipped else ''))
return bulk_back(url_for('users'))
# ---------------------------------------------------------------- dashboard
@app.route('/')
+2
View File
@@ -19,6 +19,8 @@
<div class="actions">
<button class="btn btn-primary">Opslaan</button>
<a class="btn btn-outline-secondary" href="{{ url_for('users') }}">Annuleren</a>
{% if u.id and u.id != g.user.id %}<button class="btn btn-outline-secondary ms-auto" form="delete-user" onclick='return confirm({{ (u.name ~ " verwijderen? Contacten, taken en activiteiten blijven zonder eigenaar staan; agenda en meldingen verdwijnen. Alleen inactief maken kan ook: haal het vinkje Actief weg.")|tojson }})'>Verwijderen</button>{% endif %}
</div>
</div></form>
{% if u.id and u.id != g.user.id %}<form id="delete-user" method="post" action="{{ url_for('user_delete', uid=u.id) }}"></form>{% endif %}
{% endblock %}
+5 -1
View File
@@ -1,4 +1,5 @@
{% extends 'base.html' %}
{% from '_bulk.html' import bulk_head, bulk_cell, bulk_bar with context %}
{% block title %}Gebruikers{% endblock %}
{% block content %}
<div class="head">
@@ -7,10 +8,11 @@
</div>
<div class="card"><div class="card-body">
<div class="table-responsive"><table class="table table-hover table-stack">
<thead><tr><th>Naam</th><th>E-mail</th><th>Rol</th><th class="num">Contacten</th><th>Status</th></tr></thead>
<thead><tr>{{ bulk_head() }}<th>Naam</th><th>E-mail</th><th>Rol</th><th class="num">Contacten</th><th>Status</th></tr></thead>
<tbody>
{% for u in rows %}
<tr class="link" onclick="location='{{ url_for('user_edit', uid=u.id) }}'">
{{ bulk_cell(u.id, u.name) }}
<td class="stack-title"><a href="{{ url_for('user_edit', uid=u.id) }}">{{ u.name }}</a></td>
<td data-label="E-mail">{{ u.email }}</td>
<td data-label="Rol">{{ ROLE.get(u.role, u.role) }}</td>
@@ -22,4 +24,6 @@
</table></div>
</div></div>
<p class="muted">Beheerders kunnen ook gebruikers en instellingen beheren. Klanten krijgen toegang via het klantportaal (op de pagina van het contact).</p>
{{ bulk_bar(url_for('users_bulk'), [('activate', 'Actief maken'), ('deactivate', 'Inactief maken (kan niet meer inloggen)'),
('delete', 'Verwijderen', '{n} gebruiker(s) verwijderen? Hun contacten, taken en activiteiten blijven zonder eigenaar staan; hun agenda en meldingen verdwijnen. Dit kan niet ongedaan worden gemaakt.')]) }}
{% endblock %}