|
|
<!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: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
background: var(--background);
|
|
|
color: var(--text);
|
|
|
line-height: 1.5;
|
|
|
min-height: 100vh;
|
|
|
}
|
|
|
|
|
|
.container {
|
|
|
max-width: 800px;
|
|
|
margin: 0 auto;
|
|
|
background: white;
|
|
|
border: 1px solid #ddd;
|
|
|
}
|
|
|
|
|
|
header {
|
|
|
background: var(--primary);
|
|
|
color: white;
|
|
|
padding: 20px 30px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
|
|
|
header h1 { font-size: 1.5em; font-weight: 500; }
|
|
|
header .version { font-size: 0.85em; opacity: 0.9; }
|
|
|
|
|
|
.logo { max-height: 50px; }
|
|
|
|
|
|
.section {
|
|
|
padding: 25px 30px;
|
|
|
border-bottom: 1px solid #eee;
|
|
|
}
|
|
|
|
|
|
.section h2 {
|
|
|
color: var(--primary);
|
|
|
font-size: 1.1em;
|
|
|
font-weight: 600;
|
|
|
text-transform: uppercase;
|
|
|
letter-spacing: 0.5px;
|
|
|
margin-bottom: 5px;
|
|
|
padding-bottom: 10px;
|
|
|
border-bottom: 2px solid var(--primary);
|
|
|
}
|
|
|
|
|
|
.section-desc {
|
|
|
color: #666;
|
|
|
font-size: 0.9em;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
.field-group {
|
|
|
margin-bottom: 18px;
|
|
|
}
|
|
|
|
|
|
label {
|
|
|
display: block;
|
|
|
font-weight: 500;
|
|
|
margin-bottom: 6px;
|
|
|
font-size: 0.95em;
|
|
|
}
|
|
|
|
|
|
label .required {
|
|
|
color: var(--error);
|
|
|
}
|
|
|
|
|
|
input[type="text"],
|
|
|
input[type="number"],
|
|
|
input[type="date"],
|
|
|
select,
|
|
|
textarea {
|
|
|
width: 100%;
|
|
|
padding: 10px 12px;
|
|
|
border: 1px solid #ccc;
|
|
|
font-size: 0.95em;
|
|
|
background: white;
|
|
|
}
|
|
|
|
|
|
input:focus, select:focus, textarea:focus {
|
|
|
outline: none;
|
|
|
border-color: var(--primary);
|
|
|
}
|
|
|
|
|
|
input.invalid, select.invalid, textarea.invalid {
|
|
|
border-color: var(--error);
|
|
|
background: #fff5f5;
|
|
|
}
|
|
|
|
|
|
.error-message {
|
|
|
color: var(--error);
|
|
|
font-size: 0.8em;
|
|
|
margin-top: 4px;
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.invalid + .error-message { display: block; }
|
|
|
|
|
|
.checkbox-group, .multiselect-group {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 15px;
|
|
|
}
|
|
|
|
|
|
.checkbox-item, .multiselect-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
|
}
|
|
|
|
|
|
.checkbox-item input, .multiselect-item input {
|
|
|
width: 16px;
|
|
|
height: 16px;
|
|
|
}
|
|
|
|
|
|
.toggle-container {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.toggle {
|
|
|
position: relative;
|
|
|
width: 44px;
|
|
|
height: 22px;
|
|
|
}
|
|
|
|
|
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
|
|
|
|
.toggle-slider {
|
|
|
position: absolute;
|
|
|
cursor: pointer;
|
|
|
inset: 0;
|
|
|
background: #ccc;
|
|
|
transition: 0.3s;
|
|
|
}
|
|
|
|
|
|
.toggle-slider::before {
|
|
|
content: '';
|
|
|
position: absolute;
|
|
|
height: 16px;
|
|
|
width: 16px;
|
|
|
left: 3px;
|
|
|
bottom: 3px;
|
|
|
background: white;
|
|
|
transition: 0.3s;
|
|
|
}
|
|
|
|
|
|
.toggle input:checked + .toggle-slider {
|
|
|
background: var(--primary);
|
|
|
}
|
|
|
|
|
|
.toggle input:checked + .toggle-slider::before {
|
|
|
transform: translateX(22px);
|
|
|
}
|
|
|
|
|
|
.photo-container {
|
|
|
border: 1px dashed #999;
|
|
|
padding: 25px;
|
|
|
text-align: center;
|
|
|
cursor: pointer;
|
|
|
background: #fafafa;
|
|
|
}
|
|
|
|
|
|
.photo-container:hover {
|
|
|
border-color: var(--primary);
|
|
|
}
|
|
|
|
|
|
.photo-container.has-photo {
|
|
|
padding: 10px;
|
|
|
border-style: solid;
|
|
|
}
|
|
|
|
|
|
.photo-preview {
|
|
|
max-width: 100%;
|
|
|
max-height: 250px;
|
|
|
}
|
|
|
|
|
|
.photo-buttons {
|
|
|
margin-top: 10px;
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.btn {
|
|
|
padding: 10px 20px;
|
|
|
border: none;
|
|
|
font-size: 0.95em;
|
|
|
font-weight: 500;
|
|
|
cursor: pointer;
|
|
|
text-transform: uppercase;
|
|
|
letter-spacing: 0.5px;
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
background: var(--primary);
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
.btn-primary:hover {
|
|
|
background: color-mix(in srgb, var(--primary), black 10%);
|
|
|
}
|
|
|
|
|
|
.btn-secondary {
|
|
|
background: #666;
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
.btn-success {
|
|
|
background: var(--success);
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
.btn-danger {
|
|
|
background: var(--error);
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
.actions {
|
|
|
padding: 25px 30px;
|
|
|
background: #f5f5f5;
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 12px;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
.status-bar {
|
|
|
padding: 10px 30px;
|
|
|
background: #eee;
|
|
|
font-size: 0.8em;
|
|
|
color: #666;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
|
|
|
.save-indicator {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
|
}
|
|
|
|
|
|
.toast {
|
|
|
position: fixed;
|
|
|
bottom: 20px;
|
|
|
right: 20px;
|
|
|
padding: 12px 20px;
|
|
|
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) {
|
|
|
.section { padding: 15px 20px; }
|
|
|
header { padding: 15px 20px; flex-direction: column; gap: 10px; }
|
|
|
.actions { 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 & 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 & 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'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> |