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
+133
View File
@@ -0,0 +1,133 @@
{% extends 'base.html' %}
{% block title %}{{ r.name }}{% endblock %}
{% block content %}
<div class="head">
<div>
<a class="label" href="{{ url_for('relations', kind=r.kind) }}">← {{ 'Klanten' if r.kind == 'klant' else 'Partners' }}</a>
<h1>{{ r.name }}</h1>
<div class="muted"><span class="chip">{{ RKIND.get(r.kind) }}</span> {{ [r.type, r.city]|select|join(' · ') }}</div>
</div>
<div class="actions">
{% if r.kind == 'klant' and members %}
<form method="post" action="{{ url_for('quote_new') }}" class="inline">
<input type="hidden" name="contact_id" value="{{ members[0].id }}">
<button class="btn ghost">Nieuwe offerte</button>
</form>
{% endif %}
<a class="btn" href="{{ url_for('relation_edit', rid=r.id) }}">Bewerken</a>
</div>
</div>
<div class="two">
<div>
<div class="card">
<h2>Contactpersonen</h2>
<div class="members">
{% for m in members %}
<div class="member">
<div>
<a href="{{ url_for('contact_view', cid=m.id) }}">{{ m.name }}</a>
{% if m.relation_role %}<span class="muted">· {{ m.relation_role }}</span>{% endif %}
<div class="muted">{{ [m.email, m.phone]|select|join(' · ') }}</div>
</div>
<div class="actions" style="margin:0">
{% if m.type != 'partner' %}<span class="chip {{ m.status }}">{{ STATUS.get(m.status) }}</span>{% endif %}
<form method="post" action="{{ url_for('relation_member_remove', rid=r.id, cid=m.id) }}" class="inline" onsubmit="return confirm('{{ m.name }} loskoppelen van {{ r.name }}?')">
<button class="linkbtn">Loskoppelen</button>
</form>
</div>
</div>
{% else %}
<p class="empty">Nog geen contactpersonen.</p>
{% endfor %}
</div>
<form method="post" action="{{ url_for('relation_member', rid=r.id) }}" class="actions">
{% if candidates %}
<select name="contact_id" style="width:auto;flex:1;min-width:180px" aria-label="Bestaand contact" required>
<option value="">Bestaand contact toevoegen…</option>
{% for c in candidates %}<option value="{{ c.id }}">{{ c.name }}{% if c.email %} ({{ c.email }}){% endif %}</option>{% endfor %}
</select>
<input name="relation_role" placeholder="Rol" style="width:160px" aria-label="Rol">
<button class="btn ghost small">Toevoegen</button>
{% endif %}
<a class="btn small" href="{{ url_for('contact_new', relation_id=r.id, company=r.name if r.kind == 'partner' else None) }}">Nieuwe contactpersoon</a>
</form>
</div>
{% if r.kind == 'partner' %}
<div class="card">
<h2>Producten van deze leverancier</h2>
{% if products %}
<div class="table-wrap"><table>
<thead><tr><th>SKU</th><th>Naam</th><th class="num">Prijs excl.</th><th>Bijgewerkt</th></tr></thead>
<tbody>
{% for p in products %}
<tr class="link" onclick="location='{{ url_for('product_edit', pid=p.id) }}'">
<td><code>{{ p.sku }}</code></td>
<td><a href="{{ url_for('product_edit', pid=p.id) }}">{{ p.name }}</a>{% if not p.active %} <span class="chip verloren">Inactief</span>{% endif %}</td>
<td class="num">{{ p.price|euro }}</td>
<td class="muted">{{ (p.updated_at or p.created_at)|dt('%d-%m-%Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% else %}<p class="empty">Nog geen producten gekoppeld. Vul deze naam in als leverancier bij een product of in een import.</p>{% endif %}
</div>
{% endif %}
<div class="card">
<h2>Recente activiteit</h2>
<ul class="timeline">
{% for a in activities %}
<li>
<div class="meta">{{ ACT.get(a.type, a.type) }} · <a href="{{ url_for('contact_view', cid=a.contact_id) }}">{{ a.contact_name }}</a> · {{ a.created_at|dt }}{% if a.user_name %} · {{ a.user_name }}{% endif %}</div>
<div class="body">{{ a.body }}</div>
</li>
{% else %}
<li class="empty">Nog geen activiteit.</li>
{% endfor %}
</ul>
</div>
</div>
<div>
<div class="card">
<h2>Gegevens</h2>
<dl class="props">
{% if r.type %}<dt>Type</dt><dd>{{ r.type }}</dd>{% endif %}
{% if r.email %}<dt>E-mail</dt><dd><a href="mailto:{{ r.email }}">{{ r.email }}</a></dd>{% endif %}
{% if r.phone %}<dt>Telefoon</dt><dd><a href="tel:{{ r.phone }}">{{ r.phone }}</a></dd>{% endif %}
{% if r.address or r.city %}<dt>Adres</dt><dd>{{ [r.address, (r.postcode ~ ' ' ~ r.city)|trim]|select|join('\n') }}</dd>{% endif %}
{% if r.website %}<dt>Website</dt><dd><a href="{{ r.website if r.website.startswith('http') else 'https://' ~ r.website }}" target="_blank" rel="noopener">{{ r.website }}</a></dd>{% endif %}
{% if r.kvk %}<dt>KvK</dt><dd>{{ r.kvk }}</dd>{% endif %}
<dt>Aangemaakt</dt><dd>{{ r.created_at|dt('%d-%m-%Y') }}</dd>
</dl>
{% if r.notes %}<h3>Notities</h3><p style="white-space:pre-line;margin:0">{{ r.notes }}</p>{% endif %}
</div>
{% if tasks %}
<div class="card">
<h2>Open taken</h2>
{% for t in tasks %}
<form method="post" action="{{ url_for('task_done', tid=t.id) }}" class="task">
<button class="check" aria-label="Afronden"></button>
<div><div>{{ t.title }}</div><div class="muted">{{ t.contact_name }}{% if t.due_date %} · <span class="{{ 'overdue' if t.due_date < today }}">{{ t.due_date|dt('%d-%m-%Y') }}</span>{% endif %}</div></div>
</form>
{% endfor %}
</div>
{% endif %}
{% if r.kind == 'klant' %}
<div class="card">
<h2>Offertes</h2>
{% for qt in quotes %}
<p style="margin:0 0 .6rem"><a href="{{ url_for('quote_edit', qid=qt.id) }}">{{ qt.number }}</a> <span class="chip {{ qt.status }}">{{ QSTATUS.get(qt.status) }}</span><br>
<span class="muted">{{ qt.contact_name }} · {{ qt.date|dt('%d-%m-%Y') }} · {{ qt.total|euro }}</span></p>
{% else %}
<p class="empty">Nog geen offertes.</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endblock %}