This commit is contained in:
+444
-449
@@ -1042,465 +1042,460 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function returnByCode(code) {
|
async function returnByCode(code) {
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
setScanStatus('Verarbeite Rückgabe...', 'warn');
|
setScanStatus('Verarbeite Rückgabe...', 'warn');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/library_return_by_code', {
|
const resp = await fetch('/api/library_return_by_code', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({item_code: code})
|
body: JSON.stringify({ item_code: code })
|
||||||
});
|
});
|
||||||
const result = await resp.json();
|
const result = await resp.json();
|
||||||
if (!resp.ok || !result.ok) {
|
if (!resp.ok || !result.ok) {
|
||||||
setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error');
|
setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error');
|
||||||
showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error');
|
showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok');
|
setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok');
|
||||||
showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok');
|
showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok');
|
||||||
await loadLibraryItems();
|
await loadLibraryItems();
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Return by code failed:', err);
|
console.error('Return by code failed:', err);
|
||||||
setScanStatus('Fehler bei Rückgabe.', 'error');
|
setScanStatus('Fehler bei Rückgabe.', 'error');
|
||||||
showSmallConfirm('Fehler bei Rückgabe.', 'error');
|
showSmallConfirm('Fehler bei Rückgabe.', 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (result.action === 'borrowed') {
|
||||||
setScanStatus('Verarbeite Mediencode...', 'warn');
|
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
|
||||||
const response = await fetch('/api/library_scan_action', {
|
showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok');
|
||||||
method: 'POST',
|
} else if (result.action === 'returned') {
|
||||||
headers: {'Content-Type': 'application/json'},
|
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
|
||||||
body: JSON.stringify({
|
showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok');
|
||||||
student_card_id: activeStudentCardId,
|
} else {
|
||||||
item_code: scannedCode
|
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
|
||||||
})
|
showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok');
|
||||||
});
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
await loadLibraryItems();
|
||||||
if (!response.ok || !result.ok) {
|
} catch (err) {
|
||||||
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
|
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) {
|
||||||
|
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
|
||||||
|
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
|
||||||
|
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();
|
||||||
|
let borrowCount = parseInt(countPrompt || '1', 10);
|
||||||
|
if (!Number.isFinite(borrowCount) || borrowCount < 1) {
|
||||||
|
borrowCount = 1;
|
||||||
|
}
|
||||||
|
if (borrowCount > maxAvailable) {
|
||||||
|
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';
|
||||||
|
durationField.name = 'borrow_duration_days';
|
||||||
|
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;
|
||||||
|
el.textContent = message;
|
||||||
|
el.classList.remove('ok', 'warn', 'error');
|
||||||
|
if (kind) el.classList.add(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSmallConfirm(message, kind='ok') {
|
||||||
|
// Append the small helper text in German
|
||||||
|
const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.';
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`;
|
||||||
|
el.innerHTML = `<div>${escapeHtml(String(message || ''))}</div><div style="opacity:0.9; margin-left:8px; font-size:0.85em;">${helper}</div><div class="close-x">×</div>`;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
// close handler
|
||||||
|
el.querySelector('.close-x').addEventListener('click', () => {
|
||||||
|
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||||
|
});
|
||||||
|
// auto remove after 3 seconds
|
||||||
|
setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveStudentCard(cardId) {
|
||||||
|
activeStudentCardId = (cardId || '').trim().toUpperCase();
|
||||||
|
const input = document.getElementById('activeStudentCard');
|
||||||
|
if (input) input.value = activeStudentCardId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeScannedCode(code) {
|
||||||
|
return (code || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemTypeLabel(type) {
|
||||||
|
const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch' };
|
||||||
|
return labels[type] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens the modal and fetches the data
|
||||||
|
function showItemDetail(itemId) {
|
||||||
|
const detailContent = document.getElementById('detailContent');
|
||||||
|
const detailModal = document.getElementById('detailModal');
|
||||||
|
|
||||||
|
// 1. Show the loading state immediately
|
||||||
|
detailContent.innerHTML = '<p>Loading details...</p>';
|
||||||
|
detailModal.style.display = 'flex';
|
||||||
|
|
||||||
|
// 2. Fetch the data
|
||||||
|
fetch(`/api/item_detail/${itemId}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(html => {
|
||||||
|
// 3. Clean the HTML and display it
|
||||||
|
detailContent.innerHTML = DOMPurify.sanitize(html);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error loading detail:', err);
|
||||||
|
detailContent.innerHTML = '<p>Sorry, we could not load the item details. Please try again later.</p>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the modal via the 'x' button
|
||||||
|
function closeDetailModal() {
|
||||||
|
document.getElementById('detailModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the modal if the user clicks the dark background overlay
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('detailModal');
|
||||||
|
// If the click happened directly on the dark overlay (not the white box)
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeDetailModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
|
||||||
|
// =========================================================================
|
||||||
|
function wireScannerUi() {
|
||||||
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
|
const resetBtn = document.getElementById('resetCardBtn');
|
||||||
|
const modeSelect = document.getElementById('scanModeSelect');
|
||||||
|
const keyboardToggle = document.getElementById('keyboardScannerToggle');
|
||||||
|
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.addEventListener('click', async () => {
|
||||||
|
if (scannerRunning) {
|
||||||
|
await stopScanner();
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn');
|
||||||
|
} else if (modeSelect.value === 'card_only') {
|
||||||
|
setScanStatus('Nur Ausweis-Modus aktiv.', 'warn');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardToggle) {
|
||||||
|
keyboardToggle.addEventListener('change', () => {
|
||||||
|
keyboardScannerEnabled = !!keyboardToggle.checked;
|
||||||
|
if (keyboardScannerEnabled) {
|
||||||
|
document.addEventListener('keydown', keyboardScanKeydownHandler);
|
||||||
|
setScanStatus('Physischer Scanner aktiv (Schnellmodus empfohlen).', 'ok');
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('keydown', keyboardScanKeydownHandler);
|
||||||
|
setScanStatus('Physischer Scanner deaktiviert.', 'warn');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run when DOM structure is entirely ready
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
wireScannerUi(); // Setup scanner control buttons
|
||||||
|
loadLibraryItems(); // Fetch your database items right away!
|
||||||
|
|
||||||
|
// Safely connect standard Filters and Search inputs inside DOMContentLoaded
|
||||||
|
const filterToggleBtn = document.getElementById('filterToggleBtn');
|
||||||
|
if (filterToggleBtn) {
|
||||||
|
filterToggleBtn.addEventListener('click', () => {
|
||||||
|
filterPanelOpen = !filterPanelOpen;
|
||||||
|
document.getElementById('filterPanel').classList.toggle('open');
|
||||||
|
filterToggleBtn.classList.toggle('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyFilterBtn = document.getElementById('applyFilterBtn');
|
||||||
|
if (applyFilterBtn) {
|
||||||
|
applyFilterBtn.addEventListener('click', () => {
|
||||||
|
activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
|
||||||
|
activeFilters.type = document.getElementById('filterType').value;
|
||||||
|
activeFilters.status = document.getElementById('filterStatus').value;
|
||||||
|
applyFiltersAndSearch(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilterBtn = document.getElementById('clearFilterBtn');
|
||||||
|
if (clearFilterBtn) {
|
||||||
|
clearFilterBtn.addEventListener('click', () => {
|
||||||
|
document.getElementById('filterISBN').value = '';
|
||||||
|
document.getElementById('filterType').value = '';
|
||||||
|
document.getElementById('filterStatus').value = '';
|
||||||
|
activeFilters = {isbn: '', type: '', status: ''};
|
||||||
|
applyFiltersAndSearch(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const librarySearch = document.getElementById('librarySearch');
|
||||||
|
if (librarySearch) {
|
||||||
|
librarySearch.addEventListener('input', (e) => {
|
||||||
|
currentSearchTerm = (e.target.value || '').toLowerCase();
|
||||||
|
applyFiltersAndSearch(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMoreBtn = document.getElementById('loadMoreBtn');
|
||||||
|
if (loadMoreBtn) {
|
||||||
|
loadMoreBtn.addEventListener('click', () => {
|
||||||
|
renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
|
||||||
|
renderItems();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const manualReturnBtn = document.getElementById('manualReturnBtn');
|
||||||
|
const manualItemCode = document.getElementById('manualItemCode');
|
||||||
|
if (manualReturnBtn && manualItemCode) {
|
||||||
|
manualReturnBtn.addEventListener('click', async () => {
|
||||||
|
const code = (manualItemCode.value || '').trim();
|
||||||
|
if (!code) {
|
||||||
|
alert('Bitte einen Mediencode eingeben.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await returnByCode(code);
|
||||||
if (result.action === 'borrowed') {
|
|
||||||
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
|
|
||||||
showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok');
|
|
||||||
} else if (result.action === 'returned') {
|
|
||||||
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
|
|
||||||
showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok');
|
|
||||||
} else {
|
|
||||||
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
|
|
||||||
showSmallConfirm(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';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// Edit Modal Form processing
|
||||||
// 4. UI INTERACTIONS & UTILITIES
|
const editForm = document.getElementById('editLibraryForm');
|
||||||
// =========================================================================
|
if (editForm) {
|
||||||
function borrowItem(itemId) {
|
try {
|
||||||
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
|
if (isGroupedEdit) {
|
||||||
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
|
const payload = {
|
||||||
alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
|
series_group_id: currentItem.SeriesGroupId,
|
||||||
return;
|
...sharedPayload,
|
||||||
}
|
items: groupMembers.map(member => ({
|
||||||
|
id: member._id,
|
||||||
const defaultCardId = activeStudentCardId || '';
|
code_4: codeByItemId.get(member._id) || ''
|
||||||
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();
|
|
||||||
let borrowCount = parseInt(countPrompt || '1', 10);
|
|
||||||
if (!Number.isFinite(borrowCount) || borrowCount < 1) {
|
|
||||||
borrowCount = 1;
|
|
||||||
}
|
|
||||||
if (borrowCount > maxAvailable) {
|
|
||||||
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';
|
|
||||||
durationField.name = 'borrow_duration_days';
|
|
||||||
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;
|
|
||||||
el.textContent = message;
|
|
||||||
el.classList.remove('ok', 'warn', 'error');
|
|
||||||
if (kind) el.classList.add(kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSmallConfirm(message, kind = 'ok') {
|
|
||||||
// Append the small helper text in German
|
|
||||||
const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.';
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`;
|
|
||||||
el.innerHTML = `<div>${escapeHtml(String(message || ''))}</div><div style="opacity:0.9; margin-left:8px; font-size:0.85em;">${helper}</div><div class="close-x">×</div>`;
|
|
||||||
document.body.appendChild(el);
|
|
||||||
// close handler
|
|
||||||
el.querySelector('.close-x').addEventListener('click', () => {
|
|
||||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
||||||
});
|
|
||||||
// auto remove after 3 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActiveStudentCard(cardId) {
|
|
||||||
activeStudentCardId = (cardId || '').trim().toUpperCase();
|
|
||||||
const input = document.getElementById('activeStudentCard');
|
|
||||||
if (input) input.value = activeStudentCardId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeScannedCode(code) {
|
|
||||||
return (code || '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemTypeLabel(type) {
|
|
||||||
const labels = {'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch'};
|
|
||||||
return labels[type] || type;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opens the modal and fetches the data
|
|
||||||
function showItemDetail(itemId) {
|
|
||||||
const detailContent = document.getElementById('detailContent');
|
|
||||||
const detailModal = document.getElementById('detailModal');
|
|
||||||
|
|
||||||
// 1. Show the loading state immediately
|
|
||||||
detailContent.innerHTML = '<p>Loading details...</p>';
|
|
||||||
detailModal.style.display = 'flex';
|
|
||||||
|
|
||||||
// 2. Fetch the data
|
|
||||||
fetch(`/api/item_detail/${itemId}`)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then(html => {
|
|
||||||
// 3. Clean the HTML and display it
|
|
||||||
detailContent.innerHTML = DOMPurify.sanitize(html);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Error loading detail:', err);
|
|
||||||
detailContent.innerHTML = '<p>Sorry, we could not load the item details. Please try again later.</p>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closes the modal via the 'x' button
|
|
||||||
function closeDetailModal() {
|
|
||||||
document.getElementById('detailModal').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closes the modal if the user clicks the dark background overlay
|
|
||||||
window.onclick = function (event) {
|
|
||||||
const modal = document.getElementById('detailModal');
|
|
||||||
// If the click happened directly on the dark overlay (not the white box)
|
|
||||||
if (event.target === modal) {
|
|
||||||
closeDetailModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
|
|
||||||
// =========================================================================
|
|
||||||
function wireScannerUi() {
|
|
||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
|
||||||
const resetBtn = document.getElementById('resetCardBtn');
|
|
||||||
const modeSelect = document.getElementById('scanModeSelect');
|
|
||||||
const keyboardToggle = document.getElementById('keyboardScannerToggle');
|
|
||||||
|
|
||||||
if (toggleBtn) {
|
|
||||||
toggleBtn.addEventListener('click', async () => {
|
|
||||||
if (scannerRunning) {
|
|
||||||
await stopScanner();
|
|
||||||
} else {
|
|
||||||
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) {
|
|
||||||
setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn');
|
|
||||||
} else if (modeSelect.value === 'card_only') {
|
|
||||||
setScanStatus('Nur Ausweis-Modus aktiv.', 'warn');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyboardToggle) {
|
|
||||||
keyboardToggle.addEventListener('change', () => {
|
|
||||||
keyboardScannerEnabled = !!keyboardToggle.checked;
|
|
||||||
if (keyboardScannerEnabled) {
|
|
||||||
document.addEventListener('keydown', keyboardScanKeydownHandler);
|
|
||||||
setScanStatus('Physischer Scanner aktiv (Schnellmodus empfohlen).', 'ok');
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('keydown', keyboardScanKeydownHandler);
|
|
||||||
setScanStatus('Physischer Scanner deaktiviert.', 'warn');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run when DOM structure is entirely ready
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
wireScannerUi(); // Setup scanner control buttons
|
|
||||||
loadLibraryItems(); // Fetch your database items right away!
|
|
||||||
|
|
||||||
// Safely connect standard Filters and Search inputs inside DOMContentLoaded
|
|
||||||
const filterToggleBtn = document.getElementById('filterToggleBtn');
|
|
||||||
if (filterToggleBtn) {
|
|
||||||
filterToggleBtn.addEventListener('click', () => {
|
|
||||||
filterPanelOpen = !filterPanelOpen;
|
|
||||||
document.getElementById('filterPanel').classList.toggle('open');
|
|
||||||
filterToggleBtn.classList.toggle('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyFilterBtn = document.getElementById('applyFilterBtn');
|
|
||||||
if (applyFilterBtn) {
|
|
||||||
applyFilterBtn.addEventListener('click', () => {
|
|
||||||
activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
|
|
||||||
activeFilters.type = document.getElementById('filterType').value;
|
|
||||||
activeFilters.status = document.getElementById('filterStatus').value;
|
|
||||||
applyFiltersAndSearch(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearFilterBtn = document.getElementById('clearFilterBtn');
|
|
||||||
if (clearFilterBtn) {
|
|
||||||
clearFilterBtn.addEventListener('click', () => {
|
|
||||||
document.getElementById('filterISBN').value = '';
|
|
||||||
document.getElementById('filterType').value = '';
|
|
||||||
document.getElementById('filterStatus').value = '';
|
|
||||||
activeFilters = {isbn: '', type: '', status: ''};
|
|
||||||
applyFiltersAndSearch(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const librarySearch = document.getElementById('librarySearch');
|
|
||||||
if (librarySearch) {
|
|
||||||
librarySearch.addEventListener('input', (e) => {
|
|
||||||
currentSearchTerm = (e.target.value || '').toLowerCase();
|
|
||||||
applyFiltersAndSearch(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMoreBtn = document.getElementById('loadMoreBtn');
|
|
||||||
if (loadMoreBtn) {
|
|
||||||
loadMoreBtn.addEventListener('click', () => {
|
|
||||||
renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
|
|
||||||
renderItems();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const manualReturnBtn = document.getElementById('manualReturnBtn');
|
|
||||||
const manualItemCode = document.getElementById('manualItemCode');
|
|
||||||
if (manualReturnBtn && manualItemCode) {
|
|
||||||
manualReturnBtn.addEventListener('click', async () => {
|
|
||||||
const code = (manualItemCode.value || '').trim();
|
|
||||||
if (!code) {
|
|
||||||
alert('Bitte einen Mediencode eingeben.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await returnByCode(code);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const editForm = document.getElementById('editLibraryForm');
|
|
||||||
if (editForm) {
|
|
||||||
editForm.addEventListener('submit', async function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const itemId = document.getElementById('editLibraryItemId').value;
|
|
||||||
const currentItem = libraryItems.find(i => i._id === itemId);
|
|
||||||
if (!currentItem) return;
|
|
||||||
|
|
||||||
const sharedPayload = {
|
|
||||||
name: document.getElementById('editLibraryName').value,
|
|
||||||
item_type: document.getElementById('editLibraryType').value,
|
|
||||||
isbn: document.getElementById('editLibraryIsbn').value,
|
|
||||||
ort: document.getElementById('editLibraryLocation').value,
|
|
||||||
beschreibung: document.getElementById('editLibraryDescription').value,
|
|
||||||
ansch_jahr: currentItem.Anschaffungsjahr || '',
|
|
||||||
ansch_kost: currentItem.Anschaffungskosten || '',
|
|
||||||
reservierbar: currentItem.Reservierbar !== false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]'));
|
const response = await fetch('/update_group', {
|
||||||
const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()]));
|
method: 'POST',
|
||||||
const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem];
|
headers: {
|
||||||
const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1;
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}',
|
||||||
|
'X-CSRF-Token': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
const result = await response.json();
|
||||||
if (isGroupedEdit) {
|
if (response.ok && result.success) {
|
||||||
const payload = {
|
alert(result.message || 'Gruppe erfolgreich aktualisiert!');
|
||||||
series_group_id: currentItem.SeriesGroupId,
|
closeEditLibraryModal();
|
||||||
...sharedPayload,
|
pagingState.loading = false;
|
||||||
items: groupMembers.map(member => ({
|
await loadLibraryItems();
|
||||||
id: member._id,
|
} else {
|
||||||
code_4: codeByItemId.get(member._id) || ''
|
alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.');
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch('/update_group', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': '{{ csrf_token }}',
|
|
||||||
'X-CSRF-Token': '{{ csrf_token }}'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (response.ok && result.success) {
|
|
||||||
alert(result.message || 'Gruppe erfolgreich aktualisiert!');
|
|
||||||
closeEditLibraryModal();
|
|
||||||
pagingState.loading = false;
|
|
||||||
await loadLibraryItems();
|
|
||||||
} else {
|
|
||||||
alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const primaryCodeInput = codeInputs[0];
|
|
||||||
const payload = {
|
|
||||||
name: sharedPayload.name,
|
|
||||||
item_type: sharedPayload.item_type,
|
|
||||||
isbn: sharedPayload.isbn,
|
|
||||||
code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(),
|
|
||||||
ort: sharedPayload.ort,
|
|
||||||
beschreibung: sharedPayload.beschreibung
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/api/library_item/${itemId}/update`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': '{{ csrf_token }}',
|
|
||||||
'X-CSRF-Token': '{{ csrf_token }}'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (response.ok && result.ok) {
|
|
||||||
alert(result.message || 'Medium erfolgreich aktualisiert!');
|
|
||||||
closeEditLibraryModal();
|
|
||||||
pagingState.loading = false;
|
|
||||||
await loadLibraryItems();
|
|
||||||
} else {
|
|
||||||
alert(result.message || 'Fehler beim Speichern der Änderungen.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update failed:', error);
|
|
||||||
alert('Netzwerkfehler beim Aktualisieren des Mediums.');
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
}
|
const primaryCodeInput = codeInputs[0];
|
||||||
});
|
const payload = {
|
||||||
|
name: sharedPayload.name,
|
||||||
|
item_type: sharedPayload.item_type,
|
||||||
|
isbn: sharedPayload.isbn,
|
||||||
|
code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(),
|
||||||
|
ort: sharedPayload.ort,
|
||||||
|
beschreibung: sharedPayload.beschreibung
|
||||||
|
};
|
||||||
|
|
||||||
async function fetchLibraryGroupMembers(seriesGroupId) {
|
const response = await fetch(`/api/library_item/${itemId}/update`, {
|
||||||
if (!seriesGroupId) return [];
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}',
|
||||||
|
'X-CSRF-Token': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok && result.ok) {
|
||||||
|
alert(result.message || 'Medium erfolgreich aktualisiert!');
|
||||||
|
closeEditLibraryModal();
|
||||||
|
pagingState.loading = false;
|
||||||
|
await loadLibraryItems();
|
||||||
|
} else {
|
||||||
|
alert(result.message || 'Fehler beim Speichern der Änderungen.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update failed:', error);
|
||||||
|
alert('Netzwerkfehler beim Aktualisieren des Mediums.');
|
||||||
|
}
|
||||||
|
editForm.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const itemId = document.getElementById('editLibraryItemId').value;
|
||||||
|
const currentItem = libraryItems.find(i => i._id === itemId);
|
||||||
|
if (!currentItem) return;
|
||||||
|
|
||||||
|
const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]'));
|
||||||
|
|
||||||
|
const sharedPayload = {
|
||||||
|
name: document.getElementById('editLibraryName').value,
|
||||||
|
item_type: document.getElementById('editLibraryType').value,
|
||||||
|
isbn: document.getElementById('editLibraryIsbn').value,
|
||||||
|
ort: document.getElementById('editLibraryLocation').value,
|
||||||
|
beschreibung: document.getElementById('editLibraryDescription').value,
|
||||||
|
ansch_jahr: currentItem.Anschaffungsjahr || '',
|
||||||
|
ansch_kost: currentItem.Anschaffungskosten || '',
|
||||||
|
reservierbar: currentItem.Reservierbar !== false,
|
||||||
|
};
|
||||||
|
const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()]));
|
||||||
|
const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem];
|
||||||
|
const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchLibraryGroupMembers(seriesGroupId) {
|
||||||
|
if (!seriesGroupId) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`);
|
const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`);
|
||||||
@@ -1595,9 +1590,9 @@
|
|||||||
document.getElementById('editLibraryModal').style.display = 'flex';
|
document.getElementById('editLibraryModal').style.display = 'flex';
|
||||||
};
|
};
|
||||||
|
|
||||||
function closeEditLibraryModal() {
|
function closeEditLibraryModal() {
|
||||||
document.getElementById('editLibraryModal').style.display = 'none';
|
document.getElementById('editLibraryModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user