You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1450 lines
46 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="EasySmartInventory v1.0.1">
<title>Machine_Inventarisatie</title>
<style>
:root {
--primary: #0066cc;
--secondary: #28a745;
--background: #f8f9fa;
--text: #333333;
--error: #dc3545;
--success: #28a745;
--border-radius: 12px;
--shadow: 0 4px 6px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 25px rgba(0,0,0,0.15);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, var(--background) 0%, #e9ecef 100%);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
overflow: hidden;
}
header {
background: linear-gradient(135deg, var(--primary) 0%, color-mix(in srgb, var(--primary), black 20%) 100%);
color: white;
padding: 30px;
text-align: center;
}
header h1 { font-size: 1.8em; margin-bottom: 5px; }
header .version { opacity: 0.8; font-size: 0.9em; }
.logo { max-height: 60px; margin-bottom: 15px; }
.section {
padding: 25px 30px;
border-bottom: 1px solid #e9ecef;
}
.section:last-of-type { border-bottom: none; }
.section h2 {
color: var(--primary);
font-size: 1.3em;
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 10px;
}
.section h2::before {
content: '';
width: 4px;
height: 24px;
background: var(--primary);
border-radius: 2px;
}
.section-desc {
color: #6c757d;
font-size: 0.9em;
margin-bottom: 20px;
}
.field-group {
margin-bottom: 20px;
}
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: var(--text);
}
label .required {
color: var(--error);
margin-left: 4px;
}
input[type="text"],
input[type="number"],
input[type="date"],
select,
textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 1em;
transition: all 0.2s ease;
background: white;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary), transparent 80%);
}
input.invalid, select.invalid, textarea.invalid {
border-color: var(--error);
}
.error-message {
color: var(--error);
font-size: 0.85em;
margin-top: 5px;
display: none;
}
.invalid + .error-message { display: block; }
.checkbox-group, .multiselect-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.checkbox-item, .multiselect-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #f8f9fa;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
}
.checkbox-item:hover, .multiselect-item:hover {
background: color-mix(in srgb, var(--primary), transparent 90%);
}
.checkbox-item input, .multiselect-item input {
width: 18px;
height: 18px;
accent-color: var(--primary);
}
.toggle-container {
display: flex;
align-items: center;
gap: 12px;
}
.toggle {
position: relative;
width: 50px;
height: 26px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: #ccc;
border-radius: 26px;
transition: 0.3s;
}
.toggle-slider::before {
content: '';
position: absolute;
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.3s;
}
.toggle input:checked + .toggle-slider {
background: var(--primary);
}
.toggle input:checked + .toggle-slider::before {
transform: translateX(24px);
}
.photo-container {
border: 2px dashed #ddd;
border-radius: 12px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: #fafafa;
}
.photo-container:hover {
border-color: var(--primary);
background: color-mix(in srgb, var(--primary), transparent 95%);
}
.photo-container.has-photo {
padding: 10px;
border-style: solid;
}
.photo-preview {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
}
.photo-buttons {
margin-top: 10px;
display: flex;
gap: 10px;
justify-content: center;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: color-mix(in srgb, var(--primary), black 15%);
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-success {
background: var(--success);
color: white;
}
.btn-success:hover {
background: color-mix(in srgb, var(--success), black 15%);
}
.btn-danger {
background: var(--error);
color: white;
}
.actions {
padding: 30px;
background: #f8f9fa;
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
}
.status-bar {
padding: 10px 30px;
background: #e9ecef;
font-size: 0.85em;
color: #6c757d;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.save-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.save-indicator.saving { color: var(--primary); }
.save-indicator.saved { color: var(--success); }
.toast {
position: fixed;
bottom: 20px;
right: 20px;
padding: 15px 25px;
background: var(--text);
color: white;
border-radius: 8px;
box-shadow: var(--shadow-lg);
transform: translateY(100px);
opacity: 0;
transition: all 0.3s ease;
z-index: 1000;
}
.toast.show {
transform: translateY(0);
opacity: 1;
}
.toast.success { background: var(--success); }
.toast.error { background: var(--error); }
@media (max-width: 768px) {
body { padding: 10px; }
.section { padding: 20px; }
header { padding: 20px; }
header h1 { font-size: 1.4em; }
.actions { flex-direction: column; }
.btn { width: 100%; justify-content: center; }
}
.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%;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Machine_Inventarisatie</h1>
<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>
<p class="section-desc">Algemene gegevens van het apparaat</p>
<div class="field-group">
<label for="serienummer">Serienummer<span class="required">*</span></label>
<input type="text" id="serienummer" data-required="true" data-min-length="3" placeholder="Voer serienummer in">
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="type_nummer">Type-/modelnummer</label>
<input type="text" id="type_nummer" placeholder="Bijv. HP ProBook 450 G8">
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="merk">Merk / fabrikant<span class="required">*</span></label>
<select id="merk" data-required="true">
<option value="">-- Selecteer --</option>
<option value="HP">HP</option>
<option value="Dell">Dell</option>
<option value="Lenovo">Lenovo</option>
<option value="Apple">Apple</option>
<option value="ASUS">ASUS</option>
<option value="Acer">Acer</option>
<option value="Microsoft">Microsoft</option>
<option value="Samsung">Samsung</option>
<option value="Anders">Anders</option>
</select>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="soort_apparaat">Soort apparaat / categorie<span class="required">*</span></label>
<select id="soort_apparaat" data-required="true">
<option value="">-- Selecteer --</option>
<option value="Laptop">Laptop</option>
<option value="Desktop">Desktop</option>
<option value="Server">Server</option>
<option value="Printer">Printer</option>
<option value="Scanner">Scanner</option>
<option value="Monitor">Monitor</option>
<option value="Netwerkapparatuur">Netwerkapparatuur</option>
<option value="Mobiel apparaat">Mobiel apparaat</option>
<option value="Anders">Anders</option>
</select>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="productienaam">Productienaam of -omschrijving</label>
<input type="text" id="productienaam" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="productiejaar">Productiejaar</label>
<input type="number" id="productiejaar" min="1990" max="2030" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="aanschafdatum">Aanschafdatum</label>
<input type="date" id="aanschafdatum" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="aankoopprijs">Aankoopprijs of vervangingswaarde (€)</label>
<input type="number" id="aankoopprijs" min="0" placeholder="0.00">
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="asset_id">Intern identificatienummer (asset-ID)</label>
<input type="text" id="asset_id" placeholder="Bijv. AST-2024-001">
<div class="error-message"></div>
</div>
</section>
<section class="section">
<h2>Locatie &amp; Toewijzing</h2>
<p class="section-desc">Waar bevindt het apparaat zich en wie is verantwoordelijk</p>
<div class="field-group">
<label for="locatie_adres">Locatienummer of adres</label>
<input type="text" id="locatie_adres" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="ruimte">Ruimte / afdeling / verdieping</label>
<input type="text" id="ruimte" placeholder="Bijv. Kantoor 2.15, IT-afdeling">
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="gebruiker">Gebruiker / verantwoordelijke persoon</label>
<input type="text" id="gebruiker" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="vestiging">Vestiging / filiaal</label>
<select id="vestiging" >
<option value="">-- Selecteer --</option>
<option value="Hoofdkantoor">Hoofdkantoor</option>
<option value="Vestiging Noord">Vestiging Noord</option>
<option value="Vestiging Zuid">Vestiging Zuid</option>
<option value="Vestiging Oost">Vestiging Oost</option>
<option value="Vestiging West">Vestiging West</option>
<option value="Thuiswerker">Thuiswerker</option>
<option value="Anders">Anders</option>
</select>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="mobiel_gebruik">Mobiel gebruik (werkplek/voertuig)</label>
<div class="toggle-container">
<label class="toggle">
<input type="checkbox" id="mobiel_gebruik" class="boolean-field">
<span class="toggle-slider"></span>
</label>
<span>Ja</span>
</div>
</div>
</section>
<section class="section">
<h2>Technische gegevens</h2>
<p class="section-desc">Technische specificaties en netwerkinformatie</p>
<div class="field-group">
<label for="specificaties">Specificaties (vermogen, capaciteit, etc.)</label>
<textarea id="specificaties" rows="3" placeholder="Bijv. Intel i7, 16GB RAM, 512GB SSD"></textarea>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="component_serienummers">Serienummers van componenten</label>
<textarea id="component_serienummers" rows="2" ></textarea>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="software_versie">Softwareversie / firmwareversie</label>
<input type="text" id="software_versie" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="ip_adres">IP-adres</label>
<input type="text" id="ip_adres" placeholder="Bijv. 192.168.1.100">
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="mac_adres">MAC-adres</label>
<input type="text" id="mac_adres" placeholder="Bijv. AA:BB:CC:DD:EE:FF">
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="energieklasse">Energieklasse</label>
<select id="energieklasse" >
<option value="">-- Selecteer --</option>
<option value="A+++">A+++</option>
<option value="A++">A++</option>
<option value="A+">A+</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<option value="Onbekend">Onbekend</option>
</select>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="certificeringen">Certificeringen</label>
<div class="multiselect-group" data-id="certificeringen">
<label class="multiselect-item">
<input type="checkbox" name="certificeringen" value="CE">
<span>CE</span>
</label>
<label class="multiselect-item">
<input type="checkbox" name="certificeringen" value="ISO 9001">
<span>ISO 9001</span>
</label>
<label class="multiselect-item">
<input type="checkbox" name="certificeringen" value="ISO 27001">
<span>ISO 27001</span>
</label>
<label class="multiselect-item">
<input type="checkbox" name="certificeringen" value="Energy Star">
<span>Energy Star</span>
</label>
<label class="multiselect-item">
<input type="checkbox" name="certificeringen" value="RoHS">
<span>RoHS</span>
</label>
<label class="multiselect-item">
<input type="checkbox" name="certificeringen" value="GDPR Compliant">
<span>GDPR Compliant</span>
</label>
</div>
</div>
</section>
<section class="section">
<h2>Status en conditie</h2>
<p class="section-desc">Huidige status en onderhoudsinformatie</p>
<div class="field-group">
<label for="status">Huidige status<span class="required">*</span></label>
<select id="status" data-required="true">
<option value="">-- Selecteer --</option>
<option value="In gebruik">In gebruik</option>
<option value="In opslag">In opslag</option>
<option value="Buiten gebruik">Buiten gebruik</option>
<option value="Defect">Defect</option>
<option value="In reparatie">In reparatie</option>
<option value="Verhuurd">Verhuurd</option>
<option value="Gereserveerd">Gereserveerd</option>
</select>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="conditie">Conditie / onderhoudstoestand</label>
<select id="conditie" >
<option value="">-- Selecteer --</option>
<option value="Uitstekend">Uitstekend</option>
<option value="Goed">Goed</option>
<option value="Redelijk">Redelijk</option>
<option value="Matig">Matig</option>
<option value="Slecht">Slecht</option>
</select>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="laatste_onderhoud">Laatste onderhoudsdatum</label>
<input type="date" id="laatste_onderhoud" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="volgend_onderhoud">Volgende geplande onderhoudsdatum</label>
<input type="date" id="volgend_onderhoud" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="garantie_tot">Garantie tot</label>
<input type="date" id="garantie_tot" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="leverancier">Leverancier / onderhoudspartner</label>
<input type="text" id="leverancier" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="onderhoudscontract">Onderhoudscontractnummer</label>
<input type="text" id="onderhoudscontract" >
<div class="error-message"></div>
</div>
</section>
<section class="section">
<h2>Documentatie &amp; beheer</h2>
<p class="section-desc">Administratieve gegevens en documentatie</p>
<div class="field-group">
<label for="factuurnummer">Factuurnummer / aankoopreferentie</label>
<input type="text" id="factuurnummer" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="leveringsdatum">Leveringsdatum</label>
<input type="date" id="leveringsdatum" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="handleiding_link">Handleidingen / documenten (link)</label>
<input type="text" id="handleiding_link" placeholder="URL naar documentatie">
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="foto">Foto van het apparaat</label>
<div class="photo-container" data-max-width="1200" data-max-height="1200">
<input type="file" accept="image/*" capture="environment" style="display:none">
<img id="foto" class="photo-preview" src="" alt="Foto preview" style="display:none">
<div class="photo-placeholder">📷 Klik om foto te maken of uploaden</div>
<div class="photo-buttons" style="display:none">
<button type="button" class="btn btn-secondary" onclick="removePhoto('foto')">Verwijderen</button>
</div>
</div>
</div>
<div class="field-group">
<label for="opmerkingen">Opmerkingen / bijzonderheden</label>
<textarea id="opmerkingen" rows="4" placeholder="Modificaties, risico&#x27;s, speciale aandachtspunten..."></textarea>
<div class="error-message"></div>
</div>
</section>
<section class="section">
<h2>Afvoer / einde levensduur</h2>
<p class="section-desc">Informatie over afvoer en recycling</p>
<div class="field-group">
<label for="datum_buiten_gebruik">Datum buiten gebruikstelling</label>
<input type="date" id="datum_buiten_gebruik" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="reden_afvoer">Reden van afvoer</label>
<select id="reden_afvoer" >
<option value="">-- Selecteer --</option>
<option value="Einde levensduur">Einde levensduur</option>
<option value="Defect (niet repareerbaar)">Defect (niet repareerbaar)</option>
<option value="Verouderd">Verouderd</option>
<option value="Vervangen">Vervangen</option>
<option value="Verkocht">Verkocht</option>
<option value="Gestolen">Gestolen</option>
<option value="Verloren">Verloren</option>
<option value="Anders">Anders</option>
</select>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="afvoermethode">Verwijderlocatie of afvoermethode</label>
<input type="text" id="afvoermethode" >
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="hergebruik_info">Hergebruik / donatie / recycling informatie</label>
<textarea id="hergebruik_info" rows="2" ></textarea>
<div class="error-message"></div>
</div>
</section>
<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-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.1</span>
</div>
</div>
<div id="toast" class="toast"></div>
<script>
(function() {
'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 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();
setupPhotoHandlers();
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() {
document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
field.value = '';
field.classList.remove('invalid');
});
document.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
document.querySelectorAll('.photo-preview').forEach(img => {
img.src = '';
img.style.display = 'none';
img.parentElement.classList.remove('has-photo');
});
history.replaceState(null, '', window.location.pathname);
}
// Load saved data from localStorage or URL
function loadSavedData() {
// Try URL hash first
if (CONFIG.autosave.use_url_hash && window.location.hash) {
try {
const hashData = decodeURIComponent(window.location.hash.substring(1));
const params = new URLSearchParams(hashData);
params.forEach((value, key) => {
setFieldValue(key, value);
});
currentFormId = getFormUniqueId();
return;
} catch (e) {
console.warn('Could not parse URL hash:', e);
}
}
// Try to load most recent form from localStorage
if (CONFIG.autosave.use_localstorage) {
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);
}
}
}
// Set field value by ID
function setFieldValue(id, value) {
const field = document.getElementById(id);
if (!field) return;
if (field.type === 'checkbox') {
field.checked = value === 'true' || value === true;
} else if (field.classList.contains('multiselect-field')) {
// Multiselect checkboxes
const values = Array.isArray(value) ? value : value.split(',');
values.forEach(v => {
const cb = document.querySelector(`input[name="${id}"][value="${v}"]`);
if (cb) cb.checked = true;
});
} else if (field.tagName === 'IMG') {
// Photo field
if (value) {
field.src = value;
field.parentElement.classList.add('has-photo');
}
} else {
field.value = value;
}
}
// Get all form data
function getFormData() {
const data = {};
// Text, number, date, select, textarea
document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
if (field.id) {
data[field.id] = field.value;
}
});
// Checkboxes (boolean)
document.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
if (field.id) {
data[field.id] = field.checked;
}
});
// Multiselect
document.querySelectorAll('.multiselect-group').forEach(group => {
const id = group.dataset.id;
const checked = Array.from(group.querySelectorAll('input:checked')).map(cb => cb.value);
data[id] = checked;
});
// Photo
document.querySelectorAll('.photo-preview').forEach(img => {
const id = img.id;
if (img.src && !img.src.includes('data:image/svg')) {
data[id] = img.src;
}
});
return data;
}
// Save data
function saveData() {
const data = getFormData();
const formId = getFormUniqueId();
// 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(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);
}
}
// Update URL hash
if (CONFIG.autosave.use_url_hash) {
const params = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
if (value && value !== '' && !(Array.isArray(value) && value.length === 0)) {
params.set(key, Array.isArray(value) ? value.join(',') : value);
}
});
const hash = params.toString();
if (hash) {
history.replaceState(null, '', '#' + hash);
}
}
hasChanges = false;
updateSaveIndicator('saved');
}
// Setup auto-save
function setupAutoSave() {
if (!CONFIG.autosave.enabled) return;
const interval = CONFIG.autosave.interval_seconds * 1000;
document.addEventListener('input', function() {
hasChanges = true;
updateSaveIndicator('saving');
clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveData, interval);
});
document.addEventListener('change', function() {
hasChanges = true;
updateSaveIndicator('saving');
clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveData, interval);
});
}
// Update save indicator
function updateSaveIndicator(status) {
const indicator = document.getElementById('save-indicator');
if (!indicator) return;
indicator.className = 'save-indicator ' + status;
const statusText = {
'loaded': 'Gegevens geladen',
'saving': 'Opslaan...',
'saved': 'Opgeslagen ✓',
'waiting': 'Vul ID-velden in om op te slaan'
};
indicator.textContent = statusText[status] || status;
}
// Setup validation
function setupValidation() {
document.querySelectorAll('[data-required="true"]').forEach(field => {
field.addEventListener('blur', function() {
validateField(this);
});
});
document.querySelectorAll('[data-min-length]').forEach(field => {
field.addEventListener('blur', function() {
validateField(this);
});
});
}
// Validate single field
function validateField(field) {
let valid = true;
let message = '';
const value = field.value.trim();
if (field.dataset.required === 'true' && !value) {
valid = false;
message = 'Dit veld is verplicht';
}
if (valid && field.dataset.minLength && value.length < parseInt(field.dataset.minLength)) {
valid = false;
message = `Minimaal ${field.dataset.minLength} karakters vereist`;
}
if (valid && field.type === 'number') {
const num = parseFloat(value);
if (field.min && num < parseFloat(field.min)) {
valid = false;
message = `Minimale waarde is ${field.min}`;
}
if (field.max && num > parseFloat(field.max)) {
valid = false;
message = `Maximale waarde is ${field.max}`;
}
}
field.classList.toggle('invalid', !valid);
const errorEl = field.nextElementSibling;
if (errorEl && errorEl.classList.contains('error-message')) {
errorEl.textContent = message;
}
return valid;
}
// Validate all fields
function validateAll() {
let valid = true;
document.querySelectorAll('[data-required="true"], [data-min-length]').forEach(field => {
if (!validateField(field)) {
valid = false;
}
});
return valid;
}
// Setup photo handlers
function setupPhotoHandlers() {
document.querySelectorAll('.photo-container').forEach(container => {
const input = container.querySelector('input[type="file"]');
const preview = container.querySelector('.photo-preview');
const fieldId = preview.id;
const maxWidth = parseInt(container.dataset.maxWidth) || 1200;
const maxHeight = parseInt(container.dataset.maxHeight) || 1200;
container.addEventListener('click', function(e) {
if (e.target.tagName !== 'BUTTON') {
input.click();
}
});
input.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
processImage(file, maxWidth, maxHeight, function(dataUrl) {
preview.src = dataUrl;
container.classList.add('has-photo');
hasChanges = true;
saveData();
});
});
});
}
// Process and resize image
function processImage(file, maxWidth, maxHeight, callback) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
let width = img.width;
let height = img.height;
if (width > maxWidth) {
height = height * (maxWidth / width);
width = maxWidth;
}
if (height > maxHeight) {
width = width * (maxHeight / height);
height = maxHeight;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
callback(canvas.toDataURL('image/jpeg', 0.8));
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// Remove photo
window.removePhoto = function(fieldId) {
const preview = document.getElementById(fieldId);
const container = preview.parentElement;
preview.src = '';
container.classList.remove('has-photo');
hasChanges = true;
saveData();
};
// Export to CSV
window.exportCSV = function() {
if (!validateAll()) {
showToast('Vul eerst alle verplichte velden in', 'error');
return;
}
const data = getFormData();
const headers = Object.keys(data);
const values = Object.values(data).map(v => {
if (Array.isArray(v)) return v.join('; ');
if (typeof v === 'string' && (v.includes(',') || v.includes('"') || v.includes('\n'))) {
return '"' + v.replace(/"/g, '""') + '"';
}
return v;
});
// UTF-8 BOM for Excel
const BOM = '\uFEFF';
const csv = BOM + headers.join(',') + '\n' + values.join(',');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = CONFIG.name + '_' + formatDate(new Date()) + '.csv';
a.click();
URL.revokeObjectURL(url);
showToast('CSV geëxporteerd!', 'success');
};
// Send email
window.sendEmail = function(btn) {
if (!validateAll()) {
showToast('Vul eerst alle verplichte velden in', 'error');
return;
}
// Show loading state immediately
const button = btn || document.querySelector('[onclick*="sendEmail"]');
const originalText = button ? button.innerHTML : '';
if (button) {
button.innerHTML = '⏳ Email voorbereiden...';
button.disabled = true;
}
// Use setTimeout to allow UI to update before heavy processing
setTimeout(function() {
const data = getFormData();
// Build subject
let subject = CONFIG.export.mailto.subject_prefix;
if (CONFIG.export.mailto.include_timestamp) {
subject += ' - ' + formatDateTime(new Date());
}
CONFIG.export.mailto.subject_fields.forEach(fieldId => {
if (data[fieldId]) {
subject += ' - ' + data[fieldId];
}
});
// Build body
let body = 'INVENTARISATIE GEGEVENS\n';
body += '========================\n\n';
body += 'Formulier: ' + CONFIG.name + '\n';
body += 'Versie: ' + CONFIG.version + '\n';
body += 'Datum: ' + formatDateTime(new Date()) + '\n\n';
// Collect photos separately
const photos = [];
Object.entries(data).forEach(([key, value]) => {
if (value && value !== '') {
// Check if this is a photo (base64 image)
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({
name: labelText,
extension: ext,
data: value
});
} else {
const label = document.querySelector(`label[for="${key}"]`);
const labelText = label ? label.textContent.replace('*', '').trim() : key;
const displayValue = Array.isArray(value) ? value.join(', ') : value;
body += labelText + ': ' + displayValue + '\n';
}
}
});
body += '\n========================\n';
// 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';
photos.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) +
'?subject=' + encodeURIComponent(subject) +
'&body=' + encodeURIComponent(body);
// Restore button state
if (button) {
button.innerHTML = originalText;
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');
return;
}
window.location.href = mailto;
}, 50);
};
// Clear form
window.clearForm = function() {
if (!confirm('Weet u zeker dat u alle gegevens wilt wissen?')) return;
document.querySelectorAll('input[type="text"], input[type="number"], input[type="date"], select, textarea').forEach(field => {
field.value = '';
field.classList.remove('invalid');
});
document.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
document.querySelectorAll('.photo-preview').forEach(img => {
img.src = '';
img.parentElement.classList.remove('has-photo');
});
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');
toast.textContent = message;
toast.className = 'toast ' + type + ' show';
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// Format date
function formatDate(date) {
return date.toISOString().split('T')[0];
}
// Format datetime
function formatDateTime(date) {
return date.toLocaleDateString('nl-NL') + ' ' +
date.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
})();
</script>
</body>
</html>

Powered by TurnKey Linux.