Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0911b362fd | |||
| 71716f339a | |||
| 36ccee38cb | |||
| 9255c87f57 | |||
| a6b246a92b |
@@ -505,6 +505,10 @@
|
|||||||
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)" readonly>
|
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)" readonly>
|
||||||
<button id="resetCardBtn" class="button" type="button">Ausweis löschen</button>
|
<button id="resetCardBtn" class="button" type="button">Ausweis löschen</button>
|
||||||
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
|
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
|
||||||
|
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
|
||||||
|
<input type="checkbox" id="keyboardScannerToggle">
|
||||||
|
<span style="font-size:0.9em;">Physischer Scanner</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<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.
|
||||||
@@ -632,6 +636,11 @@
|
|||||||
let activeStudentCardId = '';
|
let activeStudentCardId = '';
|
||||||
let lastScanValue = '';
|
let lastScanValue = '';
|
||||||
let lastScanAt = 0;
|
let lastScanAt = 0;
|
||||||
|
// Keyboard-scanner support (physical scanners that act as keyboard wedges)
|
||||||
|
let keyboardScannerEnabled = false;
|
||||||
|
let keyboardScanBuffer = '';
|
||||||
|
let keyboardLastKeyAt = 0;
|
||||||
|
const KEYBOARD_SCAN_INTERCHAR_MS = 100; // max time between keystrokes to consider them one scan
|
||||||
|
|
||||||
const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1');
|
const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1');
|
||||||
|
|
||||||
@@ -912,6 +921,43 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Keyboard scanner handling (physical scanners that send chars then Enter)
|
||||||
|
// =========================================================================
|
||||||
|
function keyboardScanKeydownHandler(e) {
|
||||||
|
// Only active when explicitly enabled
|
||||||
|
if (!keyboardScannerEnabled) return;
|
||||||
|
|
||||||
|
// Ignore if focus is in an input/textarea/contenteditable to avoid interfering with typing
|
||||||
|
const active = document.activeElement;
|
||||||
|
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// If Enter/Return pressed -> finalize buffer
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const code = keyboardScanBuffer.trim();
|
||||||
|
keyboardScanBuffer = '';
|
||||||
|
keyboardLastKeyAt = 0;
|
||||||
|
if (!code) return;
|
||||||
|
// Process exactly like a camera scan
|
||||||
|
handleScanSuccess(code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only accept common printable characters; ignore modifier keys
|
||||||
|
if (e.key.length === 1) {
|
||||||
|
// If time gap too big, start new buffer
|
||||||
|
if (keyboardLastKeyAt && (now - keyboardLastKeyAt) > KEYBOARD_SCAN_INTERCHAR_MS) {
|
||||||
|
keyboardScanBuffer = '';
|
||||||
|
}
|
||||||
|
keyboardScanBuffer += e.key;
|
||||||
|
keyboardLastKeyAt = now;
|
||||||
|
// Prevent default so scanner input doesn't accidentally move focus or trigger shortcuts
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleScanSuccess(decodedText) {
|
function handleScanSuccess(decodedText) {
|
||||||
const scannedCode = normalizeScannedCode(decodedText);
|
const scannedCode = normalizeScannedCode(decodedText);
|
||||||
if (!scannedCode) return;
|
if (!scannedCode) return;
|
||||||
@@ -1145,6 +1191,7 @@
|
|||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
const resetBtn = document.getElementById('resetCardBtn');
|
const resetBtn = document.getElementById('resetCardBtn');
|
||||||
const modeSelect = document.getElementById('scanModeSelect');
|
const modeSelect = document.getElementById('scanModeSelect');
|
||||||
|
const keyboardToggle = document.getElementById('keyboardScannerToggle');
|
||||||
|
|
||||||
if (toggleBtn) {
|
if (toggleBtn) {
|
||||||
toggleBtn.addEventListener('click', async () => {
|
toggleBtn.addEventListener('click', async () => {
|
||||||
@@ -1172,6 +1219,19 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Run when DOM structure is entirely ready
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Fach/Kategorie') }} (Filter 1)</h2>
|
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Jahrgang') }} (Filter 1)</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'System/Bereich') }} (Filter 2)</h2>
|
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'Fach') }} (Filter 2)</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
||||||
|
|||||||
@@ -812,12 +812,12 @@
|
|||||||
<div id="range_generator_group" class="form-group" style="display:none; background: #f9f9f9; padding: 15px; border-radius: 5px; border: 1px solid #ddd; margin-bottom: 15px;">
|
<div id="range_generator_group" class="form-group" style="display:none; background: #f9f9f9; padding: 15px; border-radius: 5px; border: 1px solid #ddd; margin-bottom: 15px;">
|
||||||
<label style="font-weight: bold;">Code-Bereich automatisch generieren:</label>
|
<label style="font-weight: bold;">Code-Bereich automatisch generieren:</label>
|
||||||
<div style="display: flex; gap: 10px; margin-bottom: 10px; align-items: center;">
|
<div style="display: flex; gap: 10px; margin-bottom: 10px; align-items: center;">
|
||||||
<input type="text" id="range_prefix" placeholder="Präfix (z.B. IT-)" class="form-control" style="flex: 2;">
|
<input type="text" id="range_prefix" placeholder="Präfix optional (z.B. IT-)" class="form-control" style="flex: 2;">
|
||||||
<input type="number" id="range_start" placeholder="Start (z.B. 1)" class="form-control" style="flex: 1;">
|
<input type="number" id="range_start" placeholder="Start (z.B. 1)" class="form-control" style="flex: 1;">
|
||||||
<span style="font-weight: bold;">bis</span>
|
<span style="font-weight: bold;">bis</span>
|
||||||
<input type="text" id="range_end" placeholder="Ende (z.B. 020)" class="form-control" style="flex: 1;">
|
<input type="text" id="range_end" placeholder="Ende (z.B. 020)" class="form-control" style="flex: 1;">
|
||||||
</div>
|
</div>
|
||||||
<small style="display:block; color:#666; margin-bottom: 10px;">Tipp: Die Anzahl der Ziffern im "Ende"-Feld bestimmt die führenden Nullen (z.B. Ende "050" macht aus Start "1" einen "001").</small>
|
<small style="display:block; color:#666; margin-bottom: 10px;">Tipp: Der Präfix ist optional. Die Anzahl der Ziffern im "Ende"-Feld bestimmt die führenden Nullen (z.B. Ende "050" macht aus Start "1" einen "001").</small>
|
||||||
<button type="button" class="btn btn-secondary" onclick="generateCodeRange()">Bereich generieren & einfügen</button>
|
<button type="button" class="btn btn-secondary" onclick="generateCodeRange()">Bereich generieren & einfügen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -852,9 +852,9 @@
|
|||||||
</select>
|
</select>
|
||||||
<small style="display:block; color:#666;">Wählen Sie einen Medientyp aus zur Klassifizierung.</small>
|
<small style="display:block; color:#666;">Wählen Sie einen Medientyp aus zur Klassifizierung.</small>
|
||||||
</div>
|
</div>
|
||||||
<h3>Kategorie/Typ:</h3>
|
<h3>Kategorie/Typ/Fach:</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" name="library_category" id="library_category" placeholder="z.B. Belletristik, Sachbücher, Nachschlagewerke, etc.">
|
<input type="text" name="library_category" id="library_category" placeholder="z.B. Belletristik, Sachbücher, Nachschlagewerke, etc. (optional)">
|
||||||
<small style="display:block; color:#666;">Geben Sie hier eine beliebige Kategorie ein zur freien Klassifizierung.</small>
|
<small style="display:block; color:#666;">Geben Sie hier eine beliebige Kategorie ein zur freien Klassifizierung.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1024,6 +1024,90 @@
|
|||||||
<script>
|
<script>
|
||||||
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
|
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
|
||||||
|
|
||||||
|
function getFocusableFormFields(form) {
|
||||||
|
if (!form) return [];
|
||||||
|
return Array.from(form.querySelectorAll('input, select, textarea, button'))
|
||||||
|
.filter((element) => {
|
||||||
|
if (element.disabled || element.getAttribute('aria-hidden') === 'true') return false;
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||||
|
const tagName = element.tagName.toLowerCase();
|
||||||
|
const type = (element.type || '').toLowerCase();
|
||||||
|
return tagName !== 'button' || type !== 'button';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNextFormField(currentField) {
|
||||||
|
const form = currentField && currentField.form ? currentField.form : null;
|
||||||
|
const fields = getFocusableFormFields(form || document);
|
||||||
|
const currentIndex = fields.indexOf(currentField);
|
||||||
|
|
||||||
|
if (currentIndex >= 0) {
|
||||||
|
for (let i = currentIndex + 1; i < fields.length; i++) {
|
||||||
|
const nextField = fields[i];
|
||||||
|
const nextStyle = window.getComputedStyle(nextField);
|
||||||
|
if (nextStyle.display !== 'none' && nextStyle.visibility !== 'hidden') {
|
||||||
|
nextField.focus();
|
||||||
|
if (nextField.tagName.toLowerCase() === 'select') {
|
||||||
|
nextField.click();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIsbnLookupFromScanner() {
|
||||||
|
const isbnField = document.getElementById('isbn');
|
||||||
|
if (!isbnField) return false;
|
||||||
|
|
||||||
|
const normalizedIsbn = typeof normalizeIsbnClient === 'function' ? normalizeIsbnClient(isbnField.value) : isbnField.value.trim();
|
||||||
|
if (!normalizedIsbn) return false;
|
||||||
|
|
||||||
|
if (typeof updateIsbnLiveValidation === 'function') {
|
||||||
|
updateIsbnLiveValidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof fetchBookInfo === 'function') {
|
||||||
|
fetchBookInfo('upload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.querySelectorAll('form').forEach(function (form) {
|
||||||
|
form.addEventListener('keydown', function (event) {
|
||||||
|
const target = event.target;
|
||||||
|
if (event.key !== 'Enter') return;
|
||||||
|
if (!target || target.tagName === 'TEXTAREA' && !event.shiftKey) return;
|
||||||
|
const tagName = target.tagName.toLowerCase();
|
||||||
|
const type = (target.type || '').toLowerCase();
|
||||||
|
if (!['input', 'select', 'textarea'].includes(tagName)) return;
|
||||||
|
if (['button', 'submit', 'reset', 'file', 'checkbox', 'radio', 'hidden'].includes(type)) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (target.id === 'isbn' || target.name === 'isbn') {
|
||||||
|
handleIsbnLookupFromScanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
focusNextFormField(target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Function to check if a file is a video
|
// Function to check if a file is a video
|
||||||
function isVideoFile(filename) {
|
function isVideoFile(filename) {
|
||||||
const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.m4v', '.3gp'];
|
const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.m4v', '.3gp'];
|
||||||
@@ -1195,7 +1279,7 @@
|
|||||||
let generatedCodes = [];
|
let generatedCodes = [];
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
let numStr = i.toString().padStart(paddingLength, '0');
|
let numStr = i.toString().padStart(paddingLength, '0');
|
||||||
generatedCodes.push(`${prefix}${numStr}`);
|
generatedCodes.push(prefix ? `${prefix}${numStr}` : numStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeField = document.getElementById('code_4');
|
const codeField = document.getElementById('code_4');
|
||||||
@@ -1385,10 +1469,32 @@
|
|||||||
scanModeSelect.addEventListener('change', toggleScanMode);
|
scanModeSelect.addEventListener('change', toggleScanMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. ISBN Live-Validierung
|
// 4. ISBN Live-Validierung und automatische Abfrage nach Scan/Enter
|
||||||
const isbnInput = document.getElementById('isbn');
|
const isbnInput = document.getElementById('isbn');
|
||||||
if (isbnInput && typeof updateIsbnLiveValidation === 'function') {
|
if (isbnInput) {
|
||||||
isbnInput.addEventListener('input', updateIsbnLiveValidation);
|
if (typeof updateIsbnLiveValidation === 'function') {
|
||||||
|
isbnInput.addEventListener('input', updateIsbnLiveValidation);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isbnLookupTimer = null;
|
||||||
|
isbnInput.addEventListener('input', function () {
|
||||||
|
clearTimeout(isbnLookupTimer);
|
||||||
|
const normalizedIsbn = typeof normalizeIsbnClient === 'function' ? normalizeIsbnClient(isbnInput.value) : isbnInput.value.trim();
|
||||||
|
if (normalizedIsbn) {
|
||||||
|
isbnLookupTimer = setTimeout(function () {
|
||||||
|
handleIsbnLookupFromScanner();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
isbnInput.addEventListener('change', function () {
|
||||||
|
if (typeof normalizeIsbnClient === 'function') {
|
||||||
|
const normalizedIsbn = normalizeIsbnClient(isbnInput.value);
|
||||||
|
if (normalizedIsbn) {
|
||||||
|
handleIsbnLookupFromScanner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Load predefined filter values for dropdowns
|
// Load predefined filter values for dropdowns
|
||||||
|
|||||||
@@ -17,6 +17,12 @@
|
|||||||
<div class="user-management-container">
|
<div class="user-management-container">
|
||||||
<h2>Benutzer</h2>
|
<h2>Benutzer</h2>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<a href="{{ url_for('register') }}" class="btn btn-success">
|
||||||
|
Neuen Benutzer registrieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('admin_anonymize_names') }}" class="mb-3">
|
<form method="POST" action="{{ url_for('admin_anonymize_names') }}" class="mb-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
Reference in New Issue
Block a user