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:
Machiel
2026-10-15 11:58:52 +02:00
co-authored by Claude Opus 5.5
parent bffa053c05
commit 7d302dc405
48 changed files with 2769 additions and 698 deletions
+79
View File
@@ -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 &amp; 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>