From 16f4cc7bc80fe08f599fc889083cd73b210e06a5 Mon Sep 17 00:00:00 2001 From: killercow Date: Mon, 12 Jan 2026 12:45:43 +0000 Subject: [PATCH] Initial commit: EasySmartInventory v1.0.0 - Python HTML generator from YAML config - 3 themes: modern, corporate, minimal - Field types: text, number, date, dropdown, multiselect, boolean, photo, textarea - Auto-save to localStorage and URL hash - CSV export with base64 photos - Mailto with configurable subject - Responsive design for mobile/tablet/desktop - Company logo and color customization --- .gitignore | 9 + README.md | 111 +++ examples/config.yaml | 334 +++++++ examples/inventory_corporate.html | 1078 ++++++++++++++++++++++ examples/inventory_minimal.html | 1109 +++++++++++++++++++++++ examples/inventory_modern.html | 1140 +++++++++++++++++++++++ project_findings.md | 78 ++ project_plan.md | 157 ++++ project_progress.md | 40 + requirements.txt | 2 + src/__init__.py | 1 + src/generator.py | 255 ++++++ src/templates.py | 1398 +++++++++++++++++++++++++++++ src/yaml_parser.py | 225 +++++ 14 files changed, 5937 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 examples/config.yaml create mode 100644 examples/inventory_corporate.html create mode 100644 examples/inventory_minimal.html create mode 100644 examples/inventory_modern.html create mode 100644 project_findings.md create mode 100644 project_plan.md create mode 100644 project_progress.md create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/generator.py create mode 100644 src/templates.py create mode 100644 src/yaml_parser.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb9fc87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.venv/ +__pycache__/ +*.pyc +.secrets +exports/*.csv +*.egg-info/ +dist/ +build/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8a7f13 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# EasySmartInventory + +Genereer interactieve standalone HTML inventarisatieformulieren op basis van YAML configuratie. + +## Features + +- πŸ“ **YAML configuratie** - Definieer alle velden, validatie en opties in YAML +- πŸ“± **Responsive** - Werkt op iPad, iPhone, Android en desktop +- πŸ’Ύ **Auto-save** - Automatisch opslaan naar localStorage +- πŸ“€ **CSV Export** - Exporteer naar CSV met base64 foto's +- πŸ“§ **Mailto** - Direct mailen vanuit de browser +- 🎨 **Themes** - Kies uit modern, corporate of minimal stijl +- 🏒 **Branding** - Eigen logo en kleuren + +## Installatie + +```bash +cd EasySmartInventory +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +## Gebruik + +```bash +# Genereer HTML uit YAML configuratie +python src/generator.py examples/config.yaml + +# Output: examples/Machine_Inventarisatie.html +``` + +## YAML Configuratie + +Zie `examples/config.yaml` voor een volledig voorbeeld. + +### Basis structuur + +```yaml +name: "Inventarisatie Naam" +version: "1.0.0" + +style: + theme: "modern" # modern, corporate, minimal + logo: "base64_string_or_url" + primary_color: "#007bff" + +export: + csv: + enabled: true + mailto: + enabled: true + to: "inventory@company.com" + subject_prefix: "Inventarisatie" + subject_fields: ["Serienummer"] + +sections: + - name: "Sectie Naam" + fields: + - id: "veld_id" + label: "Veld Label" + type: "text" + required: true +``` + +### Field Types + +| Type | Beschrijving | Extra opties | +|------|--------------|--------------| +| `text` | Tekstveld | `min_length`, `placeholder` | +| `number` | Numeriek veld | `min`, `max` | +| `date` | Datumveld | `format` (dd-mm-yyyy) | +| `dropdown` | Keuzelijst | `options` (array) | +| `multiselect` | Meerdere keuzes | `options` (array) | +| `boolean` | Ja/Nee toggle | - | +| `photo` | Foto upload | `max_width`, `max_height` | +| `textarea` | Groot tekstveld | `rows` | + +### Validatie opties + +- `required: true` - Veld is verplicht +- `min_length: 5` - Minimaal aantal karakters +- `min: 0` / `max: 100` - Bereik voor nummers + +## Voorbeelden + +Drie voorbeeldstijlen zijn beschikbaar: + +- `examples/inventory_modern.html` - Modern design met shadows +- `examples/inventory_corporate.html` - Zakelijk en professioneel +- `examples/inventory_minimal.html` - Minimalistisch en clean + +## Export + +### CSV +- Klik "Exporteer CSV" om data te downloaden +- Foto's worden opgeslagen als base64 +- UTF-8 encoding met BOM voor Excel + +### Email +- Klik "Verstuur per Email" om mailclient te openen +- Subject bevat configureerbare prefix + veldwaarden +- Body bevat gestructureerde data + +## Versie + +Huidige versie: 1.0.0 + +## Licentie + +MIT License diff --git a/examples/config.yaml b/examples/config.yaml new file mode 100644 index 0000000..747df15 --- /dev/null +++ b/examples/config.yaml @@ -0,0 +1,334 @@ +# Machine Inventarisatie - Voorbeeld Configuratie +# EasySmartInventory v1.0.0 + +name: "Machine_Inventarisatie" +version: "1.0.0" + +# Styling configuratie +style: + theme: "modern" # modern, corporate, minimal + logo: "" # Base64 string of URL naar logo + primary_color: "#0066cc" + secondary_color: "#28a745" + background_color: "#f8f9fa" + text_color: "#333333" + error_color: "#dc3545" + success_color: "#28a745" + +# Export configuratie +export: + csv: + enabled: true + folder: "./exports" + include_photo: true + mailto: + enabled: true + to: "inventaris@bedrijf.nl" + subject_prefix: "Inventarisatie" + subject_fields: ["serienummer", "merk"] + include_timestamp: true + +# Auto-save configuratie +autosave: + enabled: true + interval_seconds: 5 + use_url_hash: true + use_localstorage: true + +# Secties en velden +sections: + - name: "Basisinformatie" + description: "Algemene gegevens van het apparaat" + fields: + - id: "serienummer" + label: "Serienummer" + type: "text" + required: true + min_length: 3 + placeholder: "Voer serienummer in" + + - id: "type_nummer" + label: "Type-/modelnummer" + type: "text" + required: false + placeholder: "Bijv. HP ProBook 450 G8" + + - id: "merk" + label: "Merk / fabrikant" + type: "dropdown" + required: true + options: + - "HP" + - "Dell" + - "Lenovo" + - "Apple" + - "ASUS" + - "Acer" + - "Microsoft" + - "Samsung" + - "Anders" + + - id: "soort_apparaat" + label: "Soort apparaat / categorie" + type: "dropdown" + required: true + options: + - "Laptop" + - "Desktop" + - "Server" + - "Printer" + - "Scanner" + - "Monitor" + - "Netwerkapparatuur" + - "Mobiel apparaat" + - "Anders" + + - id: "productienaam" + label: "Productienaam of -omschrijving" + type: "text" + required: false + + - id: "productiejaar" + label: "Productiejaar" + type: "number" + required: false + min: 1990 + max: 2030 + + - id: "aanschafdatum" + label: "Aanschafdatum" + type: "date" + required: false + format: "dd-mm-yyyy" + + - id: "aankoopprijs" + label: "Aankoopprijs of vervangingswaarde (€)" + type: "number" + required: false + min: 0 + placeholder: "0.00" + + - id: "asset_id" + label: "Intern identificatienummer (asset-ID)" + type: "text" + required: false + placeholder: "Bijv. AST-2024-001" + + - name: "Locatie & Toewijzing" + description: "Waar bevindt het apparaat zich en wie is verantwoordelijk" + fields: + - id: "locatie_adres" + label: "Locatienummer of adres" + type: "text" + required: false + + - id: "ruimte" + label: "Ruimte / afdeling / verdieping" + type: "text" + required: false + placeholder: "Bijv. Kantoor 2.15, IT-afdeling" + + - id: "gebruiker" + label: "Gebruiker / verantwoordelijke persoon" + type: "text" + required: false + + - id: "vestiging" + label: "Vestiging / filiaal" + type: "dropdown" + required: false + options: + - "Hoofdkantoor" + - "Vestiging Noord" + - "Vestiging Zuid" + - "Vestiging Oost" + - "Vestiging West" + - "Thuiswerker" + - "Anders" + + - id: "mobiel_gebruik" + label: "Mobiel gebruik (werkplek/voertuig)" + type: "boolean" + required: false + + - name: "Technische gegevens" + description: "Technische specificaties en netwerkinformatie" + fields: + - id: "specificaties" + label: "Specificaties (vermogen, capaciteit, etc.)" + type: "textarea" + required: false + rows: 3 + placeholder: "Bijv. Intel i7, 16GB RAM, 512GB SSD" + + - id: "component_serienummers" + label: "Serienummers van componenten" + type: "textarea" + required: false + rows: 2 + + - id: "software_versie" + label: "Softwareversie / firmwareversie" + type: "text" + required: false + + - id: "ip_adres" + label: "IP-adres" + type: "text" + required: false + placeholder: "Bijv. 192.168.1.100" + + - id: "mac_adres" + label: "MAC-adres" + type: "text" + required: false + placeholder: "Bijv. AA:BB:CC:DD:EE:FF" + + - id: "energieklasse" + label: "Energieklasse" + type: "dropdown" + required: false + options: + - "A+++" + - "A++" + - "A+" + - "A" + - "B" + - "C" + - "D" + - "Onbekend" + + - id: "certificeringen" + label: "Certificeringen" + type: "multiselect" + required: false + options: + - "CE" + - "ISO 9001" + - "ISO 27001" + - "Energy Star" + - "RoHS" + - "GDPR Compliant" + + - name: "Status en conditie" + description: "Huidige status en onderhoudsinformatie" + fields: + - id: "status" + label: "Huidige status" + type: "dropdown" + required: true + options: + - "In gebruik" + - "In opslag" + - "Buiten gebruik" + - "Defect" + - "In reparatie" + - "Verhuurd" + - "Gereserveerd" + + - id: "conditie" + label: "Conditie / onderhoudstoestand" + type: "dropdown" + required: false + options: + - "Uitstekend" + - "Goed" + - "Redelijk" + - "Matig" + - "Slecht" + + - id: "laatste_onderhoud" + label: "Laatste onderhoudsdatum" + type: "date" + required: false + format: "dd-mm-yyyy" + + - id: "volgend_onderhoud" + label: "Volgende geplande onderhoudsdatum" + type: "date" + required: false + format: "dd-mm-yyyy" + + - id: "garantie_tot" + label: "Garantie tot" + type: "date" + required: false + format: "dd-mm-yyyy" + + - id: "leverancier" + label: "Leverancier / onderhoudspartner" + type: "text" + required: false + + - id: "onderhoudscontract" + label: "Onderhoudscontractnummer" + type: "text" + required: false + + - name: "Documentatie & beheer" + description: "Administratieve gegevens en documentatie" + fields: + - id: "factuurnummer" + label: "Factuurnummer / aankoopreferentie" + type: "text" + required: false + + - id: "leveringsdatum" + label: "Leveringsdatum" + type: "date" + required: false + format: "dd-mm-yyyy" + + - id: "handleiding_link" + label: "Handleidingen / documenten (link)" + type: "text" + required: false + placeholder: "URL naar documentatie" + + - id: "foto" + label: "Foto van het apparaat" + type: "photo" + required: false + max_width: 1200 + max_height: 1200 + + - id: "opmerkingen" + label: "Opmerkingen / bijzonderheden" + type: "textarea" + required: false + rows: 4 + placeholder: "Modificaties, risico's, speciale aandachtspunten..." + + - name: "Afvoer / einde levensduur" + description: "Informatie over afvoer en recycling" + fields: + - id: "datum_buiten_gebruik" + label: "Datum buiten gebruikstelling" + type: "date" + required: false + format: "dd-mm-yyyy" + + - id: "reden_afvoer" + label: "Reden van afvoer" + type: "dropdown" + required: false + options: + - "Einde levensduur" + - "Defect (niet repareerbaar)" + - "Verouderd" + - "Vervangen" + - "Verkocht" + - "Gestolen" + - "Verloren" + - "Anders" + + - id: "afvoermethode" + label: "Verwijderlocatie of afvoermethode" + type: "text" + required: false + + - id: "hergebruik_info" + label: "Hergebruik / donatie / recycling informatie" + type: "textarea" + required: false + rows: 2 diff --git a/examples/inventory_corporate.html b/examples/inventory_corporate.html new file mode 100644 index 0000000..67ad386 --- /dev/null +++ b/examples/inventory_corporate.html @@ -0,0 +1,1078 @@ + + + + + + + Machine_Inventarisatie + + + +
+
+ +

Machine_Inventarisatie

+
Versie 1.0.0
+
+ +
+
+

Basisinformatie

+

Algemene gegevens van het apparaat

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+

Locatie & Toewijzing

+

Waar bevindt het apparaat zich en wie is verantwoordelijk

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + Ja +
+
+
+
+

Technische gegevens

+

Technische specificaties en netwerkinformatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + + + + + +
+
+
+
+

Status en conditie

+

Huidige status en onderhoudsinformatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+

Documentatie & beheer

+

Administratieve gegevens en documentatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + +
πŸ“· Klik om foto te maken of uploaden
+ +
+
+
+ + +
+
+
+
+

Afvoer / einde levensduur

+

Informatie over afvoer en recycling

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + + +
+ +
+ +
+ Gereed + EasySmartInventory v1.0.0 +
+
+ +
+ + + + \ No newline at end of file diff --git a/examples/inventory_minimal.html b/examples/inventory_minimal.html new file mode 100644 index 0000000..fce1a92 --- /dev/null +++ b/examples/inventory_minimal.html @@ -0,0 +1,1109 @@ + + + + + + + Machine_Inventarisatie + + + +
+
+ +

Machine_Inventarisatie

+
Versie 1.0.0
+
+ +
+
+

Basisinformatie

+

Algemene gegevens van het apparaat

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+

Locatie & Toewijzing

+

Waar bevindt het apparaat zich en wie is verantwoordelijk

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + Ja +
+
+
+
+

Technische gegevens

+

Technische specificaties en netwerkinformatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + + + + + +
+
+
+
+

Status en conditie

+

Huidige status en onderhoudsinformatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+

Documentatie & beheer

+

Administratieve gegevens en documentatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + +
πŸ“· Klik om foto te maken of uploaden
+ +
+
+
+ + +
+
+
+
+

Afvoer / einde levensduur

+

Informatie over afvoer en recycling

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + + +
+ +
+ +
+ Gereed + EasySmartInventory v1.0.0 +
+
+ +
+ + + + \ No newline at end of file diff --git a/examples/inventory_modern.html b/examples/inventory_modern.html new file mode 100644 index 0000000..6abc3c1 --- /dev/null +++ b/examples/inventory_modern.html @@ -0,0 +1,1140 @@ + + + + + + + Machine_Inventarisatie + + + +
+
+ +

Machine_Inventarisatie

+
Versie 1.0.0
+
+ +
+
+

Basisinformatie

+

Algemene gegevens van het apparaat

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+

Locatie & Toewijzing

+

Waar bevindt het apparaat zich en wie is verantwoordelijk

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + Ja +
+
+
+
+

Technische gegevens

+

Technische specificaties en netwerkinformatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + + + + + +
+
+
+
+

Status en conditie

+

Huidige status en onderhoudsinformatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+

Documentatie & beheer

+

Administratieve gegevens en documentatie

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + +
πŸ“· Klik om foto te maken of uploaden
+ +
+
+
+ + +
+
+
+
+

Afvoer / einde levensduur

+

Informatie over afvoer en recycling

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + + +
+ +
+ +
+ Gereed + EasySmartInventory v1.0.0 +
+
+ +
+ + + + \ No newline at end of file diff --git a/project_findings.md b/project_findings.md new file mode 100644 index 0000000..7e191e7 --- /dev/null +++ b/project_findings.md @@ -0,0 +1,78 @@ +# EasySmartInventory - Findings & Decisions + +## Requirements Analysis + +### Must-Have Features +1. **YAML configuratie** - Alle velden en opties via YAML +2. **Standalone HTML** - Enkel bestand, geen externe dependencies +3. **Field types**: + - Text (met min_length validatie) + - Number (met min/max) + - Date (configureerbaar formaat) + - Dropdown (single select) + - Multiselect (checkboxes) + - Boolean (ja/nee toggle) + - Photo (camera/upload, base64) +4. **Verplichte velden** - Markering en validatie +5. **Auto-save** - LocalStorage met URL state +6. **CSV Export** - Met base64 foto +7. **Mailto** - Configureerbare prefix en velden in subject +8. **Responsive** - iPad, iPhone, Android, Desktop +9. **Versienummer** - In HTML zichtbaar +10. **Bedrijfslogo** - Configureerbaar + +### Technical Decisions + +#### 1. Standalone HTML Approach +**Beslissing**: Alle CSS en JS inline in HTML +**Reden**: Makkelijk te delen, geen dependencies +**Trade-off**: Groter bestand, maar acceptabel + +#### 2. Auto-save Strategy +**Beslissing**: LocalStorage + URL parameters +**Implementatie**: +- LocalStorage key = document naam + versie +- URL hash voor sharing (#field1=value1&field2=value2) +- Save elke 5 seconden bij wijzigingen + +#### 3. Mailto Limitatie +**Probleem**: Mailto kan geen echte bijlagen +**Oplossing**: +- Foto base64 in body (kan lang zijn) +- Of: aparte instructie om foto apart te mailen +- Body bevat gestructureerde data voor parsing + +#### 4. CSV Export +**Beslissing**: Client-side generatie met download +**Implementatie**: +- Blob + download link +- UTF-8 met BOM voor Excel compatibiliteit +- Foto als base64 string in cel + +#### 5. Photo Handling +**Beslissing**: HTML5 FileReader + Canvas +**Features**: +- Camera capture op mobiel +- File upload op desktop +- Resize voor performance +- Base64 encoding + +## Style Guidelines + +### Themes +1. **Modern** - Rounded corners, gradients, shadows +2. **Corporate** - Clean lines, neutral colors, professional +3. **Minimal** - Maximum whitespace, simple borders + +### Color Scheme (configureerbaar) +- Primary: Hoofdkleur (buttons, headers) +- Secondary: Accentkleur +- Background: Pagina achtergrond +- Text: Tekstkleur +- Error: Validatie errors +- Success: Bevestigingen + +## Open Questions +- [ ] Maximale foto resolutie voor base64 in CSV? +- [ ] Offline support nodig? +- [ ] Meerdere talen ondersteuning? diff --git a/project_plan.md b/project_plan.md new file mode 100644 index 0000000..8a67c0c --- /dev/null +++ b/project_plan.md @@ -0,0 +1,157 @@ +# EasySmartInventory - Project Plan + +## Overview +- **Goal**: Python tool dat interactieve HTML inventarisatieformulieren genereert op basis van YAML configuratie +- **Deadline**: Flexibel +- **Priority**: High + +## Deliverables +1. **Python Generator** (`src/generator.py`) - Leest YAML, genereert standalone HTML +2. **YAML Schema** (`examples/config.yaml`) - Configuratiebestand met alle opties +3. **3 Voorbeeld HTML bestanden** - Verschillende stijlen (modern, corporate, minimal) + +## Architectuur + +``` +EasySmartInventory/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ generator.py # Main HTML generator +β”‚ β”œβ”€β”€ yaml_parser.py # YAML config parser +β”‚ └── templates.py # HTML/CSS/JS templates +β”œβ”€β”€ templates/ +β”‚ └── base.html # Jinja2 base template +β”œβ”€β”€ styles/ +β”‚ β”œβ”€β”€ modern.css +β”‚ β”œβ”€β”€ corporate.css +β”‚ └── minimal.css +β”œβ”€β”€ examples/ +β”‚ β”œβ”€β”€ config.yaml # Voorbeeld configuratie +β”‚ β”œβ”€β”€ inventory_modern.html +β”‚ β”œβ”€β”€ inventory_corporate.html +β”‚ └── inventory_minimal.html +β”œβ”€β”€ exports/ # CSV export folder +β”œβ”€β”€ docs/ +β”‚ └── README.md +β”œβ”€β”€ requirements.txt +└── README.md +``` + +## Phases + +### Phase 1: Core Structure (M size) +- [x] Git repo setup +- [ ] Project structure +- [ ] YAML schema design +- [ ] Base Python generator + +### Phase 2: HTML Features (L size) +- [ ] Field types implementatie +- [ ] Validation +- [ ] Auto-save (localStorage) +- [ ] CSV export +- [ ] Mailto functionaliteit +- [ ] Foto capture (base64) + +### Phase 3: Styling (M size) +- [ ] Responsive CSS framework +- [ ] 3 verschillende themes +- [ ] Logo support + +### Phase 4: Testing & Docs (S size) +- [ ] Generate voorbeelden +- [ ] Test responsive +- [ ] Documentatie +- [ ] Push to Gitea + +## YAML Schema Design + +```yaml +# Configuratie structuur +name: "Inventarisatie Naam" +version: "1.0.0" + +# Styling +style: + theme: "modern" # modern, corporate, minimal + logo: "base64_or_url" + primary_color: "#007bff" + +# Export opties +export: + csv: + enabled: true + folder: "./exports" + mailto: + enabled: true + to: "email@example.com" + subject_prefix: "Inventarisatie" + subject_fields: ["Serienummer"] + +# Velden definitie +sections: + - name: "Basisinformatie" + fields: + - id: "serienummer" + label: "Serienummer" + type: "text" + required: true + min_length: 5 + + - id: "type_nummer" + label: "Type-/modelnummer" + type: "text" + + - id: "merk" + label: "Merk / fabrikant" + type: "dropdown" + options: ["HP", "Dell", "Lenovo", "Apple", "Anders"] + + - id: "soort_apparaat" + label: "Soort apparaat" + type: "dropdown" + options: ["Laptop", "Desktop", "Server", "Printer", "Anders"] + + - id: "productiejaar" + label: "Productiejaar" + type: "number" + min: 1990 + max: 2030 + + - id: "aanschafdatum" + label: "Aanschafdatum" + type: "date" + format: "dd-mm-yyyy" + + - id: "in_gebruik" + label: "In gebruik" + type: "boolean" + + - id: "certificeringen" + label: "Certificeringen" + type: "multiselect" + options: ["CE", "ISO9001", "ISO27001", "GDPR"] + + - id: "foto" + label: "Foto apparaat" + type: "photo" +``` + +## Risks + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Mailto bijlage limitatie | Medium | Base64 in body of apart veld | +| LocalStorage limiet | Low | Compressie, waarschuwing | +| Cross-browser support | Medium | Testen, polyfills | + +## Agent Assignments + +| Task | Agent | Status | +|------|-------|--------| +| Architecture | Senior Developer | In progress | +| YAML Schema | Programming | Pending | +| HTML Generator | Programming | Pending | +| CSS Themes | GUI Designer | Pending | +| UX Flow | UX Designer | Pending | +| Testing | Junior Developer | Pending | diff --git a/project_progress.md b/project_progress.md new file mode 100644 index 0000000..a734e84 --- /dev/null +++ b/project_progress.md @@ -0,0 +1,40 @@ +# EasySmartInventory - Progress Log + +## Session 1 - 2026-01-12 + +### Completed +- [x] Requirements analysis van EasySmartInventorie.txt +- [x] Git repository geΓ―nitialiseerd +- [x] Project structure opgezet +- [x] 3-file planning pattern aangemaakt +- [x] YAML schema ontworpen +- [x] Architectuur beslissingen gedocumenteerd +- [x] Python YAML parser (yaml_parser.py) +- [x] HTML generator (generator.py) +- [x] CSS themes (modern, corporate, minimal) +- [x] JavaScript auto-save, validation, CSV export, mailto +- [x] Alle field types geΓ―mplementeerd +- [x] 3 voorbeeld HTML bestanden gegenereerd + +### Generated Files +- `examples/inventory_modern.html` (35,456 bytes) +- `examples/inventory_corporate.html` (33,894 bytes) +- `examples/inventory_minimal.html` (34,481 bytes) + +### Features Implemented +- βœ… YAML configuratie parsing +- βœ… 8 field types: text, number, date, textarea, dropdown, multiselect, boolean, photo +- βœ… Validatie (required, min_length, min/max) +- βœ… Auto-save naar localStorage +- βœ… URL hash voor state sharing +- βœ… CSV export met base64 foto's +- βœ… Mailto met configureerbare prefix +- βœ… 3 responsive themes +- βœ… Company logo support +- βœ… Versienummer tracking + +### Notes +- Mailto kan geen echte bijlagen - foto's worden apart vermeld in email body +- LocalStorage + URL hash voor state management +- Alle CSS/JS inline voor standalone HTML +- UTF-8 BOM toegevoegd aan CSV voor Excel compatibiliteit diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8fedc8c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyyaml>=6.0 +jinja2>=3.1.0 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..becf35a --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +# EasySmartInventory diff --git a/src/generator.py b/src/generator.py new file mode 100644 index 0000000..426aa1b --- /dev/null +++ b/src/generator.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +""" +EasySmartInventory Generator +Genereert standalone HTML inventarisatieformulieren op basis van YAML configuratie + +Gebruik: + python generator.py [--output ] [--theme ] +""" + +import argparse +import json +import sys +from pathlib import Path +from html import escape + +from yaml_parser import parse_yaml, validate_config, InventoryConfig, FieldConfig +from templates import get_theme_css, JAVASCRIPT + + +def generate_field_html(field: FieldConfig) -> str: + """Genereer HTML voor een veld""" + required_attr = 'data-required="true"' if field.required else '' + required_mark = '*' if field.required else '' + min_length_attr = f'data-min-length="{field.min_length}"' if field.min_length else '' + placeholder_attr = f'placeholder="{escape(field.placeholder)}"' if field.placeholder else '' + + html = f'
\n' + html += f' \n' + + if field.type == "text": + html += f' \n' + html += f'
\n' + + elif field.type == "number": + min_attr = f'min="{field.min}"' if field.min is not None else '' + max_attr = f'max="{field.max}"' if field.max is not None else '' + html += f' \n' + html += f'
\n' + + elif field.type == "date": + html += f' \n' + html += f'
\n' + + elif field.type == "textarea": + html += f' \n' + html += f'
\n' + + elif field.type == "dropdown": + html += f' \n' + html += f'
\n' + + elif field.type == "multiselect": + html += f'
\n' + for option in field.options: + safe_value = escape(option) + html += f' \n' + html += f'
\n' + + elif field.type == "boolean": + html += f'
\n' + html += f' \n' + html += f' Ja\n' + html += f'
\n' + + elif field.type == "photo": + html += f'
\n' + html += f' \n' + html += f' \n' + html += f'
πŸ“· Klik om foto te maken of uploaden
\n' + html += f' \n' + html += f'
\n' + + html += '
\n' + return html + + +def generate_html(config: InventoryConfig) -> str: + """Genereer complete standalone HTML""" + + # Config voor JavaScript + js_config = { + "name": config.name, + "version": config.version, + "autosave": { + "enabled": config.autosave.enabled, + "interval_seconds": config.autosave.interval_seconds, + "use_url_hash": config.autosave.use_url_hash, + "use_localstorage": config.autosave.use_localstorage, + }, + "export": { + "csv": { + "enabled": config.export.csv.enabled, + "include_photo": config.export.csv.include_photo, + }, + "mailto": { + "enabled": config.export.mailto.enabled, + "to": config.export.mailto.to, + "subject_prefix": config.export.mailto.subject_prefix, + "subject_fields": config.export.mailto.subject_fields, + "include_timestamp": config.export.mailto.include_timestamp, + }, + }, + } + + # CSS genereren + css = get_theme_css(config.style.theme, config.style) + + # Extra CSS voor photo container + css += """ +.photo-container.has-photo .photo-placeholder { display: none; } +.photo-container.has-photo .photo-preview { display: block !important; } +.photo-container.has-photo .photo-buttons { display: flex !important; } +""" + + # JavaScript met config + js = JAVASCRIPT.replace("{CONFIG_JSON}", json.dumps(js_config, ensure_ascii=False)) + + # Secties genereren + sections_html = "" + for section in config.sections: + sections_html += f'
\n' + sections_html += f'

{escape(section.name)}

\n' + if section.description: + sections_html += f'

{escape(section.description)}

\n' + + for field in section.fields: + sections_html += generate_field_html(field) + + sections_html += '
\n' + + # Logo HTML + logo_html = "" + if config.style.logo: + if config.style.logo.startswith("data:") or config.style.logo.startswith("http"): + logo_html = f'' + else: + logo_html = f'' + + # Actions HTML + actions_html = '
\n' + if config.export.csv.enabled: + actions_html += ' \n' + if config.export.mailto.enabled: + actions_html += ' \n' + actions_html += ' \n' + actions_html += '
\n' + + # Complete HTML + html = f''' + + + + + + {escape(config.name)} + + + +
+
+ {logo_html} +

{escape(config.name)}

+
Versie {config.version}
+
+ +
+{sections_html} +{actions_html} +
+ +
+ Gereed + EasySmartInventory v{config.version} +
+
+ +
+ + + +''' + + return html + + +def main(): + parser = argparse.ArgumentParser( + description="Genereer standalone HTML inventarisatieformulieren" + ) + parser.add_argument("config", help="Pad naar YAML configuratiebestand") + parser.add_argument("-o", "--output", help="Output HTML bestand (default: .html)") + parser.add_argument("-t", "--theme", choices=["modern", "corporate", "minimal"], + help="Override theme van YAML config") + + args = parser.parse_args() + + # Parse YAML + config_path = Path(args.config) + if not config_path.exists(): + print(f"Error: Configuratiebestand niet gevonden: {config_path}") + sys.exit(1) + + print(f"πŸ“– Lezen configuratie: {config_path}") + config = parse_yaml(str(config_path)) + + # Valideer + errors = validate_config(config) + if errors: + print("❌ Validatie errors:") + for error in errors: + print(f" - {error}") + sys.exit(1) + + # Override theme indien opgegeven + if args.theme: + config.style.theme = args.theme + + # Output pad bepalen + if args.output: + output_path = Path(args.output) + else: + output_path = config_path.parent / f"{config.name}.html" + + # Genereer HTML + print(f"βš™οΈ Genereren HTML met theme '{config.style.theme}'...") + html = generate_html(config) + + # Schrijf bestand + output_path.write_text(html, encoding="utf-8") + print(f"βœ… HTML gegenereerd: {output_path}") + print(f" Grootte: {len(html):,} bytes") + print(f" Secties: {len(config.sections)}") + + total_fields = sum(len(s.fields) for s in config.sections) + print(f" Velden: {total_fields}") + + +if __name__ == "__main__": + main() diff --git a/src/templates.py b/src/templates.py new file mode 100644 index 0000000..2eb5ecd --- /dev/null +++ b/src/templates.py @@ -0,0 +1,1398 @@ +""" +HTML/CSS/JS Templates voor EasySmartInventory +Bevat alle inline templates voor standalone HTML generatie +""" + +# CSS Themes +THEME_MODERN = """ +:root { + --primary: {primary_color}; + --secondary: {secondary_color}; + --background: {background_color}; + --text: {text_color}; + --error: {error_color}; + --success: {success_color}; + --border-radius: 12px; + --shadow: 0 4px 6px rgba(0,0,0,0.1); + --shadow-lg: 0 10px 25px rgba(0,0,0,0.15); +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background: linear-gradient(135deg, var(--background) 0%, #e9ecef 100%); + color: var(--text); + line-height: 1.6; + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 900px; + margin: 0 auto; + background: white; + border-radius: var(--border-radius); + box-shadow: var(--shadow-lg); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, var(--primary) 0%, color-mix(in srgb, var(--primary), black 20%) 100%); + color: white; + padding: 30px; + text-align: center; +} + +header h1 { font-size: 1.8em; margin-bottom: 5px; } +header .version { opacity: 0.8; font-size: 0.9em; } + +.logo { max-height: 60px; margin-bottom: 15px; } + +.section { + padding: 25px 30px; + border-bottom: 1px solid #e9ecef; +} + +.section:last-of-type { border-bottom: none; } + +.section h2 { + color: var(--primary); + font-size: 1.3em; + margin-bottom: 5px; + display: flex; + align-items: center; + gap: 10px; +} + +.section h2::before { + content: ''; + width: 4px; + height: 24px; + background: var(--primary); + border-radius: 2px; +} + +.section-desc { + color: #6c757d; + font-size: 0.9em; + margin-bottom: 20px; +} + +.field-group { + margin-bottom: 20px; +} + +label { + display: block; + font-weight: 600; + margin-bottom: 8px; + color: var(--text); +} + +label .required { + color: var(--error); + margin-left: 4px; +} + +input[type="text"], +input[type="number"], +input[type="date"], +select, +textarea { + width: 100%; + padding: 12px 16px; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1em; + transition: all 0.2s ease; + background: white; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary), transparent 80%); +} + +input.invalid, select.invalid, textarea.invalid { + border-color: var(--error); +} + +.error-message { + color: var(--error); + font-size: 0.85em; + margin-top: 5px; + display: none; +} + +.invalid + .error-message { display: block; } + +.checkbox-group, .multiselect-group { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.checkbox-item, .multiselect-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: #f8f9fa; + border-radius: 20px; + cursor: pointer; + transition: all 0.2s; +} + +.checkbox-item:hover, .multiselect-item:hover { + background: color-mix(in srgb, var(--primary), transparent 90%); +} + +.checkbox-item input, .multiselect-item input { + width: 18px; + height: 18px; + accent-color: var(--primary); +} + +.toggle-container { + display: flex; + align-items: center; + gap: 12px; +} + +.toggle { + position: relative; + width: 50px; + height: 26px; +} + +.toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + inset: 0; + background: #ccc; + border-radius: 26px; + transition: 0.3s; +} + +.toggle-slider::before { + content: ''; + position: absolute; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background: white; + border-radius: 50%; + transition: 0.3s; +} + +.toggle input:checked + .toggle-slider { + background: var(--primary); +} + +.toggle input:checked + .toggle-slider::before { + transform: translateX(24px); +} + +.photo-container { + border: 2px dashed #ddd; + border-radius: 12px; + padding: 30px; + text-align: center; + cursor: pointer; + transition: all 0.2s; + background: #fafafa; +} + +.photo-container:hover { + border-color: var(--primary); + background: color-mix(in srgb, var(--primary), transparent 95%); +} + +.photo-container.has-photo { + padding: 10px; + border-style: solid; +} + +.photo-preview { + max-width: 100%; + max-height: 300px; + border-radius: 8px; +} + +.photo-buttons { + margin-top: 10px; + display: flex; + gap: 10px; + justify-content: center; +} + +.btn { + padding: 12px 24px; + border: none; + border-radius: 8px; + font-size: 1em; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover { + background: color-mix(in srgb, var(--primary), black 15%); + transform: translateY(-2px); + box-shadow: var(--shadow); +} + +.btn-secondary { + background: #6c757d; + color: white; +} + +.btn-secondary:hover { + background: #5a6268; +} + +.btn-success { + background: var(--success); + color: white; +} + +.btn-success:hover { + background: color-mix(in srgb, var(--success), black 15%); +} + +.btn-danger { + background: var(--error); + color: white; +} + +.actions { + padding: 30px; + background: #f8f9fa; + display: flex; + flex-wrap: wrap; + gap: 15px; + justify-content: center; +} + +.status-bar { + padding: 10px 30px; + background: #e9ecef; + font-size: 0.85em; + color: #6c757d; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} + +.save-indicator { + display: flex; + align-items: center; + gap: 8px; +} + +.save-indicator.saving { color: var(--primary); } +.save-indicator.saved { color: var(--success); } + +.toast { + position: fixed; + bottom: 20px; + right: 20px; + padding: 15px 25px; + background: var(--text); + color: white; + border-radius: 8px; + box-shadow: var(--shadow-lg); + transform: translateY(100px); + opacity: 0; + transition: all 0.3s ease; + z-index: 1000; +} + +.toast.show { + transform: translateY(0); + opacity: 1; +} + +.toast.success { background: var(--success); } +.toast.error { background: var(--error); } + +@media (max-width: 768px) { + body { padding: 10px; } + .section { padding: 20px; } + header { padding: 20px; } + header h1 { font-size: 1.4em; } + .actions { flex-direction: column; } + .btn { width: 100%; justify-content: center; } +} +""" + +THEME_CORPORATE = """ +:root { + --primary: {primary_color}; + --secondary: {secondary_color}; + --background: {background_color}; + --text: {text_color}; + --error: {error_color}; + --success: {success_color}; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: var(--background); + color: var(--text); + line-height: 1.5; + min-height: 100vh; +} + +.container { + max-width: 800px; + margin: 0 auto; + background: white; + border: 1px solid #ddd; +} + +header { + background: var(--primary); + color: white; + padding: 20px 30px; + display: flex; + align-items: center; + justify-content: space-between; +} + +header h1 { font-size: 1.5em; font-weight: 500; } +header .version { font-size: 0.85em; opacity: 0.9; } + +.logo { max-height: 50px; } + +.section { + padding: 25px 30px; + border-bottom: 1px solid #eee; +} + +.section h2 { + color: var(--primary); + font-size: 1.1em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 5px; + padding-bottom: 10px; + border-bottom: 2px solid var(--primary); +} + +.section-desc { + color: #666; + font-size: 0.9em; + margin-bottom: 20px; +} + +.field-group { + margin-bottom: 18px; +} + +label { + display: block; + font-weight: 500; + margin-bottom: 6px; + font-size: 0.95em; +} + +label .required { + color: var(--error); +} + +input[type="text"], +input[type="number"], +input[type="date"], +select, +textarea { + width: 100%; + padding: 10px 12px; + border: 1px solid #ccc; + font-size: 0.95em; + background: white; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); +} + +input.invalid, select.invalid, textarea.invalid { + border-color: var(--error); + background: #fff5f5; +} + +.error-message { + color: var(--error); + font-size: 0.8em; + margin-top: 4px; + display: none; +} + +.invalid + .error-message { display: block; } + +.checkbox-group, .multiselect-group { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.checkbox-item, .multiselect-item { + display: flex; + align-items: center; + gap: 6px; +} + +.checkbox-item input, .multiselect-item input { + width: 16px; + height: 16px; +} + +.toggle-container { + display: flex; + align-items: center; + gap: 10px; +} + +.toggle { + position: relative; + width: 44px; + height: 22px; +} + +.toggle input { opacity: 0; width: 0; height: 0; } + +.toggle-slider { + position: absolute; + cursor: pointer; + inset: 0; + background: #ccc; + transition: 0.3s; +} + +.toggle-slider::before { + content: ''; + position: absolute; + height: 16px; + width: 16px; + left: 3px; + bottom: 3px; + background: white; + transition: 0.3s; +} + +.toggle input:checked + .toggle-slider { + background: var(--primary); +} + +.toggle input:checked + .toggle-slider::before { + transform: translateX(22px); +} + +.photo-container { + border: 1px dashed #999; + padding: 25px; + text-align: center; + cursor: pointer; + background: #fafafa; +} + +.photo-container:hover { + border-color: var(--primary); +} + +.photo-container.has-photo { + padding: 10px; + border-style: solid; +} + +.photo-preview { + max-width: 100%; + max-height: 250px; +} + +.photo-buttons { + margin-top: 10px; + display: flex; + gap: 10px; + justify-content: center; +} + +.btn { + padding: 10px 20px; + border: none; + font-size: 0.95em; + font-weight: 500; + cursor: pointer; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover { + background: color-mix(in srgb, var(--primary), black 10%); +} + +.btn-secondary { + background: #666; + color: white; +} + +.btn-success { + background: var(--success); + color: white; +} + +.btn-danger { + background: var(--error); + color: white; +} + +.actions { + padding: 25px 30px; + background: #f5f5f5; + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: flex-end; +} + +.status-bar { + padding: 10px 30px; + background: #eee; + font-size: 0.8em; + color: #666; + display: flex; + justify-content: space-between; +} + +.save-indicator { + display: flex; + align-items: center; + gap: 6px; +} + +.toast { + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 20px; + background: var(--text); + color: white; + transform: translateY(100px); + opacity: 0; + transition: all 0.3s; + z-index: 1000; +} + +.toast.show { transform: translateY(0); opacity: 1; } +.toast.success { background: var(--success); } +.toast.error { background: var(--error); } + +@media (max-width: 768px) { + .section { padding: 15px 20px; } + header { padding: 15px 20px; flex-direction: column; gap: 10px; } + .actions { justify-content: center; } +} +""" + +THEME_MINIMAL = """ +:root { + --primary: {primary_color}; + --secondary: {secondary_color}; + --background: {background_color}; + --text: {text_color}; + --error: {error_color}; + --success: {success_color}; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; + background: white; + color: var(--text); + line-height: 1.7; + min-height: 100vh; + padding: 40px 20px; +} + +.container { + max-width: 700px; + margin: 0 auto; +} + +header { + margin-bottom: 50px; + padding-bottom: 30px; + border-bottom: 1px solid #eee; +} + +header h1 { + font-size: 2em; + font-weight: 300; + color: var(--text); +} + +header .version { + color: #999; + font-size: 0.85em; + margin-top: 5px; +} + +.logo { max-height: 40px; margin-bottom: 20px; } + +.section { + margin-bottom: 50px; +} + +.section h2 { + font-size: 0.9em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 2px; + color: #999; + margin-bottom: 25px; +} + +.section-desc { + color: #666; + font-size: 0.9em; + margin-top: -20px; + margin-bottom: 25px; +} + +.field-group { + margin-bottom: 25px; +} + +label { + display: block; + font-size: 0.9em; + margin-bottom: 8px; + color: #666; +} + +label .required { + color: var(--error); +} + +input[type="text"], +input[type="number"], +input[type="date"], +select, +textarea { + width: 100%; + padding: 12px 0; + border: none; + border-bottom: 1px solid #ddd; + font-size: 1em; + background: transparent; + transition: border-color 0.2s; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-bottom-color: var(--primary); +} + +input.invalid, select.invalid, textarea.invalid { + border-bottom-color: var(--error); +} + +.error-message { + color: var(--error); + font-size: 0.8em; + margin-top: 5px; + display: none; +} + +.invalid + .error-message { display: block; } + +select { + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0 center; + padding-right: 20px; +} + +.checkbox-group, .multiselect-group { + display: flex; + flex-wrap: wrap; + gap: 20px; +} + +.checkbox-item, .multiselect-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.95em; +} + +.checkbox-item input, .multiselect-item input { + width: 18px; + height: 18px; + accent-color: var(--primary); +} + +.toggle-container { + display: flex; + align-items: center; + gap: 12px; +} + +.toggle { + position: relative; + width: 40px; + height: 20px; +} + +.toggle input { opacity: 0; width: 0; height: 0; } + +.toggle-slider { + position: absolute; + cursor: pointer; + inset: 0; + background: #ddd; + border-radius: 20px; + transition: 0.3s; +} + +.toggle-slider::before { + content: ''; + position: absolute; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background: white; + border-radius: 50%; + transition: 0.3s; +} + +.toggle input:checked + .toggle-slider { + background: var(--primary); +} + +.toggle input:checked + .toggle-slider::before { + transform: translateX(20px); +} + +.photo-container { + border: 1px solid #eee; + padding: 40px; + text-align: center; + cursor: pointer; + transition: border-color 0.2s; +} + +.photo-container:hover { + border-color: var(--primary); +} + +.photo-container.has-photo { + padding: 15px; +} + +.photo-preview { + max-width: 100%; + max-height: 250px; +} + +.photo-buttons { + margin-top: 15px; + display: flex; + gap: 10px; + justify-content: center; +} + +.btn { + padding: 12px 30px; + border: 1px solid; + font-size: 0.9em; + cursor: pointer; + background: transparent; + transition: all 0.2s; +} + +.btn-primary { + border-color: var(--primary); + color: var(--primary); +} + +.btn-primary:hover { + background: var(--primary); + color: white; +} + +.btn-secondary { + border-color: #999; + color: #999; +} + +.btn-secondary:hover { + background: #999; + color: white; +} + +.btn-success { + border-color: var(--success); + color: var(--success); +} + +.btn-success:hover { + background: var(--success); + color: white; +} + +.btn-danger { + border-color: var(--error); + color: var(--error); +} + +.btn-danger:hover { + background: var(--error); + color: white; +} + +.actions { + margin-top: 50px; + padding-top: 30px; + border-top: 1px solid #eee; + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.status-bar { + margin-top: 30px; + font-size: 0.8em; + color: #999; + display: flex; + justify-content: space-between; +} + +.save-indicator { + display: flex; + align-items: center; + gap: 6px; +} + +.toast { + position: fixed; + bottom: 30px; + right: 30px; + padding: 15px 25px; + background: var(--text); + color: white; + transform: translateY(100px); + opacity: 0; + transition: all 0.3s; + z-index: 1000; +} + +.toast.show { transform: translateY(0); opacity: 1; } +.toast.success { background: var(--success); } +.toast.error { background: var(--error); } + +@media (max-width: 768px) { + body { padding: 20px 15px; } + header h1 { font-size: 1.5em; } + .section { margin-bottom: 35px; } + .actions { flex-direction: column; } + .btn { width: 100%; text-align: center; } +} +""" + +THEMES = { + "modern": THEME_MODERN, + "corporate": THEME_CORPORATE, + "minimal": THEME_MINIMAL, +} + +# JavaScript voor de HTML pagina +JAVASCRIPT = """ +(function() { + 'use strict'; + + const CONFIG = {CONFIG_JSON}; + const STORAGE_KEY = 'inventory_' + CONFIG.name + '_' + CONFIG.version; + let saveTimeout = null; + let hasChanges = false; + + // Initialize + document.addEventListener('DOMContentLoaded', function() { + loadSavedData(); + setupAutoSave(); + setupValidation(); + setupPhotoHandlers(); + updateSaveIndicator('loaded'); + }); + + // Load saved data from localStorage or URL + function loadSavedData() { + // Try URL hash first + if (CONFIG.autosave.use_url_hash && window.location.hash) { + try { + const hashData = decodeURIComponent(window.location.hash.substring(1)); + const params = new URLSearchParams(hashData); + params.forEach((value, key) => { + setFieldValue(key, value); + }); + return; + } catch (e) { + console.warn('Could not parse URL hash:', e); + } + } + + // Try localStorage + if (CONFIG.autosave.use_localstorage) { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + const data = JSON.parse(saved); + Object.entries(data).forEach(([key, value]) => { + setFieldValue(key, value); + }); + } + } catch (e) { + console.warn('Could not load from localStorage:', e); + } + } + } + + // Set field value by ID + function setFieldValue(id, value) { + const field = document.getElementById(id); + if (!field) return; + + if (field.type === 'checkbox') { + field.checked = value === 'true' || value === true; + } else if (field.classList.contains('multiselect-field')) { + // Multiselect checkboxes + const values = Array.isArray(value) ? value : value.split(','); + values.forEach(v => { + const cb = document.querySelector(`input[name="${id}"][value="${v}"]`); + if (cb) cb.checked = true; + }); + } else if (field.tagName === 'IMG') { + // Photo field + if (value) { + field.src = value; + field.parentElement.classList.add('has-photo'); + } + } else { + field.value = value; + } + } + + // Get all form data + function getFormData() { + const data = {}; + + // Text, number, date, select, textarea + document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => { + if (field.id) { + data[field.id] = field.value; + } + }); + + // Checkboxes (boolean) + document.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => { + if (field.id) { + data[field.id] = field.checked; + } + }); + + // Multiselect + document.querySelectorAll('.multiselect-group').forEach(group => { + const id = group.dataset.id; + const checked = Array.from(group.querySelectorAll('input:checked')).map(cb => cb.value); + data[id] = checked; + }); + + // Photo + document.querySelectorAll('.photo-preview').forEach(img => { + const id = img.id; + if (img.src && !img.src.includes('data:image/svg')) { + data[id] = img.src; + } + }); + + return data; + } + + // Save data + function saveData() { + const data = getFormData(); + + // Save to localStorage + if (CONFIG.autosave.use_localstorage) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + } catch (e) { + console.warn('Could not save to localStorage:', e); + } + } + + // Update URL hash + if (CONFIG.autosave.use_url_hash) { + const params = new URLSearchParams(); + Object.entries(data).forEach(([key, value]) => { + if (value && value !== '' && !(Array.isArray(value) && value.length === 0)) { + params.set(key, Array.isArray(value) ? value.join(',') : value); + } + }); + const hash = params.toString(); + if (hash) { + history.replaceState(null, '', '#' + hash); + } + } + + hasChanges = false; + updateSaveIndicator('saved'); + } + + // Setup auto-save + function setupAutoSave() { + if (!CONFIG.autosave.enabled) return; + + const interval = CONFIG.autosave.interval_seconds * 1000; + + document.addEventListener('input', function() { + hasChanges = true; + updateSaveIndicator('saving'); + + clearTimeout(saveTimeout); + saveTimeout = setTimeout(saveData, interval); + }); + + document.addEventListener('change', function() { + hasChanges = true; + updateSaveIndicator('saving'); + + clearTimeout(saveTimeout); + saveTimeout = setTimeout(saveData, interval); + }); + } + + // Update save indicator + function updateSaveIndicator(status) { + const indicator = document.getElementById('save-indicator'); + if (!indicator) return; + + indicator.className = 'save-indicator ' + status; + + const statusText = { + 'loaded': 'Gegevens geladen', + 'saving': 'Opslaan...', + 'saved': 'Opgeslagen βœ“' + }; + + indicator.textContent = statusText[status] || status; + } + + // Setup validation + function setupValidation() { + document.querySelectorAll('[data-required="true"]').forEach(field => { + field.addEventListener('blur', function() { + validateField(this); + }); + }); + + document.querySelectorAll('[data-min-length]').forEach(field => { + field.addEventListener('blur', function() { + validateField(this); + }); + }); + } + + // Validate single field + function validateField(field) { + let valid = true; + let message = ''; + + const value = field.value.trim(); + + if (field.dataset.required === 'true' && !value) { + valid = false; + message = 'Dit veld is verplicht'; + } + + if (valid && field.dataset.minLength && value.length < parseInt(field.dataset.minLength)) { + valid = false; + message = `Minimaal ${field.dataset.minLength} karakters vereist`; + } + + if (valid && field.type === 'number') { + const num = parseFloat(value); + if (field.min && num < parseFloat(field.min)) { + valid = false; + message = `Minimale waarde is ${field.min}`; + } + if (field.max && num > parseFloat(field.max)) { + valid = false; + message = `Maximale waarde is ${field.max}`; + } + } + + field.classList.toggle('invalid', !valid); + + const errorEl = field.nextElementSibling; + if (errorEl && errorEl.classList.contains('error-message')) { + errorEl.textContent = message; + } + + return valid; + } + + // Validate all fields + function validateAll() { + let valid = true; + + document.querySelectorAll('[data-required="true"], [data-min-length]').forEach(field => { + if (!validateField(field)) { + valid = false; + } + }); + + return valid; + } + + // Setup photo handlers + function setupPhotoHandlers() { + document.querySelectorAll('.photo-container').forEach(container => { + const input = container.querySelector('input[type="file"]'); + const preview = container.querySelector('.photo-preview'); + const fieldId = preview.id; + const maxWidth = parseInt(container.dataset.maxWidth) || 1200; + const maxHeight = parseInt(container.dataset.maxHeight) || 1200; + + container.addEventListener('click', function(e) { + if (e.target.tagName !== 'BUTTON') { + input.click(); + } + }); + + input.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (!file) return; + + processImage(file, maxWidth, maxHeight, function(dataUrl) { + preview.src = dataUrl; + container.classList.add('has-photo'); + hasChanges = true; + saveData(); + }); + }); + }); + } + + // Process and resize image + function processImage(file, maxWidth, maxHeight, callback) { + const reader = new FileReader(); + reader.onload = function(e) { + const img = new Image(); + img.onload = function() { + const canvas = document.createElement('canvas'); + let width = img.width; + let height = img.height; + + if (width > maxWidth) { + height = height * (maxWidth / width); + width = maxWidth; + } + if (height > maxHeight) { + width = width * (maxHeight / height); + height = maxHeight; + } + + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + callback(canvas.toDataURL('image/jpeg', 0.8)); + }; + img.src = e.target.result; + }; + reader.readAsDataURL(file); + } + + // Remove photo + window.removePhoto = function(fieldId) { + const preview = document.getElementById(fieldId); + const container = preview.parentElement; + preview.src = ''; + container.classList.remove('has-photo'); + hasChanges = true; + saveData(); + }; + + // Export to CSV + window.exportCSV = function() { + if (!validateAll()) { + showToast('Vul eerst alle verplichte velden in', 'error'); + return; + } + + const data = getFormData(); + const headers = Object.keys(data); + const values = Object.values(data).map(v => { + if (Array.isArray(v)) return v.join('; '); + if (typeof v === 'string' && (v.includes(',') || v.includes('"') || v.includes('\\n'))) { + return '"' + v.replace(/"/g, '""') + '"'; + } + return v; + }); + + // UTF-8 BOM for Excel + const BOM = '\\uFEFF'; + const csv = BOM + headers.join(',') + '\\n' + values.join(','); + + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = CONFIG.name + '_' + formatDate(new Date()) + '.csv'; + a.click(); + + URL.revokeObjectURL(url); + showToast('CSV geΓ«xporteerd!', 'success'); + }; + + // Send email + window.sendEmail = function() { + if (!validateAll()) { + showToast('Vul eerst alle verplichte velden in', 'error'); + return; + } + + const data = getFormData(); + + // Build subject + let subject = CONFIG.export.mailto.subject_prefix; + if (CONFIG.export.mailto.include_timestamp) { + subject += ' - ' + formatDateTime(new Date()); + } + CONFIG.export.mailto.subject_fields.forEach(fieldId => { + if (data[fieldId]) { + subject += ' - ' + data[fieldId]; + } + }); + + // Build body + let body = 'INVENTARISATIE GEGEVENS\\n'; + body += '========================\\n\\n'; + body += 'Formulier: ' + CONFIG.name + '\\n'; + body += 'Versie: ' + CONFIG.version + '\\n'; + body += 'Datum: ' + formatDateTime(new Date()) + '\\n\\n'; + + Object.entries(data).forEach(([key, value]) => { + if (value && value !== '' && !key.includes('foto') && !key.includes('photo')) { + const label = document.querySelector(`label[for="${key}"]`); + const labelText = label ? label.textContent.replace('*', '').trim() : key; + const displayValue = Array.isArray(value) ? value.join(', ') : value; + body += labelText + ': ' + displayValue + '\\n'; + } + }); + + body += '\\n========================\\n'; + body += 'Let op: Foto\\'s kunnen niet via mailto worden verzonden.\\n'; + body += 'Gebruik CSV export voor complete data inclusief foto\\'s.'; + + const mailto = 'mailto:' + encodeURIComponent(CONFIG.export.mailto.to) + + '?subject=' + encodeURIComponent(subject) + + '&body=' + encodeURIComponent(body); + + window.location.href = mailto; + }; + + // Clear form + window.clearForm = function() { + if (!confirm('Weet u zeker dat u alle gegevens wilt wissen?')) return; + + document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => { + field.value = ''; + field.classList.remove('invalid'); + }); + + document.querySelectorAll('input[type="checkbox"]').forEach(cb => { + cb.checked = false; + }); + + document.querySelectorAll('.photo-preview').forEach(img => { + img.src = ''; + img.parentElement.classList.remove('has-photo'); + }); + + localStorage.removeItem(STORAGE_KEY); + history.replaceState(null, '', window.location.pathname); + + showToast('Formulier gewist', 'success'); + }; + + // Show toast notification + function showToast(message, type) { + const toast = document.getElementById('toast'); + toast.textContent = message; + toast.className = 'toast ' + type + ' show'; + + setTimeout(() => { + toast.classList.remove('show'); + }, 3000); + } + + // Format date + function formatDate(date) { + return date.toISOString().split('T')[0]; + } + + // Format datetime + function formatDateTime(date) { + return date.toLocaleDateString('nl-NL') + ' ' + + date.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + } +})(); +""" + +def get_theme_css(theme: str, style_config) -> str: + """Get CSS for theme with color variables filled in""" + template = THEMES.get(theme, THEMES["modern"]) + # Use string replacement instead of format() to avoid CSS brace conflicts + css = template.replace("{primary_color}", style_config.primary_color) + css = css.replace("{secondary_color}", style_config.secondary_color) + css = css.replace("{background_color}", style_config.background_color) + css = css.replace("{text_color}", style_config.text_color) + css = css.replace("{error_color}", style_config.error_color) + css = css.replace("{success_color}", style_config.success_color) + return css diff --git a/src/yaml_parser.py b/src/yaml_parser.py new file mode 100644 index 0000000..fc7ae5a --- /dev/null +++ b/src/yaml_parser.py @@ -0,0 +1,225 @@ +""" +YAML Parser voor EasySmartInventory +Parseert en valideert YAML configuratiebestanden +""" + +import yaml +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class FieldConfig: + """Configuratie voor een enkel veld""" + id: str + label: str + type: str + required: bool = False + placeholder: str = "" + min_length: int = 0 + min: float = None + max: float = None + format: str = "" + options: list = field(default_factory=list) + rows: int = 3 + max_width: int = 1200 + max_height: int = 1200 + + +@dataclass +class SectionConfig: + """Configuratie voor een sectie""" + name: str + description: str = "" + fields: list[FieldConfig] = field(default_factory=list) + + +@dataclass +class StyleConfig: + """Styling configuratie""" + theme: str = "modern" + logo: str = "" + primary_color: str = "#0066cc" + secondary_color: str = "#28a745" + background_color: str = "#f8f9fa" + text_color: str = "#333333" + error_color: str = "#dc3545" + success_color: str = "#28a745" + + +@dataclass +class CsvExportConfig: + """CSV export configuratie""" + enabled: bool = True + folder: str = "./exports" + include_photo: bool = True + + +@dataclass +class MailtoConfig: + """Mailto configuratie""" + enabled: bool = True + to: str = "" + subject_prefix: str = "Inventarisatie" + subject_fields: list = field(default_factory=list) + include_timestamp: bool = True + + +@dataclass +class ExportConfig: + """Export configuratie""" + csv: CsvExportConfig = field(default_factory=CsvExportConfig) + mailto: MailtoConfig = field(default_factory=MailtoConfig) + + +@dataclass +class AutosaveConfig: + """Autosave configuratie""" + enabled: bool = True + interval_seconds: int = 5 + use_url_hash: bool = True + use_localstorage: bool = True + + +@dataclass +class InventoryConfig: + """Hoofdconfiguratie voor inventarisatie""" + name: str + version: str = "1.0.0" + style: StyleConfig = field(default_factory=StyleConfig) + export: ExportConfig = field(default_factory=ExportConfig) + autosave: AutosaveConfig = field(default_factory=AutosaveConfig) + sections: list[SectionConfig] = field(default_factory=list) + + +def parse_field(field_data: dict) -> FieldConfig: + """Parse een veld configuratie""" + return FieldConfig( + id=field_data.get("id", ""), + label=field_data.get("label", ""), + type=field_data.get("type", "text"), + required=field_data.get("required", False), + placeholder=field_data.get("placeholder", ""), + min_length=field_data.get("min_length", 0), + min=field_data.get("min"), + max=field_data.get("max"), + format=field_data.get("format", ""), + options=field_data.get("options", []), + rows=field_data.get("rows", 3), + max_width=field_data.get("max_width", 1200), + max_height=field_data.get("max_height", 1200), + ) + + +def parse_section(section_data: dict) -> SectionConfig: + """Parse een sectie configuratie""" + fields = [parse_field(f) for f in section_data.get("fields", [])] + return SectionConfig( + name=section_data.get("name", ""), + description=section_data.get("description", ""), + fields=fields, + ) + + +def parse_style(style_data: dict) -> StyleConfig: + """Parse style configuratie""" + if not style_data: + return StyleConfig() + return StyleConfig( + theme=style_data.get("theme", "modern"), + logo=style_data.get("logo", ""), + primary_color=style_data.get("primary_color", "#0066cc"), + secondary_color=style_data.get("secondary_color", "#28a745"), + background_color=style_data.get("background_color", "#f8f9fa"), + text_color=style_data.get("text_color", "#333333"), + error_color=style_data.get("error_color", "#dc3545"), + success_color=style_data.get("success_color", "#28a745"), + ) + + +def parse_export(export_data: dict) -> ExportConfig: + """Parse export configuratie""" + if not export_data: + return ExportConfig() + + csv_data = export_data.get("csv", {}) + mailto_data = export_data.get("mailto", {}) + + return ExportConfig( + csv=CsvExportConfig( + enabled=csv_data.get("enabled", True), + folder=csv_data.get("folder", "./exports"), + include_photo=csv_data.get("include_photo", True), + ), + mailto=MailtoConfig( + enabled=mailto_data.get("enabled", True), + to=mailto_data.get("to", ""), + subject_prefix=mailto_data.get("subject_prefix", "Inventarisatie"), + subject_fields=mailto_data.get("subject_fields", []), + include_timestamp=mailto_data.get("include_timestamp", True), + ), + ) + + +def parse_autosave(autosave_data: dict) -> AutosaveConfig: + """Parse autosave configuratie""" + if not autosave_data: + return AutosaveConfig() + return AutosaveConfig( + enabled=autosave_data.get("enabled", True), + interval_seconds=autosave_data.get("interval_seconds", 5), + use_url_hash=autosave_data.get("use_url_hash", True), + use_localstorage=autosave_data.get("use_localstorage", True), + ) + + +def parse_yaml(yaml_path: str) -> InventoryConfig: + """Parse YAML bestand naar InventoryConfig""" + with open(yaml_path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + sections = [parse_section(s) for s in data.get("sections", [])] + + return InventoryConfig( + name=data.get("name", "Inventarisatie"), + version=data.get("version", "1.0.0"), + style=parse_style(data.get("style", {})), + export=parse_export(data.get("export", {})), + autosave=parse_autosave(data.get("autosave", {})), + sections=sections, + ) + + +def validate_config(config: InventoryConfig) -> list[str]: + """Valideer de configuratie en return lijst met errors""" + errors = [] + + if not config.name: + errors.append("Naam is verplicht") + + if not config.sections: + errors.append("Minimaal één sectie is verplicht") + + field_ids = set() + for section in config.sections: + if not section.name: + errors.append("Sectie naam is verplicht") + + for field in section.fields: + if not field.id: + errors.append(f"Veld ID is verplicht in sectie '{section.name}'") + elif field.id in field_ids: + errors.append(f"Dubbel veld ID: '{field.id}'") + else: + field_ids.add(field.id) + + if not field.label: + errors.append(f"Veld label is verplicht voor '{field.id}'") + + if field.type == "dropdown" and not field.options: + errors.append(f"Dropdown '{field.id}' heeft geen opties") + + if field.type == "multiselect" and not field.options: + errors.append(f"Multiselect '{field.id}' heeft geen opties") + + return errors