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
+54
View File
@@ -0,0 +1,54 @@
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}CRM{% endblock %} · Van Emous CRM</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kantumruy+Pro:wght@500;600&family=Mukta:wght@400;600;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script>
/* inhoud verbergen tot de GSAP-intro klaar staat; na 3 s altijd zichtbaar */
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.documentElement.classList.add('fx-wait');
setTimeout(() => document.documentElement.classList.remove('fx-wait'), 3000);
}
</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>
</head>
<body>
{% block body %}
{% set ep = request.endpoint or '' %}
<aside class="side">
<a href="{{ url_for('dashboard') }}" class="brand"><img src="{{ url_for('static', filename='logo.svg') }}" alt="Van Emous"></a>
{% set ctype = request.args.get('type') or (c.type if c is defined and c and c.type is defined else '') %}
{% set rkind = request.args.get('kind') or (r.kind if r is defined and r and r.kind is defined else '') %}
<nav>
<a href="{{ url_for('dashboard') }}" class="{{ 'active' if ep == 'dashboard' }}">Dashboard</a>
<a href="{{ url_for('contacts', type='lead') }}" class="{{ 'active' if ep.startswith('contact') and ctype == 'lead' }}">Leads</a>
<a href="{{ url_for('relations', kind='klant') }}" class="{{ 'active' if ep.startswith('relation') and rkind == 'klant' }}">Klanten</a>
<a href="{{ url_for('relations', kind='partner') }}" class="{{ 'active' if ep.startswith('relation') and rkind == 'partner' }}">Partners</a>
<a href="{{ url_for('contacts') }}" class="{{ 'active' if ep.startswith('contact') and ctype != 'lead' }}">Contactpersonen</a>
<a href="{{ url_for('inbox') }}" class="{{ 'active' if ep.startswith('inbox') }}">Inbox {% if unread %}<span class="count">{{ unread }}</span>{% endif %}</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>
{% endif %}
</nav>
<a href="{{ url_for('account') }}" class="logout">{{ g.user.name }}</a>
<a href="{{ url_for('logout') }}" class="logout">Uitloggen</a>
</aside>
<main>
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat, m in msgs %}<div class="flash {{ cat }}">{{ m }}</div>{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</main>
{% endblock %}
</body>
</html>