Compare commits

...

13 Commits

Author SHA1 Message Date
Aiirondev_dev e9006f5a07 Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 00:38:48 +02:00
Aiirondev_dev a11bce17c5 Fixy of the api_library_items()
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 00:31:57 +02:00
Aiirondev_dev 743e5b1c16 Reviewed the edeting function
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-08-10 23:40:02 +02:00
Aiirondev_dev 4e47ef0c88 Implement library item return by code API and enhance UI for manual returns
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-10 22:17:52 +02:00
Aiirondev_dev 0911b362fd Changes to match the designated endpoint of the user when uploading an Item to dont have the system Jump between the systems.
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-10 21:35:13 +02:00
Aiirondev_dev 71716f339a Add keyboard scanner support with toggle option in library table
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-10 21:23:33 +02:00
Aiirondev_dev 36ccee38cb Addition of the button as described in the Issue #17
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-10 17:41:18 +02:00
Aiirondev_dev 9255c87f57 Implement ISBN live validation and automatic lookup on input change
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-10 14:16:20 +02:00
Aiirondev_dev a6b246a92b Refactor upload form fields for clarity and enhance focus navigation
Release Inventarsystem / release-docker (push) Successful in 3m15s
2026-08-10 14:10:08 +02:00
Aiirondev_dev 25f52eeeb5 Change sfor the issue #16 with some of the described changes as addtion of a arrow pointig down or the rearange ment done
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-10 00:24:05 +02:00
Aiirondev_dev c07d4e0bdd Changes to match the designated endpoint of the user when uploading an Item to dont have the system Jump between the systems.
Release Inventarsystem / release-docker (push) Successful in 2m19s
2026-08-09 23:59:13 +02:00
Aiirondev_dev 0cacfb0871 Quik and more or less temporary fix for the Issue #43 further changes are not required
Release Inventarsystem / release-docker (push) Successful in 3m16s
2026-08-09 23:39:21 +02:00
Aiirondev_dev d3bfaa4580 update of an outadet approach of the user adding in the current version 2026-08-09 21:15:13 +02:00
5 changed files with 978 additions and 593 deletions
+156 -24
View File
@@ -225,12 +225,95 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab
if client:
client.close()
summary = {'examined': examined, 'updated': updated, 'failures': failures, 'dry_run': bool(dry_run)}
@app.route('/api/library_return_by_code', methods=['POST'])
def api_library_return_by_code():
"""
Return a library item by scanning its code only (no student card required).
This marks active ausleihungen for the item as completed and updates item status.
"""
if 'username' not in session:
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
if not cfg.MODULES.is_enabled('library'):
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
payload = request.get_json(silent=True) or {}
item_code_raw = str(payload.get('item_code') or payload.get('code') or '').strip()
if not item_code_raw:
return jsonify({'ok': False, 'message': 'Mediencode fehlt.'}), 400
normalized_isbn = normalize_and_validate_isbn(item_code_raw)
normalized_code = item_code_raw.upper()
client = None
try:
_append_audit_event_standalone('student_cards_rollover', summary)
except Exception:
app.logger.warning('Audit write failed for student_cards_rollover')
return summary
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
items_col = db['items']
ausleihungen_col = db['ausleihungen']
query_or = [
{'Code_4': item_code_raw},
{'Code_4': normalized_code},
]
if normalized_isbn:
query_or.append({'ISBN': normalized_isbn})
item_doc = items_col.find_one({
'ItemType': {'$in': LIBRARY_ITEM_TYPES},
'$or': query_or
})
if not item_doc:
return jsonify({'ok': False, 'message': 'Kein Bibliotheksmedium für diesen Code gefunden.'}), 404
item_id = str(item_doc['_id'])
now = datetime.datetime.now()
# If item already available -> nothing to return
if item_doc.get('Verfuegbar', True):
return jsonify({'ok': False, 'message': 'Dieses Medium ist nicht als ausgeliehen markiert.'}), 409
# Mark active ausleihungen as completed
update_result = ausleihungen_col.update_many(
{'Item': item_id, 'Status': 'active'},
{'$set': {
'Status': 'completed',
'End': now,
'LastUpdated': now
}}
)
# Update item status to available
borrower_name = str(item_doc.get('User') or '').strip() or ''
it.update_item_status(item_id, True, borrower_name)
_append_audit_event_standalone(
event_type='ausleihung_returned_by_code',
payload={
'channel': 'library_return_code',
'item_id': item_id,
'item_name': item_doc.get('Name', ''),
'completed_records': update_result.modified_count,
'performed_by': session.get('username')
}
)
return jsonify({
'ok': True,
'action': 'returned',
'item_id': item_id,
'item_name': item_doc.get('Name', ''),
'completed_records': update_result.modified_count,
'message': f"{item_doc.get('Name', 'Medium')} wurde zurückgegeben."
}), 200
except Exception as e:
app.logger.error(f"Error in library return by code: {e}")
return jsonify({'ok': False, 'message': 'Fehler beim Verarbeiten der Rückgabe.'}), 500
finally:
if client:
client.close()
# Admin route to trigger rollover manually
@@ -3231,7 +3314,6 @@ def api_library_items():
query = {
'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']},
'IsGroupedSubItem': {'$ne': True},
'Deleted': {'$ne': True}
}
@@ -3249,7 +3331,12 @@ def api_library_items():
'User': 1,
'Ort': 1,
'Beschreibung': 1,
'Image': 1
'Image': 1,
'SeriesGroupId': 1,
'SeriesCount': 1,
'SeriesPosition': 1,
'IsGroupedSubItem': 1,
'ParentItemId': 1,
}
total_count = items_db.count_documents(query)
@@ -3277,6 +3364,10 @@ def api_library_items():
'Beschreibung': 1,
'Image': 1,
'ParentItemId': 1,
'SeriesGroupId': 1,
'SeriesCount': 1,
'SeriesPosition': 1,
'IsGroupedSubItem': 1,
}
child_items = list(items_db.find({
'ParentItemId': {'$in': parent_ids_list},
@@ -3390,6 +3481,48 @@ def api_library_items():
return jsonify({'error': 'An error occurred while fetching library items'}), 500
@app.route('/api/library_group/<series_group_id>')
def api_library_group(series_group_id):
"""Fetch all items belonging to one library series group."""
if 'username' not in session:
return jsonify({'items': []}), 401
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
items_col = db['items']
query = {
'SeriesGroupId': series_group_id,
'Deleted': {'$ne': True},
'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']},
}
projection = {
'Name': 1,
'ISBN': 1,
'Code_4': 1,
'Code4': 1,
'ItemType': 1,
'Ort': 1,
'Beschreibung': 1,
'SeriesGroupId': 1,
'SeriesCount': 1,
'SeriesPosition': 1,
'IsGroupedSubItem': 1,
'ParentItemId': 1,
}
items = list(items_col.find(query, projection).sort([('SeriesPosition', 1), ('Name', 1), ('_id', 1)]))
for item in items:
item['_id'] = str(item['_id'])
client.close()
return jsonify({'items': items, 'count': len(items), 'series_group_id': series_group_id})
except Exception as exc:
app.logger.error('Error loading library group %s: %s', series_group_id, exc)
return jsonify({'items': [], 'message': 'Gruppe konnte nicht geladen werden.'}), 500
@app.route('/api/library_scan_action', methods=['POST'])
def api_library_scan_action():
"""
@@ -5177,17 +5310,13 @@ def upload_item():
fs = get_gridfs()
can_access_admin_home = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings')
if can_access_admin_home:
success_redirect_endpoint = 'home_admin'
elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'):
success_redirect_endpoint = 'home_library'
if cfg.MODULES.is_enabled('library') and sanitize_form_value(request.form.get('item_type_input', '')) != "other":
success_redirect_endpoint = 'library'
else:
success_redirect_endpoint = 'home_admin'
# Detect if request is from mobile device
is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
is_ios = 'iPhone' in request.headers.get('User-Agent', '') or 'iPad' in request.headers.get('User-Agent', '')
# Log mobile request for debugging
if is_mobile:
@@ -6257,17 +6386,20 @@ def update_group():
# 1. Shared Fields (Group Logic)
# These apply to every item in the group
shared_update = {
'Name': data.get('name'),
'Ort': data.get('ort'),
'Beschreibung': data.get('beschreibung'),
'Anschaffungsjahr': data.get('ansch_jahr'),
'Anschaffungskosten': data.get('ansch_kost'),
'Reservierbar': data.get('reservierbar'),
'ISBN': data.get('isbn'),
'ItemType': data.get('item_type'),
'LastUpdated': datetime.datetime.now()
}
shared_update = {'LastUpdated': datetime.datetime.now()}
for source_key, target_key in (
('name', 'Name'),
('ort', 'Ort'),
('beschreibung', 'Beschreibung'),
('ansch_jahr', 'Anschaffungsjahr'),
('ansch_kost', 'Anschaffungskosten'),
('reservierbar', 'Reservierbar'),
('isbn', 'ISBN'),
('item_type', 'ItemType'),
):
value = data.get(source_key)
if value is not None:
shared_update[target_key] = value
# 2. Individual Updates (Specific Code Logic)
# Expected format: [{'id': '...', 'code_4': '...'}, ...]
File diff suppressed because it is too large Load Diff
+2 -50
View File
@@ -19,7 +19,7 @@
<div class="col-md-4">
<div class="card mb-4">
<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 class="card-body">
<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="card mb-4">
<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 class="card-body">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
@@ -110,54 +110,6 @@
</div>
</div>
</div>
<!-- Filter 3 -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">{{ filter_names.get('3', 'Typ/Art') }} (Filter 3)</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=3) }}" class="mb-4">
<div class="input-group">
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
<h5 class="mb-3">Vorhandene Werte</h5>
{% if filter3_values %}
<div class="list-group">
{% for value in filter3_values %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-3-{{ loop.index }}')">Bearbeiten</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=3, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
</form>
</div>
</div>
<form id="edit-3-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=3, old_value=value) }}" style="display: none;" class="mt-2">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-3-{{ loop.index }}')">Abbrechen</button>
</div>
</form>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">Keine Werte definiert.</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="alert alert-warning">
+166 -59
View File
@@ -761,6 +761,21 @@
<h1>{{ page_title|default('Artikel hochladen') }}</h1>
<form method="POST" action="{{ url_for('upload_item') }}" enctype="multipart/form-data">
<input type="hidden" name="upload_mode" value="{{ upload_mode|default('item') }}">
{% if show_library_features %}
<div class="form-group">
<label for="isbn">ISBN/Barcode (für Bild-Suche):</label>
<div class="isbn-input-group">
<input type="text" id="isbn" name="isbn" placeholder="ISBN oder Barcode eingeben..." required>
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Informationen abrufen</button>
</div>
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
<small id="isbn-scan-status" style="display:block; color:#666; margin-top:6px;">Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert.</small>
<div id="book-info-container" class="book-info-container"></div>
</div>
{% endif %}
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
@@ -784,7 +799,44 @@
<label for="beschreibung">Beschreibung:</label>
<textarea id="beschreibung" name="beschreibung" required></textarea>
</div>
<div class="form-group">
<label for="scan_mode">Erfassungs-Modus:</label>
<select id="scan_mode" name="scan_mode" class="form-control" onchange="toggleScanMode()">
<option value="single">Einzel-Code (Scanner stoppt nach 1 Scan) ↓ </option>
<option value="continuous">Fortlaufend scannen (Mehrere Codes nacheinander)</option>
<option value="range">Code-Bereich generieren (z.B. ABC-001 bis ABC-010)</option>
</select>
</div>
<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>
<div style="display: flex; gap: 10px; margin-bottom: 10px; align-items: center;">
<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;">
<span style="font-weight: bold;">bis</span>
<input type="text" id="range_end" placeholder="Ende (z.B. 020)" class="form-control" style="flex: 1;">
</div>
<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>
</div>
<div class="form-group" id="primary_code_group">
<label for="code_4">Basis-Code</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="code_4" name="code_4" class="form-control" placeholder="Haupt-Barcode" required>
<button type="button" id="scan-code4-btn" class="btn btn-primary">Barcode scannen</button>
</div>
<div id="code4-scanner" style="display:none; margin-top: 10px;"></div>
<small id="code4-scan-status" class="form-text text-muted"></small>
</div>
<div class="form-group" id="individual_codes_group">
<label for="individual_codes">Weitere Einzelcodes (je Zeile ein Code)</label>
<textarea id="individual_codes" name="individual_codes" rows="6" placeholder="z.B.&#10;ABC-001&#10;ABC-002" class="form-control"></textarea>
<small style="display:block; color:#666; margin-top: 5px;">Bei Anzahl > 1 können hier individuelle Codes pro Item gesetzt werden. Der Scanner setzt immer zuerst den Basis-Code im Feld oben; weitere Einzelcodes werden hier angehängt.</small>
</div>
{% if show_library_features %}
<!-- Library Mode: Single Customizable Filter -->
<div class="filter-inputs">
@@ -800,9 +852,9 @@
</select>
<small style="display:block; color:#666;">Wählen Sie einen Medientyp aus zur Klassifizierung.</small>
</div>
<h3>Kategorie/Typ:</h3>
<h3>Kategorie/Typ/Fach:</h3>
<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>
</div>
</div>
@@ -902,42 +954,6 @@
<label for="anschaffungskosten">Anschaffungskosten (€)</label>
<input id="anschaffungskosten" name="anschaffungskosten">
</div>
<div class="form-group">
<label for="scan_mode">Erfassungs-Modus:</label>
<select id="scan_mode" name="scan_mode" class="form-control" onchange="toggleScanMode()">
<option value="single">Einzel-Code (Scanner stoppt nach 1 Scan)</option>
<option value="continuous">Fortlaufend scannen (Mehrere Codes nacheinander)</option>
<option value="range">Code-Bereich generieren (z.B. ABC-001 bis ABC-010)</option>
</select>
</div>
<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>
<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="number" id="range_start" placeholder="Start (z.B. 1)" class="form-control" style="flex: 1;">
<span style="font-weight: bold;">bis</span>
<input type="text" id="range_end" placeholder="Ende (z.B. 020)" class="form-control" style="flex: 1;">
</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>
<button type="button" class="btn btn-secondary" onclick="generateCodeRange()">Bereich generieren & einfügen</button>
</div>
<div class="form-group" id="primary_code_group">
<label for="code_4">Basis-Code</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="code_4" name="code_4" class="form-control" placeholder="Haupt-Barcode" required>
<button type="button" id="scan-code4-btn" class="btn btn-primary">Barcode scannen</button>
</div>
<div id="code4-scanner" style="display:none; margin-top: 10px;"></div>
<small id="code4-scan-status" class="form-text text-muted"></small>
</div>
<div class="form-group" id="individual_codes_group">
<label for="individual_codes">Weitere Einzelcodes (je Zeile ein Code)</label>
<textarea id="individual_codes" name="individual_codes" rows="6" placeholder="z.B.&#10;ABC-001&#10;ABC-002" class="form-control"></textarea>
<small style="display:block; color:#666; margin-top: 5px;">Bei Anzahl > 1 können hier individuelle Codes pro Item gesetzt werden. Der Scanner setzt immer zuerst den Basis-Code im Feld oben; weitere Einzelcodes werden hier angehängt.</small>
</div>
<!-- Image upload (hidden for library mode) -->
<div class="form-group" {% if show_library_features %}style="display:none;"{% endif %}>
<label for="images">Bilder/Videos:</label>
@@ -946,27 +962,12 @@
<!-- Add image preview area -->
<div class="image-preview-container" id="image-preview-container"></div>
</div>
<!-- ISBN fields (library page only) -->
{% if show_library_features %}
<div class="form-group">
<label for="isbn">ISBN/Barcode (für Bild-Suche):</label>
<div class="isbn-input-group">
<input type="text" id="isbn" name="isbn" placeholder="ISBN oder Barcode eingeben..." required>
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Informationen abrufen</button>
</div>
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
<small id="isbn-scan-status" style="display:block; color:#666; margin-top:6px;">Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert.</small>
<div id="book-info-container" class="book-info-container"></div>
</div>
<div class="form-group">
<label for="reservierbar" style="display:inline-block; width:auto; margin-right:10px;">Reservierbar:</label>
<input type="checkbox" id="reservierbar" name="reservierbar" style="width:auto;">
<small style="display:block; color:#666;">Wenn deaktiviert, kann der Artikel nicht im Voraus reserviert werden (Sofort-Ausleihe bleibt möglich).</small>
</div>
{% endif %}
<button type="submit" class="submit-button">{{ 'Bücher hochladen' if show_library_features else 'Artikel hochladen' }}</button>
</form>
@@ -1023,6 +1024,90 @@
<script>
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 isVideoFile(filename) {
const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.m4v', '.3gp'];
@@ -1194,7 +1279,7 @@
let generatedCodes = [];
for (let i = start; i <= end; i++) {
let numStr = i.toString().padStart(paddingLength, '0');
generatedCodes.push(`${prefix}${numStr}`);
generatedCodes.push(prefix ? `${prefix}${numStr}` : numStr);
}
const codeField = document.getElementById('code_4');
@@ -1384,10 +1469,32 @@
scanModeSelect.addEventListener('change', toggleScanMode);
}
// 4. ISBN Live-Validierung
// 4. ISBN Live-Validierung und automatische Abfrage nach Scan/Enter
const isbnInput = document.getElementById('isbn');
if (isbnInput && typeof updateIsbnLiveValidation === 'function') {
isbnInput.addEventListener('input', updateIsbnLiveValidation);
if (isbnInput) {
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
+5 -9
View File
@@ -17,15 +17,11 @@
<div class="user-management-container">
<h2>Benutzer</h2>
<form method="POST" action="{{ url_for('admin_anonymize_names') }}" class="mb-3">
<button
type="submit"
class="btn btn-outline-danger"
onclick="return confirm('Sollen alle gespeicherten Klarnamen dauerhaft in Synonym-Kuerzel umgewandelt werden?')"
>
Gespeicherte Namen anonymisieren
</button>
</form>
<div class="mb-3">
<a href="{{ url_for('register') }}" class="btn btn-success">
Neuen Benutzer registrieren
</a>
</div>
<div class="filter-bar mb-3">
<div class="row g-2 align-items-end">