Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94fc01c08a | |||
| 788ec1db62 | |||
| 725584a104 | |||
| fbcc3b4522 | |||
| adc2bad3fb | |||
| f7be7d9bf0 |
+1
-1
@@ -1 +1 @@
|
||||
v0.8.12
|
||||
v0.8.15
|
||||
|
||||
+101
-5
@@ -84,7 +84,7 @@ from Web.modules.terminplaner.blueprint import appoint_bp as terminplaner_bp
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
import Web.modules.database.settings as cfg
|
||||
from Web.modules.database.settings import MongoClient
|
||||
from tenant import get_tenant_context, get_tenant_trial_status, purge_expired_trial_tenants
|
||||
from tenant import get_tenant_context, get_tenant_db, get_tenant_trial_status, purge_expired_trial_tenants
|
||||
|
||||
|
||||
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
||||
@@ -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'}:
|
||||
@@ -4799,6 +4803,98 @@ def get_bookings():
|
||||
client.close()
|
||||
|
||||
|
||||
@app.route('/get_user_appointments')
|
||||
def get_user_appointments():
|
||||
"""Return the current user's planned and active appointments for the calendar."""
|
||||
if 'username' not in session:
|
||||
return jsonify({'ok': False, 'error': 'unauthorized'}), 401
|
||||
|
||||
client = None
|
||||
try:
|
||||
username = session.get('username')
|
||||
start = request.args.get('start')
|
||||
end = request.args.get('end')
|
||||
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = get_tenant_db(client)
|
||||
ausleihungen = db['ausleihungen']
|
||||
items_col = db['items']
|
||||
|
||||
query = {
|
||||
'User': username,
|
||||
'Status': {'$in': ['planned', 'active', 'completed']},
|
||||
}
|
||||
|
||||
if start and end:
|
||||
try:
|
||||
start_dt = datetime.datetime.fromisoformat(start.replace('Z', '+00:00'))
|
||||
end_dt = datetime.datetime.fromisoformat(end.replace('Z', '+00:00'))
|
||||
if start_dt.tzinfo is not None:
|
||||
start_dt = start_dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
|
||||
if end_dt.tzinfo is not None:
|
||||
end_dt = end_dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
|
||||
|
||||
query['$or'] = [
|
||||
{'Start': {'$lt': end_dt}, 'End': {'$gt': start_dt}},
|
||||
{'Start': {'$lt': end_dt}, 'End': {'$exists': False}},
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
bookings = list(ausleihungen.find(query).sort('Start', 1))
|
||||
|
||||
result = []
|
||||
for booking in bookings:
|
||||
start_dt = booking.get('Start')
|
||||
if not start_dt:
|
||||
continue
|
||||
|
||||
end_dt = booking.get('End')
|
||||
if not end_dt and isinstance(start_dt, datetime.datetime):
|
||||
end_dt = start_dt + datetime.timedelta(minutes=45)
|
||||
elif not end_dt:
|
||||
end_dt = start_dt
|
||||
|
||||
item_id = str(booking.get('Item') or '')
|
||||
item_doc = None
|
||||
if item_id:
|
||||
try:
|
||||
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
|
||||
except Exception:
|
||||
item_doc = None
|
||||
|
||||
item_name = item_id or 'Termin'
|
||||
if item_doc:
|
||||
item_name = item_doc.get('Name') or item_doc.get('Code_4') or item_name
|
||||
|
||||
period = booking.get('Period')
|
||||
title = item_name
|
||||
if period:
|
||||
title = f"{title} - {period}. Std"
|
||||
|
||||
status = booking.get('VerifiedStatus') or booking.get('Status') or 'unknown'
|
||||
result.append({
|
||||
'id': str(booking.get('_id')),
|
||||
'title': title,
|
||||
'start': start_dt.isoformat() if isinstance(start_dt, datetime.datetime) else str(start_dt),
|
||||
'end': end_dt.isoformat() if isinstance(end_dt, datetime.datetime) else str(end_dt),
|
||||
'status': status,
|
||||
'itemId': item_id,
|
||||
'userName': str(booking.get('User') or ''),
|
||||
'notes': str(booking.get('Notes') or ''),
|
||||
'period': period,
|
||||
'isCurrentUser': True,
|
||||
'itemBorrower': '',
|
||||
})
|
||||
|
||||
return jsonify({'ok': True, 'bookings': result})
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': str(e), 'bookings': []}), 500
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
@app.route('/api/booking_conflicts')
|
||||
def api_booking_conflicts():
|
||||
"""
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 %}
|
||||
@@ -13,7 +13,7 @@
|
||||
{% block content %}
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">
|
||||
<h1>Schulstunden-Terminplan für Ausleihen</h1>
|
||||
<h1>Schulstunden-Terminplan für Termine</h1>
|
||||
<div class="calendar-actions">
|
||||
<button id="prev-day">Vorheriger Tag</button>
|
||||
<span id="current-day-display"></span>
|
||||
@@ -27,21 +27,51 @@
|
||||
<button id="new-booking" class="primary-button">Neue Reservierung</button>
|
||||
</div>
|
||||
<div class="calendar-legend">
|
||||
<span class="legend-item"><span class="legend-color current"></span> Aktuelle Ausleihungen</span>
|
||||
<span class="legend-item"><span class="legend-color planned"></span> Geplante Ausleihungen</span>
|
||||
<span class="legend-item"><span class="legend-color completed"></span> Abgeschlossene Ausleihungen</span>
|
||||
<span class="legend-item"><span class="legend-color your-bookings"></span> Ihre Ausleihungen</span>
|
||||
<span class="legend-item"><span class="legend-color current"></span> Aktuelle Termine</span>
|
||||
<span class="legend-item"><span class="legend-color planned"></span> Geplante Termine</span>
|
||||
<span class="legend-item"><span class="legend-color completed"></span> Abgeschlossene Termine</span>
|
||||
<span class="legend-item"><span class="legend-color your-bookings"></span> Ihre Termine</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-options">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" id="show-completed-bookings">
|
||||
Abgeschlossene Ausleihungen anzeigen
|
||||
Abgeschlossene Termine anzeigen
|
||||
</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();
|
||||
@@ -291,7 +326,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const endStr = info.endStr;
|
||||
|
||||
// Load events from the server
|
||||
fetch('/get_bookings?start=' + startStr + '&end=' + endStr)
|
||||
fetch('/get_user_appointments?start=' + startStr + '&end=' + endStr)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user