Compare commits

..

4 Commits

7 changed files with 124 additions and 19 deletions
+1 -1
View File
@@ -32,4 +32,4 @@ RUN if [ "$NUITKA_BUILD" = "1" ]; then \
WORKDIR /app/Web
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "60", "--graceful-timeout", "20", "--max-requests", "1000", "--max-requests-jitter", "100", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "30", "--graceful-timeout", "20", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
+97 -9
View File
@@ -2282,7 +2282,7 @@ def library_admin():
@app.route('/student_cards_admin', methods=['GET', 'POST'])
def student_cards_admin():
"""
Admin page for managing student library cards (Schülerausweise).
Admin page for managing student library cards (Bibliotheksausweis).
Only accessible by admins and only when the student cards module is enabled.
"""
if 'username' not in session:
@@ -2410,7 +2410,7 @@ def student_cards_admin():
@app.route('/student_cards_print', methods=['GET'])
def student_cards_print():
"""
Generate a printable template for all student library cards (Schülerausweise).
Generate a printable template for all student library cards (Bibliotheksausweis).
Only accessible by admins and only when the student cards module is enabled.
"""
if 'username' not in session:
@@ -2950,6 +2950,7 @@ def get_items():
available_only = str(request.args.get('available_only', '')).strip().lower() in ('1', 'true', 'yes', 'on')
offset_raw = request.args.get('offset')
limit_raw = request.args.get('limit')
light_mode_param = str(request.args.get('light_mode', '')).strip().lower()
pagination_requested = offset_raw is not None or limit_raw is not None
if pagination_requested:
@@ -2966,6 +2967,13 @@ def get_items():
offset = 0
limit = None
if light_mode_param in ('1', 'true', 'yes', 'on'):
light_mode = True
elif light_mode_param in ('0', 'false', 'no', 'off'):
light_mode = False
else:
light_mode = (offset == 0)
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
items_col = db['items']
@@ -2977,18 +2985,97 @@ def get_items():
total_count = items_col.count_documents(base_query)
items_cur = items_col.find(base_query).sort([('Name', 1), ('_id', 1)])
light_projection = {
'Name': 1,
'Code_4': 1,
'Images': 1,
'ThumbnailInfo': 1,
'Verfuegbar': 1,
'Filter': 1,
'Filter2': 1,
'Filter3': 1,
'Ort': 1,
'User': 1,
'ItemType': 1,
}
# Full projection: all details for detailed view
full_projection = {
'Name': 1,
'Ort': 1,
'Beschreibung': 1,
'Filter': 1,
'Filter2': 1,
'Filter3': 1,
'Code_4': 1,
'Images': 1,
'ThumbnailInfo': 1,
'Verfuegbar': 1,
'User': 1,
'BorrowerInfo': 1,
'appointments': 1,
'BlockedNow': 1,
'Reservierbar': 1,
'DamageReports': 1,
'ISBN': 1,
'Author': 1,
'Autor': 1,
'Anschaffungsjahr': 1,
'Anschaffungskosten': 1,
'Condition': 1,
'HasDamage': 1,
'ItemType': 1,
'SeriesGroupId': 1,
'LastUpdated': 1,
}
parent_projection = light_projection if light_mode else full_projection
items_cur = items_col.find(base_query, parent_projection).sort([('Name', 1), ('_id', 1)])
if pagination_requested:
items_cur = items_cur.skip(offset).limit(limit)
items = []
for itm in items_cur:
item_id_str = str(itm['_id'])
grouped_children = list(items_col.find({
'ParentItemId': item_id_str,
parent_items = list(items_cur)
parent_ids = [str(item.get('_id')) for item in parent_items if item.get('_id') is not None]
children_by_parent = {}
if parent_ids:
# Light mode: minimal child data for counting only
light_child_projection = {
'_id': 1,
'ParentItemId': 1,
'Code_4': 1,
'Verfuegbar': 1,
'Name': 1,
}
# Full mode: complete child data with all details
full_child_projection = {
'_id': 1,
'ParentItemId': 1,
'Code_4': 1,
'Verfuegbar': 1,
'Name': 1,
'Images': 1,
'ThumbnailInfo': 1,
'Beschreibung': 1,
}
child_projection = light_child_projection if light_mode else full_child_projection
child_cursor = items_col.find({
'ParentItemId': {'$in': parent_ids},
'IsGroupedSubItem': True,
'Deleted': {'$ne': True},
}))
}, child_projection)
for child in child_cursor:
parent_id = str(child.get('ParentItemId') or '')
if not parent_id:
continue
children_by_parent.setdefault(parent_id, []).append(child)
items = []
for itm in parent_items:
item_id_str = str(itm['_id'])
grouped_children = children_by_parent.get(item_id_str, [])
grouped_count = 1 + len(grouped_children)
grouped_units = [itm] + grouped_children
@@ -3028,6 +3115,7 @@ def get_items():
'limit': limit if limit is not None else count,
'count': count,
'total': total_count,
'light_mode': light_mode,
'has_more': pagination_requested and ((offset + count) < total_count)
})
except Exception as e:
+1 -1
View File
@@ -483,7 +483,7 @@
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
{% if student_cards_module_enabled %}
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Schülerausweise</a></li>
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">System</h6></li>
+5 -1
View File
@@ -728,6 +728,7 @@
let mainItemsNextOffset = 0;
let mainItemsHasMore = false;
let mainItemsLoadingMore = false;
let mainItemsLightMode = true; // Track if we're in light mode to gradually request full data
let mainItemsObserver = null;
let mainItemsSentinel = null;
let mainItemsLoadingIndicator = null;
@@ -788,7 +789,10 @@
}
function loadItems(offset = 0, append = false) {
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ITEMS_PAGE_SIZE}`)
// Für Pages nach der ersten: Explizit vollständige Daten laden (light_mode=false)
// Erste Page: light_mode wird automatisch enablet
const lightModeParam = offset > 0 ? '&light_mode=false' : '';
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ITEMS_PAGE_SIZE}${lightModeParam}`)
.then(response => response.json())
.then(data => {
const itemsContainer = document.querySelector('#items-container');
+15 -2
View File
@@ -415,13 +415,17 @@
}
.bulk-delete-row {
display: flex;
display: none;
justify-content: flex-start;
align-items: center;
margin-bottom: 8px;
gap: 8px;
}
.bulk-delete-mode-active .bulk-delete-row {
display: flex;
}
.bulk-delete-toggle {
display: inline-flex;
align-items: center;
@@ -3056,6 +3060,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
bulkDeleteDrawerOpen = Boolean(open);
const drawer = document.getElementById('bulk-delete-drawer');
const toggle = document.getElementById('bulk-delete-drawer-toggle');
const itemsContainer = document.querySelector('#items-container');
if (drawer) {
drawer.classList.toggle('open', bulkDeleteDrawerOpen);
drawer.setAttribute('aria-hidden', bulkDeleteDrawerOpen ? 'false' : 'true');
@@ -3065,6 +3070,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
toggle.innerHTML = bulkDeleteDrawerOpen ? '<span class="drawer-icon">✕</span>' : '<span class="drawer-icon">⚙</span>';
toggle.title = bulkDeleteDrawerOpen ? 'Massenlöschung schließen' : 'Massenlöschung öffnen';
}
// Toggle checkbox visibility: show checkboxes only when bulk delete mode is active
if (itemsContainer) {
itemsContainer.classList.toggle('bulk-delete-mode-active', bulkDeleteDrawerOpen);
}
}
function toggleBulkDeleteDrawer() {
@@ -3310,6 +3319,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
let mainAdminItemsNextOffset = 0;
let mainAdminItemsHasMore = false;
let mainAdminItemsLoadingMore = false;
let mainAdminLightMode = true; // Track if we're in light mode to gradually request full data
let mainAdminItemsObserver = null;
let mainAdminItemsSentinel = null;
let mainAdminItemsLoadingIndicator = null;
@@ -3371,7 +3381,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
function loadItems(offset = 0, append = false) {
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}`)
// Für Pages nach der ersten: Explizit vollständige Daten laden (light_mode=false)
// Erste Page: light_mode wird automatisch enablet
const lightModeParam = offset > 0 ? '&light_mode=false' : '';
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}${lightModeParam}`)
.then(response => response.json())
.then(data => {
const itemsContainer = document.querySelector('#items-container');
@@ -124,12 +124,12 @@
<div class="container">
<div class="icon">📇</div>
<h1>Schülerausweis-Download</h1>
<p>Generieren Sie eine PDF mit Barcodes aller Schülerausweise zum direkten Drucken</p>
<p>Generieren Sie eine PDF mit Barcodes aller Bibliotheksausweise zum direkten Drucken</p>
<div class="info-box">
<strong>📌 Was wird heruntergeladen?</strong>
<p>Eine druckfertige PDF mit:</p>
<p>✓ Alle Schülerausweise</p>
<p>✓ Alle Bibliotheksausweise</p>
<p>✓ Scanbare CODE128 Barcodes</p>
<p>✓ Optimiert für A4-Druck (2 Karten pro Seite)</p>
<p>✓ Professionelle Qualität</p>
+3 -3
View File
@@ -8,7 +8,7 @@
-->
{% extends "base.html" %}
{% block title %}Schülerausweise - Inventarsystem{% endblock %}
{% block title %}Bibliotheksausweise - Inventarsystem{% endblock %}
{% block content %}
<style>
@@ -229,7 +229,7 @@
<div class="container">
<div class="student-card-header">
<div>
<h1>📚 Schülerausweise (Bibliotek)</h1>
<h1>📚 Bibliotheksausweise (Bibliotek)</h1>
</div>
<div class="export-buttons">
<a href="{{ url_for('student_card_barcode_download') }}" class="btn-print" style="background: #28a745;">📥 Alle Ausweise (PDF)</a>
@@ -342,7 +342,7 @@
</table>
{% else %}
<div class="empty-state">
<p>Keine Schülerausweise registriert.</p>
<p>Keine Bibliotheksausweise registriert.</p>
<p>Fügen Sie ein neues Ausweis oben hinzu.</p>
</div>
{% endif %}