Slight fixes of thew Code scanning and Items processing

This commit is contained in:
2026-06-07 12:42:27 +02:00
parent 042afc19e2
commit 116261d334
+124 -229
View File
@@ -549,9 +549,12 @@
</div> </div>
</div> </div>
<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 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 = [];
@@ -573,13 +576,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) {
@@ -588,7 +598,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;
@@ -627,7 +636,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) {
@@ -679,7 +687,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');
@@ -697,8 +704,7 @@
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');
@@ -724,10 +730,8 @@
${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</button>` : ''} ${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</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:#dc2626;color:#fff; margin-left:6px;" onclick="confirmDeleteLibraryItem('${item._id}')">Löschen</button>` : ''}
</td> </td>
</tr> </tr>`;
`; }).join('');
})()}
`).join('');
const canLoadMoreRows = renderedCount < visibleItems.length; const canLoadMoreRows = renderedCount < visibleItems.length;
if (canLoadMoreRows) { if (canLoadMoreRows) {
@@ -742,13 +746,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';
@@ -772,7 +769,6 @@
} }
loadLibraryItems(); loadLibraryItems();
} else { } else {
// Server redirected to HTML (flash messages). Reload to show state.
location.reload(); location.reload();
} }
} catch (e) { } catch (e) {
@@ -783,90 +779,19 @@
} }
} }
// 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');
div.textContent = text;
return div.innerHTML;
}
function showItemDetail(itemId) {
// Load and show detail modal
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));
}
function closeDetailModal() {
document.getElementById('detailModal').style.display = 'none';
}
// Keep track of the scanner state globally/outer scope
let scannerRunning = false; // Maps directly to your 'isScanning' flag style
let activeScannerCallback = null; // Dynamically routes the scanned barcode to the caller
let lastScanValue = null;
let lastScanAt = 0;
let activeStudentCardId = ''; // Managed globally by your system
// ========================================================================= // =========================================================================
// 1. CORE SCANNER ROUTING ENGINE (QUAGGA2) // 3. CORE SCANNER ROUTING ENGINE (QUAGGA2)
// ========================================================================= // =========================================================================
function startScanner(targetCallback) { function startScanner(targetCallback) {
// if (scannerRunning) return;
const readerWrap = document.getElementById('scanReaderWrap'); const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn'); const toggleBtn = document.getElementById('toggleScannerBtn');
// Store the custom action that should happen when a barcode is found
activeScannerCallback = targetCallback; activeScannerCallback = targetCallback;
if (readerWrap) readerWrap.style.display = 'block'; if (readerWrap) readerWrap.style.display = 'block';
@@ -876,12 +801,11 @@
inputStream: { inputStream: {
name: "Live", name: "Live",
type: "LiveStream", type: "LiveStream",
// Targets your interior relative viewport box
target: document.querySelector('#library-scanner-container'), target: document.querySelector('#library-scanner-container'),
constraints: { constraints: {
width: 640, width: 640,
height: 480, height: 480,
facingMode: "environment" // Force rear camera facingMode: "environment"
}, },
}, },
decoder: { decoder: {
@@ -906,7 +830,6 @@
Quagga.start(); Quagga.start();
scannerRunning = true; scannerRunning = true;
// Update the main toggle button text if no specific edit callback is provided
if (!targetCallback && toggleBtn) { if (!targetCallback && toggleBtn) {
toggleBtn.textContent = 'Scanner stoppen'; toggleBtn.textContent = 'Scanner stoppen';
} }
@@ -921,56 +844,34 @@
const toggleBtn = document.getElementById('toggleScannerBtn'); const toggleBtn = document.getElementById('toggleScannerBtn');
Quagga.stop(); Quagga.stop();
scannerRunning = false; scannerRunning = false;
activeScannerCallback = null; // Clear out callback routing activeScannerCallback = null;
if (readerWrap) readerWrap.style.display = 'none'; if (readerWrap) readerWrap.style.display = 'none';
if (toggleBtn) toggleBtn.textContent = 'Scanner starten'; if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
setScanStatus('Scanner gestoppt.', 'warn'); setScanStatus('Scanner gestoppt.', 'warn');
} }
// Unified global reader callback routing data based on who opened the scanner
Quagga.onDetected(function(data) { Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return; if (!data || !data.codeResult || !data.codeResult.code) return;
const barcode = String(data.codeResult.code || '').trim(); const barcode = String(data.codeResult.code || '').trim();
console.log("Barcode detected:", barcode); console.log("Barcode detected:", barcode);
// Keep a local reference to the active callback before stopping the engine
const currentCallback = activeScannerCallback; const currentCallback = activeScannerCallback;
// Critical: Stop scanning immediately to prevent multiple triggers
stopScanner(); stopScanner();
// 1. If an explicit modal/edit function asked for the scan, route to it directly
if (typeof currentCallback === "function") { if (typeof currentCallback === "function") {
currentCallback(barcode); currentCallback(barcode);
} else { } else {
// 2. Fallback Default: Pass it to your library dashboard's primary business processing
handleScanSuccess(barcode); handleScanSuccess(barcode);
} }
}); });
// Event Listener for the main standalone library processing scanner button
document.getElementById('toggleScannerBtn')?.addEventListener('click', async function() {
if (scannerRunning) {
await stopScanner();
} else {
await startScanner(null); // Passing null flags it to run default library processing
}
});
// =========================================================================
// 2. LIBRARY WORKFLOW AND MODAL IMPLEMENTATIONS
// =========================================================================
function handleScanSuccess(decodedText) { function handleScanSuccess(decodedText) {
const scannedCode = normalizeScannedCode(decodedText); const scannedCode = normalizeScannedCode(decodedText);
if (!scannedCode) return; if (!scannedCode) return;
// Debounce checks
const now = Date.now(); const now = Date.now();
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) { if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
return; return;
@@ -988,6 +889,45 @@
processQuickToggleScan(scannedCode); 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() { function scanIntoEditCode() {
const scanReaderWrap = document.getElementById('scanReaderWrap'); const scanReaderWrap = document.getElementById('scanReaderWrap');
const editCodeInput = document.getElementById('edit-code4'); const editCodeInput = document.getElementById('edit-code4');
@@ -996,7 +936,6 @@
return; return;
} }
// Toggle close if it's already running
if (scannerRunning && scanReaderWrap.style.display !== 'none') { if (scannerRunning && scanReaderWrap.style.display !== 'none') {
stopScanner(); stopScanner();
scanEditBtn.textContent = 'Barcode scannen'; scanEditBtn.textContent = 'Barcode scannen';
@@ -1005,14 +944,18 @@
scanEditBtn.textContent = 'Scanner schließen'; scanEditBtn.textContent = 'Scanner schließen';
// Launch the scanner with custom callback routing to the item edit fields
startScanner(function(decodedText) { startScanner(function(decodedText) {
editCodeInput.value = decodedText; editCodeInput.value = decodedText;
if(typeof validateCodeField === "function") {
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null); validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
}
scanEditBtn.textContent = 'Barcode scannen'; 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') {
@@ -1086,62 +1029,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 { function escapeHtml(text) {
setScanStatus('Verarbeite Mediencode...', 'warn'); const div = document.createElement('div');
const response = await fetch('/api/library_scan_action', { div.textContent = text;
method: 'POST', return div.innerHTML;
headers: {'Content-Type': 'application/json'}, }
body: JSON.stringify({
student_card_id: activeStudentCardId, function showItemDetail(itemId) {
item_code: scannedCode 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));
const result = await response.json();
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return;
} }
if (result.action === 'borrowed') { function closeDetailModal() {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok'); document.getElementById('detailModal').style.display = 'none';
} 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');
}
} }
// =========================================================================
// 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');
@@ -1169,83 +1097,50 @@
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');
function openEditLibraryItem(itemId) { document.getElementById('filterToggleBtn').classList.toggle('active');
const item = getLibraryItemById(itemId);
if (!item) {
alert('Element nicht gefunden.');
return;
}
const modal = document.getElementById('editLibraryModal');
document.getElementById('editLibraryItemId').value = item._id || '';
document.getElementById('editLibraryName').value = item.Name || '';
document.getElementById('editLibraryAuthor').value = item.Autor || item.Author || '';
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
document.getElementById('editLibraryLocation').value = item.Ort || '';
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() {
const modal = document.getElementById('editLibraryModal');
modal.style.display = 'none';
}
async function submitEditLibraryItem(event) {
event.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value;
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 {
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.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);
});
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);
});
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();
});
// Run when DOM structure is entirely ready
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>