Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a06393a915 | |||
| 1b9bfe1b14 | |||
| 3cdd1552d9 | |||
| 80a206f524 | |||
| 3217da8b15 | |||
| 22bc7c9c3c | |||
| f156cc178a | |||
| 116261d334 | |||
| 042afc19e2 | |||
| 28d049b52a | |||
| 8e478aeba4 | |||
| 1b38d1f0c8 | |||
| e7e24b3fae | |||
| fc80857fbe | |||
| f763ad064d |
+148
-74
@@ -9376,17 +9376,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 +9514,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:
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
+423
-440
File diff suppressed because it is too large
Load Diff
+103
-41
@@ -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
|
||||
|
||||
@@ -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
@@ -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. ABC-001 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>
|
||||
|
||||
Reference in New Issue
Block a user