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
|
||||
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']
|
||||
db = client[MONGODB_DB]
|
||||
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))
|
||||
# Use the established user-specific route logic for appointments.
|
||||
bookings = au.get_ausleihung_by_user(
|
||||
username,
|
||||
status=['planned', 'active', 'completed'],
|
||||
use_client_side_verification=True,
|
||||
)
|
||||
bookings = sorted(bookings, key=lambda b: b.get('Start') or datetime.datetime.min)
|
||||
|
||||
result = []
|
||||
for booking in bookings:
|
||||
@@ -4873,6 +4855,8 @@ def get_user_appointments():
|
||||
title = f"{title} - {period}. Std"
|
||||
|
||||
status = booking.get('VerifiedStatus') or booking.get('Status') or 'unknown'
|
||||
if status == 'active':
|
||||
status = 'current'
|
||||
result.append({
|
||||
'id': str(booking.get('_id')),
|
||||
'title': title,
|
||||
|
||||
@@ -102,7 +102,7 @@ def update(id,slots_used: list):
|
||||
items = db['appointments']
|
||||
|
||||
update_data = {
|
||||
'slots_booked': [slots_used],
|
||||
'slots_booked': slots_used,
|
||||
'LastUpdated': datetime.datetime.now()
|
||||
}
|
||||
|
||||
@@ -171,4 +171,62 @@ def remove(id):
|
||||
print(f"Error removing appointment: {e}")
|
||||
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
|
||||
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.database.termine as termin
|
||||
import Web.modules.database.settings as cfg
|
||||
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):
|
||||
if isinstance(time_span, list):
|
||||
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:
|
||||
link = url_for('terminplaner.client', appointment_id=str(appointment_id), _external=True)
|
||||
except Exception:
|
||||
tenant_context = get_tenant_context()
|
||||
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"
|
||||
host = _resolve_public_base_url()
|
||||
link = host + "/terminplaner/client/" + str(appointment_id)
|
||||
|
||||
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_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:
|
||||
link = url_for('terminplaner.client', appointment_id=id_str, _external=True)
|
||||
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
|
||||
subject = f"Terminanfrage von {user}"
|
||||
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:
|
||||
calendar_link = url_for('terminplaner.calendar_export', appointment_id=id_str, _external=True)
|
||||
except Exception:
|
||||
tenant_context = get_tenant_context()
|
||||
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"
|
||||
host = _resolve_public_base_url()
|
||||
calendar_link = host + "/terminplaner/calendar/" + id_str + ".ics"
|
||||
|
||||
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,
|
||||
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:
|
||||
return guard
|
||||
|
||||
current_user = session.get('username', '')
|
||||
upcoming_events = appointment_service.get_user_upcoming_events(current_user) if current_user else []
|
||||
|
||||
return render_template(
|
||||
'terminplaner.html',
|
||||
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="dayGridMonth">Monat</button>
|
||||
</div>
|
||||
<button id="new-booking" class="primary-button">Neue Reservierung</button>
|
||||
<button id="new-booking" class="primary-button">Zur Termin-Konfiguration</button>
|
||||
</div>
|
||||
<div class="calendar-legend">
|
||||
<span class="legend-item"><span class="legend-color current"></span> Aktuelle Termine</span>
|
||||
@@ -73,128 +73,16 @@
|
||||
<div id="calendar"></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 -->
|
||||
<div id="event-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h2>Ausleihungs Details</h2>
|
||||
<h2>Termin-Details</h2>
|
||||
<div id="event-details">
|
||||
<!-- Event details will be loaded dynamically -->
|
||||
</div>
|
||||
<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>
|
||||
@@ -210,7 +98,6 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize variables
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
const bookingModal = document.getElementById('booking-modal');
|
||||
const eventModal = document.getElementById('event-modal');
|
||||
const newBookingBtn = document.getElementById('new-booking');
|
||||
let currentEventId = null;
|
||||
@@ -452,7 +339,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const borrowerIndicator = document.createElement('div');
|
||||
borrowerIndicator.className = 'borrowed-indicator';
|
||||
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.top = '2px';
|
||||
borrowerIndicator.style.left = '2px';
|
||||
@@ -526,53 +413,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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
|
||||
function showEventDetails(event) {
|
||||
const details = document.getElementById('event-details');
|
||||
@@ -584,7 +424,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get status text based on status value
|
||||
let statusText = 'Unbekannt';
|
||||
if (event.extendedProps.status === 'current') {
|
||||
statusText = 'Aktuell ausgeliehen';
|
||||
statusText = 'Aktiv';
|
||||
} else if (event.extendedProps.status === 'planned') {
|
||||
statusText = 'Geplant';
|
||||
} else if (event.extendedProps.status === 'completed') {
|
||||
@@ -603,17 +443,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check if item is already borrowed by someone else
|
||||
let borrowerInfo = '';
|
||||
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
|
||||
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>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>
|
||||
${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>` : ''}
|
||||
<p><strong>Status:</strong> ${statusText}</p>
|
||||
${borrowerInfo}
|
||||
@@ -631,149 +471,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
eventModal.style.display = 'block';
|
||||
}
|
||||
|
||||
// New booking button
|
||||
// Route users to the dedicated configuration flow instead of creating bookings here.
|
||||
newBookingBtn.addEventListener('click', function() {
|
||||
openBookingModal();
|
||||
window.location.href = '/terminplaner/configure';
|
||||
});
|
||||
|
||||
// Close modals when clicking X
|
||||
document.querySelectorAll('.close').forEach(closeBtn => {
|
||||
// Close event details modal when clicking X
|
||||
document.querySelectorAll('#event-modal .close').forEach(closeBtn => {
|
||||
closeBtn.addEventListener('click', function() {
|
||||
bookingModal.style.display = 'none';
|
||||
eventModal.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Close modals when clicking outside
|
||||
// Close event details modal when clicking outside
|
||||
window.addEventListener('click', function(event) {
|
||||
if (event.target === bookingModal) {
|
||||
bookingModal.style.display = 'none';
|
||||
}
|
||||
if (event.target === eventModal) {
|
||||
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
|
||||
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 : '';
|
||||
fetch('/cancel_booking/' + currentEventId, {
|
||||
method: 'POST',
|
||||
@@ -786,7 +504,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (data.success) {
|
||||
eventModal.style.display = 'none';
|
||||
calendar.refetchEvents();
|
||||
alert('Ausleihe erfolgreich storniert!');
|
||||
alert('Termin erfolgreich storniert!');
|
||||
} else {
|
||||
alert('Fehler: ' + data.error);
|
||||
}
|
||||
@@ -802,492 +520,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
showCompletedBookings = this.checked;
|
||||
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>
|
||||
|
||||
|
||||
@@ -58,6 +58,45 @@
|
||||
<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>
|
||||
</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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
"inventory": {
|
||||
"enabled": true
|
||||
},
|
||||
"terminplan": {
|
||||
"enabled": true
|
||||
},
|
||||
"library": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user