Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions[bot] 725584a104 chore: bump version to v0.8.14 2026-05-30 12:46:44 +00:00
Aiirondev_dev fbcc3b4522 feat: enhance calendar loading experience with skeleton UI 2026-05-30 14:46:22 +02:00
github-actions[bot] adc2bad3fb chore: bump version to v0.8.13 2026-05-30 12:31:16 +00:00
Aiirondev_dev f7be7d9bf0 feat: implement maintenance and not found error pages with appropriate messaging 2026-05-30 14:30:35 +02:00
7 changed files with 197 additions and 16 deletions
+1 -1
View File
@@ -1 +1 @@
v0.8.12
v0.8.14
+8 -4
View File
@@ -626,14 +626,18 @@ def handle_unexpected_exception(e):
if request.is_json or request.path.startswith('/api/'):
return jsonify({'error': 'Internal server error', 'status': 500}), 500
if 'username' in session:
try:
return render_template(
'maintenance.html',
error_code=500,
error_message='Das System führt gerade Wartungsarbeiten durch.',
), 500
except Exception:
try:
return render_template('error.html', error_code=500, error_message='Interner Serverfehler.'), 500
return render_template('error.html', error_code=500, error_message='Das System führt gerade Wartungsarbeiten durch.'), 500
except Exception:
return 'Internal Server Error', 500
return redirect(url_for('login'))
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'}:
+13 -5
View File
@@ -22,6 +22,14 @@ from Web.modules.database.settings import MongoClient
from bson.objectid import ObjectId
import datetime
def _get_tenant_db(client):
try:
from tenant import get_tenant_db
return get_tenant_db(client)
except Exception:
return client[cfg.MONGODB_DB]
def _active_record_query(extra_query=None):
"""Build a query that excludes logically deleted records."""
base_query = {'Deleted': {'$ne': True}}
@@ -33,7 +41,7 @@ def _active_record_query(extra_query=None):
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False):
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
db = _get_tenant_db(client)
items = db['appointments']
item = {
@@ -68,7 +76,7 @@ def get_item(id):
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
db = _get_tenant_db(client)
items = db['appointments']
item = items.find_one(_active_record_query({'_id': ObjectId(id)}))
client.close()
@@ -90,7 +98,7 @@ def update(id,slots_used: list):
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
db = _get_tenant_db(client)
items = db['appointments']
update_data = {
@@ -124,7 +132,7 @@ def remove_slot(id, date_start_time, name):
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
db = _get_tenant_db(client)
items = db['appointments']
# Attempt to pull the exact element (stored as an array/tuple)
@@ -152,7 +160,7 @@ def remove(id):
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
db = _get_tenant_db(client)
items = db['appointments']
result = items.delete_one({'_id': ObjectId(id)})
+10 -4
View File
@@ -13,6 +13,14 @@ def _require_module_enabled():
return redirect(url_for('home'))
return None
def _appointment_not_found_response():
return render_template(
'terminplaner_not_found.html',
error_code=404,
error_message='Der Termin wurde nicht gefunden.',
), 404
@appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
def client(appointment_id):
"""
@@ -24,8 +32,7 @@ def client(appointment_id):
available = appointment_service.get_available(appointment_id)
if not available:
flash('Der Termin wurde nicht gefunden.', 'error')
return redirect(url_for('terminplan'))
return _appointment_not_found_response()
if request.method == 'POST':
start_daytime = request.form.get('start_day_time')
@@ -113,8 +120,7 @@ def calendar_export(appointment_id):
ics_content = appointment_service.build_calendar_ics(appointment_id)
if not ics_content:
flash('Der Termin wurde nicht gefunden.', 'error')
return redirect(url_for('terminplaner.configure'))
return _appointment_not_found_response()
response = Response(ics_content, mimetype='text/calendar; charset=utf-8')
response.headers['Content-Disposition'] = f'attachment; filename=terminplan-{appointment_id}.ics'
+28
View File
@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}Wartungsarbeiten - Inventarsystem{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-lg-8 col-xl-7">
<div class="card border-0 shadow-lg rounded-4 overflow-hidden">
<div class="card-header text-white" style="background: linear-gradient(135deg, #0f4c5c, #16697a);">
<p class="text-uppercase small fw-semibold mb-1 opacity-75">Systemstatus</p>
<h1 class="h3 mb-0 fw-bold">Wartungsarbeiten</h1>
</div>
<div class="card-body p-4 p-md-5 bg-white text-center">
<div class="display-3 fw-bold text-warning mb-3">500</div>
<p class="lead mb-2">Das System führt gerade Wartungsarbeiten durch.</p>
<p class="text-muted mb-4">Bitte versuchen Sie es in wenigen Minuten erneut. Falls das Problem bestehen bleibt, informieren Sie bitte die Administration.</p>
<div class="d-flex flex-column flex-sm-row justify-content-center gap-2">
<a href="{{ url_for('login') }}" class="btn btn-primary btn-lg">Zum Login</a>
<a href="javascript:history.back()" class="btn btn-outline-secondary btn-lg">Zurück</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+109 -2
View File
@@ -41,7 +41,37 @@
</label>
</div>
<div id="calendar"></div>
<div class="calendar-stage">
<div id="calendar-skeleton" class="calendar-skeleton" aria-hidden="true">
<div class="skeleton-toolbar">
<span class="skeleton-pill"></span>
<span class="skeleton-pill"></span>
<span class="skeleton-pill"></span>
</div>
<div class="skeleton-grid">
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
</div>
</div>
<div id="calendar"></div>
</div>
<!-- Modal for new bookings -->
<div id="booking-modal" class="modal">
@@ -254,7 +284,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize calendar
calendar = new FullCalendar.Calendar(calendarEl, {
plugins: [FullCalendar.dayGridPlugin, FullCalendar.timeGridPlugin],
initialView: 'timeGridDay',
locale: 'de',
headerToolbar: false, // We're using our custom header
@@ -281,6 +310,12 @@ document.addEventListener('DOMContentLoaded', function() {
minute: '2-digit',
hour12: false
},
loading: function(isLoading) {
const skeleton = document.getElementById('calendar-skeleton');
if (skeleton) {
skeleton.classList.toggle('is-hidden', !isLoading);
}
},
selectAllow: function(info) {
const startHour = info.start.getHours();
const endHour = info.end.getHours();
@@ -452,6 +487,10 @@ document.addEventListener('DOMContentLoaded', function() {
// Render calendar
try {
const calendarSkeleton = document.getElementById('calendar-skeleton');
if (calendarSkeleton) {
calendarSkeleton.classList.remove('is-hidden');
}
calendar.render();
setCalendarView(activeCalendarView);
@@ -1259,6 +1298,74 @@ document.addEventListener('DOMContentLoaded', function() {
padding: 20px;
}
.calendar-stage {
position: relative;
min-height: 620px;
}
.calendar-skeleton {
position: absolute;
inset: 0;
z-index: 5;
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 20px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 250, 252, 0.96));
padding: 20px;
overflow: hidden;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.calendar-skeleton.is-hidden {
display: none;
}
.skeleton-toolbar {
display: flex;
gap: 10px;
margin-bottom: 18px;
}
.skeleton-pill,
.skeleton-day-header,
.skeleton-slot {
position: relative;
overflow: hidden;
background: linear-gradient(90deg, #e9eef5 0%, #f7f9fc 50%, #e9eef5 100%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
.skeleton-pill {
width: 90px;
height: 34px;
border-radius: 999px;
}
.skeleton-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 10px;
}
.skeleton-day-header {
height: 28px;
border-radius: 10px;
}
.skeleton-slot {
height: 72px;
border-radius: 14px;
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.calendar-header {
margin-bottom: 20px;
}
+28
View File
@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}Termin nicht gefunden - Inventarsystem{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-lg-8 col-xl-7">
<div class="card border-0 shadow-lg rounded-4 overflow-hidden">
<div class="card-header text-white" style="background: linear-gradient(135deg, #0f4c5c, #16697a);">
<p class="text-uppercase small fw-semibold mb-1 opacity-75">Terminplaner</p>
<h1 class="h3 mb-0 fw-bold">Termin nicht gefunden</h1>
</div>
<div class="card-body p-4 p-md-5 bg-white text-center">
<div class="display-3 fw-bold text-danger mb-3">404</div>
<p class="lead mb-2">Der angeforderte Termin existiert nicht mehr oder der Link ist ungültig.</p>
<p class="text-muted mb-4">Bitte prüfen Sie den Link erneut oder öffnen Sie den Terminplaner von der Startseite aus.</p>
<div class="d-flex flex-column flex-sm-row justify-content-center gap-2">
<a href="{{ url_for('home') }}" class="btn btn-primary btn-lg">Zur Startseite</a>
<a href="javascript:history.back()" class="btn btn-outline-secondary btn-lg">Zurück</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}