Compare commits

..

4 Commits
main ... v1.0.1

Author SHA1 Message Date
killercow 03991517b2 Compress photos for email to fit mailto limits
2 weeks ago
killercow 38bcbd90c7 Fix: photo upload on iOS and remove empty photo message from email
2 weeks ago
killercow 7c48fac373 Fix: buttons not working due to form-selector conflict
2 weeks ago
killercow cfd852cbc3 v1.0.1: Add form history with unique ID support
2 weeks ago

@ -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"

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="EasySmartInventory v1.0.0">
<meta name="generator" content="EasySmartInventory v1.0.1">
<title>Machine_Inventarisatie</title>
<style>
@ -287,6 +287,43 @@ input.invalid, select.invalid, textarea.invalid {
.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%;
}
}
</style>
</head>
<body>
@ -294,9 +331,17 @@ input.invalid, select.invalid, textarea.invalid {
<header>
<h1>Machine_Inventarisatie</h1>
<div class="version">Versie 1.0.0</div>
<div class="version">Versie 1.0.1</div>
</header>
<div class="form-selector-container">
<label for="form-selector">📋 Opgeslagen formulieren:</label>
<select id="form-selector">
<option value="">-- Selecteer --</option>
</select>
<button type="button" class="btn btn-danger" onclick="deleteForm()">🗑️ Verwijder</button>
</div>
<form id="inventory-form" onsubmit="return false;">
<section class="section">
<h2>Basisinformatie</h2>
@ -613,14 +658,14 @@ input.invalid, select.invalid, textarea.invalid {
<div class="actions">
<button type="button" class="btn btn-primary" onclick="exportCSV()">📥 Exporteer CSV</button>
<button type="button" class="btn btn-success" onclick="sendEmail(this)">📧 Verstuur per Email</button>
<button type="button" class="btn btn-danger" onclick="clearForm()">🗑️ Formulier Wissen</button>
<button type="button" class="btn btn-secondary" onclick="clearForm()">🔄 Nieuw Formulier</button>
</div>
</form>
<div class="status-bar">
<span id="save-indicator" class="save-indicator">Gereed</span>
<span>EasySmartInventory v1.0.0</span>
<span>EasySmartInventory v1.0.1</span>
</div>
</div>
@ -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,178 @@ 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 = '<option value="">-- Selecteer opgeslagen formulier --</option>';
selector.innerHTML += '<option value="__new__"> Nieuw formulier</option>';
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() {
const form = document.getElementById('inventory-form');
if (!form) return;
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id !== 'form-selector') {
field.value = '';
field.classList.remove('invalid');
}
});
form.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
form.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 +874,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);
}
}
}
@ -705,30 +920,32 @@ input.invalid, select.invalid, textarea.invalid {
// Get all form data
function getFormData() {
const data = {};
const form = document.getElementById('inventory-form');
if (!form) return data;
// Text, number, date, select, textarea
document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id) {
// Text, number, date, select, textarea (only within the form)
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id && field.id !== 'form-selector') {
data[field.id] = field.value;
}
});
// Checkboxes (boolean)
document.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
form.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
if (field.id) {
data[field.id] = field.checked;
}
});
// Multiselect
document.querySelectorAll('.multiselect-group').forEach(group => {
form.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 => {
form.querySelectorAll('.photo-preview').forEach(img => {
const id = img.id;
if (img.src && !img.src.includes('data:image/svg')) {
data[id] = img.src;
@ -741,11 +958,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 +1041,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;
@ -880,27 +1120,54 @@ input.invalid, select.invalid, textarea.invalid {
document.querySelectorAll('.photo-container').forEach(container => {
const input = container.querySelector('input[type="file"]');
const preview = container.querySelector('.photo-preview');
const fieldId = preview.id;
const placeholder = container.querySelector('.photo-placeholder');
const fieldId = preview ? preview.id : null;
const maxWidth = parseInt(container.dataset.maxWidth) || 1200;
const maxHeight = parseInt(container.dataset.maxHeight) || 1200;
// Make the placeholder clickable with a label
if (placeholder && input) {
// Create a unique ID for the input
const inputId = 'photo-input-' + (fieldId || Math.random().toString(36).substr(2, 9));
input.id = inputId;
// Wrap placeholder in a label for better iOS support
const label = document.createElement('label');
label.setAttribute('for', inputId);
label.style.cursor = 'pointer';
label.style.display = 'block';
label.innerHTML = placeholder.innerHTML;
placeholder.innerHTML = '';
placeholder.appendChild(label);
}
// Also handle click on container (for desktop)
container.addEventListener('click', function(e) {
if (e.target.tagName !== 'BUTTON') {
// Don't trigger if clicking on buttons or if already handled by label
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'LABEL' || e.target.closest('label')) {
return;
}
if (input) {
input.click();
}
});
if (input) {
input.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
processImage(file, maxWidth, maxHeight, function(dataUrl) {
if (preview) {
preview.src = dataUrl;
preview.style.display = 'block';
}
container.classList.add('has-photo');
hasChanges = true;
saveData();
});
});
}
});
}
@ -936,6 +1203,40 @@ input.invalid, select.invalid, textarea.invalid {
reader.readAsDataURL(file);
}
// Compress image for email (much smaller)
function compressImageForEmail(dataUrl, callback) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const maxSize = 300; // Max 300x300 for email
let width = img.width;
let height = img.height;
// Scale down to max 300px
if (width > height) {
if (width > maxSize) {
height = height * (maxSize / width);
width = maxSize;
}
} else {
if (height > maxSize) {
width = width * (maxSize / height);
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
// Very low quality for email
callback(canvas.toDataURL('image/jpeg', 0.3));
};
img.src = dataUrl;
}
// Remove photo
window.removePhoto = function(fieldId) {
const preview = document.getElementById(fieldId);
@ -995,7 +1296,7 @@ input.invalid, select.invalid, textarea.invalid {
}
// Use setTimeout to allow UI to update before heavy processing
setTimeout(function() {
setTimeout(async function() {
const data = getFormData();
// Build subject
@ -1016,8 +1317,9 @@ input.invalid, select.invalid, textarea.invalid {
body += 'Versie: ' + CONFIG.version + '\n';
body += 'Datum: ' + formatDateTime(new Date()) + '\n\n';
// Collect photos separately
// Collect photos separately and compress them
const photos = [];
const photoPromises = [];
Object.entries(data).forEach(([key, value]) => {
if (value && value !== '') {
@ -1025,14 +1327,18 @@ input.invalid, select.invalid, textarea.invalid {
if (typeof value === 'string' && value.startsWith('data:image/')) {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
// Extract extension from data URL (e.g., data:image/jpeg;base64,...)
const mimeMatch = value.match(/data:image\/([a-z]+);/);
const ext = mimeMatch ? mimeMatch[1] : 'jpg';
photos.push({
// Compress photo for email
const promise = new Promise((resolve) => {
compressImageForEmail(value, function(compressedData) {
resolve({
name: labelText,
extension: ext,
data: value
extension: 'jpg',
data: compressedData
});
});
});
photoPromises.push(promise);
} else {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
@ -1042,25 +1348,25 @@ input.invalid, select.invalid, textarea.invalid {
}
});
// Wait for all photos to be compressed
const compressedPhotos = await Promise.all(photoPromises);
body += '\n========================\n';
// Add photos section if there are any
if (photos.length > 0) {
body += '\nFOTO BIJLAGEN (BASE64)\n';
body += '========================\n';
body += 'Onderstaande foto\'s zijn gecodeerd in base64 formaat.\n';
body += 'Kopieer de tekst tussen START en EINDE naar een base64 decoder\n';
body += 'of gebruik een online tool zoals base64-image.de\n\n';
// Add photos section only if there are photos
if (compressedPhotos.length > 0) {
body += '\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\n';
body += '==============================================\n';
body += 'Foto\'s zijn verkleind naar 300x300px voor email.\n';
body += 'Voor originele kwaliteit, gebruik CSV export.\n\n';
photos.forEach((photo, index) => {
compressedPhotos.forEach((photo, index) => {
const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension;
body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\n';
body += '>>> START BASE64 >>>\n';
body += photo.data + '\n';
body += '<<< EINDE BASE64 <<<\n\n';
});
} else {
body += 'Geen foto\'s toegevoegd aan dit formulier.\n';
}
const mailto = 'mailto:' + encodeURIComponent(CONFIG.export.mailto.to) +
@ -1073,9 +1379,9 @@ input.invalid, select.invalid, textarea.invalid {
button.disabled = false;
}
// Check if mailto URL is too long (most clients support ~2000 chars)
if (mailto.length > 100000) {
showToast('Email te groot door foto. Gebruik CSV export.', 'error');
// Check if mailto URL is too long
if (mailto.length > 64000) {
showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error');
return;
}
@ -1101,12 +1407,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');

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="EasySmartInventory v1.0.0">
<meta name="generator" content="EasySmartInventory v1.0.1">
<title>Machine_Inventarisatie</title>
<style>
@ -318,6 +318,43 @@ select {
.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%;
}
}
</style>
</head>
<body>
@ -325,9 +362,17 @@ select {
<header>
<h1>Machine_Inventarisatie</h1>
<div class="version">Versie 1.0.0</div>
<div class="version">Versie 1.0.1</div>
</header>
<div class="form-selector-container">
<label for="form-selector">📋 Opgeslagen formulieren:</label>
<select id="form-selector">
<option value="">-- Selecteer --</option>
</select>
<button type="button" class="btn btn-danger" onclick="deleteForm()">🗑️ Verwijder</button>
</div>
<form id="inventory-form" onsubmit="return false;">
<section class="section">
<h2>Basisinformatie</h2>
@ -644,14 +689,14 @@ select {
<div class="actions">
<button type="button" class="btn btn-primary" onclick="exportCSV()">📥 Exporteer CSV</button>
<button type="button" class="btn btn-success" onclick="sendEmail(this)">📧 Verstuur per Email</button>
<button type="button" class="btn btn-danger" onclick="clearForm()">🗑️ Formulier Wissen</button>
<button type="button" class="btn btn-secondary" onclick="clearForm()">🔄 Nieuw Formulier</button>
</div>
</form>
<div class="status-bar">
<span id="save-indicator" class="save-indicator">Gereed</span>
<span>EasySmartInventory v1.0.0</span>
<span>EasySmartInventory v1.0.1</span>
</div>
</div>
@ -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,178 @@ 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 = '<option value="">-- Selecteer opgeslagen formulier --</option>';
selector.innerHTML += '<option value="__new__"> Nieuw formulier</option>';
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() {
const form = document.getElementById('inventory-form');
if (!form) return;
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id !== 'form-selector') {
field.value = '';
field.classList.remove('invalid');
}
});
form.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
form.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 +905,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);
}
}
}
@ -736,30 +951,32 @@ select {
// Get all form data
function getFormData() {
const data = {};
const form = document.getElementById('inventory-form');
if (!form) return data;
// Text, number, date, select, textarea
document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id) {
// Text, number, date, select, textarea (only within the form)
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id && field.id !== 'form-selector') {
data[field.id] = field.value;
}
});
// Checkboxes (boolean)
document.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
form.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
if (field.id) {
data[field.id] = field.checked;
}
});
// Multiselect
document.querySelectorAll('.multiselect-group').forEach(group => {
form.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 => {
form.querySelectorAll('.photo-preview').forEach(img => {
const id = img.id;
if (img.src && !img.src.includes('data:image/svg')) {
data[id] = img.src;
@ -772,11 +989,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 +1072,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;
@ -911,27 +1151,54 @@ select {
document.querySelectorAll('.photo-container').forEach(container => {
const input = container.querySelector('input[type="file"]');
const preview = container.querySelector('.photo-preview');
const fieldId = preview.id;
const placeholder = container.querySelector('.photo-placeholder');
const fieldId = preview ? preview.id : null;
const maxWidth = parseInt(container.dataset.maxWidth) || 1200;
const maxHeight = parseInt(container.dataset.maxHeight) || 1200;
// Make the placeholder clickable with a label
if (placeholder && input) {
// Create a unique ID for the input
const inputId = 'photo-input-' + (fieldId || Math.random().toString(36).substr(2, 9));
input.id = inputId;
// Wrap placeholder in a label for better iOS support
const label = document.createElement('label');
label.setAttribute('for', inputId);
label.style.cursor = 'pointer';
label.style.display = 'block';
label.innerHTML = placeholder.innerHTML;
placeholder.innerHTML = '';
placeholder.appendChild(label);
}
// Also handle click on container (for desktop)
container.addEventListener('click', function(e) {
if (e.target.tagName !== 'BUTTON') {
// Don't trigger if clicking on buttons or if already handled by label
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'LABEL' || e.target.closest('label')) {
return;
}
if (input) {
input.click();
}
});
if (input) {
input.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
processImage(file, maxWidth, maxHeight, function(dataUrl) {
if (preview) {
preview.src = dataUrl;
preview.style.display = 'block';
}
container.classList.add('has-photo');
hasChanges = true;
saveData();
});
});
}
});
}
@ -967,6 +1234,40 @@ select {
reader.readAsDataURL(file);
}
// Compress image for email (much smaller)
function compressImageForEmail(dataUrl, callback) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const maxSize = 300; // Max 300x300 for email
let width = img.width;
let height = img.height;
// Scale down to max 300px
if (width > height) {
if (width > maxSize) {
height = height * (maxSize / width);
width = maxSize;
}
} else {
if (height > maxSize) {
width = width * (maxSize / height);
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
// Very low quality for email
callback(canvas.toDataURL('image/jpeg', 0.3));
};
img.src = dataUrl;
}
// Remove photo
window.removePhoto = function(fieldId) {
const preview = document.getElementById(fieldId);
@ -1026,7 +1327,7 @@ select {
}
// Use setTimeout to allow UI to update before heavy processing
setTimeout(function() {
setTimeout(async function() {
const data = getFormData();
// Build subject
@ -1047,8 +1348,9 @@ select {
body += 'Versie: ' + CONFIG.version + '\n';
body += 'Datum: ' + formatDateTime(new Date()) + '\n\n';
// Collect photos separately
// Collect photos separately and compress them
const photos = [];
const photoPromises = [];
Object.entries(data).forEach(([key, value]) => {
if (value && value !== '') {
@ -1056,14 +1358,18 @@ select {
if (typeof value === 'string' && value.startsWith('data:image/')) {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
// Extract extension from data URL (e.g., data:image/jpeg;base64,...)
const mimeMatch = value.match(/data:image\/([a-z]+);/);
const ext = mimeMatch ? mimeMatch[1] : 'jpg';
photos.push({
// Compress photo for email
const promise = new Promise((resolve) => {
compressImageForEmail(value, function(compressedData) {
resolve({
name: labelText,
extension: ext,
data: value
extension: 'jpg',
data: compressedData
});
});
});
photoPromises.push(promise);
} else {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
@ -1073,25 +1379,25 @@ select {
}
});
// Wait for all photos to be compressed
const compressedPhotos = await Promise.all(photoPromises);
body += '\n========================\n';
// Add photos section if there are any
if (photos.length > 0) {
body += '\nFOTO BIJLAGEN (BASE64)\n';
body += '========================\n';
body += 'Onderstaande foto\'s zijn gecodeerd in base64 formaat.\n';
body += 'Kopieer de tekst tussen START en EINDE naar een base64 decoder\n';
body += 'of gebruik een online tool zoals base64-image.de\n\n';
// Add photos section only if there are photos
if (compressedPhotos.length > 0) {
body += '\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\n';
body += '==============================================\n';
body += 'Foto\'s zijn verkleind naar 300x300px voor email.\n';
body += 'Voor originele kwaliteit, gebruik CSV export.\n\n';
photos.forEach((photo, index) => {
compressedPhotos.forEach((photo, index) => {
const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension;
body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\n';
body += '>>> START BASE64 >>>\n';
body += photo.data + '\n';
body += '<<< EINDE BASE64 <<<\n\n';
});
} else {
body += 'Geen foto\'s toegevoegd aan dit formulier.\n';
}
const mailto = 'mailto:' + encodeURIComponent(CONFIG.export.mailto.to) +
@ -1104,9 +1410,9 @@ select {
button.disabled = false;
}
// Check if mailto URL is too long (most clients support ~2000 chars)
if (mailto.length > 100000) {
showToast('Email te groot door foto. Gebruik CSV export.', 'error');
// Check if mailto URL is too long
if (mailto.length > 64000) {
showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error');
return;
}
@ -1132,12 +1438,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');

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="EasySmartInventory v1.0.0">
<meta name="generator" content="EasySmartInventory v1.0.1">
<title>Machine_Inventarisatie</title>
<style>
@ -349,6 +349,43 @@ input.invalid, select.invalid, textarea.invalid {
.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%;
}
}
</style>
</head>
<body>
@ -356,9 +393,17 @@ input.invalid, select.invalid, textarea.invalid {
<header>
<h1>Machine_Inventarisatie</h1>
<div class="version">Versie 1.0.0</div>
<div class="version">Versie 1.0.1</div>
</header>
<div class="form-selector-container">
<label for="form-selector">📋 Opgeslagen formulieren:</label>
<select id="form-selector">
<option value="">-- Selecteer --</option>
</select>
<button type="button" class="btn btn-danger" onclick="deleteForm()">🗑️ Verwijder</button>
</div>
<form id="inventory-form" onsubmit="return false;">
<section class="section">
<h2>Basisinformatie</h2>
@ -675,14 +720,14 @@ input.invalid, select.invalid, textarea.invalid {
<div class="actions">
<button type="button" class="btn btn-primary" onclick="exportCSV()">📥 Exporteer CSV</button>
<button type="button" class="btn btn-success" onclick="sendEmail(this)">📧 Verstuur per Email</button>
<button type="button" class="btn btn-danger" onclick="clearForm()">🗑️ Formulier Wissen</button>
<button type="button" class="btn btn-secondary" onclick="clearForm()">🔄 Nieuw Formulier</button>
</div>
</form>
<div class="status-bar">
<span id="save-indicator" class="save-indicator">Gereed</span>
<span>EasySmartInventory v1.0.0</span>
<span>EasySmartInventory v1.0.1</span>
</div>
</div>
@ -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,178 @@ 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 = '<option value="">-- Selecteer opgeslagen formulier --</option>';
selector.innerHTML += '<option value="__new__"> Nieuw formulier</option>';
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() {
const form = document.getElementById('inventory-form');
if (!form) return;
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id !== 'form-selector') {
field.value = '';
field.classList.remove('invalid');
}
});
form.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
form.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 +936,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);
}
}
}
@ -767,30 +982,32 @@ input.invalid, select.invalid, textarea.invalid {
// Get all form data
function getFormData() {
const data = {};
const form = document.getElementById('inventory-form');
if (!form) return data;
// Text, number, date, select, textarea
document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id) {
// Text, number, date, select, textarea (only within the form)
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id && field.id !== 'form-selector') {
data[field.id] = field.value;
}
});
// Checkboxes (boolean)
document.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
form.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
if (field.id) {
data[field.id] = field.checked;
}
});
// Multiselect
document.querySelectorAll('.multiselect-group').forEach(group => {
form.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 => {
form.querySelectorAll('.photo-preview').forEach(img => {
const id = img.id;
if (img.src && !img.src.includes('data:image/svg')) {
data[id] = img.src;
@ -803,11 +1020,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 +1103,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;
@ -942,27 +1182,54 @@ input.invalid, select.invalid, textarea.invalid {
document.querySelectorAll('.photo-container').forEach(container => {
const input = container.querySelector('input[type="file"]');
const preview = container.querySelector('.photo-preview');
const fieldId = preview.id;
const placeholder = container.querySelector('.photo-placeholder');
const fieldId = preview ? preview.id : null;
const maxWidth = parseInt(container.dataset.maxWidth) || 1200;
const maxHeight = parseInt(container.dataset.maxHeight) || 1200;
// Make the placeholder clickable with a label
if (placeholder && input) {
// Create a unique ID for the input
const inputId = 'photo-input-' + (fieldId || Math.random().toString(36).substr(2, 9));
input.id = inputId;
// Wrap placeholder in a label for better iOS support
const label = document.createElement('label');
label.setAttribute('for', inputId);
label.style.cursor = 'pointer';
label.style.display = 'block';
label.innerHTML = placeholder.innerHTML;
placeholder.innerHTML = '';
placeholder.appendChild(label);
}
// Also handle click on container (for desktop)
container.addEventListener('click', function(e) {
if (e.target.tagName !== 'BUTTON') {
// Don't trigger if clicking on buttons or if already handled by label
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'LABEL' || e.target.closest('label')) {
return;
}
if (input) {
input.click();
}
});
if (input) {
input.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
processImage(file, maxWidth, maxHeight, function(dataUrl) {
if (preview) {
preview.src = dataUrl;
preview.style.display = 'block';
}
container.classList.add('has-photo');
hasChanges = true;
saveData();
});
});
}
});
}
@ -998,6 +1265,40 @@ input.invalid, select.invalid, textarea.invalid {
reader.readAsDataURL(file);
}
// Compress image for email (much smaller)
function compressImageForEmail(dataUrl, callback) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const maxSize = 300; // Max 300x300 for email
let width = img.width;
let height = img.height;
// Scale down to max 300px
if (width > height) {
if (width > maxSize) {
height = height * (maxSize / width);
width = maxSize;
}
} else {
if (height > maxSize) {
width = width * (maxSize / height);
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
// Very low quality for email
callback(canvas.toDataURL('image/jpeg', 0.3));
};
img.src = dataUrl;
}
// Remove photo
window.removePhoto = function(fieldId) {
const preview = document.getElementById(fieldId);
@ -1057,7 +1358,7 @@ input.invalid, select.invalid, textarea.invalid {
}
// Use setTimeout to allow UI to update before heavy processing
setTimeout(function() {
setTimeout(async function() {
const data = getFormData();
// Build subject
@ -1078,8 +1379,9 @@ input.invalid, select.invalid, textarea.invalid {
body += 'Versie: ' + CONFIG.version + '\n';
body += 'Datum: ' + formatDateTime(new Date()) + '\n\n';
// Collect photos separately
// Collect photos separately and compress them
const photos = [];
const photoPromises = [];
Object.entries(data).forEach(([key, value]) => {
if (value && value !== '') {
@ -1087,14 +1389,18 @@ input.invalid, select.invalid, textarea.invalid {
if (typeof value === 'string' && value.startsWith('data:image/')) {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
// Extract extension from data URL (e.g., data:image/jpeg;base64,...)
const mimeMatch = value.match(/data:image\/([a-z]+);/);
const ext = mimeMatch ? mimeMatch[1] : 'jpg';
photos.push({
// Compress photo for email
const promise = new Promise((resolve) => {
compressImageForEmail(value, function(compressedData) {
resolve({
name: labelText,
extension: ext,
data: value
extension: 'jpg',
data: compressedData
});
});
});
photoPromises.push(promise);
} else {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
@ -1104,25 +1410,25 @@ input.invalid, select.invalid, textarea.invalid {
}
});
// Wait for all photos to be compressed
const compressedPhotos = await Promise.all(photoPromises);
body += '\n========================\n';
// Add photos section if there are any
if (photos.length > 0) {
body += '\nFOTO BIJLAGEN (BASE64)\n';
body += '========================\n';
body += 'Onderstaande foto\'s zijn gecodeerd in base64 formaat.\n';
body += 'Kopieer de tekst tussen START en EINDE naar een base64 decoder\n';
body += 'of gebruik een online tool zoals base64-image.de\n\n';
// Add photos section only if there are photos
if (compressedPhotos.length > 0) {
body += '\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\n';
body += '==============================================\n';
body += 'Foto\'s zijn verkleind naar 300x300px voor email.\n';
body += 'Voor originele kwaliteit, gebruik CSV export.\n\n';
photos.forEach((photo, index) => {
compressedPhotos.forEach((photo, index) => {
const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension;
body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\n';
body += '>>> START BASE64 >>>\n';
body += photo.data + '\n';
body += '<<< EINDE BASE64 <<<\n\n';
});
} else {
body += 'Geen foto\'s toegevoegd aan dit formulier.\n';
}
const mailto = 'mailto:' + encodeURIComponent(CONFIG.export.mailto.to) +
@ -1135,9 +1441,9 @@ input.invalid, select.invalid, textarea.invalid {
button.disabled = false;
}
// Check if mailto URL is too long (most clients support ~2000 chars)
if (mailto.length > 100000) {
showToast('Email te groot door foto. Gebruik CSV export.', 'error');
// Check if mailto URL is too long
if (mailto.length > 64000) {
showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error');
return;
}
@ -1163,12 +1469,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');

@ -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`

@ -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 = '''<div class="form-selector-container">
<label for="form-selector">📋 Opgeslagen formulieren:</label>
<select id="form-selector">
<option value="">-- Selecteer --</option>
</select>
<button type="button" class="btn btn-danger" onclick="deleteForm()">🗑 Verwijder</button>
</div>
'''
# Secties genereren
sections_html = ""
for section in config.sections:
@ -154,7 +204,7 @@ def generate_html(config: InventoryConfig) -> str:
actions_html += ' <button type="button" class="btn btn-primary" onclick="exportCSV()">📥 Exporteer CSV</button>\n'
if config.export.mailto.enabled:
actions_html += ' <button type="button" class="btn btn-success" onclick="sendEmail(this)">📧 Verstuur per Email</button>\n'
actions_html += ' <button type="button" class="btn btn-danger" onclick="clearForm()">🗑️ Formulier Wissen</button>\n'
actions_html += ' <button type="button" class="btn btn-secondary" onclick="clearForm()">🔄 Nieuw Formulier</button>\n'
actions_html += '</div>\n'
# Complete HTML
@ -177,6 +227,7 @@ def generate_html(config: InventoryConfig) -> str:
<div class="version">Versie {config.version}</div>
</header>
{form_selector_html}
<form id="inventory-form" onsubmit="return false;">
{sections_html}
{actions_html}

@ -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,178 @@ 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 = '<option value="">-- Selecteer opgeslagen formulier --</option>';
selector.innerHTML += '<option value="__new__"> Nieuw formulier</option>';
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() {
const form = document.getElementById('inventory-form');
if (!form) return;
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id !== 'form-selector') {
field.value = '';
field.classList.remove('invalid');
}
});
form.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
form.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 +1140,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);
}
}
}
@ -1016,30 +1186,32 @@ JAVASCRIPT = """
// Get all form data
function getFormData() {
const data = {};
const form = document.getElementById('inventory-form');
if (!form) return data;
// Text, number, date, select, textarea
document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id) {
// Text, number, date, select, textarea (only within the form)
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id && field.id !== 'form-selector') {
data[field.id] = field.value;
}
});
// Checkboxes (boolean)
document.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
form.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
if (field.id) {
data[field.id] = field.checked;
}
});
// Multiselect
document.querySelectorAll('.multiselect-group').forEach(group => {
form.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 => {
form.querySelectorAll('.photo-preview').forEach(img => {
const id = img.id;
if (img.src && !img.src.includes('data:image/svg')) {
data[id] = img.src;
@ -1052,11 +1224,33 @@ JAVASCRIPT = """
// Save data
function saveData() {
const data = getFormData();
const formId = getFormUniqueId();
// Save to localStorage
// Only save if we have a valid form ID
if (!formId) {
updateSaveIndicator('waiting');
return;
}
// 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 +1307,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;
@ -1191,27 +1386,54 @@ JAVASCRIPT = """
document.querySelectorAll('.photo-container').forEach(container => {
const input = container.querySelector('input[type="file"]');
const preview = container.querySelector('.photo-preview');
const fieldId = preview.id;
const placeholder = container.querySelector('.photo-placeholder');
const fieldId = preview ? preview.id : null;
const maxWidth = parseInt(container.dataset.maxWidth) || 1200;
const maxHeight = parseInt(container.dataset.maxHeight) || 1200;
// Make the placeholder clickable with a label
if (placeholder && input) {
// Create a unique ID for the input
const inputId = 'photo-input-' + (fieldId || Math.random().toString(36).substr(2, 9));
input.id = inputId;
// Wrap placeholder in a label for better iOS support
const label = document.createElement('label');
label.setAttribute('for', inputId);
label.style.cursor = 'pointer';
label.style.display = 'block';
label.innerHTML = placeholder.innerHTML;
placeholder.innerHTML = '';
placeholder.appendChild(label);
}
// Also handle click on container (for desktop)
container.addEventListener('click', function(e) {
if (e.target.tagName !== 'BUTTON') {
// Don't trigger if clicking on buttons or if already handled by label
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'LABEL' || e.target.closest('label')) {
return;
}
if (input) {
input.click();
}
});
if (input) {
input.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
processImage(file, maxWidth, maxHeight, function(dataUrl) {
if (preview) {
preview.src = dataUrl;
preview.style.display = 'block';
}
container.classList.add('has-photo');
hasChanges = true;
saveData();
});
});
}
});
}
@ -1247,6 +1469,40 @@ JAVASCRIPT = """
reader.readAsDataURL(file);
}
// Compress image for email (much smaller)
function compressImageForEmail(dataUrl, callback) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const maxSize = 300; // Max 300x300 for email
let width = img.width;
let height = img.height;
// Scale down to max 300px
if (width > height) {
if (width > maxSize) {
height = height * (maxSize / width);
width = maxSize;
}
} else {
if (height > maxSize) {
width = width * (maxSize / height);
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
// Very low quality for email
callback(canvas.toDataURL('image/jpeg', 0.3));
};
img.src = dataUrl;
}
// Remove photo
window.removePhoto = function(fieldId) {
const preview = document.getElementById(fieldId);
@ -1306,7 +1562,7 @@ JAVASCRIPT = """
}
// Use setTimeout to allow UI to update before heavy processing
setTimeout(function() {
setTimeout(async function() {
const data = getFormData();
// Build subject
@ -1327,8 +1583,9 @@ JAVASCRIPT = """
body += 'Versie: ' + CONFIG.version + '\\n';
body += 'Datum: ' + formatDateTime(new Date()) + '\\n\\n';
// Collect photos separately
// Collect photos separately and compress them
const photos = [];
const photoPromises = [];
Object.entries(data).forEach(([key, value]) => {
if (value && value !== '') {
@ -1336,14 +1593,18 @@ JAVASCRIPT = """
if (typeof value === 'string' && value.startsWith('data:image/')) {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
// Extract extension from data URL (e.g., data:image/jpeg;base64,...)
const mimeMatch = value.match(/data:image\\/([a-z]+);/);
const ext = mimeMatch ? mimeMatch[1] : 'jpg';
photos.push({
// Compress photo for email
const promise = new Promise((resolve) => {
compressImageForEmail(value, function(compressedData) {
resolve({
name: labelText,
extension: ext,
data: value
extension: 'jpg',
data: compressedData
});
});
});
photoPromises.push(promise);
} else {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
@ -1353,25 +1614,25 @@ JAVASCRIPT = """
}
});
// Wait for all photos to be compressed
const compressedPhotos = await Promise.all(photoPromises);
body += '\\n========================\\n';
// Add photos section if there are any
if (photos.length > 0) {
body += '\\nFOTO BIJLAGEN (BASE64)\\n';
body += '========================\\n';
body += 'Onderstaande foto\\'s zijn gecodeerd in base64 formaat.\\n';
body += 'Kopieer de tekst tussen START en EINDE naar een base64 decoder\\n';
body += 'of gebruik een online tool zoals base64-image.de\\n\\n';
// Add photos section only if there are photos
if (compressedPhotos.length > 0) {
body += '\\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\\n';
body += '==============================================\\n';
body += 'Foto\\'s zijn verkleind naar 300x300px voor email.\\n';
body += 'Voor originele kwaliteit, gebruik CSV export.\\n\\n';
photos.forEach((photo, index) => {
compressedPhotos.forEach((photo, index) => {
const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension;
body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\\n';
body += '>>> START BASE64 >>>\\n';
body += photo.data + '\\n';
body += '<<< EINDE BASE64 <<<\\n\\n';
});
} else {
body += 'Geen foto\\'s toegevoegd aan dit formulier.\\n';
}
const mailto = 'mailto:' + encodeURIComponent(CONFIG.export.mailto.to) +
@ -1384,9 +1645,9 @@ JAVASCRIPT = """
button.disabled = false;
}
// Check if mailto URL is too long (most clients support ~2000 chars)
if (mailto.length > 100000) {
showToast('Email te groot door foto. Gebruik CSV export.', 'error');
// Check if mailto URL is too long
if (mailto.length > 64000) {
showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error');
return;
}
@ -1412,12 +1673,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');

@ -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", []),
)

Loading…
Cancel
Save

Powered by TurnKey Linux.