Compare commits

...

4 Commits

+194 -123
View File
@@ -445,7 +445,7 @@
<!-- Customizable Filter Panel -->
<!-- Student card / quick scan workflow -->
<div class="library-scan-panel">
<div class="library-scan-panel">
<div class="library-scan-controls">
<select id="scanModeSelect" aria-label="Scan-Modus">
<option value="card_only">Nur Ausweis erfassen</option>
@@ -458,8 +458,10 @@
<div id="scanStatus" class="library-scan-status">
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
</div>
<div id="scanReaderWrap" class="library-scan-reader-wrap">
<div id="libraryQrReader" class="library-scan-reader"></div>
<div id="scanReaderWrap" class="library-scan-reader-wrap" style="display: none;">
<div id="libraryQrReader" class="library-scan-reader">
<div id="library-scanner-container"></div>
</div>
</div>
</div>
<div id="filterPanel" class="library-filter-panel">
@@ -847,28 +849,186 @@
document.getElementById('detailModal').style.display = 'none';
}
// Keep track of the scanner state globally/outer scope
let scannerRunning = false;
// 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)
// =========================================================================
function startScanner(targetCallback) {
// if (scannerRunning) return;
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
// Store the custom action that should happen when a barcode is found
activeScannerCallback = targetCallback;
if (readerWrap) readerWrap.style.display = 'block';
setScanStatus('Initializing camera...', 'warn');
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
// Targets your interior relative viewport box
target: document.querySelector('#library-scanner-container'),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Force rear camera
},
},
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;
// Update the main toggle button text if no specific edit callback is provided
if (!targetCallback && toggleBtn) {
toggleBtn.textContent = 'Scanner stoppen';
}
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
});
}
function stopScanner() {
if (!scannerRunning) return;
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
Quagga.stop();
scannerRunning = false;
activeScannerCallback = null; // Clear out callback routing
if (readerWrap) readerWrap.style.display = 'none';
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
setScanStatus('Scanner gestoppt.', 'warn');
}
// Unified global reader callback routing data based on who opened the scanner
Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return;
const barcode = String(data.codeResult.code || '').trim();
console.log("Barcode detected:", barcode);
// Keep a local reference to the active callback before stopping the engine
const currentCallback = activeScannerCallback;
// Critical: Stop scanning immediately to prevent multiple triggers
stopScanner();
// 1. If an explicit modal/edit function asked for the scan, route to it directly
if (typeof currentCallback === "function") {
currentCallback(barcode);
} else {
// 2. Fallback Default: Pass it to your library dashboard's primary business processing
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) {
const scannedCode = normalizeScannedCode(decodedText);
if (!scannedCode) return;
// Debounce checks
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 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;
}
// Toggle close if it's already running
if (scannerRunning && scanReaderWrap.style.display !== 'none') {
stopScanner();
scanEditBtn.textContent = 'Barcode scannen';
return;
}
scanEditBtn.textContent = 'Scanner schließen';
// Launch the scanner with custom callback routing to the item edit fields
startScanner(function(decodedText) {
editCodeInput.value = decodedText;
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
scanEditBtn.textContent = 'Barcode scannen';
});
}
function borrowItem(itemId) {
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
alert('Dieses Medium ist als defekt/zerstör markiert und kann nicht ausgeliehen werden.');
alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
return;
}
const defaultCardId = activeStudentCardId || '';
const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase();
if (!cardId) {
alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.');
return;
}
setActiveStudentCard(cardId);
const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim();
const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1);
const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim();
@@ -880,29 +1040,29 @@
alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = `/ausleihen/${itemId}`;
const csrfField = document.createElement('input');
csrfField.type = 'hidden';
csrfField.name = 'csrf_token';
csrfField.value = '{{ csrf_token }}';
form.appendChild(csrfField);
const cardField = document.createElement('input');
cardField.type = 'hidden';
cardField.name = 'borrower_card_id';
cardField.value = cardId;
form.appendChild(cardField);
const returnTargetField = document.createElement('input');
returnTargetField.type = 'hidden';
returnTargetField.name = 'return_to';
returnTargetField.value = 'library';
form.appendChild(returnTargetField);
if (durationInput) {
const durationField = document.createElement('input');
durationField.type = 'hidden';
@@ -910,17 +1070,17 @@
durationField.value = durationInput;
form.appendChild(durationField);
}
const countField = document.createElement('input');
countField.type = 'hidden';
countField.name = 'exemplare_count';
countField.value = String(borrowCount || 1);
form.appendChild(countField);
document.body.appendChild(form);
form.submit();
}
function setScanStatus(message, kind = '') {
const el = document.getElementById('scanStatus');
if (!el) return;
@@ -930,7 +1090,7 @@
el.classList.add(kind);
}
}
function setActiveStudentCard(cardId) {
activeStudentCardId = (cardId || '').trim().toUpperCase();
const input = document.getElementById('activeStudentCard');
@@ -938,18 +1098,18 @@
input.value = activeStudentCardId;
}
}
function normalizeScannedCode(code) {
return (code || '').trim();
}
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', {
@@ -960,13 +1120,13 @@
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') {
@@ -974,125 +1134,36 @@
} 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) {
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);
}
// Global Quagga reader hook
Quagga.onDetected(function(data) {
if (data && data.codeResult && data.codeResult.code) {
handleScanSuccess(data.codeResult.code);
}
});
async function startScanner() {
if (scannerRunning) return;
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
if (readerWrap) readerWrap.style.display = 'block';
setScanStatus('Initializing camera...', 'warn');
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
// Points to the interior viewport layout box
target: document.querySelector('#libraryQrReader'),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Force rear mobile lenses
},
},
decoder: {
// Limited down precisely to standard library/ID configurations
readers: [
"code_128_reader",
"ean_reader",
"code_39_reader",
"upc_reader",
"codabar_reader",
"i2of5_reader" // Matches original 'ITF' format mapping
]
}
}, 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 (toggleBtn) toggleBtn.textContent = 'Scanner stoppen';
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
});
}
async function stopScanner() {
if (!scannerRunning) return;
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
Quagga.stop();
scannerRunning = false;
if (readerWrap) readerWrap.style.display = 'none';
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
setScanStatus('Scanner gestoppt.', 'warn');
}
function wireScannerUi() {
const toggleBtn = document.getElementById('toggleScannerBtn');
const resetBtn = document.getElementById('resetCardBtn');
const modeSelect = document.getElementById('scanModeSelect');
if (toggleBtn) {
toggleBtn.addEventListener('click', async () => {
if (scannerRunning) {
await stopScanner();
} else {
await startScanner();
await startScanner(null);
}
});
}
if (resetBtn) {
resetBtn.addEventListener('click', () => {
setActiveStudentCard('');
setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn');
});
}
if (modeSelect) {
modeSelect.addEventListener('change', () => {
if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) {
@@ -1105,18 +1176,18 @@
});
}
}
function getLibraryItemById(itemId) {
return libraryItems.find(i => String(i._id) === String(itemId));
}
function openEditLibraryItem(itemId) {
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 || '';
@@ -1128,7 +1199,7 @@
document.getElementById('editLibraryType').value = (item.ItemType || 'book');
modal.style.display = 'flex';
}
function closeEditLibraryModal() {
const modal = document.getElementById('editLibraryModal');
modal.style.display = 'none';