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.

1492 lines
48 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;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
background: white;
color: var(--text);
line-height: 1.7;
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 700px;
margin: 0 auto;
}
header {
margin-bottom: 50px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
header h1 {
font-size: 2em;
font-weight: 300;
color: var(--text);
}
header .version {
color: #999;
font-size: 0.85em;
margin-top: 5px;
}
.logo { max-height: 40px; margin-bottom: 20px; }
.section {
margin-bottom: 50px;
}
.section h2 {
font-size: 0.9em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
color: #999;
margin-bottom: 25px;
}
.section-desc {
color: #666;
font-size: 0.9em;
margin-top: -20px;
margin-bottom: 25px;
}
.field-group {
margin-bottom: 25px;
}
label {
display: block;
font-size: 0.9em;
margin-bottom: 8px;
color: #666;
}
label .required {
color: var(--error);
}
input[type="text"],
input[type="number"],
input[type="date"],
select,
textarea {
width: 100%;
padding: 12px 0;
border: none;
border-bottom: 1px solid #ddd;
font-size: 1em;
background: transparent;
transition: border-color 0.2s;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-bottom-color: var(--primary);
}
input.invalid, select.invalid, textarea.invalid {
border-bottom-color: var(--error);
}
.error-message {
color: var(--error);
font-size: 0.8em;
margin-top: 5px;
display: none;
}
.invalid + .error-message { display: block; }
select {
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0 center;
padding-right: 20px;
}
.checkbox-group, .multiselect-group {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.checkbox-item, .multiselect-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95em;
}
.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: 40px;
height: 20px;
}
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: #ddd;
border-radius: 20px;
transition: 0.3s;
}
.toggle-slider::before {
content: '';
position: absolute;
height: 14px;
width: 14px;
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(20px);
}
.photo-container {
border: 1px solid #eee;
padding: 40px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.photo-container:hover {
border-color: var(--primary);
}
.photo-container.has-photo {
padding: 15px;
}
.photo-preview {
max-width: 100%;
max-height: 250px;
}
.photo-buttons {
margin-top: 15px;
display: flex;
gap: 10px;
justify-content: center;
}
.btn {
padding: 12px 30px;
border: 1px solid;
font-size: 0.9em;
cursor: pointer;
background: transparent;
transition: all 0.2s;
}
.btn-primary {
border-color: var(--primary);
color: var(--primary);
}
.btn-primary:hover {
background: var(--primary);
color: white;
}
.btn-secondary {
border-color: #999;
color: #999;
}
.btn-secondary:hover {
background: #999;
color: white;
}
.btn-success {
border-color: var(--success);
color: var(--success);
}
.btn-success:hover {
background: var(--success);
color: white;
}
.btn-danger {
border-color: var(--error);
color: var(--error);
}
.btn-danger:hover {
background: var(--error);
color: white;
}
.actions {
margin-top: 50px;
padding-top: 30px;
border-top: 1px solid #eee;
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.status-bar {
margin-top: 30px;
font-size: 0.8em;
color: #999;
display: flex;
justify-content: space-between;
}
.save-indicator {
display: flex;
align-items: center;
gap: 6px;
}
.toast {
position: fixed;
bottom: 30px;
right: 30px;
padding: 15px 25px;
background: var(--text);
color: white;
transform: translateY(100px);
opacity: 0;
transition: all 0.3s;
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: 20px 15px; }
header h1 { font-size: 1.5em; }
.section { margin-bottom: 35px; }
.actions { flex-direction: column; }
.btn { width: 100%; text-align: 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() {
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
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 = {};
const form = document.getElementById('inventory-form');
if (!form) return data;
// 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)
form.querySelectorAll('input[type="checkbox"].boolean-field').forEach(field => {
if (field.id) {
data[field.id] = field.checked;
}
});
// Multiselect
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
form.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 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) {
// 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();
});
});
}
});
}
// 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);
}
// 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);
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(async 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 and compress them
const photos = [];
const photoPromises = [];
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;
// Compress photo for email
const promise = new Promise((resolve) => {
compressImageForEmail(value, function(compressedData) {
resolve({
name: labelText,
extension: 'jpg',
data: compressedData
});
});
});
photoPromises.push(promise);
} 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';
}
}
});
// Wait for all photos to be compressed
const compressedPhotos = await Promise.all(photoPromises);
body += '\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';
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';
});
}
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
if (mailto.length > 64000) {
showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', '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.