Compare commits

...

17 Commits

Author SHA1 Message Date
Aiirondev_dev 1633118d7f center fix for the edit Modal 2026-06-10 12:48:14 +02:00
Aiirondev_dev a4b900c986 Fixes regarding ht eupload changes 2026-06-10 12:39:08 +02:00
Aiirondev_dev a06393a915 New implementiation of the Code Scannning mecanism 2026-06-10 12:26:25 +02:00
Aiirondev_dev 1b9bfe1b14 Fix of the edeting funktion in the Library Module 2026-06-10 11:24:33 +02:00
Aiirondev_dev 3cdd1552d9 Fix of a library funktion 2026-06-10 11:08:43 +02:00
Aiirondev_dev 80a206f524 Finisched the removal of the Author 2026-06-07 23:16:05 +02:00
Aiirondev_dev 3217da8b15 Improved ISBN Funktionality 2026-06-07 22:57:32 +02:00
Aiirondev_dev 22bc7c9c3c upload barcode implementation 2026-06-07 22:28:04 +02:00
Aiirondev_dev f156cc178a library author removing 2026-06-07 22:21:01 +02:00
Aiirondev_dev 116261d334 Slight fixes of thew Code scanning and Items processing 2026-06-07 12:42:27 +02:00
Aiirondev_dev 042afc19e2 slight changes to the Bar code scanner 2026-06-07 12:33:13 +02:00
Aiirondev_dev 28d049b52a smal changes fixes for the library Code Scanner 2026-06-07 00:14:57 +02:00
Aiirondev_dev 8e478aeba4 slight fixes 2026-06-07 00:03:21 +02:00
Aiirondev_dev 1b38d1f0c8 changed the wrapper of the new code scanning library 2026-06-06 23:52:19 +02:00
Aiirondev_dev e7e24b3fae further changes to the new Barcode system 2026-06-06 23:41:37 +02:00
Aiirondev_dev fc80857fbe minor changes to the Inpout scanning 2026-06-06 23:31:50 +02:00
Aiirondev_dev f763ad064d Changes from the deprecated qrcode scanning tool, to a more modern option in the library 2026-06-06 22:46:50 +02:00
7 changed files with 1159 additions and 1005 deletions
+221 -115
View File
@@ -5207,55 +5207,87 @@ def upload_item():
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# Check if base code is unique for single-item uploads
if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]):
existing_item = it._get_items_collection().find_one({"code_4": code_4[0]})
if existing_item:
error_msg = 'Der Code wird bereits verwendet. Umleitung zum Eintrag.'
if is_mobile:
return jsonify({
'success': False,
'message': error_msg,
'existing_item_id': str(existing_item['_id']),
'redirect_to_item': True
}), 400
flash(error_msg, 'info')
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
# -------------------------------------------------------------------
# 1. PARSE AND UNIFY CODES
# -------------------------------------------------------------------
# Assuming `code_4` might come in as a single string or a list depending on your form setup
primary_code_raw = request.form.get('code_4', '').strip()
individual_codes_raw = request.form.get('individual_codes', '').strip()
all_item_codes = []
if primary_code_raw:
all_item_codes.append(primary_code_raw)
if individual_codes_raw:
# Split textarea by newlines, clean whitespace, and ignore empty lines
extra_codes = [c.strip() for c in individual_codes_raw.replace('\r', '').split('\n') if c.strip()]
# Add to master list, ensuring no duplicates within the submitted list itself
for c in extra_codes:
if c not in all_item_codes:
all_item_codes.append(c)
# -------------------------------------------------------------------
# 2. OVERRIDE ITEM COUNT
# -------------------------------------------------------------------
# If the user scanned/generated codes, the amount of codes IS the item count.
# Otherwise, fallback to a manual item_count field if it exists.
if len(all_item_codes) > 0:
item_count = len(all_item_codes)
else:
try:
item_count = int(request.form.get('item_count', 1))
except ValueError:
item_count = 1
# -------------------------------------------------------------------
# 3. IMAGE VALIDATION
# -------------------------------------------------------------------
if upload_mode != 'library' and not is_duplicating and not images and not duplicate_images and not book_cover_image:
error_msg = 'Bitte laden Sie mindestens ein Bild hoch'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
else:
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# -------------------------------------------------------------------
# 4. UNIFIED DB UNIQUENESS VALIDATION
# -------------------------------------------------------------------
# Validate every code in our master list against the database
for code in all_item_codes:
if not it.is_code_unique(code):
# Special case: If they only uploaded ONE item, redirect them to that existing item
if item_count == 1:
existing_item = it._get_items_collection().find_one({"code_4": code})
if existing_item:
error_msg = 'Der Code wird bereits verwendet. Umleitung zum Eintrag.'
if is_mobile:
return jsonify({
'success': False,
'message': error_msg,
'existing_item_id': str(existing_item['_id']),
'redirect_to_item': True
}), 400
flash(error_msg, 'info')
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
# If multiple items are being uploaded, just throw a standard error
error_msg = f'Der Code "{code}" wird bereits von einem anderen Artikel verwendet. Bitte überprüfen Sie Ihre Eingaben.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
else:
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# Validate optional per-item codes
if individual_codes:
if len(individual_codes) > item_count:
error_msg = f'Zu viele Einzelcodes angegeben ({len(individual_codes)}), erlaubt sind maximal {item_count}.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
if len(set(individual_codes)) != len(individual_codes):
error_msg = 'Doppelte Einzelcodes erkannt. Bitte alle Codes eindeutig eintragen.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
for specific_code in individual_codes:
if not it.is_code_unique(specific_code):
error_msg = f'Der Einzelcode "{specific_code}" wird bereits verwendet.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# -------------------------------------------------------------------
# 5. BATCH CODE GENERATOR (Remains mostly unchanged)
# -------------------------------------------------------------------
def generate_unique_batch_code(base_code, position):
"""Generate a unique code for every item in a batch."""
"""Generate a unique code for every item in a batch if no specific code is provided."""
if not base_code:
return None
@@ -9376,17 +9408,137 @@ def search_word(word):
except Exception as e:
return jsonify({"success": False, "response": str(e)})
def _fetch_from_google_books(clean_isbn):
"""Source 1: Google Books API (Free, No Key required for basic use)"""
try:
url = f"https://www.googleapis.com/books/v1/volumes?q=isbn:{clean_isbn}"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if data.get('totalItems', 0) > 0 and data.get('items'):
book_info = data['items'][0].get('volumeInfo', {})
sale_info = data['items'][0].get('saleInfo', {})
price = None
retail_price = sale_info.get('retailPrice', {})
list_price = sale_info.get('listPrice', {})
if retail_price and 'amount' in retail_price:
price = f"{retail_price['amount']} {retail_price.get('currencyCode', '')}"
elif list_price and 'amount' in list_price:
price = f"{list_price['amount']} {list_price.get('currencyCode', '')}"
thumbnail = book_info.get('imageLinks', {}).get('thumbnail', '')
if thumbnail:
thumbnail = thumbnail.replace('http:', 'https:')
return {
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('authors', ['Unknown Author'])),
"publisher": book_info.get('publisher', 'Unknown Publisher'),
"publishedDate": book_info.get('publishedDate', 'Unknown Date'),
"description": book_info.get('description', 'Keine Beschreibung verfügbar'),
"pageCount": book_info.get('pageCount', 'Unknown'),
"price": price,
"thumbnail": thumbnail,
"source": "google-books"
}
except Exception as e:
print(f"Google Books error: {e}")
return None
def _fetch_from_lobid_germany(clean_isbn):
"""
Source 2: Lobid.org (hbz Network)
The ultimate free, keyless fallback for German Schoolbooks and DACH literature.
"""
try:
url = f"https://lobid.org/resources?q=isbn:{clean_isbn}&format=json"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if 'member' in data and len(data['member']) > 0:
book = data['member'][0]
# Extract Authors (Lobid uses a nested contribution array)
authors = []
for contrib in book.get('contribution', []):
if 'agent' in contrib and 'label' in contrib['agent']:
authors.append(contrib['agent']['label'])
# Extract Publishers
publishers = []
for pub in book.get('publication', []):
if 'publishedBy' in pub:
# sometimes publishedBy is a list, sometimes a string
pub_data = pub['publishedBy']
if isinstance(pub_data, list):
publishers.extend(pub_data)
else:
publishers.append(pub_data)
# Extract Date
pub_date = 'Unknown Date'
if book.get('publication'):
pub_date = book['publication'][0].get('startDate', 'Unknown Date')
# Extract Pages
pages = book.get('extent', ['Unknown'])[0] if 'extent' in book else 'Unknown'
return {
"title": book.get('title', 'Unknown Title'),
"authors": ', '.join(authors) if authors else 'Unknown Author',
"publisher": ', '.join(publishers) if publishers else 'Unknown Publisher',
"publishedDate": str(pub_date),
"description": 'Keine Beschreibung verfügbar (Schulbuch / Fachliteratur)',
"pageCount": pages,
"price": None,
"thumbnail": "", # Lobid rarely has covers, we will fallback to OpenLibrary cover below
"source": "lobid-germany"
}
except Exception as e:
print(f"Lobid error: {e}")
return None
def _fetch_from_open_library(clean_isbn):
"""Source 3: Open Library Search API (Free, No Key required)"""
try:
url = f"https://openlibrary.org/search.json?isbn={clean_isbn}"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if data.get('numFound', 0) > 0 and data.get('docs'):
book_info = data['docs'][0]
return {
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('author_name', ['Unknown Author'])),
"publisher": ', '.join(book_info.get('publisher', ['Unknown Publisher'])),
"publishedDate": str(book_info.get('publish_date', ['Unknown Date'])[0]),
"description": 'Keine Beschreibung verfügbar',
"pageCount": book_info.get('number_of_pages_median', 'Unknown'),
"price": None,
"thumbnail": f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg",
"source": "openlibrary"
}
except Exception as e:
print(f"OpenLibrary Search error: {e}")
return None
@app.route('/fetch_book_info/<isbn>')
def fetch_book_info(isbn):
"""
API endpoint to fetch book information by ISBN using Google Books API
Args:
isbn (str): ISBN to look up
Returns:
dict: Book information or error message
API endpoint to fetch book information by ISBN using multiple open sources.
Optimized for global literature AND German educational books.
"""
# Authorization Checks
if 'username' not in session or not us.check_admin(session['username']):
return jsonify({"error": "Not authorized"}), 403
@@ -9394,79 +9546,33 @@ def fetch_book_info(isbn):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
try:
# Validation
clean_isbn = normalize_and_validate_isbn(isbn)
if not clean_isbn:
return jsonify({"error": "Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden."}), 400
# First source: Google Books
response = requests.get(
f"https://www.googleapis.com/books/v1/volumes?q=isbn:{clean_isbn}",
timeout=10
)
# Define the Provider Chain (Order matters: General -> German/School -> Global Fallback)
providers = [
_fetch_from_google_books,
_fetch_from_lobid_germany,
_fetch_from_open_library
]
if response.status_code == 200:
data = response.json()
if data.get('totalItems', 0) > 0 and data.get('items'):
book_info = data['items'][0].get('volumeInfo', {})
sale_info = data['items'][0].get('saleInfo', {})
price = None
retail_price = sale_info.get('retailPrice', {})
list_price = sale_info.get('listPrice', {})
if retail_price and 'amount' in retail_price:
price = f"{retail_price['amount']} {retail_price.get('currencyCode', '')}"
elif list_price and 'amount' in list_price:
price = f"{list_price['amount']} {list_price.get('currencyCode', '')}"
thumbnail = book_info.get('imageLinks', {}).get('thumbnail', '')
if thumbnail:
thumbnail = thumbnail.replace('http:', 'https:')
return jsonify({
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('authors', ['Unknown Author'])),
"publisher": book_info.get('publisher', 'Unknown Publisher'),
"publishedDate": book_info.get('publishedDate', 'Unknown Date'),
"description": book_info.get('description', 'No description available'),
"pageCount": book_info.get('pageCount', 'Unknown'),
"price": price,
"thumbnail": thumbnail,
"isbn": clean_isbn,
"source": "google-books"
})
# Fallback: OpenLibrary
ol_response = requests.get(
f"https://openlibrary.org/isbn/{clean_isbn}.json",
timeout=10
)
if ol_response.status_code == 200:
ol_data = ol_response.json()
author_names = []
for author_ref in ol_data.get('authors', []):
key = author_ref.get('key')
if not key:
continue
try:
author_resp = requests.get(f"https://openlibrary.org{key}.json", timeout=8)
if author_resp.status_code == 200:
author_names.append(author_resp.json().get('name'))
except Exception:
continue
return jsonify({
"title": ol_data.get('title', 'Unknown Title'),
"authors": ', '.join([a for a in author_names if a]) if author_names else 'Unknown Author',
"publisher": ', '.join(ol_data.get('publishers', [])) if ol_data.get('publishers') else 'Unknown Publisher',
"publishedDate": ol_data.get('publish_date', 'Unknown Date'),
"description": (ol_data.get('description', {}).get('value') if isinstance(ol_data.get('description'), dict) else ol_data.get('description')) or 'No description available',
"pageCount": ol_data.get('number_of_pages', 'Unknown'),
"price": None,
"thumbnail": f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg",
"isbn": clean_isbn,
"source": "openlibrary"
})
# Iterate through providers until a book is found
for provider in providers:
book_data = provider(clean_isbn)
if book_data:
# Add the ISBN back into the response payload
book_data["isbn"] = clean_isbn
# If a provider found the book but had no cover, assign a default OpenLibrary cover fallback
if not book_data.get("thumbnail"):
book_data["thumbnail"] = f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg"
return jsonify(book_data)
# If all providers fail
return jsonify({"error": f"Kein Buch zu dieser ISBN gefunden: {clean_isbn}"}), 404
except Exception as e:
-5
View File
@@ -1295,11 +1295,6 @@
</li>
{% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Hier sehen Sie Ihre bibliotheksbezogenen Ausleihen.">Meine Medien</a>
</li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a>
+61 -50
View File
@@ -10,24 +10,6 @@
<h1>📚 Bibliothek</h1>
<p class="subtitle">Bücher, CDs und weitere Medien</p>
</div>
<div class="header-controls">
<!-- Favorites Toggle -->
<div class="view-switch">
<button id="favoriteToggle" class="toggle-btn" aria-label="Favoriten anzeigen/verbergen">
<span class="toggle-icon"></span>
<span class="toggle-label">Favoriten</span>
</button>
</div>
<!-- View Mode Toggle -->
<div class="view-switch">
<button id="viewModeToggle" class="toggle-btn" aria-label="Ansichtsmodus wechseln">
<span class="toggle-icon"></span>
<span class="toggle-label">Tabelle</span>
</button>
</div>
</div>
</div>
<!-- Search & Filter Section -->
@@ -205,8 +187,7 @@
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/html5-qrcode/minified/html5-qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
// View mode persistence
const LIBRARY_VIEW_MODE_KEY = 'inventarLibraryViewMode';
@@ -625,50 +606,80 @@ document.getElementById('clearFiltersBtn').addEventListener('click', function()
closeAllFilters();
});
// Favorites toggle
document.getElementById('favoriteToggle').addEventListener('click', function() {
this.classList.toggle('open');
// TODO: Implement favorites filtering
});
// Scanner toggle
let scanner = null;
// Scanner state tracking
let isScanning = false;
document.getElementById('scannerBtn').addEventListener('click', function(e) {
e.stopPropagation();
const container = document.getElementById('qrContainer');
const isOpen = container.style.display !== 'none';
const btn = this;
if (isOpen) {
// Close the scanner
container.style.display = 'none';
if (scanner) scanner.clear();
this.classList.remove('open');
this.setAttribute('aria-expanded', 'false');
} else {
container.style.display = 'block';
this.classList.add('open');
this.setAttribute('aria-expanded', 'true');
Quagga.stop();
isScanning = false;
if (!scanner) {
scanner = new Html5Qrcode('qr-reader');
scanner.start(
{ facingMode: "environment" },
{ fps: 10, qrbox: 250 },
onScanSuccess,
onScanError
);
}
btn.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
} else {
// Open the scanner
container.style.display = 'block';
btn.classList.add('open');
btn.setAttribute('aria-expanded', 'true');
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
// Targets the element where the video stream will inject
target: document.querySelector('#qr-reader'),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Forces back camera
},
},
decoder: {
// Optimized for 1D barcodes (e.g., student IDs, member cards)
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
}
}, function(err) {
if (err) {
console.error("Initialization error:", err);
alert("Kamera konnte nicht gestartet werden.");
return;
}
Quagga.start();
isScanning = true;
});
}
});
function onScanSuccess(decodedText, decodedResult) {
const studentId = decodedText.trim();
// Single Quagga event listener for successful scans
Quagga.onDetected(function(data) {
const decodedText = data.codeResult.code;
const studentId = String(decodedText || '').trim();
// Stop scanning immediately to prevent multiple triggers
Quagga.stop();
isScanning = false;
// Reset UI states to closed
const container = document.getElementById('qrContainer');
const btn = document.getElementById('scannerBtn');
if (container) container.style.display = 'none';
if (btn) {
btn.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
}
// Route the scanned data to your input
document.getElementById('studentIdInput').value = studentId;
alert('Ausweis gescannt: ' + studentId);
}
function onScanError(error) {
// Silently ignore scan errors
}
});
</script>
<style>
File diff suppressed because it is too large Load Diff
+103 -41
View File
@@ -525,8 +525,7 @@
window.isDebug = false; // Set to true only for development environment
</script>
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
// Global state
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
@@ -535,51 +534,114 @@
let codeSearchTerm = '';
let descSearchIds = null; // Set of matching IDs or null when disabled
// QR scanner toggle setup
document.addEventListener('DOMContentLoaded', function() {
const scanButton = document.getElementById('scanButton');
// Keep track of the scanner state globally/outer scope
let isScanning = false;
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
function startScanner(targetCallback) {
const qrReader = document.getElementById('qr-reader');
let html5QrcodeScanner = null;
const statusText = document.getElementById('status');
if (!scanButton || !qrReader) return;
// Store the custom action that should happen when a barcode is found
activeScannerCallback = targetCallback;
const setScannerUi = (isOpen) => {
scanButton.classList.toggle('is-active', isOpen);
scanButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
scanButton.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
};
// Show the container
if (qrReader) qrReader.style.display = 'block';
if (statusText) statusText.innerText = "Initializing camera...";
scanButton.addEventListener('click', function() {
if (qrReader.style.display !== 'block') {
qrReader.style.display = 'block';
html5QrcodeScanner = new Html5QrcodeScanner(
'qr-reader', { fps: 10, qrbox: 250 }
);
html5QrcodeScanner.render((decodedText) => {
html5QrcodeScanner.clear();
qrReader.style.display = 'none';
// Put scanned code into the search box and trigger search
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = decodedText;
searchByCode();
}
setScannerUi(false);
});
setScannerUi(true);
} else {
if (html5QrcodeScanner) {
html5QrcodeScanner.clear();
}
qrReader.style.display = 'none';
setScannerUi(false);
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
// Make sure this targets your correct HTML container element
target: document.querySelector('#scanner-container'),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Force rear camera
},
},
decoder: {
// Your required formats mapped to Quagga2 reader strings
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
}
}, function(err) {
if (err) {
console.error("Initialization error:", err);
if (statusText) statusText.innerText = "Camera Error";
return;
}
Quagga.start();
isScanning = true;
// If we are using the main search button, manage its UI state
if (!targetCallback) {
setScannerUi(true);
}
if (statusText) statusText.innerText = "Scanning...";
});
}
function stopScanner() {
Quagga.stop();
isScanning = false;
activeScannerCallback = null; // Reset the callback routing
const qrReader = document.getElementById('qr-reader');
if (qrReader) qrReader.style.display = 'none';
setScannerUi(false);
}
function setScannerUi(isOpen) {
const scanBtn = document.getElementById('scanButton');
if (!scanBtn) return;
scanBtn.classList.toggle('is-active', isOpen);
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
}
// Unified global reader callback
Quagga.onDetected(function(data) {
const barcode = String(data.codeResult.code || '').trim();
console.log("Barcode detected:", barcode);
// Keep a local reference to the active callback before shutting down the engine
const currentCallback = activeScannerCallback;
// Critical: Stop scanning immediately to prevent multiple triggers
stopScanner();
// Route data based on who opened the scanner
if (typeof currentCallback === "function") {
// Send data directly to the specific edit field's logic
currentCallback(barcode);
} else {
// Fallback Default: Main search box logic
const resultText = document.getElementById('barcode-result');
if (resultText) resultText.innerText = barcode;
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = barcode;
if (typeof searchByCode === "function") {
searchByCode();
}
}
}
});
// Event Listener for the main standalone search button
document.getElementById('scanButton')?.addEventListener('click', function() {
if (!isScanning) {
// Pass null so it defaults to filling the standard search box
startScanner(null);
} else {
stopScanner();
}
});
// Return preferred primary and fallback image URLs based on extension
+57 -71
View File
@@ -2436,7 +2436,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="qr-container">
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
<div id="qr-reader" style="display: none;">
<h3>Status: <span id="status">Ready</span></h3>
<h3>Status: <span id="status">Bereit</span></h3>
<div id="scanner-container"></div>
</div>
</div>
@@ -2733,7 +2733,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
window.isDebug = false; // Set to true only for development environment
</script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script src="/static/js/scanner.js"></script>
<script>
// Function to check if a file is a video
function isVideoFile(filename) {
@@ -2771,14 +2770,18 @@ document.addEventListener('DOMContentLoaded', ()=>{
// No-op in production
return;
}
// Keep track of the scanner state globally/outer scope
let isScanning = false;
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
function startScanner() {
function startScanner(targetCallback) {
const qrReader = document.getElementById('qr-reader');
const statusText = document.getElementById('status');
// Store the custom action that should happen when a barcode is found
activeScannerCallback = targetCallback;
// Show the container
if (qrReader) qrReader.style.display = 'block';
if (statusText) statusText.innerText = "Initializing camera...";
@@ -2808,7 +2811,12 @@ document.addEventListener('DOMContentLoaded', ()=>{
Quagga.start();
isScanning = true;
setScannerUi(true);
// If we are using the main search button, manage its UI state
if (!targetCallback) {
setScannerUi(true);
}
if (statusText) statusText.innerText = "Scanning...";
});
}
@@ -2816,6 +2824,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
function stopScanner() {
Quagga.stop();
isScanning = false;
activeScannerCallback = null; // Reset the callback routing
const qrReader = document.getElementById('qr-reader');
if (qrReader) qrReader.style.display = 'none';
@@ -2832,37 +2841,46 @@ document.addEventListener('DOMContentLoaded', ()=>{
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
}
// 4. Quagga event listener for successful scans
// Unified global reader callback
Quagga.onDetected(function(data) {
const barcode = data.codeResult.code;
const barcode = String(data.codeResult.code || '').trim();
console.log("Barcode detected:", barcode);
// Keep a local reference to the active callback before shutting down the engine
const currentCallback = activeScannerCallback;
// Critical: Stop scanning immediately to prevent multiple triggers
stopScanner();
// Update your results display if you have them
const resultText = document.getElementById('barcode-result');
if (resultText) resultText.innerText = barcode;
// Route data based on who opened the scanner
if (typeof currentCallback === "function") {
// Send data directly to the specific edit field's logic
currentCallback(barcode);
} else {
// Fallback Default: Main search box logic
const resultText = document.getElementById('barcode-result');
if (resultText) resultText.innerText = barcode;
// Put the scanned code in the search box
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = barcode;
// Trigger your automatic search
if (typeof searchByCode === "function") {
searchByCode();
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = barcode;
if (typeof searchByCode === "function") {
searchByCode();
}
}
}
});
// 5. Wire up your Toggle Button click event
document.getElementById('scanButton').addEventListener('click', function() {
// Event Listener for the main standalone search button
document.getElementById('scanButton')?.addEventListener('click', function() {
if (!isScanning) {
startScanner();
// Pass null so it defaults to filling the standard search box
startScanner(null);
} else {
stopScanner();
}
});
function scanIntoEditCode() {
const qrReader = document.getElementById('qr-reader');
const editCodeInput = document.getElementById('edit-code4');
@@ -2871,37 +2889,21 @@ document.addEventListener('DOMContentLoaded', ()=>{
return;
}
if (qrReader.style.display !== 'none') {
if (hybridScanner) {
hybridScanner.stop();
hybridScanner = null;
}
qrReader.style.display = 'none';
// Toggle close if it's already running
if (isScanning && qrReader.style.display !== 'none') {
stopScanner();
scanEditBtn.textContent = 'Barcode scannen';
return;
}
qrReader.style.display = 'block';
scanEditBtn.textContent = 'Scanner schließen';
hybridScanner = new HybridScanner({
videoId: 'qr-reader-video',
canvasId: 'qr-reader-canvas',
formats: ['CODE_128', 'CODE_39', 'QR_CODE'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
hybridScanner.stop();
qrReader.style.display = 'none';
editCodeInput.value = String(decodedText || '').trim();
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
scanEditBtn.textContent = 'Barcode scannen';
},
onError: function(error) {
console.error('Scanner error:', error);
}
// Start scanner with custom logic mapping to the Code input field
startScanner(function(decodedText) {
editCodeInput.value = decodedText;
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
scanEditBtn.textContent = 'Barcode scannen';
});
hybridScanner.start();
}
function scanIntoEditIsbn() {
@@ -2912,39 +2914,23 @@ document.addEventListener('DOMContentLoaded', ()=>{
return;
}
if (qrReader.style.display !== 'none') {
if (hybridScanner) {
hybridScanner.stop();
hybridScanner = null;
}
qrReader.style.display = 'none';
// Toggle close if it's already running
if (isScanning && qrReader.style.display !== 'none') {
stopScanner();
scanIsbnBtn.textContent = 'ISBN scannen';
return;
}
qrReader.style.display = 'block';
scanIsbnBtn.textContent = 'Scanner schließen';
hybridScanner = new HybridScanner({
videoId: 'qr-reader-video',
canvasId: 'qr-reader-canvas',
formats: ['ISBN', 'EAN_13', 'EAN_8', 'QR_CODE'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
hybridScanner.stop();
qrReader.style.display = 'none';
editIsbnInput.value = String(decodedText || '').trim();
scanIsbnBtn.textContent = 'ISBN scannen';
if (typeof fetchBookInfo === 'function') {
fetchBookInfo('edit');
}
},
onError: function(error) {
console.error('Scanner error:', error);
// Start scanner with custom logic mapping to the ISBN input field
startScanner(function(decodedText) {
editIsbnInput.value = decodedText;
scanIsbnBtn.textContent = 'ISBN scannen';
if (typeof fetchBookInfo === 'function') {
fetchBookInfo('edit');
}
});
hybridScanner.start();
}
function rebuildFilter3Options() {
+294 -283
View File
@@ -831,7 +831,7 @@
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<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>
@@ -869,25 +869,41 @@
<label for="anschaffungskosten">Anschaffungskosten (€)</label>
<input id="anschaffungskosten" name="anschaffungskosten">
</div>
<div class="form-group">
<label for="code_4">Code</label>
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<input id="code_4" name="code_4">
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
<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;">Einzelcode manuell setzen oder per Scanner erfassen.</small>
<div id="code4-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
<small id="code4-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
<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">
<label for="item_count">Anzahl dieser Sorte</label>
<input type="number" id="item_count" name="item_count" min="1" max="100" value="1">
<small style="display:block; color:#666;">Es werden eigene Items erstellt, aber in der Übersicht als ein Artikel gebündelt angezeigt.</small>
<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">
<label for="individual_codes">Einzelcodes (optional, je Zeile ein Code)</label>
<textarea id="individual_codes" name="individual_codes" rows="4" placeholder="z.B.\nABC-001\nABC-002"></textarea>
<small style="display:block; color:#666;">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 können hier bei Bedarf manuell ergänzt werden.</small>
<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 %}>
@@ -970,8 +986,7 @@
document.getElementById('csv-help-modal').style.display = 'none';
}
</script>
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
@@ -984,12 +999,12 @@
// Global variable to store fetched book data
let currentBookData = null;
let code4ScannerInstance = null;
let code4ScannerRunning = false;
// Quagga2 engine state management properties
let scannerRunning = false;
let activeScannerCallback = null; // Dynamically handles routing data to the active caller field
let code4LastScanned = '';
let code4LastScannedAt = 0;
let isbnScannerInstance = null;
let isbnScannerRunning = false;
function setCode4ScanStatus(message, isError = false) {
const statusEl = document.getElementById('code4-scan-status');
@@ -1042,58 +1057,240 @@
statusEl.style.color = '#b00020';
}
function startCode4Scanner() {
const scannerBox = document.getElementById('code4-scanner');
const scanButton = document.getElementById('scan-code4-btn');
const codeField = document.getElementById('code_4');
if (!scannerBox || !scanButton || !codeField) return;
// Stop ISBN scanner if currently running to avoid camera conflicts.
if (isbnScannerRunning && isbnScannerInstance) {
isbnScannerInstance.clear().catch(() => {});
const isbnScannerBox = document.getElementById('isbn-scanner');
const isbnScanButton = document.getElementById('scan-isbn-btn');
if (isbnScannerBox) isbnScannerBox.style.display = 'none';
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
isbnScannerRunning = false;
// Centered Quagga2 Engine Initialization Pattern
function runEngineInitialization(targetSelector, activeCallback, completionMsg, errorStatusSetter) {
if (scannerRunning) {
Quagga.stop();
scannerRunning = false;
}
if (code4ScannerRunning && code4ScannerInstance) {
code4ScannerInstance.clear().catch(() => {});
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
code4ScannerRunning = false;
setCode4ScanStatus('Scanner gestoppt.');
return;
}
activeScannerCallback = activeCallback;
scannerBox.style.display = 'block';
scanButton.textContent = 'Scanner stoppen';
code4ScannerInstance = new Html5QrcodeScanner('code4-scanner', {
fps: 10,
qrbox: 250,
rememberLastUsedCamera: true
});
code4ScannerRunning = true;
setCode4ScanStatus('Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.');
code4ScannerInstance.render((decodedText) => {
const scannedCode = String(decodedText || '').trim();
if (!scannedCode) return;
const now = Date.now();
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) {
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;
}
code4LastScanned = scannedCode;
code4LastScannedAt = now;
codeField.value = scannedCode;
validateCodeField(codeField);
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`);
}, () => {});
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 (falls nicht schon woanders definiert)
let code4LastScanned = '';
let 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');
// Scanner stoppen, falls der Modus gewechselt wird und er noch läuft
if (scannerBox && scannerBox.style.display !== 'none') {
if (typeof killScannerHardware === 'function') killScannerHardware();
scannerBox.style.display = 'none';
if (scanButton) scanButton.textContent = 'Barcode scannen';
}
if (mode === 'range') {
rangeGroup.style.display = 'block';
if(scanButton) scanButton.style.display = 'none'; // Verstecke Scanner im Range-Modus
} else {
rangeGroup.style.display = 'none';
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);
// Die Länge des End-Wertes bestimmt das Padding (die führenden Nullen)
// Beispiel: Ende = "050" -> Länge 3 -> "001", "002", etc.
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++) {
// Nullen auffüllen
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) {
// 1. Basis-Code setzen (falls leer oder wenn wir komplett neu generieren)
if (!codeField.value || confirm("Soll der aktuelle Basis-Code überschrieben werden?")) {
codeField.value = generatedCodes[0];
if (typeof validateCodeField === 'function') validateCodeField(codeField);
generatedCodes.shift(); // Den ersten aus dem Array entfernen, da er im Basis-Feld steht
}
// 2. Den Rest in die Textarea einfügen
if (generatedCodes.length > 0) {
let existing = individualCodesArea.value.trim();
// Wenn schon was drin steht, machen wir einen Zeilenumbruch, sonst nicht
individualCodesArea.value = existing ? existing + '\n' + generatedCodes.join('\n') : generatedCodes.join('\n');
}
// Visuelles Feedback
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') {
if (typeof killScannerHardware === 'function') killScannerHardware();
isbnScannerBox.style.display = 'none';
const isbnScanButton = document.getElementById('scan-isbn-btn');
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Scanner gestoppt.');
}
// Scanner ausschalten, wenn er bereits läuft
if (scannerBox.style.display !== 'none') {
if (typeof killScannerHardware === 'function') 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();
// Verhindere Doppel-Scans desselben Codes innerhalb von 1.5 Sekunden
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) {
return;
}
code4LastScanned = decodedText;
code4LastScannedAt = now;
if (scanMode === 'single') {
// VERHALTEN: Einzel-Scan
if (typeof killScannerHardware === 'function') 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') {
// VERHALTEN: Fortlaufender Scan
let currentAreaCodes = individualCodesArea.value.split('\n').map(c => c.trim()).filter(c => c !== '');
if (!codeField.value) {
// Basis-Code setzen, falls noch leer
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) {
// In Textarea anhängen, falls Code noch nicht existiert
individualCodesArea.value += (individualCodesArea.value ? '\n' : '') + decodedText;
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Zusatz-Code erfasst: ${decodedText}`);
// Audio-Feedback für kontinuierliches Scannen (optional, aber sehr hilfreich)
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; // Hoher Beep
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
);
} else {
console.error("runEngineInitialization is not defined. Ensure your scanner script is loaded.");
}
}
function startIsbnScanner() {
if (!libraryModuleEnabled) {
setIsbnScanStatus('Bibliotheks-Modul ist deaktiviert.', true);
@@ -1104,59 +1301,49 @@
const isbnField = document.getElementById('isbn');
if (!scannerBox || !scanButton || !isbnField) return;
// Stop Code_4 scanner if currently running to avoid camera conflicts.
if (code4ScannerRunning && code4ScannerInstance) {
code4ScannerInstance.clear().catch(() => {});
const codeScannerBox = document.getElementById('code4-scanner');
const codeScanButton = document.getElementById('scan-code4-btn');
// Automatically clean up running Base Code_4 scanner to prevent track multi-binding
const codeScannerBox = document.getElementById('code4-scanner');
const codeScanButton = document.getElementById('scan-code4-btn');
if (codeScannerBox && codeScannerBox.style.display !== 'none') {
killScannerHardware();
if (codeScannerBox) codeScannerBox.style.display = 'none';
if (codeScanButton) codeScanButton.textContent = 'Barcode scannen';
code4ScannerRunning = false;
setCode4ScanStatus('Scanner gestoppt.');
}
if (isbnScannerRunning && isbnScannerInstance) {
isbnScannerInstance.clear().catch(() => {});
if (scannerBox.style.display !== 'none') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
isbnScannerRunning = false;
setIsbnScanStatus('Scanner gestoppt.');
return;
}
scannerBox.style.display = 'block';
scanButton.textContent = 'Scanner stoppen';
isbnScannerInstance = new Html5QrcodeScanner('isbn-scanner', {
fps: 10,
qrbox: 250,
rememberLastUsedCamera: true
});
isbnScannerRunning = true;
setIsbnScanStatus('Scanner läuft. Bitte ISBN-Code erfassen.');
isbnScannerInstance.render((decodedText) => {
const scannedCode = String(decodedText || '').trim();
if (!scannedCode) return;
runEngineInitialization(
'#isbn-scanner',
function(decodedText) {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
const normalizedIsbn = normalizeIsbnClient(scannedCode);
isbnField.value = normalizedIsbn || scannedCode;
updateIsbnLiveValidation();
if (normalizedIsbn) {
setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
} else {
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
}
const normalizedIsbn = normalizeIsbnClient(decodedText);
isbnField.value = normalizedIsbn || decodedText;
updateIsbnLiveValidation();
isbnScannerInstance.clear().catch(() => {});
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
isbnScannerRunning = false;
if (normalizedIsbn) {
// Automatically load book metadata after a valid ISBN scan.
fetchBookInfo('upload');
}
}, () => {});
if (normalizedIsbn) {
setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
// Automatically load book metadata after a valid ISBN scan.
fetchBookInfo('upload');
} else {
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
}
},
'Scanner läuft. Bitte ISBN-Code erfassen.',
setIsbnScanStatus
);
}
// Load predefined filter values for dropdowns
@@ -1558,6 +1745,7 @@
}, 10000);
}
// Exported function for use within parent layout fields
function validateCodeField(input, excludeId = null) {
const code = input.value.trim();
@@ -1732,188 +1920,11 @@
if (typeof duplicateData.images === 'string') {
try {
duplicateData.images = JSON.parse(duplicateData.images);
console.log("Parsed images from string:", duplicateData.images);
} catch (e) {
console.warn("Failed to parse images string, using as single item:", duplicateData.images);
duplicateData.images = [duplicateData.images];
console.error("Error parsing duplicate images array:", e);
}
}
console.log(`Final duplicateData.images (${Array.isArray(duplicateData.images) ? 'array' : typeof duplicateData.images}):`, duplicateData.images);
}
}
// Then check for sessionStorage data (takes priority)
const sessionData = sessionStorage.getItem('duplicateItemData');
if (sessionData) {
try {
duplicateData = JSON.parse(sessionData);
sessionStorage.removeItem('duplicateItemData'); // Clean up
console.log("Session storage duplicate data:", duplicateData);
} catch (e) {
console.error('Error parsing session duplicate data:', e);
}
}
// Function to prefill form with duplicate data
function prefillFormWithDuplicateData() {
if (!duplicateData) return;
// Fill basic fields
const nameField = document.getElementById('name');
if (nameField && duplicateData.name) nameField.value = duplicateData.name;
const descField = document.getElementById('beschreibung');
if (descField && duplicateData.description) descField.value = duplicateData.description;
const yearField = document.getElementById('anschaffungsjahr');
if (yearField && duplicateData.year) yearField.value = duplicateData.year;
const costField = document.getElementById('anschaffungskosten');
if (costField && duplicateData.cost) costField.value = duplicateData.cost;
// Fill location dropdown
const locationField = document.getElementById('ort');
if (locationField && duplicateData.location) {
// Wait for locations to load, then set value
setTimeout(() => {
locationField.value = duplicateData.location;
}, 100);
}
// Fill filter arrays - handle up to 4 values per filter type
setTimeout(() => {
// Filter 1 (Unterrichtsfach)
if (duplicateData.filter1 && Array.isArray(duplicateData.filter1)) {
for (let j = 1; j <= Math.min(4, duplicateData.filter1.length); j++) {
const filterField = document.getElementById(`filter1-${j}`);
if (filterField && duplicateData.filter1[j-1]) {
filterField.value = duplicateData.filter1[j-1];
}
}
}
// Filter 2 (Jahrgangsstufe)
if (duplicateData.filter2 && Array.isArray(duplicateData.filter2)) {
for (let j = 1; j <= Math.min(4, duplicateData.filter2.length); j++) {
const filterField = document.getElementById(`filter2-${j}`);
if (filterField && duplicateData.filter2[j-1]) {
filterField.value = duplicateData.filter2[j-1];
}
}
}
// Filter 3 (Thema) - text inputs
if (duplicateData.filter3 && Array.isArray(duplicateData.filter3)) {
for (let j = 1; j <= Math.min(4, duplicateData.filter3.length); j++) {
const filterField = document.getElementById(`filter3-${j}`);
if (filterField && duplicateData.filter3[j-1]) {
filterField.value = duplicateData.filter3[j-1];
}
}
}
}, 200); // Wait for filter options to load
// Handle images - add hidden inputs for duplicate images
if (duplicateData.images && duplicateData.images.length > 0) {
console.log("Duplicate images data:", duplicateData.images);
const form = document.querySelector('form[action*="upload_item"]');
if (form) {
// Add hidden field to indicate duplication
const isDuplicatingField = document.createElement('input');
isDuplicatingField.type = 'hidden';
isDuplicatingField.name = 'is_duplicating';
isDuplicatingField.value = 'true';
form.appendChild(isDuplicatingField);
// Add each image as a duplicate image
const images = duplicateData.images || [];
console.log(`Adding ${images.length} images as hidden fields`);
if (Array.isArray(images)) {
images.forEach((imageName, index) => {
if (imageName) {
console.log(`Adding duplicate image ${index}: ${imageName}`);
const imageField = document.createElement('input');
imageField.type = 'hidden';
imageField.name = 'duplicate_images';
imageField.value = imageName;
form.appendChild(imageField);
}
});
} else {
console.warn("Images is not an array:", images);
// Try to handle as single item if it's a string
if (typeof images === 'string' && images) {
console.log("Adding single image:", images);
const imageField = document.createElement('input');
imageField.type = 'hidden';
imageField.name = 'duplicate_images';
imageField.value = images;
form.appendChild(imageField);
}
}
// Show image preview
showDuplicateImagePreview(duplicateData.images);
}
}
}
// Function to show preview of duplicate images
function showDuplicateImagePreview(images) {
const previewContainer = document.getElementById('image-preview-container');
if (!previewContainer) return;
console.log("showDuplicateImagePreview called with:", images);
// Make sure images is treated as an array
if (!Array.isArray(images)) {
if (typeof images === 'string') {
// Try to parse if it's a JSON string
try {
images = JSON.parse(images);
} catch (e) {
// If parsing fails, wrap the single string in an array
images = [images];
}
} else {
console.error("Invalid images data:", images);
return;
}
}
previewContainer.innerHTML = '';
console.log(`Processing ${images.length} images for preview`);
images.forEach((imageName, index) => {
console.log(`Processing image ${index}:`, imageName);
const preview = document.createElement('div');
preview.className = 'image-preview-item';
preview.innerHTML = `
<img src="/uploads/${imageName}" alt="Duplicate Image ${index + 1}" class="duplicate-image-preview">
<div class="image-controls">
<span>Von Original übernommen</span>
<button type="button" onclick="removeDuplicateImage('${imageName}', this)" class="remove-duplicate-image-button">Entfernen</button>
</div>
`;
previewContainer.appendChild(preview);
});
}
// Function to remove a duplicate image
function removeDuplicateImage(imageName, button) {
// Remove the hidden input
const hiddenInputs = document.querySelectorAll('input[name="duplicate_images"]');
hiddenInputs.forEach(input => {
if (input.value === imageName) {
input.remove();
}
});
// Remove the preview
const previewItem = button.closest('.image-preview-item');
if (previewItem) {
previewItem.remove();
console.log("Parsed duplicate images successfully.");
}
}
</script>