v0.0.2: pre-productie (Bootstrap 5.3.3, LightPdf klant-PDF, server-setup en release-script)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bffa053c05
commit
7d302dc405
@@ -2,12 +2,12 @@
|
||||
{% block title %}Mijn account{% endblock %}
|
||||
{% block content %}
|
||||
<div class="head"><div><div class="kicker">{{ ROLE.get(g.user.role) }}</div><h1>{{ g.user.name }}</h1><div class="muted">{{ g.user.email }}</div></div></div>
|
||||
<form method="post" class="card" style="max-width:560px">
|
||||
<form method="post" class="card" style="max-width:560px"><div class="card-body">
|
||||
<h2>Wachtwoord wijzigen</h2>
|
||||
<div class="fields">
|
||||
<div class="full"><label for="old">Huidig wachtwoord</label><input id="old" type="password" name="old_password" required></div>
|
||||
<div class="full"><label for="new">Nieuw wachtwoord</label><input id="new" type="password" name="new_password" minlength="8" required autocomplete="new-password"></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label" for="old">Huidig wachtwoord</label><input class="form-control" id="old" type="password" name="old_password" required></div>
|
||||
<div class="col-12"><label class="form-label" for="new">Nieuw wachtwoord</label><input class="form-control" id="new" type="password" name="new_password" minlength="8" required autocomplete="new-password"></div>
|
||||
</div>
|
||||
<div class="actions"><button class="btn">Opslaan</button></div>
|
||||
</form>
|
||||
<div class="actions"><button class="btn btn-primary">Opslaan</button></div>
|
||||
</div></form>
|
||||
{% endblock %}
|
||||
|
||||
+17
-13
@@ -2,32 +2,36 @@
|
||||
{% set titles = {'setup': 'Welkom', 'login': 'Inloggen', 'portal_login': 'Klantportaal', 'invite': 'Wachtwoord instellen'} %}
|
||||
{% block title %}{{ titles[mode] }}{% endblock %}
|
||||
{% block body %}
|
||||
<div class="auth-wrap">
|
||||
<div class="auth">
|
||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Van Emous">
|
||||
{% with msgs = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, m in msgs %}<div class="flash {{ cat }}">{{ m }}</div>{% endfor %}
|
||||
{% endwith %}
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<form method="post">
|
||||
<h1>{{ titles[mode] }}</h1>
|
||||
<div class="row g-3">
|
||||
{% if mode == 'setup' %}
|
||||
<p class="muted">Maak het eerste beheerdersaccount aan.</p>
|
||||
<label for="name">Naam</label><input id="name" name="name" required autofocus>
|
||||
<label for="email" style="margin-top:1rem">E-mailadres</label><input id="email" type="email" name="email" required>
|
||||
<p class="muted mb-0">Maak het eerste beheerdersaccount aan.</p>
|
||||
<div class="col-12"><label class="form-label" for="name">Naam</label><input class="form-control" id="name" name="name" required autofocus></div>
|
||||
<div class="col-12"><label class="form-label" for="email">E-mailadres</label><input class="form-control" id="email" type="email" name="email" required></div>
|
||||
{% elif mode == 'invite' %}
|
||||
<p class="muted">Welkom {{ c.name }}. Kies een wachtwoord voor het klantportaal. Daarna logt u in met {{ c.email }}.</p>
|
||||
<p class="muted mb-0">Welkom {{ c.name }}. Kies een wachtwoord voor het klantportaal. Daarna logt u in met {{ c.email }}.</p>
|
||||
{% else %}
|
||||
{% if mode == 'portal_login' %}<p class="muted">Bekijk uw offertes en deel bestanden met ons.</p>{% endif %}
|
||||
<label for="email">E-mailadres</label><input id="email" type="email" name="email" required autofocus>
|
||||
{% if mode == 'portal_login' %}<p class="muted mb-0">Bekijk uw offertes en deel bestanden met ons.</p>{% endif %}
|
||||
<div class="col-12"><label class="form-label" for="email">E-mailadres</label><input class="form-control" id="email" type="email" name="email" required autofocus autocomplete="username"></div>
|
||||
{% endif %}
|
||||
<label for="pw" style="margin-top:1rem">Wachtwoord</label>
|
||||
<input id="pw" type="password" name="password" {{ 'minlength=8' if mode in ('setup', 'invite') }} required>
|
||||
<div class="col-12"><label class="form-label" for="pw">Wachtwoord</label>
|
||||
<input class="form-control" id="pw" type="password" name="password" {{ 'minlength=8' if mode in ('setup', 'invite') }} required autocomplete="{{ 'new-password' if mode in ('setup', 'invite') else 'current-password' }}"></div>
|
||||
{% if mode in ('setup', 'invite') %}
|
||||
<label for="pw2" style="margin-top:1rem">Herhaal wachtwoord</label>
|
||||
<input id="pw2" type="password" name="password2" minlength="8" required>
|
||||
<div class="col-12"><label class="form-label" for="pw2">Herhaal wachtwoord</label>
|
||||
<input class="form-control" id="pw2" type="password" name="password2" minlength="8" required autocomplete="new-password"></div>
|
||||
{% endif %}
|
||||
<div class="actions"><button class="btn">{{ 'Inloggen' if mode in ('login', 'portal_login') else 'Opslaan en doorgaan' }}</button></div>
|
||||
</div>
|
||||
<div class="actions"><button class="btn btn-primary w-100">{{ 'Inloggen' if mode in ('login', 'portal_login') else 'Opslaan en doorgaan' }}</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+47
-26
@@ -7,6 +7,7 @@
|
||||
<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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<script>
|
||||
/* inhoud verbergen tot de GSAP-intro klaar staat; na 3 s altijd zichtbaar */
|
||||
@@ -15,40 +16,60 @@
|
||||
setTimeout(() => document.documentElement.classList.remove('fx-wait'), 3000);
|
||||
}
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous" defer></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 '' %}
|
||||
<div class="app">
|
||||
<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>
|
||||
<div class="offcanvas-lg offcanvas-start side-panel" id="sidenav" tabindex="-1" aria-label="Menu">
|
||||
<div class="offcanvas-header">
|
||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Van Emous">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#sidenav" aria-label="Menu sluiten"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<a href="{{ url_for('dashboard') }}" class="brand d-none d-lg-block"><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>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<div class="app-main">
|
||||
<header class="topbar d-lg-none">
|
||||
<button class="menu-btn" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidenav" aria-controls="sidenav">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>Menu
|
||||
</button>
|
||||
<a href="{{ url_for('dashboard') }}"><img src="{{ url_for('static', filename='logo.svg') }}" alt="Van Emous"></a>
|
||||
{% if unread %}<a href="{{ url_for('inbox') }}" class="count" aria-label="{{ unread }} ongelezen e-mail">{{ unread }}</a>{% endif %}
|
||||
</header>
|
||||
<main class="content">
|
||||
{% 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>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{# De engine haalt kosten/*.csv en assets/ relatief op: base = de gekopieerde configurator. Alle CRM-links zijn absoluut. #}
|
||||
<base href="{{ url_for('static', filename='configurator/') }}">
|
||||
<title>Bestek · Offerte {{ qt.number }} · 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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<style>
|
||||
.content { margin: 0 auto; max-width: 980px; }
|
||||
.bs-ch { margin-bottom: 1.5rem; }
|
||||
.bs-ch-head { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; border-bottom: 2px solid var(--ink); padding-bottom: 0.3rem; margin-bottom: 0.25rem; }
|
||||
.bs-ch-head h2 { margin: 0; font-size: 1.1rem; }
|
||||
.bs-ch table { width: 100%; border-collapse: collapse; }
|
||||
.bs-ch td { padding: 0.35rem 0.25rem; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||
.bs-ch td.num { text-align: right; white-space: nowrap; width: 9rem; }
|
||||
.bs-info td { color: var(--ink-muted); font-style: italic; }
|
||||
.bs-ch td.num.muted { width: 7rem; }
|
||||
.bs-ch .chip { text-transform: none; letter-spacing: 0; font-weight: 400; font-size: 0.7rem; margin-left: 0.3rem; }
|
||||
.bs-total { margin-left: auto; width: 320px; max-width: 100%; }
|
||||
.bs-total div { display: flex; justify-content: space-between; padding: 0.2rem 0; }
|
||||
.bs-total .grand { border-top: 2px solid var(--ink); margin-top: 0.3rem; padding-top: 0.5rem; font-family: var(--font-heading); font-size: 1.15rem; }
|
||||
.bs-opts .form-check { margin-bottom: 0.4rem; }
|
||||
@media (max-width: 640px) { .bs-ch td.num { width: auto; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="content">
|
||||
<div class="head">
|
||||
<div>
|
||||
<a class="label" href="{{ url_for('quote_edit', qid=qt.id) }}">← Offerte {{ qt.number }}</a>
|
||||
<h1>Bestek uit ontwerp</h1>
|
||||
<div class="muted">{{ design_file.title }}{% if c %} · {{ c.name }}{% endif %} · ontvangen {{ design_file.created_at|dt }}</div>
|
||||
</div>
|
||||
{% if designs|length > 1 %}
|
||||
<form method="get" class="actions" action="{{ url_for('quote_bestek', qid=qt.id) }}">
|
||||
<select class="form-select w-auto" name="file" onchange="this.form.submit()" aria-label="Ontwerp">
|
||||
{% for d in designs %}<option value="{{ d.id }}" {{ 'selected' if d.id == design_file.id }}>{{ d.title }} · {{ d.created_at|dt('%d-%m-%Y') }}</option>{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="bestek">
|
||||
<div class="card"><div class="card-body">
|
||||
<p class="muted">Hoeveelheden en prijzen uit de configurator (zelfde berekening als Kosten & tijden: kostprijs met
|
||||
algemene kosten en marge, prijzen uit de kosten-CSV's). Bedragen excl. btw. Schroeven, zaag- en boorwerk staan per hoofdstuk samen onder "Bevestiging en bewerking".
|
||||
Posten met een code worden gekoppeld aan het product met die SKU (bestaat het nog niet, dan wordt het aangemaakt).</p>
|
||||
<div class="bs-body">{% if engine %}<p class="empty">Bestek berekenen…</p>{% else %}<p class="empty">De 3D-engine van de configurator ontbreekt. Draai tools/configurator-sync.sh in de CRM-map.</p>{% endif %}</div>
|
||||
<div class="bs-total"></div>
|
||||
</div></div>
|
||||
|
||||
<form method="post" action="{{ url_for('quote_bestek_save', qid=qt.id) }}" class="card bs-opts"><div class="card-body">
|
||||
<input type="hidden" name="bestek">
|
||||
<input type="hidden" name="file_id" value="{{ design_file.id }}">
|
||||
<h2>In de offerte zetten</h2>
|
||||
<div class="form-check"><input class="form-check-input" type="radio" id="mode-post" name="mode" value="post" checked><label class="form-check-label" for="mode-post"><strong>Per post</strong>: elke regel van het bestek als offerteregel met aantal en prijs per eenheid (bijv. 42 st. × prijs per spot)</label></div>
|
||||
<div class="form-check"><input class="form-check-input" type="radio" id="mode-ch" name="mode" value="hoofdstuk"><label class="form-check-label" for="mode-ch"><strong>Per hoofdstuk</strong>: één regel per hoofdstuk (1 post) met de posten in de omschrijving</label></div>
|
||||
<div class="form-check"><input class="form-check-input" type="checkbox" id="opt-replace" name="replace" value="1" {{ 'checked' if has_lines }}><label class="form-check-label" for="opt-replace">Bestaande regels van de offerte vervangen{% if has_lines %} ({{ has_lines }} regel{{ 's' if has_lines != 1 }}){% endif %}</label></div>
|
||||
<div class="form-check"><input class="form-check-input" type="checkbox" id="opt-prices" name="update_prices" value="1"><label class="form-check-label" for="opt-prices">Prijzen van bestaande producten bijwerken naar deze berekening (wordt vastgelegd in de producthistorie)</label></div>
|
||||
<p class="hint">Het bestek (alle posten met hoeveelheden) komt als bijlage bij de offerte, ook in de afdrukversie en het klantportaal.</p>
|
||||
<div class="actions"><button class="btn btn-primary" type="submit" disabled>In offerte zetten</button>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('quote_edit', qid=qt.id) }}">Annuleren</a></div>
|
||||
</div></form>
|
||||
</div>
|
||||
</main>
|
||||
{% if engine %}
|
||||
<script type="application/json" id="design">{{ design|tojson }}</script>
|
||||
<script type="application/json" id="bestek-meta">{{ {'design': {'file_id': design_file.id, 'title': design_file.title}, 'configurator': design.get('configurator') or ''}|tojson }}</script>
|
||||
{% for src in scripts %}<script src="{{ src }}"></script>
|
||||
{% endfor %}<script src="{{ url_for('static', filename='bestek.js') }}"></script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
+125
-84
@@ -1,25 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ c.name }}{% if c.type == 'lead' %}
|
||||
<dialog id="convert" class="dialog">
|
||||
<form method="post" action="{{ url_for('contact_convert', cid=c.id) }}">
|
||||
<h2>Omzetten naar klant</h2>
|
||||
<p class="muted">De contactpersoon komt in een klantrelatie, bijvoorbeeld een huishouden of familie.</p>
|
||||
<label><input type="radio" name="mode" value="new" checked onchange="this.form.relation_id.disabled=true;this.form.new_relation.disabled=false"> Nieuwe klantrelatie</label>
|
||||
<input name="new_relation" value="{{ c.company or ('Familie ' ~ c.name.split(' ')[-1]) }}" aria-label="Naam relatie" style="margin:.4rem 0 1rem">
|
||||
{% if klant_relations %}
|
||||
<label><input type="radio" name="mode" value="existing" onchange="this.form.relation_id.disabled=false;this.form.new_relation.disabled=true"> Bestaande klantrelatie</label>
|
||||
<select name="relation_id" disabled aria-label="Bestaande relatie" style="margin-top:.4rem">
|
||||
{% for r in klant_relations %}<option value="{{ r.id }}">{{ r.name }}{% if r.type %} · {{ r.type }}{% endif %}</option>{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<button class="btn">Omzetten</button>
|
||||
<button class="btn quiet" type="button" onclick="this.closest('dialog').close()">Annuleren</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block title %}{{ c.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="head">
|
||||
<div>
|
||||
@@ -30,24 +10,77 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<form method="post" action="{{ url_for('contact_status', cid=c.id) }}" class="inline">
|
||||
<select name="status" onchange="this.form.submit()" aria-label="Status" style="width:auto">
|
||||
<select class="form-select w-auto" name="status" onchange="this.form.submit()" aria-label="Status">
|
||||
{% for k, l in STATUSES %}<option value="{{ k }}" {{ 'selected' if c.status == k }}>{{ l }}</option>{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
{% if c.type == 'lead' %}
|
||||
<button class="btn ghost" type="button" onclick="document.getElementById('convert').showModal()">Omzetten naar klant</button>
|
||||
<button class="btn btn-outline-primary" type="button" onclick="document.getElementById('convert').showModal()">Omzetten naar klant</button>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('quote_new') }}" class="inline">
|
||||
<input type="hidden" name="contact_id" value="{{ c.id }}">
|
||||
<button class="btn ghost">Nieuwe offerte</button>
|
||||
<button class="btn btn-outline-primary">Nieuwe offerte</button>
|
||||
</form>
|
||||
<a class="btn" href="{{ url_for('contact_edit', cid=c.id) }}">Bewerken</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('contact_edit', cid=c.id) }}">Bewerken</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="two">
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="row gx-4 stack">
|
||||
<div class="col-lg-7 col-xl-8">
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="card-top">
|
||||
<h2>Gekozen model</h2>
|
||||
{% if design %}
|
||||
<div class="actions">
|
||||
{% if designs|length > 1 %}
|
||||
<form method="post" action="{{ url_for('contact_design', cid=c.id) }}" class="inline">
|
||||
<select class="form-select form-select-sm w-auto" name="file_id" onchange="this.form.submit()" aria-label="Ander ontwerp kiezen">
|
||||
{% for d in designs %}<option value="{{ d.id }}" {{ 'selected' if d.id == design.id }}>{{ d.title }} · {{ d.created_at|dt('%d-%m-%Y') }}</option>{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('quote_new') }}" class="inline">
|
||||
<input type="hidden" name="contact_id" value="{{ c.id }}"><input type="hidden" name="bestek" value="1">
|
||||
<button class="btn btn-primary btn-sm">Offerte met bestek</button>
|
||||
</form>
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('design_viewer', fid=design.id) }}" target="_blank">Openen</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('upload_file', name=design.stored) }}" download="{{ design.original }}">JSON</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if design %}
|
||||
<div class="viewer-frame"><iframe src="{{ url_for('design_viewer', fid=design.id) }}" title="3D-weergave {{ design.title }}" loading="lazy" allowfullscreen></iframe></div>
|
||||
<p class="muted" style="margin:.75rem 0 0"><strong style="color:var(--ink)">{{ design.title }}</strong>
|
||||
· {{ design_meta.units }} unit{{ 's' if design_meta.units != 1 }}{% if design_meta.address %} · {{ design_meta.address }}{% endif %}
|
||||
· ontvangen {{ design.created_at|dt }}{% if design_meta.version %} · configurator {{ design_meta.version }}{% endif %}</p>
|
||||
{% if design_meta.note %}<p style="white-space:pre-line;margin:.5rem 0 0">{{ design_meta.note }}</p>{% endif %}
|
||||
<p class="pdf-links" style="margin:.5rem 0 0">Tekeningen:
|
||||
{% for k, label in PDF_KINDS.items() %}{% set p = pdfs.get(k) %}
|
||||
<a data-pdf="{{ k }}" href="{{ p.url if p else '#' }}" target="_blank" {{ 'hidden' if not p }}>{{ (label[:1]|upper) ~ label[1:] }}</a><span class="muted" data-pdf-wait="{{ k }}" {{ 'hidden' if p }}>{{ label }} wordt gemaakt…</span>{% if k == 'werk' %} <span class="muted">(intern)</span> ·{% endif %}
|
||||
{% endfor %}
|
||||
<span class="muted">· opnieuw maken via PDF in de weergave</span></p>
|
||||
<script>
|
||||
addEventListener('message', (e) => {
|
||||
if (e.origin !== location.origin || !e.data || e.data.type !== 'tectum-pdf') return;
|
||||
const a = document.querySelector(`[data-pdf="${e.data.kind}"]`), w = document.querySelector(`[data-pdf-wait="${e.data.kind}"]`);
|
||||
if (a) { a.href = e.data.url; a.hidden = false; }
|
||||
if (w) w.hidden = true;
|
||||
});
|
||||
</script>
|
||||
{% elif designs %}
|
||||
<p class="muted">Er is geen model gekozen.</p>
|
||||
<form method="post" action="{{ url_for('contact_design', cid=c.id) }}" class="d-flex flex-wrap gap-2">
|
||||
<select class="form-select w-auto" name="file_id" aria-label="Ontwerp">
|
||||
{% for d in designs %}<option value="{{ d.id }}">{{ d.title }} · {{ d.created_at|dt('%d-%m-%Y') }}</option>{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-outline-primary">Kiezen</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="empty">Nog geen model. Een ontwerp uit de configurator (Bestand › Opslaan, JSON) komt hier vanzelf als je het bij Bestanden uploadt of als het met het formulier meekomt.</p>
|
||||
{% endif %}
|
||||
</div></div>
|
||||
|
||||
<div class="card o-2"><div class="card-body">
|
||||
<h2>Taken</h2>
|
||||
{% for t in tasks %}
|
||||
<form method="post" action="{{ url_for('task_done', tid=t.id) }}" class="task {{ 'done' if t.done }}">
|
||||
@@ -56,46 +89,44 @@
|
||||
<div class="muted">{{ t.user_name or '' }}{% if t.due_date %} · <span class="{{ 'overdue' if not t.done and t.due_date < today }}">{{ t.due_date|dt('%d-%m-%Y') }}</span>{% endif %}</div></div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
<form method="post" action="{{ url_for('contact_task', cid=c.id) }}" style="margin-top:1rem">
|
||||
<div class="inline-fields">
|
||||
<input name="title" placeholder="Bijv. terugbellen, offerte nabellen" required aria-label="Taak">
|
||||
<input type="date" name="due_date" aria-label="Datum">
|
||||
</div>
|
||||
<div class="actions" style="margin-top:.5rem">
|
||||
<select name="user_id" style="width:auto" aria-label="Voor">
|
||||
<form method="post" action="{{ url_for('contact_task', cid=c.id) }}" class="row g-2 mt-2">
|
||||
<div class="col-12 col-md"><input class="form-control" name="title" placeholder="Bijv. terugbellen, offerte nabellen" required aria-label="Taak"></div>
|
||||
<div class="col-6 col-md-auto"><input class="form-control" type="date" name="due_date" aria-label="Datum"></div>
|
||||
<div class="col-6 col-md-auto">
|
||||
<select class="form-select" name="user_id" aria-label="Voor">
|
||||
{% for u in USERS %}<option value="{{ u.id }}" {{ 'selected' if u.id == g.user.id }}>{{ u.name }}</option>{% endfor %}
|
||||
</select>
|
||||
<button class="btn ghost small">Taak toevoegen</button>
|
||||
</div>
|
||||
<div class="col-12 col-md-auto"><button class="btn btn-outline-primary w-100">Taak toevoegen</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card o-3"><div class="card-body">
|
||||
<h2>Notitie toevoegen</h2>
|
||||
<form method="post" action="{{ url_for('contact_note', cid=c.id) }}">
|
||||
<textarea name="body" required placeholder="Gebeld, afspraak gemaakt, wensen…" aria-label="Notitie"></textarea>
|
||||
<div class="actions"><button class="btn">Opslaan</button></div>
|
||||
<textarea class="form-control" name="body" required placeholder="Gebeld, afspraak gemaakt, wensen…" aria-label="Notitie"></textarea>
|
||||
<div class="actions"><button class="btn btn-primary">Opslaan</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
{% if c.email %}
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<details>
|
||||
<summary>E-mail sturen</summary>
|
||||
<form method="post" action="{{ url_for('mail_send') }}">
|
||||
<input type="hidden" name="contact_id" value="{{ c.id }}">
|
||||
<div class="fields">
|
||||
<div class="full"><label>Aan</label><input type="email" name="to" value="{{ c.email }}" required></div>
|
||||
<div class="full"><label>Onderwerp</label><input name="subject" required></div>
|
||||
<div class="full"><label>Bericht</label><textarea name="body" rows="8" required></textarea></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label" for="m-to">Aan</label><input class="form-control" id="m-to" type="email" name="to" value="{{ c.email }}" required></div>
|
||||
<div class="col-12"><label class="form-label" for="m-subject">Onderwerp</label><input class="form-control" id="m-subject" name="subject" required></div>
|
||||
<div class="col-12"><label class="form-label" for="m-body">Bericht</label><textarea class="form-control" id="m-body" name="body" rows="8" required></textarea></div>
|
||||
</div>
|
||||
<div class="actions"><button class="btn">Versturen</button></div>
|
||||
<div class="actions"><button class="btn btn-primary">Versturen</button></div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
</div></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Tijdlijn</h2>
|
||||
<ul class="timeline">
|
||||
{% for a in activities %}
|
||||
@@ -107,28 +138,28 @@
|
||||
<li class="empty">Nog geen activiteit.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
{% if emails %}
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>E-mail</h2>
|
||||
<div class="table-wrap"><table>
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<tbody>
|
||||
{% for e in emails %}
|
||||
<tr class="link" onclick="location='{{ url_for('inbox_view', eid=e.id) }}'">
|
||||
<td><span class="chip">{{ 'Ontvangen' if e.direction == 'in' else 'Verstuurd' }}</span></td>
|
||||
<td><a href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a></td>
|
||||
<td class="muted num">{{ e.date|dt }}</td>
|
||||
<td class="stack-title"><a href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a></td>
|
||||
<td data-label="Richting"><span class="chip">{{ 'Ontvangen' if e.direction == 'in' else 'Verstuurd' }}</span></td>
|
||||
<td data-label="Datum" class="muted num">{{ e.date|dt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="col-lg-5 col-xl-4">
|
||||
<div class="card o-1"><div class="card-body">
|
||||
<h2>Gegevens</h2>
|
||||
<dl class="props">
|
||||
<dt>Status</dt><dd><span class="chip {{ c.status }}">{{ STATUS.get(c.status, c.status) }}</span></dd>
|
||||
@@ -151,19 +182,23 @@
|
||||
{% for k, v in extra.items() %}<dt>{{ k }}</dt><dd>{{ v if v is string else v|tojson }}</dd>{% endfor %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Bestanden</h2>
|
||||
{% if files %}
|
||||
<div class="files">
|
||||
{% for f in files %}
|
||||
<div class="file">
|
||||
<a href="{{ url_for('upload_file', name=f.stored) }}" target="_blank" class="thumb">
|
||||
<a href="{{ url_for('design_viewer', fid=f.id) if f.title else url_for('upload_file', name=f.stored) }}" target="_blank" class="thumb">
|
||||
{% if f.mime.startswith('image/') %}<img src="{{ url_for('upload_file', name=f.stored) }}" alt="{{ f.original }}" loading="lazy">
|
||||
{% else %}{{ f.original.rsplit('.', 1)[-1][:4] if '.' in f.original else 'bestand' }}{% endif %}
|
||||
{% elif f.title %}3D{% else %}{{ f.original.rsplit('.', 1)[-1][:4] if '.' in f.original else 'bestand' }}{% endif %}
|
||||
</a>
|
||||
<div class="name" title="{{ f.original }}">{{ f.original }}</div>
|
||||
<div class="name" title="{{ f.original }}">{{ f.title or f.original }}</div>
|
||||
{% if f.kind == 'werk' %}<div class="muted">Intern, niet in portaal</div>{% endif %}
|
||||
{% if f.title %}{% if c.design_file_id == f.id %}<div class="muted">Gekozen model</div>{% else %}
|
||||
<form method="post" action="{{ url_for('contact_design', cid=c.id) }}"><input type="hidden" name="file_id" value="{{ f.id }}"><button class="linkbtn">Kies als model</button></form>
|
||||
{% endif %}{% endif %}
|
||||
<form method="post" action="{{ url_for('file_delete', fid=f.id) }}" onsubmit="return confirm('Bestand verwijderen?')">
|
||||
<button class="linkbtn">Verwijderen</button>
|
||||
</form>
|
||||
@@ -172,12 +207,12 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('contact_upload', cid=c.id) }}" enctype="multipart/form-data">
|
||||
<input type="file" name="files" multiple required aria-label="Bestanden kiezen">
|
||||
<div class="actions"><button class="btn ghost small">Uploaden</button></div>
|
||||
<input class="form-control" type="file" name="files" multiple required aria-label="Bestanden kiezen">
|
||||
<div class="actions"><button class="btn btn-outline-primary btn-sm">Uploaden</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<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>
|
||||
@@ -185,27 +220,27 @@
|
||||
{% else %}
|
||||
<p class="empty">Nog geen offertes.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Klantportaal</h2>
|
||||
{% if c.portal_password_hash %}
|
||||
<p class="muted" style="margin-top:0">Actief{% if c.portal_last_login %} · laatst ingelogd {{ c.portal_last_login|dt }}{% endif %}</p>
|
||||
<p class="muted">Actief{% if c.portal_last_login %} · laatst ingelogd {{ c.portal_last_login|dt }}{% endif %}</p>
|
||||
{% elif portal_link %}
|
||||
<p class="muted" style="margin-top:0">Uitnodiging klaar, nog niet geactiveerd:</p>
|
||||
<p class="muted">Uitnodiging klaar, nog niet geactiveerd:</p>
|
||||
<p><code>{{ portal_link }}</code></p>
|
||||
{% else %}
|
||||
<p class="muted" style="margin-top:0">De klant kan via het portaal offertes bekijken en accepteren, en bestanden en berichten sturen.</p>
|
||||
<p class="muted">De klant kan via het portaal offertes bekijken en accepteren, en bestanden en berichten sturen.</p>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('contact_portal', cid=c.id) }}" class="actions">
|
||||
{% if portal_link and c.email %}<button class="btn small" name="action" value="mail">Mail uitnodiging</button>{% endif %}
|
||||
{% if not c.portal_password_hash %}<button class="btn ghost small" name="action" value="invite">{{ 'Nieuwe link' if portal_link else 'Uitnodiging maken' }}</button>{% endif %}
|
||||
{% if c.portal_password_hash or portal_link %}<button class="btn quiet small" name="action" value="revoke">Toegang intrekken</button>{% endif %}
|
||||
{% if portal_link and c.email %}<button class="btn btn-primary btn-sm" name="action" value="mail">Mail uitnodiging</button>{% endif %}
|
||||
{% if not c.portal_password_hash %}<button class="btn btn-outline-primary btn-sm" name="action" value="invite">{{ 'Nieuwe link' if portal_link else 'Uitnodiging maken' }}</button>{% endif %}
|
||||
{% if c.portal_password_hash or portal_link %}<button class="btn btn-outline-secondary btn-sm" name="action" value="revoke">Toegang intrekken</button>{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<form method="post" action="{{ url_for('contact_delete', cid=c.id) }}" onsubmit="return confirm('Contact met alle notities en bestanden verwijderen?')">
|
||||
<button class="btn quiet small">Contact verwijderen</button>
|
||||
<form method="post" action="{{ url_for('contact_delete', cid=c.id) }}" class="mb-4" onsubmit="return confirm('Contact met alle notities en bestanden verwijderen?')">
|
||||
<button class="btn btn-outline-secondary btn-sm">Contact verwijderen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,17 +249,23 @@
|
||||
<form method="post" action="{{ url_for('contact_convert', cid=c.id) }}">
|
||||
<h2>Omzetten naar klant</h2>
|
||||
<p class="muted">De contactpersoon komt in een klantrelatie, bijvoorbeeld een huishouden of familie.</p>
|
||||
<label><input type="radio" name="mode" value="new" checked onchange="this.form.relation_id.disabled=true;this.form.new_relation.disabled=false"> Nieuwe klantrelatie</label>
|
||||
<input name="new_relation" value="{{ c.company or ('Familie ' ~ c.name.split(' ')[-1]) }}" aria-label="Naam relatie" style="margin:.4rem 0 1rem">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="mode-new" name="mode" value="new" checked onchange="this.form.relation_id && (this.form.relation_id.disabled=true);this.form.new_relation.disabled=false">
|
||||
<label class="form-check-label" for="mode-new">Nieuwe klantrelatie</label>
|
||||
</div>
|
||||
<input class="form-control" name="new_relation" value="{{ c.company or ('Familie ' ~ c.name.split(' ')[-1]) }}" aria-label="Naam relatie" style="margin:.4rem 0 1rem">
|
||||
{% if klant_relations %}
|
||||
<label><input type="radio" name="mode" value="existing" onchange="this.form.relation_id.disabled=false;this.form.new_relation.disabled=true"> Bestaande klantrelatie</label>
|
||||
<select name="relation_id" disabled aria-label="Bestaande relatie" style="margin-top:.4rem">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="mode-existing" name="mode" value="existing" onchange="this.form.relation_id.disabled=false;this.form.new_relation.disabled=true">
|
||||
<label class="form-check-label" for="mode-existing">Bestaande klantrelatie</label>
|
||||
</div>
|
||||
<select class="form-select" name="relation_id" disabled aria-label="Bestaande relatie" style="margin-top:.4rem">
|
||||
{% for r in klant_relations %}<option value="{{ r.id }}">{{ r.name }}{% if r.type %} · {{ r.type }}{% endif %}</option>{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<button class="btn">Omzetten</button>
|
||||
<button class="btn quiet" type="button" onclick="this.closest('dialog').close()">Annuleren</button>
|
||||
<button class="btn btn-primary">Omzetten</button>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="this.closest('dialog').close()">Annuleren</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
+24
-24
@@ -7,33 +7,33 @@
|
||||
<h1>{{ ('Nieuwe ' ~ TYPE.get(c.type, 'lead')|lower) if new else 'Contact bewerken' }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" class="card">
|
||||
<div class="fields">
|
||||
<div><label for="name">Naam *</label><input id="name" name="name" value="{{ c.name }}" required></div>
|
||||
<div><label for="company">Bedrijf</label><input id="company" name="company" value="{{ c.company }}"></div>
|
||||
<div><label for="email">E-mail</label><input id="email" type="email" name="email" value="{{ c.email }}"></div>
|
||||
<div><label for="phone">Telefoon</label><input id="phone" name="phone" value="{{ c.phone }}"></div>
|
||||
<div class="full"><label for="address">Adres</label><input id="address" name="address" value="{{ c.address }}"></div>
|
||||
<div><label for="postcode">Postcode</label><input id="postcode" name="postcode" value="{{ c.postcode }}"></div>
|
||||
<div><label for="city">Plaats</label><input id="city" name="city" value="{{ c.city }}"></div>
|
||||
<div><label for="status">Status</label>
|
||||
<select id="status" name="status">
|
||||
<form method="post" class="card"><div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label" for="name">Naam *</label><input class="form-control" id="name" name="name" value="{{ c.name }}" required></div>
|
||||
<div class="col-md-6"><label class="form-label" for="company">Bedrijf</label><input class="form-control" id="company" name="company" value="{{ c.company }}"></div>
|
||||
<div class="col-md-6"><label class="form-label" for="email">E-mail</label><input class="form-control" id="email" type="email" name="email" value="{{ c.email }}"></div>
|
||||
<div class="col-md-6"><label class="form-label" for="phone">Telefoon</label><input class="form-control" id="phone" type="tel" name="phone" value="{{ c.phone }}"></div>
|
||||
<div class="col-12"><label class="form-label" for="address">Adres</label><input class="form-control" id="address" name="address" value="{{ c.address }}"></div>
|
||||
<div class="col-sm-4"><label class="form-label" for="postcode">Postcode</label><input class="form-control" id="postcode" name="postcode" value="{{ c.postcode }}"></div>
|
||||
<div class="col-sm-8"><label class="form-label" for="city">Plaats</label><input class="form-control" id="city" name="city" value="{{ c.city }}"></div>
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="status">Status</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
{% for k, l in STATUSES %}<option value="{{ k }}" {{ 'selected' if c.status == k }}>{{ l }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div><label for="type">Soort</label>
|
||||
<select id="type" name="type">
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="type">Soort</label>
|
||||
<select class="form-select" id="type" name="type">
|
||||
{% for k, l in TYPES %}<option value="{{ k }}" {{ 'selected' if c.type == k }}>{{ l }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div><label for="assigned">Toegewezen aan</label>
|
||||
<select id="assigned" name="assigned_user_id">
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="assigned">Toegewezen aan</label>
|
||||
<select class="form-select" id="assigned" name="assigned_user_id">
|
||||
<option value="">— Niemand —</option>
|
||||
{% for u in USERS %}<option value="{{ u.id }}" {{ 'selected' if c.assigned_user_id|string == u.id|string }}>{{ u.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div><label for="relation">Relatie</label>
|
||||
<select id="relation" name="relation_id">
|
||||
<div class="col-md-6"><label class="form-label" for="relation">Relatie</label>
|
||||
<select class="form-select" id="relation" name="relation_id">
|
||||
<option value="">— Geen —</option>
|
||||
{% for kind, label in RELATION_KINDS %}
|
||||
<optgroup label="{{ label }}s">
|
||||
@@ -43,14 +43,14 @@
|
||||
</select>
|
||||
<p class="hint">Huishouden, familie of bedrijf van de klant, of de partner waar deze persoon werkt.</p>
|
||||
</div>
|
||||
<div><label for="role">Rol binnen relatie</label><input id="role" name="relation_role" value="{{ c.relation_role }}" placeholder="Bijv. bewoner, mantelzorger, inkoper"></div>
|
||||
<div><label for="value">Verwachte waarde (€)</label><input id="value" type="number" step="0.01" name="value" value="{{ c.value or '' }}"></div>
|
||||
<div><label for="source">Bron</label><input id="source" name="source" value="{{ c.source }}" placeholder="Bijv. website, beurs, telefoon"></div>
|
||||
<div class="full"><label for="notes">Vaste notities</label><textarea id="notes" name="notes">{{ c.notes }}</textarea></div>
|
||||
<div class="col-md-6"><label class="form-label" for="role">Rol binnen relatie</label><input class="form-control" id="role" name="relation_role" value="{{ c.relation_role }}" placeholder="Bijv. bewoner, mantelzorger, inkoper"></div>
|
||||
<div class="col-md-6"><label class="form-label" for="value">Verwachte waarde (€)</label><input class="form-control" id="value" type="number" step="0.01" name="value" value="{{ c.value or '' }}" inputmode="decimal"></div>
|
||||
<div class="col-md-6"><label class="form-label" for="source">Bron</label><input class="form-control" id="source" name="source" value="{{ c.source }}" placeholder="Bijv. website, beurs, telefoon"></div>
|
||||
<div class="col-12"><label class="form-label" for="notes">Vaste notities</label><textarea class="form-control" id="notes" name="notes">{{ c.notes }}</textarea></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn">Opslaan</button>
|
||||
<a class="btn quiet" href="{{ url_for('contacts') if new else url_for('contact_view', cid=c.id) }}">Annuleren</a>
|
||||
<button class="btn btn-primary">Opslaan</button>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('contacts') if new else url_for('contact_view', cid=c.id) }}">Annuleren</a>
|
||||
</div>
|
||||
</form>
|
||||
</div></form>
|
||||
{% endblock %}
|
||||
|
||||
+16
-15
@@ -6,10 +6,10 @@
|
||||
<div class="actions">
|
||||
<form class="search" method="get">
|
||||
{% for k in ('type', 'status', 'owner') %}{% if a[k] %}<input type="hidden" name="{{ k }}" value="{{ a[k] }}">{% endif %}{% endfor %}
|
||||
<input type="search" name="q" value="{{ term }}" placeholder="Zoek op naam, e-mail, plaats…" aria-label="Zoeken">
|
||||
<input class="form-control" type="search" name="q" value="{{ term }}" placeholder="Zoek op naam, e-mail, plaats…" aria-label="Zoeken">
|
||||
</form>
|
||||
<a class="btn ghost" href="{{ url_for('contacts_export') }}">Exporteer CSV</a>
|
||||
<a class="btn" href="{{ url_for('contact_new', type=a.type or 'lead') }}">{{ 'Nieuwe klant' if a.type == 'klant' else 'Nieuwe lead' }}</a>
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('contacts_export') }}">Exporteer CSV</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('contact_new', type=a.type or 'lead') }}">{{ 'Nieuwe klant' if a.type == 'klant' else 'Nieuwe lead' }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,25 +30,26 @@
|
||||
<a href="{{ url_for('contacts', owner='none', **base) }}" class="{{ 'on' if a.owner == 'none' }}">Niet toegewezen</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
{% if rows %}
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Naam</th><th>Relatie</th><th>Telefoon</th><th>Plaats</th><th>Bron</th><th>Toegewezen</th><th>Status</th><th>Aangemaakt</th></tr></thead>
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<thead><tr><th>Naam</th><th>Relatie</th><th>Telefoon</th><th>Plaats</th><th>Model</th><th>Bron</th><th>Toegewezen</th><th>Status</th><th>Aangemaakt</th></tr></thead>
|
||||
<tbody>
|
||||
{% for c in rows %}
|
||||
<tr class="link" onclick="location='{{ url_for('contact_view', cid=c.id) }}'">
|
||||
<td><a href="{{ url_for('contact_view', cid=c.id) }}">{{ c.name }}</a><div class="muted">{{ c.email }}</div></td>
|
||||
<td>{% if c.relation_id %}<a href="{{ url_for('relation_view', rid=c.relation_id) }}" onclick="event.stopPropagation()">{{ c.relation_name }}</a>{% if c.relation_role %}<div class="muted">{{ c.relation_role }}</div>{% endif %}{% else %}{{ c.company }}{% endif %}</td>
|
||||
<td>{{ c.phone }}</td>
|
||||
<td>{{ c.city }}</td>
|
||||
<td>{{ c.source }}</td>
|
||||
<td>{{ c.user_name or '—' }}</td>
|
||||
<td>{% if not a.type %}<span class="chip">{{ TYPE.get(c.type) }}</span> {% endif %}<span class="chip {{ c.status }}">{{ STATUS.get(c.status, c.status) }}</span></td>
|
||||
<td class="muted">{{ c.created_at|dt('%d-%m-%Y') }}</td>
|
||||
<td class="stack-title"><a href="{{ url_for('contact_view', cid=c.id) }}">{{ c.name }}</a><div class="muted">{{ c.email }}</div></td>
|
||||
<td data-label="Relatie">{% if c.relation_id %}<span><a href="{{ url_for('relation_view', rid=c.relation_id) }}" onclick="event.stopPropagation()">{{ c.relation_name }}</a>{% if c.relation_role %}<span class="muted d-block">{{ c.relation_role }}</span>{% endif %}</span>{% else %}{{ c.company }}{% endif %}</td>
|
||||
<td data-label="Telefoon">{{ c.phone }}</td>
|
||||
<td data-label="Plaats">{{ c.city }}</td>
|
||||
<td data-label="Model">{{ c.design_title or '' }}</td>
|
||||
<td data-label="Bron">{{ c.source }}</td>
|
||||
<td data-label="Toegewezen">{{ c.user_name or '—' }}</td>
|
||||
<td data-label="Status"><span>{% if not a.type %}<span class="chip">{{ TYPE.get(c.type) }}</span> {% endif %}<span class="chip {{ c.status }}">{{ STATUS.get(c.status, c.status) }}</span></span></td>
|
||||
<td data-label="Aangemaakt" class="muted">{{ c.created_at|dt('%d-%m-%Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">Geen contacten gevonden.</p>{% endif %}
|
||||
</div>
|
||||
</div></div>
|
||||
{% endblock %}
|
||||
|
||||
+92
-84
@@ -3,101 +3,109 @@
|
||||
{% block content %}
|
||||
<div class="head">
|
||||
<div><div class="kicker">Overzicht</div><h1>Dashboard</h1></div>
|
||||
<a class="btn" href="{{ url_for('contact_new') }}">Nieuw contact</a>
|
||||
<div class="actions"><a class="btn btn-primary" href="{{ url_for('contact_new') }}">Nieuw contact</a></div>
|
||||
</div>
|
||||
|
||||
<div class="kpis">
|
||||
<div class="kpi"><div class="label">Nieuwe leads · 30 dagen</div><div class="value">{{ stats.new30 }}</div><div class="muted">{{ stats.customers }} klantrelaties · {{ stats.partners }} partners</div></div>
|
||||
<div class="kpi"><div class="label">Open leads</div><div class="value">{{ stats.open_leads }}</div>
|
||||
<div class="muted">{{ stats.pipeline_value|euro }} verwacht{% if stats.unassigned %} · <a href="{{ url_for('contacts', type='lead', owner='none') }}">{{ stats.unassigned }} niet toegewezen</a>{% endif %}</div></div>
|
||||
<div class="kpi"><div class="label">Open offertes</div><div class="value">{{ stats.open_quotes }}</div><div class="muted">{{ stats.open_value|euro }} excl. btw</div></div>
|
||||
<div class="kpi"><div class="label">Gewonnen · 30 dagen</div><div class="value">{{ stats.won_value|euro }}</div><div class="muted">excl. btw</div></div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-6 col-xl-3"><div class="kpi"><div class="label">Nieuwe leads · 30 dagen</div><div class="value">{{ stats.new30 }}</div><div class="muted">{{ stats.customers }} klantrelaties · {{ stats.partners }} partners</div></div></div>
|
||||
<div class="col-sm-6 col-xl-3"><div class="kpi"><div class="label">Open leads</div><div class="value">{{ stats.open_leads }}</div>
|
||||
<div class="muted">{{ stats.pipeline_value|euro }} verwacht{% if stats.unassigned %} · <a href="{{ url_for('contacts', type='lead', owner='none') }}">{{ stats.unassigned }} niet toegewezen</a>{% endif %}</div></div></div>
|
||||
<div class="col-sm-6 col-xl-3"><div class="kpi"><div class="label">Open offertes</div><div class="value">{{ stats.open_quotes }}</div><div class="muted">{{ stats.open_value|euro }} excl. btw</div></div></div>
|
||||
<div class="col-sm-6 col-xl-3"><div class="kpi"><div class="label">Gewonnen · 30 dagen</div><div class="value">{{ stats.won_value|euro }}</div><div class="muted">excl. btw</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Leads per status</h2>
|
||||
<div class="bars">
|
||||
{% for key, label, n in pipeline %}
|
||||
<a class="bar" href="{{ url_for('contacts', type='lead', status=key) }}">
|
||||
<span>{{ label }}</span>
|
||||
<span class="track"><span class="fill" style="width: {{ (n / pipe_max * 100)|round(1) }}%"></span></span>
|
||||
<span class="num">{{ n }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Leads per week</h2>
|
||||
<div class="cols">
|
||||
{% for wk, n in weekly %}
|
||||
<div title="Week {{ wk }}: {{ n }} lead(s)">
|
||||
<span>{{ n or '' }}</span>
|
||||
<span class="c" style="height: {{ (n / week_max * 75)|round(1) }}%"></span>
|
||||
<span>{{ wk }}</span>
|
||||
<div class="row g-4 mb-4 card-grid">
|
||||
<div class="col-md-6 col-xxl-4">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Leads per status</h2>
|
||||
<div class="bars">
|
||||
{% for key, label, n in pipeline %}
|
||||
<a class="bar" href="{{ url_for('contacts', type='lead', status=key) }}">
|
||||
<span>{{ label }}</span>
|
||||
<span class="track"><span class="fill" style="width: {{ (n / pipe_max * 100)|round(1) }}%"></span></span>
|
||||
<span class="num">{{ n }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Bronnen</h2>
|
||||
{% if sources %}
|
||||
<div class="bars">
|
||||
{% for s in sources %}
|
||||
<div class="bar">
|
||||
<span title="{{ s.s }}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ s.s }}</span>
|
||||
<span class="track"><span class="fill" style="width: {{ (s.n / source_max * 100)|round(1) }}%"></span></span>
|
||||
<span class="num">{{ s.n }}</span>
|
||||
<div class="col-md-6 col-xxl-4">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Leads per week</h2>
|
||||
<div class="cols">
|
||||
{% for wk, n in weekly %}
|
||||
<div title="Week {{ wk }}: {{ n }} lead(s)">
|
||||
<span>{{ n or '' }}</span>
|
||||
<span class="c" style="height: {{ (n / week_max * 75)|round(1) }}%"></span>
|
||||
<span>{{ wk }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}<p class="empty">Nog geen leads.</p>{% endif %}
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="col-md-12 col-xxl-4">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Bronnen</h2>
|
||||
{% if sources %}
|
||||
<div class="bars">
|
||||
{% for s in sources %}
|
||||
<div class="bar">
|
||||
<span title="{{ s.s }}">{{ s.s }}</span>
|
||||
<span class="track"><span class="fill" style="width: {{ (s.n / source_max * 100)|round(1) }}%"></span></span>
|
||||
<span class="num">{{ s.n }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}<p class="empty">Nog geen leads.</p>{% endif %}
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="two">
|
||||
<div class="card">
|
||||
<h2>Laatste leads</h2>
|
||||
{% if recent %}
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Naam</th><th>Bron</th><th>Toegewezen</th><th>Status</th><th>Binnen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for c in recent %}
|
||||
<tr class="link" onclick="location='{{ url_for('contact_view', cid=c.id) }}'">
|
||||
<td><a href="{{ url_for('contact_view', cid=c.id) }}">{{ c.name }}</a><div class="muted">{{ c.email }}</div></td>
|
||||
<td>{{ c.source }}</td>
|
||||
<td>{{ c.user_name or '—' }}</td>
|
||||
<td><span class="chip {{ c.status }}">{{ STATUS.get(c.status, c.status) }}</span></td>
|
||||
<td class="muted">{{ c.created_at|dt }}</td>
|
||||
</tr>
|
||||
<div class="row gx-4">
|
||||
<div class="col-xl-8">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Laatste leads</h2>
|
||||
{% if recent %}
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<thead><tr><th>Naam</th><th>Bron</th><th>Toegewezen</th><th>Status</th><th>Binnen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for c in recent %}
|
||||
<tr class="link" onclick="location='{{ url_for('contact_view', cid=c.id) }}'">
|
||||
<td class="stack-title"><a href="{{ url_for('contact_view', cid=c.id) }}">{{ c.name }}</a><div class="muted">{{ c.email }}</div></td>
|
||||
<td data-label="Bron">{{ c.source }}</td>
|
||||
<td data-label="Toegewezen">{{ c.user_name or '—' }}</td>
|
||||
<td data-label="Status"><span class="chip {{ c.status }}">{{ STATUS.get(c.status, c.status) }}</span></td>
|
||||
<td data-label="Binnen" class="muted">{{ c.created_at|dt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">Nog geen contacten. Voeg er een toe of koppel een formulier via Instellingen.</p>{% endif %}
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Mijn 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">{% if t.contact_id %}<a href="{{ url_for('contact_view', cid=t.contact_id) }}">{{ t.contact_name }}</a>{% endif %}
|
||||
{% if t.due_date %}· <span class="{{ 'overdue' if t.due_date < today }}">{{ t.due_date|dt('%d-%m-%Y') }}</span>{% endif %}</div></div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="empty">Geen openstaande taken.</p>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">Nog geen contacten. Voeg er een toe of koppel een formulier via Instellingen.</p>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="card">
|
||||
<h2>Mijn 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">{% if t.contact_id %}<a href="{{ url_for('contact_view', cid=t.contact_id) }}">{{ t.contact_name }}</a>{% endif %}
|
||||
{% if t.due_date %}· <span class="{{ 'overdue' if t.due_date < today }}">{{ t.due_date|dt('%d-%m-%Y') }}</span>{% endif %}</div></div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="empty">Geen openstaande taken.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Ongelezen e-mail</h2>
|
||||
{% for e in mails %}
|
||||
<p><a href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a><br><span class="muted">{{ e.from_name or e.from_addr }} · {{ e.date|dt }}</span></p>
|
||||
{% else %}
|
||||
<p class="empty">Geen ongelezen berichten.</p>
|
||||
{% endfor %}
|
||||
<a href="{{ url_for('inbox') }}">Naar inbox</a>
|
||||
</div>
|
||||
</div></div>
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Ongelezen e-mail</h2>
|
||||
{% for e in mails %}
|
||||
<p><a href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a><br><span class="muted">{{ e.from_name or e.from_addr }} · {{ e.date|dt }}</span></p>
|
||||
{% else %}
|
||||
<p class="empty">Geen ongelezen berichten.</p>
|
||||
{% endfor %}
|
||||
<a href="{{ url_for('inbox') }}">Naar inbox</a>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+11
-11
@@ -5,35 +5,35 @@
|
||||
<div>
|
||||
<a class="label" href="{{ url_for('inbox') }}">← Inbox</a>
|
||||
<h1>{{ e.subject }}</h1>
|
||||
<div class="muted">
|
||||
<div class="muted" style="overflow-wrap:anywhere">
|
||||
{% if e.direction == 'in' %}Van {{ e.from_name }} <{{ e.from_addr }}>{% else %}Aan {{ e.to_addr }}{% endif %}
|
||||
· {{ e.date|dt }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{% if contact %}
|
||||
<a class="btn ghost" href="{{ url_for('contact_view', cid=contact.id) }}">Naar {{ contact.name }}</a>
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('contact_view', cid=contact.id) }}">Naar {{ contact.name }}</a>
|
||||
{% elif e.direction == 'in' %}
|
||||
<form method="post" action="{{ url_for('inbox_contact', eid=e.id) }}" class="inline"><button class="btn ghost">Maak contact aan</button></form>
|
||||
<form method="post" action="{{ url_for('inbox_contact', eid=e.id) }}" class="inline"><button class="btn btn-outline-primary">Maak contact aan</button></form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div class="mailbody">{{ e.body }}</div></div>
|
||||
<div class="card"><div class="card-body"><div class="mailbody">{{ e.body }}</div></div></div>
|
||||
|
||||
{% if e.direction == 'in' %}
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Beantwoorden</h2>
|
||||
<form method="post" action="{{ url_for('mail_send') }}">
|
||||
<input type="hidden" name="reply_to" value="{{ e.id }}">
|
||||
{% if contact %}<input type="hidden" name="contact_id" value="{{ contact.id }}">{% endif %}
|
||||
<div class="fields">
|
||||
<div><label>Aan</label><input type="email" name="to" value="{{ e.from_addr }}" required></div>
|
||||
<div><label>Onderwerp</label><input name="subject" value="{{ e.subject if e.subject.lower().startswith('re:') else 'Re: ' ~ e.subject }}" required></div>
|
||||
<div class="full"><label>Bericht</label><textarea name="body" rows="8" required></textarea></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label" for="to">Aan</label><input class="form-control" id="to" type="email" name="to" value="{{ e.from_addr }}" required></div>
|
||||
<div class="col-md-6"><label class="form-label" for="subject">Onderwerp</label><input class="form-control" id="subject" name="subject" value="{{ e.subject if e.subject.lower().startswith('re:') else 'Re: ' ~ e.subject }}" required></div>
|
||||
<div class="col-12"><label class="form-label" for="body">Bericht</label><textarea class="form-control" id="body" name="body" rows="8" required></textarea></div>
|
||||
</div>
|
||||
<div class="actions"><button class="btn">Versturen</button></div>
|
||||
<div class="actions"><button class="btn btn-primary">Versturen</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
+24
-20
@@ -2,27 +2,31 @@
|
||||
{% block title %}Importeren{% endblock %}
|
||||
{% block content %}
|
||||
<div class="head"><div><div class="kicker">Gegevens</div><h1>Importeren</h1></div></div>
|
||||
<div class="two">
|
||||
<form method="post" enctype="multipart/form-data" class="card">
|
||||
<h2>Bestand kiezen</h2>
|
||||
<div class="fields">
|
||||
<div class="full"><label for="kind">Wat importeer je?</label>
|
||||
<select id="kind" name="kind">
|
||||
<option value="contacts">Leads / klanten</option>
|
||||
<option value="relations" {{ 'selected' if request.args.get('kind') == 'relations' }}>Relaties (klanten of partners)</option>
|
||||
<option value="products" {{ 'selected' if request.args.get('kind') == 'products' }}>Producten</option>
|
||||
</select>
|
||||
<div class="row g-4 card-grid">
|
||||
<div class="col-xl-7">
|
||||
<form method="post" enctype="multipart/form-data" class="card"><div class="card-body">
|
||||
<h2>Bestand kiezen</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label" for="kind">Wat importeer je?</label>
|
||||
<select class="form-select" id="kind" name="kind">
|
||||
<option value="contacts">Leads / klanten</option>
|
||||
<option value="relations" {{ 'selected' if request.args.get('kind') == 'relations' }}>Relaties (klanten of partners)</option>
|
||||
<option value="products" {{ 'selected' if request.args.get('kind') == 'products' }}>Producten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12"><label class="form-label" for="file">CSV- of JSON-bestand</label><input class="form-control" id="file" type="file" name="file" accept=".csv,.json,.txt" required></div>
|
||||
</div>
|
||||
<div class="full"><label for="file">CSV- of JSON-bestand</label><input id="file" type="file" name="file" accept=".csv,.json,.txt" required></div>
|
||||
</div>
|
||||
<div class="actions"><button class="btn">Volgende: kolommen controleren</button></div>
|
||||
</form>
|
||||
<div class="card">
|
||||
<h2>Formaten</h2>
|
||||
<p><strong>CSV</strong>: scheidingsteken <code>;</code>, <code>,</code> of tab, de eerste regel met kolomnamen. Regels die beginnen met <code>#</code> worden overgeslagen. Bedragen als <code>6,00</code>, <code>1.234,50</code> of <code>€ 6</code>.</p>
|
||||
<p><strong>JSON</strong>: een lijst met objecten, of een object met een lijst erin, zoals <code>{"leads": [{"naam": "…", "email": "…"}]}</code>.</p>
|
||||
<p><strong>Configurator</strong>: <code>kosten/leveranciers.csv</code> en <code>intern.csv</code> worden als producten herkend (code, omschrijving, eenheid, prijs, leverancier). Een woningontwerp (<code>house-design</code>-JSON) upload je bij een contact onder Bestanden.</p>
|
||||
<p class="muted" style="margin-bottom:0">Je ziet eerst een voorbeeld en kiest per kolom waar die heen moet. Bestaande contacten (zelfde e-mail) en producten (zelfde SKU) worden bijgewerkt in plaats van dubbel aangemaakt. Elke productwijziging komt in de mutatiehistorie; offertes houden hun eigen prijs.</p>
|
||||
<div class="actions"><button class="btn btn-primary">Volgende: kolommen controleren</button></div>
|
||||
</div></form>
|
||||
</div>
|
||||
<div class="col-xl-5">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Formaten</h2>
|
||||
<p><strong>CSV</strong>: scheidingsteken <code>;</code>, <code>,</code> of tab, de eerste regel met kolomnamen. Regels die beginnen met <code>#</code> worden overgeslagen. Bedragen als <code>6,00</code>, <code>1.234,50</code> of <code>€ 6</code>.</p>
|
||||
<p><strong>JSON</strong>: een lijst met objecten, of een object met een lijst erin, zoals <code>{"leads": [{"naam": "…", "email": "…"}]}</code>.</p>
|
||||
<p><strong>Configurator</strong>: <code>kosten/leveranciers.csv</code> en <code>intern.csv</code> worden als producten herkend (code, omschrijving, eenheid, prijs, leverancier). Een woningontwerp (<code>house-design</code>-JSON) upload je bij een contact onder Bestanden.</p>
|
||||
<p class="muted">Je ziet eerst een voorbeeld en kiest per kolom waar die heen moet. Bestaande contacten (zelfde e-mail) en producten (zelfde SKU) worden bijgewerkt in plaats van dubbel aangemaakt. Elke productwijziging komt in de mutatiehistorie; offertes houden hun eigen prijs.</p>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,17 +7,18 @@
|
||||
<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 class="actions"><button class="btn btn-primary">{{ total }} rij(en) importeren</button></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Kolommen koppelen</h2>
|
||||
<div class="table-wrap"><table>
|
||||
<p class="hint d-md-none" style="margin:-.4rem 0 .75rem">Schuif de tabel opzij voor de andere kolommen.</p>
|
||||
<div class="table-responsive"><table class="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 }}">
|
||||
<select class="form-select form-select-sm" 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>
|
||||
@@ -30,30 +31,30 @@
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% if total > rows|length %}<p class="hint">Eerste {{ rows|length }} van {{ total }} rijen.</p>{% endif %}
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Opties</h2>
|
||||
<div class="fields">
|
||||
<div class="row g-3">
|
||||
{% 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>
|
||||
<div class="col-md-6"><label class="form-label" for="o-type">Importeren als</label><select class="form-select" id="o-type" name="type">{% for k, l in TYPES %}<option value="{{ k }}">{{ l }}</option>{% endfor %}</select></div>
|
||||
<div class="col-md-6"><label class="form-label" for="o-status">Status (als de kolom ontbreekt)</label><select class="form-select" id="o-status" name="status">{% for k, l in STATUSES %}<option value="{{ k }}">{{ l }}</option>{% endfor %}</select></div>
|
||||
<div class="col-md-6"><label class="form-label" for="o-user">Toewijzen aan</label><select class="form-select" id="o-user" name="assigned_user_id"><option value="">— Niemand —</option>{% for u in USERS %}<option value="{{ u.id }}">{{ u.name }}</option>{% endfor %}</select></div>
|
||||
<div class="col-md-6"><label class="form-label" for="o-source">Bron (als de kolom ontbreekt)</label><input class="form-control" id="o-source" 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>
|
||||
<div class="col-md-6"><label class="form-label" for="rkind">Soort</label><select class="form-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 class="col-md-6"><label class="form-label" for="o-rtype">Type (als de kolom ontbreekt)</label>
|
||||
<select class="form-select" id="o-rtype" 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>
|
||||
<div class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" id="o-deact" name="deactivate_missing" value="1"><label class="form-check-label" for="o-deact">Producten met een SKU die niet in dit bestand staan op inactief zetten (volledige prijslijst)</label></div>
|
||||
<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 class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" id="o-update" name="update" value="1" checked><label class="form-check-label" for="o-update">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>
|
||||
</div>
|
||||
</div></div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
+9
-10
@@ -2,10 +2,9 @@
|
||||
{% block title %}Inbox{% endblock %}
|
||||
{% block content %}
|
||||
<div class="head">
|
||||
<div><div class="kicker">E-mail</div><h1>Inbox</h1></div>
|
||||
<div><div class="kicker">E-mail</div><h1>Inbox</h1>{% if last_sync %}<div class="muted">Laatst opgehaald: {{ last_sync|dt }}</div>{% endif %}</div>
|
||||
<div class="actions">
|
||||
{% if last_sync %}<span class="muted">Laatst opgehaald: {{ last_sync|dt }}</span>{% endif %}
|
||||
<form method="post" action="{{ url_for('inbox_sync') }}" class="inline"><button class="btn">Nieuwe e-mail ophalen</button></form>
|
||||
<form method="post" action="{{ url_for('inbox_sync') }}" class="inline"><button class="btn btn-primary">Nieuwe e-mail ophalen</button></form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,21 +18,21 @@
|
||||
<a href="{{ url_for('inbox', f='unlinked') }}" class="{{ 'on' if f == 'unlinked' }}">Niet gekoppeld aan contact</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
{% if rows %}
|
||||
<div class="table-wrap"><table>
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<thead><tr><th>Van</th><th>Onderwerp</th><th>Contact</th><th class="num">Datum</th></tr></thead>
|
||||
<tbody>
|
||||
{% for e in rows %}
|
||||
<tr class="link {{ 'unread' if not e.is_read }}" onclick="location='{{ url_for('inbox_view', eid=e.id) }}'">
|
||||
<td>{% if not e.is_read %}<span class="dot" aria-label="Ongelezen"></span>{% endif %}{{ e.from_name or e.from_addr }}</td>
|
||||
<td><a href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a></td>
|
||||
<td>{{ e.contact_name or '' }}</td>
|
||||
<td class="muted num">{{ e.date|dt }}</td>
|
||||
<td class="stack-title">{% if not e.is_read %}<span class="dot" aria-label="Ongelezen"></span>{% endif %}{{ e.from_name or e.from_addr }}<a class="d-block d-md-none" href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a></td>
|
||||
<td class="d-none d-md-table-cell"><a href="{{ url_for('inbox_view', eid=e.id) }}">{{ e.subject }}</a></td>
|
||||
<td data-label="Contact">{{ e.contact_name or '' }}</td>
|
||||
<td data-label="Datum" class="muted num">{{ e.date|dt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">Geen berichten.</p>{% endif %}
|
||||
</div>
|
||||
</div></div>
|
||||
{% endblock %}
|
||||
|
||||
+44
-41
@@ -1,70 +1,73 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Klantportaal{% endblock %}
|
||||
{% block body %}
|
||||
<script>document.body.classList.add('portal')</script>
|
||||
<header class="portal-top">
|
||||
<div class="wrap">
|
||||
<div class="container d-flex justify-content-between align-items-center gap-3">
|
||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="{{ s.company_name or 'Van Emous' }}">
|
||||
<a href="{{ url_for('portal_logout') }}">Uitloggen</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="portal-main">
|
||||
<div class="portal-main container">
|
||||
{% with msgs = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, m in msgs %}<div class="flash {{ cat }}">{{ m }}</div>{% endfor %}
|
||||
{% endwith %}
|
||||
<div class="head"><div><div class="kicker">Klantportaal</div><h1>Welkom, {{ c.name }}</h1></div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Uw offertes</h2>
|
||||
{% if quotes %}
|
||||
<div class="table-wrap"><table>
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<thead><tr><th>Nummer</th><th>Onderwerp</th><th>Datum</th><th>Status</th><th class="num">Totaal incl. btw</th></tr></thead>
|
||||
<tbody>
|
||||
{% for qt in quotes %}
|
||||
<tr class="link" onclick="location='{{ url_for('portal_quote', qid=qt.id) }}'">
|
||||
<td><a href="{{ url_for('portal_quote', qid=qt.id) }}">{{ qt.number }}</a></td>
|
||||
<td>{{ qt.title }}</td>
|
||||
<td>{{ qt.date|dt('%d-%m-%Y') }}</td>
|
||||
<td><span class="chip {{ qt.status }}">{{ 'Wacht op uw reactie' if qt.status == 'verzonden' else QSTATUS.get(qt.status) }}</span></td>
|
||||
<td class="num">{{ qt.total|euro }}</td>
|
||||
<td class="stack-title"><a href="{{ url_for('portal_quote', qid=qt.id) }}">{{ qt.number }}</a></td>
|
||||
<td data-label="Onderwerp">{{ qt.title }}</td>
|
||||
<td data-label="Datum">{{ qt.date|dt('%d-%m-%Y') }}</td>
|
||||
<td data-label="Status"><span class="chip {{ qt.status }}">{{ 'Wacht op uw reactie' if qt.status == 'verzonden' else QSTATUS.get(qt.status) }}</span></td>
|
||||
<td data-label="Totaal incl. btw" class="num">{{ qt.total|euro }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">Er staan nog geen offertes voor u klaar.</p>{% endif %}
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Bestanden</h2>
|
||||
{% if files %}
|
||||
<div class="files">
|
||||
{% for f in files %}
|
||||
<div class="file">
|
||||
<a href="{{ url_for('portal_file', name=f.stored) }}" target="_blank" class="thumb">
|
||||
{% if f.mime.startswith('image/') %}<img src="{{ url_for('portal_file', name=f.stored) }}" alt="{{ f.original }}" loading="lazy">
|
||||
{% else %}{{ f.original.rsplit('.', 1)[-1][:4] if '.' in f.original else 'bestand' }}{% endif %}
|
||||
</a>
|
||||
<div class="name" title="{{ f.original }}">{{ f.original }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('portal_upload') }}" enctype="multipart/form-data">
|
||||
<label for="files">Foto's of documenten delen (bijv. van uw tuin of tekeningen)</label>
|
||||
<input id="files" type="file" name="files" multiple required>
|
||||
<div class="actions"><button class="btn ghost small">Uploaden</button></div>
|
||||
</form>
|
||||
<div class="row g-4 mb-4 card-grid">
|
||||
<div class="col-md-6">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Bestanden</h2>
|
||||
{% if files %}
|
||||
<div class="files">
|
||||
{% for f in files %}
|
||||
<div class="file">
|
||||
<a href="{{ url_for('portal_file', name=f.stored) }}" target="_blank" class="thumb">
|
||||
{% if f.mime.startswith('image/') %}<img src="{{ url_for('portal_file', name=f.stored) }}" alt="{{ f.original }}" loading="lazy">
|
||||
{% else %}{{ f.original.rsplit('.', 1)[-1][:4] if '.' in f.original else 'bestand' }}{% endif %}
|
||||
</a>
|
||||
<div class="name" title="{{ f.original }}">{{ f.original }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('portal_upload') }}" enctype="multipart/form-data">
|
||||
<label class="form-label" for="files">Foto's of documenten delen (bijv. van uw tuin of tekeningen)</label>
|
||||
<input class="form-control" id="files" type="file" name="files" multiple required>
|
||||
<div class="actions"><button class="btn btn-outline-primary btn-sm">Uploaden</button></div>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Bericht sturen</h2>
|
||||
<form method="post" action="{{ url_for('portal_message') }}">
|
||||
<textarea name="body" required aria-label="Bericht" placeholder="Uw vraag of opmerking"></textarea>
|
||||
<div class="actions"><button class="btn">Versturen</button></div>
|
||||
</form>
|
||||
{% for m in messages %}
|
||||
<p class="muted" style="margin:.75rem 0 0">{{ m.created_at|dt }}<br><span style="color:var(--ink);white-space:pre-line">{{ m.body[8:] }}</span></p>
|
||||
{% endfor %}
|
||||
<div class="col-md-6">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Bericht sturen</h2>
|
||||
<form method="post" action="{{ url_for('portal_message') }}">
|
||||
<textarea class="form-control" name="body" required aria-label="Bericht" placeholder="Uw vraag of opmerking"></textarea>
|
||||
<div class="actions"><button class="btn btn-primary">Versturen</button></div>
|
||||
</form>
|
||||
{% for m in messages %}
|
||||
<p class="muted" style="margin:.75rem 0 0">{{ m.created_at|dt }}<br><span style="color:var(--ink);white-space:pre-line">{{ m.body[8:] }}</span></p>
|
||||
{% endfor %}
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% if s.company_phone or s.company_email %}
|
||||
|
||||
+31
-31
@@ -4,61 +4,61 @@
|
||||
<div class="head">
|
||||
<div><a class="label" href="{{ url_for('products') }}">← Producten</a><h1>{{ 'Product bewerken' if p.id else 'Nieuw product' }}</h1></div>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" class="card">
|
||||
<div class="fields">
|
||||
<div class="full"><label for="name">Naam *</label><input id="name" name="name" value="{{ p.name }}" required></div>
|
||||
<div><label for="sku">SKU / artikelnummer</label><input id="sku" name="sku" value="{{ p.sku }}">
|
||||
<form method="post" enctype="multipart/form-data" class="card"><div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label" for="name">Naam *</label><input class="form-control" id="name" name="name" value="{{ p.name }}" required></div>
|
||||
<div class="col-md-6"><label class="form-label" for="sku">SKU / artikelnummer</label><input class="form-control" id="sku" name="sku" value="{{ p.sku }}">
|
||||
<p class="hint">Vaste sleutel: imports en de API werken het product met deze SKU bij.</p></div>
|
||||
<div><label for="supplier">Leverancier</label><input id="supplier" name="supplier" value="{{ p.supplier }}" list="suppliers" autocomplete="off">
|
||||
<div class="col-md-6"><label class="form-label" for="supplier">Leverancier</label><input class="form-control" id="supplier" name="supplier" value="{{ p.supplier }}" list="suppliers" autocomplete="off">
|
||||
<datalist id="suppliers">{% for r in suppliers %}<option value="{{ r.name }}">{% endfor %}</datalist>
|
||||
<p class="hint">{% if p.supplier_id %}<a href="{{ url_for('relation_view', rid=p.supplier_id) }}">Naar partner</a> · {% endif %}Een nieuwe naam wordt een partner van het type Leverancier.</p></div>
|
||||
<div><label for="unit">Eenheid</label><input id="unit" name="unit" value="{{ p.unit }}" placeholder="stuk, m², uur"></div>
|
||||
<div><label for="price">Prijs excl. btw (€)</label><input id="price" type="number" step="0.01" name="price" value="{{ p.price }}" required></div>
|
||||
<div><label for="vat">Btw %</label>
|
||||
<select id="vat" name="vat">
|
||||
<div class="col-sm-4"><label class="form-label" for="unit">Eenheid</label><input class="form-control" id="unit" name="unit" value="{{ p.unit }}" placeholder="stuk, m², uur"></div>
|
||||
<div class="col-sm-4"><label class="form-label" for="price">Prijs excl. btw (€)</label><input class="form-control" id="price" type="number" step="0.01" name="price" value="{{ p.price }}" required inputmode="decimal"></div>
|
||||
<div class="col-sm-4"><label class="form-label" for="vat">Btw %</label>
|
||||
<select class="form-select" id="vat" name="vat">
|
||||
{% for v in [21, 9, 0] %}<option value="{{ v }}" {{ 'selected' if p.vat == v }}>{{ v }}%</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="full"><label for="description">Omschrijving</label><textarea id="description" name="description">{{ p.description }}</textarea>
|
||||
<div class="col-12"><label class="form-label" for="description">Omschrijving</label><textarea class="form-control" id="description" name="description">{{ p.description }}</textarea>
|
||||
<p class="hint">Wordt standaard onder de productnaam op de offerte gezet.</p></div>
|
||||
<div class="full">
|
||||
<label for="image">Afbeelding</label>
|
||||
{% if p.image %}<img src="{{ url_for('upload_file', name=p.image) }}" alt="" style="max-width:220px;border-radius:var(--radius-md);display:block;margin-bottom:.75rem">{% endif %}
|
||||
<input id="image" type="file" name="image" accept="image/*">
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="image">Afbeelding</label>
|
||||
{% if p.image %}<img src="{{ url_for('upload_file', name=p.image) }}" alt="" class="d-block mb-3 img-fluid" style="max-width:220px;border-radius:var(--radius-md)">{% endif %}
|
||||
<input class="form-control" id="image" type="file" name="image" accept="image/*">
|
||||
</div>
|
||||
<div class="full"><label><input type="checkbox" name="active" value="1" {{ 'checked' if p.active }}>Actief (beschikbaar in offertes)</label></div>
|
||||
<div class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" id="active" name="active" value="1" {{ 'checked' if p.active }}><label class="form-check-label" for="active">Actief (beschikbaar in offertes)</label></div></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn">Opslaan</button>
|
||||
<a class="btn quiet" href="{{ url_for('products') }}">Annuleren</a>
|
||||
<button class="btn btn-primary">Opslaan</button>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('products') }}">Annuleren</a>
|
||||
</div>
|
||||
</form>
|
||||
</div></form>
|
||||
{% if p.id %}
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Mutaties</h2>
|
||||
<p class="hint" style="margin-top:-.4rem">Bestaande offertes houden hun eigen prijs; wijzigingen gelden voor nieuwe offerteregels.{% if used %} Gebruikt in {{ used }} offerte(s).{% endif %}</p>
|
||||
<p class="hint" style="margin:-.4rem 0 1rem">Bestaande offertes houden hun eigen prijs; wijzigingen gelden voor nieuwe offerteregels.{% if used %} Gebruikt in {{ used }} offerte(s).{% endif %}</p>
|
||||
{% if history %}
|
||||
<div class="table-wrap"><table>
|
||||
<div class="table-responsive"><table class="table table-stack">
|
||||
<thead><tr><th>Datum</th><th>Veld</th><th>Oud</th><th>Nieuw</th><th>Bron</th></tr></thead>
|
||||
<tbody>
|
||||
{% for h in history %}
|
||||
<tr>
|
||||
<td class="muted">{{ h.created_at|dt }}</td>
|
||||
<td>{{ LABELS.get(h.field, h.field) }}</td>
|
||||
{% if h.field == 'price' %}<td class="num">{{ h.old_value|float|euro }}</td><td class="num">{{ h.new_value|float|euro }}
|
||||
{% set diff = h.new_value|float - h.old_value|float %}{% if h.old_value|float %}<span class="muted">({{ '+' if diff > 0 }}{{ (diff / h.old_value|float * 100)|round(1)|nr }}%)</span>{% endif %}</td>
|
||||
{% elif h.field == 'created' %}<td></td><td class="num">{{ h.new_value|float|euro }}</td>
|
||||
{% elif h.field == 'active' %}<td>{{ 'ja' if h.old_value == '1' else 'nee' }}</td><td>{{ 'ja' if h.new_value == '1' else 'nee' }}</td>
|
||||
{% else %}<td>{{ h.old_value|truncate(60) }}</td><td>{{ h.new_value|truncate(60) }}</td>{% endif %}
|
||||
<td class="muted">{{ h.source }}{% if h.user_name %} · {{ h.user_name }}{% endif %}</td>
|
||||
<td class="stack-title muted">{{ h.created_at|dt }}</td>
|
||||
<td data-label="Veld">{{ LABELS.get(h.field, h.field) }}</td>
|
||||
{% if h.field == 'price' %}<td data-label="Oud" class="num">{{ h.old_value|float|euro }}</td><td data-label="Nieuw" class="num"><span>{{ h.new_value|float|euro }}
|
||||
{% set diff = h.new_value|float - h.old_value|float %}{% if h.old_value|float %}<span class="muted">({{ '+' if diff > 0 }}{{ (diff / h.old_value|float * 100)|round(1)|nr }}%)</span>{% endif %}</span></td>
|
||||
{% elif h.field == 'created' %}<td data-label="Oud"></td><td data-label="Nieuw" class="num">{{ h.new_value|float|euro }}</td>
|
||||
{% elif h.field == 'active' %}<td data-label="Oud">{{ 'ja' if h.old_value == '1' else 'nee' }}</td><td data-label="Nieuw">{{ 'ja' if h.new_value == '1' else 'nee' }}</td>
|
||||
{% else %}<td data-label="Oud">{{ h.old_value|truncate(60) }}</td><td data-label="Nieuw">{{ h.new_value|truncate(60) }}</td>{% endif %}
|
||||
<td data-label="Bron" class="muted">{{ h.source }}{% if h.user_name %} · {{ h.user_name }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">Nog geen mutaties.</p>{% endif %}
|
||||
</div>
|
||||
</div></div>
|
||||
<form method="post" action="{{ url_for('product_delete', pid=p.id) }}" onsubmit="return confirm('{{ 'Dit product staat in offertes en wordt op inactief gezet.' if used else 'Product verwijderen?' }}')">
|
||||
<button class="btn quiet small">{{ 'Archiveren' if used else 'Product verwijderen' }}</button>
|
||||
<button class="btn btn-outline-secondary btn-sm">{{ 'Archiveren' if used else 'Product verwijderen' }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
+17
-17
@@ -6,11 +6,11 @@
|
||||
<div class="actions">
|
||||
<form class="search" method="get">
|
||||
<input type="hidden" name="show" value="{{ show }}">
|
||||
<input type="search" name="q" value="{{ term }}" placeholder="Zoek op naam, SKU, leverancier…" aria-label="Zoeken">
|
||||
<input class="form-control" type="search" name="q" value="{{ term }}" placeholder="Zoek op naam, SKU, leverancier…" aria-label="Zoeken">
|
||||
</form>
|
||||
<a class="btn ghost" href="{{ url_for('products_export') }}">Exporteer CSV</a>
|
||||
<a class="btn ghost" href="{{ url_for('import_start', kind='products') }}">Importeren</a>
|
||||
<a class="btn" href="{{ url_for('product_edit') }}">Nieuw product</a>
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('products_export') }}">Exporteer CSV</a>
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('import_start', kind='products') }}">Importeren</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('product_edit') }}">Nieuw product</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters">
|
||||
@@ -19,26 +19,26 @@
|
||||
{% endfor %}
|
||||
<span class="muted">{{ rows|length }} product(en)</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
{% if rows %}
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th></th><th>Naam</th><th>SKU</th><th>Leverancier</th><th>Eenheid</th><th class="num">Prijs excl.</th><th class="num">Btw</th><th>Bijgewerkt</th><th>Status</th></tr></thead>
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<thead><tr><th class="d-none d-md-table-cell"></th><th>Naam</th><th>SKU</th><th>Leverancier</th><th>Eenheid</th><th class="num">Prijs excl.</th><th class="num">Btw</th><th>Bijgewerkt</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in rows %}
|
||||
<tr class="link" onclick="location='{{ url_for('product_edit', pid=p.id) }}'">
|
||||
<td style="width:64px">{% if p.image %}<img class="thumb-sm" src="{{ url_for('upload_file', name=p.image) }}" alt="">{% else %}<span class="thumb-sm"></span>{% endif %}</td>
|
||||
<td><a href="{{ url_for('product_edit', pid=p.id) }}">{{ p.name }}</a><div class="muted">{{ p.description|truncate(90) }}</div></td>
|
||||
<td><code>{{ p.sku }}</code></td>
|
||||
<td>{% if p.supplier_id %}<a href="{{ url_for('relation_view', rid=p.supplier_id) }}" onclick="event.stopPropagation()">{{ p.supplier }}</a>{% else %}{{ p.supplier }}{% endif %}</td>
|
||||
<td>{{ p.unit }}</td>
|
||||
<td class="num">{{ p.price|euro }}</td>
|
||||
<td class="num">{{ p.vat|nr }}%</td>
|
||||
<td class="muted">{{ (p.updated_at or p.created_at)|dt('%d-%m-%Y') }}</td>
|
||||
<td>{% if not p.active %}<span class="chip verloren">Inactief</span>{% endif %}</td>
|
||||
<td class="d-none d-md-table-cell" style="width:64px">{% if p.image %}<img class="thumb-sm" src="{{ url_for('upload_file', name=p.image) }}" alt="">{% else %}<span class="thumb-sm"></span>{% endif %}</td>
|
||||
<td class="stack-title"><a href="{{ url_for('product_edit', pid=p.id) }}">{{ p.name }}</a><div class="muted">{{ p.description|truncate(90) }}</div></td>
|
||||
<td data-label="SKU"><code>{{ p.sku }}</code></td>
|
||||
<td data-label="Leverancier">{% if p.supplier_id %}<a href="{{ url_for('relation_view', rid=p.supplier_id) }}" onclick="event.stopPropagation()">{{ p.supplier }}</a>{% else %}{{ p.supplier }}{% endif %}</td>
|
||||
<td data-label="Eenheid">{{ p.unit }}</td>
|
||||
<td data-label="Prijs excl." class="num">{{ p.price|euro }}</td>
|
||||
<td data-label="Btw" class="num">{{ p.vat|nr }}%</td>
|
||||
<td data-label="Bijgewerkt" class="muted">{{ (p.updated_at or p.created_at)|dt('%d-%m-%Y') }}</td>
|
||||
<td data-label="Status">{% if not p.active %}<span class="chip verloren">Inactief</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">{{ 'Geen producten gevonden.' if term or show != 'active' else 'Nog geen producten. Voeg ze toe of importeer een CSV; daarna kies je ze in offertes.' }}</p>{% endif %}
|
||||
</div>
|
||||
</div></div>
|
||||
{% endblock %}
|
||||
|
||||
+54
-31
@@ -2,8 +2,8 @@
|
||||
{% block title %}Offerte {{ qt.number }}{% endblock %}
|
||||
{% macro row(l) %}
|
||||
<tr>
|
||||
<td>
|
||||
<select name="l_product" class="prod" aria-label="Product">
|
||||
<td data-label="Product">
|
||||
<select name="l_product" class="prod form-select form-select-sm" aria-label="Product">
|
||||
<option value="">Vrije regel</option>
|
||||
{% for p in products %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if l.product_id == p.id }}
|
||||
@@ -12,14 +12,14 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td><textarea name="l_desc" rows="1" aria-label="Omschrijving">{{ l.description }}</textarea></td>
|
||||
<td><input type="number" step="any" name="l_qty" value="{{ l.qty|default(1) }}" aria-label="Aantal"></td>
|
||||
<td><input name="l_unit" value="{{ l.unit }}" aria-label="Eenheid"></td>
|
||||
<td><input type="number" step="0.01" name="l_price" value="{{ l.price }}" aria-label="Prijs">
|
||||
<td data-label="Omschrijving"><textarea class="form-control form-control-sm" name="l_desc" rows="1" aria-label="Omschrijving">{{ l.description }}</textarea></td>
|
||||
<td data-label="Aantal"><input class="form-control form-control-sm" type="number" step="any" inputmode="decimal" name="l_qty" value="{{ l.qty|default(1) }}" aria-label="Aantal"></td>
|
||||
<td data-label="Eenheid"><input class="form-control form-control-sm" name="l_unit" value="{{ l.unit }}" aria-label="Eenheid"></td>
|
||||
<td data-label="Prijs excl."><input class="form-control form-control-sm" type="number" step="0.01" inputmode="decimal" name="l_price" value="{{ l.price }}" aria-label="Prijs">
|
||||
{% if l.product_id and prices.get(l.product_id) is not none and prices[l.product_id] != l.price %}<p class="hint" style="margin:.2rem 0 0">Actueel: {{ prices[l.product_id]|euro }}</p>{% endif %}</td>
|
||||
<td><select name="l_vat" aria-label="Btw">{% for v in [21, 9, 0] %}<option value="{{ v }}" {{ 'selected' if l.vat|default(21) == v }}>{{ v }}%</option>{% endfor %}</select></td>
|
||||
<td class="num amt"></td>
|
||||
<td><button type="button" class="linkbtn del" aria-label="Regel verwijderen">✕</button></td>
|
||||
<td data-label="Btw"><select class="form-select form-select-sm" name="l_vat" aria-label="Btw">{% for v in [21, 9, 0] %}<option value="{{ v }}" {{ 'selected' if l.vat|default(21) == v }}>{{ v }}%</option>{% endfor %}</select></td>
|
||||
<td class="num amt" data-label="Totaal"></td>
|
||||
<td class="del-cell"><button type="button" class="linkbtn del" aria-label="Regel verwijderen">✕</button></td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
{% block content %}
|
||||
@@ -27,52 +27,75 @@
|
||||
<div class="head">
|
||||
<div><a class="label" href="{{ url_for('quotes') }}">← Offertes</a><h1>Offerte {{ qt.number }}</h1></div>
|
||||
<div class="actions">
|
||||
<button class="btn ghost" name="then" value="print">Opslaan en afdrukken</button>
|
||||
<button class="btn">Opslaan</button>
|
||||
<button class="btn btn-outline-primary" name="then" value="print">Opslaan en afdrukken</button>
|
||||
<button class="btn btn-primary">Opslaan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="fields">
|
||||
<div><label for="contact">Klant</label>
|
||||
<select id="contact" name="contact_id">
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label" for="contact">Klant</label>
|
||||
<select class="form-select" id="contact" name="contact_id">
|
||||
<option value="">— Kies een contact —</option>
|
||||
{% for c in contacts %}<option value="{{ c.id }}" {{ 'selected' if c.id == qt.contact_id }}>{{ c.name }}{% if c.company %} ({{ c.company }}){% endif %}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div><label for="status">Status</label>
|
||||
<select id="status" name="status">{% for k, l in QUOTE_STATUSES %}<option value="{{ k }}" {{ 'selected' if qt.status == k }}>{{ l }}</option>{% endfor %}</select>
|
||||
<div class="col-md-6"><label class="form-label" for="status">Status</label>
|
||||
<select class="form-select" id="status" name="status">{% for k, l in QUOTE_STATUSES %}<option value="{{ k }}" {{ 'selected' if qt.status == k }}>{{ l }}</option>{% endfor %}</select>
|
||||
<p class="hint">Op "Verzonden" zetten = zichtbaar in het klantportaal.</p>
|
||||
</div>
|
||||
<div><label for="date">Datum</label><input id="date" type="date" name="date" value="{{ qt.date }}"></div>
|
||||
<div><label for="valid">Geldig tot</label><input id="valid" type="date" name="valid_until" value="{{ qt.valid_until or '' }}"></div>
|
||||
<div class="full"><label for="title">Onderwerp</label><input id="title" name="title" value="{{ qt.title }}" placeholder="Bijv. Mantelzorgwoning model Amstel"></div>
|
||||
<div class="full"><label for="intro">Inleiding</label><textarea id="intro" name="intro">{{ qt.intro }}</textarea></div>
|
||||
<div class="col-6"><label class="form-label" for="date">Datum</label><input class="form-control" id="date" type="date" name="date" value="{{ qt.date }}"></div>
|
||||
<div class="col-6"><label class="form-label" for="valid">Geldig tot</label><input class="form-control" id="valid" type="date" name="valid_until" value="{{ qt.valid_until or '' }}"></div>
|
||||
<div class="col-12"><label class="form-label" for="title">Onderwerp</label><input class="form-control" id="title" name="title" value="{{ qt.title }}" placeholder="Bijv. Mantelzorgwoning model Amstel"></div>
|
||||
<div class="col-12"><label class="form-label" for="intro">Inleiding</label><textarea class="form-control" id="intro" name="intro">{{ qt.intro }}</textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Regels</h2>
|
||||
<div class="table-wrap"><table class="lines">
|
||||
<div class="table-responsive"><table class="table lines">
|
||||
<thead><tr><th style="min-width:170px">Product</th><th style="min-width:240px">Omschrijving</th><th style="width:90px">Aantal</th><th style="width:100px">Eenheid</th><th style="width:130px">Prijs excl.</th><th style="width:90px">Btw</th><th class="num" style="width:120px">Totaal</th><th></th></tr></thead>
|
||||
<tbody id="rows">{% for l in lines %}{{ row(l) }}{% endfor %}</tbody>
|
||||
</table></div>
|
||||
<div class="actions"><button type="button" class="btn ghost small" id="add">Regel toevoegen</button></div>
|
||||
<div class="actions"><button type="button" class="btn btn-outline-primary btn-sm" id="add">Regel toevoegen</button></div>
|
||||
<table class="totals">
|
||||
<tr><td>Subtotaal</td><td class="num" id="t-sub"></td></tr>
|
||||
<tbody id="t-vat"></tbody>
|
||||
<tr class="grand"><td>Totaal</td><td class="num" id="t-total"></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<label for="notes">Opmerkingen en voorwaarden</label>
|
||||
<textarea id="notes" name="notes">{{ qt.notes }}</textarea>
|
||||
</div>
|
||||
<div class="card"><div class="card-body">
|
||||
<label class="form-label" for="notes">Opmerkingen en voorwaarden</label>
|
||||
<textarea class="form-control" id="notes" name="notes">{{ qt.notes }}</textarea>
|
||||
</div></div>
|
||||
</form>
|
||||
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="card-top">
|
||||
<h2>Bestek uit ontwerp</h2>
|
||||
<div class="actions">
|
||||
{% if bestek %}
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('quote_print', qid=qt.id) }}#bestek">Bekijken</a>
|
||||
{% if designs %}<a class="btn btn-outline-primary btn-sm" href="{{ url_for('quote_bestek', qid=qt.id) }}">Opnieuw berekenen</a>{% endif %}
|
||||
<form method="post" action="{{ url_for('quote_bestek_delete', qid=qt.id) }}" class="inline" onsubmit="return confirm('Bestek loskoppelen? De offerteregels blijven staan.')"><button class="btn btn-outline-secondary btn-sm">Loskoppelen</button></form>
|
||||
{% elif designs %}
|
||||
<a class="btn btn-primary btn-sm" href="{{ url_for('quote_bestek', qid=qt.id) }}">Bestek maken</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if bestek %}
|
||||
<p class="muted" style="margin:0">{{ bestek.design.title }} · berekend {{ bestek.computedAt[:19]|dt }} · {{ bestek.chapters|length }} hoofdstukken ·
|
||||
{{ bestek.totals.exBtw|euro }} excl. btw ({{ bestek.totals.inclBtw|euro }} incl.). Het bestek staat als bijlage bij de offerte, ook in het klantportaal.</p>
|
||||
{% elif designs %}
|
||||
<p class="muted" style="margin:0">Hoeveelheden en prijzen uit het gekozen model van de klant (gevel, dak, kozijnen, stopcontacten, leidingen, …) als offerteregels en bijlage.</p>
|
||||
{% else %}
|
||||
<p class="empty" style="padding:0">Kies (en sla op) een klant met een gekozen model om een bestek uit het ontwerp te maken.</p>
|
||||
{% endif %}
|
||||
</div></div>
|
||||
|
||||
<form method="post" action="{{ url_for('quote_delete', qid=qt.id) }}" onsubmit="return confirm('Offerte verwijderen?')">
|
||||
<button class="btn quiet small">Offerte verwijderen</button>
|
||||
<button class="btn btn-outline-secondary btn-sm">Offerte verwijderen</button>
|
||||
</form>
|
||||
|
||||
<template id="tpl">{{ row({}) }}</template>
|
||||
|
||||
@@ -40,6 +40,13 @@
|
||||
.notes { margin-top: 32px; }
|
||||
.sign { margin-top: 40px; display: flex; gap: 48px; }
|
||||
.sign div { flex: 1; border-top: 1px solid rgba(0,0,0,.3); padding-top: 6px; color: #5c6670; font-size: 12px; }
|
||||
.bestek { break-before: page; margin-top: 48px; }
|
||||
.bestek h2 { font-family: "Kantumruy Pro", sans-serif; font-weight: 500; font-size: 15px; margin: 22px 0 4px; display: flex; justify-content: space-between; border-bottom: 2px solid #212121; padding-bottom: 3px; }
|
||||
.bestek table { width: 100%; border-collapse: collapse; }
|
||||
.bestek td { padding: 4px 8px 4px 0; border-bottom: 1px solid rgba(0,0,0,.08); vertical-align: top; }
|
||||
.bestek tr { break-inside: avoid; }
|
||||
.bestek .info td { color: #5c6670; font-style: italic; }
|
||||
.bestek .intro { color: #5c6670; font-size: 13px; }
|
||||
footer { margin-top: 40px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,.08); color: #5c6670; font-size: 12px; text-align: center; }
|
||||
@media print {
|
||||
body { background: #fff; }
|
||||
@@ -124,6 +131,22 @@
|
||||
{% if s.quote_footer %}<div style="white-space:pre-line;margin-bottom:6px">{{ s.quote_footer }}</div>{% endif %}
|
||||
{{ [('KvK ' ~ s.company_kvk) if s.company_kvk, ('Btw ' ~ s.company_btw) if s.company_btw, ('IBAN ' ~ s.company_iban) if s.company_iban]|select|join(' · ') }}
|
||||
</footer>
|
||||
|
||||
{% if bestek %}
|
||||
<section class="bestek" id="bestek">
|
||||
<h1>Bestek{% if bestek.design.title %}: {{ bestek.design.title }}{% endif %}</h1>
|
||||
<p class="intro">Bijlage bij offerte {{ qt.number }}. Omschrijving en hoeveelheden van de woning volgens het ontwerp{% if bestek.units %}
|
||||
({{ bestek.units }} module{{ 's' if bestek.units != 1 }}){% endif %}, berekend op {{ bestek.computedAt[:19]|dt('%d-%m-%Y') }}. Hoeveelheden zijn netto
|
||||
volgens tekening; de prijzen staan in de offerte.</p>
|
||||
{% for ch in bestek.chapters %}
|
||||
<h2><span>{{ loop.index }}. {{ ch.name }}</span></h2>
|
||||
<table><tbody>
|
||||
{% for l in ch.lines %}<tr><td>{{ l.label }}</td><td class="num">{{ l.qty|nr }} {{ l.unit }}</td></tr>{% endfor %}
|
||||
{% for l in ch.info %}<tr class="info"><td>{{ l.label }}</td><td class="num">{{ l.qty|nr }} {{ l.unit }}</td></tr>{% endfor %}
|
||||
</tbody></table>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-10
@@ -3,29 +3,31 @@
|
||||
{% block content %}
|
||||
<div class="head">
|
||||
<div><div class="kicker">Verkoop</div><h1>Offertes</h1></div>
|
||||
<form method="post" action="{{ url_for('quote_new') }}" class="inline"><button class="btn">Nieuwe offerte</button></form>
|
||||
<div class="actions">
|
||||
<form method="post" action="{{ url_for('quote_new') }}" class="inline"><button class="btn btn-primary">Nieuwe offerte</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<a href="{{ url_for('quotes') }}" class="{{ 'on' if not status }}">Alle</a>
|
||||
{% for k, l in QUOTE_STATUSES %}<a href="{{ url_for('quotes', status=k) }}" class="{{ 'on' if status == k }}">{{ l }}</a>{% endfor %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
{% if rows %}
|
||||
<div class="table-wrap"><table>
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<thead><tr><th>Nummer</th><th>Klant</th><th>Onderwerp</th><th>Datum</th><th>Status</th><th class="num">Totaal incl.</th></tr></thead>
|
||||
<tbody>
|
||||
{% for qt in rows %}
|
||||
<tr class="link" onclick="location='{{ url_for('quote_edit', qid=qt.id) }}'">
|
||||
<td><a href="{{ url_for('quote_edit', qid=qt.id) }}">{{ qt.number }}</a></td>
|
||||
<td>{{ qt.contact_name or '—' }}</td>
|
||||
<td>{{ qt.title }}</td>
|
||||
<td>{{ qt.date|dt('%d-%m-%Y') }}</td>
|
||||
<td><span class="chip {{ qt.status }}">{{ QSTATUS.get(qt.status, qt.status) }}</span></td>
|
||||
<td class="num">{{ qt.total|euro }}</td>
|
||||
<td class="stack-title"><a href="{{ url_for('quote_edit', qid=qt.id) }}">{{ qt.number }}</a></td>
|
||||
<td data-label="Klant">{{ qt.contact_name or '—' }}</td>
|
||||
<td data-label="Onderwerp">{{ qt.title }}</td>
|
||||
<td data-label="Datum">{{ qt.date|dt('%d-%m-%Y') }}</td>
|
||||
<td data-label="Status"><span class="chip {{ qt.status }}">{{ QSTATUS.get(qt.status, qt.status) }}</span></td>
|
||||
<td data-label="Totaal incl." class="num">{{ qt.total|euro }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">Nog geen offertes.</p>{% endif %}
|
||||
</div>
|
||||
</div></div>
|
||||
{% endblock %}
|
||||
|
||||
+54
-32
@@ -11,16 +11,16 @@
|
||||
{% 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>
|
||||
<button class="btn btn-outline-primary">Nieuwe offerte</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<a class="btn" href="{{ url_for('relation_edit', rid=r.id) }}">Bewerken</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('relation_edit', rid=r.id) }}">Bewerken</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="two">
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="row gx-4 stack">
|
||||
<div class="col-lg-7 col-xl-8">
|
||||
<div class="card o-2"><div class="card-body">
|
||||
<h2>Contactpersonen</h2>
|
||||
<div class="members">
|
||||
{% for m in members %}
|
||||
@@ -28,9 +28,9 @@
|
||||
<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 class="muted" style="overflow-wrap:anywhere">{{ [m.email, m.phone]|select|join(' · ') }}</div>
|
||||
</div>
|
||||
<div class="actions" style="margin:0">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
{% 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>
|
||||
@@ -41,41 +41,63 @@
|
||||
<p class="empty">Nog geen contactpersonen.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('relation_member', rid=r.id) }}" class="actions">
|
||||
<form method="post" action="{{ url_for('relation_member', rid=r.id) }}" class="row g-2 align-items-center mt-3">
|
||||
{% 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>
|
||||
<div class="col-md">
|
||||
<select class="form-select" name="contact_id" 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>
|
||||
</div>
|
||||
<div class="col-8 col-md-3"><input class="form-control" name="relation_role" placeholder="Rol" aria-label="Rol"></div>
|
||||
<div class="col-4 col-md-auto"><button class="btn btn-outline-primary w-100">Toevoegen</button></div>
|
||||
{% 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>
|
||||
<div class="col-12"><a class="btn btn-primary btn-sm" href="{{ url_for('contact_new', relation_id=r.id, company=r.name if r.kind == 'partner' else None) }}">Nieuwe contactpersoon</a></div>
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
{% if designs %}
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Ontwerpen en tekeningen</h2>
|
||||
<div class="table-responsive"><table class="table table-stack">
|
||||
<thead><tr><th>Ontwerp</th><th>Contact</th><th>Tekeningen</th><th>Ontvangen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for d in designs %}{% set p = pdfs.get(d.id, {}) %}
|
||||
<tr>
|
||||
<td class="stack-title"><a href="{{ url_for('design_viewer', fid=d.id) }}" target="_blank">{{ d.title }}</a>{% if d.chosen %} <span class="chip">Gekozen model</span>{% endif %}</td>
|
||||
<td data-label="Contact"><a href="{{ url_for('contact_view', cid=d.contact_id) }}">{{ d.contact_name }}</a></td>
|
||||
<td data-label="Tekeningen"><span>{% for k, label in PDF_KINDS.items() %}{% if p.get(k) %}<a href="{{ p[k].url }}" target="_blank">{{ (label[:1]|upper) ~ label[1:] }}</a>{% if not loop.last %} · {% endif %}{% endif %}{% endfor %}
|
||||
{% if not p %}<span class="muted">Wordt gemaakt bij openen van het contact</span>{% endif %}</span></td>
|
||||
<td data-label="Ontvangen" class="muted">{{ d.created_at|dt('%d-%m-%Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div></div>
|
||||
{% endif %}
|
||||
|
||||
{% if r.kind == 'partner' %}
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Producten van deze leverancier</h2>
|
||||
{% if products %}
|
||||
<div class="table-wrap"><table>
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<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>
|
||||
<td data-label="SKU"><code>{{ p.sku }}</code></td>
|
||||
<td class="stack-title"><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 data-label="Prijs excl." class="num">{{ p.price|euro }}</td>
|
||||
<td data-label="Bijgewerkt" 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>
|
||||
</div></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Recente activiteit</h2>
|
||||
<ul class="timeline">
|
||||
{% for a in activities %}
|
||||
@@ -87,11 +109,11 @@
|
||||
<li class="empty">Nog geen activiteit.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="col-lg-5 col-xl-4">
|
||||
<div class="card o-1"><div class="card-body">
|
||||
<h2>Gegevens</h2>
|
||||
<dl class="props">
|
||||
{% if r.type %}<dt>Type</dt><dd>{{ r.type }}</dd>{% endif %}
|
||||
@@ -103,10 +125,10 @@
|
||||
<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>
|
||||
</div></div>
|
||||
|
||||
{% if tasks %}
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Open taken</h2>
|
||||
{% for t in tasks %}
|
||||
<form method="post" action="{{ url_for('task_done', tid=t.id) }}" class="task">
|
||||
@@ -114,11 +136,11 @@
|
||||
<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>
|
||||
</div></div>
|
||||
{% endif %}
|
||||
|
||||
{% if r.kind == 'klant' %}
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<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>
|
||||
@@ -126,7 +148,7 @@
|
||||
{% else %}
|
||||
<p class="empty">Nog geen offertes.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,33 +7,33 @@
|
||||
<h1>{{ 'Relatie bewerken' if r.id else ('Nieuwe ' ~ RKIND.get(r.kind, 'relatie')|lower) }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" class="card">
|
||||
<div class="fields">
|
||||
<div class="full"><label for="name">Naam *</label><input id="name" name="name" value="{{ r.name }}" required placeholder="{{ 'Bijv. Familie Jansen' if r.kind == 'klant' else 'Bijv. Houthandel De Vries' }}"></div>
|
||||
<div><label for="kind">Soort</label>
|
||||
<select id="kind" name="kind">{% for k, l in RELATION_KINDS %}<option value="{{ k }}" {{ 'selected' if r.kind == k }}>{{ l }}</option>{% endfor %}</select>
|
||||
<form method="post" class="card"><div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label" for="name">Naam *</label><input class="form-control" id="name" name="name" value="{{ r.name }}" required placeholder="{{ 'Bijv. Familie Jansen' if r.kind == 'klant' else 'Bijv. Houthandel De Vries' }}"></div>
|
||||
<div class="col-md-6"><label class="form-label" for="kind">Soort</label>
|
||||
<select class="form-select" id="kind" name="kind">{% for k, l in RELATION_KINDS %}<option value="{{ k }}" {{ 'selected' if r.kind == k }}>{{ l }}</option>{% endfor %}</select>
|
||||
</div>
|
||||
<div><label for="type">Type</label>
|
||||
<input id="type" name="type" value="{{ r.type }}" list="types" autocomplete="off">
|
||||
<div class="col-md-6"><label class="form-label" for="type">Type</label>
|
||||
<input class="form-control" id="type" name="type" value="{{ r.type }}" list="types" autocomplete="off">
|
||||
<datalist id="types">{% for t in types %}<option value="{{ t }}">{% endfor %}</datalist>
|
||||
</div>
|
||||
<div><label for="email">E-mail</label><input id="email" type="email" name="email" value="{{ r.email }}"></div>
|
||||
<div><label for="phone">Telefoon</label><input id="phone" name="phone" value="{{ r.phone }}"></div>
|
||||
<div class="full"><label for="address">Adres</label><input id="address" name="address" value="{{ r.address }}"></div>
|
||||
<div><label for="postcode">Postcode</label><input id="postcode" name="postcode" value="{{ r.postcode }}"></div>
|
||||
<div><label for="city">Plaats</label><input id="city" name="city" value="{{ r.city }}"></div>
|
||||
<div><label for="website">Website</label><input id="website" name="website" value="{{ r.website }}"></div>
|
||||
<div><label for="kvk">KvK-nummer</label><input id="kvk" name="kvk" value="{{ r.kvk }}"></div>
|
||||
<div class="full"><label for="notes">Notities</label><textarea id="notes" name="notes">{{ r.notes }}</textarea></div>
|
||||
<div class="col-md-6"><label class="form-label" for="email">E-mail</label><input class="form-control" id="email" type="email" name="email" value="{{ r.email }}"></div>
|
||||
<div class="col-md-6"><label class="form-label" for="phone">Telefoon</label><input class="form-control" id="phone" type="tel" name="phone" value="{{ r.phone }}"></div>
|
||||
<div class="col-12"><label class="form-label" for="address">Adres</label><input class="form-control" id="address" name="address" value="{{ r.address }}"></div>
|
||||
<div class="col-sm-4"><label class="form-label" for="postcode">Postcode</label><input class="form-control" id="postcode" name="postcode" value="{{ r.postcode }}"></div>
|
||||
<div class="col-sm-8"><label class="form-label" for="city">Plaats</label><input class="form-control" id="city" name="city" value="{{ r.city }}"></div>
|
||||
<div class="col-md-6"><label class="form-label" for="website">Website</label><input class="form-control" id="website" name="website" value="{{ r.website }}"></div>
|
||||
<div class="col-md-6"><label class="form-label" for="kvk">KvK-nummer</label><input class="form-control" id="kvk" name="kvk" value="{{ r.kvk }}"></div>
|
||||
<div class="col-12"><label class="form-label" for="notes">Notities</label><textarea class="form-control" id="notes" name="notes">{{ r.notes }}</textarea></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn">Opslaan</button>
|
||||
<a class="btn quiet" href="{{ url_for('relation_view', rid=r.id) if r.id else url_for('relations', kind=r.kind) }}">Annuleren</a>
|
||||
<button class="btn btn-primary">Opslaan</button>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('relation_view', rid=r.id) if r.id else url_for('relations', kind=r.kind) }}">Annuleren</a>
|
||||
</div>
|
||||
</form>
|
||||
</div></form>
|
||||
{% if r.id %}
|
||||
<form method="post" action="{{ url_for('relation_delete', rid=r.id) }}" onsubmit="return confirm('Relatie verwijderen? Contactpersonen en producten blijven bestaan.')">
|
||||
<button class="btn quiet small">Relatie verwijderen</button>
|
||||
<button class="btn btn-outline-secondary btn-sm">Relatie verwijderen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
+12
-12
@@ -11,10 +11,10 @@
|
||||
<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">
|
||||
<input class="form-control" 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>
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('import_start', kind='relations') }}">Importeren</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('relation_edit', kind=kind, type=type or None) }}">{{ 'Nieuwe klant' if kind == 'klant' else 'Nieuwe partner' }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,23 +25,23 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
{% if rows %}
|
||||
<div class="table-wrap"><table>
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<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 %}
|
||||
<td class="stack-title"><a href="{{ url_for('relation_view', rid=r.id) }}">{{ r.name }}</a></td>
|
||||
<td data-label="Type">{% if r.type %}<span class="chip">{{ r.type }}</span>{% endif %}</td>
|
||||
<td data-label="Personen"><span>{{ r.member_names or '—' }}{% if r.members > 4 %} <span class="muted">+{{ r.members - 4 }}</span>{% endif %}</span></td>
|
||||
<td data-label="Plaats">{{ r.city }}</td>
|
||||
<td data-label="Contact" class="muted">{{ [r.phone, r.email]|select|join(' · ') }}</td>
|
||||
{% if kind == 'partner' %}<td data-label="Producten" 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>
|
||||
</div></div>
|
||||
{% endblock %}
|
||||
|
||||
+32
-32
@@ -1,16 +1,16 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Instellingen{% endblock %}
|
||||
{% macro field(key, label, type='text', placeholder='', full=false) %}
|
||||
<div class="{{ 'full' if full }}"><label for="{{ key }}">{{ label }}</label><input id="{{ key }}" type="{{ type }}" name="{{ key }}" value="{{ s.get(key, '') }}" placeholder="{{ placeholder }}"></div>
|
||||
<div class="{{ 'col-12' if full else 'col-md-6 col-xl-4' }}"><label class="form-label" for="{{ key }}">{{ label }}</label><input class="form-control" id="{{ key }}" type="{{ type }}" name="{{ key }}" value="{{ s.get(key, '') }}" placeholder="{{ placeholder }}"></div>
|
||||
{% endmacro %}
|
||||
{% block content %}
|
||||
<div class="head"><div><div class="kicker">Beheer</div><h1>Instellingen</h1></div></div>
|
||||
|
||||
<form method="post">
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Bedrijfsgegevens</h2>
|
||||
<p class="hint" style="margin-top:-.4rem;margin-bottom:1rem">Deze gegevens staan op de offertes.</p>
|
||||
<div class="fields">
|
||||
<p class="hint" style="margin:-.4rem 0 1rem">Deze gegevens staan op de offertes.</p>
|
||||
<div class="row g-3">
|
||||
{{ field('company_name', 'Bedrijfsnaam', full=true) }}
|
||||
{{ field('company_address', 'Adres') }}
|
||||
{{ field('company_postcode_city', 'Postcode en plaats') }}
|
||||
@@ -21,66 +21,66 @@
|
||||
{{ field('company_btw', 'Btw-nummer') }}
|
||||
{{ field('company_iban', 'IBAN') }}
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Offertes</h2>
|
||||
<div class="fields">
|
||||
<div class="row g-3">
|
||||
{{ field('quote_prefix', 'Voorvoegsel nummer', placeholder='OFF') }}
|
||||
{{ field('quote_valid_days', 'Standaard geldig (dagen)', 'number') }}
|
||||
<div class="full"><label for="quote_intro">Standaard inleiding</label><textarea id="quote_intro" name="quote_intro">{{ s.quote_intro }}</textarea></div>
|
||||
<div class="full"><label for="quote_footer">Voettekst (bijv. betalingsvoorwaarden)</label><textarea id="quote_footer" name="quote_footer">{{ s.quote_footer }}</textarea></div>
|
||||
<div class="col-12"><label class="form-label" for="quote_intro">Standaard inleiding</label><textarea class="form-control" id="quote_intro" name="quote_intro">{{ s.quote_intro }}</textarea></div>
|
||||
<div class="col-12"><label class="form-label" for="quote_footer">Voettekst (bijv. betalingsvoorwaarden)</label><textarea class="form-control" id="quote_footer" name="quote_footer">{{ s.quote_footer }}</textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card" id="mail">
|
||||
<div class="card" id="mail"><div class="card-body">
|
||||
<h2>E-mail koppelen</h2>
|
||||
<p class="hint" style="margin-top:-.4rem;margin-bottom:1rem">Gebruik voor Gmail of Microsoft 365 een app-wachtwoord. Wachtwoorden staan in de database: houd de map <code>data/</code> privé.</p>
|
||||
<p class="hint" style="margin:-.4rem 0 1rem">Gebruik voor Gmail of Microsoft 365 een app-wachtwoord. Wachtwoorden staan in de database: houd de map <code>data/</code> privé.</p>
|
||||
<h3>Ontvangen (IMAP)</h3>
|
||||
<div class="fields">
|
||||
<div class="row g-3">
|
||||
{{ field('imap_host', 'Server', placeholder='imap.gmail.com') }}
|
||||
{{ field('imap_port', 'Poort', 'number', '993') }}
|
||||
{{ field('imap_user', 'Gebruikersnaam', placeholder='info@vanemous.nl') }}
|
||||
<div><label for="imap_pass">Wachtwoord</label><input id="imap_pass" type="password" name="imap_pass" placeholder="{{ '•••••••• (ingesteld)' if s.imap_pass else '' }}" autocomplete="new-password"></div>
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="imap_pass">Wachtwoord</label><input class="form-control" id="imap_pass" type="password" name="imap_pass" placeholder="{{ '•••••••• (ingesteld)' if s.imap_pass else '' }}" autocomplete="new-password"></div>
|
||||
{{ field('imap_folder', 'Map', placeholder='INBOX') }}
|
||||
{{ field('imap_days', 'Aantal dagen terug ophalen', 'number', '30') }}
|
||||
</div>
|
||||
<h3>Versturen (SMTP)</h3>
|
||||
<div class="fields">
|
||||
<div class="row g-3">
|
||||
{{ field('smtp_host', 'Server', placeholder='smtp.gmail.com') }}
|
||||
{{ field('smtp_port', 'Poort', 'number', '587') }}
|
||||
{{ field('smtp_user', 'Gebruikersnaam', placeholder='leeg = zelfde als IMAP') }}
|
||||
<div><label for="smtp_pass">Wachtwoord</label><input id="smtp_pass" type="password" name="smtp_pass" placeholder="{{ '•••••••• (ingesteld)' if s.smtp_pass else 'leeg = zelfde als IMAP' }}" autocomplete="new-password"></div>
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="smtp_pass">Wachtwoord</label><input class="form-control" id="smtp_pass" type="password" name="smtp_pass" placeholder="{{ '•••••••• (ingesteld)' if s.smtp_pass else 'leeg = zelfde als IMAP' }}" autocomplete="new-password"></div>
|
||||
{{ field('smtp_from', 'Afzender', placeholder='Van Emous <info@vanemous.nl>', full=true) }}
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Formulieren en API</h2>
|
||||
<div class="fields">
|
||||
<div class="row g-3">
|
||||
{{ field('allowed_origins', 'Toegestane websites (komma-gescheiden)', placeholder='https://www.vanemous.nl', full=true) }}
|
||||
<div><label for="api_assign_user">Nieuwe leads toewijzen aan</label>
|
||||
<select id="api_assign_user" name="api_assign_user">
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="api_assign_user">Nieuwe leads toewijzen aan</label>
|
||||
<select class="form-select" id="api_assign_user" name="api_assign_user">
|
||||
<option value="">— Niemand —</option>
|
||||
{% for u in USERS %}<option value="{{ u.id }}" {{ 'selected' if s.api_assign_user == u.id|string }}>{{ u.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Relatietypes</h2>
|
||||
<p class="hint" style="margin-top:-.4rem;margin-bottom:1rem">Komma-gescheiden. Bestaande relaties houden hun type als je er een weghaalt.</p>
|
||||
<div class="fields">
|
||||
<div class="full"><label for="klant_types">Klanten (bundels)</label><input id="klant_types" name="klant_types" value="{{ s.klant_types or 'Huishouden / familie, Particulier, Zakelijk, Zorginstelling' }}"></div>
|
||||
<div class="full"><label for="partner_types">Partners</label><input id="partner_types" name="partner_types" value="{{ s.partner_types or 'Leverancier, Aannemer, Installateur, Architect / adviseur, Gemeente, Overig' }}"></div>
|
||||
<p class="hint" style="margin:-.4rem 0 1rem">Komma-gescheiden. Bestaande relaties houden hun type als je er een weghaalt.</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label" for="klant_types">Klanten (bundels)</label><input class="form-control" id="klant_types" name="klant_types" value="{{ s.klant_types or 'Huishouden / familie, Particulier, Zakelijk, Zorginstelling' }}"></div>
|
||||
<div class="col-12"><label class="form-label" for="partner_types">Partners</label><input class="form-control" id="partner_types" name="partner_types" value="{{ s.partner_types or 'Leverancier, Aannemer, Installateur, Architect / adviseur, Gemeente, Overig' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="actions" style="margin-bottom:1.5rem"><button class="btn">Instellingen opslaan</button></div>
|
||||
<div class="actions" style="margin-bottom:1.5rem"><button class="btn btn-primary">Instellingen opslaan</button></div>
|
||||
</form>
|
||||
|
||||
<div class="card">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>API-koppeling</h2>
|
||||
<dl class="props">
|
||||
<dt>Endpoint</dt><dd><code>POST {{ api_url }}</code></dd>
|
||||
@@ -123,7 +123,7 @@ const { ok, id } = await res.json();{% endfilter %}</pre>
|
||||
|
||||
<form method="post" onsubmit="return confirm('Nieuwe sleutel maken? Bestaande formulieren werken dan niet meer.')">
|
||||
<input type="hidden" name="action" value="apikey">
|
||||
<button class="btn quiet small">Nieuwe API-sleutel maken</button>
|
||||
<button class="btn btn-outline-secondary btn-sm">Nieuwe API-sleutel maken</button>
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
{% endblock %}
|
||||
|
||||
+12
-12
@@ -4,21 +4,21 @@
|
||||
<div class="head">
|
||||
<div><a class="label" href="{{ url_for('users') }}">← Gebruikers</a><h1>{{ 'Gebruiker bewerken' if u.id else 'Nieuwe gebruiker' }}</h1></div>
|
||||
</div>
|
||||
<form method="post" class="card">
|
||||
<div class="fields">
|
||||
<div><label for="name">Naam *</label><input id="name" name="name" value="{{ u.name }}" required></div>
|
||||
<div><label for="email">E-mailadres (inlognaam) *</label><input id="email" type="email" name="email" value="{{ u.email }}" required></div>
|
||||
<div><label for="role">Rol</label>
|
||||
<select id="role" name="role">{% for k, l in ROLES %}<option value="{{ k }}" {{ 'selected' if u.role == k }}>{{ l }}</option>{% endfor %}</select>
|
||||
<form method="post" class="card"><div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label" for="name">Naam *</label><input class="form-control" id="name" name="name" value="{{ u.name }}" required></div>
|
||||
<div class="col-md-6"><label class="form-label" for="email">E-mailadres (inlognaam) *</label><input class="form-control" id="email" type="email" name="email" value="{{ u.email }}" required></div>
|
||||
<div class="col-md-6"><label class="form-label" for="role">Rol</label>
|
||||
<select class="form-select" id="role" name="role">{% for k, l in ROLES %}<option value="{{ k }}" {{ 'selected' if u.role == k }}>{{ l }}</option>{% endfor %}</select>
|
||||
</div>
|
||||
<div><label for="password">{{ 'Nieuw wachtwoord' if u.id else 'Wachtwoord *' }}</label>
|
||||
<input id="password" type="password" name="password" minlength="8" {{ 'required' if not u.id }} autocomplete="new-password">
|
||||
<div class="col-md-6"><label class="form-label" for="password">{{ 'Nieuw wachtwoord' if u.id else 'Wachtwoord *' }}</label>
|
||||
<input class="form-control" id="password" type="password" name="password" minlength="8" {{ 'required' if not u.id }} autocomplete="new-password">
|
||||
{% if u.id %}<p class="hint">Leeg laten om het huidige wachtwoord te houden.</p>{% endif %}</div>
|
||||
<div class="full"><label><input type="checkbox" name="active" value="1" {{ 'checked' if u.active }}>Actief (mag inloggen)</label></div>
|
||||
<div class="col-12"><div class="form-check"><input class="form-check-input" type="checkbox" id="active" name="active" value="1" {{ 'checked' if u.active }}><label class="form-check-label" for="active">Actief (mag inloggen)</label></div></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn">Opslaan</button>
|
||||
<a class="btn quiet" href="{{ url_for('users') }}">Annuleren</a>
|
||||
<button class="btn btn-primary">Opslaan</button>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('users') }}">Annuleren</a>
|
||||
</div>
|
||||
</form>
|
||||
</div></form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
{% block content %}
|
||||
<div class="head">
|
||||
<div><div class="kicker">Team</div><h1>Gebruikers</h1></div>
|
||||
<a class="btn" href="{{ url_for('user_edit') }}">Nieuwe gebruiker</a>
|
||||
<div class="actions"><a class="btn btn-primary" href="{{ url_for('user_edit') }}">Nieuwe gebruiker</a></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="table-wrap"><table>
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<thead><tr><th>Naam</th><th>E-mail</th><th>Rol</th><th class="num">Contacten</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in rows %}
|
||||
<tr class="link" onclick="location='{{ url_for('user_edit', uid=u.id) }}'">
|
||||
<td><a href="{{ url_for('user_edit', uid=u.id) }}">{{ u.name }}</a></td>
|
||||
<td>{{ u.email }}</td>
|
||||
<td>{{ ROLE.get(u.role, u.role) }}</td>
|
||||
<td class="num">{{ u.n }}</td>
|
||||
<td>{% if not u.active %}<span class="chip verloren">Inactief</span>{% endif %}</td>
|
||||
<td class="stack-title"><a href="{{ url_for('user_edit', uid=u.id) }}">{{ u.name }}</a></td>
|
||||
<td data-label="E-mail">{{ u.email }}</td>
|
||||
<td data-label="Rol">{{ ROLE.get(u.role, u.role) }}</td>
|
||||
<td data-label="Contacten" class="num">{{ u.n }}</td>
|
||||
<td data-label="Status">{% if not u.active %}<span class="chip verloren">Inactief</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div></div>
|
||||
<p class="muted">Beheerders kunnen ook gebruikers en instellingen beheren. Klanten krijgen toegang via het klantportaal (op de pagina van het contact).</p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{# De engine verwijst relatief naar assets/ en kosten/: base = de gekopieerde configurator. #}
|
||||
<base href="{{ url_for('static', filename='configurator/') }}">
|
||||
<title>{{ name }} · 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=Mukta:wght@400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--orange: #b95913; --orange-dark: #a24e11; --orange-tint: rgba(185, 89, 19, 0.1);
|
||||
--ink: #212121; --ink-muted: #5c6670; --white: #ffffff; --ultra-light: #f9faf5; --line: rgba(0, 0, 0, 0.08);
|
||||
--radius-sm: 0.5rem; --radius-pill: 50px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; }
|
||||
body { font: 15px/1.5 "Mukta", system-ui, sans-serif; color: var(--ink); background: var(--white); font-variant-numeric: tabular-nums; }
|
||||
.vw { height: 100%; display: flex; flex-direction: column; background: var(--white); }
|
||||
.vw-stage { position: relative; flex: 1; min-height: 0; background: #dfe6ec; overflow: hidden; }
|
||||
.vw-3d, .vw-plan { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.vw-3d canvas { display: block; touch-action: none; }
|
||||
.vw-plan { display: none; background: #fff; }
|
||||
.view-plan .vw-plan { display: block; }
|
||||
.view-plan .vw-3d { visibility: hidden; }
|
||||
.vw-bar { position: absolute; top: 12px; left: 12px; right: 12px; display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; justify-content: space-between; pointer-events: none; }
|
||||
.vw-bar > * { pointer-events: auto; }
|
||||
.vw-viewbar, .vw-tools { display: flex; background: rgba(255, 255, 255, 0.94); border: 1px solid var(--line); border-radius: var(--radius-pill); padding: 3px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }
|
||||
.vw button { font: 600 14px/1 "Mukta", system-ui, sans-serif; color: var(--ink); background: none; border: 0; border-radius: var(--radius-pill); padding: 8px 14px; cursor: pointer; }
|
||||
.vw button:hover { background: var(--orange-tint); }
|
||||
.vw button.on { background: var(--orange); color: var(--white); }
|
||||
.vw button:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.vw-tools button { padding: 8px 10px; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.vw-tools-wrap { position: relative; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
|
||||
.vw-pop { position: absolute; top: calc(100% + 6px); right: 0; z-index: 2; width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 90px); overflow: auto;
|
||||
background: var(--white); border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14); padding: 12px 14px; font-size: 14px; }
|
||||
.vw-pop[hidden] { display: none; }
|
||||
.vw-pop h3 { margin: 0 0 8px; font-size: 12px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-muted); }
|
||||
.vw-pop select { width: 100%; font: inherit; padding: 6px 8px; border: 1px solid #8f969c; border-radius: var(--radius-sm); margin-bottom: 10px; }
|
||||
.vw-checks { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 10px; }
|
||||
.vw-checks label { display: flex; gap: 6px; align-items: center; cursor: pointer; }
|
||||
.vw-checks input { accent-color: var(--orange); }
|
||||
.vw-pdf { display: flex; justify-content: space-between; gap: 10px; padding: 8px 0; border-top: 1px solid var(--line); }
|
||||
.vw-pdf:first-child { border-top: 0; padding-top: 0; }
|
||||
.vw-pdf span { display: block; color: var(--ink-muted); font-size: 12px; line-height: 1.35; }
|
||||
.vw-pdf > div:last-child { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; white-space: nowrap; }
|
||||
.vw-pdf a { color: var(--orange); font-weight: 600; text-decoration: none; }
|
||||
.vw .vw-pdf button { padding: 4px 10px; border: 1px solid rgba(185, 89, 19, 0.35); color: var(--orange); font-size: 13px; }
|
||||
.vw-tools svg { width: 16px; height: 16px; display: block; }
|
||||
.vw-cutbar { position: absolute; left: 12px; top: 58px; display: none; align-items: center; gap: 10px; background: rgba(255, 255, 255, 0.94); border: 1px solid var(--line); border-radius: var(--radius-pill); padding: 6px 14px; font-size: 13px; }
|
||||
.view-cut .vw-cutbar { display: flex; }
|
||||
.vw-cutbar input { accent-color: var(--orange); width: 140px; }
|
||||
.vw-cutbar output { min-width: 3.5em; font-weight: 600; }
|
||||
.vw-hint { position: absolute; left: 12px; bottom: 10px; font-size: 12px; color: var(--ink-muted); background: rgba(255, 255, 255, 0.85); padding: 2px 10px; border-radius: var(--radius-pill); }
|
||||
.vw-toast { position: absolute; left: 50%; bottom: 44px; transform: translateX(-50%); background: var(--ink); color: var(--white); font-size: 13px; padding: 6px 14px; border-radius: var(--radius-pill); opacity: 0; transition: opacity 0.2s; pointer-events: none; white-space: nowrap; }
|
||||
.vw-toast.on { opacity: 0.92; }
|
||||
.vw-busy, .vw-msg { position: absolute; inset: 0; display: none; place-items: center; text-align: center; padding: 2rem; color: var(--ink-muted); }
|
||||
.is-busy .vw-busy, .is-error .vw-msg { display: grid; }
|
||||
.vw-facts { display: flex; flex-wrap: wrap; gap: 6px 22px; padding: 10px 16px; border-top: 1px solid var(--line); background: var(--ultra-light); font-size: 13px; }
|
||||
.vw-facts span { display: block; color: var(--ink-muted); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
|
||||
.vw-facts b { font-weight: 600; }
|
||||
@media (max-width: 560px) { .vw-hint { display: none; } .vw button { padding: 7px 10px; } .vw-tools span { display: none; } .vw-viewbar button { padding: 7px 9px; font-size: 13px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="viewer" class="vw is-busy">
|
||||
<div class="vw-stage">
|
||||
<div class="vw-3d"></div>
|
||||
<canvas class="vw-plan" aria-label="Plattegrond, noord boven"></canvas>
|
||||
<div class="vw-bar">
|
||||
<div class="vw-viewbar" role="group" aria-label="Weergave">
|
||||
<button type="button" data-view="3d" class="on">3D</button>
|
||||
<button type="button" data-view="cut" title="Wanden afgesneden op een hoogte boven de vloer">Doorsnede</button>
|
||||
<button type="button" data-view="plan">Plattegrond</button>
|
||||
</div>
|
||||
<div class="vw-tools-wrap">
|
||||
<div class="vw-tools">
|
||||
<button type="button" class="vw-layers-btn" data-pop=".vw-pop-layers" title="Bouwfase en lagen (regelwerk, elektra, …)" aria-label="Lagen">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 1 0.8 4.6 8 8.2l7.2-3.6L8 1Zm-5.1 3.6L8 2.3l5.1 2.3L8 6.9 2.9 4.6Z"/><path d="m1.6 7.3-.8.4L8 11.3l7.2-3.6-.8-.4L8 10.5 1.6 7.3Z"/><path d="m1.6 10.4-.8.4L8 14.4l7.2-3.6-.8-.4L8 13.6l-6.4-3.2Z"/></svg><span>Lagen</span>
|
||||
</button>
|
||||
<button type="button" data-pop=".vw-pop-pdf" title="PDF: werktekening en klant-PDF" aria-label="PDF">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M4 1h5.5L13 4.5V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1Zm.2 1.2v11.6h7.6V5.2H9V2.2H4.2Z"/><path d="M5.5 8h5v1.1h-5V8Zm0 2.4h5v1.1h-5v-1.1Z"/></svg><span>PDF</span>
|
||||
</button>
|
||||
<button type="button" class="vw-shot" title="Afbeelding opslaan" aria-label="Afbeelding opslaan">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 11.5a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm0-1.2a1.8 1.8 0 1 1 0-3.6 1.8 1.8 0 0 1 0 3.6Z"/><path d="M5.6 2h4.8l1 1.5H14a1 1 0 0 1 1 1V13a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1h2.6l1-1.5Zm.6 1.2-1 1.5H2.2v8.1h11.6V4.7h-2.9l-1-1.5H6.2Z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="vw-full" title="Schermvullend" aria-label="Schermvullend" hidden>
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1.5 1h4v1.2H2.7v2.8H1.5V1Zm9 0h4v4h-1.2V2.2h-2.8V1ZM1.5 11h1.2v2.8h2.8V15h-4v-4Zm11.8 0h1.2v4h-4v-1.2h2.8V11Z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="vw-pop vw-pop-layers" hidden>
|
||||
<h3>Bouwfase</h3>
|
||||
<select aria-label="Bouwfase"></select>
|
||||
<h3>Lagen</h3>
|
||||
<div class="vw-checks"></div>
|
||||
</div>
|
||||
<div class="vw-pop vw-pop-pdf" hidden><div class="vw-pdfs"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="vw-cutbar"><span>Snijhoogte</span>
|
||||
<input type="range" min="300" max="3000" step="50" value="1500" aria-label="Snijhoogte boven de vloer">
|
||||
<output>1,50 m</output></label>
|
||||
<div class="vw-hint"></div>
|
||||
<div class="vw-toast" role="status" aria-live="polite"></div>
|
||||
<div class="vw-busy" aria-live="polite">Ontwerp opbouwen…</div>
|
||||
<div class="vw-msg"></div>
|
||||
</div>
|
||||
<div class="vw-facts"></div>
|
||||
</div>
|
||||
{% if engine %}
|
||||
<script type="application/json" id="design">{{ design|tojson }}</script>
|
||||
<script type="application/json" id="viewer-cfg">{{ cfg|tojson }}</script>
|
||||
{% for src in scripts %}<script src="{{ src }}"></script>
|
||||
{% endfor %}<script src="{{ url_for('static', filename='viewer.js') }}"></script>
|
||||
{% else %}
|
||||
<script>
|
||||
document.getElementById('viewer').className = 'vw is-error';
|
||||
document.querySelector('.vw-msg').textContent = 'De 3D-engine van de configurator ontbreekt. Draai tools/configurator-sync.sh in de CRM-map.';
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user