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
+47
View File
@@ -0,0 +1,47 @@
{% extends 'base.html' %}
{% block title %}{{ 'Klanten' if kind == 'klant' else 'Partners' }}{% endblock %}
{% block content %}
<div class="head">
<div>
<div class="kicker">Relaties</div>
<h1>{{ 'Klanten' if kind == 'klant' else 'Partners' }}</h1>
<div class="muted">{{ 'Huishoudens, families en bedrijven met hun contactpersonen.' if kind == 'klant' else 'Leveranciers, aannemers en andere partners.' }}</div>
</div>
<div class="actions">
<form class="search" method="get">
<input type="hidden" name="kind" value="{{ kind }}">
{% if type %}<input type="hidden" name="type" value="{{ type }}">{% endif %}
<input type="search" name="q" value="{{ term }}" placeholder="Zoek op naam, plaats, persoon…" aria-label="Zoeken">
</form>
<a class="btn ghost" href="{{ url_for('import_start', kind='relations') }}">Importeren</a>
<a class="btn" href="{{ url_for('relation_edit', kind=kind, type=type or None) }}">{{ 'Nieuwe klant' if kind == 'klant' else 'Nieuwe partner' }}</a>
</div>
</div>
<div class="filters">
<a href="{{ url_for('relations', kind=kind, q=term or None) }}" class="{{ 'on' if not type }}">Alle</a>
{% for t in types %}
<a href="{{ url_for('relations', kind=kind, type=t, q=term or None) }}" class="{{ 'on' if type == t }}">{{ t }}{% if counts.get(t) %} <span class="muted">{{ counts[t] }}</span>{% endif %}</a>
{% endfor %}
</div>
<div class="card">
{% if rows %}
<div class="table-wrap"><table>
<thead><tr><th>Naam</th><th>Type</th><th>Contactpersonen</th><th>Plaats</th><th>Contact</th>{% if kind == 'partner' %}<th class="num">Producten</th>{% endif %}</tr></thead>
<tbody>
{% for r in rows %}
<tr class="link" onclick="location='{{ url_for('relation_view', rid=r.id) }}'">
<td><a href="{{ url_for('relation_view', rid=r.id) }}">{{ r.name }}</a></td>
<td>{% if r.type %}<span class="chip">{{ r.type }}</span>{% endif %}</td>
<td>{{ r.member_names or '—' }}{% if r.members > 4 %} <span class="muted">+{{ r.members - 4 }}</span>{% endif %}</td>
<td>{{ r.city }}</td>
<td class="muted">{{ [r.phone, r.email]|select|join(' · ') }}</td>
{% if kind == 'partner' %}<td class="num">{{ r.products or '' }}</td>{% endif %}
</tr>
{% endfor %}
</tbody>
</table></div>
{% else %}<p class="empty">Nog geen {{ 'klantrelaties. Een lead die je omzet naar klant komt hier vanzelf.' if kind == 'klant' else 'partners. Leveranciers uit een productimport komen hier vanzelf.' }}</p>{% endif %}
</div>
{% endblock %}