Enhance library items API with pagination support and improve loading mechanism in UI

This commit is contained in:
2026-04-12 17:22:49 +02:00
parent daccf3334b
commit c078de0076
4 changed files with 346 additions and 62 deletions
+104 -11
View File
@@ -1742,21 +1742,62 @@ 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)
db = client[MONGODB_DB] db = client[MONGODB_DB]
items_db = db['items'] items_db = db['items']
ausleihungen_db = db['ausleihungen'] ausleihungen_db = db['ausleihungen']
query = {
'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({ library_items = list(
'ItemType': {'$in': ['book', 'cd', 'dvd', 'media']} 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'
@@ -1795,8 +1838,16 @@ def api_library_items():
item['LibraryDisplayStatus'] = 'available' item['LibraryDisplayStatus'] = 'available'
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,14 +2947,40 @@ 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')
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
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},
}) }
total_count = items_col.count_documents(base_query)
items_cur = items_col.find(base_query).sort([('Name', 1), ('_id', 1)])
if pagination_requested:
items_cur = items_cur.skip(offset).limit(limit)
items = [] items = []
for itm in items_cur: for itm in items_cur:
item_id_str = str(itm['_id']) item_id_str = str(itm['_id'])
@@ -2937,11 +3015,26 @@ 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,
'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>')
+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
+37 -8
View File
@@ -724,8 +724,10 @@
// 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') }}")
function loadItems(offset = 0, append = false) {
fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ITEMS_PAGE_SIZE}`)
.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 +737,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');
@@ -996,7 +1010,9 @@
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 (first card, which is now Z)
itemsContainer.scrollLeft = 0; if (!append) {
itemsContainer.scrollLeft = 0;
}
// Apply filters // Apply filters
applyFilters(); applyFilters();
@@ -1011,9 +1027,22 @@
if (activeFilters.filter1.length > 0 || activeFilters.filter2.length > 0) { if (activeFilters.filter1.length > 0 || activeFilters.filter2.length > 0) {
rebuildFilter3Options(); 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);
}
}
}) })
.catch(error => { .catch(error => {
console.error('Error fetching items:', error); console.error('Error fetching items:', error);
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>';
+41 -9
View File
@@ -3255,8 +3255,10 @@ 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') }}")
function loadItems(offset = 0, append = false) {
fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}`)
.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 +3268,12 @@ 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) { 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 +3286,27 @@ document.addEventListener('DOMContentLoaded', ()=>{
return; return;
} }
if (itemsIndicator) { if (itemsIndicator) {
itemsIndicator.textContent = `Objekte im System: ${data.items.length}`; const shownCount = allItems.length;
const totalCount = Number(data.total || shownCount);
itemsIndicator.textContent = `Objekte im System: ${shownCount}${data.has_more ? ` / ${totalCount} (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 +3538,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 +3555,22 @@ document.addEventListener('DOMContentLoaded', ()=>{
rebuildFilter3Options(); rebuildFilter3Options();
} }
/* favorites render count removed */ /* 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);
}
}
}) })
.catch(error => { .catch(error => {
console.error('Error fetching items:', error); console.error('Error fetching items:', error);
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 =