Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a776872e0 | |||
| 55a74654db | |||
| 7473334714 |
+4
-202
@@ -3927,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
|
||||
@@ -3940,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,
|
||||
@@ -4014,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,
|
||||
@@ -5388,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):
|
||||
@@ -5819,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()
|
||||
|
||||
+100
-160
@@ -4,59 +4,60 @@
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
/* The Dark Background Overlay */
|
||||
/* =========================================
|
||||
1. Modals & Overlays
|
||||
========================================= */
|
||||
.modal {
|
||||
display: none; /* Managed by JS (none/flex) */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
/* Display is managed by JS (none/flex) */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inset: 0; /* Modern shorthand for top: 0, right: 0, bottom: 0, left: 0 */
|
||||
z-index: 1000;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* The White Modal Box */
|
||||
.modal-content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
|
||||
/* Flexbox allows the header to stay fixed while the body scrolls */
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 85vh;
|
||||
width: 100%;
|
||||
max-width: 900px; /* Consolidated from conflicting 600px/900px rules */
|
||||
max-height: 90vh;
|
||||
background-color: var(--ui-surface, #fff);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* The Close Button */
|
||||
.close {
|
||||
align-self: flex-end;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 20px;
|
||||
color: #aaa;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: #000;
|
||||
color: var(--ui-text, #000);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* The Scrollable Content Area */
|
||||
#detailContent {
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* Library table-only view styles */
|
||||
/* =========================================
|
||||
2. Layout & Headers
|
||||
========================================= */
|
||||
.library-table-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
@@ -79,13 +80,15 @@
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
/* Library search bar */
|
||||
/* =========================================
|
||||
3. Search, Filter & Scan Panels
|
||||
========================================= */
|
||||
.library-search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.library-search-input {
|
||||
@@ -95,10 +98,9 @@
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
background: white;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Scanner panel */
|
||||
.library-scan-panel {
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #d9dde4;
|
||||
@@ -129,26 +131,18 @@
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.library-scan-status.ok {
|
||||
color: #1f7a38;
|
||||
}
|
||||
|
||||
.library-scan-status.warn {
|
||||
color: #9a6700;
|
||||
}
|
||||
|
||||
.library-scan-status.error {
|
||||
color: #b42318;
|
||||
}
|
||||
.library-scan-status.ok { color: #1f7a38; }
|
||||
.library-scan-status.warn { color: #9a6700; }
|
||||
.library-scan-status.error { color: #b42318; }
|
||||
|
||||
.library-scan-reader-wrap {
|
||||
display: none;
|
||||
margin-top: 12px;
|
||||
max-width: 460px;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #d9dde4;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--ui-surface);
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.library-scan-reader {
|
||||
@@ -156,14 +150,15 @@
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
.library-filter-toggle-btn {
|
||||
padding: 10px 16px;
|
||||
background: #f0f2f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: var(--ui-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -178,10 +173,9 @@
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
/* Filter panel */
|
||||
.library-filter-panel {
|
||||
display: none;
|
||||
background: white;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
@@ -210,10 +204,10 @@
|
||||
}
|
||||
|
||||
.filter-item label {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
color: var(--ui-text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.filter-item input,
|
||||
@@ -222,7 +216,8 @@
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
background: #fff;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-item input:focus,
|
||||
@@ -236,8 +231,8 @@
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.filter-buttons .button {
|
||||
@@ -246,11 +241,13 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Library table */
|
||||
/* =========================================
|
||||
4. Tables & Data Display
|
||||
========================================= */
|
||||
.library-items-table {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
@@ -266,8 +263,8 @@
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--ui-text);
|
||||
font-size: 0.9em;
|
||||
color: var(--ui-text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -288,7 +285,6 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Table cells */
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
color: var(--ui-text);
|
||||
@@ -302,26 +298,14 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-available {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
.status-available { background: #e8f5e9; color: #2e7d32; }
|
||||
.status-borrowed { background: #fff3e0; color: #e65100; }
|
||||
.status-damaged { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
.status-borrowed {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.status-damaged {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-actions .button {
|
||||
@@ -330,26 +314,25 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.library-empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.library-empty-state h3 {
|
||||
font-size: 1.3em;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.library-load-row {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.library-load-hint {
|
||||
@@ -357,54 +340,9 @@
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Small confirmation popup (toast) */
|
||||
.small-popup {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(17, 24, 39, 0.96);
|
||||
color: #fff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 24px rgba(2,6,23,0.6);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
max-width: 90%;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.small-popup.ok { background: rgba(16,185,129,0.95); color: #032; }
|
||||
.small-popup.error { background: rgba(239,68,68,0.95); color: #210; }
|
||||
.small-popup .close-x { margin-left: 8px; cursor: pointer; font-weight: 700; }
|
||||
|
||||
/* Modal styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--ui-surface);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
max-width: 900px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
5. Edit Grid Forms
|
||||
========================================= */
|
||||
.edit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -416,10 +354,10 @@
|
||||
}
|
||||
|
||||
.edit-grid label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.edit-grid input,
|
||||
@@ -437,37 +375,44 @@
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 10px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
/* =========================================
|
||||
6. Notifications / Toasts
|
||||
========================================= */
|
||||
.small-popup {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
max-width: 90%;
|
||||
padding: 12px 16px;
|
||||
background: rgba(17, 24, 39, 0.96);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95em;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0 6px 24px rgba(2,6,23,0.6);
|
||||
}
|
||||
|
||||
.small-popup.ok { background: rgba(16,185,129,0.95); color: #032; }
|
||||
.small-popup.error { background: rgba(239,68,68,0.95); color: #210; }
|
||||
|
||||
.small-popup .close-x {
|
||||
margin-left: 8px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
/* =========================================
|
||||
7. Mobile Responsiveness
|
||||
========================================= */
|
||||
@media (max-width: 768px) {
|
||||
.library-table-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.library-search-bar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.library-search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.library-items-table {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.library-table-container { padding: 10px; }
|
||||
.library-search-bar { flex-direction: column; }
|
||||
.library-search-input { width: 100%; }
|
||||
.library-items-table { font-size: 0.85em; }
|
||||
|
||||
.library-items-table th,
|
||||
.library-items-table td {
|
||||
@@ -479,17 +424,12 @@
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter-row,
|
||||
.library-scan-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.library-scan-reader-wrap {
|
||||
max-width: 100%;
|
||||
}
|
||||
.library-scan-reader-wrap { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -939,7 +879,7 @@
|
||||
const response = await fetch(`/api/is_student_card/${encodeURIComponent(code)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'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 }}'
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user