506 lines
20 KiB
HTML
506 lines
20 KiB
HTML
<!--
|
|
Copyright 2025-2026 AIIrondev
|
|
Licensed under the Inventarsystem EULA.
|
|
-->
|
|
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ page_title|default('Artikel bearbeiten') }} - Inventarsystem{% endblock %}
|
|
|
|
{% block content %}
|
|
<style>
|
|
.edit-container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background-color: var(--ui-surface);
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.edit-form h1 {
|
|
color: var(--ui-text);
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
font-size: 2em;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
color: var(--ui-text);
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select,
|
|
.form-group textarea {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 16px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.form-group textarea {
|
|
height: 100px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.filter-inputs {
|
|
background-color: var(--ui-surface-soft);
|
|
padding: 20px;
|
|
border-radius: 5px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.filter-inputs h3 {
|
|
color: var(--ui-text);
|
|
margin-bottom: 15px;
|
|
font-size: 1.2em;
|
|
}
|
|
|
|
.multi-filter {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
}
|
|
|
|
.filter-dropdown-select {
|
|
width: 100%;
|
|
padding: 8px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
background-color: white;
|
|
}
|
|
|
|
.isbn-input-group {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.isbn-input-group input {
|
|
flex: 1;
|
|
}
|
|
|
|
.fetch-isbn-button {
|
|
background-color: #007bff;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.fetch-isbn-button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
|
|
/* Scanner Elements */
|
|
#code4-scanner video, #code4-scanner canvas,
|
|
#isbn-scanner video, #isbn-scanner canvas {
|
|
width: 100%;
|
|
max-width: 640px;
|
|
height: auto;
|
|
border-radius: 5px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
#code4-scanner canvas, #isbn-scanner canvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
|
|
#code4-scanner, #isbn-scanner {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Image Management */
|
|
.existing-images-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.existing-image-card {
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
padding: 8px;
|
|
text-align: center;
|
|
background: #fff;
|
|
width: 120px;
|
|
}
|
|
|
|
.existing-image-card img {
|
|
width: 100%;
|
|
height: 100px;
|
|
object-fit: cover;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.existing-image-card label {
|
|
font-size: 0.8em;
|
|
margin-top: 5px;
|
|
cursor: pointer;
|
|
font-weight: normal;
|
|
}
|
|
|
|
.add-new-btn {
|
|
background-color: #007bff;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
margin-top: 5px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.submit-button {
|
|
background-color: #28a745;
|
|
color: white;
|
|
border: none;
|
|
padding: 15px 30px;
|
|
border-radius: 5px;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
margin-top: 20px;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.submit-button:hover {
|
|
background-color: #218838;
|
|
}
|
|
|
|
.edit-new-location-container {
|
|
display: none;
|
|
margin-top: 10px;
|
|
}
|
|
</style>
|
|
|
|
<div class="edit-container">
|
|
<div class="edit-form">
|
|
<h1>{{ page_title|default('Artikel bearbeiten') }}</h1>
|
|
<form method="POST" action="{{ url_for('item_edit', id=item._id) }}" enctype="multipart/form-data">
|
|
<input type="hidden" name="item_id" value="{{ item._id }}">
|
|
|
|
{% if show_library_features %}
|
|
<!-- ================= LIBRARY SPECIFIC FIELDS ================= -->
|
|
<div class="form-group">
|
|
<label for="isbn">ISBN / Barcode:</label>
|
|
<div class="isbn-input-group">
|
|
<input type="text" id="isbn" name="isbn" value="{{ item.ISBN|default('') }}" placeholder="ISBN oder Barcode eingeben...">
|
|
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
|
|
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('edit')">Informationen abrufen</button>
|
|
</div>
|
|
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
|
|
<small id="isbn-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
|
|
<div id="book-info-container"></div>
|
|
</div>
|
|
|
|
<div class="filter-inputs" style="margin-bottom: 20px;">
|
|
<h3>Medientyp</h3>
|
|
<div class="form-group">
|
|
<select name="item_type_input" id="item_type_input">
|
|
<option value="Buch" {% if item.ItemType == 'Buch' %}selected{% endif %}>Buch</option>
|
|
<option value="Schulbuch" {% if item.ItemType == 'Schulbuch' %}selected{% endif %}>Schulbuch</option>
|
|
<option value="CD" {% if item.ItemType == 'CD' %}selected{% endif %}>CD</option>
|
|
<option value="DVD" {% if item.ItemType == 'DVD' %}selected{% endif %}>DVD</option>
|
|
<option value="Sonstiges" {% if item.ItemType == 'Sonstiges' %}selected{% endif %}>Sonstiges</option>
|
|
</select>
|
|
</div>
|
|
<h3>Bibliotheks-Kategorie:</h3>
|
|
<div class="form-group">
|
|
<input type="text" name="library_category" id="library_category" value="{{ item.library_category|default('') }}" placeholder="z.B. Belletristik, Sachbücher, etc.">
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- ================= COMMON CORE FIELDS ================= -->
|
|
<div class="form-group">
|
|
<label for="name">Name / Titel:</label>
|
|
<input type="text" id="name" name="name" value="{{ item.Name|default('') }}" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="ort">Ort / Standort:</label>
|
|
<select id="ort" name="ort" data-selected="{{ item.Ort|default('') }}" required>
|
|
<option value="">-- Bitte Ort auswählen --</option>
|
|
{% if item.Ort %}
|
|
<option value="{{ item.Ort }}" selected>{{ item.Ort }}</option>
|
|
{% endif %}
|
|
</select>
|
|
<button type="button" class="add-new-btn" id="add-new-location-btn">Neuen Ort hinzufügen</button>
|
|
<div id="new-location-container" class="edit-new-location-container">
|
|
<input type="text" id="new-location-input" placeholder="Neuen Ort eingeben">
|
|
<button type="button" onclick="addNewLocation()">Hinzufügen</button>
|
|
<button type="button" onclick="cancelAddLocation()">Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="beschreibung">Beschreibung:</label>
|
|
<textarea id="beschreibung" name="beschreibung" required>{{ item.Beschreibung|default('') }}</textarea>
|
|
</div>
|
|
|
|
<div class="form-group" id="primary_code_group">
|
|
<label for="code_4">Basis-Code (Haupt-Barcode)</label>
|
|
<div style="display: flex; gap: 10px;">
|
|
<input type="text" id="code_4" name="code_4" class="form-control" value="{{ item.Code_4|default('') }}" required>
|
|
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
|
|
</div>
|
|
<div id="code4-scanner" style="display:none; margin-top: 10px;"></div>
|
|
<small id="code4-scan-status" class="form-text text-muted"></small>
|
|
</div>
|
|
|
|
<div class="form-group" id="individual_codes_group">
|
|
<label for="individual_codes">Weitere Einzelcodes der Gruppe (je Zeile ein Code)</label>
|
|
<textarea id="individual_codes" name="individual_codes" rows="4" class="form-control" placeholder="z.B. ABC-001 ABC-002">{{ item.IndividualCodes|default('') }}</textarea>
|
|
<small style="display:block; color:#666; margin-top: 5px;">Der Basis-Code steht oben. Alle weiteren Gruppenmitglieder werden hier untereinander aufgeführt.</small>
|
|
</div>
|
|
|
|
<!-- ================= SYSTEM FILTERS 1-3 (ALWAYS AVAILABLE) ================= -->
|
|
<div class="filter-inputs">
|
|
<h3>Unterrichtsfach (Filter 1):</h3>
|
|
<div class="multi-filter">
|
|
{% for idx in range(4) %}
|
|
<div class="form-group">
|
|
<label for="filter1-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
|
|
<select id="filter1-{{ idx + 1 }}" name="filter" class="filter-dropdown-select" data-selected="{{ item.Filter[idx] if item.Filter and item.Filter|length > idx else '' }}">
|
|
<option value="">-- Optional --</option>
|
|
</select>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<h3>Jahrgangsstufe (Filter 2):</h3>
|
|
<div class="multi-filter">
|
|
{% for idx in range(4) %}
|
|
<div class="form-group">
|
|
<label for="filter2-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
|
|
<select id="filter2-{{ idx + 1 }}" name="filter2" class="filter-dropdown-select" data-selected="{{ item.Filter2[idx] if item.Filter2 and item.Filter2|length > idx else '' }}">
|
|
<option value="">-- Optional --</option>
|
|
</select>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<h3>Schlagwort (Filter 3):</h3>
|
|
<div class="multi-filter">
|
|
{% for idx in range(4) %}
|
|
<div class="form-group">
|
|
<label for="filter3-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
|
|
<input type="text" id="filter3-{{ idx + 1 }}" name="filter3" value="{{ item.Filter3[idx] if item.Filter3 and item.Filter3|length > idx else '' }}">
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ================= DATES & FINANCIALS ================= -->
|
|
<div class="form-group">
|
|
<label for="anschaffungsjahr">Anschaffungsjahr:</label>
|
|
<input type="date" id="anschaffungsjahr" name="anschaffungsjahr" value="{{ item.Anschaffungsjahr|default('') }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="anschaffungskosten">Anschaffungskosten (€):</label>
|
|
<input type="text" id="anschaffungskosten" name="anschaffungskosten" value="{{ item.Anschaffungskosten|default('') }}">
|
|
</div>
|
|
|
|
{% if not show_library_features %}
|
|
<!-- ================= INVENTORY IMAGE MANAGEMENT ================= -->
|
|
<div class="form-group">
|
|
<label>Bestehende Bilder behalten:</label>
|
|
{% if item.Images and item.Images|length > 0 %}
|
|
<div class="existing-images-grid">
|
|
{% for img in item.Images %}
|
|
<div class="existing-image-card">
|
|
<img src="{{ url_for('uploaded_file', filename=img) }}" alt="Bild">
|
|
<label>
|
|
<input type="checkbox" name="existing_images" value="{{ img }}" checked>
|
|
Behalten
|
|
</label>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p style="color:#777; font-size:0.9em;">Keine Bilder vorhanden.</p>
|
|
{% endif %}
|
|
|
|
<label for="images" style="margin-top:15px;">Neue Bilder/Videos hinzufügen:</label>
|
|
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif, .mp4, .mov, .avi, .mkv, .webm" multiple>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="form-group">
|
|
<label for="reservierbar" style="display:inline-block; width:auto; margin-right:10px;">Reservierbar:</label>
|
|
<input type="checkbox" id="reservierbar" name="reservierbar" style="width:auto;" {% if item.Reservierbar %}checked{% endif %}>
|
|
</div>
|
|
|
|
<button type="submit" class="submit-button">Änderungen speichern</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
|
<script>
|
|
let scannerRunning = false;
|
|
let activeScannerCallback = null;
|
|
let code4LastScanned = '';
|
|
let code4LastScannedAt = 0;
|
|
|
|
// Load filter dropdown options and select current values
|
|
function loadAndSelectFilterValues(filterNumber) {
|
|
fetch(`/get_predefined_filter_values/${filterNumber}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
for (let i = 1; i <= 4; i++) {
|
|
const select = document.getElementById(`filter${filterNumber}-${i}`);
|
|
if (!select) continue;
|
|
|
|
const selectedValue = select.getAttribute('data-selected') || '';
|
|
|
|
data.values.forEach(val => {
|
|
if (!val || String(val).trim() === '') return;
|
|
const opt = document.createElement('option');
|
|
opt.value = val;
|
|
opt.textContent = val;
|
|
select.appendChild(opt);
|
|
});
|
|
|
|
if (selectedValue && !Array.from(select.options).some(o => o.value === selectedValue)) {
|
|
const customOpt = document.createElement('option');
|
|
customOpt.value = selectedValue;
|
|
customOpt.textContent = selectedValue;
|
|
select.appendChild(customOpt);
|
|
}
|
|
|
|
select.value = selectedValue;
|
|
}
|
|
})
|
|
.catch(err => console.error(`Error loading Filter ${filterNumber}:`, err));
|
|
}
|
|
|
|
function loadLocationOptions() {
|
|
fetch('/get_predefined_locations')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const select = document.getElementById('ort');
|
|
if (!select) return;
|
|
const currentVal = select.getAttribute('data-selected') || select.value;
|
|
|
|
data.locations.forEach(loc => {
|
|
if (!Array.from(select.options).some(o => o.value === loc)) {
|
|
const opt = document.createElement('option');
|
|
opt.value = loc;
|
|
opt.textContent = loc;
|
|
select.appendChild(opt);
|
|
}
|
|
});
|
|
|
|
select.value = currentVal;
|
|
})
|
|
.catch(err => console.error('Error loading locations:', err));
|
|
}
|
|
|
|
function addNewLocation() {
|
|
const input = document.getElementById('new-location-input');
|
|
const val = input.value.trim();
|
|
if (!val) return;
|
|
const select = document.getElementById('ort');
|
|
const opt = document.createElement('option');
|
|
opt.value = val;
|
|
opt.textContent = val;
|
|
opt.selected = true;
|
|
select.appendChild(opt);
|
|
document.getElementById('new-location-container').style.display = 'none';
|
|
input.value = '';
|
|
}
|
|
|
|
function cancelAddLocation() {
|
|
document.getElementById('new-location-container').style.display = 'none';
|
|
document.getElementById('new-location-input').value = '';
|
|
}
|
|
|
|
function runEngineInitialization(targetSelector, activeCallback, completionMsg, errorStatusSetter) {
|
|
if (scannerRunning) { Quagga.stop(); scannerRunning = false; }
|
|
activeScannerCallback = activeCallback;
|
|
|
|
Quagga.init({
|
|
inputStream: { name: "Live", type: "LiveStream", target: document.querySelector(targetSelector), constraints: { width: 640, height: 480, facingMode: "environment" } },
|
|
decoder: { readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"] }
|
|
}, function(err) {
|
|
if (err) { errorStatusSetter("Kamera-Fehler.", true); return; }
|
|
Quagga.start(); scannerRunning = true; errorStatusSetter(completionMsg, false);
|
|
});
|
|
}
|
|
|
|
function killScannerHardware() {
|
|
if (!scannerRunning) return;
|
|
Quagga.stop(); scannerRunning = false; activeScannerCallback = null;
|
|
}
|
|
|
|
Quagga.onDetected(function(data) {
|
|
if (!data || !data.codeResult || !data.codeResult.code) return;
|
|
if (typeof activeScannerCallback === "function") activeScannerCallback(String(data.codeResult.code).trim());
|
|
});
|
|
|
|
function startCode4Scanner() {
|
|
const scannerBox = document.getElementById('code4-scanner');
|
|
const scanBtn = document.getElementById('scan-code4-btn');
|
|
const baseField = document.getElementById('code_4');
|
|
const indArea = document.getElementById('individual_codes');
|
|
|
|
if (scannerBox.style.display !== 'none') {
|
|
killScannerHardware(); scannerBox.style.display = 'none'; scanBtn.textContent = 'Barcode scannen'; return;
|
|
}
|
|
|
|
scannerBox.style.display = 'block'; scanBtn.textContent = 'Scanner stoppen';
|
|
runEngineInitialization('#code4-scanner', function(decodedText) {
|
|
const now = Date.now();
|
|
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) return;
|
|
code4LastScanned = decodedText; code4LastScannedAt = now;
|
|
|
|
killScannerHardware(); scannerBox.style.display = 'none'; scanBtn.textContent = 'Barcode scannen';
|
|
|
|
if (!baseField.value.trim()) {
|
|
baseField.value = decodedText;
|
|
} else {
|
|
let codes = indArea.value.split('\n').map(c => c.trim()).filter(c => c);
|
|
if (!codes.includes(decodedText) && baseField.value.trim() !== decodedText) {
|
|
codes.push(decodedText);
|
|
indArea.value = codes.join('\n');
|
|
}
|
|
}
|
|
}, 'Scanner läuft...', msg => { document.getElementById('code4-scan-status').textContent = msg; });
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadLocationOptions();
|
|
|
|
// Always load Filter 1 and Filter 2 options for all items
|
|
loadAndSelectFilterValues(1);
|
|
loadAndSelectFilterValues(2);
|
|
|
|
const scanCodeBtn = document.getElementById('scan-code4-btn');
|
|
if (scanCodeBtn) scanCodeBtn.addEventListener('click', startCode4Scanner);
|
|
|
|
const addLocBtn = document.getElementById('add-new-location-btn');
|
|
if (addLocBtn) addLocBtn.addEventListener('click', () => {
|
|
const c = document.getElementById('new-location-container');
|
|
c.style.display = c.style.display === 'none' ? 'block' : 'none';
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |