feat: Enhance tenant management and appointment deletion functionality
This commit is contained in:
@@ -166,7 +166,7 @@ def remove(id):
|
|||||||
result = items.delete_one({'_id': ObjectId(id)})
|
result = items.delete_one({'_id': ObjectId(id)})
|
||||||
|
|
||||||
client.close()
|
client.close()
|
||||||
return result.modified_count > 0
|
return result.deleted_count > 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error removing appointment: {e}")
|
print(f"Error removing appointment: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -179,6 +179,7 @@ def remove_done():
|
|||||||
items = db['appointments']
|
items = db['appointments']
|
||||||
|
|
||||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||||
|
removed_count = 0
|
||||||
|
|
||||||
cursor = items.find(
|
cursor = items.find(
|
||||||
_active_record_query(
|
_active_record_query(
|
||||||
@@ -191,9 +192,10 @@ def remove_done():
|
|||||||
for item in cursor:
|
for item in cursor:
|
||||||
item['_id'] = str(item.get('_id'))
|
item['_id'] = str(item.get('_id'))
|
||||||
result = items.delete_one({'_id': ObjectId(item['_id'])})
|
result = items.delete_one({'_id': ObjectId(item['_id'])})
|
||||||
|
removed_count += result.deleted_count
|
||||||
|
|
||||||
client.close()
|
client.close()
|
||||||
return result.modified_count > 0
|
return removed_count > 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error removing appointment: {e}")
|
print(f"Error removing appointment: {e}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ def _resolve_public_base_url() -> str:
|
|||||||
return f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
|
return f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
|
||||||
|
|
||||||
|
|
||||||
|
def _current_tenant_id() -> str:
|
||||||
|
tenant_context = get_tenant_context()
|
||||||
|
if tenant_context and getattr(tenant_context, 'tenant_id', None):
|
||||||
|
return str(tenant_context.tenant_id)
|
||||||
|
if has_request_context():
|
||||||
|
try:
|
||||||
|
return str(request.args.get('tenant', '') or request.args.get('tenant_id', '') or '').strip()
|
||||||
|
except Exception:
|
||||||
|
return ''
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
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()]
|
||||||
@@ -70,11 +82,14 @@ def build_calendar_ics(appointment_id: str) -> str | None:
|
|||||||
time_span = item.get('time_span', []) or []
|
time_span = item.get('time_span', []) or []
|
||||||
creator = item.get('user', 'Terminplaner')
|
creator = item.get('user', 'Terminplaner')
|
||||||
note = item.get('note', '') or ''
|
note = item.get('note', '') or ''
|
||||||
|
tenant_id = _current_tenant_id()
|
||||||
try:
|
try:
|
||||||
link = url_for('terminplaner.client', appointment_id=str(appointment_id), _external=True)
|
link = url_for('terminplaner.client', appointment_id=str(appointment_id), tenant=tenant_id or None, _external=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
host = _resolve_public_base_url()
|
host = _resolve_public_base_url()
|
||||||
link = host + "/terminplaner/client/" + str(appointment_id)
|
link = host + "/terminplaner/client/" + str(appointment_id)
|
||||||
|
if tenant_id:
|
||||||
|
link += f"?tenant={tenant_id}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start_date = datetime.datetime.strptime(str(date_start), '%Y-%m-%d').date()
|
start_date = datetime.datetime.strptime(str(date_start), '%Y-%m-%d').date()
|
||||||
@@ -133,21 +148,26 @@ def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
|
|||||||
normalized_mail = _normalize_mail_list(mail)
|
normalized_mail = _normalize_mail_list(mail)
|
||||||
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_id = _current_tenant_id()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
link = url_for('terminplaner.client', appointment_id=id_str, _external=True)
|
link = url_for('terminplaner.client', appointment_id=id_str, tenant=tenant_id or None, _external=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
host = _resolve_public_base_url()
|
host = _resolve_public_base_url()
|
||||||
link = host + "/terminplaner/client/" + id_str
|
link = host + "/terminplaner/client/" + id_str
|
||||||
|
if tenant_id:
|
||||||
|
link += f"?tenant={tenant_id}"
|
||||||
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}"
|
||||||
calendar_link = None
|
calendar_link = None
|
||||||
if calendar_enabled:
|
if calendar_enabled:
|
||||||
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, tenant=tenant_id or None, _external=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
host = _resolve_public_base_url()
|
host = _resolve_public_base_url()
|
||||||
calendar_link = host + "/terminplaner/calendar/" + id_str + ".ics"
|
calendar_link = host + "/terminplaner/calendar/" + id_str + ".ics"
|
||||||
|
if tenant_id:
|
||||||
|
calendar_link += f"?tenant={tenant_id}"
|
||||||
|
|
||||||
email_body = note_link
|
email_body = note_link
|
||||||
if calendar_link:
|
if calendar_link:
|
||||||
@@ -324,6 +344,7 @@ def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
|
|||||||
|
|
||||||
appointments = termin.get_upcoming_for_user(user_name, limit=limit)
|
appointments = termin.get_upcoming_for_user(user_name, limit=limit)
|
||||||
host = _resolve_public_base_url()
|
host = _resolve_public_base_url()
|
||||||
|
tenant_id = _current_tenant_id()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for item in appointments:
|
for item in appointments:
|
||||||
@@ -332,14 +353,18 @@ def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
link = url_for('terminplaner.client', appointment_id=appointment_id, _external=True)
|
link = url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id or None, _external=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
link = host + '/terminplaner/client/' + appointment_id
|
link = host + '/terminplaner/client/' + appointment_id
|
||||||
|
if tenant_id:
|
||||||
|
link += f'?tenant={tenant_id}'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
calendar_link = url_for('terminplaner.calendar_export', appointment_id=appointment_id, _external=True)
|
calendar_link = url_for('terminplaner.calendar_export', appointment_id=appointment_id, tenant=tenant_id or None, _external=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
calendar_link = host + '/terminplaner/calendar/' + appointment_id + '.ics'
|
calendar_link = host + '/terminplaner/calendar/' + appointment_id + '.ics'
|
||||||
|
if tenant_id:
|
||||||
|
calendar_link += f'?tenant={tenant_id}'
|
||||||
|
|
||||||
slots_total = int(item.get('slots', 0) or 0)
|
slots_total = int(item.get('slots', 0) or 0)
|
||||||
slots_booked = item.get('slots_booked', []) or []
|
slots_booked = item.get('slots_booked', []) or []
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from flask import Blueprint, render_template, request, session, url_for, redirec
|
|||||||
from flask import Response
|
from flask import Response
|
||||||
import Web.modules.terminplaner.backend_server as appointment_service
|
import Web.modules.terminplaner.backend_server as appointment_service
|
||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
|
import Web.modules.database.termine as termin
|
||||||
|
import Web.modules.database.user as us
|
||||||
|
|
||||||
# Create a blueprint instance
|
# Create a blueprint instance
|
||||||
appoint_bp = Blueprint('terminplaner', __name__)
|
appoint_bp = Blueprint('terminplaner', __name__)
|
||||||
@@ -21,6 +23,17 @@ def _appointment_not_found_response():
|
|||||||
error_message='Der Termin wurde nicht gefunden.',
|
error_message='Der Termin wurde nicht gefunden.',
|
||||||
), 404
|
), 404
|
||||||
|
|
||||||
|
|
||||||
|
def _current_tenant_id():
|
||||||
|
try:
|
||||||
|
from Web.tenant import get_tenant_context
|
||||||
|
ctx = get_tenant_context()
|
||||||
|
if ctx and getattr(ctx, 'tenant_id', None):
|
||||||
|
return str(ctx.tenant_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return str(session.get('tenant_id', '') or '').strip()
|
||||||
|
|
||||||
@appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
|
@appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
|
||||||
def client(appointment_id):
|
def client(appointment_id):
|
||||||
"""
|
"""
|
||||||
@@ -57,8 +70,37 @@ def client(appointment_id):
|
|||||||
appointment_id=appointment_id,
|
appointment_id=appointment_id,
|
||||||
available=available,
|
available=available,
|
||||||
current_user=session.get('username', ''),
|
current_user=session.get('username', ''),
|
||||||
|
tenant_id=_current_tenant_id(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@appoint_bp.route('/delete/<appointment_id>', methods=['POST'])
|
||||||
|
def delete_appointment(appointment_id):
|
||||||
|
guard = _require_module_enabled()
|
||||||
|
if guard:
|
||||||
|
return guard
|
||||||
|
|
||||||
|
if 'username' not in session:
|
||||||
|
flash('Bitte mit einem Konto anmelden.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
appointment = termin.get_item(appointment_id)
|
||||||
|
if not appointment:
|
||||||
|
return _appointment_not_found_response()
|
||||||
|
|
||||||
|
current_user = str(session.get('username', '')).strip()
|
||||||
|
appointment_user = str(appointment.get('user', '')).strip()
|
||||||
|
if not us.check_admin(current_user) and appointment_user != current_user:
|
||||||
|
flash('Sie dürfen diesen Termin nicht löschen.', 'error')
|
||||||
|
return redirect(url_for('terminplaner.main', tenant=_current_tenant_id() or None))
|
||||||
|
|
||||||
|
if termin.remove(appointment_id):
|
||||||
|
flash('Der Terminplan wurde gelöscht.', 'success')
|
||||||
|
else:
|
||||||
|
flash('Der Terminplan konnte nicht gelöscht werden.', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('terminplaner.main', tenant=_current_tenant_id() or None))
|
||||||
|
|
||||||
@appoint_bp.route('/configure', methods=['GET', 'POST'])
|
@appoint_bp.route('/configure', methods=['GET', 'POST'])
|
||||||
def configure():
|
def configure():
|
||||||
"""
|
"""
|
||||||
@@ -134,10 +176,12 @@ def main():
|
|||||||
|
|
||||||
current_user = session.get('username', '')
|
current_user = session.get('username', '')
|
||||||
upcoming_events = appointment_service.get_user_upcoming_events(current_user) if current_user else []
|
upcoming_events = appointment_service.get_user_upcoming_events(current_user) if current_user else []
|
||||||
|
tenant_id = _current_tenant_id()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'terminplaner.html',
|
'terminplaner.html',
|
||||||
school_periods=cfg.SCHOOL_PERIODS,
|
school_periods=cfg.SCHOOL_PERIODS,
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
upcoming_events=upcoming_events,
|
upcoming_events=upcoming_events,
|
||||||
|
tenant_id=tenant_id,
|
||||||
)
|
)
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<div class="card border-0 shadow-lg rounded-4 h-100">
|
<div class="card border-0 shadow-lg rounded-4 h-100">
|
||||||
<div class="card-body p-4 p-md-5 bg-white">
|
<div class="card-body p-4 p-md-5 bg-white">
|
||||||
<h2 class="h4 fw-bold mb-4">Buchung absenden</h2>
|
<h2 class="h4 fw-bold mb-4">Buchung absenden</h2>
|
||||||
<form method="post" action="{{ url_for('terminplaner.client', appointment_id=appointment_id) }}" class="vstack gap-3">
|
<form method="post" action="{{ url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id) }}" class="vstack gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label for="start_day_time" class="form-label fw-semibold">Gewünschter Zeitpunkt</label>
|
<label for="start_day_time" class="form-label fw-semibold">Gewünschter Zeitpunkt</label>
|
||||||
<input type="text" id="start_day_time" name="start_day_time" class="form-control form-control-lg" placeholder="2026-05-29 10:30" required>
|
<input type="text" id="start_day_time" name="start_day_time" class="form-control form-control-lg" placeholder="2026-05-29 10:30" required>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
|
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
|
||||||
<button type="submit" class="btn btn-primary btn-lg">Termin buchen</button>
|
<button type="submit" class="btn btn-primary btn-lg">Termin buchen</button>
|
||||||
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main') }}">Zur Übersicht</a>
|
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main', tenant=tenant_id) }}">Zur Übersicht</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
<p class="lead mb-0" style="max-width: 60ch; opacity: .95;">Erstellen Sie neue Terminreihen, teilen Sie Buchungslinks und öffnen Sie den Kalender für bestehende Reservierungen.</p>
|
<p class="lead mb-0" style="max-width: 60ch; opacity: .95;">Erstellen Sie neue Terminreihen, teilen Sie Buchungslinks und öffnen Sie den Kalender für bestehende Reservierungen.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<a class="btn btn-light btn-lg fw-semibold" href="{{ url_for('terminplaner.configure') }}">Neue Planung</a>
|
<a class="btn btn-light btn-lg fw-semibold" href="{{ url_for('terminplaner.configure', tenant=tenant_id) }}">Neue Planung</a>
|
||||||
<a class="btn btn-outline-light btn-lg fw-semibold" href="{{ url_for('terminplan') }}">Kalender öffnen</a>
|
<a class="btn btn-outline-light btn-lg fw-semibold" href="{{ url_for('terminplan', tenant=tenant_id) }}">Kalender öffnen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="display-6 mb-3">🗓️</div>
|
<div class="display-6 mb-3">🗓️</div>
|
||||||
<h2 class="h4 fw-bold">Kalender</h2>
|
<h2 class="h4 fw-bold">Kalender</h2>
|
||||||
<p class="mb-4 text-muted">Sehen Sie vorhandene Termine, ihre Auslastung und die aktuellen Reservierungen im Kalender.</p>
|
<p class="mb-4 text-muted">Sehen Sie vorhandene Termine, ihre Auslastung und die aktuellen Reservierungen im Kalender.</p>
|
||||||
<a class="btn btn-primary w-100" href="{{ url_for('terminplan') }}">Zum Kalender</a>
|
<a class="btn btn-primary w-100" href="{{ url_for('terminplan', tenant=tenant_id) }}">Zum Kalender</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="display-6 mb-3">✍️</div>
|
<div class="display-6 mb-3">✍️</div>
|
||||||
<h2 class="h4 fw-bold">Neue Planung</h2>
|
<h2 class="h4 fw-bold">Neue Planung</h2>
|
||||||
<p class="mb-4 text-muted">Erstellen Sie einen neuen Terminplan und verschicken Sie den Buchungslink an Ihre Zielgruppe.</p>
|
<p class="mb-4 text-muted">Erstellen Sie einen neuen Terminplan und verschicken Sie den Buchungslink an Ihre Zielgruppe.</p>
|
||||||
<a class="btn btn-outline-primary w-100" href="{{ url_for('terminplaner.configure') }}">Konfiguration öffnen</a>
|
<a class="btn btn-outline-primary w-100" href="{{ url_for('terminplaner.configure', tenant=tenant_id) }}">Konfiguration öffnen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,6 +87,9 @@
|
|||||||
{% if event.calendar_link %}
|
{% if event.calendar_link %}
|
||||||
<a class="btn btn-sm btn-outline-primary" href="{{ event.calendar_link }}">.ics</a>
|
<a class="btn btn-sm btn-outline-primary" href="{{ event.calendar_link }}">.ics</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<form method="post" action="{{ url_for('terminplaner.delete_appointment', appointment_id=event.appointment_id, tenant=tenant_id) }}" class="d-inline" onsubmit="return confirm('Diesen Terminplan wirklich löschen?');">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">Entfernen</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -473,6 +473,20 @@ class TenantContext:
|
|||||||
if not has_request_context():
|
if not has_request_context():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Query parameters are useful for public links that must open a specific tenant
|
||||||
|
# even when the host/subdomain cannot be mapped reliably.
|
||||||
|
tenant_from_query = (
|
||||||
|
request.args.get('tenant', '').strip()
|
||||||
|
or request.args.get('tenant_id', '').strip()
|
||||||
|
or request.args.get('tenantId', '').strip()
|
||||||
|
)
|
||||||
|
if tenant_from_query:
|
||||||
|
matched_tenant = _find_registered_tenant_id(tenant_from_query) or tenant_from_query
|
||||||
|
self.tenant_id = matched_tenant
|
||||||
|
self.config = get_tenant_config(matched_tenant)
|
||||||
|
session['tenant_id'] = matched_tenant
|
||||||
|
return self._get_db_name(matched_tenant)
|
||||||
|
|
||||||
# Priority 1: X-Tenant-ID header (for testing/internal APIs)
|
# Priority 1: X-Tenant-ID header (for testing/internal APIs)
|
||||||
tenant_from_header = request.headers.get('X-Tenant-ID', '').strip()
|
tenant_from_header = request.headers.get('X-Tenant-ID', '').strip()
|
||||||
if tenant_from_header:
|
if tenant_from_header:
|
||||||
@@ -531,6 +545,10 @@ class TenantContext:
|
|||||||
potential_subdomain = parts[0]
|
potential_subdomain = parts[0]
|
||||||
if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'):
|
if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'):
|
||||||
matched_tenant = _find_registered_tenant_id(potential_subdomain)
|
matched_tenant = _find_registered_tenant_id(potential_subdomain)
|
||||||
|
if not matched_tenant and potential_subdomain.startswith('school'):
|
||||||
|
matched_tenant = _find_registered_tenant_id('schule' + potential_subdomain[len('school'):])
|
||||||
|
elif not matched_tenant and potential_subdomain.startswith('schule'):
|
||||||
|
matched_tenant = _find_registered_tenant_id('school' + potential_subdomain[len('schule'):])
|
||||||
if matched_tenant:
|
if matched_tenant:
|
||||||
self.subdomain = potential_subdomain
|
self.subdomain = potential_subdomain
|
||||||
self.tenant_id = matched_tenant
|
self.tenant_id = matched_tenant
|
||||||
|
|||||||
Reference in New Issue
Block a user