Compare commits

...

11 Commits

4 changed files with 608 additions and 795 deletions
+145 -71
View File
@@ -9376,17 +9376,137 @@ def search_word(word):
except Exception as e: except Exception as e:
return jsonify({"success": False, "response": str(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>') @app.route('/fetch_book_info/<isbn>')
def fetch_book_info(isbn): def fetch_book_info(isbn):
""" """
API endpoint to fetch book information by ISBN using Google Books API API endpoint to fetch book information by ISBN using multiple open sources.
Optimized for global literature AND German educational books.
Args:
isbn (str): ISBN to look up
Returns:
dict: Book information or error message
""" """
# Authorization Checks
if 'username' not in session or not us.check_admin(session['username']): if 'username' not in session or not us.check_admin(session['username']):
return jsonify({"error": "Not authorized"}), 403 return jsonify({"error": "Not authorized"}), 403
@@ -9394,79 +9514,33 @@ def fetch_book_info(isbn):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403 return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
try: try:
# Validation
clean_isbn = normalize_and_validate_isbn(isbn) clean_isbn = normalize_and_validate_isbn(isbn)
if not clean_isbn: if not clean_isbn:
return jsonify({"error": "Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden."}), 400 return jsonify({"error": "Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden."}), 400
# First source: Google Books # Define the Provider Chain (Order matters: General -> German/School -> Global Fallback)
response = requests.get( providers = [
f"https://www.googleapis.com/books/v1/volumes?q=isbn:{clean_isbn}", _fetch_from_google_books,
timeout=10 _fetch_from_lobid_germany,
) _fetch_from_open_library
]
if response.status_code == 200: # Iterate through providers until a book is found
data = response.json() for provider in providers:
if data.get('totalItems', 0) > 0 and data.get('items'): book_data = provider(clean_isbn)
book_info = data['items'][0].get('volumeInfo', {})
sale_info = data['items'][0].get('saleInfo', {})
price = None if book_data:
retail_price = sale_info.get('retailPrice', {}) # Add the ISBN back into the response payload
list_price = sale_info.get('listPrice', {}) book_data["isbn"] = clean_isbn
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 a provider found the book but had no cover, assign a default OpenLibrary cover fallback
if thumbnail: if not book_data.get("thumbnail"):
thumbnail = thumbnail.replace('http:', 'https:') book_data["thumbnail"] = f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg"
return jsonify({ return jsonify(book_data)
"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"
})
# If all providers fail
return jsonify({"error": f"Kein Buch zu dieser ISBN gefunden: {clean_isbn}"}), 404 return jsonify({"error": f"Kein Buch zu dieser ISBN gefunden: {clean_isbn}"}), 404
except Exception as e: except Exception as e:
+1 -25
View File
@@ -10,24 +10,6 @@
<h1>📚 Bibliothek</h1> <h1>📚 Bibliothek</h1>
<p class="subtitle">Bücher, CDs und weitere Medien</p> <p class="subtitle">Bücher, CDs und weitere Medien</p>
</div> </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> </div>
<!-- Search & Filter Section --> <!-- Search & Filter Section -->
@@ -624,14 +606,8 @@ document.getElementById('clearFiltersBtn').addEventListener('click', function()
closeAllFilters(); closeAllFilters();
}); });
// Favorites toggle
document.getElementById('favoriteToggle').addEventListener('click', function() {
this.classList.toggle('open');
// TODO: Implement favorites filtering
});
// Scanner toggle // Scanner toggle
l// Scanner state tracking // Scanner state tracking
let isScanning = false; let isScanning = false;
document.getElementById('scannerBtn').addEventListener('click', function(e) { document.getElementById('scannerBtn').addEventListener('click', function(e) {
+269 -372
View File
@@ -431,7 +431,7 @@
type="text" type="text"
id="librarySearch" id="librarySearch"
class="library-search-input" class="library-search-input"
placeholder="Nach Titel, Autor, ISBN suchen..." placeholder="Nach Titel, ISBN suchen..."
> >
<button <button
id="filterToggleBtn" id="filterToggleBtn"
@@ -445,7 +445,7 @@
<!-- Customizable Filter Panel --> <!-- Customizable Filter Panel -->
<!-- Student card / quick scan workflow --> <!-- Student card / quick scan workflow -->
<div class="library-scan-panel"> <div class="library-scan-panel">
<div class="library-scan-controls"> <div class="library-scan-controls">
<select id="scanModeSelect" aria-label="Scan-Modus"> <select id="scanModeSelect" aria-label="Scan-Modus">
<option value="card_only">Nur Ausweis erfassen</option> <option value="card_only">Nur Ausweis erfassen</option>
@@ -458,16 +458,14 @@
<div id="scanStatus" class="library-scan-status"> <div id="scanStatus" class="library-scan-status">
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode. Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
</div> </div>
<div id="scanReaderWrap" class="library-scan-reader-wrap"> <div id="scanReaderWrap" class="library-scan-reader-wrap" style="display: none;">
<div id="libraryQrReader" class="library-scan-reader"></div> <div id="libraryQrReader" class="library-scan-reader">
<div id="library-scanner-container"></div>
</div>
</div> </div>
</div> </div>
<div id="filterPanel" class="library-filter-panel"> <div id="filterPanel" class="library-filter-panel">
<div class="filter-row"> <div class="filter-row">
<div class="filter-item">
<label for="filterAuthor">Autor/Künstler:</label>
<input type="text" id="filterAuthor" placeholder="z.B. Goethe">
</div>
<div class="filter-item"> <div class="filter-item">
<label for="filterISBN">ISBN:</label> <label for="filterISBN">ISBN:</label>
<input type="text" id="filterISBN" placeholder="z.B. 978-3..."> <input type="text" id="filterISBN" placeholder="z.B. 978-3...">
@@ -514,7 +512,6 @@
<thead> <thead>
<tr> <tr>
<th style="width: 24%;">Titel</th> <th style="width: 24%;">Titel</th>
<th style="width: 14%;">Autor/Künstler</th>
<th style="width: 12%;">ISBN/Code</th> <th style="width: 12%;">ISBN/Code</th>
<th style="width: 8%;">Typ</th> <th style="width: 8%;">Typ</th>
<th style="width: 8%;">Anzahl</th> <th style="width: 8%;">Anzahl</th>
@@ -547,15 +544,17 @@
</div> </div>
</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 src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script> <script>
// State // =========================================================================
// 1. GLOBAL STATE DEFINITIONS
// =========================================================================
let libraryItems = []; let libraryItems = [];
let filteredItems = []; let filteredItems = [];
let visibleItems = []; let visibleItems = [];
let currentSearchTerm = ''; let currentSearchTerm = '';
let activeFilters = { let activeFilters = {
author: '',
isbn: '', isbn: '',
type: '', type: '',
status: '' status: ''
@@ -571,13 +570,20 @@
const RENDER_BATCH_COUNT = 120; const RENDER_BATCH_COUNT = 120;
let renderedCount = INITIAL_RENDER_COUNT; let renderedCount = INITIAL_RENDER_COUNT;
let filterPanelOpen = false; let filterPanelOpen = false;
// Scanner Related State Variables
let scannerInstance = null; let scannerInstance = null;
let scannerRunning = false; let scannerRunning = false;
let activeScannerCallback = null;
let activeStudentCardId = ''; let activeStudentCardId = '';
let lastScanValue = ''; let lastScanValue = '';
let lastScanAt = 0; let lastScanAt = 0;
const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1'); const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1');
// =========================================================================
// 2. DATA LOADING & FILTERING ENGINE
// =========================================================================
async function fetchLibraryPage(offset, limit) { async function fetchLibraryPage(offset, limit) {
const response = await fetch(`/api/library_items?offset=${offset}&limit=${limit}`); const response = await fetch(`/api/library_items?offset=${offset}&limit=${limit}`);
if (!response.ok) { if (!response.ok) {
@@ -586,7 +592,6 @@
return response.json(); return response.json();
} }
// Load items progressively: first page immediately, rest in background
async function loadLibraryItems() { async function loadLibraryItems() {
if (pagingState.loading) return; if (pagingState.loading) return;
pagingState.loading = true; pagingState.loading = true;
@@ -625,7 +630,6 @@
pagingState.total = page.total || pagingState.total; pagingState.total = page.total || pagingState.total;
pagingState.hasMore = Boolean(page.has_more); pagingState.hasMore = Boolean(page.has_more);
// Keep the current view reactive while avoiding a full rerender burst.
applyFiltersAndSearch(false); applyFiltersAndSearch(false);
if ('requestIdleCallback' in window) { if ('requestIdleCallback' in window) {
@@ -641,18 +645,16 @@
} }
function filterItems(items) { function filterItems(items) {
const author = activeFilters.author;
const isbn = activeFilters.isbn; const isbn = activeFilters.isbn;
const type = activeFilters.type; const type = activeFilters.type;
const status = activeFilters.status; const status = activeFilters.status;
return items.filter(item => { return items.filter(item => {
const authorMatch = !author || (item.Autor || item.Author || '').toLowerCase().includes(author);
const isbnMatch = !isbn || (item.ISBN || item.Code_4 || item.Code4 || '').toLowerCase().includes(isbn); const isbnMatch = !isbn || (item.ISBN || item.Code_4 || item.Code4 || '').toLowerCase().includes(isbn);
const typeMatch = !type || (item.ItemType || 'book') === type; const typeMatch = !type || (item.ItemType || 'book') === type;
const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available'); const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available');
const statusMatch = !status || statusKey === status; const statusMatch = !status || statusKey === status;
return authorMatch && isbnMatch && typeMatch && statusMatch; return isbnMatch && typeMatch && statusMatch;
}); });
} }
@@ -660,7 +662,6 @@
if (!currentSearchTerm) return items; if (!currentSearchTerm) return items;
return items.filter(item => return items.filter(item =>
(item.Name || '').toLowerCase().includes(currentSearchTerm) || (item.Name || '').toLowerCase().includes(currentSearchTerm) ||
(item.Autor || item.Author || '').toLowerCase().includes(currentSearchTerm) ||
(item.ISBN || item.Code_4 || item.Code4 || '').toLowerCase().includes(currentSearchTerm) (item.ISBN || item.Code_4 || item.Code4 || '').toLowerCase().includes(currentSearchTerm)
); );
} }
@@ -677,7 +678,6 @@
renderItems(); renderItems();
} }
// Display current view with incremental rendering
function renderItems() { function renderItems() {
const tbody = document.getElementById('itemsTableBody'); const tbody = document.getElementById('itemsTableBody');
const emptyState = document.getElementById('emptyState'); const emptyState = document.getElementById('emptyState');
@@ -695,37 +695,33 @@
emptyState.style.display = 'none'; emptyState.style.display = 'none';
const rowsToRender = visibleItems.slice(0, renderedCount); const rowsToRender = visibleItems.slice(0, renderedCount);
tbody.innerHTML = rowsToRender.map(item => ` tbody.innerHTML = rowsToRender.map(item => {
${(() => { const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available');
const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available'); const statusClass = statusKey === 'damaged' ? 'status-damaged' : (statusKey === 'borrowed' ? 'status-borrowed' : 'status-available');
const statusClass = statusKey === 'damaged' ? 'status-damaged' : (statusKey === 'borrowed' ? 'status-borrowed' : 'status-available'); const statusText = statusKey === 'damaged' ? 'Defekt/Zerstört' : (statusKey === 'borrowed' ? 'Ausgeliehen' : 'Verfügbar');
const statusText = statusKey === 'damaged' ? 'Defekt/Zerstört' : (statusKey === 'borrowed' ? 'Ausgeliehen' : 'Verfügbar'); const actionLabel = statusKey === 'available' ? 'Ausleihen' : (statusKey === 'borrowed' ? 'Reservieren' : 'Nicht ausleihbar');
const actionLabel = statusKey === 'available' ? 'Ausleihen' : (statusKey === 'borrowed' ? 'Reservieren' : 'Nicht ausleihbar'); const actionDisabled = statusKey === 'damaged' ? 'disabled' : '';
const actionDisabled = statusKey === 'damaged' ? 'disabled' : ''; return `
return ` <tr>
<tr> <td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td> <td>${escapeHtml(item.ISBN || item.Code_4 || item.Code4 || '-')}</td>
<td>${escapeHtml(item.Autor || item.Author || '-')}</td> <td>${getItemTypeLabel(item.ItemType || 'book')}</td>
<td>${escapeHtml(item.ISBN || item.Code_4 || item.Code4 || '-')}</td> <td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td>
<td>${getItemTypeLabel(item.ItemType || 'book')}</td> <td>
<td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td> <span class="table-status ${statusClass}">
<td> ${statusText}
<span class="table-status ${statusClass}"> </span>
${statusText} </td>
</span> <td class="table-actions">
</td> <button class="button" onclick="showItemDetail('${item._id}')">Details</button>
<td class="table-actions"> <button class="button" style="background: #28a745; color: white;" onclick="borrowItem('${item._id}')" ${actionDisabled}>
<button class="button" onclick="showItemDetail('${item._id}')">Details</button> ${actionLabel}
<button class="button" style="background: #28a745; color: white;" onclick="borrowItem('${item._id}')" ${actionDisabled}> </button>
${actionLabel} ${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</button>` : ''}
</button> ${canEditLibraryItems ? `<button class="button" style="background:#dc2626;color:#fff; margin-left:6px;" onclick="confirmDeleteLibraryItem('${item._id}')">Löschen</button>` : ''}
${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</button>` : ''} </td>
${canEditLibraryItems ? `<button class="button" style="background:#dc2626;color:#fff; margin-left:6px;" onclick="confirmDeleteLibraryItem('${item._id}')">Löschen</button>` : ''} </tr>`;
</td> }).join('');
</tr>
`;
})()}
`).join('');
const canLoadMoreRows = renderedCount < visibleItems.length; const canLoadMoreRows = renderedCount < visibleItems.length;
if (canLoadMoreRows) { if (canLoadMoreRows) {
@@ -740,13 +736,6 @@
} }
} }
// Confirm and call library item delete endpoint
function confirmDeleteLibraryItem(itemId) {
if (!itemId) return;
if (!confirm('Dieses Bibliotheksmedium revisionssicher löschen? Diese Aktion deaktiviert alle zugehörigen Exemplare und archiviert Medien.')) return;
deleteLibraryItem(itemId);
}
async function deleteLibraryItem(itemId) { async function deleteLibraryItem(itemId) {
if (!itemId) return; if (!itemId) return;
document.body.style.cursor = 'wait'; document.body.style.cursor = 'wait';
@@ -770,7 +759,6 @@
} }
loadLibraryItems(); loadLibraryItems();
} else { } else {
// Server redirected to HTML (flash messages). Reload to show state.
location.reload(); location.reload();
} }
} catch (e) { } catch (e) {
@@ -781,72 +769,183 @@
} }
} }
// Filter toggle function confirmDeleteLibraryItem(itemId) {
document.getElementById('filterToggleBtn').addEventListener('click', () => { if (!itemId) return;
filterPanelOpen = !filterPanelOpen; if (!confirm('Dieses Bibliotheksmedium revisionssicher löschen? Diese Aktion deaktiviert alle zugehörigen Exemplare und archiviert Medien.')) return;
const panel = document.getElementById('filterPanel'); deleteLibraryItem(itemId);
const btn = document.getElementById('filterToggleBtn');
panel.classList.toggle('open');
btn.classList.toggle('active');
});
// Apply filters
document.getElementById('applyFilterBtn').addEventListener('click', () => {
activeFilters.author = document.getElementById('filterAuthor').value.toLowerCase();
activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
activeFilters.type = document.getElementById('filterType').value;
activeFilters.status = document.getElementById('filterStatus').value;
applyFiltersAndSearch(true);
});
// Clear filters
document.getElementById('clearFilterBtn').addEventListener('click', () => {
document.getElementById('filterAuthor').value = '';
document.getElementById('filterISBN').value = '';
document.getElementById('filterType').value = '';
document.getElementById('filterStatus').value = '';
activeFilters = { author: '', isbn: '', type: '', status: '' };
applyFiltersAndSearch(true);
});
// Search input
document.getElementById('librarySearch').addEventListener('input', (e) => {
currentSearchTerm = (e.target.value || '').toLowerCase();
applyFiltersAndSearch(true);
});
document.getElementById('loadMoreBtn').addEventListener('click', () => {
renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
renderItems();
});
// Helper functions
function getItemTypeLabel(type) {
const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige' };
return labels[type] || type;
} }
function escapeHtml(text) { // =========================================================================
const div = document.createElement('div'); // 3. CORE SCANNER ROUTING ENGINE (QUAGGA2)
div.textContent = text; // =========================================================================
return div.innerHTML; function startScanner(targetCallback) {
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
activeScannerCallback = targetCallback;
if (readerWrap) readerWrap.style.display = 'block';
setScanStatus('Initializing camera...', 'warn');
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: document.querySelector('#library-scanner-container'),
constraints: {
width: 640,
height: 480,
facingMode: "environment"
},
},
decoder: {
readers: [
"code_128_reader",
"ean_reader",
"code_39_reader",
"upc_reader",
"codabar_reader",
"i2of5_reader"
]
}
}, function(err) {
if (err) {
console.error('Scanner start failed:', err);
if (readerWrap) readerWrap.style.display = 'none';
const detail = (err && (err.message || err.name)) ? ` (${err.message || err.name})` : '';
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
return;
}
Quagga.start();
scannerRunning = true;
if (!targetCallback && toggleBtn) {
toggleBtn.textContent = 'Scanner stoppen';
}
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
});
} }
function showItemDetail(itemId) { function stopScanner() {
// Load and show detail modal if (!scannerRunning) return;
fetch(`/api/item_detail/${itemId}`)
.then(r => r.text()) const readerWrap = document.getElementById('scanReaderWrap');
.then(html => { const toggleBtn = document.getElementById('toggleScannerBtn');
document.getElementById('detailContent').innerHTML = html;
document.getElementById('detailModal').style.display = 'flex'; Quagga.stop();
}) scannerRunning = false;
.catch(err => console.error('Error loading detail:', err)); activeScannerCallback = null;
if (readerWrap) readerWrap.style.display = 'none';
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
setScanStatus('Scanner gestoppt.', 'warn');
} }
function closeDetailModal() { Quagga.onDetected(function(data) {
document.getElementById('detailModal').style.display = 'none'; if (!data || !data.codeResult || !data.codeResult.code) return;
const barcode = String(data.codeResult.code || '').trim();
console.log("Barcode detected:", barcode);
const currentCallback = activeScannerCallback;
stopScanner();
if (typeof currentCallback === "function") {
currentCallback(barcode);
} else {
handleScanSuccess(barcode);
}
});
function handleScanSuccess(decodedText) {
const scannedCode = normalizeScannedCode(decodedText);
if (!scannedCode) return;
const now = Date.now();
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
return;
}
lastScanValue = scannedCode;
lastScanAt = now;
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
if (mode === 'card_only') {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
processQuickToggleScan(scannedCode);
} }
async function processQuickToggleScan(scannedCode) {
if (!activeStudentCardId) {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
try {
setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
student_card_id: activeStudentCardId,
item_code: scannedCode
})
});
const result = await response.json();
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return;
}
if (result.action === 'borrowed') {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
} else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
} else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
}
await loadLibraryItems();
} catch (err) {
console.error('Quick scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
}
}
function scanIntoEditCode() {
const scanReaderWrap = document.getElementById('scanReaderWrap');
const editCodeInput = document.getElementById('edit-code4');
const scanEditBtn = document.getElementById('scan-edit-code-btn');
if (!scanReaderWrap || !editCodeInput || !scanEditBtn) {
return;
}
if (scannerRunning && scanReaderWrap.style.display !== 'none') {
stopScanner();
scanEditBtn.textContent = 'Barcode scannen';
return;
}
scanEditBtn.textContent = 'Scanner schließen';
startScanner(function(decodedText) {
editCodeInput.value = decodedText;
if(typeof validateCodeField === "function") {
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
}
scanEditBtn.textContent = 'Barcode scannen';
});
}
// =========================================================================
// 4. UI INTERACTIONS & UTILITIES
// =========================================================================
function borrowItem(itemId) { function borrowItem(itemId) {
const selectedItem = (libraryItems || []).find(item => item._id === itemId); const selectedItem = (libraryItems || []).find(item => item._id === itemId);
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') { if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
@@ -920,210 +1019,47 @@
if (!el) return; if (!el) return;
el.textContent = message; el.textContent = message;
el.classList.remove('ok', 'warn', 'error'); el.classList.remove('ok', 'warn', 'error');
if (kind) { if (kind) el.classList.add(kind);
el.classList.add(kind);
}
} }
function setActiveStudentCard(cardId) { function setActiveStudentCard(cardId) {
activeStudentCardId = (cardId || '').trim().toUpperCase(); activeStudentCardId = (cardId || '').trim().toUpperCase();
const input = document.getElementById('activeStudentCard'); const input = document.getElementById('activeStudentCard');
if (input) { if (input) input.value = activeStudentCardId;
input.value = activeStudentCardId;
}
} }
function normalizeScannedCode(code) { function normalizeScannedCode(code) {
return (code || '').trim(); return (code || '').trim();
} }
async function processQuickToggleScan(scannedCode) { function getItemTypeLabel(type) {
if (!activeStudentCardId) { const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige' };
setActiveStudentCard(scannedCode); return labels[type] || type;
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
try {
setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
student_card_id: activeStudentCardId,
item_code: scannedCode
})
});
const result = await response.json();
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return;
}
if (result.action === 'borrowed') {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
} else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
} else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
}
await loadLibraryItems();
} catch (err) {
console.error('Quick scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
}
} }
function handleScanSuccess(decodedText) { function escapeHtml(text) {
const scannedCode = normalizeScannedCode(decodedText); const div = document.createElement('div');
if (!scannedCode) return; div.textContent = text;
return div.innerHTML;
const now = Date.now();
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
return;
}
lastScanValue = scannedCode;
lastScanAt = now;
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
if (mode === 'card_only') {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
processQuickToggleScan(scannedCode);
} }
function handleScanError() { function showItemDetail(itemId) {
// Intentionally silent to avoid UI spam while camera searches codes. fetch(`/api/item_detail/${itemId}`)
.then(r => r.text())
.then(html => {
document.getElementById('detailContent').innerHTML = html;
document.getElementById('detailModal').style.display = 'flex';
})
.catch(err => console.error('Error loading detail:', err));
} }
async function ensureScannerLibraryLoaded() { function closeDetailModal() {
if (typeof Html5QrcodeScanner !== 'undefined') { document.getElementById('detailModal').style.display = 'none';
return true;
}
const sources = [
'https://cdn.jsdelivr.net/npm/html5-qrcode/minified/html5-qrcode.min.js'
];
for (const src of sources) {
try {
await new Promise((resolve, reject) => {
const existing = document.querySelector(`script[data-scanner-src="${src}"]`);
if (existing) {
const onLoad = () => resolve();
const onError = () => reject(new Error('Script load failed'));
existing.addEventListener('load', onLoad, { once: true });
existing.addEventListener('error', onError, { once: true });
setTimeout(() => {
existing.removeEventListener('load', onLoad);
existing.removeEventListener('error', onError);
if (typeof Html5QrcodeScanner !== 'undefined') {
resolve();
} else {
reject(new Error('Script not available'));
}
}, 1200);
return;
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.defer = true;
script.dataset.scannerSrc = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Script load failed'));
document.head.appendChild(script);
});
if (typeof Html5QrcodeScanner !== 'undefined') {
return true;
}
} catch (err) {
console.warn('Scanner library load failed from', src, err);
}
}
return false;
}
async function startScanner() {
if (scannerRunning) return;
const scannerLoaded = await ensureScannerLibraryLoaded();
if (!scannerLoaded) {
setScanStatus('Scanner-Bibliothek konnte nicht geladen werden.', 'error');
return;
}
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
readerWrap.style.display = 'block';
try {
const formats = [];
if (typeof Html5QrcodeSupportedFormats !== 'undefined') {
formats.push(
Html5QrcodeSupportedFormats.QR_CODE,
Html5QrcodeSupportedFormats.EAN_13,
Html5QrcodeSupportedFormats.EAN_8,
Html5QrcodeSupportedFormats.CODE_128,
Html5QrcodeSupportedFormats.CODE_39,
Html5QrcodeSupportedFormats.UPC_A,
Html5QrcodeSupportedFormats.UPC_E,
Html5QrcodeSupportedFormats.ITF,
Html5QrcodeSupportedFormats.CODABAR
);
}
const scannerConfig = {
fps: 10,
rememberLastUsedCamera: true,
aspectRatio: 1.333334
};
if (formats.length > 0) {
scannerConfig.formatsToSupport = formats;
}
scannerInstance = scannerInstance || new Html5QrcodeScanner(
'libraryQrReader',
scannerConfig,
false
);
scannerInstance.render(handleScanSuccess, handleScanError);
scannerRunning = true;
toggleBtn.textContent = 'Scanner stoppen';
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
} catch (err) {
console.error('Scanner start failed:', err);
readerWrap.style.display = 'none';
const detail = (err && (err.message || err.name)) ? ` (${err.message || err.name})` : '';
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
}
}
async function stopScanner() {
if (!scannerRunning || !scannerInstance) return;
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
try {
await scannerInstance.clear();
} catch (err) {
console.error('Scanner stop failed:', err);
}
scannerRunning = false;
scannerInstance = null;
readerWrap.style.display = 'none';
toggleBtn.textContent = 'Scanner starten';
setScanStatus('Scanner gestoppt.', 'warn');
} }
// =========================================================================
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
// =========================================================================
function wireScannerUi() { function wireScannerUi() {
const toggleBtn = document.getElementById('toggleScannerBtn'); const toggleBtn = document.getElementById('toggleScannerBtn');
const resetBtn = document.getElementById('resetCardBtn'); const resetBtn = document.getElementById('resetCardBtn');
@@ -1134,7 +1070,7 @@
if (scannerRunning) { if (scannerRunning) {
await stopScanner(); await stopScanner();
} else { } else {
await startScanner(); await startScanner(null);
} }
}); });
} }
@@ -1151,83 +1087,48 @@
if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) { if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) {
setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn'); setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn');
} else if (modeSelect.value === 'card_only') { } else if (modeSelect.value === 'card_only') {
setScanStatus('Modus aktiv: Nur Ausweis erfassen.', 'warn'); setScanStatus('Nur Ausweis-Modus aktiv.', 'warn');
} else {
setScanStatus('Schnellmodus aktiv: jetzt Mediencode scannen.', 'warn');
} }
}); });
} }
} }
function getLibraryItemById(itemId) { // Connect standard Filters and Search inputs
return libraryItems.find(i => String(i._id) === String(itemId)); document.getElementById('filterToggleBtn').addEventListener('click', () => {
} filterPanelOpen = !filterPanelOpen;
document.getElementById('filterPanel').classList.toggle('open');
document.getElementById('filterToggleBtn').classList.toggle('active');
});
function openEditLibraryItem(itemId) { document.getElementById('applyFilterBtn').addEventListener('click', () => {
const item = getLibraryItemById(itemId); activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
if (!item) { activeFilters.type = document.getElementById('filterType').value;
alert('Element nicht gefunden.'); activeFilters.status = document.getElementById('filterStatus').value;
return; applyFiltersAndSearch(true);
} });
const modal = document.getElementById('editLibraryModal'); document.getElementById('clearFilterBtn').addEventListener('click', () => {
document.getElementById('editLibraryItemId').value = item._id || ''; document.getElementById('filterISBN').value = '';
document.getElementById('editLibraryName').value = item.Name || ''; document.getElementById('filterType').value = '';
document.getElementById('editLibraryAuthor').value = item.Autor || item.Author || ''; document.getElementById('filterStatus').value = '';
document.getElementById('editLibraryDescription').value = item.Beschreibung || ''; activeFilters = { isbn: '', type: '', status: '' };
document.getElementById('editLibraryLocation').value = item.Ort || ''; applyFiltersAndSearch(true);
document.getElementById('editLibraryIsbn').value = item.ISBN || ''; });
document.getElementById('editLibraryCode4').value = item.Code_4 || '';
document.getElementById('editLibraryType').value = (item.ItemType || 'book');
modal.style.display = 'flex';
}
function closeEditLibraryModal() { document.getElementById('librarySearch').addEventListener('input', (e) => {
const modal = document.getElementById('editLibraryModal'); currentSearchTerm = (e.target.value || '').toLowerCase();
modal.style.display = 'none'; applyFiltersAndSearch(true);
} });
async function submitEditLibraryItem(event) { document.getElementById('loadMoreBtn').addEventListener('click', () => {
event.preventDefault(); renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
const itemId = document.getElementById('editLibraryItemId').value; renderItems();
const payload = { });
name: document.getElementById('editLibraryName').value,
autor: document.getElementById('editLibraryAuthor').value,
beschreibung: document.getElementById('editLibraryDescription').value,
ort: document.getElementById('editLibraryLocation').value,
isbn: document.getElementById('editLibraryIsbn').value,
code_4: document.getElementById('editLibraryCode4').value,
item_type: document.getElementById('editLibraryType').value
};
try { // Run when DOM structure is entirely ready
const response = await fetch(`/api/library_item/${itemId}/update`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok || !result.ok) {
alert(result.message || 'Bearbeiten fehlgeschlagen.');
return;
}
closeEditLibraryModal();
await loadLibraryItems();
setScanStatus('Bibliotheksmedium aktualisiert.', 'ok');
} catch (err) {
console.error('Library edit failed:', err);
alert('Fehler beim Bearbeiten des Bibliotheksmediums.');
}
}
// Load on page init
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadLibraryItems(); wireScannerUi(); // Setup scanner control buttons
wireScannerUi(); loadLibraryItems(); // Fetch your database items right away!
const editForm = document.getElementById('editLibraryForm');
if (editForm) {
editForm.addEventListener('submit', submitEditLibraryItem);
}
}); });
</script> </script>
@@ -1242,10 +1143,6 @@
<label for="editLibraryName">Titel</label> <label for="editLibraryName">Titel</label>
<input id="editLibraryName" required> <input id="editLibraryName" required>
</div> </div>
<div>
<label for="editLibraryAuthor">Autor/Künstler</label>
<input id="editLibraryAuthor">
</div>
<div> <div>
<label for="editLibraryType">Medientyp</label> <label for="editLibraryType">Medientyp</label>
<select id="editLibraryType"> <select id="editLibraryType">
+117 -251
View File
@@ -970,8 +970,7 @@
document.getElementById('csv-help-modal').style.display = 'none'; document.getElementById('csv-help-modal').style.display = 'none';
} }
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
<script> <script>
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }}; const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
@@ -984,12 +983,12 @@
// Global variable to store fetched book data // Global variable to store fetched book data
let currentBookData = null; 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 code4LastScanned = '';
let code4LastScannedAt = 0; let code4LastScannedAt = 0;
let isbnScannerInstance = null;
let isbnScannerRunning = false;
function setCode4ScanStatus(message, isError = false) { function setCode4ScanStatus(message, isError = false) {
const statusEl = document.getElementById('code4-scan-status'); const statusEl = document.getElementById('code4-scan-status');
@@ -1042,56 +1041,109 @@
statusEl.style.color = '#b00020'; statusEl.style.color = '#b00020';
} }
// Centered Quagga2 Engine Initialization Pattern
function runEngineInitialization(targetSelector, activeCallback, completionMsg, errorStatusSetter) {
if (scannerRunning) {
Quagga.stop();
scannerRunning = false;
}
activeScannerCallback = activeCallback;
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: document.querySelector(targetSelector),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Force rear camera lenses
},
},
decoder: {
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
}
}, function(err) {
if (err) {
console.error("Initialization error:", err);
errorStatusSetter("Kamera konnte nicht gestartet werden.", true);
return;
}
Quagga.start();
scannerRunning = true;
errorStatusSetter(completionMsg, false);
});
}
function killScannerHardware() {
if (!scannerRunning) return;
Quagga.stop();
scannerRunning = false;
activeScannerCallback = null;
}
// Unified tracking capture listener
Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return;
const rawBarcode = String(data.codeResult.code || '').trim();
const currentTargetCallback = activeScannerCallback;
if (typeof currentTargetCallback === "function") {
currentTargetCallback(rawBarcode);
}
});
function startCode4Scanner() { function startCode4Scanner() {
const scannerBox = document.getElementById('code4-scanner'); const scannerBox = document.getElementById('code4-scanner');
const scanButton = document.getElementById('scan-code4-btn'); const scanButton = document.getElementById('scan-code4-btn');
const codeField = document.getElementById('code_4'); const codeField = document.getElementById('code_4');
if (!scannerBox || !scanButton || !codeField) return; if (!scannerBox || !scanButton || !codeField) return;
// Stop ISBN scanner if currently running to avoid camera conflicts. // Automatically clean up running ISBN scanner to prevent track multi-binding
if (isbnScannerRunning && isbnScannerInstance) { const isbnScannerBox = document.getElementById('isbn-scanner');
isbnScannerInstance.clear().catch(() => {}); const isbnScanButton = document.getElementById('scan-isbn-btn');
const isbnScannerBox = document.getElementById('isbn-scanner'); if (isbnScannerBox && isbnScannerBox.style.display !== 'none') {
const isbnScanButton = document.getElementById('scan-isbn-btn'); killScannerHardware();
if (isbnScannerBox) isbnScannerBox.style.display = 'none'; if (isbnScannerBox) isbnScannerBox.style.display = 'none';
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen'; if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
isbnScannerRunning = false; setIsbnScanStatus('Scanner gestoppt.');
} }
if (code4ScannerRunning && code4ScannerInstance) { if (scannerBox.style.display !== 'none') {
code4ScannerInstance.clear().catch(() => {}); killScannerHardware();
scannerBox.style.display = 'none'; scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen'; scanButton.textContent = 'Barcode scannen';
code4ScannerRunning = false;
setCode4ScanStatus('Scanner gestoppt.'); setCode4ScanStatus('Scanner gestoppt.');
return; return;
} }
scannerBox.style.display = 'block'; scannerBox.style.display = 'block';
scanButton.textContent = 'Scanner stoppen'; 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) => { runEngineInitialization(
const scannedCode = String(decodedText || '').trim(); '#code4-scanner',
if (!scannedCode) return; function(decodedText) {
const now = Date.now();
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) {
return;
}
code4LastScanned = decodedText;
code4LastScannedAt = now;
const now = Date.now(); killScannerHardware();
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) { scannerBox.style.display = 'none';
return; scanButton.textContent = 'Barcode scannen';
}
code4LastScanned = scannedCode;
code4LastScannedAt = now;
codeField.value = scannedCode; codeField.value = decodedText;
validateCodeField(codeField); validateCodeField(codeField);
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`); setCode4ScanStatus(`Code_4 gesetzt: ${decodedText}`);
}, () => {}); },
'Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.',
setCode4ScanStatus
);
} }
function startIsbnScanner() { function startIsbnScanner() {
@@ -1104,59 +1156,49 @@
const isbnField = document.getElementById('isbn'); const isbnField = document.getElementById('isbn');
if (!scannerBox || !scanButton || !isbnField) return; if (!scannerBox || !scanButton || !isbnField) return;
// Stop Code_4 scanner if currently running to avoid camera conflicts. // Automatically clean up running Base Code_4 scanner to prevent track multi-binding
if (code4ScannerRunning && code4ScannerInstance) { const codeScannerBox = document.getElementById('code4-scanner');
code4ScannerInstance.clear().catch(() => {}); const codeScanButton = document.getElementById('scan-code4-btn');
const codeScannerBox = document.getElementById('code4-scanner'); if (codeScannerBox && codeScannerBox.style.display !== 'none') {
const codeScanButton = document.getElementById('scan-code4-btn'); killScannerHardware();
if (codeScannerBox) codeScannerBox.style.display = 'none'; if (codeScannerBox) codeScannerBox.style.display = 'none';
if (codeScanButton) codeScanButton.textContent = 'Barcode scannen'; if (codeScanButton) codeScanButton.textContent = 'Barcode scannen';
code4ScannerRunning = false;
setCode4ScanStatus('Scanner gestoppt.'); setCode4ScanStatus('Scanner gestoppt.');
} }
if (isbnScannerRunning && isbnScannerInstance) { if (scannerBox.style.display !== 'none') {
isbnScannerInstance.clear().catch(() => {}); killScannerHardware();
scannerBox.style.display = 'none'; scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen'; scanButton.textContent = 'ISBN scannen';
isbnScannerRunning = false;
setIsbnScanStatus('Scanner gestoppt.'); setIsbnScanStatus('Scanner gestoppt.');
return; return;
} }
scannerBox.style.display = 'block'; scannerBox.style.display = 'block';
scanButton.textContent = 'Scanner stoppen'; 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) => { runEngineInitialization(
const scannedCode = String(decodedText || '').trim(); '#isbn-scanner',
if (!scannedCode) return; function(decodedText) {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
const normalizedIsbn = normalizeIsbnClient(scannedCode); const normalizedIsbn = normalizeIsbnClient(decodedText);
isbnField.value = normalizedIsbn || scannedCode; isbnField.value = normalizedIsbn || decodedText;
updateIsbnLiveValidation(); updateIsbnLiveValidation();
if (normalizedIsbn) {
setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
} else {
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
}
isbnScannerInstance.clear().catch(() => {}); if (normalizedIsbn) {
scannerBox.style.display = 'none'; setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
scanButton.textContent = 'ISBN scannen'; // Automatically load book metadata after a valid ISBN scan.
isbnScannerRunning = false; fetchBookInfo('upload');
} else {
if (normalizedIsbn) { setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
// Automatically load book metadata after a valid ISBN scan. }
fetchBookInfo('upload'); },
} 'Scanner läuft. Bitte ISBN-Code erfassen.',
}, () => {}); setIsbnScanStatus
);
} }
// Load predefined filter values for dropdowns // Load predefined filter values for dropdowns
@@ -1558,6 +1600,7 @@
}, 10000); }, 10000);
} }
// Exported function for use within parent layout fields
function validateCodeField(input, excludeId = null) { function validateCodeField(input, excludeId = null) {
const code = input.value.trim(); const code = input.value.trim();
@@ -1732,188 +1775,11 @@
if (typeof duplicateData.images === 'string') { if (typeof duplicateData.images === 'string') {
try { try {
duplicateData.images = JSON.parse(duplicateData.images); duplicateData.images = JSON.parse(duplicateData.images);
console.log("Parsed images from string:", duplicateData.images);
} catch (e) { } catch (e) {
console.warn("Failed to parse images string, using as single item:", duplicateData.images); console.error("Error parsing duplicate images array:", e);
duplicateData.images = [duplicateData.images];
} }
} }
console.log(`Final duplicateData.images (${Array.isArray(duplicateData.images) ? 'array' : typeof duplicateData.images}):`, duplicateData.images); console.log("Parsed duplicate images successfully.");
}
}
// 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();
} }
} }
</script> </script>