Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0911b362fd | |||
| 71716f339a | |||
| 36ccee38cb | |||
| 9255c87f57 | |||
| a6b246a92b | |||
| 25f52eeeb5 | |||
| c07d4e0bdd | |||
| 0cacfb0871 | |||
| d3bfaa4580 | |||
| 36531662d3 |
@@ -42,53 +42,64 @@ jobs:
|
||||
EVENT_NAME: ${{ gitea.event_name }}
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
RELEASE_TYPE: ${{ gitea.event.inputs.release_type || 'patch' }}
|
||||
RUN_NUMBER: ${{ gitea.run_number }}
|
||||
DOCKER_API_VERSION: '1.44'
|
||||
run: |
|
||||
latest_stable_tag() {
|
||||
local releases_file="$1"
|
||||
python3 -c "import json, sys; releases = json.load(open(sys.argv[1], encoding='utf-8')); print(next((release.get('tag_name', '').strip() for release in releases if not release.get('prerelease') and not release.get('draft')), ''))" "$releases_file"
|
||||
}
|
||||
|
||||
next_dev_suffix() {
|
||||
local releases_file="$1"
|
||||
local base_tag="$2"
|
||||
python3 -c "import json, re, sys; releases_path, base_tag = sys.argv[1], sys.argv[2].strip(); pattern = re.compile(r'^' + re.escape(base_tag) + r'-dev\\.(\\d+)$'); releases = json.load(open(releases_path, encoding='utf-8')); highest = max([int(match.group(1)) for release in releases for match in [pattern.match(release.get('tag_name', '').strip())] if match], default=0); print(highest + 1)" "$releases_file" "$base_tag"
|
||||
}
|
||||
|
||||
make_dev_tag() {
|
||||
local releases_file="$1"
|
||||
local stable_tag="$2"
|
||||
local major minor patch next_suffix
|
||||
|
||||
if [[ "$stable_tag" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
major=${BASH_REMATCH[1]}
|
||||
minor=${BASH_REMATCH[2]}
|
||||
patch=${BASH_REMATCH[3]}
|
||||
else
|
||||
major=0
|
||||
minor=8
|
||||
patch=31
|
||||
fi
|
||||
|
||||
patch=$((patch + 1))
|
||||
next_suffix="$(next_dev_suffix "$releases_file" "v${major}.${minor}.${patch}")"
|
||||
printf 'v%s.%s.%s-dev.%s' "$major" "$minor" "$patch" "$next_suffix"
|
||||
}
|
||||
|
||||
releases_file="$(mktemp)"
|
||||
trap 'rm -f "${releases_file:-}"' EXIT
|
||||
|
||||
if [ "$EVENT_NAME" = "push" ] && [ "$REF_NAME" = "main" ]; then
|
||||
prerelease=true
|
||||
latest_tag="v0.8.31"
|
||||
if meta_json=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/json" "https://git.invario-software.eu/api/v1/repos/$REPO/releases/latest" 2>/dev/null); then
|
||||
tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
if [ -n "$tag_name" ]; then
|
||||
latest_tag="$tag_name"
|
||||
fi
|
||||
if ! curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/json" "https://git.invario-software.eu/api/v1/repos/$REPO/releases" -o "$releases_file"; then
|
||||
echo "Error: could not fetch release list"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$latest_tag" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
major=${BASH_REMATCH[1]}
|
||||
minor=${BASH_REMATCH[2]}
|
||||
patch=${BASH_REMATCH[3]}
|
||||
else
|
||||
major=0
|
||||
minor=8
|
||||
patch=31
|
||||
latest_tag="$(latest_stable_tag "$releases_file")"
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.8.31"
|
||||
fi
|
||||
|
||||
patch=$((patch + 1))
|
||||
TAG="v${major}.${minor}.${patch}-dev.${RUN_NUMBER:-0}"
|
||||
TAG="$(make_dev_tag "$releases_file" "$latest_tag")"
|
||||
elif [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$RELEASE_TYPE" = "dev" ]; then
|
||||
prerelease=true
|
||||
latest_tag="v0.8.31"
|
||||
if meta_json=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/json" "https://git.invario-software.eu/api/v1/repos/$REPO/releases/latest" 2>/dev/null); then
|
||||
tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
if [ -n "$tag_name" ]; then
|
||||
latest_tag="$tag_name"
|
||||
fi
|
||||
if ! curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/json" "https://git.invario-software.eu/api/v1/repos/$REPO/releases" -o "$releases_file"; then
|
||||
echo "Error: could not fetch release list"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$latest_tag" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
major=${BASH_REMATCH[1]}
|
||||
minor=${BASH_REMATCH[2]}
|
||||
patch=${BASH_REMATCH[3]}
|
||||
else
|
||||
major=0
|
||||
minor=8
|
||||
patch=31
|
||||
latest_tag="$(latest_stable_tag "$releases_file")"
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.8.31"
|
||||
fi
|
||||
|
||||
patch=$((patch + 1))
|
||||
TAG="v${major}.${minor}.${patch}-dev.${RUN_NUMBER:-0}"
|
||||
TAG="$(make_dev_tag "$releases_file" "$latest_tag")"
|
||||
elif [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then
|
||||
prerelease=false
|
||||
TAG="$REF_NAME"
|
||||
|
||||
+2
-6
@@ -5177,17 +5177,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:
|
||||
|
||||
@@ -505,6 +505,10 @@
|
||||
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)" readonly>
|
||||
<button id="resetCardBtn" class="button" type="button">Ausweis löschen</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 id="scanStatus" class="library-scan-status">
|
||||
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
|
||||
@@ -632,6 +636,11 @@
|
||||
let activeStudentCardId = '';
|
||||
let lastScanValue = '';
|
||||
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');
|
||||
|
||||
@@ -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) {
|
||||
const scannedCode = normalizeScannedCode(decodedText);
|
||||
if (!scannedCode) return;
|
||||
@@ -1145,6 +1191,7 @@
|
||||
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 () => {
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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. ABC-001 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. ABC-001 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
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
<div class="user-management-container">
|
||||
<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">
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
Reference in New Issue
Block a user