From cfd852cbc34e54aabfbadb636ca191549bac6298 Mon Sep 17 00:00:00 2001 From: killercow Date: Mon, 12 Jan 2026 13:38:50 +0000 Subject: [PATCH] v1.0.1: Add form history with unique ID support New features: - unique_id_fields in YAML config to define form identity - Form selector dropdown to switch between saved forms - Multiple forms can be saved per device - Auto-detect existing forms when filling ID fields - Delete saved forms functionality - Forms persist in localStorage with index Changes: - yaml_parser.py: Added unique_id_fields support - generator.py: Added form selector UI - templates.py: Complete rewrite of save/load logic for multi-form support - config.yaml: Example with serienummer and asset_id as unique fields --- examples/config.yaml | 11 +- examples/inventory_corporate.html | 295 +++++++++++++++++++++++++++--- examples/inventory_minimal.html | 295 +++++++++++++++++++++++++++--- examples/inventory_modern.html | 295 +++++++++++++++++++++++++++--- project_progress.md | 48 ++--- src/generator.py | 55 +++++- src/templates.py | 240 ++++++++++++++++++++++-- src/yaml_parser.py | 6 +- 8 files changed, 1138 insertions(+), 107 deletions(-) diff --git a/examples/config.yaml b/examples/config.yaml index 747df15..2b1e418 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,8 +1,8 @@ # Machine Inventarisatie - Voorbeeld Configuratie -# EasySmartInventory v1.0.0 +# EasySmartInventory v1.0.1 name: "Machine_Inventarisatie" -version: "1.0.0" +version: "1.0.1" # Styling configuratie style: @@ -35,6 +35,13 @@ autosave: use_url_hash: true use_localstorage: true +# Unieke identificatie voor formulieren +# Deze velden bepalen samen het unieke ID voor opgeslagen formulieren +# Hiermee kunnen meerdere formulieren op hetzelfde apparaat worden opgeslagen +unique_id_fields: + - "serienummer" + - "asset_id" + # Secties en velden sections: - name: "Basisinformatie" diff --git a/examples/inventory_corporate.html b/examples/inventory_corporate.html index 2f7cf0c..ad6b8cb 100644 --- a/examples/inventory_corporate.html +++ b/examples/inventory_corporate.html @@ -3,7 +3,7 @@ - + Machine_Inventarisatie @@ -294,9 +331,17 @@ input.invalid, select.invalid, textarea.invalid {

Machine_Inventarisatie

-
Versie 1.0.0
+
Versie 1.0.1
+
+ + + +
+

Basisinformatie

