|
|
|
@@ -614,6 +614,199 @@
|
|
|
|
|
</div>
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// CUSTOM MODAL HELPERS (Replaces native browser alert, confirm & prompt)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
function createModalOverlay() {
|
|
|
|
|
const overlay = document.createElement('div');
|
|
|
|
|
overlay.style.position = 'fixed';
|
|
|
|
|
overlay.style.top = '0';
|
|
|
|
|
overlay.style.left = '0';
|
|
|
|
|
overlay.style.width = '100vw';
|
|
|
|
|
overlay.style.height = '100vh';
|
|
|
|
|
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
|
|
|
|
overlay.style.display = 'flex';
|
|
|
|
|
overlay.style.alignItems = 'center';
|
|
|
|
|
overlay.style.justifyContent = 'center';
|
|
|
|
|
overlay.style.zIndex = '999999';
|
|
|
|
|
return overlay;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createModalBox() {
|
|
|
|
|
const box = document.createElement('div');
|
|
|
|
|
box.style.backgroundColor = '#fff';
|
|
|
|
|
box.style.padding = '24px';
|
|
|
|
|
box.style.borderRadius = '8px';
|
|
|
|
|
box.style.boxShadow = '0 10px 25px rgba(0,0,0,0.2)';
|
|
|
|
|
box.style.fontFamily = 'sans-serif';
|
|
|
|
|
box.style.minWidth = '320px';
|
|
|
|
|
box.style.maxWidth = '90%';
|
|
|
|
|
box.style.textAlign = 'left';
|
|
|
|
|
box.style.color = '#333';
|
|
|
|
|
return box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function customAlert(message) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const overlay = createModalOverlay();
|
|
|
|
|
const box = createModalBox();
|
|
|
|
|
|
|
|
|
|
const text = document.createElement('p');
|
|
|
|
|
text.textContent = message;
|
|
|
|
|
text.style.marginBottom = '20px';
|
|
|
|
|
text.style.lineHeight = '1.5';
|
|
|
|
|
text.style.whiteSpace = 'pre-wrap';
|
|
|
|
|
|
|
|
|
|
const btnContainer = document.createElement('div');
|
|
|
|
|
btnContainer.style.display = 'flex';
|
|
|
|
|
btnContainer.style.justifyContent = 'flex-end';
|
|
|
|
|
|
|
|
|
|
const btn = document.createElement('button');
|
|
|
|
|
btn.textContent = 'OK';
|
|
|
|
|
btn.style.padding = '8px 16px';
|
|
|
|
|
btn.style.cursor = 'pointer';
|
|
|
|
|
btn.style.border = 'none';
|
|
|
|
|
btn.style.backgroundColor = '#007bff';
|
|
|
|
|
btn.style.color = '#fff';
|
|
|
|
|
btn.style.borderRadius = '4px';
|
|
|
|
|
|
|
|
|
|
btn.onclick = () => {
|
|
|
|
|
document.body.removeChild(overlay);
|
|
|
|
|
resolve();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
btnContainer.appendChild(btn);
|
|
|
|
|
box.appendChild(text);
|
|
|
|
|
box.appendChild(btnContainer);
|
|
|
|
|
overlay.appendChild(box);
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
btn.focus();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function customConfirm(message) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const overlay = createModalOverlay();
|
|
|
|
|
const box = createModalBox();
|
|
|
|
|
|
|
|
|
|
const text = document.createElement('p');
|
|
|
|
|
text.textContent = message;
|
|
|
|
|
text.style.marginBottom = '20px';
|
|
|
|
|
text.style.lineHeight = '1.5';
|
|
|
|
|
text.style.whiteSpace = 'pre-wrap';
|
|
|
|
|
|
|
|
|
|
const btnContainer = document.createElement('div');
|
|
|
|
|
btnContainer.style.display = 'flex';
|
|
|
|
|
btnContainer.style.justifyContent = 'flex-end';
|
|
|
|
|
btnContainer.style.gap = '10px';
|
|
|
|
|
|
|
|
|
|
const cancelBtn = document.createElement('button');
|
|
|
|
|
cancelBtn.textContent = 'Abbrechen';
|
|
|
|
|
cancelBtn.style.padding = '8px 16px';
|
|
|
|
|
cancelBtn.style.cursor = 'pointer';
|
|
|
|
|
cancelBtn.style.border = '1px solid #ccc';
|
|
|
|
|
cancelBtn.style.backgroundColor = '#f8f9fa';
|
|
|
|
|
cancelBtn.style.color = '#333';
|
|
|
|
|
cancelBtn.style.borderRadius = '4px';
|
|
|
|
|
|
|
|
|
|
const okBtn = document.createElement('button');
|
|
|
|
|
okBtn.textContent = 'OK';
|
|
|
|
|
okBtn.style.padding = '8px 16px';
|
|
|
|
|
okBtn.style.cursor = 'pointer';
|
|
|
|
|
okBtn.style.border = 'none';
|
|
|
|
|
okBtn.style.backgroundColor = '#007bff';
|
|
|
|
|
okBtn.style.color = '#fff';
|
|
|
|
|
okBtn.style.borderRadius = '4px';
|
|
|
|
|
|
|
|
|
|
const closeAndResolve = (val) => {
|
|
|
|
|
document.body.removeChild(overlay);
|
|
|
|
|
resolve(val);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cancelBtn.onclick = () => closeAndResolve(false);
|
|
|
|
|
okBtn.onclick = () => closeAndResolve(true);
|
|
|
|
|
|
|
|
|
|
btnContainer.appendChild(cancelBtn);
|
|
|
|
|
btnContainer.appendChild(okBtn);
|
|
|
|
|
box.appendChild(text);
|
|
|
|
|
box.appendChild(btnContainer);
|
|
|
|
|
overlay.appendChild(box);
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
okBtn.focus();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function customPrompt(message, defaultValue = '') {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const overlay = createModalOverlay();
|
|
|
|
|
const box = createModalBox();
|
|
|
|
|
|
|
|
|
|
const text = document.createElement('p');
|
|
|
|
|
text.textContent = message;
|
|
|
|
|
text.style.marginBottom = '15px';
|
|
|
|
|
text.style.lineHeight = '1.5';
|
|
|
|
|
text.style.whiteSpace = 'pre-wrap';
|
|
|
|
|
|
|
|
|
|
const input = document.createElement('input');
|
|
|
|
|
input.type = 'text';
|
|
|
|
|
input.value = defaultValue;
|
|
|
|
|
input.style.width = '100%';
|
|
|
|
|
input.style.marginBottom = '20px';
|
|
|
|
|
input.style.padding = '10px';
|
|
|
|
|
input.style.boxSizing = 'border-box';
|
|
|
|
|
input.style.border = '1px solid #ccc';
|
|
|
|
|
input.style.borderRadius = '4px';
|
|
|
|
|
|
|
|
|
|
const btnContainer = document.createElement('div');
|
|
|
|
|
btnContainer.style.display = 'flex';
|
|
|
|
|
btnContainer.style.justifyContent = 'flex-end';
|
|
|
|
|
btnContainer.style.gap = '10px';
|
|
|
|
|
|
|
|
|
|
const cancelBtn = document.createElement('button');
|
|
|
|
|
cancelBtn.textContent = 'Abbrechen';
|
|
|
|
|
cancelBtn.style.padding = '8px 16px';
|
|
|
|
|
cancelBtn.style.cursor = 'pointer';
|
|
|
|
|
cancelBtn.style.border = '1px solid #ccc';
|
|
|
|
|
cancelBtn.style.backgroundColor = '#f8f9fa';
|
|
|
|
|
cancelBtn.style.color = '#333';
|
|
|
|
|
cancelBtn.style.borderRadius = '4px';
|
|
|
|
|
|
|
|
|
|
const okBtn = document.createElement('button');
|
|
|
|
|
okBtn.textContent = 'OK';
|
|
|
|
|
okBtn.style.padding = '8px 16px';
|
|
|
|
|
okBtn.style.cursor = 'pointer';
|
|
|
|
|
okBtn.style.border = 'none';
|
|
|
|
|
okBtn.style.backgroundColor = '#007bff';
|
|
|
|
|
okBtn.style.color = '#fff';
|
|
|
|
|
okBtn.style.borderRadius = '4px';
|
|
|
|
|
|
|
|
|
|
const closeAndResolve = (val) => {
|
|
|
|
|
document.body.removeChild(overlay);
|
|
|
|
|
resolve(val);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cancelBtn.onclick = () => closeAndResolve(null);
|
|
|
|
|
okBtn.onclick = () => closeAndResolve(input.value);
|
|
|
|
|
|
|
|
|
|
input.onkeydown = (e) => {
|
|
|
|
|
if (e.key === 'Enter') closeAndResolve(input.value);
|
|
|
|
|
if (e.key === 'Escape') closeAndResolve(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
btnContainer.appendChild(cancelBtn);
|
|
|
|
|
btnContainer.appendChild(okBtn);
|
|
|
|
|
|
|
|
|
|
box.appendChild(text);
|
|
|
|
|
box.appendChild(input);
|
|
|
|
|
box.appendChild(btnContainer);
|
|
|
|
|
overlay.appendChild(box);
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
|
|
|
|
|
input.focus();
|
|
|
|
|
input.select();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 1. GLOBAL STATE DEFINITIONS
|
|
|
|
|
// =========================================================================
|
|
|
|
@@ -884,7 +1077,7 @@
|
|
|
|
|
name: "Live",
|
|
|
|
|
type: "LiveStream",
|
|
|
|
|
// This MUST match the inner div where the video should appear
|
|
|
|
|
target: document.querySelector('.library-scan-reader'),
|
|
|
|
|
target: document.querySelector('.library-scan-reader'),
|
|
|
|
|
constraints: {
|
|
|
|
|
width: 640,
|
|
|
|
|
height: 480,
|
|
|
|
@@ -897,7 +1090,7 @@
|
|
|
|
|
},
|
|
|
|
|
decoder: {
|
|
|
|
|
// Keep only the barcode types you actually use to improve performance
|
|
|
|
|
readers: ["code_128_reader", "ean_reader", "code_39_reader"]
|
|
|
|
|
readers: ["code_128_reader", "ean_reader", "code_39_reader"]
|
|
|
|
|
},
|
|
|
|
|
locate: true
|
|
|
|
|
}, function(err) {
|
|
|
|
@@ -1243,31 +1436,39 @@
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 4. UI INTERACTIONS & UTILITIES
|
|
|
|
|
// =========================================================================
|
|
|
|
|
function borrowItem(itemId) {
|
|
|
|
|
async 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.');
|
|
|
|
|
await customAlert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultCardId = activeStudentCardId || '';
|
|
|
|
|
const cardId = (window.prompt('Bitte Bibliotheksausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase();
|
|
|
|
|
const promptCardResult = await customPrompt('Bitte Bibliotheksausweis-ID eingeben:', defaultCardId);
|
|
|
|
|
if (promptCardResult === null) return;
|
|
|
|
|
|
|
|
|
|
const cardId = promptCardResult.trim().toUpperCase();
|
|
|
|
|
if (!cardId) {
|
|
|
|
|
alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Bibliotheksausweis-ID erforderlich.');
|
|
|
|
|
await customAlert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Bibliotheksausweis-ID erforderlich.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setActiveStudentCard(cardId);
|
|
|
|
|
|
|
|
|
|
const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim();
|
|
|
|
|
const promptDurationResult = await customPrompt('Ausleihdauer in Tagen (optional):');
|
|
|
|
|
if (promptDurationResult === null) return;
|
|
|
|
|
const durationInput = promptDurationResult.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);
|
|
|
|
|
const countPromptResult = await customPrompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1');
|
|
|
|
|
if (countPromptResult === null) return;
|
|
|
|
|
|
|
|
|
|
let borrowCount = parseInt(countPromptResult.trim() || '1', 10);
|
|
|
|
|
if (!Number.isFinite(borrowCount) || borrowCount < 1) {
|
|
|
|
|
borrowCount = 1;
|
|
|
|
|
}
|
|
|
|
|
if (borrowCount > maxAvailable) {
|
|
|
|
|
alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
|
|
|
|
|
await customAlert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|