Enhance library items API with pagination support and improve loading mechanism in UI
This commit is contained in:
@@ -290,6 +290,20 @@
|
||||
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 {
|
||||
display: none;
|
||||
@@ -501,6 +515,10 @@
|
||||
<!-- Items will be loaded here -->
|
||||
</tbody>
|
||||
</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>
|
||||
|
||||
<!-- Empty state -->
|
||||
@@ -523,6 +541,24 @@
|
||||
// State
|
||||
let libraryItems = [];
|
||||
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 scannerInstance = null;
|
||||
let scannerRunning = false;
|
||||
@@ -531,32 +567,124 @@
|
||||
let lastScanAt = 0;
|
||||
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() {
|
||||
if (pagingState.loading) return;
|
||||
pagingState.loading = true;
|
||||
try {
|
||||
const response = await fetch('/api/library_items');
|
||||
if (!response.ok) throw new Error('Failed to load library items');
|
||||
libraryItems = await response.json();
|
||||
displayItems(libraryItems);
|
||||
const firstPage = await fetchLibraryPage(0, API_PAGE_SIZE);
|
||||
libraryItems = firstPage.items || [];
|
||||
pagingState.offset = (firstPage.offset || 0) + (firstPage.count || libraryItems.length);
|
||||
pagingState.total = firstPage.total || libraryItems.length;
|
||||
pagingState.hasMore = Boolean(firstPage.has_more);
|
||||
|
||||
applyFiltersAndSearch(true);
|
||||
|
||||
if (pagingState.hasMore) {
|
||||
loadRemainingLibraryItemsInBackground();
|
||||
}
|
||||
} catch (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>';
|
||||
} finally {
|
||||
pagingState.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Display items in table
|
||||
function displayItems(items) {
|
||||
async function loadRemainingLibraryItemsInBackground() {
|
||||
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 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 = '';
|
||||
emptyState.style.display = 'block';
|
||||
loadMoreRow.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
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 statusClass = statusKey === 'damaged' ? 'status-damaged' : (statusKey === 'borrowed' ? 'status-borrowed' : 'status-available');
|
||||
@@ -567,7 +695,7 @@
|
||||
<tr>
|
||||
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</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>
|
||||
<span class="table-status ${statusClass}">
|
||||
@@ -585,6 +713,18 @@
|
||||
`;
|
||||
})()}
|
||||
`).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
|
||||
@@ -598,21 +738,11 @@
|
||||
|
||||
// Apply filters
|
||||
document.getElementById('applyFilterBtn').addEventListener('click', () => {
|
||||
const author = document.getElementById('filterAuthor').value.toLowerCase();
|
||||
const isbn = document.getElementById('filterISBN').value.toLowerCase();
|
||||
const type = document.getElementById('filterType').value;
|
||||
const status = document.getElementById('filterStatus').value;
|
||||
|
||||
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);
|
||||
activeFilters.author = document.getElementById('filterAuthor').value.toLowerCase();
|
||||
activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
|
||||
activeFilters.type = document.getElementById('filterType').value;
|
||||
activeFilters.status = document.getElementById('filterStatus').value;
|
||||
applyFiltersAndSearch(true);
|
||||
});
|
||||
|
||||
// Clear filters
|
||||
@@ -621,19 +751,19 @@
|
||||
document.getElementById('filterISBN').value = '';
|
||||
document.getElementById('filterType').value = '';
|
||||
document.getElementById('filterStatus').value = '';
|
||||
displayItems(libraryItems);
|
||||
activeFilters = { author: '', isbn: '', type: '', status: '' };
|
||||
applyFiltersAndSearch(true);
|
||||
});
|
||||
|
||||
// Search input
|
||||
document.getElementById('librarySearch').addEventListener('input', (e) => {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const toSearch = filteredItems.length > 0 ? filteredItems : libraryItems;
|
||||
const results = toSearch.filter(item =>
|
||||
(item.Name || '').toLowerCase().includes(searchTerm) ||
|
||||
(item.Autor || item.Author || '').toLowerCase().includes(searchTerm) ||
|
||||
(item.ISBN || item.Code4 || '').toLowerCase().includes(searchTerm)
|
||||
);
|
||||
displayItems(results);
|
||||
currentSearchTerm = (e.target.value || '').toLowerCase();
|
||||
applyFiltersAndSearch(true);
|
||||
});
|
||||
|
||||
document.getElementById('loadMoreBtn').addEventListener('click', () => {
|
||||
renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
|
||||
renderItems();
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
|
||||
+37
-8
@@ -724,8 +724,10 @@
|
||||
// Add this line to define allItems globally
|
||||
let allItems = [];
|
||||
|
||||
function loadItems() {
|
||||
fetch("{{ url_for('get_items') }}")
|
||||
const MAIN_ITEMS_PAGE_SIZE = 120;
|
||||
|
||||
function loadItems(offset = 0, append = false) {
|
||||
fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ITEMS_PAGE_SIZE}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const itemsContainer = document.querySelector('#items-container');
|
||||
@@ -735,27 +737,39 @@
|
||||
const filter3Values = new Set();
|
||||
|
||||
// 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
|
||||
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>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 nameB = b.Name ? b.Name.toLowerCase() : '';
|
||||
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 || []);
|
||||
window.currentFavorites = favoriteIds;
|
||||
try { sessionStorage.setItem('favoritesCache', JSON.stringify(Array.from(favoriteIds))); } catch(e){}
|
||||
data.items.forEach(item => {
|
||||
pageItems.forEach(item => {
|
||||
try {
|
||||
const card = document.createElement('div');
|
||||
card.classList.add('item-card');
|
||||
@@ -996,7 +1010,9 @@
|
||||
populateFilterOptions('filter3-options', [...filter3Values].sort(), 3);
|
||||
|
||||
// Start display from leftmost position (first card, which is now Z)
|
||||
itemsContainer.scrollLeft = 0;
|
||||
if (!append) {
|
||||
itemsContainer.scrollLeft = 0;
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
applyFilters();
|
||||
@@ -1011,9 +1027,22 @@
|
||||
if (activeFilters.filter1.length > 0 || activeFilters.filter2.length > 0) {
|
||||
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 => {
|
||||
console.error('Error fetching items:', error);
|
||||
if (append) {
|
||||
return;
|
||||
}
|
||||
const itemsContainer = document.querySelector('#items-container');
|
||||
itemsContainer.innerHTML =
|
||||
'<div class="error-message">Fehler beim Laden der Objekte. Bitte versuchen Sie es später erneut.</div>';
|
||||
|
||||
@@ -3255,8 +3255,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
});
|
||||
|
||||
// Function to load items from server
|
||||
function loadItems() {
|
||||
fetch("{{ url_for('get_items') }}")
|
||||
const MAIN_ADMIN_ITEMS_PAGE_SIZE = 120;
|
||||
|
||||
function loadItems(offset = 0, append = false) {
|
||||
fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const itemsContainer = document.querySelector('#items-container');
|
||||
@@ -3266,9 +3268,12 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const filter1Values = new Set();
|
||||
const filter2Values = new Set();
|
||||
const filter3Values = new Set();
|
||||
allItems = data.items || [];
|
||||
itemsContainer.innerHTML = '';
|
||||
if (!data.items || data.items.length === 0) {
|
||||
const pageItems = data.items || [];
|
||||
allItems = append ? allItems.concat(pageItems) : pageItems;
|
||||
if (!append) {
|
||||
itemsContainer.innerHTML = '';
|
||||
}
|
||||
if (!append && (!pageItems || pageItems.length === 0)) {
|
||||
itemsContainer.innerHTML = `
|
||||
<div class="no-items-message">
|
||||
<div>Keine Objekte gefunden</div>
|
||||
@@ -3281,15 +3286,27 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
return;
|
||||
}
|
||||
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 nameB = a.Name ? a.Name.toLowerCase() : '';
|
||||
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 || []);
|
||||
data.items.forEach(item => {
|
||||
pageItems.forEach(item => {
|
||||
try {
|
||||
const card = document.createElement('div');
|
||||
card.classList.add('item-card');
|
||||
@@ -3521,7 +3538,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
populateFilterOptions('filter2-options', [...filter2Values].sort(), 2);
|
||||
populateFilterOptions('filter3-options', [...filter3Values].sort(), 3);
|
||||
|
||||
itemsContainer.scrollLeft = 0;
|
||||
if (!append) {
|
||||
itemsContainer.scrollLeft = 0;
|
||||
}
|
||||
applyFilters();
|
||||
updateBulkDeleteSummary();
|
||||
|
||||
@@ -3536,9 +3555,22 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
rebuildFilter3Options();
|
||||
}
|
||||
/* 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 => {
|
||||
console.error('Error fetching items:', error);
|
||||
if (append) {
|
||||
return;
|
||||
}
|
||||
const itemsContainer = document.querySelector('#items-container');
|
||||
const itemsIndicator = document.getElementById('items-indicator');
|
||||
itemsContainer.innerHTML =
|
||||
|
||||
Reference in New Issue
Block a user