@@ -613,14 +658,14 @@ input.invalid, select.invalid, textarea.invalid {
- +
Gereed - EasySmartInventory v1.0.0 + EasySmartInventory v1.0.1
@@ -631,13 +676,15 @@ input.invalid, select.invalid, textarea.invalid { (function() { 'use strict'; - const CONFIG = {"name": "Machine_Inventarisatie", "version": "1.0.0", "autosave": {"enabled": true, "interval_seconds": 5, "use_url_hash": true, "use_localstorage": true}, "export": {"csv": {"enabled": true, "include_photo": true}, "mailto": {"enabled": true, "to": "inventaris@bedrijf.nl", "subject_prefix": "Inventarisatie", "subject_fields": ["serienummer", "merk"], "include_timestamp": true}}}; - const STORAGE_KEY = 'inventory_' + CONFIG.name + '_' + CONFIG.version; + const CONFIG = {"name": "Machine_Inventarisatie", "version": "1.0.1", "autosave": {"enabled": true, "interval_seconds": 5, "use_url_hash": true, "use_localstorage": true}, "export": {"csv": {"enabled": true, "include_photo": true}, "mailto": {"enabled": true, "to": "inventaris@bedrijf.nl", "subject_prefix": "Inventarisatie", "subject_fields": ["serienummer", "merk"], "include_timestamp": true}}, "unique_id_fields": ["serienummer", "asset_id"]}; + const FORMS_INDEX_KEY = 'inventory_' + CONFIG.name + '_forms_index'; + let currentFormId = null; let saveTimeout = null; let hasChanges = false; // Initialize document.addEventListener('DOMContentLoaded', function() { + setupFormSelector(); loadSavedData(); setupAutoSave(); setupValidation(); @@ -645,6 +692,173 @@ input.invalid, select.invalid, textarea.invalid { updateSaveIndicator('loaded'); }); + // Get unique form ID based on configured fields + function getFormUniqueId() { + if (!CONFIG.unique_id_fields || CONFIG.unique_id_fields.length === 0) { + return 'default'; + } + const parts = []; + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field && field.value && field.value.trim()) { + parts.push(field.value.trim()); + } + }); + return parts.length > 0 ? parts.join('_') : null; + } + + // Get storage key for a specific form + function getStorageKey(formId) { + return 'inventory_' + CONFIG.name + '_' + CONFIG.version + '_form_' + formId; + } + + // Get all saved forms from index + function getSavedForms() { + try { + const index = localStorage.getItem(FORMS_INDEX_KEY); + return index ? JSON.parse(index) : []; + } catch (e) { + return []; + } + } + + // Save form to index + function saveFormToIndex(formId, label) { + const forms = getSavedForms(); + const existing = forms.findIndex(f => f.id === formId); + const formInfo = { + id: formId, + label: label || formId, + lastModified: new Date().toISOString() + }; + if (existing >= 0) { + forms[existing] = formInfo; + } else { + forms.push(formInfo); + } + localStorage.setItem(FORMS_INDEX_KEY, JSON.stringify(forms)); + updateFormSelector(); + } + + // Remove form from index + function removeFormFromIndex(formId) { + const forms = getSavedForms().filter(f => f.id !== formId); + localStorage.setItem(FORMS_INDEX_KEY, JSON.stringify(forms)); + localStorage.removeItem(getStorageKey(formId)); + updateFormSelector(); + } + + // Setup form selector dropdown + function setupFormSelector() { + const selector = document.getElementById('form-selector'); + if (!selector) return; + + updateFormSelector(); + + selector.addEventListener('change', function() { + const selectedId = this.value; + if (selectedId === '__new__') { + clearFormData(); + currentFormId = null; + showToast('Nieuw formulier gestart', 'success'); + } else if (selectedId) { + loadFormById(selectedId); + } + }); + + // Watch unique ID fields for changes + if (CONFIG.unique_id_fields) { + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field) { + field.addEventListener('blur', function() { + const newId = getFormUniqueId(); + if (newId && newId !== currentFormId) { + // Check if this form already exists + const existing = getSavedForms().find(f => f.id === newId); + if (existing) { + if (confirm('Er bestaat al een formulier met deze ID. Wilt u dit formulier laden?')) { + loadFormById(newId); + } + } + } + }); + } + }); + } + } + + // Update form selector options + function updateFormSelector() { + const selector = document.getElementById('form-selector'); + if (!selector) return; + + const forms = getSavedForms(); + const currentValue = selector.value; + + // Clear and rebuild + selector.innerHTML = ''; + selector.innerHTML += ''; + + if (forms.length > 0) { + const optgroup = document.createElement('optgroup'); + optgroup.label = 'Opgeslagen formulieren (' + forms.length + ')'; + + forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + forms.forEach(form => { + const option = document.createElement('option'); + option.value = form.id; + const date = new Date(form.lastModified).toLocaleString('nl-NL'); + option.textContent = form.label + ' (' + date + ')'; + if (form.id === currentFormId) { + option.selected = true; + } + optgroup.appendChild(option); + }); + selector.appendChild(optgroup); + } + } + + // Load a specific form by ID + function loadFormById(formId) { + try { + const saved = localStorage.getItem(getStorageKey(formId)); + if (saved) { + clearFormData(); + const data = JSON.parse(saved); + Object.entries(data).forEach(([key, value]) => { + setFieldValue(key, value); + }); + currentFormId = formId; + updateFormSelector(); + showToast('Formulier geladen: ' + formId, 'success'); + } + } catch (e) { + console.warn('Could not load form:', e); + showToast('Kon formulier niet laden', 'error'); + } + } + + // Clear form data (without removing from storage) + function clearFormData() { + 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.style.display = 'none'; + img.parentElement.classList.remove('has-photo'); + }); + + history.replaceState(null, '', window.location.pathname); + } + // Load saved data from localStorage or URL function loadSavedData() { // Try URL hash first @@ -655,24 +869,20 @@ input.invalid, select.invalid, textarea.invalid { params.forEach((value, key) => { setFieldValue(key, value); }); + currentFormId = getFormUniqueId(); return; } catch (e) { console.warn('Could not parse URL hash:', e); } } - // Try localStorage + // Try to load most recent form from 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); + const forms = getSavedForms(); + if (forms.length > 0) { + // Load most recent + forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + loadFormById(forms[0].id); } } } @@ -741,11 +951,33 @@ input.invalid, select.invalid, textarea.invalid { // Save data function saveData() { const data = getFormData(); + const formId = getFormUniqueId(); + + // Only save if we have a valid form ID + if (!formId) { + updateSaveIndicator('waiting'); + return; + } - // Save to localStorage + // Save to localStorage with form-specific key if (CONFIG.autosave.use_localstorage) { try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + localStorage.setItem(getStorageKey(formId), JSON.stringify(data)); + + // Build label from unique ID fields + const labelParts = []; + if (CONFIG.unique_id_fields) { + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field && field.value) { + labelParts.push(field.value); + } + }); + } + const label = labelParts.join(' - ') || formId; + + saveFormToIndex(formId, label); + currentFormId = formId; } catch (e) { console.warn('Could not save to localStorage:', e); } @@ -802,7 +1034,8 @@ input.invalid, select.invalid, textarea.invalid { const statusText = { 'loaded': 'Gegevens geladen', 'saving': 'Opslaan...', - 'saved': 'Opgeslagen βœ“' + 'saved': 'Opgeslagen βœ“', + 'waiting': 'Vul ID-velden in om op te slaan' }; indicator.textContent = statusText[status] || status; @@ -1101,12 +1334,32 @@ input.invalid, select.invalid, textarea.invalid { img.parentElement.classList.remove('has-photo'); }); - localStorage.removeItem(STORAGE_KEY); history.replaceState(null, '', window.location.pathname); + currentFormId = null; + updateFormSelector(); showToast('Formulier gewist', 'success'); }; + // Delete saved form + window.deleteForm = function() { + if (!currentFormId) { + showToast('Geen formulier geselecteerd om te verwijderen', 'error'); + return; + } + + if (!confirm('Weet u zeker dat u dit opgeslagen formulier wilt verwijderen?\n\nFormulier: ' + currentFormId)) { + return; + } + + removeFormFromIndex(currentFormId); + clearFormData(); + currentFormId = null; + updateFormSelector(); + + showToast('Formulier verwijderd', 'success'); + }; + // Show toast notification function showToast(message, type) { const toast = document.getElementById('toast'); diff --git a/examples/inventory_minimal.html b/examples/inventory_minimal.html index 647c42e..17a7644 100644 --- a/examples/inventory_minimal.html +++ b/examples/inventory_minimal.html @@ -3,7 +3,7 @@ - + Machine_Inventarisatie @@ -325,9 +362,17 @@ select {

Machine_Inventarisatie

-
Versie 1.0.0
+
Versie 1.0.1
+
+ + + +
+

Basisinformatie

@@ -644,14 +689,14 @@ select {
- +
Gereed - EasySmartInventory v1.0.0 + EasySmartInventory v1.0.1
@@ -662,13 +707,15 @@ select { (function() { 'use strict'; - const CONFIG = {"name": "Machine_Inventarisatie", "version": "1.0.0", "autosave": {"enabled": true, "interval_seconds": 5, "use_url_hash": true, "use_localstorage": true}, "export": {"csv": {"enabled": true, "include_photo": true}, "mailto": {"enabled": true, "to": "inventaris@bedrijf.nl", "subject_prefix": "Inventarisatie", "subject_fields": ["serienummer", "merk"], "include_timestamp": true}}}; - const STORAGE_KEY = 'inventory_' + CONFIG.name + '_' + CONFIG.version; + const CONFIG = {"name": "Machine_Inventarisatie", "version": "1.0.1", "autosave": {"enabled": true, "interval_seconds": 5, "use_url_hash": true, "use_localstorage": true}, "export": {"csv": {"enabled": true, "include_photo": true}, "mailto": {"enabled": true, "to": "inventaris@bedrijf.nl", "subject_prefix": "Inventarisatie", "subject_fields": ["serienummer", "merk"], "include_timestamp": true}}, "unique_id_fields": ["serienummer", "asset_id"]}; + const FORMS_INDEX_KEY = 'inventory_' + CONFIG.name + '_forms_index'; + let currentFormId = null; let saveTimeout = null; let hasChanges = false; // Initialize document.addEventListener('DOMContentLoaded', function() { + setupFormSelector(); loadSavedData(); setupAutoSave(); setupValidation(); @@ -676,6 +723,173 @@ select { updateSaveIndicator('loaded'); }); + // Get unique form ID based on configured fields + function getFormUniqueId() { + if (!CONFIG.unique_id_fields || CONFIG.unique_id_fields.length === 0) { + return 'default'; + } + const parts = []; + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field && field.value && field.value.trim()) { + parts.push(field.value.trim()); + } + }); + return parts.length > 0 ? parts.join('_') : null; + } + + // Get storage key for a specific form + function getStorageKey(formId) { + return 'inventory_' + CONFIG.name + '_' + CONFIG.version + '_form_' + formId; + } + + // Get all saved forms from index + function getSavedForms() { + try { + const index = localStorage.getItem(FORMS_INDEX_KEY); + return index ? JSON.parse(index) : []; + } catch (e) { + return []; + } + } + + // Save form to index + function saveFormToIndex(formId, label) { + const forms = getSavedForms(); + const existing = forms.findIndex(f => f.id === formId); + const formInfo = { + id: formId, + label: label || formId, + lastModified: new Date().toISOString() + }; + if (existing >= 0) { + forms[existing] = formInfo; + } else { + forms.push(formInfo); + } + localStorage.setItem(FORMS_INDEX_KEY, JSON.stringify(forms)); + updateFormSelector(); + } + + // Remove form from index + function removeFormFromIndex(formId) { + const forms = getSavedForms().filter(f => f.id !== formId); + localStorage.setItem(FORMS_INDEX_KEY, JSON.stringify(forms)); + localStorage.removeItem(getStorageKey(formId)); + updateFormSelector(); + } + + // Setup form selector dropdown + function setupFormSelector() { + const selector = document.getElementById('form-selector'); + if (!selector) return; + + updateFormSelector(); + + selector.addEventListener('change', function() { + const selectedId = this.value; + if (selectedId === '__new__') { + clearFormData(); + currentFormId = null; + showToast('Nieuw formulier gestart', 'success'); + } else if (selectedId) { + loadFormById(selectedId); + } + }); + + // Watch unique ID fields for changes + if (CONFIG.unique_id_fields) { + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field) { + field.addEventListener('blur', function() { + const newId = getFormUniqueId(); + if (newId && newId !== currentFormId) { + // Check if this form already exists + const existing = getSavedForms().find(f => f.id === newId); + if (existing) { + if (confirm('Er bestaat al een formulier met deze ID. Wilt u dit formulier laden?')) { + loadFormById(newId); + } + } + } + }); + } + }); + } + } + + // Update form selector options + function updateFormSelector() { + const selector = document.getElementById('form-selector'); + if (!selector) return; + + const forms = getSavedForms(); + const currentValue = selector.value; + + // Clear and rebuild + selector.innerHTML = ''; + selector.innerHTML += ''; + + if (forms.length > 0) { + const optgroup = document.createElement('optgroup'); + optgroup.label = 'Opgeslagen formulieren (' + forms.length + ')'; + + forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + forms.forEach(form => { + const option = document.createElement('option'); + option.value = form.id; + const date = new Date(form.lastModified).toLocaleString('nl-NL'); + option.textContent = form.label + ' (' + date + ')'; + if (form.id === currentFormId) { + option.selected = true; + } + optgroup.appendChild(option); + }); + selector.appendChild(optgroup); + } + } + + // Load a specific form by ID + function loadFormById(formId) { + try { + const saved = localStorage.getItem(getStorageKey(formId)); + if (saved) { + clearFormData(); + const data = JSON.parse(saved); + Object.entries(data).forEach(([key, value]) => { + setFieldValue(key, value); + }); + currentFormId = formId; + updateFormSelector(); + showToast('Formulier geladen: ' + formId, 'success'); + } + } catch (e) { + console.warn('Could not load form:', e); + showToast('Kon formulier niet laden', 'error'); + } + } + + // Clear form data (without removing from storage) + function clearFormData() { + 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.style.display = 'none'; + img.parentElement.classList.remove('has-photo'); + }); + + history.replaceState(null, '', window.location.pathname); + } + // Load saved data from localStorage or URL function loadSavedData() { // Try URL hash first @@ -686,24 +900,20 @@ select { params.forEach((value, key) => { setFieldValue(key, value); }); + currentFormId = getFormUniqueId(); return; } catch (e) { console.warn('Could not parse URL hash:', e); } } - // Try localStorage + // Try to load most recent form from 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); + const forms = getSavedForms(); + if (forms.length > 0) { + // Load most recent + forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + loadFormById(forms[0].id); } } } @@ -772,11 +982,33 @@ select { // Save data function saveData() { const data = getFormData(); + const formId = getFormUniqueId(); + + // Only save if we have a valid form ID + if (!formId) { + updateSaveIndicator('waiting'); + return; + } - // Save to localStorage + // Save to localStorage with form-specific key if (CONFIG.autosave.use_localstorage) { try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + localStorage.setItem(getStorageKey(formId), JSON.stringify(data)); + + // Build label from unique ID fields + const labelParts = []; + if (CONFIG.unique_id_fields) { + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field && field.value) { + labelParts.push(field.value); + } + }); + } + const label = labelParts.join(' - ') || formId; + + saveFormToIndex(formId, label); + currentFormId = formId; } catch (e) { console.warn('Could not save to localStorage:', e); } @@ -833,7 +1065,8 @@ select { const statusText = { 'loaded': 'Gegevens geladen', 'saving': 'Opslaan...', - 'saved': 'Opgeslagen βœ“' + 'saved': 'Opgeslagen βœ“', + 'waiting': 'Vul ID-velden in om op te slaan' }; indicator.textContent = statusText[status] || status; @@ -1132,12 +1365,32 @@ select { img.parentElement.classList.remove('has-photo'); }); - localStorage.removeItem(STORAGE_KEY); history.replaceState(null, '', window.location.pathname); + currentFormId = null; + updateFormSelector(); showToast('Formulier gewist', 'success'); }; + // Delete saved form + window.deleteForm = function() { + if (!currentFormId) { + showToast('Geen formulier geselecteerd om te verwijderen', 'error'); + return; + } + + if (!confirm('Weet u zeker dat u dit opgeslagen formulier wilt verwijderen?\n\nFormulier: ' + currentFormId)) { + return; + } + + removeFormFromIndex(currentFormId); + clearFormData(); + currentFormId = null; + updateFormSelector(); + + showToast('Formulier verwijderd', 'success'); + }; + // Show toast notification function showToast(message, type) { const toast = document.getElementById('toast'); diff --git a/examples/inventory_modern.html b/examples/inventory_modern.html index 5648de8..1cc968f 100644 --- a/examples/inventory_modern.html +++ b/examples/inventory_modern.html @@ -3,7 +3,7 @@ - + Machine_Inventarisatie @@ -356,9 +393,17 @@ input.invalid, select.invalid, textarea.invalid {

Machine_Inventarisatie

-
Versie 1.0.0
+
Versie 1.0.1
+
+ + + +
+

Basisinformatie

@@ -675,14 +720,14 @@ input.invalid, select.invalid, textarea.invalid {
- +
Gereed - EasySmartInventory v1.0.0 + EasySmartInventory v1.0.1
@@ -693,13 +738,15 @@ input.invalid, select.invalid, textarea.invalid { (function() { 'use strict'; - const CONFIG = {"name": "Machine_Inventarisatie", "version": "1.0.0", "autosave": {"enabled": true, "interval_seconds": 5, "use_url_hash": true, "use_localstorage": true}, "export": {"csv": {"enabled": true, "include_photo": true}, "mailto": {"enabled": true, "to": "inventaris@bedrijf.nl", "subject_prefix": "Inventarisatie", "subject_fields": ["serienummer", "merk"], "include_timestamp": true}}}; - const STORAGE_KEY = 'inventory_' + CONFIG.name + '_' + CONFIG.version; + const CONFIG = {"name": "Machine_Inventarisatie", "version": "1.0.1", "autosave": {"enabled": true, "interval_seconds": 5, "use_url_hash": true, "use_localstorage": true}, "export": {"csv": {"enabled": true, "include_photo": true}, "mailto": {"enabled": true, "to": "inventaris@bedrijf.nl", "subject_prefix": "Inventarisatie", "subject_fields": ["serienummer", "merk"], "include_timestamp": true}}, "unique_id_fields": ["serienummer", "asset_id"]}; + const FORMS_INDEX_KEY = 'inventory_' + CONFIG.name + '_forms_index'; + let currentFormId = null; let saveTimeout = null; let hasChanges = false; // Initialize document.addEventListener('DOMContentLoaded', function() { + setupFormSelector(); loadSavedData(); setupAutoSave(); setupValidation(); @@ -707,6 +754,173 @@ input.invalid, select.invalid, textarea.invalid { updateSaveIndicator('loaded'); }); + // Get unique form ID based on configured fields + function getFormUniqueId() { + if (!CONFIG.unique_id_fields || CONFIG.unique_id_fields.length === 0) { + return 'default'; + } + const parts = []; + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field && field.value && field.value.trim()) { + parts.push(field.value.trim()); + } + }); + return parts.length > 0 ? parts.join('_') : null; + } + + // Get storage key for a specific form + function getStorageKey(formId) { + return 'inventory_' + CONFIG.name + '_' + CONFIG.version + '_form_' + formId; + } + + // Get all saved forms from index + function getSavedForms() { + try { + const index = localStorage.getItem(FORMS_INDEX_KEY); + return index ? JSON.parse(index) : []; + } catch (e) { + return []; + } + } + + // Save form to index + function saveFormToIndex(formId, label) { + const forms = getSavedForms(); + const existing = forms.findIndex(f => f.id === formId); + const formInfo = { + id: formId, + label: label || formId, + lastModified: new Date().toISOString() + }; + if (existing >= 0) { + forms[existing] = formInfo; + } else { + forms.push(formInfo); + } + localStorage.setItem(FORMS_INDEX_KEY, JSON.stringify(forms)); + updateFormSelector(); + } + + // Remove form from index + function removeFormFromIndex(formId) { + const forms = getSavedForms().filter(f => f.id !== formId); + localStorage.setItem(FORMS_INDEX_KEY, JSON.stringify(forms)); + localStorage.removeItem(getStorageKey(formId)); + updateFormSelector(); + } + + // Setup form selector dropdown + function setupFormSelector() { + const selector = document.getElementById('form-selector'); + if (!selector) return; + + updateFormSelector(); + + selector.addEventListener('change', function() { + const selectedId = this.value; + if (selectedId === '__new__') { + clearFormData(); + currentFormId = null; + showToast('Nieuw formulier gestart', 'success'); + } else if (selectedId) { + loadFormById(selectedId); + } + }); + + // Watch unique ID fields for changes + if (CONFIG.unique_id_fields) { + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field) { + field.addEventListener('blur', function() { + const newId = getFormUniqueId(); + if (newId && newId !== currentFormId) { + // Check if this form already exists + const existing = getSavedForms().find(f => f.id === newId); + if (existing) { + if (confirm('Er bestaat al een formulier met deze ID. Wilt u dit formulier laden?')) { + loadFormById(newId); + } + } + } + }); + } + }); + } + } + + // Update form selector options + function updateFormSelector() { + const selector = document.getElementById('form-selector'); + if (!selector) return; + + const forms = getSavedForms(); + const currentValue = selector.value; + + // Clear and rebuild + selector.innerHTML = ''; + selector.innerHTML += ''; + + if (forms.length > 0) { + const optgroup = document.createElement('optgroup'); + optgroup.label = 'Opgeslagen formulieren (' + forms.length + ')'; + + forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + forms.forEach(form => { + const option = document.createElement('option'); + option.value = form.id; + const date = new Date(form.lastModified).toLocaleString('nl-NL'); + option.textContent = form.label + ' (' + date + ')'; + if (form.id === currentFormId) { + option.selected = true; + } + optgroup.appendChild(option); + }); + selector.appendChild(optgroup); + } + } + + // Load a specific form by ID + function loadFormById(formId) { + try { + const saved = localStorage.getItem(getStorageKey(formId)); + if (saved) { + clearFormData(); + const data = JSON.parse(saved); + Object.entries(data).forEach(([key, value]) => { + setFieldValue(key, value); + }); + currentFormId = formId; + updateFormSelector(); + showToast('Formulier geladen: ' + formId, 'success'); + } + } catch (e) { + console.warn('Could not load form:', e); + showToast('Kon formulier niet laden', 'error'); + } + } + + // Clear form data (without removing from storage) + function clearFormData() { + 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.style.display = 'none'; + img.parentElement.classList.remove('has-photo'); + }); + + history.replaceState(null, '', window.location.pathname); + } + // Load saved data from localStorage or URL function loadSavedData() { // Try URL hash first @@ -717,24 +931,20 @@ input.invalid, select.invalid, textarea.invalid { params.forEach((value, key) => { setFieldValue(key, value); }); + currentFormId = getFormUniqueId(); return; } catch (e) { console.warn('Could not parse URL hash:', e); } } - // Try localStorage + // Try to load most recent form from 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); + const forms = getSavedForms(); + if (forms.length > 0) { + // Load most recent + forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + loadFormById(forms[0].id); } } } @@ -803,11 +1013,33 @@ input.invalid, select.invalid, textarea.invalid { // Save data function saveData() { const data = getFormData(); + const formId = getFormUniqueId(); + + // Only save if we have a valid form ID + if (!formId) { + updateSaveIndicator('waiting'); + return; + } - // Save to localStorage + // Save to localStorage with form-specific key if (CONFIG.autosave.use_localstorage) { try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + localStorage.setItem(getStorageKey(formId), JSON.stringify(data)); + + // Build label from unique ID fields + const labelParts = []; + if (CONFIG.unique_id_fields) { + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field && field.value) { + labelParts.push(field.value); + } + }); + } + const label = labelParts.join(' - ') || formId; + + saveFormToIndex(formId, label); + currentFormId = formId; } catch (e) { console.warn('Could not save to localStorage:', e); } @@ -864,7 +1096,8 @@ input.invalid, select.invalid, textarea.invalid { const statusText = { 'loaded': 'Gegevens geladen', 'saving': 'Opslaan...', - 'saved': 'Opgeslagen βœ“' + 'saved': 'Opgeslagen βœ“', + 'waiting': 'Vul ID-velden in om op te slaan' }; indicator.textContent = statusText[status] || status; @@ -1163,12 +1396,32 @@ input.invalid, select.invalid, textarea.invalid { img.parentElement.classList.remove('has-photo'); }); - localStorage.removeItem(STORAGE_KEY); history.replaceState(null, '', window.location.pathname); + currentFormId = null; + updateFormSelector(); showToast('Formulier gewist', 'success'); }; + // Delete saved form + window.deleteForm = function() { + if (!currentFormId) { + showToast('Geen formulier geselecteerd om te verwijderen', 'error'); + return; + } + + if (!confirm('Weet u zeker dat u dit opgeslagen formulier wilt verwijderen?\n\nFormulier: ' + currentFormId)) { + return; + } + + removeFormFromIndex(currentFormId); + clearFormData(); + currentFormId = null; + updateFormSelector(); + + showToast('Formulier verwijderd', 'success'); + }; + // Show toast notification function showToast(message, type) { const toast = document.getElementById('toast'); diff --git a/project_progress.md b/project_progress.md index a734e84..a2328c9 100644 --- a/project_progress.md +++ b/project_progress.md @@ -1,6 +1,27 @@ # EasySmartInventory - Progress Log -## Session 1 - 2026-01-12 +## Session 2 - 2026-01-12 (v1.0.1) + +### New Feature: Form History +- [x] Added `unique_id_fields` to YAML schema +- [x] Updated yaml_parser.py for new config +- [x] JavaScript multi-form storage with index +- [x] Form selector dropdown in UI +- [x] "Nieuw formulier" button +- [x] "Verwijder" button for saved forms +- [x] Auto-detection of existing forms by unique ID +- [x] Version bump to 1.0.1 + +### How it works: +1. Configure `unique_id_fields` in YAML (e.g., serienummer, asset_id) +2. When user fills in these fields, form is saved with unique ID +3. Dropdown shows all saved forms with last modified date +4. User can switch between forms or start new one +5. Forms persist in localStorage per device + +--- + +## Session 1 - 2026-01-12 (v1.0.0) ### Completed - [x] Requirements analysis van EasySmartInventorie.txt @@ -15,26 +36,9 @@ - [x] JavaScript auto-save, validation, CSV export, mailto - [x] Alle field types geΓ―mplementeerd - [x] 3 voorbeeld HTML bestanden gegenereerd +- [x] Base64 foto's in email body ### 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 +- `examples/inventory_modern.html` +- `examples/inventory_corporate.html` +- `examples/inventory_minimal.html` diff --git a/src/generator.py b/src/generator.py index 964bfc7..d97b814 100644 --- a/src/generator.py +++ b/src/generator.py @@ -112,21 +112,71 @@ def generate_html(config: InventoryConfig) -> str: "include_timestamp": config.export.mailto.include_timestamp, }, }, + "unique_id_fields": config.unique_id_fields, } # CSS genereren css = get_theme_css(config.style.theme, config.style) - # Extra CSS voor photo container + # Extra CSS voor photo container en form selector 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; } + +.form-selector-container { + padding: 15px 30px; + background: linear-gradient(to right, #f8f9fa, #e9ecef); + border-bottom: 1px solid #ddd; + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap; +} + +.form-selector-container label { + font-weight: 600; + margin: 0; + white-space: nowrap; +} + +.form-selector-container select { + flex: 1; + min-width: 200px; + max-width: 400px; +} + +.form-selector-container .btn { + padding: 8px 16px; + font-size: 0.9em; +} + +@media (max-width: 768px) { + .form-selector-container { + flex-direction: column; + align-items: stretch; + } + .form-selector-container select { + max-width: 100%; + } +} """ # JavaScript met config js = JAVASCRIPT.replace("{CONFIG_JSON}", json.dumps(js_config, ensure_ascii=False)) + # Form selector HTML (alleen als unique_id_fields geconfigureerd zijn) + form_selector_html = "" + if config.unique_id_fields: + form_selector_html = '''
+ + + +
+''' + # Secties genereren sections_html = "" for section in config.sections: @@ -154,7 +204,7 @@ def generate_html(config: InventoryConfig) -> str: actions_html += ' \n' if config.export.mailto.enabled: actions_html += ' \n' - actions_html += ' \n' + actions_html += ' \n' actions_html += '\n' # Complete HTML @@ -177,6 +227,7 @@ def generate_html(config: InventoryConfig) -> str:
Versie {config.version}
+ {form_selector_html}
{sections_html} {actions_html} diff --git a/src/templates.py b/src/templates.py index 2ff1bde..0ac5f21 100644 --- a/src/templates.py +++ b/src/templates.py @@ -943,12 +943,14 @@ JAVASCRIPT = """ 'use strict'; const CONFIG = {CONFIG_JSON}; - const STORAGE_KEY = 'inventory_' + CONFIG.name + '_' + CONFIG.version; + const FORMS_INDEX_KEY = 'inventory_' + CONFIG.name + '_forms_index'; + let currentFormId = null; let saveTimeout = null; let hasChanges = false; // Initialize document.addEventListener('DOMContentLoaded', function() { + setupFormSelector(); loadSavedData(); setupAutoSave(); setupValidation(); @@ -956,6 +958,173 @@ JAVASCRIPT = """ updateSaveIndicator('loaded'); }); + // Get unique form ID based on configured fields + function getFormUniqueId() { + if (!CONFIG.unique_id_fields || CONFIG.unique_id_fields.length === 0) { + return 'default'; + } + const parts = []; + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field && field.value && field.value.trim()) { + parts.push(field.value.trim()); + } + }); + return parts.length > 0 ? parts.join('_') : null; + } + + // Get storage key for a specific form + function getStorageKey(formId) { + return 'inventory_' + CONFIG.name + '_' + CONFIG.version + '_form_' + formId; + } + + // Get all saved forms from index + function getSavedForms() { + try { + const index = localStorage.getItem(FORMS_INDEX_KEY); + return index ? JSON.parse(index) : []; + } catch (e) { + return []; + } + } + + // Save form to index + function saveFormToIndex(formId, label) { + const forms = getSavedForms(); + const existing = forms.findIndex(f => f.id === formId); + const formInfo = { + id: formId, + label: label || formId, + lastModified: new Date().toISOString() + }; + if (existing >= 0) { + forms[existing] = formInfo; + } else { + forms.push(formInfo); + } + localStorage.setItem(FORMS_INDEX_KEY, JSON.stringify(forms)); + updateFormSelector(); + } + + // Remove form from index + function removeFormFromIndex(formId) { + const forms = getSavedForms().filter(f => f.id !== formId); + localStorage.setItem(FORMS_INDEX_KEY, JSON.stringify(forms)); + localStorage.removeItem(getStorageKey(formId)); + updateFormSelector(); + } + + // Setup form selector dropdown + function setupFormSelector() { + const selector = document.getElementById('form-selector'); + if (!selector) return; + + updateFormSelector(); + + selector.addEventListener('change', function() { + const selectedId = this.value; + if (selectedId === '__new__') { + clearFormData(); + currentFormId = null; + showToast('Nieuw formulier gestart', 'success'); + } else if (selectedId) { + loadFormById(selectedId); + } + }); + + // Watch unique ID fields for changes + if (CONFIG.unique_id_fields) { + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field) { + field.addEventListener('blur', function() { + const newId = getFormUniqueId(); + if (newId && newId !== currentFormId) { + // Check if this form already exists + const existing = getSavedForms().find(f => f.id === newId); + if (existing) { + if (confirm('Er bestaat al een formulier met deze ID. Wilt u dit formulier laden?')) { + loadFormById(newId); + } + } + } + }); + } + }); + } + } + + // Update form selector options + function updateFormSelector() { + const selector = document.getElementById('form-selector'); + if (!selector) return; + + const forms = getSavedForms(); + const currentValue = selector.value; + + // Clear and rebuild + selector.innerHTML = ''; + selector.innerHTML += ''; + + if (forms.length > 0) { + const optgroup = document.createElement('optgroup'); + optgroup.label = 'Opgeslagen formulieren (' + forms.length + ')'; + + forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + forms.forEach(form => { + const option = document.createElement('option'); + option.value = form.id; + const date = new Date(form.lastModified).toLocaleString('nl-NL'); + option.textContent = form.label + ' (' + date + ')'; + if (form.id === currentFormId) { + option.selected = true; + } + optgroup.appendChild(option); + }); + selector.appendChild(optgroup); + } + } + + // Load a specific form by ID + function loadFormById(formId) { + try { + const saved = localStorage.getItem(getStorageKey(formId)); + if (saved) { + clearFormData(); + const data = JSON.parse(saved); + Object.entries(data).forEach(([key, value]) => { + setFieldValue(key, value); + }); + currentFormId = formId; + updateFormSelector(); + showToast('Formulier geladen: ' + formId, 'success'); + } + } catch (e) { + console.warn('Could not load form:', e); + showToast('Kon formulier niet laden', 'error'); + } + } + + // Clear form data (without removing from storage) + function clearFormData() { + 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.style.display = 'none'; + img.parentElement.classList.remove('has-photo'); + }); + + history.replaceState(null, '', window.location.pathname); + } + // Load saved data from localStorage or URL function loadSavedData() { // Try URL hash first @@ -966,24 +1135,20 @@ JAVASCRIPT = """ params.forEach((value, key) => { setFieldValue(key, value); }); + currentFormId = getFormUniqueId(); return; } catch (e) { console.warn('Could not parse URL hash:', e); } } - // Try localStorage + // Try to load most recent form from 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); + const forms = getSavedForms(); + if (forms.length > 0) { + // Load most recent + forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + loadFormById(forms[0].id); } } } @@ -1052,11 +1217,33 @@ JAVASCRIPT = """ // Save data function saveData() { const data = getFormData(); + const formId = getFormUniqueId(); + + // Only save if we have a valid form ID + if (!formId) { + updateSaveIndicator('waiting'); + return; + } - // Save to localStorage + // Save to localStorage with form-specific key if (CONFIG.autosave.use_localstorage) { try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + localStorage.setItem(getStorageKey(formId), JSON.stringify(data)); + + // Build label from unique ID fields + const labelParts = []; + if (CONFIG.unique_id_fields) { + CONFIG.unique_id_fields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field && field.value) { + labelParts.push(field.value); + } + }); + } + const label = labelParts.join(' - ') || formId; + + saveFormToIndex(formId, label); + currentFormId = formId; } catch (e) { console.warn('Could not save to localStorage:', e); } @@ -1113,7 +1300,8 @@ JAVASCRIPT = """ const statusText = { 'loaded': 'Gegevens geladen', 'saving': 'Opslaan...', - 'saved': 'Opgeslagen βœ“' + 'saved': 'Opgeslagen βœ“', + 'waiting': 'Vul ID-velden in om op te slaan' }; indicator.textContent = statusText[status] || status; @@ -1412,12 +1600,32 @@ JAVASCRIPT = """ img.parentElement.classList.remove('has-photo'); }); - localStorage.removeItem(STORAGE_KEY); history.replaceState(null, '', window.location.pathname); + currentFormId = null; + updateFormSelector(); showToast('Formulier gewist', 'success'); }; + // Delete saved form + window.deleteForm = function() { + if (!currentFormId) { + showToast('Geen formulier geselecteerd om te verwijderen', 'error'); + return; + } + + if (!confirm('Weet u zeker dat u dit opgeslagen formulier wilt verwijderen?\\n\\nFormulier: ' + currentFormId)) { + return; + } + + removeFormFromIndex(currentFormId); + clearFormData(); + currentFormId = null; + updateFormSelector(); + + showToast('Formulier verwijderd', 'success'); + }; + // Show toast notification function showToast(message, type) { const toast = document.getElementById('toast'); diff --git a/src/yaml_parser.py b/src/yaml_parser.py index fc7ae5a..c610cc2 100644 --- a/src/yaml_parser.py +++ b/src/yaml_parser.py @@ -85,11 +85,12 @@ class AutosaveConfig: class InventoryConfig: """Hoofdconfiguratie voor inventarisatie""" name: str - version: str = "1.0.0" + version: str = "1.0.1" 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) + unique_id_fields: list[str] = field(default_factory=list) def parse_field(field_data: dict) -> FieldConfig: @@ -182,11 +183,12 @@ def parse_yaml(yaml_path: str) -> InventoryConfig: return InventoryConfig( name=data.get("name", "Inventarisatie"), - version=data.get("version", "1.0.0"), + version=data.get("version", "1.0.1"), style=parse_style(data.get("style", {})), export=parse_export(data.get("export", {})), autosave=parse_autosave(data.get("autosave", {})), sections=sections, + unique_id_fields=data.get("unique_id_fields", []), )