v0.0.1: eerste versie van de Van Emous CRM

Flask + SQLite, in de Van Emous-huisstijl (tokens uit het design system, GSAP-effecten
volgens de motion-regels, uit bij prefers-reduced-motion).

- Leads en contactpersonen: status, toewijzen, verwachte waarde, omzetten naar klant,
  tijdlijn, bestanden, taken/opvolging
- Relaties (zoals accounts in EspoCRM): klantbundels (huishouden/familie) en partners met
  type (leverancier, aannemer, ...); leveranciers van producten worden automatisch partners
- API voor formulieren (POST /api/leads) met dubbele-leaddetectie, bestanden en
  configurator-ontwerpen (house-design JSON)
- Import van CSV/JSON (leads, relaties, producten) met kolomkoppeling; configurator-formaat
- Producten met SKU als vaste sleutel, mutatiehistorie, POST /api/products, CSV-export;
  offerteregels houden hun eigen prijs
- Offertes met btw per tarief en afdrukversie (PDF via de browser)
- Inbox via IMAP/SMTP, gekoppeld aan contacten
- Gebruikers (beheerder/medewerker) en klantportaal (offertes accepteren, bestanden, berichten)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-14 21:56:55 +02:00
co-authored by Claude Opus 5.5
commit bffa053c05
33 changed files with 4111 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
{% extends 'base.html' %}
{% block title %}Import controleren{% endblock %}
{% block content %}
<form method="post">
<input type="hidden" name="kind" value="{{ kind }}">
<div class="head">
<div><a class="label" href="{{ url_for('import_start') }}">← Ander bestand</a>
<h1>{{ filename }}</h1>
<div class="muted">{{ total }} rij(en) · {{ {'contacts': 'leads / contactpersonen', 'relations': 'relaties', 'products': 'producten'}[kind] }}</div></div>
<div class="actions"><button class="btn">{{ total }} rij(en) importeren</button></div>
</div>
<div class="card">
<h2>Kolommen koppelen</h2>
<div class="table-wrap"><table>
<thead><tr>
{% for col in cols %}
<th style="min-width:170px">
<div style="margin-bottom:.4rem">{{ col }}</div>
<select name="map_{{ loop.index0 }}" aria-label="Doelveld voor {{ col }}">
{% for k, l in targets %}<option value="{{ k }}" {{ 'selected' if mapping[loop.index0] == k }}>{{ l }}</option>{% endfor %}
</select>
</th>
{% endfor %}
</tr></thead>
<tbody>
{% for row in rows %}
<tr>{% for col in cols %}{% set v = row.get(col) %}<td>{{ (v if v is string else (v|tojson if v is not none else ''))|truncate(60) }}</td>{% endfor %}</tr>
{% endfor %}
</tbody>
</table></div>
{% if total > rows|length %}<p class="hint">Eerste {{ rows|length }} van {{ total }} rijen.</p>{% endif %}
</div>
<div class="card">
<h2>Opties</h2>
<div class="fields">
{% if kind == 'contacts' %}
<div><label>Importeren als</label><select name="type">{% for k, l in TYPES %}<option value="{{ k }}">{{ l }}</option>{% endfor %}</select></div>
<div><label>Status (als de kolom ontbreekt)</label><select name="status">{% for k, l in STATUSES %}<option value="{{ k }}">{{ l }}</option>{% endfor %}</select></div>
<div><label>Toewijzen aan</label><select name="assigned_user_id"><option value="">— Niemand —</option>{% for u in USERS %}<option value="{{ u.id }}">{{ u.name }}</option>{% endfor %}</select></div>
<div><label>Bron (als de kolom ontbreekt)</label><input name="source" value="Import"></div>
{% endif %}
{% if kind == 'relations' %}
<div><label>Soort</label><select name="relation_kind" id="rkind">{% for k, l in RELATION_KINDS %}<option value="{{ k }}" {{ 'selected' if request.args.get('rkind') == k }}>{{ l }}</option>{% endfor %}</select></div>
<div><label>Type (als de kolom ontbreekt)</label>
<select name="relation_type"><option value="">—</option>
{% for k, l in RELATION_KINDS %}<optgroup label="{{ l }}">{% for t in relation_types[k] %}<option>{{ t }}</option>{% endfor %}</optgroup>{% endfor %}
</select></div>
{% endif %}
{% if kind == 'products' %}
<div class="full"><label><input type="checkbox" name="deactivate_missing" value="1">Producten met een SKU die niet in dit bestand staan op inactief zetten (volledige prijslijst)</label>
<p class="hint">Alleen als er een kolom aan SKU gekoppeld is. Inactieve producten blijven bestaan, met hun mutaties en offertes.</p></div>
{% endif %}
<div class="full"><label><input type="checkbox" name="update" value="1" checked>Bestaande {{ 'contacten (zelfde e-mail) aanvullen' if kind == 'contacts' else 'relaties (zelfde naam of KvK) aanvullen' if kind == 'relations' else 'producten bijwerken (zelfde SKU, of zelfde naam als er geen SKU is); alleen gekoppelde kolommen worden gewijzigd' }}; uit = overslaan</label></div>
</div>
</div>
</form>
{% endblock %}