Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0f2afae57 | |||
| 1a99448a2e | |||
| aec25a13b9 | |||
| b31abe2177 | |||
| 7fd27e8c02 | |||
| 5f5784e7c3 | |||
| 613c1e11e4 | |||
| 9e3b54c1b1 | |||
| a8bce26eab | |||
| 36c391e99c | |||
| 40c3970719 | |||
| e7f4b1ce9d | |||
| d84466a3cc | |||
| 0efc01a835 | |||
| 73cf63a56d | |||
| fdcd26326b | |||
| 1fd9e230ac | |||
| cc0c4e28b7 |
+94
-14
@@ -42,6 +42,7 @@ import Web.modules.database.termine as termin
|
|||||||
import Web.modules.log.audit_log as al
|
import Web.modules.log.audit_log as al
|
||||||
import push_notifications as pn
|
import push_notifications as pn
|
||||||
import Web.modules.inventarsystem.pdf_export as pdf_export
|
import Web.modules.inventarsystem.pdf_export as pdf_export
|
||||||
|
import Web.modules.inventarsystem.excel_export as excel_export
|
||||||
import datetime
|
import datetime
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
@@ -288,7 +289,7 @@ SCHEDULER_INTERVAL = cfg.SCHEDULER_INTERVAL_MIN
|
|||||||
SSL_CERT = cfg.SSL_CERT
|
SSL_CERT = cfg.SSL_CERT
|
||||||
SSL_KEY = cfg.SSL_KEY
|
SSL_KEY = cfg.SSL_KEY
|
||||||
|
|
||||||
LIBRARY_ITEM_TYPES = ['book', 'cd', 'dvd', 'media', 'schulbuch']
|
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch')
|
||||||
INVOICE_CURRENCY = 'EUR'
|
INVOICE_CURRENCY = 'EUR'
|
||||||
|
|
||||||
NOTIFICATION_STATUS_CACHE_TTL = max(3, int(os.getenv('INVENTAR_NOTIFICATION_STATUS_CACHE_TTL', '8')))
|
NOTIFICATION_STATUS_CACHE_TTL = max(3, int(os.getenv('INVENTAR_NOTIFICATION_STATUS_CACHE_TTL', '8')))
|
||||||
@@ -2476,6 +2477,7 @@ def _upload_excel_items(scope='inventory'):
|
|||||||
parent_item_id=parent_item_id,
|
parent_item_id=parent_item_id,
|
||||||
isbn=row['isbn'],
|
isbn=row['isbn'],
|
||||||
item_type=row['item_type'],
|
item_type=row['item_type'],
|
||||||
|
is_library=False
|
||||||
)
|
)
|
||||||
if not item_id:
|
if not item_id:
|
||||||
import_errors.append((row['row_number'], 'Fehler beim Anlegen des Artikels'))
|
import_errors.append((row['row_number'], 'Fehler beim Anlegen des Artikels'))
|
||||||
@@ -2979,33 +2981,50 @@ def library_export_excel(scope='all'):
|
|||||||
username = session['username']
|
username = session['username']
|
||||||
is_admin_user = us.check_admin(username)
|
is_admin_user = us.check_admin(username)
|
||||||
|
|
||||||
import Web.modules.inventarsystem.excel_export as excel_export
|
# Base query
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
query = {
|
||||||
db = client[cfg.MONGODB_DB]
|
'ItemType': {'$in': LIBRARY_ITEM_TYPES},
|
||||||
items_collection = db['items']
|
'Deleted': {'$ne': True}
|
||||||
|
}
|
||||||
query = {'ItemType': {'$in': LIBRARY_ITEM_TYPES}, 'Deleted': {'$ne': True}}
|
|
||||||
|
|
||||||
|
# Scope modifications
|
||||||
if scope == 'borrowed_by_me':
|
if scope == 'borrowed_by_me':
|
||||||
query['User'] = username
|
query['User'] = username
|
||||||
query['Verfuegbar'] = False
|
query['Verfuegbar'] = False
|
||||||
filename = f"Bibliothek_Ausgeliehen_{username}.xlsx"
|
filename = f"Bibliothek_Ausgeliehen_{username}.xlsx"
|
||||||
|
|
||||||
elif scope == 'all_borrowed':
|
elif scope == 'all_borrowed':
|
||||||
if not is_admin_user:
|
if not is_admin_user:
|
||||||
|
# Consider adding a flash message here before redirecting
|
||||||
return redirect('/library')
|
return redirect('/library')
|
||||||
query['Verfuegbar'] = False
|
query['Verfuegbar'] = False
|
||||||
filename = "Bibliothek_Alle_Ausleihen.xlsx"
|
filename = "Bibliothek_Alle_Ausleihen.xlsx"
|
||||||
|
|
||||||
elif scope == 'schulbuecher':
|
elif scope == 'schulbuecher':
|
||||||
|
# Overwrites the base $in query, which is fine
|
||||||
query['ItemType'] = 'schulbuch'
|
query['ItemType'] = 'schulbuch'
|
||||||
filename = "Schulbuecher_Bestand.xlsx"
|
filename = "Schulbuecher_Bestand.xlsx"
|
||||||
|
|
||||||
|
elif scope == 'all_books':
|
||||||
|
# FIX: Keep the library item types restriction, but exclude 'general'
|
||||||
|
query['ItemType'] = {'$in': [t for t in LIBRARY_ITEM_TYPES if t != 'general']}
|
||||||
|
filename = "Bibliothek_Buecher.xlsx"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 'all'
|
# 'all' scope
|
||||||
filename = "Bibliothek_Gesamtbestand.xlsx"
|
filename = "Bibliothek_Gesamtbestand.xlsx"
|
||||||
|
|
||||||
|
# Database interaction
|
||||||
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
items_collection = db['items']
|
||||||
|
|
||||||
items = list(items_collection.find(query))
|
items = list(items_collection.find(query))
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
# Generate and send file
|
||||||
excel_file = excel_export.generate_library_excel(items)
|
excel_file = excel_export.generate_library_excel(items)
|
||||||
|
|
||||||
return send_file(
|
return send_file(
|
||||||
excel_file,
|
excel_file,
|
||||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
@@ -3193,7 +3212,7 @@ def api_library_items():
|
|||||||
ausleihungen_db = db['ausleihungen']
|
ausleihungen_db = db['ausleihungen']
|
||||||
|
|
||||||
query = {
|
query = {
|
||||||
'ItemType': {'$in': ['book', 'cd', 'dvd', 'media']},
|
'ItemType': {'$in': ['book', 'cd', 'dvd', 'other', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']},
|
||||||
'IsGroupedSubItem': {'$ne': True},
|
'IsGroupedSubItem': {'$ne': True},
|
||||||
'Deleted': {'$ne': True}
|
'Deleted': {'$ne': True}
|
||||||
}
|
}
|
||||||
@@ -5115,6 +5134,8 @@ def upload_item():
|
|||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
|
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
|
||||||
|
|
||||||
|
item_is_library = False
|
||||||
|
|
||||||
# Check if user may insert items
|
# Check if user may insert items
|
||||||
username = session['username']
|
username = session['username']
|
||||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||||
@@ -5159,6 +5180,9 @@ def upload_item():
|
|||||||
item_type_input = sanitize_form_value(request.form.get('item_type_input', ''))
|
item_type_input = sanitize_form_value(request.form.get('item_type_input', ''))
|
||||||
library_category = sanitize_form_value(request.form.get('library_category', ''))
|
library_category = sanitize_form_value(request.form.get('library_category', ''))
|
||||||
|
|
||||||
|
if library_caregory != '':
|
||||||
|
item_is_library = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item_count = int(item_count_raw) if item_count_raw else 1
|
item_count = int(item_count_raw) if item_count_raw else 1
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
@@ -5227,7 +5251,7 @@ def upload_item():
|
|||||||
return redirect(url_for(success_redirect_endpoint))
|
return redirect(url_for(success_redirect_endpoint))
|
||||||
|
|
||||||
item_isbn = ''
|
item_isbn = ''
|
||||||
item_type = 'general'
|
item_type = 'other'
|
||||||
if cfg.MODULES.is_enabled('library') and isbn_raw:
|
if cfg.MODULES.is_enabled('library') and isbn_raw:
|
||||||
normalized_isbn = normalize_and_validate_isbn(isbn_raw)
|
normalized_isbn = normalize_and_validate_isbn(isbn_raw)
|
||||||
if normalized_isbn:
|
if normalized_isbn:
|
||||||
@@ -5235,8 +5259,7 @@ def upload_item():
|
|||||||
item_type = 'book'
|
item_type = 'book'
|
||||||
else:
|
else:
|
||||||
item_isbn = isbn_raw
|
item_isbn = isbn_raw
|
||||||
if upload_mode == 'library':
|
|
||||||
item_type = 'book'
|
|
||||||
if item_type_input != "":
|
if item_type_input != "":
|
||||||
item_type = item_type_input
|
item_type = item_type_input
|
||||||
|
|
||||||
@@ -5253,7 +5276,6 @@ def upload_item():
|
|||||||
return jsonify({'success': False, 'message': error_msg}), 400
|
return jsonify({'success': False, 'message': error_msg}), 400
|
||||||
flash(error_msg, 'error')
|
flash(error_msg, 'error')
|
||||||
return redirect(url_for('library_admin'))
|
return redirect(url_for('library_admin'))
|
||||||
item_type = 'book'
|
|
||||||
|
|
||||||
# Only check for images if not duplicating and no duplicate images provided and no book cover
|
# Only check for images if not duplicating and no duplicate images provided and no book cover
|
||||||
# For library mode, skip this check as images come only from ISBN fetch
|
# For library mode, skip this check as images come only from ISBN fetch
|
||||||
@@ -6047,7 +6069,8 @@ def upload_item():
|
|||||||
parent_item_id=parent_item_id,
|
parent_item_id=parent_item_id,
|
||||||
isbn=item_isbn,
|
isbn=item_isbn,
|
||||||
item_type=item_type,
|
item_type=item_type,
|
||||||
library_category=library_category
|
library_category=library_category,
|
||||||
|
is_library=item_is_library
|
||||||
)
|
)
|
||||||
if not item_id:
|
if not item_id:
|
||||||
break
|
break
|
||||||
@@ -6627,6 +6650,63 @@ def edit_item(id):
|
|||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/update_group', methods=['POST'])
|
||||||
|
def update_group():
|
||||||
|
data = request.get_json()
|
||||||
|
series_group_id = data.get('series_group_id')
|
||||||
|
|
||||||
|
if not series_group_id:
|
||||||
|
return jsonify({'success': False, 'message': 'Keine Gruppen-ID'}), 400
|
||||||
|
|
||||||
|
# 1. Shared Fields (Group Logic)
|
||||||
|
# These apply to every item in the group
|
||||||
|
shared_update = {
|
||||||
|
'Name': data.get('name'),
|
||||||
|
'Ort': data.get('ort'),
|
||||||
|
'Beschreibung': data.get('beschreibung'),
|
||||||
|
'Anschaffungsjahr': data.get('ansch_jahr'),
|
||||||
|
'Anschaffungskosten': data.get('ansch_kost'),
|
||||||
|
'Reservierbar': data.get('reservierbar'),
|
||||||
|
'ISBN': data.get('isbn'),
|
||||||
|
'ItemType': data.get('item_type'),
|
||||||
|
'LastUpdated': datetime.datetime.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Individual Updates (Specific Code Logic)
|
||||||
|
# Expected format: [{'id': '...', 'code_4': '...'}, ...]
|
||||||
|
individual_items = data.get('items', [])
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
items_col = db['items']
|
||||||
|
|
||||||
|
# A. Apply Shared Attributes to the whole group
|
||||||
|
items_col.update_many(
|
||||||
|
{'SeriesGroupId': series_group_id},
|
||||||
|
{'$set': shared_update}
|
||||||
|
)
|
||||||
|
|
||||||
|
# B. Apply Unique Codes to specific items
|
||||||
|
# We iterate through the provided list to update the specific code for each ID
|
||||||
|
for item in individual_items:
|
||||||
|
item_id = item.get('id')
|
||||||
|
new_code = item.get('code_4')
|
||||||
|
|
||||||
|
if item_id:
|
||||||
|
items_col.update_one(
|
||||||
|
{'_id': ObjectId(item_id), 'SeriesGroupId': series_group_id},
|
||||||
|
{'$set': {'Code_4': new_code}}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
return jsonify({'success': True, 'message': 'Gruppe und individuelle Codes aktualisiert'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Error updating group {series_group_id}: {e}")
|
||||||
|
return jsonify({'success': False, 'message': 'Systemfehler beim Update'}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/report_damage/<id>', methods=['POST'])
|
@app.route('/report_damage/<id>', methods=['POST'])
|
||||||
def report_damage(id):
|
def report_damage(id):
|
||||||
"""Register a damage report entry for an item (admin only)."""
|
"""Register a damage report entry for an item (admin only)."""
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import Web.modules.database.settings as cfg
|
|||||||
from Web.modules.database.settings import MongoClient
|
from Web.modules.database.settings import MongoClient
|
||||||
|
|
||||||
|
|
||||||
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'media', 'schulbuch')
|
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch')
|
||||||
|
|
||||||
|
|
||||||
def _non_library_query(extra_query=None):
|
def _non_library_query(extra_query=None):
|
||||||
@@ -49,7 +49,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
|
|||||||
ansch_jahr=None, ansch_kost=None, code_4=None, reservierbar=True,
|
ansch_jahr=None, ansch_kost=None, code_4=None, reservierbar=True,
|
||||||
series_group_id=None, series_count=1, series_position=1,
|
series_group_id=None, series_count=1, series_position=1,
|
||||||
is_grouped_sub_item=False, parent_item_id=None,
|
is_grouped_sub_item=False, parent_item_id=None,
|
||||||
isbn=None, item_type='general', library_category=None):
|
isbn=None, item_type='general', library_category=None, is_library=False):
|
||||||
"""
|
"""
|
||||||
Add a new item to the inventory.
|
Add a new item to the inventory.
|
||||||
|
|
||||||
@@ -102,6 +102,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
|
|||||||
'ISBN': isbn,
|
'ISBN': isbn,
|
||||||
'library_category': library_category,
|
'library_category': library_category,
|
||||||
'ItemType': item_type,
|
'ItemType': item_type,
|
||||||
|
'is_library': is_library,
|
||||||
'SeriesGroupId': series_group_id,
|
'SeriesGroupId': series_group_id,
|
||||||
'SeriesCount': series_count,
|
'SeriesCount': series_count,
|
||||||
'SeriesPosition': series_position,
|
'SeriesPosition': series_position,
|
||||||
|
|||||||
@@ -37,18 +37,21 @@ def generate_library_excel(items):
|
|||||||
item.get("ReturnDate", ""),
|
item.get("ReturnDate", ""),
|
||||||
item.get("Anschaffungskosten", "")
|
item.get("Anschaffungskosten", "")
|
||||||
]
|
]
|
||||||
ws.append(row)
|
|
||||||
|
|
||||||
for col in ws.columns:
|
# FIX: Clean the row data INSIDE the loop for every single item
|
||||||
max_length = 0
|
clean_row = []
|
||||||
column = col[0].column_letter
|
for value in row:
|
||||||
for cell in col:
|
if isinstance(value, list):
|
||||||
try:
|
# Converts ['A', 'B'] to "A, B" and an empty list [] to an empty string ""
|
||||||
if len(str(cell.value)) > max_length:
|
clean_row.append(", ".join(map(str, value)) if value else "")
|
||||||
max_length = len(str(cell.value))
|
elif isinstance(value, dict):
|
||||||
except:
|
# Just in case there is an embedded MongoDB sub-document
|
||||||
pass
|
clean_row.append(str(value))
|
||||||
ws.column_dimensions[column].width = min(max_length + 2, 50)
|
else:
|
||||||
|
clean_row.append(value)
|
||||||
|
|
||||||
|
# Append the safe, flattened row to the worksheet
|
||||||
|
ws.append(clean_row)
|
||||||
|
|
||||||
excel_buffer = io.BytesIO()
|
excel_buffer = io.BytesIO()
|
||||||
wb.save(excel_buffer)
|
wb.save(excel_buffer)
|
||||||
|
|||||||
+182
-109
@@ -4,6 +4,57 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<style>
|
<style>
|
||||||
|
/* The Dark Background Overlay */
|
||||||
|
.modal {
|
||||||
|
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;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 85vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Close Button */
|
||||||
|
.close {
|
||||||
|
align-self: flex-end;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover,
|
||||||
|
.close:focus {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Scrollable Content Area */
|
||||||
|
#detailContent {
|
||||||
|
overflow-y: auto; /* Adds scrollbar only if needed */
|
||||||
|
padding-right: 10px; /* Prevents text from rubbing against the scrollbar */
|
||||||
|
}
|
||||||
/* Library table-only view styles */
|
/* Library table-only view styles */
|
||||||
.library-table-container {
|
.library-table-container {
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
@@ -475,7 +526,7 @@
|
|||||||
<select id="filterType">
|
<select id="filterType">
|
||||||
<option value="">-- Alle Typen --</option>
|
<option value="">-- Alle Typen --</option>
|
||||||
<option value="book">Buch</option>
|
<option value="book">Buch</option>
|
||||||
<option value="schulbuch">Schulbuch</option>
|
<option value="schoolbook">Schulbuch</option>
|
||||||
<option value="cd">CD</option>
|
<option value="cd">CD</option>
|
||||||
<option value="dvd">DVD</option>
|
<option value="dvd">DVD</option>
|
||||||
<option value="other">Sonstige</option>
|
<option value="other">Sonstige</option>
|
||||||
@@ -503,6 +554,10 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="8" y1="13" x2="16" y2="13"></line><line x1="8" y1="17" x2="16" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="8" y1="13" x2="16" y2="13"></line><line x1="8" y1="17" x2="16" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||||
Alle ausgeliehen Excel
|
Alle ausgeliehen Excel
|
||||||
</button>
|
</button>
|
||||||
|
<button class="button" style="background: #10b981; color: white; display: flex; align-items: center; gap: 0.5rem;" onclick="window.location.href='/library/export/all_books'">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="8" y1="13" x2="16" y2="13"></line><line x1="8" y1="17" x2="16" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||||
|
Alle Bücher Excel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -536,15 +591,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detail Modal -->
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
|
||||||
|
|
||||||
<div id="detailModal" class="modal" style="display: none;">
|
<div id="detailModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span class="close" onclick="closeDetailModal()">×</span>
|
<span class="close" onclick="closeDetailModal()">×</span>
|
||||||
<div id="detailContent"></div>
|
<div id="detailContent"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -1033,7 +1087,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getItemTypeLabel(type) {
|
function getItemTypeLabel(type) {
|
||||||
const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige' };
|
const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch' };
|
||||||
return labels[type] || type;
|
return labels[type] || type;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1043,20 +1097,47 @@
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Opens the modal and fetches the data
|
||||||
function showItemDetail(itemId) {
|
function showItemDetail(itemId) {
|
||||||
|
const detailContent = document.getElementById('detailContent');
|
||||||
|
const detailModal = document.getElementById('detailModal');
|
||||||
|
|
||||||
|
// 1. Show the loading state immediately
|
||||||
|
detailContent.innerHTML = '<p>Loading details...</p>';
|
||||||
|
detailModal.style.display = 'flex';
|
||||||
|
|
||||||
|
// 2. Fetch the data
|
||||||
fetch(`/api/item_detail/${itemId}`)
|
fetch(`/api/item_detail/${itemId}`)
|
||||||
.then(r => r.text())
|
.then(response => {
|
||||||
.then(html => {
|
if (!response.ok) {
|
||||||
document.getElementById('detailContent').innerHTML = html;
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
document.getElementById('detailModal').style.display = 'flex';
|
}
|
||||||
|
return response.text();
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Error loading detail:', err));
|
.then(html => {
|
||||||
|
// 3. Clean the HTML and display it
|
||||||
|
detailContent.innerHTML = DOMPurify.sanitize(html);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error loading detail:', err);
|
||||||
|
detailContent.innerHTML = '<p>Sorry, we could not load the item details. Please try again later.</p>';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Closes the modal via the 'x' button
|
||||||
function closeDetailModal() {
|
function closeDetailModal() {
|
||||||
document.getElementById('detailModal').style.display = 'none';
|
document.getElementById('detailModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Closes the modal if the user clicks the dark background overlay
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('detailModal');
|
||||||
|
// If the click happened directly on the dark overlay (not the white box)
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeDetailModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
|
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -1192,145 +1273,137 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function openEditLibraryItem(itemId) {
|
window.openEditLibraryItem = function(itemId) {
|
||||||
const item = libraryItems.find(i => i._id === itemId);
|
const item = libraryItems.find(i => i._id === itemId);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
if (!item) {
|
// 1. Felder befüllen
|
||||||
alert('Fehler: Das Medium konnte nicht gefunden werden.');
|
document.getElementById('editLibraryItemId').value = item._id;
|
||||||
return;
|
document.getElementById('editLibraryName').value = item.Name;
|
||||||
}
|
document.getElementById('editLibraryType').value = item.ItemType;
|
||||||
|
|
||||||
// 1. Formular-Felder normal befüllen
|
|
||||||
document.getElementById('editLibraryItemId').value = item._id || '';
|
|
||||||
document.getElementById('editLibraryName').value = item.Name || '';
|
|
||||||
document.getElementById('editLibraryType').value = item.ItemType || 'book';
|
|
||||||
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
|
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
|
||||||
document.getElementById('editLibraryCode4').value = item.Code_4 || item.Code4 || '';
|
document.getElementById('editLibraryCode4').value = item.Code_4 || '';
|
||||||
document.getElementById('editLibraryLocation').value = item.Ort || '';
|
document.getElementById('editLibraryLocation').value = item.Ort;
|
||||||
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
|
document.getElementById('editLibraryDescription').value = item.Beschreibung;
|
||||||
|
|
||||||
// 2. Gruppen-Logik: Hole ALLE Items der Gruppe
|
// 2. Gruppen-Logik
|
||||||
const warningDiv = document.getElementById('editLibraryGroupWarning');
|
const warningDiv = document.getElementById('editLibraryGroupWarning');
|
||||||
const groupCodeContainer = document.getElementById('editLibraryAllCodes');
|
const codesContainer = document.getElementById('editLibraryAllCodes');
|
||||||
|
|
||||||
// Prüfen ob Gruppenmitgliedschaft besteht
|
|
||||||
if (item.SeriesGroupId) {
|
if (item.SeriesGroupId) {
|
||||||
// Alle Items mit gleicher ID finden und nach Position sortieren
|
// Filtern aus dem aktuell geladenen Array
|
||||||
const groupMembers = libraryItems
|
let groupMembers = libraryItems.filter(i => i.SeriesGroupId === item.SeriesGroupId);
|
||||||
.filter(i => i.SeriesGroupId === item.SeriesGroupId)
|
|
||||||
.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0));
|
|
||||||
|
|
||||||
// Liste der Codes erstellen
|
// SCHLÜSSEL: Wenn die Anzahl der gefundenen Elemente nicht mit SeriesCount übereinstimmt,
|
||||||
let codesListHtml = `
|
// haben wir die Gruppe noch nicht vollständig geladen.
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; margin-top:10px;">
|
if (groupMembers.length < (item.SeriesCount || 0)) {
|
||||||
`;
|
console.warn("Gruppe noch nicht vollständig geladen. Anzeige ggf. unvollständig.");
|
||||||
|
// Optional: Zeige einen Ladehinweis im Modal
|
||||||
|
codesContainer.textContent = "Lade restliche Gruppenmitglieder...";
|
||||||
|
} else {
|
||||||
|
// Daten sind vollständig -> Anzeigen
|
||||||
|
const codeList = groupMembers
|
||||||
|
.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0))
|
||||||
|
.map(m => m.Code_4 || "---")
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
groupMembers.forEach(member => {
|
codesContainer.textContent = codeList;
|
||||||
const isCurrent = member._id === itemId;
|
}
|
||||||
// Wenn Code_4 nicht existiert, zeige "Kein Code"
|
|
||||||
const codeVal = member.Code_4 || "---";
|
|
||||||
|
|
||||||
codesListHtml += `
|
|
||||||
<div style="text-align: center; padding: 5px; border-radius: 4px;
|
|
||||||
background: ${isCurrent ? '#0ea5e9' : '#f1f1f1'};
|
|
||||||
color: ${isCurrent ? '#fff' : '#333'};
|
|
||||||
border: 1px solid ${isCurrent ? '#0ea5e9' : '#ccc'};">
|
|
||||||
<span style="font-size: 10px; display:block;">Pos: ${member.SeriesPosition}</span>
|
|
||||||
<strong style="font-size: 14px;">${codeVal}</strong>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
codesListHtml += `</div>`;
|
|
||||||
|
|
||||||
document.getElementById('editLibraryGroupCount').textContent = groupMembers.length;
|
|
||||||
groupCodeContainer.innerHTML = codesListHtml;
|
|
||||||
|
|
||||||
|
document.getElementById('editLibraryGroupCount').textContent = groupMembers.length + " / " + (item.SeriesCount || "?");
|
||||||
warningDiv.style.display = 'block';
|
warningDiv.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
warningDiv.style.display = 'none';
|
warningDiv.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('editLibraryModal').style.display = 'flex';
|
document.getElementById('editLibraryModal').style.display = 'flex';
|
||||||
}
|
};
|
||||||
|
|
||||||
function closeEditLibraryModal() {
|
function closeEditLibraryModal() {
|
||||||
document.getElementById('editLibraryModal').style.display = 'none';
|
document.getElementById('editLibraryModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Sicherer Event-Listener für das Formular ---
|
/**
|
||||||
|
* Event-Listener für das Formular (Initialisierung)
|
||||||
|
*/
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const editForm = document.getElementById('editLibraryForm');
|
const editForm = document.getElementById('editLibraryForm');
|
||||||
|
|
||||||
if (editForm) {
|
if (editForm) {
|
||||||
editForm.addEventListener('submit', async function(e) {
|
editForm.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const itemId = document.getElementById('editLibraryItemId').value;
|
const itemId = document.getElementById('editLibraryItemId').value;
|
||||||
const updateData = {
|
const currentItem = libraryItems.find(i => i._id === itemId);
|
||||||
id: itemId,
|
|
||||||
|
if (!currentItem) return;
|
||||||
|
|
||||||
|
// 1. Alle Mitglieder der Gruppe finden, um die Code-Liste aufzubauen
|
||||||
|
const groupMembers = libraryItems.filter(i => i.SeriesGroupId === currentItem.SeriesGroupId);
|
||||||
|
const individualUpdates = groupMembers.map(member => ({
|
||||||
|
id: member._id,
|
||||||
|
// Wenn dies das bearbeitete Item ist, nimm den neuen Code, sonst den alten
|
||||||
|
code_4: (member._id === itemId) ? document.getElementById('editLibraryCode4').value : member.Code_4
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 2. Payload für das Backend bauen
|
||||||
|
const payload = {
|
||||||
|
series_group_id: currentItem.SeriesGroupId,
|
||||||
name: document.getElementById('editLibraryName').value,
|
name: document.getElementById('editLibraryName').value,
|
||||||
item_type: document.getElementById('editLibraryType').value,
|
|
||||||
isbn: document.getElementById('editLibraryIsbn').value,
|
|
||||||
code_4: document.getElementById('editLibraryCode4').value,
|
|
||||||
ort: document.getElementById('editLibraryLocation').value,
|
ort: document.getElementById('editLibraryLocation').value,
|
||||||
beschreibung: document.getElementById('editLibraryDescription').value
|
beschreibung: document.getElementById('editLibraryDescription').value,
|
||||||
|
isbn: document.getElementById('editLibraryIsbn').value,
|
||||||
|
item_type: document.getElementById('editLibraryType').value,
|
||||||
|
items: individualUpdates
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 3. Request an die Gruppen-Update Route
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/update-item', {
|
const response = await fetch('/update_group', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(updateData)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
const result = await response.json();
|
||||||
const editedItem = libraryItems.find(i => i._id === itemId);
|
if (result.success) {
|
||||||
|
alert('Gruppe erfolgreich synchronisiert!');
|
||||||
// Lokale Daten im Array synchronisieren
|
|
||||||
if (editedItem && editedItem.SeriesGroupId) {
|
|
||||||
libraryItems.forEach(item => {
|
|
||||||
if (item.SeriesGroupId === editedItem.SeriesGroupId) {
|
|
||||||
item.Name = updateData.name;
|
|
||||||
item.ItemType = updateData.item_type;
|
|
||||||
item.ISBN = updateData.isbn;
|
|
||||||
item.Ort = updateData.ort;
|
|
||||||
item.Beschreibung = updateData.beschreibung;
|
|
||||||
|
|
||||||
if (item._id === itemId) item.Code_4 = updateData.code_4;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (editedItem) {
|
|
||||||
Object.assign(editedItem, updateData);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeEditLibraryModal();
|
closeEditLibraryModal();
|
||||||
// Optional: renderLibraryTable();
|
await loadLibraryItems(); // Daten neu laden
|
||||||
|
// renderTable(); // Ggf. Tabelle neu rendern
|
||||||
} else {
|
} else {
|
||||||
const err = await response.json();
|
alert('Fehler: ' + result.message);
|
||||||
alert('Fehler beim Speichern: ' + (err.error || 'Unbekannter Fehler'));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch error:', error);
|
console.error('Update failed:', error);
|
||||||
alert('Netzwerkfehler.');
|
alert('Netzwerkfehler.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="editLibraryModal" class="modal" style="display:none;">
|
<div id="editLibraryModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content" style="max-width: 760px;">
|
<div class="modal-content" style="max-width: 760px; padding: 25px; border-radius: 8px;">
|
||||||
<span class="close" onclick="closeEditLibraryModal()">×</span>
|
<span class="close" onclick="closeEditLibraryModal()" style="cursor: pointer; float: right; font-size: 24px;">×</span>
|
||||||
<h3 style="margin-top:0;">Bibliotheksmedium bearbeiten</h3>
|
<h3 style="margin-top:0;">Bibliotheksmedium bearbeiten</h3>
|
||||||
|
|
||||||
<!-- Bereich für Gruppen-Informationen -->
|
<!-- Bereich für Gruppen-Informationen (Hier konsolidiert!) -->
|
||||||
<div id="editLibraryGroupWarning" style="display:none; background-color: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #dee2e6;">
|
<div id="editLibraryGroupWarning" style="display:none; background-color: #fff; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #0ea5e9;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
|
||||||
<strong style="color: #0ea5e9;">Gruppen-Synchronisierung aktiv</strong>
|
<strong style="color: #0ea5e9;">Gruppen-Range (Total: <span id="editLibraryGroupCount"></span>)</strong>
|
||||||
<span style="font-size: 12px; color: #666;">Gesamt: <span id="editLibraryGroupCount"></span> Exemplare</span>
|
</div>
|
||||||
|
|
||||||
|
<p style="margin: 5px 0; font-size: 12px; color: #555;">
|
||||||
|
Alle aufgeführten Codes gehören zu diesem Datensatz:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Hier werden die Codes per JS eingefügt -->
|
||||||
|
<div id="editLibraryAllCodes" style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px;"></div>
|
||||||
|
|
||||||
|
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
|
||||||
|
<strong>Hinweis:</strong> Änderungen an Titel/Ort/Beschreibung werden auf <strong>alle</strong> Exemplare der Range übertragen.
|
||||||
</div>
|
</div>
|
||||||
<p style="margin: 0 0 10px 0; font-size: 13px;">Codes in dieser Range:</p>
|
|
||||||
<div id="editLibraryAllCodes" style="display: flex; flex-wrap: wrap; gap: 5px;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="editLibraryForm">
|
<form id="editLibraryForm">
|
||||||
@@ -1338,37 +1411,37 @@
|
|||||||
<div class="edit-grid">
|
<div class="edit-grid">
|
||||||
<div class="full">
|
<div class="full">
|
||||||
<label for="editLibraryName">Titel</label>
|
<label for="editLibraryName">Titel</label>
|
||||||
<input id="editLibraryName" required>
|
<input id="editLibraryName" required style="width: 100%;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="editLibraryType">Medientyp</label>
|
<label for="editLibraryType">Medientyp</label>
|
||||||
<select id="editLibraryType">
|
<select id="editLibraryType" style="width: 100%;">
|
||||||
<option value="book">Buch</option>
|
<option value="book">Buch</option>
|
||||||
<option value="schulbuch">Schulbuch</option>
|
<option value="schoolbook">Schulbuch</option>
|
||||||
<option value="cd">CD</option>
|
<option value="cd">CD</option>
|
||||||
<option value="dvd">DVD</option>
|
<option value="dvd">DVD</option>
|
||||||
<option value="media">Sonstige Medien</option>
|
<option value="other">Sonstige Medien</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="editLibraryIsbn">ISBN</label>
|
<label for="editLibraryIsbn">ISBN</label>
|
||||||
<input id="editLibraryIsbn" placeholder="optional ISBN-10/13">
|
<input id="editLibraryIsbn" placeholder="optional ISBN-10/13" style="width: 100%;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="editLibraryCode4">Code</label>
|
<label for="editLibraryCode4">Code</label>
|
||||||
<input id="editLibraryCode4" placeholder="optional Mediencode">
|
<input id="editLibraryCode4" placeholder="optional Mediencode" style="width: 100%;">
|
||||||
</div>
|
</div>
|
||||||
<div class="full">
|
<div class="full">
|
||||||
<label for="editLibraryLocation">Ort</label>
|
<label for="editLibraryLocation">Ort</label>
|
||||||
<input id="editLibraryLocation" required>
|
<input id="editLibraryLocation" required style="width: 100%;">
|
||||||
</div>
|
</div>
|
||||||
<div class="full">
|
<div class="full">
|
||||||
<label for="editLibraryDescription">Beschreibung</label>
|
<label for="editLibraryDescription">Beschreibung</label>
|
||||||
<textarea id="editLibraryDescription" rows="4" required></textarea>
|
<textarea id="editLibraryDescription" rows="4" required style="width: 100%;"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-actions" style="margin-top:20px;">
|
<div class="edit-actions" style="margin-top:20px;">
|
||||||
<button type="submit" class="button" style="background:#0ea5e9;color:#fff;">Speichern</button>
|
<button type="submit" class="button" style="background:#0ea5e9;color:#fff;">Speichern & Synchronisieren</button>
|
||||||
<button type="button" class="button" onclick="closeEditLibraryModal()">Abbrechen</button>
|
<button type="button" class="button" onclick="closeEditLibraryModal()">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -1380,11 +1453,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="margin: 5px 0; font-size: 12px; color: #555;">
|
<p style="margin: 5px 0; font-size: 12px; color: #555;">
|
||||||
Alle aufgeführten Codes gehören zu diesem Datensatz:
|
Alle Codes in dieser Gruppe:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Hier werden die Codes per JS eingefügt -->
|
<!-- Hier wird die Liste als Komma-Text eingefügt -->
|
||||||
<div id="editLibraryAllCodes"></div>
|
<div id="editLibraryAllCodes" style="font-family: monospace; font-size: 14px; font-weight: bold; color: #333; margin-top: 5px;"></div>
|
||||||
|
|
||||||
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
|
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
|
||||||
<strong>Hinweis:</strong> Änderungen an Titel/Ort/Beschreibung werden auf <strong>alle</strong> Exemplare der Range übertragen.
|
<strong>Hinweis:</strong> Änderungen an Titel/Ort/Beschreibung werden auf <strong>alle</strong> Exemplare der Range übertragen.
|
||||||
|
|||||||
@@ -621,6 +621,9 @@ EOF
|
|||||||
cleanup_old_dist_artifacts
|
cleanup_old_dist_artifacts
|
||||||
cleanup_docker_dangling_images
|
cleanup_docker_dangling_images
|
||||||
log_message "Update completed successfully to release $latest_tag"
|
log_message "Update completed successfully to release $latest_tag"
|
||||||
|
|
||||||
|
sudo ./opt/Inventarsystem/restart.sh
|
||||||
|
echo "Restart of the Server Completet"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user