feat: Implement upcoming appointments feature for users
- Added `remove_done` function to clean up finished appointments in the database. - Introduced `get_upcoming_for_user` function to retrieve upcoming appointments for a specific user. - Updated `backend_server.py` to include a new endpoint for fetching user-specific upcoming events. - Modified `blueprint.py` to render upcoming events in the user interface. - Enhanced `terminplan.html` to display a list of upcoming appointments with relevant details and action links. - Updated configuration to enable the new appointment planning feature.
This commit is contained in:
+10
-26
@@ -4812,36 +4812,18 @@ def get_user_appointments():
|
|||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
username = session.get('username')
|
username = session.get('username')
|
||||||
start = request.args.get('start')
|
|
||||||
end = request.args.get('end')
|
|
||||||
|
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
db = get_tenant_db(client)
|
db = client[MONGODB_DB]
|
||||||
ausleihungen = db['ausleihungen']
|
|
||||||
items_col = db['items']
|
items_col = db['items']
|
||||||
|
|
||||||
query = {
|
# Use the established user-specific route logic for appointments.
|
||||||
'User': username,
|
bookings = au.get_ausleihung_by_user(
|
||||||
'Status': {'$in': ['planned', 'active', 'completed']},
|
username,
|
||||||
}
|
status=['planned', 'active', 'completed'],
|
||||||
|
use_client_side_verification=True,
|
||||||
if start and end:
|
)
|
||||||
try:
|
bookings = sorted(bookings, key=lambda b: b.get('Start') or datetime.datetime.min)
|
||||||
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 = []
|
result = []
|
||||||
for booking in bookings:
|
for booking in bookings:
|
||||||
@@ -4873,6 +4855,8 @@ def get_user_appointments():
|
|||||||
title = f"{title} - {period}. Std"
|
title = f"{title} - {period}. Std"
|
||||||
|
|
||||||
status = booking.get('VerifiedStatus') or booking.get('Status') or 'unknown'
|
status = booking.get('VerifiedStatus') or booking.get('Status') or 'unknown'
|
||||||
|
if status == 'active':
|
||||||
|
status = 'current'
|
||||||
result.append({
|
result.append({
|
||||||
'id': str(booking.get('_id')),
|
'id': str(booking.get('_id')),
|
||||||
'title': title,
|
'title': title,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def update(id,slots_used: list):
|
|||||||
items = db['appointments']
|
items = db['appointments']
|
||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
'slots_booked': [slots_used],
|
'slots_booked': slots_used,
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,4 +171,62 @@ def remove(id):
|
|||||||
print(f"Error removing appointment: {e}")
|
print(f"Error removing appointment: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def remove_done():
|
||||||
|
"""removose already finisched appointments"""
|
||||||
|
try:
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = _get_tenant_db(client)
|
||||||
|
items = db['appointments']
|
||||||
|
|
||||||
|
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
cursor = items.find(
|
||||||
|
_active_record_query(
|
||||||
|
{
|
||||||
|
'date_end': {'$lt': today},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).sort('date_start', 1)
|
||||||
|
|
||||||
|
for item in cursor:
|
||||||
|
item['_id'] = str(item.get('_id'))
|
||||||
|
result = items.delete_one({'_id': ObjectId(item['_id'])})
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
return result.modified_count > 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing appointment: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_upcoming_for_user(user: str, limit: int = 25):
|
||||||
|
"""Return upcoming appointment plans for a user ordered by start date."""
|
||||||
|
remove_done()
|
||||||
|
try:
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = _get_tenant_db(client)
|
||||||
|
items = db['appointments']
|
||||||
|
|
||||||
|
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||||
|
cursor = items.find(
|
||||||
|
_active_record_query(
|
||||||
|
{
|
||||||
|
'user': str(user or '').strip(),
|
||||||
|
'date_end': {'$gte': today},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).sort('date_start', 1)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for item in cursor:
|
||||||
|
item['_id'] = str(item.get('_id'))
|
||||||
|
results.append(item)
|
||||||
|
if len(results) >= max(1, int(limit)):
|
||||||
|
break
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error retrieving upcoming appointments: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
@@ -3,13 +3,27 @@ Class for all funktions of the executive -> Lehrer
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from flask import url_for
|
from flask import url_for, has_request_context, request
|
||||||
import Web.modules.emailservice.email as mail_service
|
import Web.modules.emailservice.email as mail_service
|
||||||
import Web.modules.database.termine as termin
|
import Web.modules.database.termine as termin
|
||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
from Web.tenant import get_tenant_context
|
from Web.tenant import get_tenant_context
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_public_base_url() -> str:
|
||||||
|
if has_request_context():
|
||||||
|
try:
|
||||||
|
return request.url_root.rstrip('/')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tenant_context = get_tenant_context()
|
||||||
|
subdomain = ''
|
||||||
|
if tenant_context:
|
||||||
|
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
||||||
|
return f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
|
||||||
|
|
||||||
|
|
||||||
def _normalize_time_span(time_span):
|
def _normalize_time_span(time_span):
|
||||||
if isinstance(time_span, list):
|
if isinstance(time_span, list):
|
||||||
return [str(entry).strip() for entry in time_span if str(entry).strip()]
|
return [str(entry).strip() for entry in time_span if str(entry).strip()]
|
||||||
@@ -59,11 +73,7 @@ def build_calendar_ics(appointment_id: str) -> str | None:
|
|||||||
try:
|
try:
|
||||||
link = url_for('terminplaner.client', appointment_id=str(appointment_id), _external=True)
|
link = url_for('terminplaner.client', appointment_id=str(appointment_id), _external=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
tenant_context = get_tenant_context()
|
host = _resolve_public_base_url()
|
||||||
subdomain = ''
|
|
||||||
if tenant_context:
|
|
||||||
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
|
||||||
host = f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
|
|
||||||
link = host + "/terminplaner/client/" + str(appointment_id)
|
link = host + "/terminplaner/client/" + str(appointment_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -124,15 +134,10 @@ def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
|
|||||||
id = termin.add(date_start, date_end, normalized_time_span, slots, slot_lenght, user, normalized_mail, note, calendar_enabled=calendar_enabled)
|
id = termin.add(date_start, date_end, normalized_time_span, slots, slot_lenght, user, normalized_mail, note, calendar_enabled=calendar_enabled)
|
||||||
id_str = str(id)
|
id_str = str(id)
|
||||||
|
|
||||||
tenant_context = get_tenant_context()
|
|
||||||
subdomain = ''
|
|
||||||
if tenant_context:
|
|
||||||
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
link = url_for('terminplaner.client', appointment_id=id_str, _external=True)
|
link = url_for('terminplaner.client', appointment_id=id_str, _external=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
host = f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
|
host = _resolve_public_base_url()
|
||||||
link = host + "/terminplaner/client/" + id_str
|
link = host + "/terminplaner/client/" + id_str
|
||||||
subject = f"Terminanfrage von {user}"
|
subject = f"Terminanfrage von {user}"
|
||||||
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
|
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
|
||||||
@@ -141,11 +146,7 @@ def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
|
|||||||
try:
|
try:
|
||||||
calendar_link = url_for('terminplaner.calendar_export', appointment_id=id_str, _external=True)
|
calendar_link = url_for('terminplaner.calendar_export', appointment_id=id_str, _external=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
tenant_context = get_tenant_context()
|
host = _resolve_public_base_url()
|
||||||
subdomain = ''
|
|
||||||
if tenant_context:
|
|
||||||
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
|
||||||
host = f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
|
|
||||||
calendar_link = host + "/terminplaner/calendar/" + id_str + ".ics"
|
calendar_link = host + "/terminplaner/calendar/" + id_str + ".ics"
|
||||||
|
|
||||||
email_body = note_link
|
email_body = note_link
|
||||||
@@ -312,4 +313,52 @@ def get_available_user(id):
|
|||||||
- dict: all the needet information -> [Start_date, End_date, (first day Time Frame,
|
- dict: all the needet information -> [Start_date, End_date, (first day Time Frame,
|
||||||
second day Time frame, third etc.), slot lenght, (bookedslots -> list)]
|
second day Time frame, third etc.), slot lenght, (bookedslots -> list)]
|
||||||
"""
|
"""
|
||||||
return get_available(id)
|
return get_available(id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
|
||||||
|
"""Return upcoming appointment plans for overview display."""
|
||||||
|
user_name = str(user or '').strip()
|
||||||
|
if not user_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
appointments = termin.get_upcoming_for_user(user_name, limit=limit)
|
||||||
|
host = _resolve_public_base_url()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for item in appointments:
|
||||||
|
appointment_id = str(item.get('_id') or '')
|
||||||
|
if not appointment_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
link = url_for('terminplaner.client', appointment_id=appointment_id, _external=True)
|
||||||
|
except Exception:
|
||||||
|
link = host + '/terminplaner/client/' + appointment_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
calendar_link = url_for('terminplaner.calendar_export', appointment_id=appointment_id, _external=True)
|
||||||
|
except Exception:
|
||||||
|
calendar_link = host + '/terminplaner/calendar/' + appointment_id + '.ics'
|
||||||
|
|
||||||
|
slots_total = int(item.get('slots', 0) or 0)
|
||||||
|
slots_booked = item.get('slots_booked', []) or []
|
||||||
|
if not isinstance(slots_booked, list):
|
||||||
|
slots_booked = []
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
'appointment_id': appointment_id,
|
||||||
|
'date_start': str(item.get('date_start') or ''),
|
||||||
|
'date_end': str(item.get('date_end') or ''),
|
||||||
|
'time_span': item.get('time_span', []) or [],
|
||||||
|
'slots_total': slots_total,
|
||||||
|
'slots_booked': len(slots_booked),
|
||||||
|
'slots_left': max(0, slots_total - len(slots_booked)),
|
||||||
|
'note': str(item.get('note') or ''),
|
||||||
|
'link': link,
|
||||||
|
'calendar_link': calendar_link if item.get('calendar_enabled') else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -132,8 +132,12 @@ def main():
|
|||||||
if guard:
|
if guard:
|
||||||
return guard
|
return guard
|
||||||
|
|
||||||
|
current_user = session.get('username', '')
|
||||||
|
upcoming_events = appointment_service.get_user_upcoming_events(current_user) if current_user else []
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'terminplaner.html',
|
'terminplaner.html',
|
||||||
school_periods=cfg.SCHOOL_PERIODS,
|
school_periods=cfg.SCHOOL_PERIODS,
|
||||||
current_user=session.get('username', ''),
|
current_user=current_user,
|
||||||
|
upcoming_events=upcoming_events,
|
||||||
)
|
)
|
||||||
+15
-783
@@ -24,7 +24,7 @@
|
|||||||
<button type="button" class="calendar-view-btn" data-calendar-view="timeGridWeek">Woche</button>
|
<button type="button" class="calendar-view-btn" data-calendar-view="timeGridWeek">Woche</button>
|
||||||
<button type="button" class="calendar-view-btn" data-calendar-view="dayGridMonth">Monat</button>
|
<button type="button" class="calendar-view-btn" data-calendar-view="dayGridMonth">Monat</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="new-booking" class="primary-button">Neue Reservierung</button>
|
<button id="new-booking" class="primary-button">Zur Termin-Konfiguration</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="calendar-legend">
|
<div class="calendar-legend">
|
||||||
<span class="legend-item"><span class="legend-color current"></span> Aktuelle Termine</span>
|
<span class="legend-item"><span class="legend-color current"></span> Aktuelle Termine</span>
|
||||||
@@ -73,128 +73,16 @@
|
|||||||
<div id="calendar"></div>
|
<div id="calendar"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal for new bookings -->
|
|
||||||
<div id="booking-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close">×</span>
|
|
||||||
<h2>Neue Reservierung</h2>
|
|
||||||
<form id="booking-form">
|
|
||||||
<input type="hidden" name="user_id" id="user-id" value="{{ current_user }}">
|
|
||||||
|
|
||||||
<!-- Booking Type -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="booking-type">Reservierungstyp:</label>
|
|
||||||
<select id="booking-type" name="booking_type">
|
|
||||||
<option value="single">Einzeltermin</option>
|
|
||||||
<option value="range">Zeitraum</option>
|
|
||||||
<option value="recurring">Wiederkehrend</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Start Date -->
|
|
||||||
<div class="form-group" id="start-date-group">
|
|
||||||
<label for="booking-date" id="booking-date-label">Datum:</label>
|
|
||||||
<input type="date" id="booking-date" name="booking_date" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- End Date (for range) -->
|
|
||||||
<div class="form-group" id="end-date-group" style="display:none;">
|
|
||||||
<label for="booking-end-date">Bis:</label>
|
|
||||||
<input type="date" id="booking-end-date" name="booking_end_date">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Period Range Selection -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="period-start-select">Von Schulstunde:</label>
|
|
||||||
<select id="period-start-select" name="period_start" required>
|
|
||||||
<option value="">-- Bitte wählen --</option>
|
|
||||||
{% for period, details in school_periods.items() %}
|
|
||||||
<option value="{{ period }}">{{ details.label }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="period-end-select">Bis Schulstunde:</label>
|
|
||||||
<select id="period-end-select" name="period_end" required>
|
|
||||||
<option value="">-- Bitte wählen --</option>
|
|
||||||
{% for period, details in school_periods.items() %}
|
|
||||||
<option value="{{ period }}">{{ details.label }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Recurring Options -->
|
|
||||||
<div id="recurring-options" style="display:none;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="recurrence-pattern">Wiederholungsmuster:</label>
|
|
||||||
<select id="recurrence-pattern" name="recurrence_pattern">
|
|
||||||
<option value="daily">Täglich</option>
|
|
||||||
<option value="weekly" selected>Wöchentlich</option>
|
|
||||||
<option value="monthly">Monatlich</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="recurrence-end-date">Wiederholung bis:</label>
|
|
||||||
<input type="date" id="recurrence-end-date" name="recurrence_end_date">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" id="weekly-options">
|
|
||||||
<label>Wochentage:</label>
|
|
||||||
<div class="weekday-selector">
|
|
||||||
<label><input type="checkbox" name="weekdays" value="1"> Mo</label>
|
|
||||||
<label><input type="checkbox" name="weekdays" value="2"> Di</label>
|
|
||||||
<label><input type="checkbox" name="weekdays" value="3"> Mi</label>
|
|
||||||
<label><input type="checkbox" name="weekdays" value="4"> Do</label>
|
|
||||||
<label><input type="checkbox" name="weekdays" value="5"> Fr</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" id="monthly-options" style="display:none;">
|
|
||||||
<label>Tag des Monats:</label>
|
|
||||||
<select id="day-of-month" name="day_of_month">
|
|
||||||
<option value="same_day">Gleicher Tag</option>
|
|
||||||
<option value="first_weekday">Erster gleicher Wochentag</option>
|
|
||||||
<option value="last_weekday">Letzter gleicher Wochentag</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Item Selection -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="item-select">Objekt:</label>
|
|
||||||
<select id="item-select" name="item_id" required>
|
|
||||||
<option value="">-- Bitte wählen --</option>
|
|
||||||
<!-- Items will be loaded dynamically -->
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notes -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="booking-notes">Notizen:</label>
|
|
||||||
<textarea id="booking-notes" name="notes"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Booking Summary -->
|
|
||||||
<div class="booking-summary" id="booking-summary" style="display:none;">
|
|
||||||
<!-- Will be populated dynamically -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="primary-button">Ausleihung speichern</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal for event details -->
|
<!-- Modal for event details -->
|
||||||
<div id="event-modal" class="modal">
|
<div id="event-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span class="close">×</span>
|
<span class="close">×</span>
|
||||||
<h2>Ausleihungs Details</h2>
|
<h2>Termin-Details</h2>
|
||||||
<div id="event-details">
|
<div id="event-details">
|
||||||
<!-- Event details will be loaded dynamically -->
|
<!-- Event details will be loaded dynamically -->
|
||||||
</div>
|
</div>
|
||||||
<div id="event-actions">
|
<div id="event-actions">
|
||||||
<button id="cancel-booking" class="danger-button">Ausleihung stornieren</button>
|
<button id="cancel-booking" class="danger-button">Termin stornieren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +98,6 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize variables
|
// Initialize variables
|
||||||
const calendarEl = document.getElementById('calendar');
|
const calendarEl = document.getElementById('calendar');
|
||||||
const bookingModal = document.getElementById('booking-modal');
|
|
||||||
const eventModal = document.getElementById('event-modal');
|
const eventModal = document.getElementById('event-modal');
|
||||||
const newBookingBtn = document.getElementById('new-booking');
|
const newBookingBtn = document.getElementById('new-booking');
|
||||||
let currentEventId = null;
|
let currentEventId = null;
|
||||||
@@ -452,7 +339,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const borrowerIndicator = document.createElement('div');
|
const borrowerIndicator = document.createElement('div');
|
||||||
borrowerIndicator.className = 'borrowed-indicator';
|
borrowerIndicator.className = 'borrowed-indicator';
|
||||||
borrowerIndicator.innerHTML = '⚠️'; // Warning symbol
|
borrowerIndicator.innerHTML = '⚠️'; // Warning symbol
|
||||||
borrowerIndicator.title = `Bereits ausgeliehen von: ${info.event.extendedProps.itemBorrower}`;
|
borrowerIndicator.title = `Bereits verknüpft mit: ${info.event.extendedProps.itemBorrower}`;
|
||||||
borrowerIndicator.style.position = 'absolute';
|
borrowerIndicator.style.position = 'absolute';
|
||||||
borrowerIndicator.style.top = '2px';
|
borrowerIndicator.style.top = '2px';
|
||||||
borrowerIndicator.style.left = '2px';
|
borrowerIndicator.style.left = '2px';
|
||||||
@@ -526,53 +413,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.getElementById('current-day-display').textContent = dateStr;
|
document.getElementById('current-day-display').textContent = dateStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load available items for the booking form
|
|
||||||
function loadAvailableItems() {
|
|
||||||
const itemSelect = document.getElementById('item-select');
|
|
||||||
itemSelect.innerHTML = '<option value="">-- Bitte wählen --</option>';
|
|
||||||
|
|
||||||
fetch('/get_items?available_only=true')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
// Access the items array in the response
|
|
||||||
const items = data.items || [];
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
const itemId = item._id.$oid || item._id || item.id;
|
|
||||||
option.value = itemId;
|
|
||||||
option.textContent = item.Name;
|
|
||||||
itemSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
itemSelect.innerHTML += '<option value="" disabled>Keine Objekte gefunden</option>';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error loading items:', error));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open booking modal
|
|
||||||
function openBookingModal(date = null) {
|
|
||||||
// Set date to today if not provided
|
|
||||||
if (!date) {
|
|
||||||
date = calendar.getDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format the date for the date picker
|
|
||||||
const formatDate = (date) => {
|
|
||||||
return date.toISOString().split('T')[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('booking-date').value = formatDate(date);
|
|
||||||
|
|
||||||
// Load available items
|
|
||||||
loadAvailableItems();
|
|
||||||
|
|
||||||
// Show modal
|
|
||||||
bookingModal.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show event details
|
// Show event details
|
||||||
function showEventDetails(event) {
|
function showEventDetails(event) {
|
||||||
const details = document.getElementById('event-details');
|
const details = document.getElementById('event-details');
|
||||||
@@ -584,7 +424,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Get status text based on status value
|
// Get status text based on status value
|
||||||
let statusText = 'Unbekannt';
|
let statusText = 'Unbekannt';
|
||||||
if (event.extendedProps.status === 'current') {
|
if (event.extendedProps.status === 'current') {
|
||||||
statusText = 'Aktuell ausgeliehen';
|
statusText = 'Aktiv';
|
||||||
} else if (event.extendedProps.status === 'planned') {
|
} else if (event.extendedProps.status === 'planned') {
|
||||||
statusText = 'Geplant';
|
statusText = 'Geplant';
|
||||||
} else if (event.extendedProps.status === 'completed') {
|
} else if (event.extendedProps.status === 'completed') {
|
||||||
@@ -603,17 +443,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Check if item is already borrowed by someone else
|
// Check if item is already borrowed by someone else
|
||||||
let borrowerInfo = '';
|
let borrowerInfo = '';
|
||||||
if (event.extendedProps.itemBorrower && event.extendedProps.itemBorrower !== event.extendedProps.userName) {
|
if (event.extendedProps.itemBorrower && event.extendedProps.itemBorrower !== event.extendedProps.userName) {
|
||||||
borrowerInfo = `<p class="warning"><strong>Hinweis:</strong> Dieses Objekt ist aktuell ausgeliehen von <strong>${event.extendedProps.itemBorrower}</strong></p>`;
|
borrowerInfo = `<p class="warning"><strong>Hinweis:</strong> Dieser Termin ist aktuell bereits mit <strong>${event.extendedProps.itemBorrower}</strong> verknüpft.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate details
|
// Populate details
|
||||||
details.innerHTML = `
|
details.innerHTML = `
|
||||||
<p><strong>Objekt:</strong> ${event.title}</p>
|
<p><strong>Termin:</strong> ${event.title}</p>
|
||||||
<p><strong>Datum:</strong> ${new Date(event.start).toLocaleDateString('de-DE')}</p>
|
<p><strong>Datum:</strong> ${new Date(event.start).toLocaleDateString('de-DE')}</p>
|
||||||
<p><strong>Von:</strong> ${new Date(event.start).toLocaleTimeString('de-DE', {hour: '2-digit', minute:'2-digit'})}</p>
|
<p><strong>Von:</strong> ${new Date(event.start).toLocaleTimeString('de-DE', {hour: '2-digit', minute:'2-digit'})}</p>
|
||||||
<p><strong>Bis:</strong> ${new Date(event.end).toLocaleTimeString('de-DE', {hour: '2-digit', minute:'2-digit'})}</p>
|
<p><strong>Bis:</strong> ${new Date(event.end).toLocaleTimeString('de-DE', {hour: '2-digit', minute:'2-digit'})}</p>
|
||||||
${periodText}
|
${periodText}
|
||||||
<p><strong>Ausgeliehen von:</strong> ${event.extendedProps.userName}</p>
|
<p><strong>Erstellt von:</strong> ${event.extendedProps.userName}</p>
|
||||||
${event.extendedProps.notes ? `<p><strong>Notizen:</strong> ${event.extendedProps.notes}</p>` : ''}
|
${event.extendedProps.notes ? `<p><strong>Notizen:</strong> ${event.extendedProps.notes}</p>` : ''}
|
||||||
<p><strong>Status:</strong> ${statusText}</p>
|
<p><strong>Status:</strong> ${statusText}</p>
|
||||||
${borrowerInfo}
|
${borrowerInfo}
|
||||||
@@ -631,149 +471,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
eventModal.style.display = 'block';
|
eventModal.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// New booking button
|
// Route users to the dedicated configuration flow instead of creating bookings here.
|
||||||
newBookingBtn.addEventListener('click', function() {
|
newBookingBtn.addEventListener('click', function() {
|
||||||
openBookingModal();
|
window.location.href = '/terminplaner/configure';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modals when clicking X
|
// Close event details modal when clicking X
|
||||||
document.querySelectorAll('.close').forEach(closeBtn => {
|
document.querySelectorAll('#event-modal .close').forEach(closeBtn => {
|
||||||
closeBtn.addEventListener('click', function() {
|
closeBtn.addEventListener('click', function() {
|
||||||
bookingModal.style.display = 'none';
|
|
||||||
eventModal.style.display = 'none';
|
eventModal.style.display = 'none';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modals when clicking outside
|
// Close event details modal when clicking outside
|
||||||
window.addEventListener('click', function(event) {
|
window.addEventListener('click', function(event) {
|
||||||
if (event.target === bookingModal) {
|
|
||||||
bookingModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
if (event.target === eventModal) {
|
if (event.target === eventModal) {
|
||||||
eventModal.style.display = 'none';
|
eventModal.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find the form submission handler (around line 562) and enhance the debug information
|
|
||||||
|
|
||||||
document.getElementById('booking-form').addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Validate form fields
|
|
||||||
const itemId = document.getElementById('item-select').value;
|
|
||||||
const periodStart = document.getElementById('period-start-select').value;
|
|
||||||
const periodEnd = document.getElementById('period-end-select').value;
|
|
||||||
const startDate = document.getElementById('booking-date').value;
|
|
||||||
|
|
||||||
console.log("DEBUG - Form submission - Required fields:", {
|
|
||||||
itemId,
|
|
||||||
periodStart,
|
|
||||||
periodEnd,
|
|
||||||
startDate,
|
|
||||||
"itemId exists": Boolean(itemId),
|
|
||||||
"periodStart exists": Boolean(periodStart),
|
|
||||||
"periodEnd exists": Boolean(periodEnd),
|
|
||||||
"startDate exists": Boolean(startDate)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check required fields
|
|
||||||
if (!itemId || !periodStart || !periodEnd || !startDate) {
|
|
||||||
alert('Bitte füllen Sie alle erforderlichen Felder aus.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate period range
|
|
||||||
if (parseInt(periodStart) > parseInt(periodEnd)) {
|
|
||||||
alert('Die Start-Schulstunde darf nicht nach der End-Schulstunde liegen.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = document.getElementById('user-id').value || 'Admin'; // Get user ID or use fallback
|
|
||||||
const bookingType = document.getElementById('booking-type').value;
|
|
||||||
|
|
||||||
// Convert JSON data to FormData
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
// Add all required fields with correct names the server expects
|
|
||||||
formData.append('item_id', itemId);
|
|
||||||
formData.append('booking_date', startDate);
|
|
||||||
formData.append('period_start', periodStart);
|
|
||||||
formData.append('period_end', periodEnd);
|
|
||||||
formData.append('notes', document.getElementById('booking-notes').value || '');
|
|
||||||
formData.append('user_id', userId);
|
|
||||||
|
|
||||||
if (bookingType === 'range') {
|
|
||||||
const endDate = document.getElementById('booking-end-date').value;
|
|
||||||
if (!endDate) {
|
|
||||||
alert('Bitte wählen Sie ein Enddatum für den Zeitraum.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formData.append('booking_end_date', endDate);
|
|
||||||
formData.append('booking_type', 'range');
|
|
||||||
} else if (bookingType === 'recurring') {
|
|
||||||
formData.append('booking_type', 'single'); // Server expects 'single' structure for these individually sent requests
|
|
||||||
const dates = calculateDates();
|
|
||||||
if (dates.length === 0) {
|
|
||||||
alert('Es konnten keine Termine berechnet werden. Bitte überprüfen Sie Ihre Eingaben.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (confirm(`Möchten Sie wirklich ${dates.length} Termine planen?`)) {
|
|
||||||
processMultipleDates(formData, dates, periodStart, periodEnd);
|
|
||||||
}
|
|
||||||
return; // Use processMultipleDates instead
|
|
||||||
} else {
|
|
||||||
formData.append('booking_end_date', startDate); // For single bookings, end = start
|
|
||||||
formData.append('booking_type', 'single');
|
|
||||||
}
|
|
||||||
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
|
|
||||||
|
|
||||||
// Submit with FormData instead of JSON
|
|
||||||
fetch('/plan_booking', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken
|
|
||||||
},
|
|
||||||
body: formData // Remove the Content-Type header and JSON.stringify
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return response.json().then(data => {
|
|
||||||
console.error("DEBUG - Error response data:", data);
|
|
||||||
|
|
||||||
// Show detailed error information
|
|
||||||
let errorMsg = data.error || `Server returned ${response.status}: ${response.statusText}`;
|
|
||||||
|
|
||||||
if (data.missing_fields) {
|
|
||||||
errorMsg += "\nMissing fields: " + data.missing_fields.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(errorMsg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
bookingModal.style.display = 'none';
|
|
||||||
alert('Ausleihe erfolgreich geplant!');
|
|
||||||
calendar.refetchEvents();
|
|
||||||
} else {
|
|
||||||
const errorMsg = data.errors ? data.errors.join(', ') : (data.error || 'Unbekannter Fehler');
|
|
||||||
alert('Fehler: ' + errorMsg);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('DEBUG - Error submitting booking:', error);
|
|
||||||
alert('Ein Fehler ist aufgetreten: ' + error.message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel booking
|
// Cancel booking
|
||||||
document.getElementById('cancel-booking').addEventListener('click', function() {
|
document.getElementById('cancel-booking').addEventListener('click', function() {
|
||||||
if (confirm('Möchten Sie diese Ausleihe wirklich stornieren?')) {
|
if (confirm('Möchten Sie diesen Termin wirklich stornieren?')) {
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
|
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
|
||||||
fetch('/cancel_booking/' + currentEventId, {
|
fetch('/cancel_booking/' + currentEventId, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -786,7 +504,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
eventModal.style.display = 'none';
|
eventModal.style.display = 'none';
|
||||||
calendar.refetchEvents();
|
calendar.refetchEvents();
|
||||||
alert('Ausleihe erfolgreich storniert!');
|
alert('Termin erfolgreich storniert!');
|
||||||
} else {
|
} else {
|
||||||
alert('Fehler: ' + data.error);
|
alert('Fehler: ' + data.error);
|
||||||
}
|
}
|
||||||
@@ -802,492 +520,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
showCompletedBookings = this.checked;
|
showCompletedBookings = this.checked;
|
||||||
calendar.refetchEvents();
|
calendar.refetchEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper functions for dates
|
|
||||||
function formatDate(date) {
|
|
||||||
if (!date) return '';
|
|
||||||
return date.toISOString().split('T')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup booking form controls
|
|
||||||
const bookingTypeSelect = document.getElementById('booking-type');
|
|
||||||
const startDateGroup = document.getElementById('start-date-group');
|
|
||||||
const endDateGroup = document.getElementById('end-date-group');
|
|
||||||
const recurringOptions = document.getElementById('recurring-options');
|
|
||||||
const bookingDateLabel = document.getElementById('booking-date-label');
|
|
||||||
const recurrencePatternSelect = document.getElementById('recurrence-pattern');
|
|
||||||
const weeklyOptions = document.getElementById('weekly-options');
|
|
||||||
const monthlyOptions = document.getElementById('monthly-options');
|
|
||||||
const bookingSummary = document.getElementById('booking-summary');
|
|
||||||
|
|
||||||
// Add this code after the DOMContentLoaded event initialization
|
|
||||||
// Setup booking type toggle
|
|
||||||
bookingTypeSelect.addEventListener('change', function() {
|
|
||||||
const value = this.value;
|
|
||||||
|
|
||||||
// Reset all form visibility
|
|
||||||
endDateGroup.style.display = 'none';
|
|
||||||
recurringOptions.style.display = 'none';
|
|
||||||
bookingDateLabel.textContent = 'Datum:';
|
|
||||||
|
|
||||||
if (value === 'range') {
|
|
||||||
endDateGroup.style.display = 'block';
|
|
||||||
bookingDateLabel.textContent = 'Von:';
|
|
||||||
} else if (value === 'recurring') {
|
|
||||||
recurringOptions.style.display = 'block';
|
|
||||||
bookingDateLabel.textContent = 'Start:';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBookingSummary();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup recurrence pattern toggle
|
|
||||||
recurrencePatternSelect.addEventListener('change', function() {
|
|
||||||
const value = this.value;
|
|
||||||
|
|
||||||
// Reset visibility
|
|
||||||
weeklyOptions.style.display = value === 'weekly' ? 'block' : 'none';
|
|
||||||
monthlyOptions.style.display = value === 'monthly' ? 'block' : 'none';
|
|
||||||
|
|
||||||
updateBookingSummary();
|
|
||||||
updateWeekdaySelection();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate period range
|
|
||||||
function validatePeriodRange() {
|
|
||||||
const periodStart = parseInt(document.getElementById('period-start-select').value);
|
|
||||||
const periodEnd = parseInt(document.getElementById('period-end-select').value);
|
|
||||||
|
|
||||||
if (periodStart && periodEnd && periodStart > periodEnd) {
|
|
||||||
alert('Die Start-Schulstunde darf nicht nach der End-Schulstunde liegen.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update booking summary to include period range
|
|
||||||
function updateBookingSummary() {
|
|
||||||
const bookingType = bookingTypeSelect.value;
|
|
||||||
const startDate = new Date(document.getElementById('booking-date').value);
|
|
||||||
|
|
||||||
// Get the period select elements directly
|
|
||||||
const periodStartSelect = document.getElementById('period-start-select');
|
|
||||||
const periodEndSelect = document.getElementById('period-end-select');
|
|
||||||
const periodStart = periodStartSelect ? periodStartSelect.value : "";
|
|
||||||
const periodEnd = periodEndSelect ? periodEndSelect.value : "";
|
|
||||||
|
|
||||||
// Get the text labels safely
|
|
||||||
let periodLabelStart = "Keine Schulstunde ausgewählt";
|
|
||||||
let periodLabelEnd = "Keine Schulstunde ausgewählt";
|
|
||||||
|
|
||||||
if (periodStartSelect && periodStartSelect.selectedIndex >= 0) {
|
|
||||||
periodLabelStart = periodStartSelect.options[periodStartSelect.selectedIndex].text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (periodEndSelect && periodEndSelect.selectedIndex >= 0) {
|
|
||||||
periodLabelEnd = periodEndSelect.options[periodEndSelect.selectedIndex].text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide summary for single bookings or if date is invalid
|
|
||||||
if (bookingType === 'single' || isNaN(startDate.getTime())) {
|
|
||||||
bookingSummary.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bookingSummary.style.display = 'block';
|
|
||||||
let summaryHTML = '<h4>Reservierungsübersicht:</h4>';
|
|
||||||
|
|
||||||
const formattedStartDate = startDate.toLocaleDateString('de-DE', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bookingType === 'range') {
|
|
||||||
const endDate = new Date(document.getElementById('booking-end-date').value);
|
|
||||||
|
|
||||||
if (isNaN(endDate.getTime())) {
|
|
||||||
summaryHTML += '<p>Bitte wählen Sie ein gültiges Enddatum.</p>';
|
|
||||||
} else {
|
|
||||||
const formattedEndDate = endDate.toLocaleDateString('de-DE', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
|
|
||||||
summaryHTML += `
|
|
||||||
<p>Zeitraum vom ${formattedStartDate} bis ${formattedEndDate}</p>
|
|
||||||
<p>Schulstunden: ${periodLabelStart} bis ${periodLabelEnd}</p>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
} else if (bookingType === 'recurring') {
|
|
||||||
const pattern = document.getElementById('recurrence-pattern').value;
|
|
||||||
const endDate = new Date(document.getElementById('recurrence-end-date').value);
|
|
||||||
|
|
||||||
if (isNaN(endDate.getTime())) {
|
|
||||||
summaryHTML += '<p>Bitte wählen Sie ein gültiges Enddatum für die Wiederholung.</p>';
|
|
||||||
bookingSummary.innerHTML = summaryHTML;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedEndDate = endDate.toLocaleDateString('de-DE', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
|
|
||||||
let patternText = '';
|
|
||||||
let selectedDates = [];
|
|
||||||
|
|
||||||
if (pattern === 'daily') {
|
|
||||||
patternText = 'Täglich';
|
|
||||||
selectedDates = calculateDailyDates(startDate, endDate, 5);
|
|
||||||
}
|
|
||||||
else if (pattern === 'weekly') {
|
|
||||||
const selectedWeekdays = [];
|
|
||||||
document.querySelectorAll('input[name="weekdays"]:checked').forEach(cb => {
|
|
||||||
selectedWeekdays.push(parseInt(cb.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedWeekdays.length === 0) {
|
|
||||||
summaryHTML += '<p>Bitte wählen Sie mindestens einen Wochentag aus.</p>';
|
|
||||||
bookingSummary.innerHTML = summaryHTML;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const weekdayNames = ['', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
|
|
||||||
patternText = 'Wöchentlich am ' + selectedWeekdays.map(day => weekdayNames[day]).join(', ');
|
|
||||||
|
|
||||||
selectedDates = calculateWeeklyDates(startDate, endDate, selectedWeekdays, 5);
|
|
||||||
}
|
|
||||||
else if (pattern === 'monthly') {
|
|
||||||
const monthOption = document.getElementById('day-of-month').value;
|
|
||||||
|
|
||||||
if (monthOption === 'same_day') {
|
|
||||||
patternText = `Monatlich am ${startDate.getDate()}. Tag des Monats`;
|
|
||||||
} else if (monthOption === 'first_weekday') {
|
|
||||||
patternText = `Monatlich am ersten ${startDate.toLocaleDateString('de-DE', {weekday: 'long'})} des Monats`;
|
|
||||||
} else if (monthOption === 'last_weekday') {
|
|
||||||
patternText = `Monatlich am letzten ${startDate.toLocaleDateString('de-DE', {weekday: 'long'})} des Monats`;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedDates = calculateMonthlyDates(startDate, endDate, monthOption, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryHTML += `
|
|
||||||
<p>${patternText} bis zum ${formattedEndDate}</p>
|
|
||||||
<p>Schulstunden: ${periodLabelStart} bis ${periodLabelEnd}</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (selectedDates.length > 0) {
|
|
||||||
summaryHTML += '<p>Beispieltermine:</p><ul>';
|
|
||||||
selectedDates.forEach(date => {
|
|
||||||
summaryHTML += `<li>${date.toLocaleDateString('de-DE', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
})}</li>`;
|
|
||||||
});
|
|
||||||
summaryHTML += '</ul>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bookingSummary.innerHTML = summaryHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date calculation functions
|
|
||||||
function calculateDailyDates(startDate, endDate, maxSamples) {
|
|
||||||
const dates = [];
|
|
||||||
let currentDate = new Date(startDate);
|
|
||||||
|
|
||||||
while (currentDate <= endDate && dates.length < maxSamples) {
|
|
||||||
dates.push(new Date(currentDate));
|
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dates;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateWeeklyDates(startDate, endDate, weekdays, maxSamples) {
|
|
||||||
const dates = [];
|
|
||||||
let currentDate = new Date(startDate);
|
|
||||||
|
|
||||||
// Sort weekdays numerically
|
|
||||||
weekdays.sort((a, b) => a - b);
|
|
||||||
|
|
||||||
// If the start date's weekday isn't in the selected weekdays,
|
|
||||||
// move to the next selected weekday
|
|
||||||
let startWeekday = currentDate.getDay();
|
|
||||||
startWeekday = startWeekday === 0 ? 7 : startWeekday;
|
|
||||||
|
|
||||||
if (!weekdays.includes(startWeekday)) {
|
|
||||||
// Find next weekday
|
|
||||||
let nextWeekday = weekdays.find(day => day > startWeekday);
|
|
||||||
|
|
||||||
if (!nextWeekday) {
|
|
||||||
// If no higher weekday, take the first one and add a week
|
|
||||||
nextWeekday = weekdays[0];
|
|
||||||
currentDate.setDate(currentDate.getDate() + (7 - startWeekday + nextWeekday));
|
|
||||||
} else {
|
|
||||||
// Move to the next higher weekday
|
|
||||||
currentDate.setDate(currentDate.getDate() + (nextWeekday - startWeekday));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (currentDate <= endDate && dates.length < maxSamples) {
|
|
||||||
dates.push(new Date(currentDate));
|
|
||||||
|
|
||||||
// Find the next occurrence
|
|
||||||
let currentWeekday = currentDate.getDay();
|
|
||||||
currentWeekday = currentWeekday === 0 ? 7 : currentWeekday;
|
|
||||||
|
|
||||||
let nextWeekday = weekdays.find(day => day > currentWeekday);
|
|
||||||
|
|
||||||
if (!nextWeekday) {
|
|
||||||
// If no higher weekday, take the first one and add a week
|
|
||||||
nextWeekday = weekdays[0];
|
|
||||||
currentDate.setDate(currentDate.getDate() + (7 - currentWeekday + nextWeekday));
|
|
||||||
} else {
|
|
||||||
// Move to the next higher weekday
|
|
||||||
currentDate.setDate(currentDate.getDate() + (nextWeekday - currentWeekday));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dates;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateMonthlyDates(startDate, endDate, option, maxSamples) {
|
|
||||||
const dates = [];
|
|
||||||
let currentDate = new Date(startDate);
|
|
||||||
|
|
||||||
while (currentDate <= endDate && dates.length < maxSamples) {
|
|
||||||
dates.push(new Date(currentDate));
|
|
||||||
|
|
||||||
// Move to next month
|
|
||||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
|
||||||
|
|
||||||
if (option === 'same_day') {
|
|
||||||
// Keep the same day of month
|
|
||||||
const targetDay = startDate.getDate();
|
|
||||||
// Adjust if the day doesn't exist in this month
|
|
||||||
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
|
|
||||||
currentDate.setDate(Math.min(targetDay, lastDayOfMonth));
|
|
||||||
}
|
|
||||||
else if (option === 'first_weekday' || option === 'last_weekday') {
|
|
||||||
const targetWeekday = startDate.getDay();
|
|
||||||
|
|
||||||
if (option === 'first_weekday') {
|
|
||||||
// Set to first day of month
|
|
||||||
currentDate.setDate(1);
|
|
||||||
|
|
||||||
// Find first occurrence of target weekday
|
|
||||||
while (currentDate.getDay() !== targetWeekday) {
|
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Set to last day of month
|
|
||||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
|
||||||
currentDate.setDate(0);
|
|
||||||
|
|
||||||
// Find last occurrence of target weekday going backwards
|
|
||||||
while (currentDate.getDay() !== targetWeekday) {
|
|
||||||
currentDate.setDate(currentDate.getDate() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dates;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update booking summary when relevant fields change
|
|
||||||
document.getElementById('booking-end-date').addEventListener('change', updateBookingSummary);
|
|
||||||
document.getElementById('recurrence-end-date').addEventListener('change', updateBookingSummary);
|
|
||||||
document.getElementById('period-start-select').addEventListener('change', updateBookingSummary);
|
|
||||||
document.getElementById('period-end-select').addEventListener('change', updateBookingSummary);
|
|
||||||
|
|
||||||
// Add listeners to all weekday checkboxes
|
|
||||||
document.querySelectorAll('input[name="weekdays"]').forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', updateBookingSummary);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Select the correct weekday checkbox based on chosen date
|
|
||||||
function updateWeekdaySelection() {
|
|
||||||
const date = new Date(document.getElementById('booking-date').value);
|
|
||||||
if (!isNaN(date.getTime())) {
|
|
||||||
// Clear all checkboxes
|
|
||||||
document.querySelectorAll('input[name="weekdays"]').forEach(cb => cb.checked = false);
|
|
||||||
|
|
||||||
// Get day of week (0=Sunday, 1=Monday, etc.)
|
|
||||||
let dayOfWeek = date.getDay();
|
|
||||||
// Convert to 1=Monday, 2=Tuesday, ... 7=Sunday
|
|
||||||
dayOfWeek = dayOfWeek === 0 ? 7 : dayOfWeek;
|
|
||||||
|
|
||||||
// Check the corresponding checkbox
|
|
||||||
const checkbox = document.querySelector(`input[name="weekdays"][value="${dayOfWeek}"]`);
|
|
||||||
if (checkbox) checkbox.checked = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Process multiple dates for a booking with period ranges
|
|
||||||
function processMultipleDates(formData, dates, periodStart, periodEnd) {
|
|
||||||
// Track progress
|
|
||||||
let successCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
let totalCount = dates.length;
|
|
||||||
let completedCount = 0;
|
|
||||||
|
|
||||||
// Close modal early to show progress
|
|
||||||
bookingModal.style.display = 'none';
|
|
||||||
|
|
||||||
// Create a progress indicator
|
|
||||||
const progressDiv = document.createElement('div');
|
|
||||||
progressDiv.className = 'booking-progress';
|
|
||||||
progressDiv.innerHTML = `
|
|
||||||
<div class="progress-overlay">
|
|
||||||
<div class="progress-content">
|
|
||||||
<h3>Erstelle Reservierungen...</h3>
|
|
||||||
<div class="progress-bar-container">
|
|
||||||
<div class="progress-bar" style="width: 0%"></div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-text">0 von ${totalCount} abgeschlossen</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(progressDiv);
|
|
||||||
|
|
||||||
// Submit bookings sequentially
|
|
||||||
function processNextDate(index) {
|
|
||||||
if (index >= dates.length) {
|
|
||||||
// All done
|
|
||||||
document.body.removeChild(progressDiv);
|
|
||||||
|
|
||||||
if (errorCount > 0) {
|
|
||||||
alert(`${successCount} Reservierungen erfolgreich erstellt. ${errorCount} fehlgeschlagen.`);
|
|
||||||
} else {
|
|
||||||
alert(`${successCount} Reservierungen erfolgreich erstellt!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh calendar
|
|
||||||
calendar.refetchEvents();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentDate = dates[index];
|
|
||||||
|
|
||||||
// Create new FormData for this date
|
|
||||||
const newFormData = new FormData();
|
|
||||||
|
|
||||||
// Copy original form values except dates and booking type
|
|
||||||
for (const [key, value] of formData.entries()) {
|
|
||||||
if (key !== 'booking_date' && key !== 'booking_end_date' &&
|
|
||||||
key !== 'start_date' && key !== 'end_date' &&
|
|
||||||
key !== 'recurrence_end_date' && key !== 'booking_type' &&
|
|
||||||
key !== 'recurrence_pattern' && !key.startsWith('weekdays')) {
|
|
||||||
newFormData.append(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format the current date with the period start time
|
|
||||||
const periodStart = formData.get('period_start');
|
|
||||||
const periodEnd = formData.get('period_end');
|
|
||||||
|
|
||||||
const periodStartInfo = schoolPeriods[periodStart];
|
|
||||||
const periodEndInfo = schoolPeriods[periodEnd];
|
|
||||||
|
|
||||||
if (periodStartInfo && periodStartInfo.start) {
|
|
||||||
// Format date for start_date with proper time
|
|
||||||
const startYear = currentDate.getFullYear();
|
|
||||||
const startMonth = String(currentDate.getMonth() + 1).padStart(2, '0');
|
|
||||||
const startDay = String(currentDate.getDate()).padStart(2, '0');
|
|
||||||
const startTimeStr = `${startYear}-${startMonth}-${startDay}T${periodStartInfo.start}:00`;
|
|
||||||
|
|
||||||
// Set the start_date field that server expects
|
|
||||||
newFormData.append('start_date', startTimeStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the end date, use the same day with the end period time
|
|
||||||
if (periodEndInfo && periodEndInfo.end) {
|
|
||||||
const endYear = currentDate.getFullYear();
|
|
||||||
const endMonth = String(currentDate.getMonth() + 1).padStart(2, '0');
|
|
||||||
const endDay = String(currentDate.getDate()).padStart(2, '0');
|
|
||||||
const endTimeStr = `${endYear}-${endMonth}-${endDay}T${periodEndInfo.end}:00`;
|
|
||||||
|
|
||||||
// Set the end_date field that server expects
|
|
||||||
newFormData.append('end_date', endTimeStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug the data being sent
|
|
||||||
console.log(`Submitting booking ${index + 1}/${dates.length}:`,
|
|
||||||
newFormData.get('start_date'),
|
|
||||||
newFormData.get('end_date'));
|
|
||||||
|
|
||||||
// Submit this booking
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
|
|
||||||
fetch('/plan_booking', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken
|
|
||||||
},
|
|
||||||
body: newFormData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
completedCount++;
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
successCount++;
|
|
||||||
} else {
|
|
||||||
errorCount++;
|
|
||||||
console.error('Booking failed:', data.error || 'Unknown error');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgress();
|
|
||||||
processNextDate(index + 1);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
completedCount++;
|
|
||||||
errorCount++;
|
|
||||||
console.error('Booking request error:', error);
|
|
||||||
updateProgress();
|
|
||||||
processNextDate(index + 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress() {
|
|
||||||
const progressBar = progressDiv.querySelector('.progress-bar');
|
|
||||||
const progressText = progressDiv.querySelector('.progress-text');
|
|
||||||
|
|
||||||
const percentage = Math.round((completedCount / totalCount) * 100);
|
|
||||||
progressBar.style.width = `${percentage}%`;
|
|
||||||
progressText.textContent = `${completedCount} von ${totalCount} abgeschlossen`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start processing
|
|
||||||
processNextDate(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safely attach event listeners only if elements exist
|
|
||||||
const periodStartSelect = document.getElementById('period-start-select');
|
|
||||||
const periodEndSelect = document.getElementById('period-end-select');
|
|
||||||
const bookingDateElement = document.getElementById('booking-date');
|
|
||||||
|
|
||||||
if (periodStartSelect) {
|
|
||||||
periodStartSelect.addEventListener('change', updateBookingSummary);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (periodEndSelect) {
|
|
||||||
periodEndSelect.addEventListener('change', updateBookingSummary);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bookingDateElement) {
|
|
||||||
bookingDateElement.addEventListener('change', updateBookingSummary);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,45 @@
|
|||||||
<h2 class="h5 fw-bold mb-2">Angemeldet als {{ current_user }}</h2>
|
<h2 class="h5 fw-bold mb-2">Angemeldet als {{ current_user }}</h2>
|
||||||
<p class="mb-0 text-muted">Sie können Termine anlegen, den Kalender prüfen und Buchungslinks verteilen.</p>
|
<p class="mb-0 text-muted">Sie können Termine anlegen, den Kalender prüfen und Buchungslinks verteilen.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 p-4 rounded-4 bg-white shadow-sm">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mb-3">
|
||||||
|
<h2 class="h5 fw-bold mb-0">Kommende Termine</h2>
|
||||||
|
<span class="text-muted small">Alle offenen Terminpläne dieses Nutzers</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if upcoming_events %}
|
||||||
|
<div class="vstack gap-3">
|
||||||
|
{% for event in upcoming_events %}
|
||||||
|
<div class="border rounded-3 p-3">
|
||||||
|
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">{{ event.date_start }} bis {{ event.date_end }}</div>
|
||||||
|
<div class="text-muted small">ID: {{ event.appointment_id }}</div>
|
||||||
|
{% if event.time_span %}
|
||||||
|
<div class="small mt-1">Zeitfenster: {{ event.time_span|join(' | ') }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if event.note %}
|
||||||
|
<div class="small mt-1 text-muted">Notiz: {{ event.note }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-lg-end">
|
||||||
|
<div class="small mb-2">Gebucht: <strong>{{ event.slots_booked }}</strong> / {{ event.slots_total }} | Frei: <strong>{{ event.slots_left }}</strong></div>
|
||||||
|
<div class="d-flex flex-wrap gap-2 justify-content-lg-end">
|
||||||
|
<a class="btn btn-sm btn-primary" href="{{ event.link }}" target="_blank" rel="noopener">Client-Link öffnen</a>
|
||||||
|
{% if event.calendar_link %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="{{ event.calendar_link }}">.ics</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted">Keine kommenden Termine gefunden.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,6 +53,9 @@
|
|||||||
"inventory": {
|
"inventory": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
"terminplan": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user