Files
Inventarsystem/Web/templates/upload_admin.html
T

1974 lines
76 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
Copyright 2025-2026 AIIrondev
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
See Legal/LICENSE for the full license text.
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev
-->
{% extends "base.html" %}
{% block title %}{{ page_title|default('Artikel hochladen') }} - Inventarsystem{% endblock %}
{% block content %}
{% if duplicate_data and duplicate_data.images %}
<!-- Hidden server-side values for duplication, will be processed by JavaScript -->
<div id="server-duplicate-data"
data-name="{{ duplicate_data.name|default('') }}"
data-description="{{ duplicate_data.description|default('') }}"
data-location="{{ duplicate_data.location|default('') }}"
data-room="{{ duplicate_data.room|default('') }}"
data-year="{{ duplicate_data.year|default('') }}"
data-cost="{{ duplicate_data.cost|default('') }}"
data-images="{{ duplicate_data.images|tojson|safe }}"
data-filter1="{{ duplicate_data.filter1|tojson|safe if duplicate_data.filter1 else '[]' }}"
data-filter2="{{ duplicate_data.filter2|tojson|safe if duplicate_data.filter2 else '[]' }}"
data-filter3="{{ duplicate_data.filter3|tojson|safe if duplicate_data.filter3 else '[]' }}"
data-original-id="{{ duplicate_data.original_id|default('') }}"
style="display:none;">
</div>
<script>
// Pre-initialize duplicate data from server
var serverDuplicateData = null;
const dataElement = document.getElementById('server-duplicate-data');
if (dataElement) {
// Safely parse JSON arrays with error handling
function safeJsonParse(jsonStr, defaultValue = []) {
try {
if (!jsonStr) return defaultValue;
return JSON.parse(jsonStr);
} catch (e) {
console.error("Error parsing JSON:", e, "Value was:", jsonStr);
return defaultValue;
}
}
// Get images data and log it for debugging
const imagesData = dataElement.getAttribute('data-images') || "[]";
console.log("Raw images data attribute:", imagesData);
serverDuplicateData = {
name: dataElement.getAttribute('data-name') || "",
description: dataElement.getAttribute('data-description') || "",
location: dataElement.getAttribute('data-location') || "",
room: dataElement.getAttribute('data-room') || "",
year: dataElement.getAttribute('data-year') || "",
cost: dataElement.getAttribute('data-cost') || "",
images: safeJsonParse(imagesData),
filter1: safeJsonParse(dataElement.getAttribute('data-filter1')),
filter2: safeJsonParse(dataElement.getAttribute('data-filter2')),
filter3: safeJsonParse(dataElement.getAttribute('data-filter3')),
original_id: dataElement.getAttribute('data-original-id') || ""
};
console.log("Server-provided duplicate data:", serverDuplicateData);
}
</script>
{% endif %}
<style>
/* Book information display styles */
.book-info-container {
margin-top: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
min-height: 40px;
}
.book-info {
padding: 15px;
background-color: var(--ui-surface);
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 10px;
}
.book-info h4 {
color: var(--ui-text);
margin: 0 0 10px 0;
font-size: 1.2em;
font-weight: bold;
}
.book-info p {
margin: 5px 0;
color: #666;
line-height: 1.4;
}
.book-thumbnail {
max-width: 120px;
max-height: 180px;
float: right;
margin: 0 0 10px 15px;
border: 1px solid #ddd;
border-radius: 3px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.book-description {
clear: both;
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.book-description h5 {
color: var(--ui-text);
margin: 0 0 8px 0;
font-size: 1em;
font-weight: bold;
}
.book-description p {
max-height: 150px;
overflow-y: auto;
padding: 5px;
background-color: var(--ui-surface-soft);
border: 1px solid #e9ecef;
border-radius: 3px;
font-size: 0.9em;
line-height: 1.5;
}
/* Import button styling */
.import-book-button {
background-color: #28a745;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
margin-top: 15px;
transition: background-color 0.2s ease;
display: block;
width: 100%;
}
.import-book-button:hover {
background-color: #218838;
}
.import-book-button:active {
background-color: #1e7e34;
}
/* Quagga Scanner Video Styles */
#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;
}
/* ISBN input group styling */
.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;
}
/* Loading spinner */
.loading-spinner {
color: #007bff;
font-style: italic;
text-align: center;
padding: 20px;
background-color: var(--ui-surface-soft);
border: 1px solid #dee2e6;
border-radius: 4px;
}
.loading-spinner::after {
content: "...";
animation: dots 1.5s steps(5, end) infinite;
}
@keyframes dots {
0%, 20% {
color: rgba(0,0,0,0);
text-shadow: .25em 0 0 rgba(0,0,0,0), .5em 0 0 rgba(0,0,0,0);
}
40% {
color: #007bff;
text-shadow: .25em 0 0 rgba(0,0,0,0), .5em 0 0 rgba(0,0,0,0);
}
60% {
text-shadow: .25em 0 0 #007bff, .5em 0 0 rgba(0,0,0,0);
}
80%, 100% {
text-shadow: .25em 0 0 #007bff, .5em 0 0 #007bff;
}
}
/* Error message styling */
.error-message {
color: #721c24;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 12px;
margin: 10px 0;
text-align: center;
font-weight: 500;
}
/* Success message styling */
.import-success-message {
color: #155724;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 4px;
padding: 12px;
margin: 10px 0;
text-align: center;
font-weight: bold;
animation: fadeInOut 3s ease;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateY(-10px); }
10%, 90% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-10px); }
}
/* Code validation styling */
.code-checking {
background-color: #fff3cd !important;
border-color: #ffc107 !important;
position: relative;
}
.code-checking::after {
content: "Prüfe...";
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 0.8em;
color: #856404;
font-style: italic;
}
.code-valid {
background-color: #d4edda !important;
border-color: #28a745 !important;
color: #155724 !important;
}
.code-invalid {
background-color: #f8d7da !important;
border-color: #dc3545 !important;
color: #721c24 !important;
}
/* Duplicate code popup styling */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
animation: overlayFadeIn 0.3s ease;
}
@keyframes overlayFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.duplicate-code-popup {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
text-align: center;
position: relative;
animation: popupSlideIn 0.3s ease;
}
@keyframes popupSlideIn {
from {
opacity: 0;
transform: scale(0.8) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.popup-content h3 {
color: #dc3545;
margin: 0 0 15px 0;
font-size: 1.3em;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.popup-content h3::before {
content: "⚠️";
font-size: 1.2em;
}
.popup-content p {
color: var(--ui-text);
margin: 15px 0;
line-height: 1.5;
font-size: 1em;
}
.popup-content .duplicate-code {
font-weight: bold;
color: #dc3545;
background-color: #f8d7da;
padding: 5px 10px;
border-radius: 3px;
font-family: monospace;
font-size: 1.1em;
}
.popup-close-button {
background-color: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
margin-top: 20px;
transition: background-color 0.2s ease;
}
.popup-close-button:hover {
background-color: #5a6268;
}
.popup-close-x {
position: absolute;
top: 10px;
right: 15px;
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
padding: 5px;
line-height: 1;
}
.popup-close-x:hover {
color: var(--ui-text);
}
/* Upload form styles */
.upload-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);
}
.upload-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;
}
.add-new-btn {
background-color: #007bff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
margin-top: 5px;
font-size: 12px;
}
.add-new-btn:hover {
background-color: #0056b3;
}
.image-preview-container {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.image-preview {
position: relative;
display: inline-block;
}
.image-preview img {
width: 100px;
height: 100px;
object-fit: cover;
border: 1px solid #ddd;
border-radius: 4px;
}
.remove-image {
position: absolute;
top: -5px;
right: -5px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
/* Video preview styles */
.video-preview {
border: 2px dashed #17a2b8;
background-color: #f0f8ff;
}
.video-placeholder {
width: 100px;
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #e9ecef;
border-radius: 4px;
text-align: center;
}
.video-icon {
font-size: 30px;
margin-bottom: 5px;
}
.video-name {
font-size: 10px;
word-break: break-all;
max-width: 90px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Book cover preview styles */
.book-cover-preview {
margin-top: 10px;
padding: 10px;
border: 2px dashed #007bff;
border-radius: 5px;
background-color: #f8f9ff;
}
.book-cover-preview .preview-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.book-cover-preview img {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.image-loading {
padding: 10px;
text-align: center;
color: #666;
font-style: italic;
}
.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;
}
.submit-button:active {
background-color: #1e7e34;
}
/* Navigation button */
.nav-back-button {
background-color: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
display: inline-block;
margin-bottom: 20px;
transition: background-color 0.2s ease;
}
.nav-back-button:hover {
background-color: #5a6268;
color: white;
text-decoration: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.upload-container {
margin: 10px;
padding: 15px;
}
.multi-filter {
grid-template-columns: 1fr;
}
.isbn-input-group {
flex-direction: column;
gap: 8px;
}
.fetch-isbn-button {
width: 100%;
}
.book-thumbnail {
float: none;
display: block;
margin: 10px auto;
}
}
@media (max-width: 480px) {
.upload-form h1 {
font-size: 1.5em;
}
.submit-button {
padding: 12px 20px;
font-size: 16px;
}
}
/* Additional styles for elements with inline styles */
.edit-new-location-container {
display: none;
margin-top: 10px;
}
.book-cover-thumbnail {
max-width: 100px;
max-height: 150px;
border: 1px solid #ddd;
border-radius: 3px;
}
/* Standardized button styles across the application */
.import-book-button, .fetch-isbn-button, .add-new-btn, .popup-close-button, .nav-back-button,
.submit-button, button[type="submit"], button[type="button"] {
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
min-width: 120px;
height: 40px;
padding: 8px 16px;
font-weight: bold;
font-size: 0.9rem;
border-radius: 5px;
cursor: pointer;
white-space: nowrap;
box-sizing: border-box;
}
.book-cover-caption {
font-size: 0.8em;
color: #666;
margin: 5px 0;
}
.remove-book-cover-button {
background: #dc3545;
color: white;
border: none;
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 0.8em;
}
.duplicate-image-preview {
max-width: 150px;
max-height: 150px;
object-fit: cover;
}
.remove-duplicate-image-button {
background: #dc3545;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
margin-left: 10px;
}
</style>
<div class="upload-container">
{% if show_library_features %}
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
<h3 style="margin:0 0 8px 0;">Excel-Import Bibliothek (Mehrere Bücher)</h3>
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, ISBN, Code, Anzahl). Für den Bibliotheksimport ist eine gültige ISBN je Zeile erforderlich.</p>
<form method="POST" action="{{ url_for('upload_library_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
<input type="file" name="library_excel" accept=".xlsx,.csv" required>
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('library')">Beispiel-CSV herunterladen</button>
<button type="button" class="btn btn-outline-secondary" title="Format-Hilfe" onclick="showCsvHelp('library')">?</button>
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Bibliothek importieren</button>
</form>
</div>
{% else %}
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
<h3 style="margin:0 0 8px 0;">Excel-Import Inventar (Mehrere Artikel)</h3>
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, Filter1/2/3, Kosten, Jahr, Code, Anzahl).</p>
<form method="POST" action="{{ url_for('upload_inventory_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
<input type="file" name="inventory_excel" accept=".xlsx,.csv" required>
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('inventory')">Beispiel-CSV herunterladen</button>
<button type="button" class="btn btn-outline-secondary" title="Format-Hilfe" onclick="showCsvHelp('inventory')">?</button>
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Inventar importieren</button>
</form>
</div>
{% endif %}
<div class="upload-form">
<h1>{{ page_title|default('Artikel hochladen') }}</h1>
<form method="POST" action="{{ url_for('upload_item') }}" enctype="multipart/form-data">
<input type="hidden" name="upload_mode" value="{{ upload_mode|default('item') }}">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="ort">Ort:</label>
<select id="ort" name="ort" required>
<option value="">-- Bitte Ort auswählen --</option>
<!-- Options will be loaded by JavaScript -->
</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></textarea>
</div>
{% if show_library_features %}
<!-- Library Mode: Single Customizable Filter -->
<div class="filter-inputs">
<h3>Medientyp</h3>
<div class="form-group">
<select name="item_type_input" id="item_type_input">
<option value="" disabled selected>Medientyp auswählen...</option>
<option value="Buch">Buch</option>
<option value="Schulbuch">Schulbuch</option>
<option value="CD">CD</option>
<option value="DVD">DVD</option>
<option value="Sonstiges">Sonstiges</option>
</select>
<small style="display:block; color:#666;">Wählen Sie einen Medientyp aus zur Klassifizierung.</small>
</div>
<h3>Kategorie/Typ:</h3>
<div class="form-group">
<input type="text" name="library_category" id="library_category" placeholder="z.B. Belletristik, Sachbücher, Nachschlagewerke, etc.">
<small style="display:block; color:#666;">Geben Sie hier eine beliebige Kategorie ein zur freien Klassifizierung.</small>
</div>
</div>
{% else %}
<!-- Normal Item Mode: Standard Filters -->
<div class="filter-inputs">
<h3>Unterrichtsfach:</h3>
<div class="multi-filter">
<div class="form-group">
<label for="filter1-1">Wert 1:</label>
<select id="filter1-1" name="filter" class="filter-dropdown-select" required>
<option value="">-- Bitte auswählen --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-2">Wert 2:</label>
<select id="filter1-2" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-3">Wert 3:</label>
<select id="filter1-3" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-4">Wert 4:</label>
<select id="filter1-4" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
</div>
<h3>Jahrgangsstufe:</h3>
<div class="multi-filter">
<div class="form-group">
<label for="filter2-1">Wert 1:</label>
<select id="filter2-1" name="filter2" class="filter-dropdown-select" required>
<option value="">-- Bitte auswählen --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter2-2">Wert 2:</label>
<select id="filter2-2" name="filter2" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter2-3">Wert 3:</label>
<select id="filter2-3" name="filter2" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="formupload_admin-group">
<label for="filter2-4">Wert 4:</label>
<select id="filter2-4" name="filter2" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
</div>
<h3>Schlagwort:</h3>
<div class="multi-filter">
<div class="form-group">
<label for="filter3-1">Wert 1:</label>
<input type="text" id="filter3-1" name="filter3">
</div>
<div class="form-group">
<label for="filter3-2">Wert 2:</label>
<input type="text" id="filter3-2" name="filter3">
</div>
<div class="form-group">
<label for="filter3-3">Wert 3:</label>
<input type="text" id="filter3-3" name="filter3">
</div>
<div class="form-group">
<label for="filter3-4">Wert 4:</label>
<input type="text" id="filter3-4" name="filter3">
</div>
</div>
</div>
{% endif %}
<div class="form-group">
<label for="anschaffungsjahr">Anschaffungsjahr</label>
<input type="date" id="anschaffungsjahr" name="anschaffungsjahr">
</div>
<div class="form-group">
<label for="anschaffungskosten">Anschaffungskosten (€)</label>
<input id="anschaffungskosten" name="anschaffungskosten">
</div>
<div class="form-group">
<label for="scan_mode">Erfassungs-Modus:</label>
<select id="scan_mode" name="scan_mode" class="form-control" onchange="toggleScanMode()">
<option value="single">Einzel-Code (Scanner stoppt nach 1 Scan)</option>
<option value="continuous">Fortlaufend scannen (Mehrere Codes nacheinander)</option>
<option value="range">Code-Bereich generieren (z.B. ABC-001 bis ABC-010)</option>
</select>
</div>
<div id="range_generator_group" class="form-group" style="display:none; background: #f9f9f9; padding: 15px; border-radius: 5px; border: 1px solid #ddd; margin-bottom: 15px;">
<label style="font-weight: bold;">Code-Bereich automatisch generieren:</label>
<div style="display: flex; gap: 10px; margin-bottom: 10px; align-items: center;">
<input type="text" id="range_prefix" placeholder="Präfix (z.B. IT-)" class="form-control" style="flex: 2;">
<input type="number" id="range_start" placeholder="Start (z.B. 1)" class="form-control" style="flex: 1;">
<span style="font-weight: bold;">bis</span>
<input type="text" id="range_end" placeholder="Ende (z.B. 020)" class="form-control" style="flex: 1;">
</div>
<small style="display:block; color:#666; margin-bottom: 10px;">Tipp: Die Anzahl der Ziffern im "Ende"-Feld bestimmt die führenden Nullen (z.B. Ende "050" macht aus Start "1" einen "001").</small>
<button type="button" class="btn btn-secondary" onclick="generateCodeRange()">Bereich generieren & einfügen</button>
</div>
<div class="form-group" id="primary_code_group">
<label for="code_4">Basis-Code</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="code_4" name="code_4" class="form-control" placeholder="Haupt-Barcode" required>
<button type="button" id="scan-code4-btn" class="btn btn-primary">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 (je Zeile ein Code)</label>
<textarea id="individual_codes" name="individual_codes" rows="6" placeholder="z.B.&#10;ABC-001&#10;ABC-002" class="form-control"></textarea>
<small style="display:block; color:#666; margin-top: 5px;">Bei Anzahl > 1 können hier individuelle Codes pro Item gesetzt werden. Der Scanner setzt immer zuerst den Basis-Code im Feld oben; weitere Einzelcodes werden hier angehängt.</small>
</div>
<!-- Image upload (hidden for library mode) -->
<div class="form-group" {% if show_library_features %}style="display:none;"{% endif %}>
<label for="images">Bilder/Videos:</label>
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif, .mp4, .mov, .avi, .mkv, .webm, .flv, .m4v, .3gp" multiple>
<div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF, MP4, MOV, AVI, MKV, WEBM, FLV, M4V, 3GP</div>
<!-- Add image preview area -->
<div class="image-preview-container" id="image-preview-container"></div>
</div>
<!-- ISBN fields (library page only) -->
{% if show_library_features %}
<div class="form-group">
<label for="isbn">ISBN/Barcode (für Bild-Suche):</label>
<div class="isbn-input-group">
<input type="text" id="isbn" name="isbn" placeholder="ISBN oder Barcode eingeben..." required>
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Bild 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;">Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert.</small>
<div id="book-info-container" class="book-info-container"></div>
</div>
<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;">
<small style="display:block; color:#666;">Wenn deaktiviert, kann der Artikel nicht im Voraus reserviert werden (Sofort-Ausleihe bleibt möglich).</small>
</div>
{% endif %}
<button type="submit" class="submit-button">{{ 'Bücher hochladen' if show_library_features else 'Artikel hochladen' }}</button>
</form>
</div>
</div>
<!-- CSV Help Modal -->
<div id="csv-help-modal" class="popup-overlay" style="display:none;">
<div class="duplicate-code-popup" style="max-width:760px;">
<button class="popup-close-x" onclick="hideCsvHelp()">×</button>
<div class="popup-content">
<h3>CSV Import Format</h3>
<p id="csv-help-text" style="text-align:left; color:var(--ui-text);"></p>
<div style="margin-top:12px; text-align:center;">
<button class="popup-close-button" onclick="hideCsvHelp()">Schließen</button>
</div>
</div>
</div>
</div>
<script>
function downloadSampleCsv(scope) {
let headers = ['name','ort','beschreibung','filter1','filter2','filter3','anschaffungsjahr','anschaffungskosten','code_4','anzahl','isbn','item_type'];
let exampleInventory = ['Projektor','Aula','HD-Projektor für Präsentationen','Medien','Technik','','2019','450.00','PRJ-001','1','','general'];
let exampleLibrary = ['Harry Potter und der Stein der Weisen','Bibliothek','Kinderbuch von J.K. Rowling','Belletristik','','','1997','12.99','HP-001','1','9783551354013','book'];
let row = exampleInventory;
if (scope === 'library') row = exampleLibrary;
const csvContent = [headers.join(','), row.map(v=> '"'+String(v).replace(/"/g,'""')+'"').join(',')].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = scope === 'library' ? 'sample_library_import.csv' : 'sample_inventory_import.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function showCsvHelp(scope) {
const invText = `Erlaubte Spalten (Reihenfolge beliebig): name, ort, beschreibung, filter1, filter2, filter3, anschaffungsjahr, anschaffungskosten, code_4, anzahl, isbn, item_type.\n\n` +
`Hinweise:\n- Trenne Felder per Komma. Textfelder sollten in Anführungszeichen stehen, wenn sie Kommas enthalten.\n- Für Bibliotheks-Import ist eine gültige ISBN pro Zeile erforderlich (ISBN-10 oder ISBN-13).\n- 'code_4' kann leer bleiben; das System erzeugt in diesem Fall Codes.\n- 'anzahl' legt die Anzahl physischer Exemplare für diese Zeile fest.`;
const libText = invText;
document.getElementById('csv-help-text').textContent = scope === 'library' ? libText : invText;
document.getElementById('csv-help-modal').style.display = 'flex';
}
function hideCsvHelp() {
document.getElementById('csv-help-modal').style.display = 'none';
}
</script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
// Function to check if a file is a video
function isVideoFile(filename) {
const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.m4v', '.3gp'];
const extension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
return videoExtensions.includes(extension);
}
// Global variable to store fetched book data
let currentBookData = null;
// Quagga2 engine state management properties
let scannerRunning = false;
let activeScannerCallback = null; // Dynamically handles routing data to the active caller field
var code4LastScanned = '';
var code4LastScannedAt = 0;
function setCode4ScanStatus(message, isError = false) {
const statusEl = document.getElementById('code4-scan-status');
if (!statusEl) return;
statusEl.textContent = message || '';
statusEl.style.color = isError ? '#b00020' : '#666';
}
function setIsbnScanStatus(message, isError = false) {
const statusEl = document.getElementById('isbn-scan-status');
if (!statusEl) return;
statusEl.textContent = message || '';
statusEl.style.color = isError ? '#b00020' : '#666';
}
function normalizeIsbnClient(value) {
const normalized = String(value || '').replace(/[^0-9Xx]/g, '').toUpperCase();
if (normalized.length === 13 && /^\d{13}$/.test(normalized)) {
return normalized;
}
if (normalized.length === 10 && /^\d{9}[\dX]$/.test(normalized)) {
return normalized;
}
return '';
}
function updateIsbnLiveValidation() {
const isbnField = document.getElementById('isbn');
const statusEl = document.getElementById('isbn-scan-status');
if (!isbnField || !statusEl) return;
const rawValue = isbnField.value.trim();
isbnField.classList.remove('code-valid', 'code-invalid');
if (!rawValue) {
statusEl.textContent = 'Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert.';
statusEl.style.color = '#666';
return;
}
if (normalizeIsbnClient(rawValue)) {
isbnField.classList.add('code-valid');
statusEl.textContent = 'Gültiges ISBN-Format erkannt. Der Wert kann gespeichert werden.';
statusEl.style.color = '#666';
return;
}
isbnField.classList.add('code-invalid');
statusEl.textContent = 'Kein gültiges ISBN-10/13-Format erkannt. Der Wert wird trotzdem akzeptiert.';
statusEl.style.color = '#b00020';
}
// Centered Quagga2 Engine Initialization Pattern
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" // Force rear camera lenses
},
},
decoder: {
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
}
}, function(err) {
if (err) {
console.error("Initialization error:", err);
errorStatusSetter("Kamera konnte nicht gestartet werden.", true);
return;
}
Quagga.start();
scannerRunning = true;
errorStatusSetter(completionMsg, false);
});
}
function killScannerHardware() {
if (!scannerRunning) return;
Quagga.stop();
scannerRunning = false;
activeScannerCallback = null;
}
// Unified tracking capture listener
Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return;
const rawBarcode = String(data.codeResult.code || '').trim();
const currentTargetCallback = activeScannerCallback;
if (typeof currentTargetCallback === "function") {
currentTargetCallback(rawBarcode);
}
});
// Globale Variablen für den Scanner-Status
var code4LastScanned = '';
var code4LastScannedAt = 0;
/**
* Schaltet das UI basierend auf dem ausgewählten Modus um
*/
function toggleScanMode() {
const mode = document.getElementById('scan_mode').value;
const rangeGroup = document.getElementById('range_generator_group');
const scanButton = document.getElementById('scan-code4-btn');
const scannerBox = document.getElementById('code4-scanner');
if (scannerBox && scannerBox.style.display !== 'none') {
if (typeof killScannerHardware === 'function') killScannerHardware();
scannerBox.style.display = 'none';
if (scanButton) scanButton.textContent = 'Barcode scannen';
}
if (mode === 'range') {
if(rangeGroup) rangeGroup.style.display = 'block';
if(scanButton) scanButton.style.display = 'none';
} else {
if(rangeGroup) rangeGroup.style.display = 'none'; // Hier wurde der Fehler korrigiert
if(scanButton) scanButton.style.display = 'inline-block';
}
}
/**
* Generiert eine Liste von Codes basierend auf den Eingaben
*/
function generateCodeRange() {
const prefix = document.getElementById('range_prefix').value.trim();
const startStr = document.getElementById('range_start').value.trim();
const endStr = document.getElementById('range_end').value.trim();
const start = parseInt(startStr, 10);
const end = parseInt(endStr, 10);
const paddingLength = endStr.length;
if (isNaN(start) || isNaN(end) || start > end) {
alert("Bitte gültige Zahlen für Start und Ende eingeben. Der Startwert darf nicht größer als der Endwert sein.");
return;
}
let generatedCodes = [];
for (let i = start; i <= end; i++) {
let numStr = i.toString().padStart(paddingLength, '0');
generatedCodes.push(`${prefix}${numStr}`);
}
const codeField = document.getElementById('code_4');
const individualCodesArea = document.getElementById('individual_codes');
if (generatedCodes.length > 0) {
if (!codeField.value || confirm("Soll der aktuelle Basis-Code überschrieben werden?")) {
codeField.value = generatedCodes[0];
if (typeof validateCodeField === 'function') validateCodeField(codeField);
generatedCodes.shift();
}
if (generatedCodes.length > 0) {
let existing = individualCodesArea.value.trim();
individualCodesArea.value = existing ? existing + '\n' + generatedCodes.join('\n') : generatedCodes.join('\n');
}
alert(`Bereich erfolgreich generiert! (${end - start + 1} Codes)`);
}
}
/**
* Scanner-Funktion (Single & Continuous Modus)
*/
function startCode4Scanner() {
const scannerBox = document.getElementById('code4-scanner');
const scanButton = document.getElementById('scan-code4-btn');
const codeField = document.getElementById('code_4');
const individualCodesArea = document.getElementById('individual_codes');
const scanMode = document.getElementById('scan_mode').value;
if (!scannerBox || !scanButton || !codeField) return;
// Beende evtl. laufenden ISBN Scanner
const isbnScannerBox = document.getElementById('isbn-scanner');
if (isbnScannerBox && isbnScannerBox.style.display !== 'none') {
killScannerHardware();
isbnScannerBox.style.display = 'none';
const isbnScanButton = document.getElementById('scan-isbn-btn');
if (isbnScanButton) isbnScanButton.textContent = 'Barcode scannen';
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Scanner gestoppt.');
}
// Scanner ausschalten, wenn er bereits läuft
if (scannerBox.style.display !== 'none') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner gestoppt.');
return;
}
// Scanner starten
scannerBox.style.display = 'block';
scanButton.textContent = scanMode === 'continuous' ? 'Fortlaufenden Scan stoppen' : 'Scanner stoppen';
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner initialisiert. Richte die Kamera auf den Barcode...');
if (typeof runEngineInitialization === 'function') {
runEngineInitialization(
'#code4-scanner',
function(decodedText) {
const now = Date.now();
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) {
return;
}
code4LastScanned = decodedText;
code4LastScannedAt = now;
if (scanMode === 'single') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
codeField.value = decodedText;
if (typeof validateCodeField === 'function') validateCodeField(codeField);
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Code gesetzt: ${decodedText}`);
} else if (scanMode === 'continuous') {
let currentAreaCodes = individualCodesArea.value.split('\n').map(c => c.trim()).filter(c => c !== '');
if (!codeField.value) {
codeField.value = decodedText;
if (typeof validateCodeField === 'function') validateCodeField(codeField);
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Basis-Code gesetzt: ${decodedText}`);
} else if (!currentAreaCodes.includes(decodedText) && codeField.value !== decodedText) {
individualCodesArea.value += (individualCodesArea.value ? '\n' : '') + decodedText;
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Zusatz-Code erfasst: ${decodedText}`);
try {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = 'sine';
oscillator.frequency.value = 800;
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.1);
} catch (e) {
console.log("Audio feedback not supported");
}
} else {
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Code ${decodedText} wurde bereits gescannt.`);
}
}
},
'Scanner läuft. Richte die Kamera auf den Barcode.',
setCode4ScanStatus
);
}
}
/**
* ISBN Scanner-Funktion
*/
function startIsbnScanner() {
if (!libraryModuleEnabled) {
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Bibliotheks-Modul ist deaktiviert.', true);
return;
}
const scannerBox = document.getElementById('isbn-scanner');
const scanButton = document.getElementById('scan-isbn-btn');
const isbnField = document.getElementById('isbn');
if (!scannerBox || !scanButton || !isbnField) return;
const codeScannerBox = document.getElementById('code4-scanner');
const codeScanButton = document.getElementById('scan-code4-btn');
if (codeScannerBox && codeScannerBox.style.display !== 'none') {
killScannerHardware();
codeScannerBox.style.display = 'none';
codeScanButton.textContent = 'Barcode scannen';
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner gestoppt.');
}
if (scannerBox.style.display !== 'none') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Scanner gestoppt.');
return;
}
scannerBox.style.display = 'block';
scanButton.textContent = 'Scanner stoppen';
runEngineInitialization(
'#isbn-scanner',
function(decodedText) {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
const normalizedIsbn = typeof normalizeIsbnClient === 'function' ? normalizeIsbnClient(decodedText) : decodedText;
isbnField.value = normalizedIsbn || decodedText;
if (typeof updateIsbnLiveValidation === 'function') updateIsbnLiveValidation();
if (normalizedIsbn) {
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
if (typeof fetchBookInfo === 'function') fetchBookInfo('upload');
} else {
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
}
},
'Scanner läuft. Bitte ISBN-Code erfassen.',
setIsbnScanStatus
);
}
// --- WICHTIG: Event Listener Binden sobald DOM geladen ist ---
document.addEventListener('DOMContentLoaded', function() {
// 1. Basis-Code Scanner Button aktivieren
const scanCode4Btn = document.getElementById('scan-code4-btn');
if (scanCode4Btn) {
scanCode4Btn.addEventListener('click', startCode4Scanner);
}
// 2. ISBN Scanner Button aktivieren
const scanIsbnBtn = document.getElementById('scan-isbn-btn');
if (scanIsbnBtn) {
scanIsbnBtn.addEventListener('click', startIsbnScanner);
}
// 3. Dropdown-Modus Wechsel überwachen
const scanModeSelect = document.getElementById('scan_mode');
if (scanModeSelect) {
scanModeSelect.addEventListener('change', toggleScanMode);
}
// 4. ISBN Live-Validierung
const isbnInput = document.getElementById('isbn');
if (isbnInput && typeof updateIsbnLiveValidation === 'function') {
isbnInput.addEventListener('input', updateIsbnLiveValidation);
}
});
// Load predefined filter values for dropdowns
function loadPredefinedFilterValues(filterNumber) {
fetch(`/get_predefined_filter_values/${filterNumber}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
const filterSelects = [
document.getElementById(`filter${filterNumber}-1`),
document.getElementById(`filter${filterNumber}-2`),
document.getElementById(`filter${filterNumber}-3`),
document.getElementById(`filter${filterNumber}-4`)
];
filterSelects.forEach(select => {
if (select) {
// Clear existing options except the first one
while (select.children.length > 1) {
select.removeChild(select.lastChild);
}
const allOption = document.createElement('option');
allOption.value = '__ALL__';
allOption.textContent = '-- Alle auswählen --';
select.appendChild(allOption);
// Add new options - data.values contains the array
data.values.forEach(value => {
if (!value || String(value).trim() === '') {
return;
}
const option = document.createElement('option');
option.value = value;
option.textContent = value;
select.appendChild(option);
});
}
});
})
.catch(error => {
console.error(`Error loading predefined filter ${filterNumber} values:`, error);
});
}
// Load location options
function loadLocationOptions() {
fetch('/get_predefined_locations')
.then(response => response.json())
.then(data => {
const ortSelect = document.getElementById('ort');
if (ortSelect) {
// Clear existing options except the first one
while (ortSelect.children.length > 1) {
ortSelect.removeChild(ortSelect.lastChild);
}
// Add new options - data.locations contains the array
data.locations.forEach(location => {
const option = document.createElement('option');
option.value = location;
option.textContent = location;
ortSelect.appendChild(option);
});
}
})
.catch(error => {
console.error('Error loading location options:', error);
});
}
// Function to add new location
function addNewLocation() {
const newLocationInput = document.getElementById('new-location-input');
const newLocation = newLocationInput.value.trim();
if (!newLocation) {
alert('Bitte geben Sie einen Ort ein.');
return;
}
// Add to dropdown
const ortSelect = document.getElementById('ort');
const option = document.createElement('option');
option.value = newLocation;
option.textContent = newLocation;
option.selected = true;
ortSelect.appendChild(option);
// Hide the input container
document.getElementById('new-location-container').style.display = 'none';
newLocationInput.value = '';
}
// Function to cancel adding new location
function cancelAddLocation() {
document.getElementById('new-location-container').style.display = 'none';
document.getElementById('new-location-input').value = '';
}
// Function to fetch book information from ISBN
function fetchBookInfo(formType) {
if (!libraryModuleEnabled) {
alert('Bibliotheks-Modul ist deaktiviert.');
return;
}
const isbnField = document.getElementById('isbn');
const infoContainer = document.getElementById('book-info-container');
if (!isbnField || !infoContainer) {
alert('Fehler: Erforderliche Formularelemente nicht gefunden.');
return;
}
const isbn = normalizeIsbnClient(isbnField.value);
if (!isbn) {
infoContainer.innerHTML = '<div class="error-message">Bitte geben Sie eine ISBN oder einen Barcode ein.</div>';
return;
}
// Show loading indicator
infoContainer.innerHTML = '<div class="loading-spinner">Informationen werden abgerufen...</div>';
// Make API request to fetch book data
fetch(`/fetch_book_info/${isbn}`)
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('Buch nicht gefunden. Überprüfen Sie die ISBN.');
} else {
throw new Error('Fehler beim Abrufen der Buchinformationen');
}
}
return response.json();
})
.then(data => {
if (data.error) {
infoContainer.innerHTML = `<div class="error-message">${data.error}</div>`;
return;
}
// Store book data globally for import functionality
currentBookData = data;
// Display book information with import button
infoContainer.innerHTML = `
<div class="book-info">
<h4>${data.title}</h4>
${data.authors ? `<p><strong>Autor(en):</strong> ${data.authors}</p>` : ''}
${data.publisher ? `<p><strong>Verlag:</strong> ${data.publisher}</p>` : ''}
${data.publishedDate ? `<p><strong>Erscheinungsdatum:</strong> ${data.publishedDate}</p>` : ''}
${data.pageCount ? `<p><strong>Seitenanzahl:</strong> ${data.pageCount}</p>` : ''}
${data.price ? `<p><strong>Preis:</strong> ${data.price}</p>` : ''}
${data.thumbnail ? `<img src="${data.thumbnail}" alt="Buchcover" class="book-thumbnail">` : ''}
${data.description ? `
<div class="book-description">
<h5>Beschreibung:</h5>
<p>${data.description}</p>
</div>
` : ''}
<button type="button" class="import-book-button" onclick="importBookInfo('${formType}')">
Buchdaten in Formular übernehmen
</button>
</div>
`;
})
.catch(error => {
infoContainer.innerHTML = `<div class="error-message">${error.message}</div>`;
currentBookData = null;
});
}
// Function to import book information into form
function importBookInfo(formType) {
if (!currentBookData) {
alert('Keine Buchdaten verfügbar zum Import.');
return;
}
// Get form fields
const nameField = document.getElementById('name');
const descriptionField = document.getElementById('beschreibung');
const priceField = document.getElementById('anschaffungskosten'); // <-- NEU
if (!nameField || !descriptionField) {
alert('Fehler: Formularfelder nicht gefunden.');
return;
}
// Generate book title
let bookTitle = currentBookData.title || '';
if (currentBookData.authors && currentBookData.authors !== 'Unknown Author') {
bookTitle += ` - ${currentBookData.authors}`;
}
// Generate description
let description = '';
if (currentBookData.description && currentBookData.description !== 'No description available') {
description = currentBookData.description;
} else {
// Fallback description with available info
description = `Buch: ${currentBookData.title}`;
if (currentBookData.authors && currentBookData.authors !== 'Unknown Author') {
description += `\nAutor(en): ${currentBookData.authors}`;
}
if (currentBookData.publisher && currentBookData.publisher !== 'Unknown Publisher') {
description += `\nVerlag: ${currentBookData.publisher}`;
}
if (currentBookData.publishedDate && currentBookData.publishedDate !== 'Unknown Date') {
description += `\nErscheinungsdatum: ${currentBookData.publishedDate}`;
}
if (currentBookData.pageCount && currentBookData.pageCount !== 'Unknown') {
description += `\nSeiten: ${currentBookData.pageCount}`;
}
}
// Import the data into form fields
nameField.value = bookTitle;
descriptionField.value = description;
// Preis in das Formularfeld eintragen
if (priceField && currentBookData.price !== null && currentBookData.price !== undefined) {
// Wandelt den Float (z.B. 12.25) in einen String mit Komma (12,25) um
let formattedPrice = currentBookData.price.toString().replace('.', ',');
priceField.value = formattedPrice;
}
// Download and import book cover image if available
if (currentBookData.thumbnail) {
downloadBookCover(currentBookData.thumbnail);
}
// Show success message
const infoContainer = document.getElementById('book-info-container');
const successMessage = document.createElement('div');
successMessage.className = 'import-success-message';
successMessage.textContent = 'Buchdaten erfolgreich in das Formular übernommen!';
infoContainer.appendChild(successMessage);
// Remove success message after 3 seconds
setTimeout(() => {
if (successMessage.parentNode) {
successMessage.parentNode.removeChild(successMessage);
}
}, 3000);
}
// Function to download book cover image
function downloadBookCover(imageUrl) {
if (!imageUrl) {
console.log('No image URL provided');
return;
}
// Show loading indicator for image download
const imagePreviewContainer = document.getElementById('image-preview-container');
if (imagePreviewContainer) {
const loadingDiv = document.createElement('div');
loadingDiv.className = 'image-loading';
loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>';
imagePreviewContainer.appendChild(loadingDiv);
}
// Download the image via backend
fetch('/download_book_cover', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: imageUrl })
})
.then(response => response.json())
.then(data => {
// Remove loading indicator
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading');
if (loadingDiv) {
loadingDiv.remove();
}
if (data.success) {
// Create a preview of the downloaded image
const imagePreview = document.createElement('div');
imagePreview.className = 'book-cover-preview';
imagePreview.innerHTML = `
<div class="preview-item">
<img src="{{ url_for('uploaded_file', filename='') }}${data.filename}"
alt="Buchcover" class="book-cover-thumbnail">
<p class="book-cover-caption">Buchcover automatisch heruntergeladen</p>
<input type="hidden" name="book_cover_image" value="${data.filename}">
<button type="button" onclick="removeBookCover(this)"
class="remove-book-cover-button">
Entfernen
</button>
</div>
`;
if (imagePreviewContainer) {
imagePreviewContainer.appendChild(imagePreview);
}
console.log('Book cover downloaded successfully:', data.filename);
} else {
console.error('Failed to download book cover:', data.error);
// Show error message to user
if (imagePreviewContainer) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = 'Fehler beim Herunterladen des Buchcovers: ' + data.error;
errorDiv.style.fontSize = '0.8em';
errorDiv.style.padding = '5px';
errorDiv.style.marginTop = '5px';
imagePreviewContainer.appendChild(errorDiv);
// Remove error message after 5 seconds
setTimeout(() => errorDiv.remove(), 5000);
}
}
})
.catch(error => {
console.error('Error downloading book cover:', error);
// Remove loading indicator
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading');
if (loadingDiv) {
loadingDiv.remove();
}
// Show error message
if (imagePreviewContainer) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = 'Netzwerkfehler beim Herunterladen des Buchcovers';
errorDiv.style.fontSize = '0.8em';
errorDiv.style.padding = '5px';
errorDiv.style.marginTop = '5px';
imagePreviewContainer.appendChild(errorDiv);
// Remove error message after 5 seconds
setTimeout(() => errorDiv.remove(), 5000);
}
});
}
// Function to remove downloaded book cover
function removeBookCover(button) {
const previewItem = button.closest('.preview-item');
if (previewItem) {
previewItem.remove();
}
}
// Code validation functions
function checkCodeUnique(code, excludeId, callback) {
if (!code || code.trim() === '') {
callback(true);
return;
}
const url = excludeId ? `/check_code_unique/${encodeURIComponent(code)}?exclude_id=${excludeId}` : `/check_code_unique/${encodeURIComponent(code)}`;
fetch(url)
.then(response => response.json())
.then(data => {
callback(data.is_unique);
})
.catch(error => {
console.error('Error checking code uniqueness:', error);
callback(true); // Assume unique if there's an error
});
}
function showDuplicateCodeWarning(code) {
// Remove any existing popup
const existingPopup = document.querySelector('.popup-overlay');
if (existingPopup) {
existingPopup.remove();
}
// Create popup overlay
const overlay = document.createElement('div');
overlay.className = 'popup-overlay';
// Create popup content
overlay.innerHTML = `
<div class="duplicate-code-popup">
<button class="popup-close-x" onclick="this.closest('.popup-overlay').remove()">×</button>
<div class="popup-content">
<h3>Code bereits vorhanden</h3>
<p>Der Code <span class="duplicate-code">${code}</span> wird bereits von einem anderen Artikel verwendet.</p>
<p>Bitte wählen Sie einen anderen Code oder lassen Sie das Feld leer, um automatisch einen Code zu generieren.</p>
</div>
<button class="popup-close-button" onclick="this.closest('.popup-overlay').remove()">Verstanden</button>
</div>
`;
document.body.appendChild(overlay);
// Auto-remove after 10 seconds
setTimeout(() => {
if (overlay.parentNode) {
overlay.remove();
}
}, 10000);
}
// Exported function for use within parent layout fields
function validateCodeField(input, excludeId = null) {
const code = input.value.trim();
if (!code) {
// Clear any existing validation styling
input.classList.remove('code-invalid', 'code-valid');
return;
}
// Show loading state
input.classList.add('code-checking');
checkCodeUnique(code, excludeId, (isUnique) => {
input.classList.remove('code-checking');
if (isUnique) {
input.classList.remove('code-invalid');
input.classList.add('code-valid');
} else {
input.classList.remove('code-valid');
input.classList.add('code-invalid');
showDuplicateCodeWarning(code);
}
});
}
// Image preview functionality
function setupImagePreview() {
const imageInput = document.getElementById('images');
const previewContainer = document.getElementById('image-preview-container');
if (imageInput && previewContainer) {
imageInput.addEventListener('change', function(e) {
previewContainer.innerHTML = '';
const files = e.target.files;
// Validate file types before preview
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif',
'video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska',
'video/webm', 'video/x-flv', 'video/mp4', 'video/3gpp'];
let hasInvalidFile = false;
for (let i = 0; i < files.length; i++) {
if (!allowedTypes.includes(files[i].type)) {
hasInvalidFile = true;
// Clear the file input to prevent submission
imageInput.value = '';
previewContainer.innerHTML = '<div class="error-message">Fehler: Datei "' + files[i].name + '" hat ein nicht unterstütztes Format. Erlaubte Formate: JPG, JPEG, PNG, GIF, MP4, MOV, AVI, MKV, WEBM, FLV, M4V, 3GP</div>';
return; // Stop processing
}
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const preview = document.createElement('div');
preview.className = 'image-preview';
preview.innerHTML = `
<img src="${e.target.result}" alt="Preview">
<button type="button" class="remove-image" onclick="removeImagePreview(this, ${i})">×</button>
`;
previewContainer.appendChild(preview);
};
reader.readAsDataURL(file);
} else if (file.type.startsWith('video/')) {
const preview = document.createElement('div');
preview.className = 'image-preview video-preview';
preview.innerHTML = `
<div class="video-placeholder">
<div class="video-icon">🎥</div>
<div class="video-name">${file.name}</div>
</div>
<button type="button" class="remove-image" onclick="removeImagePreview(this, ${i})">×</button>
`;
previewContainer.appendChild(preview);
}
}
});
}
}
function removeImagePreview(button, index) {
const preview = button.closest('.image-preview');
if (preview) {
preview.remove();
}
// Remove the file from the input
const imageInput = document.getElementById('images');
if (imageInput) {
const dt = new DataTransfer();
const files = imageInput.files;
for (let i = 0; i < files.length; i++) {
if (i !== index) {
dt.items.add(files[i]);
}
}
imageInput.files = dt.files;
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Load predefined filter values for dropdowns
loadPredefinedFilterValues(1);
loadPredefinedFilterValues(2);
// Load location options
loadLocationOptions();
// Setup image preview
setupImagePreview();
// Setup code validation
const codeField = document.getElementById('code_4');
if (codeField) {
codeField.addEventListener('blur', function() {
validateCodeField(this);
});
}
const isbnField = document.getElementById('isbn');
if (isbnField) {
isbnField.addEventListener('input', updateIsbnLiveValidation);
isbnField.addEventListener('blur', updateIsbnLiveValidation);
updateIsbnLiveValidation();
}
// Setup add new location button
const addLocationBtn = document.getElementById('add-new-location-btn');
if (addLocationBtn) {
addLocationBtn.addEventListener('click', function() {
const container = document.getElementById('new-location-container');
if (container) {
container.style.display = container.style.display === 'none' ? 'block' : 'none';
}
});
}
// Handle duplication data if available (from sessionStorage or server)
if (typeof prefillFormWithDuplicateData === 'function') {
setTimeout(() => {
prefillFormWithDuplicateData();
}, 500); // Wait for all dropdowns to load
}
});
// Initialize duplication data - handle both server-side and sessionStorage data
let duplicateData = null;
// First check if serverDuplicateData is defined by the script above
if (typeof serverDuplicateData !== 'undefined' && serverDuplicateData) {
console.log("Using server-provided duplicateData:", serverDuplicateData);
duplicateData = serverDuplicateData;
// Ensure images is properly handled as an array
if (duplicateData.images) {
if (typeof duplicateData.images === 'string') {
try {
duplicateData.images = JSON.parse(duplicateData.images);
} catch (e) {
console.error("Error parsing duplicate images array:", e);
}
}
console.log("Parsed duplicate images successfully.");
}
}
</script>
{% endblock %}