Compare commits

...

3 Commits

Author SHA1 Message Date
Aiirondev_dev 55a74654db Addition of a missing ','
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-14 23:18:29 +02:00
Aiirondev_dev 7473334714 Changes to the Inventarsystem Workflow to Reflekt the new workflow and have an according set of buttons. So this removed all metioning of the duplication function.
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-14 23:01:01 +02:00
Aiirondev_dev d746b61a17 feat: integrate Quagga2 scanner, background pagination, and secure fetch API
Release Inventarsystem / release-docker (push) Successful in 2m15s
Configure Quagga2 barcode scanner with environment facing mode
Add fallback keyboardScanKeydownHandler for physical keyboard-wedge scanners
Secure fetch requests by including X-CSRFToken headers
Implement loadRemainingLibraryItemsInBackground for seamless data loading
Refactor showItemDetail to accurately render videos, external URLs, and local uploa
2026-08-14 22:33:52 +02:00
4 changed files with 182 additions and 600 deletions
+10 -202
View File
@@ -2659,6 +2659,12 @@ def upload_student_cards_excel():
"""Bulk import student cards from Excel."""
return _upload_student_cards_excel()
@app.route('/api/is_student_card/<id>', methods=['POST'])
def is_student_card(id):
user = sanitize_form_value(id)
exists = bool(us.student_card_exists(user))
return jsonify({"is_student_card": exists})
"""-------------------------------------------------------------File Serving-----------------------------------------------------------------------------"""
@@ -3921,7 +3927,6 @@ def upload_admin():
"""
Upload page route for inventory items.
Accessible to users with insert permission.
Supports duplication by passing duplicate_from parameter.
Returns:
flask.Response: Rendered template or redirect
@@ -3934,54 +3939,9 @@ def upload_admin():
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
# Check if this is a duplication request
duplicate_from = request.args.get('duplicate_from')
duplicate_flag = request.args.get('duplicate') # Check for sessionStorage-based duplication
duplicate_data = None
# Handle the old method (duplicate_from parameter with item ID)
if duplicate_from:
try:
original_item = it.get_item(duplicate_from)
if original_item:
duplicate_data = {
'name': original_item.get('Name', ''),
'description': original_item.get('Beschreibung', ''),
'location': original_item.get('Ort', ''),
'room': original_item.get('Raum', ''),
'category': original_item.get('Kategorie', ''),
'year': original_item.get('Anschaffungsjahr', ''),
'cost': original_item.get('Anschaffungskosten', ''),
'filter1': original_item.get('Filter1', ''),
'filter2': original_item.get('Filter2', ''),
'filter3': original_item.get('Filter3', ''),
'images': original_item.get('Images', []),
'original_id': duplicate_from
}
# Copy all filter fields (Filter1_1 through Filter3_5)
for i in range(1, 4): # Filter1, Filter2, Filter3
for j in range(1, 6): # _1 through _5
filter_key = f'Filter{i}_{j}'
if filter_key in original_item:
duplicate_data[f'filter{i}_{j}'] = original_item[filter_key]
flash('Element wird dupliziert. Bitte überprüfen Sie die Daten und passen Sie sie bei Bedarf an.', 'info')
else:
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
except Exception as e:
app.logger.warning(f"Error loading item for duplication: {e}")
flash('Fehler beim Laden der Duplizierungsdaten.', 'error')
# Handle the new method (sessionStorage-based duplication)
elif duplicate_flag == 'true':
# No server-side processing needed - JavaScript will handle sessionStorage data
# Just indicate that duplication mode is active
flash('Element wird dupliziert. Die Daten werden aus dem Session-Speicher geladen.', 'info')
return render_template(
'upload_admin.html',
username=session['username'],
duplicate_data=duplicate_data,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
show_library_features=False,
@@ -4008,46 +3968,9 @@ def library_admin():
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
duplicate_from = request.args.get('duplicate_from')
duplicate_flag = request.args.get('duplicate')
duplicate_data = None
if duplicate_from:
try:
original_item = it.get_item(duplicate_from)
if original_item:
duplicate_data = {
'name': original_item.get('Name', ''),
'description': original_item.get('Beschreibung', ''),
'location': original_item.get('Ort', ''),
'room': original_item.get('Raum', ''),
'category': original_item.get('Kategorie', ''),
'year': original_item.get('Anschaffungsjahr', ''),
'cost': original_item.get('Anschaffungskosten', ''),
'filter1': original_item.get('Filter1', ''),
'filter2': original_item.get('Filter2', ''),
'filter3': original_item.get('Filter3', ''),
'images': original_item.get('Images', []),
'original_id': duplicate_from
}
for i in range(1, 4):
for j in range(1, 6):
filter_key = f'Filter{i}_{j}'
if filter_key in original_item:
duplicate_data[f'filter{i}_{j}'] = original_item[filter_key]
flash('Buch wird dupliziert. Bitte überprüfen Sie die Daten und passen Sie sie bei Bedarf an.', 'info')
else:
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
except Exception as e:
app.logger.warning(f"Error loading item for duplication: {e}")
flash('Fehler beim Laden der Duplizierungsdaten.', 'error')
elif duplicate_flag == 'true':
flash('Buch wird dupliziert. Die Daten werden aus dem Session-Speicher geladen.', 'info')
return render_template(
'upload_admin.html',
username=session['username'],
duplicate_data=duplicate_data,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
show_library_features=True,
@@ -5382,18 +5305,18 @@ def upload_item():
individual_codes = []
if individual_codes_raw:
individual_codes = [c.strip() for c in str(individual_codes_raw).replace(',', '\n').splitlines() if c.strip()]
# Check if this is a duplication
is_duplicating = request.form.get('is_duplicating') == 'true'
# Get duplicate_images if duplicating
duplicate_images = request.form.getlist('duplicate_images') if is_duplicating else []
# Make sure duplicate_images is always a list, even if there's only one
if is_duplicating and duplicate_images and not isinstance(duplicate_images, list):
duplicate_images = [duplicate_images]
# Log details about each image
if is_duplicating and duplicate_images:
for i, img in enumerate(duplicate_images):
@@ -5813,121 +5736,6 @@ def upload_item():
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
@app.route('/duplicate_item', methods=['POST'])
def duplicate_item():
"""
Route for duplicating an existing item.
Returns JSON response with success status.
Enhanced for mobile browser compatibility.
Returns:
flask.Response: JSON response with success status and data
"""
try:
# Check authentication
if 'username' not in session:
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
# Check if user is admin
username = session['username']
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_edit', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('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 duplication for debugging
if is_mobile:
app.logger.info(f"Mobile duplication from {request.headers.get('User-Agent', 'unknown')} by {encrypt_text(username)}")
# Get original item ID
original_item_id = request.form.get('original_item_id')
if not original_item_id:
return jsonify({'success': False, 'message': 'Ursprungs-Element-ID fehlt'}), 400
# Fetch original item data
original_item = it.get_item(original_item_id)
if not original_item:
return jsonify({'success': False, 'message': 'Ursprungs-Element nicht gefunden'}), 404
# Process filters as arrays (same as stored in database)
filter1_array = original_item.get('Filter', [])
filter2_array = original_item.get('Filter2', [])
filter3_array = original_item.get('Filter3', [])
# Ensure filters are arrays
if not isinstance(filter1_array, list):
filter1_array = [filter1_array] if filter1_array else []
if not isinstance(filter2_array, list):
filter2_array = [filter2_array] if filter2_array else []
if not isinstance(filter3_array, list):
filter3_array = [filter3_array] if filter3_array else []
# Verify image paths for mobile devices to avoid issues with non-existent images
images = original_item.get('Images', [])
verified_images = []
if is_mobile:
for img in images:
img_path = os.path.join(app.config['UPLOAD_FOLDER'], img)
if os.path.exists(img_path) and os.path.isfile(img_path):
verified_images.append(img)
else:
app.logger.warning(f"Image not found for duplication: {img}")
# If we lost images in verification, log it
if len(verified_images) < len(images):
app.logger.warning(f"Only {len(verified_images)} of {len(images)} images verified for mobile duplication")
else:
verified_images = images
# For iOS devices, add more diagnostics and reduce data size if needed
if is_ios:
# Check if images exist (we now use main images as thumbnails)
images_exist = []
for img in verified_images[:5]: # Only check first 5 to save time
img_path = os.path.join(app.config['UPLOAD_FOLDER'], img)
if os.path.exists(img_path):
images_exist.append(True)
else:
images_exist.append(False)
# Log detailed diagnostics
app.logger.info(f"iOS duplication details: {len(verified_images)} images, "
f"images available: {all(images_exist)}, "
f"filter sizes: {len(filter1_array)}, {len(filter2_array)}, {len(filter3_array)}")
return jsonify({
'success': True,
'message': 'Duplizierungsdaten erfolgreich vorbereitet',
'item_data': {
'name': original_item.get('Name', ''),
'description': original_item.get('Beschreibung', ''),
'location': original_item.get('Ort', ''),
'room': original_item.get('Raum', ''),
'category': original_item.get('Kategorie', ''),
'year': original_item.get('Anschaffungsjahr', ''),
'cost': original_item.get('Anschaffungskosten', ''),
'filter1': filter1_array,
'filter2': filter2_array,
'filter3': filter3_array,
'images': verified_images, # Using verified images instead of original
'isMobile': is_mobile,
'isIOS': is_ios
}
})
except Exception as e:
print(f"Error in duplicate_item: {e}")
return jsonify({'success': False, 'message': 'Serverfehler beim Duplizieren'}), 500
def _soft_delete_item_groups(db, root_item_ids, username):
"""Soft-delete one or more item groups and their borrow records."""
now = datetime.datetime.now()
+162 -206
View File
@@ -495,12 +495,13 @@
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
<!-- Header -->
<!-- Search and Filter Toggle -->
<!-- Customizable Filter Panel -->
<div class="library-header">
<h1>📚 Bibliothek</h1>
<p>Bücher, CDs, DVDs und weitere Medien</p>
</div>
<!-- Search and Filter Toggle -->
<!-- Student card / quick scan workflow -->
<div class="library-search-bar">
<input
type="text"
@@ -517,37 +518,22 @@
</button>
</div>
<!-- Customizable Filter Panel -->
<!-- Student card / quick scan workflow -->
<div class="library-scan-panel">
<div class="library-scan-controls">
<select id="scanModeSelect" aria-label="Scan-Modus">
<option value="card_only">Nur Ausweis erfassen</option>
<option value="quick_toggle">Schnellmodus: Ausweis + Mediencode</option>
</select>
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)" >
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
<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>
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
<input type="checkbox" id="returnOnlyToggle">
<span style="font-size:0.9em;">Nur Rückgabe (nur Mediencode)</span>
</label>
<button id="manualReturnBtn" class="button" type="button" style="margin-left:6px; background:#10b981;color:white;">Rückgabe per Code</button>
</div>
<div id="scanStatus" class="library-scan-status">
Hinweis: Im Schnellmodus zuerst den Bibliotheksausweis scannen, danach den Buch-/Mediencode.
</div>
<div id="scanReaderWrap" class="library-scan-reader-wrap" style="display: none;">
<div id="libraryQrReader" class="library-scan-reader">
<div id="library-scanner-container"></div>
</div>
</div>
<div class="library-scan-controls">
<select id="scanModeSelect" aria-label="Scan-Modus">
<option value="card_only">Nur Ausweis erfassen</option>
<option value="quick_toggle">Schnellmodus: Ausweis + Mediencode</option>
<option value="continuous">Dauermodus: 1x Ausweis, dann N Medien</option>
<option value="return_only">Nur Rückgabe (nur Mediencode)</option>
</select>
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)">
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
<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>
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
</div>
<div id="filterPanel" class="library-filter-panel">
<div class="filter-row">
@@ -946,6 +932,32 @@
setScanStatus('Scanner gestoppt.', 'warn');
}
async function isStudentCardBarcode(code) {
if (!code) return false;
try {
const response = await fetch(`/api/is_student_card/${encodeURIComponent(code)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Falls du in Flask CSRF-Protect nutzt, muss der Token mitgesendet werden:
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
}
});
if (!response.ok) return false;
// Wir parsen die Antwort. Flask jsonify({"is_student_card": true/false})
const data = await response.json();
return data === true || data.is_student_card === true;
} catch (error) {
console.error('Fehler bei der API-Prüfung des Ausweises:', error);
return false;
}
}
Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return;
@@ -955,13 +967,6 @@
const currentCallback = activeScannerCallback;
stopScanner();
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
if (returnOnly) {
// direct return flow
returnByCode(barcode);
return;
}
if (typeof currentCallback === "function") {
currentCallback(barcode);
} else {
@@ -988,14 +993,8 @@
keyboardScanBuffer = '';
keyboardLastKeyAt = 0;
if (!code) return;
// If return-only mode is active, attempt direct return
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
if (returnOnly) {
returnByCode(code);
return;
}
// Process exactly like a camera scan
// Process exactly like a camera scan - centralized logic handles the rest!
handleScanSuccess(code);
return;
}
@@ -1014,33 +1013,123 @@
}
function handleScanSuccess(decodedText) {
const scannedCode = normalizeScannedCode(decodedText);
const scannedCode = typeof normalizeScannedCode === "function" ? normalizeScannedCode(decodedText) : decodedText;
if (!scannedCode) return;
const now = Date.now();
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
return;
return; // Verhindert doppeltes Scannen in kurzer Zeit
}
lastScanValue = scannedCode;
lastScanAt = now;
// Den aktuellen Modus aus dem Dropdown auslesen
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
// 1. Modus: Nur Rückgabe
if (mode === 'return_only') {
returnByCode(scannedCode);
return;
}
// 2. Modus: Nur Ausweis
if (mode === 'card_only') {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
processQuickToggleScan(scannedCode);
// 3. Modus: Schnellmodus (1x Ausweis, 1x Buch)
if (mode === 'quick_toggle') {
processQuickToggleScan(scannedCode);
return;
}
// 4. Modus: Dauermodus (1x Ausweis, Nx Bücher)
if (mode === 'continuous') {
processContinuousScan(scannedCode);
return;
}
}
async function processContinuousScan(scannedCode) {
setScanStatus('Überprüfe Code-Typ...', 'warn');
const isCard = await isStudentCardBarcode(scannedCode);
if (isCard) {
setActiveStudentCard(scannedCode);
setScanStatus(`Neuer Ausweis gesetzt: ${activeStudentCardId}. Bitte Medien scannen.`, 'ok');
showSmallConfirm(`Benutzer gewechselt zu: ${activeStudentCardId}`, 'ok');
setTimeout(() => {
if (document.getElementById('scanModeSelect').value === 'continuous') {
startScanner();
}
}, 1000);
return;
}
if (!activeStudentCardId) {
setScanStatus('Kein Ausweis aktiv! Bitte zuerst einen Schülerausweis scannen.', 'error');
showSmallConfirm('Bitte zuerst Ausweis scannen', 'error');
setTimeout(() => {
if (document.getElementById('scanModeSelect').value === 'continuous') {
startScanner();
}
}, 2000);
return;
}
try {
setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
},
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');
showSmallConfirm(result.message || 'Aktion fehlgeschlagen.', 'error');
} else 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 || 'Erfolgreich', 'ok');
}
// Tabellen-Ansicht aktualisieren
if (typeof loadLibraryItems === "function") {
await loadLibraryItems();
}
} catch (err) {
console.error('Continuous scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
showSmallConfirm('Fehler im Netzwerk/System', 'error');
}
setTimeout(() => {
if (document.getElementById('scanModeSelect').value === 'continuous') {
startScanner();
}
}, 1500);
}
async function processQuickToggleScan(scannedCode) {
// 1. Prüfen, ob "Nur Rückgabe"-Modus aktiv ist
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
if (returnOnly) {
await returnByCode(scannedCode);
return;
}
// 2. Wenn kein Ausweis gesetzt ist, wird der Code als Ausweis interpretiert
if (!activeStudentCardId) {
@@ -1434,111 +1523,30 @@
});
}
const manualReturnBtn = document.getElementById('manualReturnBtn');
const manualActionBtn = document.getElementById('manualActionBtn');
const manualItemCode = document.getElementById('manualItemCode');
if (manualReturnBtn && manualItemCode) {
manualReturnBtn.addEventListener('click', async () => {
if (manualActionBtn && manualItemCode) {
manualActionBtn.addEventListener('click', () => {
const code = (manualItemCode.value || '').trim();
if (!code) {
alert('Bitte einen Mediencode eingeben.');
alert('Bitte einen Code eingeben.');
return;
}
await returnByCode(code);
// Leitet den manuellen Code an die zentrale Logik weiter,
// die prüft, welcher Modus im Dropdown aktiv ist.
handleScanSuccess(code);
// Feld nach Eingabe leeren, um direkt den nächsten Code bereitzuhaben
manualItemCode.value = '';
});
}
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 codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]'));
// Daten aus dem Formular sammeln
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;
// API-Aufruf
try {
if (isGroupedEdit) {
const payload = {
series_group_id: currentItem.SeriesGroupId,
...sharedPayload,
items: groupMembers.map(member => ({
id: member._id,
code_4: codeByItemId.get(member._id) || ''
}))
};
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.');
// Optional: Auch auf "Enter" im Textfeld reagieren
manualItemCode.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
manualActionBtn.click();
}
});
}
@@ -1560,58 +1568,6 @@
}
}
function renderLibraryGroupCodeFields(groupMembers, currentItemId) {
const codesContainer = document.getElementById('editLibraryCodesContainer');
const groupWarning = document.getElementById('editLibraryGroupWarning');
const groupCount = document.getElementById('editLibraryGroupCount');
const groupHint = document.getElementById('editLibraryGroupHint');
if (!codesContainer) return;
const items = Array.isArray(groupMembers) ? groupMembers.slice() : [];
items.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0) || String(a.Name || '').localeCompare(String(b.Name || '')));
editLibraryState.groupMembers = items;
if (groupWarning) {
groupWarning.style.display = items.length > 1 ? 'block' : 'none';
}
if (groupCount) {
const totalCount = items.length || 1;
const declaredCount = items[0]?.SeriesCount || totalCount;
groupCount.textContent = `${totalCount} / ${declaredCount}`;
}
if (groupHint) {
groupHint.textContent = items.length > 1
? 'Jeder Code gehört zu einem eigenen Exemplar. Änderungen werden für alle Codes gespeichert.'
: 'Einzelnes Exemplar. Der Code wird direkt gespeichert.';
}
if (!items.length) {
codesContainer.innerHTML = '<div style="padding:10px 0; color:#6b7280;">Keine Codes geladen.</div>';
return;
}
codesContainer.innerHTML = items.map((member, index) => {
const codeValue = member.Code_4 || member.Code4 || '';
const labelParts = [];
if (member.SeriesPosition !== undefined && member.SeriesPosition !== null) {
labelParts.push(`Exemplar ${member.SeriesPosition}`);
} else {
labelParts.push(`Exemplar ${index + 1}`);
}
if (member._id === currentItemId) {
labelParts.push('aktuelles Medium');
}
return `
<div style="display:flex; flex-direction:column; gap:6px; margin-bottom:10px;">
<label for="editLibraryCode-${member._id}" style="font-weight:600; font-size:0.9em; color:var(--ui-text);">${escapeHtml(labelParts.join(' · '))}</label>
<input id="editLibraryCode-${member._id}" data-item-id="${escapeHtml(member._id)}" value="${escapeHtml(codeValue)}" placeholder="Mediencode" style="width:100%; padding:8px 10px; border:1px solid #d0d7e2; border-radius:6px;">
</div>
`;
}).join('');
}
function openEditLibraryItem(itemId) {
window.location.href = `/item_edit/${itemId}`;
}
+6 -80
View File
@@ -819,7 +819,7 @@
flex: 0 0 auto;
}
.ausleihen, .edit-button, .delete-button, .details-button, .duplicate-button, .schedule-button, .damage-button {
.ausleihen, .edit-button, .delete-button, .details-button, .schedule-button, .damage-button {
padding: 10px 18px;
border: none;
border-radius: 5px;
@@ -942,27 +942,6 @@
box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3);
}
/* Duplicate Button */
.duplicate-button {
background-color: #6f42c1;
color: white;
border: 2px solid #6f42c1;
}
.duplicate-button:hover {
background-color: #5a32a3;
border-color: #4e2a8e;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(111, 66, 193, 0.3);
color: white;
text-decoration: none;
}
.duplicate-button:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(111, 66, 193, 0.3);
}
.damage-button {
background-color: #fd7e14;
color: white;
@@ -1645,7 +1624,7 @@
gap: 10px !important;
}
.ausleihen, .edit-button, .delete-button, .details-button, .duplicate-button, .schedule-button {
.ausleihen, .edit-button, .delete-button, .details-button, .schedule-button {
width: 100% !important;
min-height: 44px !important;
padding: 12px 20px !important;
@@ -1777,7 +1756,7 @@
justify-content: flex-start;
}
.ausleihen, .edit-button, .delete-button, .details-button, .duplicate-button, .schedule-button {
.ausleihen, .edit-button, .delete-button, .details-button, .schedule-button {
padding: 10px 20px;
font-size: 0.9rem;
min-width: 110px;
@@ -1790,7 +1769,7 @@
gap: 8px;
}
.ausleihen, .edit-button, .delete-button, .details-button, .duplicate-button, .schedule-button {
.ausleihen, .edit-button, .delete-button, .details-button, .schedule-button {
padding: 8px 16px;
font-size: 0.85rem;
min-width: 95px;
@@ -1808,7 +1787,7 @@
gap: 15px;
}
.ausleihen, .edit-button, .delete-button, .details-button, .duplicate-button, .schedule-button {
.ausleihen, .edit-button, .delete-button, .details-button, .schedule-button {
padding: 12px 24px;
font-size: 1rem;
min-width: 120px;
@@ -1980,7 +1959,7 @@
}
/* Standardize form and document buttons */
.save-button, .cancel-button, .remove-book-cover-button, .remove-duplicate-image-button,
.save-button, .cancel-button, .remove-book-cover-button,
.import-book-button, .fetch-isbn-button, .nav-back-button {
display: inline-flex;
align-items: center;
@@ -3465,9 +3444,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
{% if current_permissions.actions.get('can_edit', False) %}
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button>
{% endif %}
{% if current_permissions.actions.get('can_insert', False) %}
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
{% endif %}
@@ -4244,7 +4220,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
: `<button class="ausleihen disabled-button" disabled>Ausgeliehen</button>`
}
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-modal-${item._id}')">Bearbeiten</button>
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
${damageReports && damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`}
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
<form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')">
@@ -5044,55 +5019,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
}
// Duplication function
function duplicateItem(itemId) {
// Show loading indicator
const loadingDiv = document.createElement('div');
loadingDiv.innerHTML = `
<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); z-index: 10000; display: flex;
align-items: center; justify-content: center;">
<div class="modal-dialog-white">
<div>Element wird dupliziert...</div>
<div class="modal-content-margin">
<div class="spinner"></div>
</div>
</div>
</div>
`;
document.body.appendChild(loadingDiv);
// Create form data for the duplicate_item request
const formData = new FormData();
formData.append('original_item_id', itemId);
// Send duplication request to get item data
fetch('/duplicate_item', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
document.body.removeChild(loadingDiv);
if (data.success) {
// Store duplication data in sessionStorage for the upload page
sessionStorage.setItem('duplicateItemData', JSON.stringify(data.item_data));
// Redirect to upload admin page
window.location.href = '/upload_admin?duplicate=true';
} else {
alert('Fehler beim Duplizieren: ' + (data.message || 'Unbekannter Fehler'));
}
})
.catch(error => {
document.body.removeChild(loadingDiv);
console.error('Error duplicating item:', error);
alert('Fehler beim Duplizieren des Elements. Bitte versuchen Sie es erneut.');
});
}
// Helper functions for appointment display
function formatAppointmentDate(dateString) {
if (!dateString) return '';
+4 -112
View File
@@ -11,59 +11,6 @@
{% block title %}{{ page_title|default('Artikel hochladen') }} - Inventarsystem{% endblock %}
{% block content %}
{% if duplicate_data and duplicate_data.images %}
<!-- Hidden server-side values for duplication, will be processed by JavaScript -->
<div id="server-duplicate-data"
data-name="{{ duplicate_data.name|default('') }}"
data-description="{{ duplicate_data.description|default('') }}"
data-location="{{ duplicate_data.location|default('') }}"
data-room="{{ duplicate_data.room|default('') }}"
data-year="{{ duplicate_data.year|default('') }}"
data-cost="{{ duplicate_data.cost|default('') }}"
data-images="{{ duplicate_data.images|tojson|safe }}"
data-filter1="{{ duplicate_data.filter1|tojson|safe if duplicate_data.filter1 else '[]' }}"
data-filter2="{{ duplicate_data.filter2|tojson|safe if duplicate_data.filter2 else '[]' }}"
data-filter3="{{ duplicate_data.filter3|tojson|safe if duplicate_data.filter3 else '[]' }}"
data-original-id="{{ duplicate_data.original_id|default('') }}"
style="display:none;">
</div>
<script>
// Pre-initialize duplicate data from server
var serverDuplicateData = null;
const dataElement = document.getElementById('server-duplicate-data');
if (dataElement) {
// Safely parse JSON arrays with error handling
function safeJsonParse(jsonStr, defaultValue = []) {
try {
if (!jsonStr) return defaultValue;
return JSON.parse(jsonStr);
} catch (e) {
console.error("Error parsing JSON:", e, "Value was:", jsonStr);
return defaultValue;
}
}
// Get images data and log it for debugging
const imagesData = dataElement.getAttribute('data-images') || "[]";
console.log("Raw images data attribute:", imagesData);
serverDuplicateData = {
name: dataElement.getAttribute('data-name') || "",
description: dataElement.getAttribute('data-description') || "",
location: dataElement.getAttribute('data-location') || "",
room: dataElement.getAttribute('data-room') || "",
year: dataElement.getAttribute('data-year') || "",
cost: dataElement.getAttribute('data-cost') || "",
images: safeJsonParse(imagesData),
filter1: safeJsonParse(dataElement.getAttribute('data-filter1')),
filter2: safeJsonParse(dataElement.getAttribute('data-filter2')),
filter3: safeJsonParse(dataElement.getAttribute('data-filter3')),
original_id: dataElement.getAttribute('data-original-id') || ""
};
console.log("Server-provided duplicate data:", serverDuplicateData);
}
</script>
{% endif %}
<style>
/* Book information display styles */
@@ -296,38 +243,11 @@
color: #721c24 !important;
}
/* Duplicate code popup styling */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
animation: overlayFadeIn 0.3s ease;
}
@keyframes overlayFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.duplicate-code-popup {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
text-align: center;
position: relative;
animation: popupSlideIn 0.3s ease;
}
@keyframes popupSlideIn {
from {
opacity: 0;
@@ -1878,11 +1798,11 @@
if (existingPopup) {
existingPopup.remove();
}
// Create popup overlay
const overlay = document.createElement('div');
overlay.className = 'popup-overlay';
// Create popup content
overlay.innerHTML = `
<div class="duplicate-code-popup">
@@ -1895,9 +1815,9 @@
<button class="popup-close-button" onclick="this.closest('.popup-overlay').remove()">Verstanden</button>
</div>
`;
document.body.appendChild(overlay);
// Auto-remove after 10 seconds
setTimeout(() => {
if (overlay.parentNode) {
@@ -2049,34 +1969,6 @@
}
});
}
// Handle duplication data if available (from sessionStorage or server)
if (typeof prefillFormWithDuplicateData === 'function') {
setTimeout(() => {
prefillFormWithDuplicateData();
}, 500); // Wait for all dropdowns to load
}
});
// Initialize duplication data - handle both server-side and sessionStorage data
let duplicateData = null;
// First check if serverDuplicateData is defined by the script above
if (typeof serverDuplicateData !== 'undefined' && serverDuplicateData) {
console.log("Using server-provided duplicateData:", serverDuplicateData);
duplicateData = serverDuplicateData;
// Ensure images is properly handled as an array
if (duplicateData.images) {
if (typeof duplicateData.images === 'string') {
try {
duplicateData.images = JSON.parse(duplicateData.images);
} catch (e) {
console.error("Error parsing duplicate images array:", e);
}
}
console.log("Parsed duplicate images successfully.");
}
}
</script>
{% endblock %}