Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 299d11c1d0 | |||
| 4d1f95cb1f | |||
| 6b197fd9d1 | |||
| 58064ee249 | |||
| f7573b9a62 | |||
| 85d758fadb | |||
| 212dd2abcf | |||
| 689819df08 | |||
| 457d32a087 | |||
| cded7b02fe |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
NUITKA_BUILD=0
|
NUITKA_BUILD=0
|
||||||
INVENTAR_HTTP_PORT=10000
|
INVENTAR_HTTP_PORT=10000
|
||||||
INVENTAR_HTTP_PORTS=10000
|
INVENTAR_HTTP_PORTS=10000
|
||||||
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
|
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
|
||||||
INVENTAR_APP_CPUS=1.0
|
INVENTAR_APP_CPUS=1.0
|
||||||
INVENTAR_APP_MEM_LIMIT=384m
|
INVENTAR_APP_MEM_LIMIT=384m
|
||||||
INVENTAR_APP_MEM_SWAP_LIMIT=768m
|
INVENTAR_APP_MEM_SWAP_LIMIT=768m
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
v0.7.42
|
||||||
+59
-9
@@ -25,9 +25,10 @@ Features:
|
|||||||
- Booking and reservation of items
|
- Booking and reservation of items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file
|
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from jinja2 import TemplateNotFound
|
||||||
import user as us
|
import user as us
|
||||||
import items as it
|
import items as it
|
||||||
import ausleihung as au
|
import ausleihung as au
|
||||||
@@ -353,7 +354,7 @@ def _enforce_active_session_user():
|
|||||||
@app.before_request
|
@app.before_request
|
||||||
def _enforce_module_access():
|
def _enforce_module_access():
|
||||||
endpoint = request.endpoint or ''
|
endpoint = request.endpoint or ''
|
||||||
if endpoint == 'static' or endpoint.startswith('static') or request.path.startswith('/api/'):
|
if endpoint == 'static' or endpoint.startswith('static'):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for name, matcher in cfg.MODULES._path_matchers.items():
|
for name, matcher in cfg.MODULES._path_matchers.items():
|
||||||
@@ -363,7 +364,18 @@ def _enforce_module_access():
|
|||||||
'inventory': "Inventar-Modul ist deaktiviert.",
|
'inventory': "Inventar-Modul ist deaktiviert.",
|
||||||
'student_cards': "Schülerausweis-Modul ist deaktiviert."
|
'student_cards': "Schülerausweis-Modul ist deaktiviert."
|
||||||
}.get(name, f"{name.capitalize()}-Modul ist deaktiviert.")
|
}.get(name, f"{name.capitalize()}-Modul ist deaktiviert.")
|
||||||
return msg, 403
|
|
||||||
|
if request.path.startswith('/api/') or request.is_json:
|
||||||
|
return jsonify({'ok': False, 'message': msg}), 403
|
||||||
|
|
||||||
|
flash(msg, 'info')
|
||||||
|
|
||||||
|
if name != 'inventory' and cfg.MODULES.is_enabled('inventory'):
|
||||||
|
return redirect(url_for('home'))
|
||||||
|
elif name != 'library' and cfg.MODULES.is_enabled('library'):
|
||||||
|
return redirect(url_for('library_view'))
|
||||||
|
|
||||||
|
return redirect(url_for('my_borrowed_items'))
|
||||||
|
|
||||||
""" -----------------------------------------------------------After Request Handlers----------------------------------------------------------------------------- """
|
""" -----------------------------------------------------------After Request Handlers----------------------------------------------------------------------------- """
|
||||||
|
|
||||||
@@ -401,6 +413,26 @@ def _set_security_headers(response):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(TemplateNotFound)
|
||||||
|
def handle_template_not_found(e):
|
||||||
|
"""Handle missing template files with a 404 response."""
|
||||||
|
app.logger.error(f"Template not found: {e.name}")
|
||||||
|
if request.is_json or request.path.startswith('/api/'):
|
||||||
|
return jsonify({'error': 'Template not found', 'status': 404}), 404
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def handle_not_found(e):
|
||||||
|
"""Handle 404 errors with a user-friendly response."""
|
||||||
|
app.logger.warning(f"404 Not Found: {request.path}")
|
||||||
|
if request.is_json or request.path.startswith('/api/'):
|
||||||
|
return jsonify({'error': 'Not found', 'status': 404}), 404
|
||||||
|
if 'username' in session:
|
||||||
|
return render_template('error.html', error_code=404, error_message='Seite nicht gefunden.'), 404
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
|
def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
|
||||||
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}:
|
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}:
|
||||||
return jsonify({'error': message}), 400
|
return jsonify({'error': message}), 400
|
||||||
@@ -2619,7 +2651,8 @@ def home():
|
|||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
|
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
|
||||||
|
open_item=request.args.get('open_item')
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||||
@@ -2662,6 +2695,7 @@ def home_admin():
|
|||||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
|
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
|
||||||
school_info=_get_school_info_for_export(),
|
school_info=_get_school_info_for_export(),
|
||||||
|
open_item=request.args.get('open_item')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -4667,12 +4701,26 @@ def upload_item():
|
|||||||
|
|
||||||
# Check if base code is unique for single-item uploads
|
# Check if base code is unique for single-item uploads
|
||||||
if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]):
|
if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]):
|
||||||
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
|
existing_item = it._get_items_collection().find_one({"code_4": code_4[0]})
|
||||||
if is_mobile:
|
if existing_item:
|
||||||
return jsonify({'success': False, 'message': error_msg}), 400
|
error_msg = 'Der Code wird bereits verwendet. Umleitung zum Eintrag.'
|
||||||
|
if is_mobile:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': error_msg,
|
||||||
|
'existing_item_id': str(existing_item['_id']),
|
||||||
|
'redirect_to_item': True
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
flash(error_msg, 'info')
|
||||||
|
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
|
||||||
else:
|
else:
|
||||||
flash(error_msg, 'error')
|
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
|
||||||
return redirect(url_for(success_redirect_endpoint))
|
if is_mobile:
|
||||||
|
return jsonify({'success': False, 'message': error_msg}), 400
|
||||||
|
else:
|
||||||
|
flash(error_msg, 'error')
|
||||||
|
return redirect(url_for(success_redirect_endpoint))
|
||||||
|
|
||||||
# Validate optional per-item codes
|
# Validate optional per-item codes
|
||||||
if individual_codes:
|
if individual_codes:
|
||||||
@@ -8539,11 +8587,13 @@ def manage_filters():
|
|||||||
# Get predefined filter values
|
# Get predefined filter values
|
||||||
filter1_values = it.get_predefined_filter_values(1)
|
filter1_values = it.get_predefined_filter_values(1)
|
||||||
filter2_values = it.get_predefined_filter_values(2)
|
filter2_values = it.get_predefined_filter_values(2)
|
||||||
|
filter3_values = it.get_predefined_filter_values(3)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'manage_filters.html',
|
'manage_filters.html',
|
||||||
filter1_values=filter1_values,
|
filter1_values=filter1_values,
|
||||||
filter2_values=filter2_values,
|
filter2_values=filter2_values,
|
||||||
|
filter3_values=filter3_values,
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
|
||||||
)
|
)
|
||||||
|
|||||||
+12
-3
@@ -570,7 +570,10 @@ def get_primary_filters():
|
|||||||
items = db['items']
|
items = db['items']
|
||||||
filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f]
|
filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f]
|
||||||
client.close()
|
client.close()
|
||||||
return filters
|
|
||||||
|
# Add predefined values
|
||||||
|
predefined = get_predefined_filter_values(1)
|
||||||
|
return sorted(list(set(filters + predefined)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error retrieving primary filters: {e}")
|
print(f"Error retrieving primary filters: {e}")
|
||||||
return []
|
return []
|
||||||
@@ -589,7 +592,10 @@ def get_secondary_filters():
|
|||||||
items = db['items']
|
items = db['items']
|
||||||
filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f]
|
filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f]
|
||||||
client.close()
|
client.close()
|
||||||
return filters
|
|
||||||
|
# Add predefined values
|
||||||
|
predefined = get_predefined_filter_values(2)
|
||||||
|
return sorted(list(set(filters + predefined)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error retrieving secondary filters: {e}")
|
print(f"Error retrieving secondary filters: {e}")
|
||||||
return []
|
return []
|
||||||
@@ -608,7 +614,10 @@ def get_tertiary_filters():
|
|||||||
items = db['items']
|
items = db['items']
|
||||||
filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f]
|
filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f]
|
||||||
client.close()
|
client.close()
|
||||||
return filters
|
|
||||||
|
# Add predefined values
|
||||||
|
predefined = get_predefined_filter_values(3)
|
||||||
|
return sorted(list(set(filters + predefined)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error retrieving tertiary filters: {e}")
|
print(f"Error retrieving tertiary filters: {e}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if error_code == 404 %}
|
||||||
|
Seite nicht gefunden - Inventarsystem
|
||||||
|
{% else %}
|
||||||
|
Fehler - Inventarsystem
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="error-container" style="text-align: center; padding: 60px 20px; min-height: 70vh; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||||
|
<div style="max-width: 600px;">
|
||||||
|
<!-- Error Code Display -->
|
||||||
|
<div style="font-size: 120px; font-weight: bold; color: #ef4444; margin-bottom: 20px;">
|
||||||
|
{{ error_code }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Title -->
|
||||||
|
<h1 style="font-size: 32px; margin: 20px 0; color: #1f2937;">
|
||||||
|
{% if error_code == 404 %}
|
||||||
|
Seite nicht gefunden
|
||||||
|
{% else %}
|
||||||
|
Ein Fehler ist aufgetreten
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<p style="font-size: 18px; color: #6b7280; margin: 20px 0 40px;">
|
||||||
|
{{ error_message or 'Es tut uns leid, aber die angeforderte Seite konnte nicht gefunden werden.' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
|
||||||
|
<a href="{{ url_for('home') }}" class="button" style="background-color: #3b82f6; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: 500;">
|
||||||
|
Zur Startseite
|
||||||
|
</a>
|
||||||
|
<a href="javascript:history.back()" class="button" style="background-color: #6b7280; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: 500;">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Help -->
|
||||||
|
<div style="margin-top: 60px; padding: 20px; background-color: #f3f4f6; border-radius: 8px;">
|
||||||
|
<p style="color: #6b7280; font-size: 14px;">
|
||||||
|
Wenn Sie denken, dass dies ein Fehler ist, oder Sie Hilfe benötigen,
|
||||||
|
<a href="{{ url_for('impressum') }}" style="color: #3b82f6; text-decoration: none;">kontaktieren Sie den Administrator</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error-container {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
+70
-6
@@ -751,6 +751,7 @@
|
|||||||
let mainItemsSentinel = null;
|
let mainItemsSentinel = null;
|
||||||
let mainItemsLoadingIndicator = null;
|
let mainItemsLoadingIndicator = null;
|
||||||
let mainItemsScrollPrefetchBound = false;
|
let mainItemsScrollPrefetchBound = false;
|
||||||
|
let cardVirtualizationObserver = null;
|
||||||
|
|
||||||
function ensureMainItemsLoadingIndicator() {
|
function ensureMainItemsLoadingIndicator() {
|
||||||
const itemsContainer = document.querySelector('#items-container');
|
const itemsContainer = document.querySelector('#items-container');
|
||||||
@@ -1189,6 +1190,55 @@
|
|||||||
mainItemsScrollPrefetchBound = true;
|
mainItemsScrollPrefetchBound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup DOM virtualization for items
|
||||||
|
if (!cardVirtualizationObserver) {
|
||||||
|
cardVirtualizationObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const card = entry.target;
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (card.dataset.deloaded === 'true') {
|
||||||
|
card.innerHTML = card.dataset.savedHtml;
|
||||||
|
card.dataset.deloaded = 'false';
|
||||||
|
card.classList.remove('is-deloaded');
|
||||||
|
const favBtn = card.querySelector('.bookmark-btn');
|
||||||
|
if (favBtn) {
|
||||||
|
favBtn.addEventListener('click', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
toggleFavorite(card.dataset.itemId, favBtn, card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const cardContent = card.querySelector('.card-content');
|
||||||
|
if (cardContent) {
|
||||||
|
cardContent.addEventListener('click', function(e) {
|
||||||
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.tagName === 'INPUT' ||
|
||||||
|
e.target.closest('button') || e.target.closest('a') || e.target.closest('.image-container')) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
openItemQuick(card.dataset.itemId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (card.dataset.deloaded !== 'true') {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
if (rect.height > 50) {
|
||||||
|
card.style.minHeight = rect.height + 'px';
|
||||||
|
card.dataset.itemId = card.querySelector('.card-content')?.dataset.itemId || '';
|
||||||
|
card.dataset.savedHtml = card.innerHTML;
|
||||||
|
card.innerHTML = '';
|
||||||
|
card.dataset.deloaded = 'true';
|
||||||
|
card.classList.add('is-deloaded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
rootMargin: isMobileLayout ? '1500px 0px 1500px 0px' : '0px 2500px 0px 2500px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe all valid cards
|
||||||
|
document.querySelectorAll('.item-card').forEach(card => cardVirtualizationObserver.observe(card));
|
||||||
|
|
||||||
if (!mainItemsHasMore) return;
|
if (!mainItemsHasMore) return;
|
||||||
|
|
||||||
mainItemsObserver = new IntersectionObserver(entries => {
|
mainItemsObserver = new IntersectionObserver(entries => {
|
||||||
@@ -2419,29 +2469,29 @@
|
|||||||
// Add these functions right after the loadFilterState() function
|
// Add these functions right after the loadFilterState() function
|
||||||
|
|
||||||
function rebuildFilter3Options() {
|
function rebuildFilter3Options() {
|
||||||
if (!allItems || allItems.length === 0) return;
|
if (!allItems) return;
|
||||||
|
|
||||||
// Get current filter 1 and filter 2 selections
|
// Get current filter 1 and filter 2 selections
|
||||||
const filter1Selections = activeFilters.filter1;
|
const filter1Selections = activeFilters.filter1;
|
||||||
const filter2Selections = activeFilters.filter2;
|
const filter2Selections = activeFilters.filter2;
|
||||||
|
|
||||||
// If both filters are empty, show all filter3 values
|
// If both filters are empty, show all filter3 values from server
|
||||||
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
|
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
|
||||||
// Get all unique filter3 values
|
// Include dynamically added ones from loaded items too, just in case
|
||||||
const allFilter3Values = new Set();
|
const combinedFilter3Values = new Set(typeof allFilter3Values !== 'undefined' ? [...allFilter3Values] : []);
|
||||||
allItems.forEach(item => {
|
allItems.forEach(item => {
|
||||||
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
|
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
|
||||||
(item.Filter3 ? [item.Filter3] : []);
|
(item.Filter3 ? [item.Filter3] : []);
|
||||||
|
|
||||||
filter3Array.forEach(value => {
|
filter3Array.forEach(value => {
|
||||||
if (value && typeof value === 'string' && value.trim() !== '') {
|
if (value && typeof value === 'string' && value.trim() !== '') {
|
||||||
allFilter3Values.add(value);
|
combinedFilter3Values.add(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate filter 3 dropdown with all values
|
// Populate filter 3 dropdown with all values
|
||||||
populateFilterOptions('filter3-options', [...allFilter3Values].sort(), 3);
|
populateFilterOptions('filter3-options', [...combinedFilter3Values].sort(), 3);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3114,6 +3164,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 300px 450px;
|
||||||
}
|
}
|
||||||
.item-card:hover {
|
.item-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
@@ -4614,6 +4666,18 @@ function openItemQuick(id){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
{% if open_item %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
if (typeof openEditModalFromServer === 'function') {
|
||||||
|
setTimeout(function() {
|
||||||
|
openEditModalFromServer('{{ open_item }}');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<style>
|
<style>
|
||||||
/* Admin/Main Content Container align */
|
/* Admin/Main Content Container align */
|
||||||
|
|||||||
@@ -403,6 +403,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 300px 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-card.bulk-selected {
|
.item-card.bulk-selected {
|
||||||
@@ -2876,30 +2878,29 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rebuildFilter3Options() {
|
function rebuildFilter3Options() {
|
||||||
if (!allItems || allItems.length === 0) return;
|
if (!allItems) return;
|
||||||
|
|
||||||
// Get current filter 1 and filter 2 selections
|
// Get current filter 1 and filter 2 selections
|
||||||
const filter1Selections = activeFilters.filter1;
|
const filter1Selections = activeFilters.filter1;
|
||||||
const filter2Selections = activeFilters.filter2;
|
const filter2Selections = activeFilters.filter2;
|
||||||
|
|
||||||
// If both filters are empty, show all filter3 values
|
// If both filters are empty, show all filter3 values from server
|
||||||
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
|
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
|
||||||
// Get all unique filter3 values
|
// Include dynamically added ones from loaded items too, just in case
|
||||||
const allFilter3Values = new Set();
|
const combinedFilter3Values = new Set(typeof allFilter3Values !== 'undefined' ? [...allFilter3Values] : []);
|
||||||
allItems.forEach(item => {
|
allItems.forEach(item => {
|
||||||
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
|
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
|
||||||
(item.Filter3 ? [item.Filter3] : []);
|
(item.Filter3 ? [item.Filter3] : []);
|
||||||
|
|
||||||
filter3Array.forEach(value => {
|
filter3Array.forEach(value => {
|
||||||
if (value && typeof value === 'string' && value.trim() !== '') {
|
if (value && typeof value === 'string' && value.trim() !== '') {
|
||||||
allFilter3Values.add(value);
|
combinedFilter3Values.add(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate filter 3 dropdown with all values
|
// Populate filter 3 dropdown with all values
|
||||||
populateFilterOptions('filter3-options', [...allFilter3Values].sort(), 3);
|
populateFilterOptions('filter3-options', [...combinedFilter3Values].sort(), 3);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter items based on filter1 and filter2 selections
|
// Filter items based on filter1 and filter2 selections
|
||||||
@@ -3356,6 +3357,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
let mainAdminItemsLoadingIndicator = null;
|
let mainAdminItemsLoadingIndicator = null;
|
||||||
let mainAdminItemsScrollPrefetchBound = false;
|
let mainAdminItemsScrollPrefetchBound = false;
|
||||||
let totalItemsInSystem = 0;
|
let totalItemsInSystem = 0;
|
||||||
|
let cardVirtualizationObserver = null;
|
||||||
|
|
||||||
function ensureMainAdminItemsLoadingIndicator() {
|
function ensureMainAdminItemsLoadingIndicator() {
|
||||||
const itemsContainer = document.querySelector('#items-container');
|
const itemsContainer = document.querySelector('#items-container');
|
||||||
@@ -3783,6 +3785,56 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
mainAdminItemsScrollPrefetchBound = true;
|
mainAdminItemsScrollPrefetchBound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup DOM virtualization for items
|
||||||
|
if (!cardVirtualizationObserver) {
|
||||||
|
cardVirtualizationObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const card = entry.target;
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (card.dataset.deloaded === 'true') {
|
||||||
|
card.innerHTML = card.dataset.savedHtml;
|
||||||
|
card.dataset.deloaded = 'false';
|
||||||
|
card.classList.remove('is-deloaded');
|
||||||
|
const favBtn = card.querySelector('.bookmark-btn');
|
||||||
|
if (favBtn) {
|
||||||
|
favBtn.addEventListener('click', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
toggleFavorite(card.dataset.itemId, favBtn, card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const cardContent = card.querySelector('.card-content');
|
||||||
|
if (cardContent) {
|
||||||
|
cardContent.addEventListener('click', function(e) {
|
||||||
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.tagName === 'INPUT' ||
|
||||||
|
e.target.closest('button') || e.target.closest('a') || e.target.closest('.image-container') ||
|
||||||
|
e.target.closest('.bulk-delete-toggle') || e.target.closest('label')) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
openItemQuick(card.dataset.itemId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (card.dataset.deloaded !== 'true') {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
if (rect.height > 50) {
|
||||||
|
card.style.minHeight = rect.height + 'px';
|
||||||
|
card.dataset.itemId = card.querySelector('.card-content')?.dataset.itemId || '';
|
||||||
|
card.dataset.savedHtml = card.innerHTML;
|
||||||
|
card.innerHTML = '';
|
||||||
|
card.dataset.deloaded = 'true';
|
||||||
|
card.classList.add('is-deloaded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
rootMargin: isMobileLayout ? '1500px 0px 1500px 0px' : '0px 2500px 0px 2500px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe all valid cards
|
||||||
|
document.querySelectorAll('.item-card').forEach(card => cardVirtualizationObserver.observe(card));
|
||||||
|
|
||||||
if (!mainAdminItemsHasMore) return;
|
if (!mainAdminItemsHasMore) return;
|
||||||
|
|
||||||
mainAdminItemsObserver = new IntersectionObserver(entries => {
|
mainAdminItemsObserver = new IntersectionObserver(entries => {
|
||||||
@@ -5716,4 +5768,16 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
{% if open_item %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
if (typeof openEditModalFromServer === 'function') {
|
||||||
|
setTimeout(function() {
|
||||||
|
openEditModalFromServer('{{ open_item }}');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -15,16 +15,16 @@
|
|||||||
<h1 class="mb-4">Filterwerte verwalten</h1>
|
<h1 class="mb-4">Filterwerte verwalten</h1>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Filter 1: Unterrichtsfach -->
|
<!-- Filter 1 -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title h5 mb-0">Unterrichtsfach (Filter 1)</h2>
|
<h2 class="card-title h5 mb-0">Fach/Kategorie (Filter 1)</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="value" class="form-control" placeholder="Neues Unterrichtsfach..." required>
|
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
|
||||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -51,16 +51,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter 2: Jahrgangsstufe -->
|
<!-- Filter 2 -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title h5 mb-0">Jahrgangsstufe (Filter 2)</h2>
|
<h2 class="card-title h5 mb-0">System/Bereich (Filter 2)</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="value" class="form-control" placeholder="Neue Jahrgangsstufe..." required>
|
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
|
||||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -86,6 +86,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter 3 -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title h5 mb-0">Typ/Art (Filter 3)</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('add_filter_value', filter_num=3) }}" class="mb-4">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
|
||||||
|
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h5 class="mb-3">Vorhandene Werte</h5>
|
||||||
|
{% if filter3_values %}
|
||||||
|
<div class="list-group">
|
||||||
|
{% for value in filter3_values %}
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
{{ value }}
|
||||||
|
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=3, value=value) }}" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger"
|
||||||
|
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">Keine Werte definiert.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
|
|||||||
@@ -709,7 +709,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<a href="{{ url_for(back_target|default('home_admin')) }}" class="nav-back-button">← Zurück zur Artikelübersicht</a>
|
|
||||||
|
|
||||||
{% if show_library_features %}
|
{% if show_library_features %}
|
||||||
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||||
|
|||||||
+1
-7
@@ -498,8 +498,6 @@ def check_nm_pwd(username, password):
|
|||||||
finally:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def add_user(
|
def add_user(
|
||||||
username,
|
username,
|
||||||
@@ -536,16 +534,12 @@ def add_user(
|
|||||||
for key, value in page_permissions.items():
|
for key, value in page_permissions.items():
|
||||||
permission_defaults['pages'][str(key)] = bool(value)
|
permission_defaults['pages'][str(key)] = bool(value)
|
||||||
|
|
||||||
alias_first = name if str(name or '').strip() else username
|
|
||||||
alias_last = last_name if str(last_name or '').strip() else ''
|
|
||||||
name_alias = build_name_synonym(alias_first, alias_last)
|
|
||||||
|
|
||||||
user_doc = {
|
user_doc = {
|
||||||
'Username': username,
|
'Username': username,
|
||||||
'Password': hashing(password),
|
'Password': hashing(password),
|
||||||
'Admin': False,
|
'Admin': False,
|
||||||
'active_ausleihung': None,
|
'active_ausleihung': None,
|
||||||
'name': name_alias,
|
'name': name.strip() if name else '',
|
||||||
'last_name': last_name.strip() if last_name else '',
|
'last_name': last_name.strip() if last_name else '',
|
||||||
'IsStudent': bool(is_student),
|
'IsStudent': bool(is_student),
|
||||||
'PermissionPreset': permission_defaults['preset'],
|
'PermissionPreset': permission_defaults['preset'],
|
||||||
|
|||||||
+47
-25
@@ -1,41 +1,63 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build: .
|
image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.39
|
||||||
container_name: inventory-app
|
container_name: inventarsystem-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
ports:
|
||||||
- MONGO_URL=mongodb://mongodb:27017/inventar
|
- "10000:8000"
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- BASE_URL=https://inventar.maximiliangruendinger.de #alle öffentliche subdomains
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongodb
|
- mongodb
|
||||||
- redis
|
- redis
|
||||||
|
environment:
|
||||||
|
INVENTAR_MONGODB_HOST: mongodb
|
||||||
|
INVENTAR_MONGODB_PORT: "27017"
|
||||||
|
INVENTAR_MONGODB_DB: Inventarsystem
|
||||||
|
INVENTAR_BACKUP_FOLDER: /data/backups
|
||||||
|
INVENTAR_LOGS_FOLDER: /data/logs
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/app/config.json:ro
|
||||||
|
- app_uploads:/app/Web/uploads
|
||||||
|
- app_thumbnails:/app/Web/thumbnails
|
||||||
|
- app_previews:/app/Web/previews
|
||||||
|
- app_qrcodes:/app/Web/QRCodes
|
||||||
|
- app_backups:/data/backups
|
||||||
|
- app_logs:/data/logs
|
||||||
|
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo:latest
|
image: mongo:7.0
|
||||||
container_name: mongodb
|
container_name: inventarsystem-mongodb
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- mongo_data:/data/db
|
- mongodb_data:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:7-alpine
|
||||||
container_name: redis
|
container_name: inventarsystem-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
|
||||||
cloudflared:
|
ports:
|
||||||
image: cloudflare/cloudflared:latest
|
- "6379:6379"
|
||||||
container_name: cloudflared
|
|
||||||
restart: unless-stopped
|
|
||||||
# Der Tunnel-Name 'homeserver' muss zu deiner credentials.json passen
|
|
||||||
command: tunnel run homeserver
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yml:/etc/cloudflared/config.yml
|
- redis_data:/data
|
||||||
- ./credentials.json:/etc/cloudflared/credentials.json
|
healthcheck:
|
||||||
depends_on:
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
- app
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mongo_data:
|
mongodb_data:
|
||||||
|
app_uploads:
|
||||||
|
app_thumbnails:
|
||||||
|
app_previews:
|
||||||
|
app_qrcodes:
|
||||||
|
app_backups:
|
||||||
|
app_logs:
|
||||||
|
redis_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user