Enhance library items API with pagination support and improve loading mechanism in UI
This commit is contained in:
+104
-11
@@ -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>')
|
||||||
|
|||||||
@@ -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
@@ -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>';
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
Reference in New Issue
Block a user