Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8ac08d5b0 | |||
| 05c4c73635 | |||
| 76dd00831e | |||
| 205a56ff24 | |||
| 60ec21f34e | |||
| 57f6d2f8e7 | |||
| 53575c1876 | |||
| 0f372f5056 |
+99
-8
@@ -33,7 +33,6 @@ import ausleihung as au
|
||||
import audit_log as al
|
||||
import datetime
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from pymongo import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
import requests
|
||||
@@ -59,6 +58,7 @@ import subprocess
|
||||
# Set base directory and centralized settings
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
import settings as cfg
|
||||
from settings import MongoClient
|
||||
|
||||
|
||||
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
||||
@@ -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,15 @@ def get_items():
|
||||
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)
|
||||
db = client[MONGODB_DB]
|
||||
items_col = db['items']
|
||||
@@ -2977,18 +2987,98 @@ 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: essentials only for fast initial render
|
||||
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 +3118,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
@@ -25,7 +25,6 @@ Sammlungsstruktur:
|
||||
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
'''
|
||||
from pymongo import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
import datetime
|
||||
import pytz
|
||||
@@ -34,6 +33,7 @@ import os
|
||||
import json
|
||||
import shutil
|
||||
import settings as cfg
|
||||
from settings import MongoClient
|
||||
|
||||
# Add this helper function after imports
|
||||
def ensure_timezone_aware(dt):
|
||||
|
||||
+1
-1
@@ -26,10 +26,10 @@ Collection Structure:
|
||||
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
'''
|
||||
from pymongo import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
import datetime
|
||||
import settings as cfg
|
||||
from settings import MongoClient
|
||||
|
||||
|
||||
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'media')
|
||||
|
||||
@@ -12,6 +12,7 @@ defaults for the web application and helper modules.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from pymongo import MongoClient as _PyMongoClient
|
||||
|
||||
# Base directory of this Web package
|
||||
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)
|
||||
if not os.path.isabs(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)
|
||||
|
||||
+192
-11
@@ -725,9 +725,74 @@
|
||||
let allItems = [];
|
||||
|
||||
const MAIN_ITEMS_PAGE_SIZE = 120;
|
||||
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) {
|
||||
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');
|
||||
@@ -1009,7 +1074,7 @@
|
||||
populateFilterOptions('filter2-options', [...filter2Values].sort(), 2);
|
||||
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.
|
||||
if (!append) {
|
||||
itemsContainer.scrollLeft = 0;
|
||||
}
|
||||
@@ -1028,18 +1093,17 @@
|
||||
rebuildFilter3Options();
|
||||
}
|
||||
|
||||
if (data.has_more) {
|
||||
const nextOffset = (data.offset || offset) + (data.count || pageItems.length);
|
||||
const continueLoad = () => loadItems(nextOffset, true);
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(continueLoad, { timeout: 250 });
|
||||
} else {
|
||||
setTimeout(continueLoad, 0);
|
||||
}
|
||||
}
|
||||
mainItemsNextOffset = (data.offset || offset) + (data.count || pageItems.length);
|
||||
mainItemsHasMore = Boolean(data.has_more);
|
||||
updateMainItemsLoadingIndicator();
|
||||
setupMainItemsLazyLoading();
|
||||
maybePrefetchMainItems();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching items:', error);
|
||||
mainItemsLoadingMore = false;
|
||||
mainItemsHasMore = false;
|
||||
updateMainItemsLoadingIndicator();
|
||||
if (append) {
|
||||
return;
|
||||
}
|
||||
@@ -1049,6 +1113,72 @@
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
const searchInput = document.getElementById('code-search');
|
||||
const rawSearchTerm = searchInput.value;
|
||||
@@ -2766,6 +2896,57 @@
|
||||
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 {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
+193
-17
@@ -323,6 +323,57 @@
|
||||
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 {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
@@ -2966,9 +3017,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
if (parsedFilters.filter3) activeFilters.filter3 = parsedFilters.filter3;
|
||||
|
||||
// Update UI to show selected filters
|
||||
updateSelectedFilters(1);
|
||||
updateSelectedFilters(2);
|
||||
updateSelectedFilters(3);
|
||||
updateSelectedFiltersDisplay(1);
|
||||
updateSelectedFiltersDisplay(2);
|
||||
updateSelectedFiltersDisplay(3);
|
||||
|
||||
// We'll rebuild Filter 3 options after items are loaded
|
||||
} catch (e) {
|
||||
@@ -3256,9 +3307,75 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
// Function to load items from server
|
||||
const MAIN_ADMIN_ITEMS_PAGE_SIZE = 120;
|
||||
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) {
|
||||
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');
|
||||
@@ -3270,6 +3387,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const filter3Values = new Set();
|
||||
const pageItems = data.items || [];
|
||||
allItems = append ? allItems.concat(pageItems) : pageItems;
|
||||
totalItemsInSystem = Number(data.total || totalItemsInSystem || pageItems.length || 0);
|
||||
if (!append) {
|
||||
itemsContainer.innerHTML = '';
|
||||
}
|
||||
@@ -3286,9 +3404,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
return;
|
||||
}
|
||||
if (itemsIndicator) {
|
||||
const shownCount = allItems.length;
|
||||
const totalCount = Number(data.total || shownCount);
|
||||
itemsIndicator.textContent = `Objekte im System: ${shownCount}${data.has_more ? ` / ${totalCount} (lädt...)` : ''}`;
|
||||
itemsIndicator.textContent = `Objekte im System: ${totalItemsInSystem}${data.has_more ? ' (lädt...)' : ''}`;
|
||||
}
|
||||
pageItems.sort((a, b) => {
|
||||
const nameA = a.Name ? a.Name.toLowerCase() : '';
|
||||
@@ -3556,18 +3672,17 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
/* favorites render count removed */
|
||||
|
||||
if (data.has_more) {
|
||||
const nextOffset = (data.offset || offset) + (data.count || pageItems.length);
|
||||
const continueLoad = () => loadItems(nextOffset, true);
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(continueLoad, { timeout: 250 });
|
||||
} else {
|
||||
setTimeout(continueLoad, 0);
|
||||
}
|
||||
}
|
||||
mainAdminItemsNextOffset = (data.offset || offset) + (data.count || pageItems.length);
|
||||
mainAdminItemsHasMore = Boolean(data.has_more);
|
||||
updateMainAdminItemsLoadingIndicator();
|
||||
setupMainAdminItemsLazyLoading();
|
||||
maybePrefetchMainAdminItems();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching items:', error);
|
||||
mainAdminItemsLoadingMore = false;
|
||||
mainAdminItemsHasMore = false;
|
||||
updateMainAdminItemsLoadingIndicator();
|
||||
if (append) {
|
||||
return;
|
||||
}
|
||||
@@ -3581,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() {
|
||||
const searchInput = document.getElementById('code-search');
|
||||
const rawSearchTerm = searchInput.value;
|
||||
@@ -3675,7 +3851,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
});
|
||||
|
||||
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)) {
|
||||
|
||||
+1
-1
@@ -10,10 +10,10 @@ Provides methods for creating, validating, and retrieving user information.
|
||||
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
'''
|
||||
from pymongo import MongoClient
|
||||
import hashlib
|
||||
from bson.objectid import ObjectId
|
||||
import settings as cfg
|
||||
from settings import MongoClient
|
||||
|
||||
|
||||
def normalize_student_card_id(card_id):
|
||||
|
||||
Reference in New Issue
Block a user