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:
@@ -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 %}
|
||||
Reference in New Issue
Block a user