@ -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- dange r" onclick = "clearForm()" > 🗑️ Formulier Wissen < / button >
< button type = "button" class = "btn btn- secon dary " 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();
// 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);
}
@ -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) => {
com pressedP hotos.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 > 100 000) {
showToast('Email te groot door foto. Gebruik CSV export .', 'error');
// Check if mailto URL is too long
if (mailto.length > 64 000) {
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');