Compare commits

..

8 Commits

7 changed files with 506 additions and 39 deletions
+99 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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')
+19
View File
@@ -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
View File
@@ -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
View File
@@ -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
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.
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):