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
+78 -8
View File
@@ -11,6 +11,20 @@ Eenvoudige CRM in Python (Flask) met SQLite, in de Van Emous-huisstijl. Versie 0
familie of bedrijf, met meerdere contactpersonen en hun rol) en **partners** met een type (leverancier,
aannemer, installateur, ...; de typen pas je aan bij Instellingen). Een lead omzetten naar klant zet de
persoon in een nieuwe of bestaande klantrelatie. Leveranciers van producten worden automatisch partners.
- **Gekozen model** per lead/contact: een woningontwerp uit de configurator (`house-design`-JSON, via het
formulier, de API of geüpload bij Bestanden) wordt het gekozen model; het nieuwste telt, een ander ontwerp
kies je op de contactpagina. Daar staat het in een **3D-viewer** op de engine van de configurator
(`/files/<id>/viewer`): 3D, doorsnede en plattegrond, met maten, m², inhoud, gewicht en een indicatieve
prijs (uit de kosten-CSV's van de configurator). In de leadlijst staat het model als kolom.
Met **Lagen** in de viewer kies je een bouwfase (t/m regelwerk, elektra, beplating, …) of losse lagen
(regelwerk, elektra, kozijnen, dak, …), zoals Lagen in de configurator, zodat de ploeg de opbouw kan bekijken.
- **PDF's per ontwerp** (`HousePdf` van de configurator, gemaakt in de viewer en bewaard bij het contact): een
**werktekening** (alle tekenbladen, elektra, zaaglijst en kosten; intern, niet in het klantportaal) en een
**klant-PDF** zoals de light-configurator die maakt (`LightPdf`: voorblad met beeld, kerncijfers, keuzes, indicatieve
prijs en QR-code, dan aanzichten en plattegrond en de werkwijze; wel in het portaal). Ontbreken ze, dan maakt de viewer ze vanzelf bij
het eerste openen; opnieuw maken via PDF in de weergave. Op de contactpagina bij het gekozen model en op de
relatiepagina (Ontwerpen en tekeningen, voor alle contactpersonen).
- Op mobiel staan bij contact en relatie de gegevens bovenaan, de 3D-weergave verderop.
- **Taken/opvolging** per contact met datum en eigenaar; "Mijn taken" op het dashboard.
- **API voor formulieren** (`POST /api/leads`): JSON, gewone formulieren en multipart met bestanden.
Nederlandse en Engelse veldnamen worden herkend, onbekende velden bewaard. Dubbele leads (zelfde
@@ -29,6 +43,22 @@ Eenvoudige CRM in Python (Flask) met SQLite, in de Van Emous-huisstijl. Versie 0
formaat als de import.
- **Offertes** met regels (product of vrije regel), btw per tarief en een afdrukversie die je als PDF
opslaat via de browser.
- **Bestek uit ontwerp** bij een offerte (of "Offerte met bestek" bij het gekozen model): de engine van de
configurator rekent het ontwerp door zoals Kosten & tijden (`static/bestek.js`, `CostModel.compute` met de
kosten-CSV's) en zet alle posten met hoeveelheden in hoofdstukken (casco, isolatie, gevel, dak, vloer,
kozijnen, afwerking, inrichting, verlichting, elektra met wcd's, schakelaars en meters leiding per type,
transport en plaatsing). Elke post krijgt zijn kostprijs plus algemene kosten en marge; samen precies het
totaal van de configurator. Schroeven, zaag- en boorwerk staan per hoofdstuk samen als "Bevestiging en
bewerking". In de offerte per post (standaard: aantal × prijs per eenheid) of per hoofdstuk; het
afrondingsverschil van de prijzen per eenheid gaat naar de laatste regel van 1 post zonder product, zodat
het totaal precies klopt. Het bestek zelf komt als bijlage (zonder prijzen) in de afdrukversie en het portaal.
- **Producten uit het bestek:** posten met een vaste prijs per eenheid (wcd, schakelaar, aansluitpunten,
leidingen per type, spots en lampen, toilet, douche, keuken per m, deuren, ramen per maat, gevel- en
dakbedekking per m², isolatie, plaatmateriaal, ...) worden gekoppeld aan een product met de kostencode van de
configurator als SKU (`elektra.wcdDubbel`, `inrichting.toilet`, `kozijnen.raam.1200x500`; dezelfde codes als
in `kosten/leveranciers.csv`). Ontbreekt het product, dan wordt het aangemaakt met de prijs per eenheid van de
berekening; bestaande productprijzen worden alleen bijgewerkt als je dat aanvinkt (met producthistorie).
De regels staan in `PRODUCT_RULES` in `static/bestek.js`.
- **Gebruikers** met de rollen beheerder (ook gebruikers en instellingen) en medewerker.
- **Klantportaal** (`/portal`): de klant krijgt een uitnodigingslink (kopiëren of mailen vanaf het
contact), stelt een wachtwoord in en kan dan offertes bekijken, accepteren of afwijzen, bestanden
@@ -44,23 +74,57 @@ python3 -m venv .venv
.venv/bin/python app.py # http://127.0.0.1:5000
```
De viewer heeft de engine van de configurator nodig (niet in git, ca. 160 MB met texturen):
```sh
tools/configurator-sync.sh [../configurator] # kopieert js/, assets/, kosten/ naar static/configurator/
```
Opnieuw draaien als er in de configurator iets aan de weergave verandert; `deploy.sh` doet dit zelf bij elke deploy.
Demoleads (lorem ipsum, elk met een standaardmodel als gekozen model):
```sh
node tools/export-models.js # standaardmodellen (HouseModel.MODELS) → demo/modellen/*.json
.venv/bin/python app.py demo # leads aanmaken; bestaande demoleads worden overgeslagen
```
Bij de eerste keer maak je het beheerdersaccount aan. Daarna vul je onder Instellingen de
bedrijfsgegevens, de e-mail en de toegestane websites voor de API in.
## Online zetten
## Online zetten (server)
Draai de app achter nginx/Apache met HTTPS, bijvoorbeeld met gunicorn:
Op een nieuwe Ubuntu-server (26.04 LTS) eenmalig, nadat de DNS van het domein naar de server wijst:
```sh
.venv/bin/pip install gunicorn
.venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 app:app
scp -r server ubuntu@SERVER:
ssh ubuntu@SERVER sudo bash server/setup.sh crm.voorbeeld.nl jij@voorbeeld.nl
```
E-mail automatisch ophalen (cron, elke 10 minuten):
`server/setup.sh` installeert Python, nginx, certbot, rsync en sqlite3, zet de tijdzone op Europe/Amsterdam,
maakt de gebruiker `crm` met `/srv/vanemous-crm` (`releases/`, `current`, `data/`), de systemd-dienst
`vanemous-crm` (gunicorn op 127.0.0.1:5050, `CRM_PROXY=1`), de nginx-site met HTTPS, de firewall (SSH, 80, 443),
automatische beveiligingsupdates en cron (e-mail ophalen elke 10 minuten, elke nacht een databaseback-up, 7 dagen rond).
Een versie uitrollen (of terugrollen) vanaf je eigen machine:
```sh
tools/release.sh crm@crm.voorbeeld.nl # nieuwste tag
tools/release.sh crm@crm.voorbeeld.nl v0.0.1 # bepaalde versie
```
*/10 * * * * cd /pad/naar/crm && .venv/bin/python app.py sync
```
Dat stuurt `git archive <tag>` en de 3D-engine uit `../configurator` naar de server; `server/activate.sh` maakt daar
een venv, back-upt de database, zet `current` om en herstart de dienst. Start de nieuwe versie niet, dan gaat hij
terug naar de vorige. Logboek: `sudo journalctl -u vanemous-crm`.
`CRM_PROXY=1` laat de app de `X-Forwarded-Proto`/`-Host`-headers van de proxy volgen (links naar buiten, zoals
portaaluitnodigingen, worden dan `https://domein/...`) en zet sessiecookies op alleen-HTTPS. De proxy moet
uploads tot 25 MB doorlaten (nginx: `client_max_body_size 25m`).
Lokaal zoals op de server, maar zonder nginx: `./deploy.sh` (zie het script).
Back-ups buiten de server (bijv. de back-upoptie van OVH of een rsync van `/srv/vanemous-crm/data`) regel je apart:
de nachtelijke kopie staat op dezelfde schijf en dekt `uploads/` niet.
## Opslag
@@ -79,5 +143,11 @@ importer.py CSV/JSON inlezen en kolommen herkennen
mail.py IMAP/SMTP
schema.sql database
templates/ pagina's (Jinja)
static/ style.css (huisstijltokens), fx.js (GSAP-effecten) en logo.svg
static/ style.css (huisstijltokens), fx.js (GSAP-effecten), logo.svg, viewer.js (3D-viewer), bestek.js
static/configurator/ engine van de configurator (tools/configurator-sync.sh, niet in git)
tools/ configurator-sync.sh, export-models.js (standaardmodellen als JSON)
demo/modellen standaardmodellen als house-design-JSON voor de demoleads
```
De plattegrond in `static/viewer.js` is overgenomen uit `js/light/light.js` (`drawPlan`) van de
configurator; verandert die, dan hier meenemen.