|
|
|
@ -3,7 +3,7 @@
|
|
|
|
<head>
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<meta name="generator" content="EasySmartInventory v1.0.1">
|
|
|
|
<meta name="generator" content="EasySmartInventory v1.0.0">
|
|
|
|
<title>Machine_Inventarisatie</title>
|
|
|
|
<title>Machine_Inventarisatie</title>
|
|
|
|
<style>
|
|
|
|
<style>
|
|
|
|
|
|
|
|
|
|
|
|
@ -349,43 +349,6 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
.photo-container.has-photo .photo-preview { display: block !important; }
|
|
|
|
.photo-container.has-photo .photo-preview { display: block !important; }
|
|
|
|
.photo-container.has-photo .photo-buttons { display: flex !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>
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<body>
|
|
|
|
@ -393,17 +356,9 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
<header>
|
|
|
|
<header>
|
|
|
|
|
|
|
|
|
|
|
|
<h1>Machine_Inventarisatie</h1>
|
|
|
|
<h1>Machine_Inventarisatie</h1>
|
|
|
|
<div class="version">Versie 1.0.1</div>
|
|
|
|
<div class="version">Versie 1.0.0</div>
|
|
|
|
</header>
|
|
|
|
</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;">
|
|
|
|
<form id="inventory-form" onsubmit="return false;">
|
|
|
|
<section class="section">
|
|
|
|
<section class="section">
|
|
|
|
<h2>Basisinformatie</h2>
|
|
|
|
<h2>Basisinformatie</h2>
|
|
|
|
@ -720,14 +675,14 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
<div class="actions">
|
|
|
|
<div class="actions">
|
|
|
|
<button type="button" class="btn btn-primary" onclick="exportCSV()">📥 Exporteer CSV</button>
|
|
|
|
<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-success" onclick="sendEmail(this)">📧 Verstuur per Email</button>
|
|
|
|
<button type="button" class="btn btn-secondary" onclick="clearForm()">🔄 Nieuw Formulier</button>
|
|
|
|
<button type="button" class="btn btn-danger" onclick="clearForm()">🗑️ Formulier Wissen</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</form>
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="status-bar">
|
|
|
|
<div class="status-bar">
|
|
|
|
<span id="save-indicator" class="save-indicator">Gereed</span>
|
|
|
|
<span id="save-indicator" class="save-indicator">Gereed</span>
|
|
|
|
<span>EasySmartInventory v1.0.1</span>
|
|
|
|
<span>EasySmartInventory v1.0.0</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
@ -738,15 +693,13 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
(function() {
|
|
|
|
(function() {
|
|
|
|
'use strict';
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
|
|
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 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 FORMS_INDEX_KEY = 'inventory_' + CONFIG.name + '_forms_index';
|
|
|
|
const STORAGE_KEY = 'inventory_' + CONFIG.name + '_' + CONFIG.version;
|
|
|
|
let currentFormId = null;
|
|
|
|
|
|
|
|
let saveTimeout = null;
|
|
|
|
let saveTimeout = null;
|
|
|
|
let hasChanges = false;
|
|
|
|
let hasChanges = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize
|
|
|
|
// Initialize
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
setupFormSelector();
|
|
|
|
|
|
|
|
loadSavedData();
|
|
|
|
loadSavedData();
|
|
|
|
setupAutoSave();
|
|
|
|
setupAutoSave();
|
|
|
|
setupValidation();
|
|
|
|
setupValidation();
|
|
|
|
@ -754,178 +707,6 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
updateSaveIndicator('loaded');
|
|
|
|
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
|
|
|
|
// Load saved data from localStorage or URL
|
|
|
|
function loadSavedData() {
|
|
|
|
function loadSavedData() {
|
|
|
|
// Try URL hash first
|
|
|
|
// Try URL hash first
|
|
|
|
@ -936,20 +717,24 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
params.forEach((value, key) => {
|
|
|
|
params.forEach((value, key) => {
|
|
|
|
setFieldValue(key, value);
|
|
|
|
setFieldValue(key, value);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
currentFormId = getFormUniqueId();
|
|
|
|
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
console.warn('Could not parse URL hash:', e);
|
|
|
|
console.warn('Could not parse URL hash:', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Try to load most recent form from localStorage
|
|
|
|
// Try localStorage
|
|
|
|
if (CONFIG.autosave.use_localstorage) {
|
|
|
|
if (CONFIG.autosave.use_localstorage) {
|
|
|
|
const forms = getSavedForms();
|
|
|
|
try {
|
|
|
|
if (forms.length > 0) {
|
|
|
|
const saved = localStorage.getItem(STORAGE_KEY);
|
|
|
|
// Load most recent
|
|
|
|
if (saved) {
|
|
|
|
forms.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified));
|
|
|
|
const data = JSON.parse(saved);
|
|
|
|
loadFormById(forms[0].id);
|
|
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
|
|
|
|
|
|
setFieldValue(key, value);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
console.warn('Could not load from localStorage:', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -982,32 +767,30 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
// Get all form data
|
|
|
|
// Get all form data
|
|
|
|
function getFormData() {
|
|
|
|
function getFormData() {
|
|
|
|
const data = {};
|
|
|
|
const data = {};
|
|
|
|
const form = document.getElementById('inventory-form');
|
|
|
|
|
|
|
|
if (!form) return data;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Text, number, date, select, textarea (only within the form)
|
|
|
|
// Text, number, date, select, textarea
|
|
|
|
form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
|
|
|
|
document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
|
|
|
|
if (field.id && field.id !== 'form-selector') {
|
|
|
|
if (field.id) {
|
|
|
|
data[field.id] = field.value;
|
|
|
|
data[field.id] = field.value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Checkboxes (boolean)
|
|
|
|
// Checkboxes (boolean)
|
|
|
|
form.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
|
|
|
|
document.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
|
|
|
|
if (field.id) {
|
|
|
|
if (field.id) {
|
|
|
|
data[field.id] = field.checked;
|
|
|
|
data[field.id] = field.checked;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Multiselect
|
|
|
|
// Multiselect
|
|
|
|
form.querySelectorAll('.multiselect-group').forEach(group => {
|
|
|
|
document.querySelectorAll('.multiselect-group').forEach(group => {
|
|
|
|
const id = group.dataset.id;
|
|
|
|
const id = group.dataset.id;
|
|
|
|
const checked = Array.from(group.querySelectorAll('input:checked')).map(cb => cb.value);
|
|
|
|
const checked = Array.from(group.querySelectorAll('input:checked')).map(cb => cb.value);
|
|
|
|
data[id] = checked;
|
|
|
|
data[id] = checked;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Photo
|
|
|
|
// Photo
|
|
|
|
form.querySelectorAll('.photo-preview').forEach(img => {
|
|
|
|
document.querySelectorAll('.photo-preview').forEach(img => {
|
|
|
|
const id = img.id;
|
|
|
|
const id = img.id;
|
|
|
|
if (img.src && !img.src.includes('data:image/svg')) {
|
|
|
|
if (img.src && !img.src.includes('data:image/svg')) {
|
|
|
|
data[id] = img.src;
|
|
|
|
data[id] = img.src;
|
|
|
|
@ -1020,33 +803,11 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
// Save data
|
|
|
|
// Save data
|
|
|
|
function saveData() {
|
|
|
|
function saveData() {
|
|
|
|
const data = getFormData();
|
|
|
|
const data = getFormData();
|
|
|
|
const formId = getFormUniqueId();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Only save if we have a valid form ID
|
|
|
|
|
|
|
|
if (!formId) {
|
|
|
|
|
|
|
|
updateSaveIndicator('waiting');
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Save to localStorage with form-specific key
|
|
|
|
// Save to localStorage
|
|
|
|
if (CONFIG.autosave.use_localstorage) {
|
|
|
|
if (CONFIG.autosave.use_localstorage) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
localStorage.setItem(getStorageKey(formId), JSON.stringify(data));
|
|
|
|
localStorage.setItem(STORAGE_KEY, 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) {
|
|
|
|
} catch (e) {
|
|
|
|
console.warn('Could not save to localStorage:', e);
|
|
|
|
console.warn('Could not save to localStorage:', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -1103,8 +864,7 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
const statusText = {
|
|
|
|
const statusText = {
|
|
|
|
'loaded': 'Gegevens geladen',
|
|
|
|
'loaded': 'Gegevens geladen',
|
|
|
|
'saving': 'Opslaan...',
|
|
|
|
'saving': 'Opslaan...',
|
|
|
|
'saved': 'Opgeslagen ✓',
|
|
|
|
'saved': 'Opgeslagen ✓'
|
|
|
|
'waiting': 'Vul ID-velden in om op te slaan'
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
indicator.textContent = statusText[status] || status;
|
|
|
|
indicator.textContent = statusText[status] || status;
|
|
|
|
@ -1182,54 +942,27 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
document.querySelectorAll('.photo-container').forEach(container => {
|
|
|
|
document.querySelectorAll('.photo-container').forEach(container => {
|
|
|
|
const input = container.querySelector('input[type="file"]');
|
|
|
|
const input = container.querySelector('input[type="file"]');
|
|
|
|
const preview = container.querySelector('.photo-preview');
|
|
|
|
const preview = container.querySelector('.photo-preview');
|
|
|
|
const placeholder = container.querySelector('.photo-placeholder');
|
|
|
|
const fieldId = preview.id;
|
|
|
|
const fieldId = preview ? preview.id : null;
|
|
|
|
|
|
|
|
const maxWidth = parseInt(container.dataset.maxWidth) || 1200;
|
|
|
|
const maxWidth = parseInt(container.dataset.maxWidth) || 1200;
|
|
|
|
const maxHeight = parseInt(container.dataset.maxHeight) || 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) {
|
|
|
|
container.addEventListener('click', function(e) {
|
|
|
|
// Don't trigger if clicking on buttons or if already handled by label
|
|
|
|
if (e.target.tagName !== 'BUTTON') {
|
|
|
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'LABEL' || e.target.closest('label')) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input) {
|
|
|
|
|
|
|
|
input.click();
|
|
|
|
input.click();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (input) {
|
|
|
|
|
|
|
|
input.addEventListener('change', function(e) {
|
|
|
|
input.addEventListener('change', function(e) {
|
|
|
|
const file = e.target.files[0];
|
|
|
|
const file = e.target.files[0];
|
|
|
|
if (!file) return;
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
|
|
processImage(file, maxWidth, maxHeight, function(dataUrl) {
|
|
|
|
processImage(file, maxWidth, maxHeight, function(dataUrl) {
|
|
|
|
if (preview) {
|
|
|
|
|
|
|
|
preview.src = dataUrl;
|
|
|
|
preview.src = dataUrl;
|
|
|
|
preview.style.display = 'block';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
container.classList.add('has-photo');
|
|
|
|
container.classList.add('has-photo');
|
|
|
|
hasChanges = true;
|
|
|
|
hasChanges = true;
|
|
|
|
saveData();
|
|
|
|
saveData();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1265,40 +998,6 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
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
|
|
|
|
// Remove photo
|
|
|
|
window.removePhoto = function(fieldId) {
|
|
|
|
window.removePhoto = function(fieldId) {
|
|
|
|
const preview = document.getElementById(fieldId);
|
|
|
|
const preview = document.getElementById(fieldId);
|
|
|
|
@ -1358,7 +1057,7 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use setTimeout to allow UI to update before heavy processing
|
|
|
|
// Use setTimeout to allow UI to update before heavy processing
|
|
|
|
setTimeout(async function() {
|
|
|
|
setTimeout(function() {
|
|
|
|
const data = getFormData();
|
|
|
|
const data = getFormData();
|
|
|
|
|
|
|
|
|
|
|
|
// Build subject
|
|
|
|
// Build subject
|
|
|
|
@ -1379,9 +1078,8 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
body += 'Versie: ' + CONFIG.version + '\n';
|
|
|
|
body += 'Versie: ' + CONFIG.version + '\n';
|
|
|
|
body += 'Datum: ' + formatDateTime(new Date()) + '\n\n';
|
|
|
|
body += 'Datum: ' + formatDateTime(new Date()) + '\n\n';
|
|
|
|
|
|
|
|
|
|
|
|
// Collect photos separately and compress them
|
|
|
|
// Collect photos separately
|
|
|
|
const photos = [];
|
|
|
|
const photos = [];
|
|
|
|
const photoPromises = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
|
|
if (value && value !== '') {
|
|
|
|
if (value && value !== '') {
|
|
|
|
@ -1389,18 +1087,14 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
if (typeof value === 'string' && value.startsWith('data:image/')) {
|
|
|
|
if (typeof value === 'string' && value.startsWith('data:image/')) {
|
|
|
|
const label = document.querySelector(`label[for="${key}"]`);
|
|
|
|
const label = document.querySelector(`label[for="${key}"]`);
|
|
|
|
const labelText = label ? label.textContent.replace('*', '').trim() : key;
|
|
|
|
const labelText = label ? label.textContent.replace('*', '').trim() : key;
|
|
|
|
|
|
|
|
// Extract extension from data URL (e.g., data:image/jpeg;base64,...)
|
|
|
|
// Compress photo for email
|
|
|
|
const mimeMatch = value.match(/data:image\/([a-z]+);/);
|
|
|
|
const promise = new Promise((resolve) => {
|
|
|
|
const ext = mimeMatch ? mimeMatch[1] : 'jpg';
|
|
|
|
compressImageForEmail(value, function(compressedData) {
|
|
|
|
photos.push({
|
|
|
|
resolve({
|
|
|
|
|
|
|
|
name: labelText,
|
|
|
|
name: labelText,
|
|
|
|
extension: 'jpg',
|
|
|
|
extension: ext,
|
|
|
|
data: compressedData
|
|
|
|
data: value
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
photoPromises.push(promise);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
const label = document.querySelector(`label[for="${key}"]`);
|
|
|
|
const label = document.querySelector(`label[for="${key}"]`);
|
|
|
|
const labelText = label ? label.textContent.replace('*', '').trim() : key;
|
|
|
|
const labelText = label ? label.textContent.replace('*', '').trim() : key;
|
|
|
|
@ -1410,25 +1104,25 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Wait for all photos to be compressed
|
|
|
|
|
|
|
|
const compressedPhotos = await Promise.all(photoPromises);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
body += '\n========================\n';
|
|
|
|
body += '\n========================\n';
|
|
|
|
|
|
|
|
|
|
|
|
// Add photos section only if there are photos
|
|
|
|
// Add photos section if there are any
|
|
|
|
if (compressedPhotos.length > 0) {
|
|
|
|
if (photos.length > 0) {
|
|
|
|
body += '\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\n';
|
|
|
|
body += '\nFOTO BIJLAGEN (BASE64)\n';
|
|
|
|
body += '==============================================\n';
|
|
|
|
body += '========================\n';
|
|
|
|
body += 'Foto\'s zijn verkleind naar 300x300px voor email.\n';
|
|
|
|
body += 'Onderstaande foto\'s zijn gecodeerd in base64 formaat.\n';
|
|
|
|
body += 'Voor originele kwaliteit, gebruik CSV export.\n\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';
|
|
|
|
|
|
|
|
|
|
|
|
compressedPhotos.forEach((photo, index) => {
|
|
|
|
photos.forEach((photo, index) => {
|
|
|
|
const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension;
|
|
|
|
const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension;
|
|
|
|
body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\n';
|
|
|
|
body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\n';
|
|
|
|
body += '>>> START BASE64 >>>\n';
|
|
|
|
body += '>>> START BASE64 >>>\n';
|
|
|
|
body += photo.data + '\n';
|
|
|
|
body += photo.data + '\n';
|
|
|
|
body += '<<< EINDE BASE64 <<<\n\n';
|
|
|
|
body += '<<< EINDE BASE64 <<<\n\n';
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
body += 'Geen foto\'s toegevoegd aan dit formulier.\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const mailto = 'mailto:' + encodeURIComponent(CONFIG.export.mailto.to) +
|
|
|
|
const mailto = 'mailto:' + encodeURIComponent(CONFIG.export.mailto.to) +
|
|
|
|
@ -1441,9 +1135,9 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
button.disabled = false;
|
|
|
|
button.disabled = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if mailto URL is too long
|
|
|
|
// Check if mailto URL is too long (most clients support ~2000 chars)
|
|
|
|
if (mailto.length > 64000) {
|
|
|
|
if (mailto.length > 100000) {
|
|
|
|
showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error');
|
|
|
|
showToast('Email te groot door foto. Gebruik CSV export.', 'error');
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1469,32 +1163,12 @@ input.invalid, select.invalid, textarea.invalid {
|
|
|
|
img.parentElement.classList.remove('has-photo');
|
|
|
|
img.parentElement.classList.remove('has-photo');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
|
|
history.replaceState(null, '', window.location.pathname);
|
|
|
|
history.replaceState(null, '', window.location.pathname);
|
|
|
|
currentFormId = null;
|
|
|
|
|
|
|
|
updateFormSelector();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showToast('Formulier gewist', 'success');
|
|
|
|
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
|
|
|
|
// Show toast notification
|
|
|
|
function showToast(message, type) {
|
|
|
|
function showToast(message, type) {
|
|
|
|
const toast = document.getElementById('toast');
|
|
|
|
const toast = document.getElementById('toast');
|
|
|
|
|