Compare commits

..

10 Commits

9 changed files with 829 additions and 78 deletions
+1 -1
View File
@@ -32,4 +32,4 @@ RUN if [ "$NUITKA_BUILD" = "1" ]; then \
WORKDIR /app/Web WORKDIR /app/Web
EXPOSE 8000 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", "-"]
+201 -17
View File
@@ -33,7 +33,6 @@ import ausleihung as au
import audit_log as al import audit_log as al
import datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from pymongo import MongoClient
from bson.objectid import ObjectId from bson.objectid import ObjectId
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
import requests import requests
@@ -59,6 +58,7 @@ import subprocess
# Set base directory and centralized settings # Set base directory and centralized settings
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
import settings as cfg import settings as cfg
from settings import MongoClient
app = Flask(__name__, static_folder='static') # Correctly set static folder app = Flask(__name__, static_folder='static') # Correctly set static folder
@@ -1742,11 +1742,25 @@ def library_loans_admin():
@app.route('/api/library_items') @app.route('/api/library_items')
def api_library_items(): def api_library_items():
""" """
API endpoint to fetch all library items (books, CDs, DVDs, media). API endpoint to fetch library items (books, CDs, DVDs, media).
Returns JSON array suitable for table view. Supports pagination via query params: offset, limit.
""" """
if 'username' not in session: if 'username' not in session:
return jsonify([]), 401 return jsonify({'items': []}), 401
offset_raw = request.args.get('offset', '0')
limit_raw = request.args.get('limit', '120')
try:
offset = max(0, int(offset_raw))
except (TypeError, ValueError):
offset = 0
try:
limit = int(limit_raw)
except (TypeError, ValueError):
limit = 120
limit = min(max(limit, 1), 500)
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
@@ -1754,9 +1768,36 @@ def api_library_items():
items_db = db['items'] items_db = db['items']
ausleihungen_db = db['ausleihungen'] ausleihungen_db = db['ausleihungen']
library_items = list(items_db.find({ query = {
'ItemType': {'$in': ['book', 'cd', 'dvd', 'media']} 'ItemType': {'$in': ['book', 'cd', 'dvd', 'media']},
})) 'Deleted': {'$ne': True}
}
projection = {
'Name': 1,
'Autor': 1,
'Author': 1,
'ISBN': 1,
'Code_4': 1,
'Code4': 1,
'ItemType': 1,
'Verfuegbar': 1,
'Condition': 1,
'HasDamage': 1,
'User': 1,
'Ort': 1,
'Beschreibung': 1,
'Image': 1
}
total_count = items_db.count_documents(query)
library_items = list(
items_db.find(query, projection)
.sort([('Name', 1), ('_id', 1)])
.skip(offset)
.limit(limit)
)
item_ids = [str(item.get('_id')) for item in library_items if item.get('_id')] item_ids = [str(item.get('_id')) for item in library_items if item.get('_id')]
active_records = [] active_records = []
@@ -1782,6 +1823,8 @@ def api_library_items():
for item in library_items: for item in library_items:
item_id = str(item['_id']) item_id = str(item['_id'])
item['_id'] = item_id item['_id'] = item_id
if item.get('Code4') in (None, '') and item.get('Code_4') not in (None, ''):
item['Code4'] = item.get('Code_4')
condition_value = str(item.get('Condition', '')).strip().lower() condition_value = str(item.get('Condition', '')).strip().lower()
has_damage = bool(item.get('HasDamage')) or condition_value == 'destroyed' has_damage = bool(item.get('HasDamage')) or condition_value == 'destroyed'
@@ -1796,7 +1839,15 @@ def api_library_items():
item['BorrowedBy'] = active_user_by_item.get(item_id) or item.get('User', '') item['BorrowedBy'] = active_user_by_item.get(item_id) or item.get('User', '')
return jsonify(library_items), 200 count = len(library_items)
return jsonify({
'items': library_items,
'offset': offset,
'limit': limit,
'count': count,
'total': total_count,
'has_more': (offset + count) < total_count
}), 200
except Exception as e: except Exception as e:
print(f"Error fetching library items: {e}") print(f"Error fetching library items: {e}")
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@@ -2882,6 +2933,7 @@ def logout():
@app.route('/get_items', methods=['GET']) @app.route('/get_items', methods=['GET'])
def get_items(): def get_items():
"""Return items plus merged favorites (session + DB) and per-item favorite flag.""" """Return items plus merged favorites (session + DB) and per-item favorite flag."""
client = None
try: try:
username = session.get('username') username = session.get('username')
# Merge DB favorites into session if logged in # Merge DB favorites into session if logged in
@@ -2895,22 +2947,138 @@ def get_items():
app.logger.warning(f"Could not merge DB favorites: {fav_err}") app.logger.warning(f"Could not merge DB favorites: {fav_err}")
favorites = set(session.get('favorites', [])) favorites = set(session.get('favorites', []))
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:
try:
offset = max(0, int(offset_raw or '0'))
except (TypeError, ValueError):
offset = 0
try:
limit = int(limit_raw or '120')
except (TypeError, ValueError):
limit = 120
limit = min(max(limit, 1), 500)
else:
offset = 0
limit = None
# Light mode: minimal fields for faster initial load (auto-enabled on offset=0 unless explicitly disabled)
# Can be manually controlled via ?light_mode=true/false parameter
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) # Auto-enable light mode for first page
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
items_col = db['items'] items_col = db['items']
items_cur = items_col.find({ base_query = {
'IsGroupedSubItem': {'$ne': True}, 'IsGroupedSubItem': {'$ne': True},
'ItemType': {'$nin': LIBRARY_ITEM_TYPES}, 'ItemType': {'$nin': LIBRARY_ITEM_TYPES},
'Deleted': {'$ne': True}, 'Deleted': {'$ne': True},
}) }
items = []
for itm in items_cur: total_count = items_col.count_documents(base_query)
item_id_str = str(itm['_id'])
grouped_children = list(items_col.find({ # Light projection: essentials only for fast initial render
'ParentItemId': item_id_str, 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)
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, 'IsGroupedSubItem': True,
'Deleted': {'$ne': 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_count = 1 + len(grouped_children)
grouped_units = [itm] + grouped_children grouped_units = [itm] + grouped_children
@@ -2937,11 +3105,27 @@ def get_items():
itm['GroupedAllCodes'] = grouped_all_codes itm['GroupedAllCodes'] = grouped_all_codes
if grouped_count > 1: if grouped_count > 1:
itm['Verfuegbar'] = len(available_units) > 0 itm['Verfuegbar'] = len(available_units) > 0
if available_only and not itm.get('Verfuegbar', False):
continue
itm['is_favorite'] = item_id_str in favorites itm['is_favorite'] = item_id_str in favorites
items.append(itm) items.append(itm)
return jsonify({'items': items, 'favorites': list(favorites)})
count = len(items)
return jsonify({
'items': items,
'favorites': list(favorites),
'offset': offset,
'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: except Exception as e:
return jsonify({'items': [], 'error': str(e)}), 500 return jsonify({'items': [], 'error': str(e)}), 500
finally:
if client:
client.close()
@app.route('/get_item/<id>') @app.route('/get_item/<id>')
+1 -1
View File
@@ -25,7 +25,6 @@ Sammlungsstruktur:
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited. Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev For commercial licensing inquiries: https://github.com/AIIrondev
''' '''
from pymongo import MongoClient
from bson.objectid import ObjectId from bson.objectid import ObjectId
import datetime import datetime
import pytz import pytz
@@ -34,6 +33,7 @@ import os
import json import json
import shutil import shutil
import settings as cfg import settings as cfg
from settings import MongoClient
# Add this helper function after imports # Add this helper function after imports
def ensure_timezone_aware(dt): def ensure_timezone_aware(dt):
+1 -1
View File
@@ -26,10 +26,10 @@ Collection Structure:
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited. Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev For commercial licensing inquiries: https://github.com/AIIrondev
''' '''
from pymongo import MongoClient
from bson.objectid import ObjectId from bson.objectid import ObjectId
import datetime import datetime
import settings as cfg import settings as cfg
from settings import MongoClient
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'media') LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'media')
+19
View File
@@ -12,6 +12,7 @@ defaults for the web application and helper modules.
""" """
import os import os
import json import json
from pymongo import MongoClient as _PyMongoClient
# Base directory of this Web package # Base directory of this Web package
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -173,3 +174,21 @@ if not os.path.isabs(BACKUP_FOLDER):
BACKUP_FOLDER = os.path.join(PROJECT_ROOT, BACKUP_FOLDER) BACKUP_FOLDER = os.path.join(PROJECT_ROOT, BACKUP_FOLDER)
if not os.path.isabs(LOGS_FOLDER): if not os.path.isabs(LOGS_FOLDER):
LOGS_FOLDER = os.path.join(PROJECT_ROOT, LOGS_FOLDER) LOGS_FOLDER = os.path.join(PROJECT_ROOT, LOGS_FOLDER)
def MongoClient(*args, **kwargs):
"""Return a lightweight MongoDB client configured from this settings module."""
client_kwargs = {
'maxPoolSize': 10,
'minPoolSize': 0,
'connectTimeoutMS': 5000,
'serverSelectionTimeoutMS': 5000,
'retryWrites': True,
}
client_kwargs.update(kwargs)
# Preserve caller-provided positional host/port arguments.
# If none are passed, use configured defaults.
if args:
return _PyMongoClient(*args, **client_kwargs)
return _PyMongoClient(MONGODB_HOST, MONGODB_PORT, **client_kwargs)
+164 -34
View File
@@ -290,6 +290,20 @@
color: #666; color: #666;
} }
.library-load-row {
margin-top: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.library-load-hint {
font-size: 0.9em;
color: #6b7280;
}
/* Modal styles */ /* Modal styles */
.modal { .modal {
display: none; display: none;
@@ -501,6 +515,10 @@
<!-- Items will be loaded here --> <!-- Items will be loaded here -->
</tbody> </tbody>
</table> </table>
<div id="loadMoreRow" class="library-load-row" style="display:none;">
<span id="loadInfo" class="library-load-hint"></span>
<button id="loadMoreBtn" class="button" type="button">Mehr laden</button>
</div>
</div> </div>
<!-- Empty state --> <!-- Empty state -->
@@ -523,6 +541,24 @@
// State // State
let libraryItems = []; let libraryItems = [];
let filteredItems = []; let filteredItems = [];
let visibleItems = [];
let currentSearchTerm = '';
let activeFilters = {
author: '',
isbn: '',
type: '',
status: ''
};
let pagingState = {
offset: 0,
total: 0,
hasMore: true,
loading: false
};
const API_PAGE_SIZE = 120;
const INITIAL_RENDER_COUNT = 120;
const RENDER_BATCH_COUNT = 120;
let renderedCount = INITIAL_RENDER_COUNT;
let filterPanelOpen = false; let filterPanelOpen = false;
let scannerInstance = null; let scannerInstance = null;
let scannerRunning = false; let scannerRunning = false;
@@ -531,32 +567,124 @@
let lastScanAt = 0; let lastScanAt = 0;
const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1'); const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1');
// Load items async function fetchLibraryPage(offset, limit) {
const response = await fetch(`/api/library_items?offset=${offset}&limit=${limit}`);
if (!response.ok) {
throw new Error('Failed to load library items');
}
return response.json();
}
// Load items progressively: first page immediately, rest in background
async function loadLibraryItems() { async function loadLibraryItems() {
if (pagingState.loading) return;
pagingState.loading = true;
try { try {
const response = await fetch('/api/library_items'); const firstPage = await fetchLibraryPage(0, API_PAGE_SIZE);
if (!response.ok) throw new Error('Failed to load library items'); libraryItems = firstPage.items || [];
libraryItems = await response.json(); pagingState.offset = (firstPage.offset || 0) + (firstPage.count || libraryItems.length);
displayItems(libraryItems); pagingState.total = firstPage.total || libraryItems.length;
pagingState.hasMore = Boolean(firstPage.has_more);
applyFiltersAndSearch(true);
if (pagingState.hasMore) {
loadRemainingLibraryItemsInBackground();
}
} catch (error) { } catch (error) {
console.error('Error loading library items:', error); console.error('Error loading library items:', error);
document.getElementById('itemsTableBody').innerHTML = '<tr><td colspan="6" style="text-align:center; color:#999;">Fehler beim Laden der Bibliothekselemente.</td></tr>'; document.getElementById('itemsTableBody').innerHTML = '<tr><td colspan="6" style="text-align:center; color:#999;">Fehler beim Laden der Bibliothekselemente.</td></tr>';
} finally {
pagingState.loading = false;
} }
} }
// Display items in table async function loadRemainingLibraryItemsInBackground() {
function displayItems(items) { while (pagingState.hasMore) {
try {
const page = await fetchLibraryPage(pagingState.offset, API_PAGE_SIZE);
const nextItems = page.items || [];
if (nextItems.length === 0) {
pagingState.hasMore = false;
break;
}
libraryItems = libraryItems.concat(nextItems);
pagingState.offset = (page.offset || pagingState.offset) + (page.count || nextItems.length);
pagingState.total = page.total || pagingState.total;
pagingState.hasMore = Boolean(page.has_more);
// Keep the current view reactive while avoiding a full rerender burst.
applyFiltersAndSearch(false);
if ('requestIdleCallback' in window) {
await new Promise(resolve => requestIdleCallback(() => resolve(), { timeout: 250 }));
} else {
await new Promise(resolve => setTimeout(resolve, 0));
}
} catch (error) {
console.error('Error loading additional library items:', error);
break;
}
}
}
function filterItems(items) {
const author = activeFilters.author;
const isbn = activeFilters.isbn;
const type = activeFilters.type;
const status = activeFilters.status;
return items.filter(item => {
const authorMatch = !author || (item.Autor || item.Author || '').toLowerCase().includes(author);
const isbnMatch = !isbn || (item.ISBN || item.Code_4 || item.Code4 || '').toLowerCase().includes(isbn);
const typeMatch = !type || (item.ItemType || 'book') === type;
const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available');
const statusMatch = !status || statusKey === status;
return authorMatch && isbnMatch && typeMatch && statusMatch;
});
}
function applySearch(items) {
if (!currentSearchTerm) return items;
return items.filter(item =>
(item.Name || '').toLowerCase().includes(currentSearchTerm) ||
(item.Autor || item.Author || '').toLowerCase().includes(currentSearchTerm) ||
(item.ISBN || item.Code_4 || item.Code4 || '').toLowerCase().includes(currentSearchTerm)
);
}
function applyFiltersAndSearch(resetRenderCount) {
const filteredByPanel = filterItems(libraryItems);
filteredItems = applySearch(filteredByPanel);
if (resetRenderCount) {
renderedCount = Math.min(INITIAL_RENDER_COUNT, filteredItems.length);
} else {
renderedCount = Math.min(Math.max(renderedCount, INITIAL_RENDER_COUNT), filteredItems.length || INITIAL_RENDER_COUNT);
}
visibleItems = filteredItems;
renderItems();
}
// Display current view with incremental rendering
function renderItems() {
const tbody = document.getElementById('itemsTableBody'); const tbody = document.getElementById('itemsTableBody');
const emptyState = document.getElementById('emptyState'); const emptyState = document.getElementById('emptyState');
const loadMoreRow = document.getElementById('loadMoreRow');
const loadInfo = document.getElementById('loadInfo');
const loadMoreBtn = document.getElementById('loadMoreBtn');
if (items.length === 0) { if (visibleItems.length === 0) {
tbody.innerHTML = ''; tbody.innerHTML = '';
emptyState.style.display = 'block'; emptyState.style.display = 'block';
loadMoreRow.style.display = 'none';
return; return;
} }
emptyState.style.display = 'none'; emptyState.style.display = 'none';
tbody.innerHTML = items.map(item => ` const rowsToRender = visibleItems.slice(0, renderedCount);
tbody.innerHTML = rowsToRender.map(item => `
${(() => { ${(() => {
const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available'); const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available');
const statusClass = statusKey === 'damaged' ? 'status-damaged' : (statusKey === 'borrowed' ? 'status-borrowed' : 'status-available'); const statusClass = statusKey === 'damaged' ? 'status-damaged' : (statusKey === 'borrowed' ? 'status-borrowed' : 'status-available');
@@ -567,7 +695,7 @@
<tr> <tr>
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td> <td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
<td>${escapeHtml(item.Autor || item.Author || '-')}</td> <td>${escapeHtml(item.Autor || item.Author || '-')}</td>
<td>${escapeHtml(item.ISBN || item.Code4 || '-')}</td> <td>${escapeHtml(item.ISBN || item.Code_4 || item.Code4 || '-')}</td>
<td>${getItemTypeLabel(item.ItemType || 'book')}</td> <td>${getItemTypeLabel(item.ItemType || 'book')}</td>
<td> <td>
<span class="table-status ${statusClass}"> <span class="table-status ${statusClass}">
@@ -585,6 +713,18 @@
`; `;
})()} })()}
`).join(''); `).join('');
const canLoadMoreRows = renderedCount < visibleItems.length;
if (canLoadMoreRows) {
loadMoreRow.style.display = 'flex';
loadMoreBtn.style.display = 'inline-block';
loadInfo.textContent = `${renderedCount} von ${visibleItems.length} angezeigt`;
} else {
const backgroundHint = pagingState.hasMore ? `Lade weitere Medien im Hintergrund (${libraryItems.length}/${pagingState.total || '?'})...` : `${visibleItems.length} Treffer`;
loadMoreRow.style.display = 'flex';
loadMoreBtn.style.display = 'none';
loadInfo.textContent = backgroundHint;
}
} }
// Filter toggle // Filter toggle
@@ -598,21 +738,11 @@
// Apply filters // Apply filters
document.getElementById('applyFilterBtn').addEventListener('click', () => { document.getElementById('applyFilterBtn').addEventListener('click', () => {
const author = document.getElementById('filterAuthor').value.toLowerCase(); activeFilters.author = document.getElementById('filterAuthor').value.toLowerCase();
const isbn = document.getElementById('filterISBN').value.toLowerCase(); activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
const type = document.getElementById('filterType').value; activeFilters.type = document.getElementById('filterType').value;
const status = document.getElementById('filterStatus').value; activeFilters.status = document.getElementById('filterStatus').value;
applyFiltersAndSearch(true);
filteredItems = libraryItems.filter(item => {
const authorMatch = !author || (item.Autor || item.Author || '').toLowerCase().includes(author);
const isbnMatch = !isbn || (item.ISBN || item.Code4 || '').toLowerCase().includes(isbn);
const typeMatch = !type || (item.ItemType || 'book') === type;
const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available');
const statusMatch = !status || statusKey === status;
return authorMatch && isbnMatch && typeMatch && statusMatch;
});
displayItems(filteredItems);
}); });
// Clear filters // Clear filters
@@ -621,19 +751,19 @@
document.getElementById('filterISBN').value = ''; document.getElementById('filterISBN').value = '';
document.getElementById('filterType').value = ''; document.getElementById('filterType').value = '';
document.getElementById('filterStatus').value = ''; document.getElementById('filterStatus').value = '';
displayItems(libraryItems); activeFilters = { author: '', isbn: '', type: '', status: '' };
applyFiltersAndSearch(true);
}); });
// Search input // Search input
document.getElementById('librarySearch').addEventListener('input', (e) => { document.getElementById('librarySearch').addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase(); currentSearchTerm = (e.target.value || '').toLowerCase();
const toSearch = filteredItems.length > 0 ? filteredItems : libraryItems; applyFiltersAndSearch(true);
const results = toSearch.filter(item => });
(item.Name || '').toLowerCase().includes(searchTerm) ||
(item.Autor || item.Author || '').toLowerCase().includes(searchTerm) || document.getElementById('loadMoreBtn').addEventListener('click', () => {
(item.ISBN || item.Code4 || '').toLowerCase().includes(searchTerm) renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
); renderItems();
displayItems(results);
}); });
// Helper functions // Helper functions
+219 -9
View File
@@ -724,8 +724,75 @@
// Add this line to define allItems globally // Add this line to define allItems globally
let allItems = []; let allItems = [];
function loadItems() { const MAIN_ITEMS_PAGE_SIZE = 120;
fetch("{{ url_for('get_items') }}") 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;
let mainItemsScrollPrefetchBound = false;
function ensureMainItemsLoadingIndicator() {
const itemsContainer = document.querySelector('#items-container');
if (!itemsContainer) return null;
if (!mainItemsLoadingIndicator) {
mainItemsLoadingIndicator = document.createElement('div');
mainItemsLoadingIndicator.id = 'main-items-loading-indicator';
mainItemsLoadingIndicator.className = 'items-loading-indicator';
mainItemsLoadingIndicator.innerHTML = `
<div class="loading-track"><div class="loading-fill"></div></div>
<div class="loading-label">Weitere Objekte werden vorbereitet...</div>
`;
}
itemsContainer.appendChild(mainItemsLoadingIndicator);
return itemsContainer;
}
function updateMainItemsLoadingIndicator() {
const itemsContainer = ensureMainItemsLoadingIndicator();
if (!itemsContainer || !mainItemsLoadingIndicator) return;
const shouldShow = mainItemsHasMore || mainItemsLoadingMore;
mainItemsLoadingIndicator.style.display = shouldShow ? 'flex' : 'none';
mainItemsLoadingIndicator.classList.toggle('is-loading', mainItemsLoadingMore);
const label = mainItemsLoadingIndicator.querySelector('.loading-label');
if (label) {
label.textContent = mainItemsLoadingMore
? 'Weitere Objekte werden geladen...'
: 'Weitere Objekte werden vorbereitet...';
}
}
function maybePrefetchMainItems() {
const itemsContainer = document.querySelector('#items-container');
if (!itemsContainer || !mainItemsHasMore || mainItemsLoadingMore) {
return;
}
const distanceToEnd = itemsContainer.scrollWidth - (itemsContainer.scrollLeft + itemsContainer.clientWidth);
const prefetchThreshold = Math.max(260, itemsContainer.clientWidth * 1.4);
if (distanceToEnd > prefetchThreshold) {
return;
}
mainItemsLoadingMore = true;
updateMainItemsLoadingIndicator();
loadItems(mainItemsNextOffset, true).finally(() => {
mainItemsLoadingMore = false;
updateMainItemsLoadingIndicator();
});
}
function loadItems(offset = 0, append = false) {
// 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(response => response.json())
.then(data => { .then(data => {
const itemsContainer = document.querySelector('#items-container'); const itemsContainer = document.querySelector('#items-container');
@@ -735,27 +802,39 @@
const filter3Values = new Set(); const filter3Values = new Set();
// Store all items data globally for filter rebuilding // Store all items data globally for filter rebuilding
allItems = data.items || []; const pageItems = data.items || [];
allItems = append ? allItems.concat(pageItems) : pageItems;
// Clear the container first // Clear the container first
itemsContainer.innerHTML = ''; if (!append) {
itemsContainer.innerHTML = '';
}
if (!data.items || data.items.length === 0) { if (!append && (!pageItems || pageItems.length === 0)) {
itemsContainer.innerHTML = '<div class="no-items-message">Keine Objekte gefunden</div>'; itemsContainer.innerHTML = '<div class="no-items-message">Keine Objekte gefunden</div>';
return; return;
} }
// Sort items by name in ascending order (A-Z) // Sort items by name in ascending order (A-Z)
data.items.sort((a, b) => { pageItems.sort((a, b) => {
const nameA = a.Name ? a.Name.toLowerCase() : ''; const nameA = a.Name ? a.Name.toLowerCase() : '';
const nameB = b.Name ? b.Name.toLowerCase() : ''; const nameB = b.Name ? b.Name.toLowerCase() : '';
return nameA.localeCompare(nameB); // Ascending order return nameA.localeCompare(nameB); // Ascending order
}); });
allItems.forEach(itemForFilters => {
const f1 = Array.isArray(itemForFilters.Filter) ? itemForFilters.Filter : (itemForFilters.Filter ? [itemForFilters.Filter] : []);
const f2 = Array.isArray(itemForFilters.Filter2) ? itemForFilters.Filter2 : (itemForFilters.Filter2 ? [itemForFilters.Filter2] : []);
const f3 = Array.isArray(itemForFilters.Filter3) ? itemForFilters.Filter3 : (itemForFilters.Filter3 ? [itemForFilters.Filter3] : []);
f1.forEach(value => { if (value && typeof value === 'string' && value.trim() !== '') filter1Values.add(value); });
f2.forEach(value => { if (value && typeof value === 'string' && value.trim() !== '') filter2Values.add(value); });
f3.forEach(value => { if (value && typeof value === 'string' && value.trim() !== '') filter3Values.add(value); });
});
const favoriteIds = new Set(data.favorites || []); const favoriteIds = new Set(data.favorites || []);
window.currentFavorites = favoriteIds; window.currentFavorites = favoriteIds;
try { sessionStorage.setItem('favoritesCache', JSON.stringify(Array.from(favoriteIds))); } catch(e){} try { sessionStorage.setItem('favoritesCache', JSON.stringify(Array.from(favoriteIds))); } catch(e){}
data.items.forEach(item => { pageItems.forEach(item => {
try { try {
const card = document.createElement('div'); const card = document.createElement('div');
card.classList.add('item-card'); card.classList.add('item-card');
@@ -995,8 +1074,10 @@
populateFilterOptions('filter2-options', [...filter2Values].sort(), 2); populateFilterOptions('filter2-options', [...filter2Values].sort(), 2);
populateFilterOptions('filter3-options', [...filter3Values].sort(), 3); populateFilterOptions('filter3-options', [...filter3Values].sort(), 3);
// Start display from leftmost position (first card, which is now Z) // Start display from leftmost position on full reload only.
itemsContainer.scrollLeft = 0; if (!append) {
itemsContainer.scrollLeft = 0;
}
// Apply filters // Apply filters
applyFilters(); applyFilters();
@@ -1011,15 +1092,93 @@
if (activeFilters.filter1.length > 0 || activeFilters.filter2.length > 0) { if (activeFilters.filter1.length > 0 || activeFilters.filter2.length > 0) {
rebuildFilter3Options(); rebuildFilter3Options();
} }
mainItemsNextOffset = (data.offset || offset) + (data.count || pageItems.length);
mainItemsHasMore = Boolean(data.has_more);
updateMainItemsLoadingIndicator();
setupMainItemsLazyLoading();
maybePrefetchMainItems();
}) })
.catch(error => { .catch(error => {
console.error('Error fetching items:', error); console.error('Error fetching items:', error);
mainItemsLoadingMore = false;
mainItemsHasMore = false;
updateMainItemsLoadingIndicator();
if (append) {
return;
}
const itemsContainer = document.querySelector('#items-container'); const itemsContainer = document.querySelector('#items-container');
itemsContainer.innerHTML = itemsContainer.innerHTML =
'<div class="error-message">Fehler beim Laden der Objekte. Bitte versuchen Sie es später erneut.</div>'; '<div class="error-message">Fehler beim Laden der Objekte. Bitte versuchen Sie es später erneut.</div>';
}); });
} }
function ensureMainItemsSentinel() {
const itemsContainer = document.querySelector('#items-container');
if (!itemsContainer) return null;
ensureMainItemsLoadingIndicator();
if (!mainItemsSentinel) {
mainItemsSentinel = document.createElement('div');
mainItemsSentinel.id = 'main-items-sentinel';
mainItemsSentinel.style.flex = '0 0 1px';
mainItemsSentinel.style.width = '1px';
mainItemsSentinel.style.minWidth = '1px';
mainItemsSentinel.style.height = '1px';
mainItemsSentinel.style.pointerEvents = 'none';
}
if (!mainItemsSentinel.parentNode) {
itemsContainer.appendChild(mainItemsSentinel);
} else {
itemsContainer.appendChild(mainItemsSentinel);
}
return itemsContainer;
}
function setupMainItemsLazyLoading() {
const itemsContainer = ensureMainItemsSentinel();
if (!itemsContainer) return;
if (mainItemsObserver) {
mainItemsObserver.disconnect();
mainItemsObserver = null;
}
if (!mainItemsScrollPrefetchBound) {
itemsContainer.addEventListener('scroll', maybePrefetchMainItems, { passive: true });
mainItemsScrollPrefetchBound = true;
}
if (!mainItemsHasMore) return;
mainItemsObserver = new IntersectionObserver(entries => {
if (!entries.some(entry => entry.isIntersecting)) {
return;
}
if (mainItemsLoadingMore || !mainItemsHasMore) {
return;
}
mainItemsLoadingMore = true;
updateMainItemsLoadingIndicator();
loadItems(mainItemsNextOffset, true).finally(() => {
mainItemsLoadingMore = false;
updateMainItemsLoadingIndicator();
});
}, {
root: itemsContainer,
threshold: 0.15,
rootMargin: '0px 900px 0px 0px'
});
mainItemsObserver.observe(mainItemsSentinel);
updateMainItemsLoadingIndicator();
}
function searchByCode() { function searchByCode() {
const searchInput = document.getElementById('code-search'); const searchInput = document.getElementById('code-search');
const rawSearchTerm = searchInput.value; const rawSearchTerm = searchInput.value;
@@ -2737,6 +2896,57 @@
scroll-behavior: smooth; scroll-behavior: smooth;
} }
.items-loading-indicator {
display: none;
flex: 0 0 220px;
min-width: 220px;
max-width: 220px;
height: 180px;
align-self: center;
margin-right: 8px;
border: 1px solid #dbe3ee;
border-radius: 10px;
background: #f8fafc;
color: #334155;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 10px;
scroll-snap-align: center;
}
.items-loading-indicator .loading-label {
font-size: 0.84rem;
text-align: center;
line-height: 1.3;
max-width: 170px;
}
.items-loading-indicator .loading-track {
width: 160px;
height: 8px;
border-radius: 999px;
background: #e2e8f0;
overflow: hidden;
}
.items-loading-indicator .loading-fill {
width: 45%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #60a5fa, #2563eb);
transform: translateX(-100%);
}
.items-loading-indicator.is-loading .loading-fill {
animation: items-loader-slide 1.05s ease-in-out infinite;
}
@keyframes items-loader-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(260%); }
}
.item-card { .item-card {
background-color: #fff; background-color: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
+221 -13
View File
@@ -323,6 +323,57 @@
scroll-behavior: smooth; scroll-behavior: smooth;
} }
.items-loading-indicator {
display: none;
flex: 0 0 220px;
min-width: 220px;
max-width: 220px;
height: 180px;
align-self: center;
margin-right: 8px;
border: 1px solid #dbe3ee;
border-radius: 10px;
background: #f8fafc;
color: #334155;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 10px;
scroll-snap-align: center;
}
.items-loading-indicator .loading-label {
font-size: 0.84rem;
text-align: center;
line-height: 1.3;
max-width: 170px;
}
.items-loading-indicator .loading-track {
width: 160px;
height: 8px;
border-radius: 999px;
background: #e2e8f0;
overflow: hidden;
}
.items-loading-indicator .loading-fill {
width: 45%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #60a5fa, #2563eb);
transform: translateX(-100%);
}
.items-loading-indicator.is-loading .loading-fill {
animation: items-loader-slide 1.05s ease-in-out infinite;
}
@keyframes items-loader-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(260%); }
}
.item-card { .item-card {
background-color: #fff; background-color: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
@@ -2966,9 +3017,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
if (parsedFilters.filter3) activeFilters.filter3 = parsedFilters.filter3; if (parsedFilters.filter3) activeFilters.filter3 = parsedFilters.filter3;
// Update UI to show selected filters // Update UI to show selected filters
updateSelectedFilters(1); updateSelectedFiltersDisplay(1);
updateSelectedFilters(2); updateSelectedFiltersDisplay(2);
updateSelectedFilters(3); updateSelectedFiltersDisplay(3);
// We'll rebuild Filter 3 options after items are loaded // We'll rebuild Filter 3 options after items are loaded
} catch (e) { } catch (e) {
@@ -3255,8 +3306,76 @@ document.addEventListener('DOMContentLoaded', ()=>{
}); });
// Function to load items from server // Function to load items from server
function loadItems() { const MAIN_ADMIN_ITEMS_PAGE_SIZE = 120;
fetch("{{ url_for('get_items') }}") 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;
let mainAdminItemsScrollPrefetchBound = false;
let totalItemsInSystem = 0;
function ensureMainAdminItemsLoadingIndicator() {
const itemsContainer = document.querySelector('#items-container');
if (!itemsContainer) return null;
if (!mainAdminItemsLoadingIndicator) {
mainAdminItemsLoadingIndicator = document.createElement('div');
mainAdminItemsLoadingIndicator.id = 'main-admin-items-loading-indicator';
mainAdminItemsLoadingIndicator.className = 'items-loading-indicator';
mainAdminItemsLoadingIndicator.innerHTML = `
<div class="loading-track"><div class="loading-fill"></div></div>
<div class="loading-label">Weitere Objekte werden vorbereitet...</div>
`;
}
itemsContainer.appendChild(mainAdminItemsLoadingIndicator);
return itemsContainer;
}
function updateMainAdminItemsLoadingIndicator() {
const itemsContainer = ensureMainAdminItemsLoadingIndicator();
if (!itemsContainer || !mainAdminItemsLoadingIndicator) return;
const shouldShow = mainAdminItemsHasMore || mainAdminItemsLoadingMore;
mainAdminItemsLoadingIndicator.style.display = shouldShow ? 'flex' : 'none';
mainAdminItemsLoadingIndicator.classList.toggle('is-loading', mainAdminItemsLoadingMore);
const label = mainAdminItemsLoadingIndicator.querySelector('.loading-label');
if (label) {
label.textContent = mainAdminItemsLoadingMore
? 'Weitere Objekte werden geladen...'
: 'Weitere Objekte werden vorbereitet...';
}
}
function maybePrefetchMainAdminItems() {
const itemsContainer = document.querySelector('#items-container');
if (!itemsContainer || !mainAdminItemsHasMore || mainAdminItemsLoadingMore) {
return;
}
const distanceToEnd = itemsContainer.scrollWidth - (itemsContainer.scrollLeft + itemsContainer.clientWidth);
const prefetchThreshold = Math.max(260, itemsContainer.clientWidth * 1.4);
if (distanceToEnd > prefetchThreshold) {
return;
}
mainAdminItemsLoadingMore = true;
updateMainAdminItemsLoadingIndicator();
loadItems(mainAdminItemsNextOffset, true).finally(() => {
mainAdminItemsLoadingMore = false;
updateMainAdminItemsLoadingIndicator();
});
}
function loadItems(offset = 0, append = false) {
// 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(response => response.json())
.then(data => { .then(data => {
const itemsContainer = document.querySelector('#items-container'); const itemsContainer = document.querySelector('#items-container');
@@ -3266,9 +3385,13 @@ document.addEventListener('DOMContentLoaded', ()=>{
const filter1Values = new Set(); const filter1Values = new Set();
const filter2Values = new Set(); const filter2Values = new Set();
const filter3Values = new Set(); const filter3Values = new Set();
allItems = data.items || []; const pageItems = data.items || [];
itemsContainer.innerHTML = ''; allItems = append ? allItems.concat(pageItems) : pageItems;
if (!data.items || data.items.length === 0) { totalItemsInSystem = Number(data.total || totalItemsInSystem || pageItems.length || 0);
if (!append) {
itemsContainer.innerHTML = '';
}
if (!append && (!pageItems || pageItems.length === 0)) {
itemsContainer.innerHTML = ` itemsContainer.innerHTML = `
<div class="no-items-message"> <div class="no-items-message">
<div>Keine Objekte gefunden</div> <div>Keine Objekte gefunden</div>
@@ -3281,15 +3404,25 @@ document.addEventListener('DOMContentLoaded', ()=>{
return; return;
} }
if (itemsIndicator) { if (itemsIndicator) {
itemsIndicator.textContent = `Objekte im System: ${data.items.length}`; itemsIndicator.textContent = `Objekte im System: ${totalItemsInSystem}${data.has_more ? ' (lädt...)' : ''}`;
} }
data.items.sort((a, b) => { pageItems.sort((a, b) => {
const nameA = a.Name ? a.Name.toLowerCase() : ''; const nameA = a.Name ? a.Name.toLowerCase() : '';
const nameB = a.Name ? a.Name.toLowerCase() : ''; const nameB = a.Name ? a.Name.toLowerCase() : '';
return nameA.localeCompare(nameB); return nameA.localeCompare(nameB);
}); });
allItems.forEach(itemForFilters => {
const f1 = Array.isArray(itemForFilters.Filter) ? itemForFilters.Filter : (itemForFilters.Filter ? [itemForFilters.Filter] : []);
const f2 = Array.isArray(itemForFilters.Filter2) ? itemForFilters.Filter2 : (itemForFilters.Filter2 ? [itemForFilters.Filter2] : []);
const f3 = Array.isArray(itemForFilters.Filter3) ? itemForFilters.Filter3 : (itemForFilters.Filter3 ? [itemForFilters.Filter3] : []);
f1.forEach(value => { if (value && typeof value === 'string' && value.trim() !== '') filter1Values.add(value); });
f2.forEach(value => { if (value && typeof value === 'string' && value.trim() !== '') filter2Values.add(value); });
f3.forEach(value => { if (value && typeof value === 'string' && value.trim() !== '') filter3Values.add(value); });
});
const favoriteIds = new Set(data.favorites || []); const favoriteIds = new Set(data.favorites || []);
data.items.forEach(item => { pageItems.forEach(item => {
try { try {
const card = document.createElement('div'); const card = document.createElement('div');
card.classList.add('item-card'); card.classList.add('item-card');
@@ -3521,7 +3654,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
populateFilterOptions('filter2-options', [...filter2Values].sort(), 2); populateFilterOptions('filter2-options', [...filter2Values].sort(), 2);
populateFilterOptions('filter3-options', [...filter3Values].sort(), 3); populateFilterOptions('filter3-options', [...filter3Values].sort(), 3);
itemsContainer.scrollLeft = 0; if (!append) {
itemsContainer.scrollLeft = 0;
}
applyFilters(); applyFilters();
updateBulkDeleteSummary(); updateBulkDeleteSummary();
@@ -3536,9 +3671,21 @@ document.addEventListener('DOMContentLoaded', ()=>{
rebuildFilter3Options(); rebuildFilter3Options();
} }
/* favorites render count removed */ /* favorites render count removed */
mainAdminItemsNextOffset = (data.offset || offset) + (data.count || pageItems.length);
mainAdminItemsHasMore = Boolean(data.has_more);
updateMainAdminItemsLoadingIndicator();
setupMainAdminItemsLazyLoading();
maybePrefetchMainAdminItems();
}) })
.catch(error => { .catch(error => {
console.error('Error fetching items:', error); console.error('Error fetching items:', error);
mainAdminItemsLoadingMore = false;
mainAdminItemsHasMore = false;
updateMainAdminItemsLoadingIndicator();
if (append) {
return;
}
const itemsContainer = document.querySelector('#items-container'); const itemsContainer = document.querySelector('#items-container');
const itemsIndicator = document.getElementById('items-indicator'); const itemsIndicator = document.getElementById('items-indicator');
itemsContainer.innerHTML = itemsContainer.innerHTML =
@@ -3549,6 +3696,67 @@ document.addEventListener('DOMContentLoaded', ()=>{
}); });
} }
function ensureMainAdminItemsSentinel() {
const itemsContainer = document.querySelector('#items-container');
if (!itemsContainer) return null;
ensureMainAdminItemsLoadingIndicator();
if (!mainAdminItemsSentinel) {
mainAdminItemsSentinel = document.createElement('div');
mainAdminItemsSentinel.id = 'main-admin-items-sentinel';
mainAdminItemsSentinel.style.flex = '0 0 1px';
mainAdminItemsSentinel.style.width = '1px';
mainAdminItemsSentinel.style.minWidth = '1px';
mainAdminItemsSentinel.style.height = '1px';
mainAdminItemsSentinel.style.pointerEvents = 'none';
}
itemsContainer.appendChild(mainAdminItemsSentinel);
return itemsContainer;
}
function setupMainAdminItemsLazyLoading() {
const itemsContainer = ensureMainAdminItemsSentinel();
if (!itemsContainer) return;
if (mainAdminItemsObserver) {
mainAdminItemsObserver.disconnect();
mainAdminItemsObserver = null;
}
if (!mainAdminItemsScrollPrefetchBound) {
itemsContainer.addEventListener('scroll', maybePrefetchMainAdminItems, { passive: true });
mainAdminItemsScrollPrefetchBound = true;
}
if (!mainAdminItemsHasMore) return;
mainAdminItemsObserver = new IntersectionObserver(entries => {
if (!entries.some(entry => entry.isIntersecting)) {
return;
}
if (mainAdminItemsLoadingMore || !mainAdminItemsHasMore) {
return;
}
mainAdminItemsLoadingMore = true;
updateMainAdminItemsLoadingIndicator();
loadItems(mainAdminItemsNextOffset, true).finally(() => {
mainAdminItemsLoadingMore = false;
updateMainAdminItemsLoadingIndicator();
});
}, {
root: itemsContainer,
threshold: 0.15,
rootMargin: '0px 900px 0px 0px'
});
mainAdminItemsObserver.observe(mainAdminItemsSentinel);
updateMainAdminItemsLoadingIndicator();
}
function searchByCode() { function searchByCode() {
const searchInput = document.getElementById('code-search'); const searchInput = document.getElementById('code-search');
const rawSearchTerm = searchInput.value; const rawSearchTerm = searchInput.value;
@@ -3643,7 +3851,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
}); });
if (itemsIndicator) { if (itemsIndicator) {
itemsIndicator.textContent = `Objekte im System: ${allItems.length} | Angezeigt: ${visibleCount}`; itemsIndicator.textContent = `Objekte im System: ${totalItemsInSystem || allItems.length} | Angezeigt: ${visibleCount}`;
} }
if (isCodeSearch && visibleCount > 0 && (codeMatchCount > 0 || exactMatchCount > 0)) { if (isCodeSearch && visibleCount > 0 && (codeMatchCount > 0 || exactMatchCount > 0)) {
+1 -1
View File
@@ -10,10 +10,10 @@ Provides methods for creating, validating, and retrieving user information.
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited. Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev For commercial licensing inquiries: https://github.com/AIIrondev
''' '''
from pymongo import MongoClient
import hashlib import hashlib
from bson.objectid import ObjectId from bson.objectid import ObjectId
import settings as cfg import settings as cfg
from settings import MongoClient
def normalize_student_card_id(card_id): def normalize_student_card_id(card_id):