Initial commit - Customer Portal for Coolify

This commit is contained in:
2025-12-17 10:08:34 +01:00
commit 9fca32567c
153 changed files with 16432 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
{% extends "admin/base.html" %}
{% block title %}Admin-Benutzer{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1><i class="bi bi-shield-lock me-2"></i>Admin-Benutzer</h1>
<p class="text-muted">{{ admin_users|length }} Administratoren</p>
</div>
<a href="{{ url_for('admin.create_admin') }}" class="btn btn-danger">
<i class="bi bi-plus-lg me-1"></i>
Neuer Admin
</a>
</div>
<div class="card bg-dark border-secondary">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Benutzername</th>
<th>Name</th>
<th>E-Mail</th>
<th>Erstellt</th>
<th>Letzter Login</th>
<th class="text-center">Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for admin in admin_users %}
<tr>
<td>
<strong>{{ admin.username }}</strong>
{% if admin.id == g.admin_user.id %}
<span class="badge bg-info ms-1">Sie</span>
{% endif %}
</td>
<td>{{ admin.name }}</td>
<td>
{% if admin.email %}
<a href="mailto:{{ admin.email }}" class="text-info text-decoration-none">
{{ admin.email }}
</a>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>{{ admin.created_at.strftime('%d.%m.%Y') if admin.created_at else '-' }}</td>
<td>
{% if admin.last_login_at %}
{{ admin.last_login_at.strftime('%d.%m.%Y %H:%M') }}
{% else %}
<span class="text-muted">Noch nie</span>
{% endif %}
</td>
<td class="text-center">
{% if admin.is_active %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-secondary">Inaktiv</span>
{% endif %}
</td>
<td class="text-end">
{% if admin.id != g.admin_user.id %}
<form action="{{ url_for('admin.toggle_admin_status', admin_id=admin.id) }}"
method="POST" class="d-inline">
{% if admin.is_active %}
<button type="submit" class="btn btn-sm btn-outline-warning"
title="Deaktivieren"
onclick="return confirm('Admin {{ admin.username }} deaktivieren?')">
<i class="bi bi-pause-circle"></i>
</button>
{% else %}
<button type="submit" class="btn btn-sm btn-outline-success"
title="Aktivieren">
<i class="bi bi-play-circle"></i>
</button>
{% endif %}
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,456 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Admin{% endblock %} - {{ branding.company_name }} Admin</title>
{% if branding.favicon_url %}
<link rel="icon" href="{{ branding.favicon_url }}" type="image/x-icon">
{% endif %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
:root {
--admin-primary: #dc3545;
--admin-primary-hover: #bb2d3b;
--sidebar-bg: {{ branding.colors.sidebar_bg }};
--sidebar-border: {{ branding.colors.border }};
--text-light: {{ branding.colors.text }};
--text-muted: {{ branding.colors.muted }};
--portal-bg: {{ branding.colors.background }};
}
body {
background: var(--portal-bg);
color: var(--text-light);
min-height: 100vh;
}
.admin-sidebar {
position: fixed;
left: 0;
top: 0;
width: 240px;
height: 100vh;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
display: flex;
flex-direction: column;
z-index: 1000;
}
.admin-sidebar-header {
padding: 1.25rem;
border-bottom: 1px solid var(--sidebar-border);
}
.admin-logo {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-light);
text-decoration: none;
font-weight: 600;
}
.admin-logo i {
color: var(--admin-primary);
font-size: 1.5rem;
}
.admin-nav-wrapper {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.admin-nav-wrapper::-webkit-scrollbar {
width: 6px;
}
.admin-nav-wrapper::-webkit-scrollbar-track {
background: transparent;
}
.admin-nav-wrapper::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.admin-nav-wrapper::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.admin-nav {
list-style: none;
padding: 1rem 0.75rem;
margin: 0;
}
.admin-nav .nav-item {
margin-bottom: 0.25rem;
}
.admin-nav .nav-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: var(--text-muted);
text-decoration: none;
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.admin-nav .nav-link:hover {
color: var(--text-light);
background: rgba(255, 255, 255, 0.05);
}
.admin-nav .nav-link.active {
color: var(--text-light);
background: var(--admin-primary);
}
.admin-sidebar-footer {
padding: 1rem;
border-top: 1px solid var(--sidebar-border);
}
.admin-main {
margin-left: 240px;
min-height: 100vh;
padding: 2rem;
}
.card {
background: var(--sidebar-bg);
border-color: var(--sidebar-border);
}
.table {
--bs-table-bg: transparent;
--bs-table-color: var(--text-light);
--bs-table-border-color: var(--sidebar-border);
}
.form-control, .form-select {
background: #0d0f12;
border-color: var(--sidebar-border);
color: var(--text-light);
}
.form-control:focus, .form-select:focus {
background: #0d0f12;
border-color: var(--admin-primary);
color: var(--text-light);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}
.form-label {
color: var(--text-light);
}
.form-text {
color: var(--text-muted) !important;
}
.form-check-label {
color: var(--text-light);
}
.card-header {
color: var(--text-light);
background: rgba(255, 255, 255, 0.03);
border-color: var(--sidebar-border);
}
.card-body {
color: var(--text-light);
}
.breadcrumb-item {
color: var(--text-muted);
}
.breadcrumb-item.active {
color: var(--text-light);
}
.breadcrumb-item + .breadcrumb-item::before {
color: var(--text-muted);
}
h1, h2, h3, h4, h5, h6 {
color: var(--text-light);
}
.modal-content {
background: var(--sidebar-bg);
color: var(--text-light);
}
.modal-header, .modal-footer {
border-color: var(--sidebar-border);
}
.btn-close {
filter: invert(1);
}
/* Text colors - ensure visibility on dark background */
.text-muted {
color: var(--text-muted) !important;
}
p, li, td, th, span, strong, label {
color: var(--text-light);
}
.card .small, .card small {
color: var(--text-light);
}
.card ul {
color: var(--text-light);
}
.alert {
color: var(--text-light);
}
.alert-info {
background: rgba(13, 202, 240, 0.15);
border-color: rgba(13, 202, 240, 0.3);
}
.alert-warning {
background: rgba(255, 193, 7, 0.15);
border-color: rgba(255, 193, 7, 0.3);
}
.alert-danger {
background: rgba(220, 53, 69, 0.15);
border-color: rgba(220, 53, 69, 0.3);
}
.alert-success {
background: rgba(25, 135, 84, 0.15);
border-color: rgba(25, 135, 84, 0.3);
}
code {
color: #17a2b8;
}
.form-text {
color: var(--text-muted) !important;
}
.table-dark {
--bs-table-color: var(--text-light);
--bs-table-bg: transparent;
}
.table-dark th {
color: var(--text-light) !important;
}
.table-dark td {
color: var(--text-light) !important;
}
.btn-danger {
background: var(--admin-primary);
border-color: var(--admin-primary);
}
.btn-danger:hover {
background: var(--admin-primary-hover);
border-color: var(--admin-primary-hover);
}
@media (max-width: 991px) {
.admin-sidebar {
transform: translateX(-100%);
transition: transform 0.3s;
}
.admin-sidebar.show {
transform: translateX(0);
}
.admin-main {
margin-left: 0;
}
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="admin-sidebar">
<div class="admin-sidebar-header">
<a href="{{ url_for('admin.index') }}" class="admin-logo">
{% if branding.logo_url %}
<img src="{{ branding.logo_url }}" alt="{{ branding.company_name }}" style="max-height: 30px;">
{% else %}
<i class="bi bi-shield-lock"></i>
{% endif %}
<span>Admin</span>
</a>
</div>
<div class="admin-nav-wrapper">
<ul class="admin-nav">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.index' %}active{% endif %}"
href="{{ url_for('admin.index') }}">
<i class="bi bi-speedometer2"></i>
<span>Dashboard</span>
</a>
</li>
<!-- Benutzer -->
<li class="nav-item mt-3">
<span class="nav-label text-muted small px-3">BENUTZER</span>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.customers' %}active{% endif %}"
href="{{ url_for('admin.customers') }}">
<i class="bi bi-people"></i>
<span>Kunden</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if 'admins' in request.endpoint %}active{% endif %}"
href="{{ url_for('admin.admins') }}">
<i class="bi bi-shield-check"></i>
<span>Administratoren</span>
</a>
</li>
<!-- Buchungen -->
<li class="nav-item mt-3">
<span class="nav-label text-muted small px-3">BUCHUNGEN</span>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.bookings' %}active{% endif %}"
href="{{ url_for('admin.bookings') }}">
<i class="bi bi-calendar-check"></i>
<span>Buchungen</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.bookings_sync' %}active{% endif %}"
href="{{ url_for('admin.bookings_sync') }}">
<i class="bi bi-arrow-repeat"></i>
<span>Sync</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.bookings_import' %}active{% endif %}"
href="{{ url_for('admin.bookings_import') }}">
<i class="bi bi-upload"></i>
<span>Import</span>
</a>
</li>
<!-- Einstellungen -->
<li class="nav-item mt-3">
<span class="nav-label text-muted small px-3">EINSTELLUNGEN</span>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.field_config' %}active{% endif %}"
href="{{ url_for('admin.field_config') }}">
<i class="bi bi-sliders"></i>
<span>Profil-Felder</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.settings_customer_view' %}active{% endif %}"
href="{{ url_for('admin.settings_customer_view') }}">
<i class="bi bi-layout-text-sidebar"></i>
<span>Kundenansicht</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.settings_mail' %}active{% endif %}"
href="{{ url_for('admin.settings_mail') }}">
<i class="bi bi-envelope"></i>
<span>Mail-Server</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.settings_otp' %}active{% endif %}"
href="{{ url_for('admin.settings_otp') }}">
<i class="bi bi-key"></i>
<span>OTP & Sicherheit</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.settings_wordpress' %}active{% endif %}"
href="{{ url_for('admin.settings_wordpress') }}">
<i class="bi bi-wordpress"></i>
<span>WordPress</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.settings_csv' %}active{% endif %}"
href="{{ url_for('admin.settings_csv') }}">
<i class="bi bi-filetype-csv"></i>
<span>CSV Export/Import</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.settings_customer_defaults' %}active{% endif %}"
href="{{ url_for('admin.settings_customer_defaults') }}">
<i class="bi bi-person-plus"></i>
<span>Kunden-Standards</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.settings_field_mapping' %}active{% endif %}"
href="{{ url_for('admin.settings_field_mapping') }}">
<i class="bi bi-arrow-left-right"></i>
<span>Feld-Mapping</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.settings_branding' %}active{% endif %}"
href="{{ url_for('admin.settings_branding') }}">
<i class="bi bi-palette"></i>
<span>Branding</span>
</a>
</li>
</ul>
</div>
<div class="admin-sidebar-footer">
<div class="d-flex align-items-center gap-2 mb-2 text-muted small">
<i class="bi bi-person-circle"></i>
<span>{{ g.admin_user.name }}</span>
</div>
<a href="{{ url_for('admin.logout') }}" class="btn btn-outline-danger btn-sm w-100">
<i class="bi bi-box-arrow-right me-1"></i>
Abmelden
</a>
</div>
</nav>
<main class="admin-main">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,200 @@
{% extends "admin/base.html" %}
{% block title %}Buchung: {{ booking.booking_number }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-2">
<li class="breadcrumb-item"><a href="{{ url_for('admin.bookings') }}" class="text-info">Buchungen</a></li>
<li class="breadcrumb-item active">{{ booking.booking_number or 'Details' }}</li>
</ol>
</nav>
<h1><i class="bi bi-calendar-check me-2"></i>{{ booking.booking_number or 'Buchung #' ~ booking.id }}</h1>
</div>
<div>
<span class="badge bg-{{ booking.status_color }} fs-6 me-2">{{ booking.status_display }}</span>
<a href="{{ url_for('admin.bookings') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Zurueck
</a>
</div>
</div>
<div class="row g-4">
<!-- Buchungsdetails -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>Buchungsdetails
</div>
<div class="card-body">
<table class="table table-dark table-borderless mb-0">
<tr>
<td class="text-muted" style="width: 140px;">Buchungsnummer</td>
<td><strong>{{ booking.booking_number or '-' }}</strong></td>
</tr>
<tr>
<td class="text-muted">WordPress ID</td>
<td>#{{ booking.wp_booking_id }}</td>
</tr>
<tr>
<td class="text-muted">Status</td>
<td><span class="badge bg-{{ booking.status_color }}">{{ booking.status_display }}</span></td>
</tr>
<tr>
<td class="text-muted">Gesamtpreis</td>
<td><strong>{{ booking.formatted_price }}</strong></td>
</tr>
<tr>
<td class="text-muted">Ticket-Typ</td>
<td>{{ booking.ticket_type or '-' }}</td>
</tr>
<tr>
<td class="text-muted">Anzahl</td>
<td>{{ booking.ticket_count or 1 }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Kunde -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-person me-2"></i>Kunde
</div>
<div class="card-body">
<table class="table table-dark table-borderless mb-0">
<tr>
<td class="text-muted" style="width: 140px;">Name</td>
<td>
<a href="{{ url_for('admin.customer_detail', customer_id=booking.customer_id) }}" class="text-info">
{{ booking.customer.display_name }}
</a>
</td>
</tr>
<tr>
<td class="text-muted">E-Mail</td>
<td>
<a href="mailto:{{ booking.customer.email }}" class="text-info">{{ booking.customer.email }}</a>
</td>
</tr>
<tr>
<td class="text-muted">Telefon</td>
<td>{{ booking.customer_phone or booking.customer.display_phone or '-' }}</td>
</tr>
<tr>
<td class="text-muted">Name bei Buchung</td>
<td>{{ booking.customer_name or '-' }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Kurs-Informationen -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-mortarboard me-2"></i>Kurs
</div>
<div class="card-body">
<table class="table table-dark table-borderless mb-0">
<tr>
<td class="text-muted" style="width: 140px;">Titel</td>
<td><strong>{{ booking.kurs_title or '-' }}</strong></td>
</tr>
<tr>
<td class="text-muted">WordPress Kurs-ID</td>
<td>#{{ booking.wp_kurs_id or '-' }}</td>
</tr>
<tr>
<td class="text-muted">Datum</td>
<td>{{ booking.formatted_date }}</td>
</tr>
<tr>
<td class="text-muted">Uhrzeit</td>
<td>{{ booking.formatted_time }}</td>
</tr>
<tr>
<td class="text-muted">Ort</td>
<td>{{ booking.kurs_location or '-' }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- sevDesk Integration -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-receipt me-2"></i>sevDesk Rechnung
</div>
<div class="card-body">
{% if booking.sevdesk_invoice_id %}
<table class="table table-dark table-borderless mb-0">
<tr>
<td class="text-muted" style="width: 140px;">Rechnungsnummer</td>
<td><strong>{{ booking.sevdesk_invoice_number or '-' }}</strong></td>
</tr>
<tr>
<td class="text-muted">sevDesk ID</td>
<td>#{{ booking.sevdesk_invoice_id }}</td>
</tr>
</table>
{% else %}
<p class="text-muted mb-0">Keine Rechnung verknuepft.</p>
{% endif %}
</div>
</div>
</div>
<!-- Timestamps -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-clock me-2"></i>Zeitstempel
</div>
<div class="card-body">
<table class="table table-dark table-borderless mb-0">
<tr>
<td class="text-muted" style="width: 160px;">Erstellt (WordPress)</td>
<td>{{ booking.wp_created_at.strftime('%d.%m.%Y um %H:%M') if booking.wp_created_at else '-' }}</td>
</tr>
<tr>
<td class="text-muted">Importiert</td>
<td>{{ booking.created_at.strftime('%d.%m.%Y um %H:%M') if booking.created_at else '-' }}</td>
</tr>
<tr>
<td class="text-muted">Letzter Sync</td>
<td>{{ booking.synced_at.strftime('%d.%m.%Y um %H:%M') if booking.synced_at else '-' }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Custom Fields -->
{% if booking.custom_fields %}
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-list-ul me-2"></i>Zusatzfelder
</div>
<div class="card-body">
<table class="table table-dark table-borderless table-sm mb-0">
{% for key, value in booking.custom_fields.items() %}
<tr>
<td class="text-muted" style="width: 280px; white-space: nowrap;">{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,464 @@
{% extends "admin/base.html" %}
{% block title %}Buchungen{% endblock %}
{% macro sort_header(column, label) %}
{% set current_dir = sort_dir if sort_by == column else 'desc' %}
{% set next_dir = 'asc' if current_dir == 'desc' else 'desc' %}
<a href="{{ url_for('admin.bookings', status=status_filter, customer_id=customer_filter, kurs=kurs_filter, q=search_query, year=year_filter, month=month_filter, sort=column, dir=next_dir, page=1, per_page=per_page) }}"
class="text-decoration-none text-light d-flex align-items-center">
{{ label }}
{% if sort_by == column %}
<i class="bi bi-caret-{{ 'up' if sort_dir == 'asc' else 'down' }}-fill ms-1 text-info"></i>
{% else %}
<i class="bi bi-caret-down ms-1 text-muted opacity-50"></i>
{% endif %}
</a>
{% endmacro %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1><i class="bi bi-calendar-check me-2"></i>Buchungen</h1>
<p class="text-muted mb-0">
{% if total_filtered != total_bookings %}
{{ total_filtered }} von {{ total_bookings }} Buchungen
{% else %}
{{ total_bookings }} Buchungen insgesamt
{% endif %}
</p>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('admin.bookings_import') }}" class="btn btn-outline-info">
<i class="bi bi-upload me-1"></i>Import
</a>
<a href="{{ url_for('admin.bookings_export', status=status_filter, customer_id=customer_filter, kurs=kurs_filter, q=search_query, year=year_filter, month=month_filter) }}" class="btn btn-success">
<i class="bi bi-download me-1"></i>CSV Export
</a>
<a href="{{ url_for('admin.bookings_sync') }}" class="btn btn-primary">
<i class="bi bi-arrow-repeat me-1"></i>Sync von WordPress
</a>
</div>
</div>
<!-- Stats Cards Row 1 -->
<div class="row g-3 mb-3">
<div class="col-6 col-md-3">
<div class="card bg-dark border-secondary h-100">
<div class="card-body text-center py-3">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="bi bi-calendar-check text-info fs-4 me-2"></i>
<h2 class="text-info mb-0">{{ total_bookings }}</h2>
</div>
<small class="text-muted">Buchungen gesamt</small>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card bg-dark border-secondary h-100">
<div class="card-body text-center py-3">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="bi bi-check-circle text-success fs-4 me-2"></i>
<h2 class="text-success mb-0">{{ confirmed_count }}</h2>
</div>
<small class="text-muted">Bestaetigt</small>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card bg-dark border-secondary h-100">
<div class="card-body text-center py-3">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="bi bi-clock text-warning fs-4 me-2"></i>
<h2 class="text-warning mb-0">{{ pending_count }}</h2>
</div>
<small class="text-muted">Ausstehend</small>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card bg-dark border-secondary h-100">
<div class="card-body text-center py-3">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="bi bi-x-circle text-danger fs-4 me-2"></i>
<h2 class="text-danger mb-0">{{ cancelled_count }}</h2>
</div>
<small class="text-muted">Storniert</small>
</div>
</div>
</div>
</div>
<!-- Stats Cards Row 2 -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card bg-dark border-secondary h-100">
<div class="card-body text-center py-3">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="bi bi-currency-euro text-success fs-4 me-2"></i>
<h2 class="text-success mb-0">{{ "{:,.2f}".format(total_revenue).replace(",", "X").replace(".", ",").replace("X", ".") }} EUR</h2>
</div>
<small class="text-muted">Gesamtumsatz (bestaetigt)</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-dark border-secondary h-100">
<div class="card-body text-center py-3">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="bi bi-calendar-plus text-primary fs-4 me-2"></i>
<h2 class="text-primary mb-0">{{ this_month_bookings }}</h2>
</div>
<small class="text-muted">Neue Buchungen diesen Monat</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-dark border-secondary h-100">
<div class="card-body text-center py-3">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="bi bi-percent text-info fs-4 me-2"></i>
<h2 class="text-info mb-0">{{ "%.1f"|format((confirmed_count / total_bookings * 100) if total_bookings > 0 else 0) }}%</h2>
</div>
<small class="text-muted">Bestaetigungsrate</small>
</div>
</div>
</div>
</div>
<!-- Charts Row -->
<div class="row g-3 mb-4">
<div class="col-md-8">
<div class="card bg-dark border-secondary">
<div class="card-header bg-dark border-secondary">
<h6 class="mb-0"><i class="bi bi-graph-up me-2"></i>Buchungen pro Monat</h6>
</div>
<div class="card-body">
<canvas id="bookingsChart" height="200"></canvas>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-dark border-secondary">
<div class="card-header bg-dark border-secondary">
<h6 class="mb-0"><i class="bi bi-trophy me-2"></i>Top 5 Kurse</h6>
</div>
<div class="card-body p-0">
<ul class="list-group list-group-flush bg-transparent">
{% for course in top_courses %}
<li class="list-group-item bg-dark border-secondary d-flex justify-content-between align-items-center">
<span class="text-light text-truncate" style="max-width: 200px;" title="{{ course.title }}">{{ course.title }}</span>
<span class="badge bg-primary rounded-pill">{{ course.count }}</span>
</li>
{% endfor %}
{% if not top_courses %}
<li class="list-group-item bg-dark border-secondary text-muted text-center">Keine Kurse</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
<!-- Filter und Suche -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<!-- Suche -->
<div class="col-md-3">
<label class="form-label">Suche</label>
<div class="input-group">
<span class="input-group-text bg-dark border-secondary"><i class="bi bi-search"></i></span>
<input type="text" name="q" class="form-control bg-dark border-secondary text-light"
placeholder="Buchungsnr., Name, E-Mail..."
value="{{ search_query }}">
</div>
</div>
<!-- Jahr Filter -->
<div class="col-md-1">
<label class="form-label">Jahr</label>
<select name="year" class="form-select bg-dark border-secondary text-light">
<option value="">Alle</option>
{% for year in available_years %}
<option value="{{ year }}" {% if year_filter == year|string %}selected{% endif %}>{{ year }}</option>
{% endfor %}
</select>
</div>
<!-- Monat Filter -->
<div class="col-md-2">
<label class="form-label">Monat</label>
<select name="month" class="form-select bg-dark border-secondary text-light">
<option value="">Alle</option>
<option value="1" {% if month_filter == '1' %}selected{% endif %}>Januar</option>
<option value="2" {% if month_filter == '2' %}selected{% endif %}>Februar</option>
<option value="3" {% if month_filter == '3' %}selected{% endif %}>Maerz</option>
<option value="4" {% if month_filter == '4' %}selected{% endif %}>April</option>
<option value="5" {% if month_filter == '5' %}selected{% endif %}>Mai</option>
<option value="6" {% if month_filter == '6' %}selected{% endif %}>Juni</option>
<option value="7" {% if month_filter == '7' %}selected{% endif %}>Juli</option>
<option value="8" {% if month_filter == '8' %}selected{% endif %}>August</option>
<option value="9" {% if month_filter == '9' %}selected{% endif %}>September</option>
<option value="10" {% if month_filter == '10' %}selected{% endif %}>Oktober</option>
<option value="11" {% if month_filter == '11' %}selected{% endif %}>November</option>
<option value="12" {% if month_filter == '12' %}selected{% endif %}>Dezember</option>
</select>
</div>
<!-- Status Filter -->
<div class="col-md-2">
<label class="form-label">Status</label>
<select name="status" class="form-select bg-dark border-secondary text-light">
<option value="">Alle</option>
<option value="confirmed" {% if status_filter == 'confirmed' %}selected{% endif %}>Bestaetigt</option>
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Ausstehend</option>
<option value="cancelled" {% if status_filter == 'cancelled' %}selected{% endif %}>Storniert</option>
<option value="cancel_requested" {% if status_filter == 'cancel_requested' %}selected{% endif %}>Storno angefr.</option>
</select>
</div>
<!-- Kunde Filter -->
<div class="col-md-2">
<label class="form-label">Kunde</label>
<select name="customer_id" class="form-select bg-dark border-secondary text-light">
<option value="">Alle Kunden</option>
{% for customer in customers_with_bookings %}
<option value="{{ customer.id }}" {% if customer_filter == customer.id|string %}selected{% endif %}>{{ customer.display_name|truncate(25) }}</option>
{% endfor %}
</select>
</div>
<!-- Kurs Filter -->
<div class="col-md-2">
<label class="form-label">Kurs</label>
<select name="kurs" class="form-select bg-dark border-secondary text-light">
<option value="">Alle Kurse</option>
{% for kurs in kurs_titles %}
<option value="{{ kurs }}" {% if kurs_filter == kurs %}selected{% endif %}>{{ kurs|truncate(30) }}</option>
{% endfor %}
</select>
</div>
<!-- Buttons -->
<div class="col-md-1 d-flex align-items-end">
<!-- Hidden sort params -->
<input type="hidden" name="sort" value="{{ sort_by }}">
<input type="hidden" name="dir" value="{{ sort_dir }}">
<button type="submit" class="btn btn-primary me-2">
<i class="bi bi-funnel"></i>
</button>
{% if status_filter or customer_filter or kurs_filter or search_query or year_filter or month_filter %}
<a href="{{ url_for('admin.bookings') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg"></i>
</a>
{% endif %}
</div>
</form>
</div>
</div>
<!-- Buchungstabelle -->
<div class="card">
<div class="card-body p-0">
{% if bookings %}
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th style="min-width: 120px;">{{ sort_header('booking_nr', 'Buchungsnr.') }}</th>
<th style="min-width: 180px;">{{ sort_header('customer', 'Kunde') }}</th>
<th style="min-width: 200px;">{{ sort_header('kurs', 'Kurs') }}</th>
<th style="min-width: 100px;">{{ sort_header('date', 'Datum') }}</th>
<th style="min-width: 80px;">{{ sort_header('price', 'Preis') }}</th>
<th style="min-width: 100px;">{{ sort_header('status', 'Status') }}</th>
<th class="text-end" style="min-width: 80px;">Aktionen</th>
</tr>
</thead>
<tbody>
{% for booking in bookings %}
<tr>
<td>
<strong>{{ booking.booking_number or '-' }}</strong>
<br>
<small class="text-muted">WP #{{ booking.wp_booking_id }}</small>
</td>
<td>
<a href="{{ url_for('admin.customer_detail', customer_id=booking.customer_id) }}" class="text-info">
{{ booking.customer.display_name }}
</a>
<br>
<small class="text-muted">{{ booking.customer.email }}</small>
</td>
<td>
{{ booking.kurs_title or '-' }}
{% if booking.kurs_location %}
<br><small class="text-muted"><i class="bi bi-geo-alt"></i> {{ booking.kurs_location }}</small>
{% endif %}
</td>
<td>
{{ booking.formatted_date }}
{% if booking.kurs_time %}
<br><small class="text-muted">{{ booking.formatted_time }}</small>
{% endif %}
</td>
<td>{{ booking.formatted_price }}</td>
<td>
<span class="badge bg-{{ booking.status_color }}">
{{ booking.status_display }}
</span>
</td>
<td class="text-end">
<a href="{{ url_for('admin.booking_detail', booking_id=booking.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<div class="card-footer bg-dark border-secondary">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted">
Seite {{ page }} von {{ total_pages }}
({{ total_filtered }} Eintraege)
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<!-- First -->
<li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link bg-dark border-secondary"
href="{{ url_for('admin.bookings', status=status_filter, customer_id=customer_filter, kurs=kurs_filter, q=search_query, year=year_filter, month=month_filter, sort=sort_by, dir=sort_dir, page=1, per_page=per_page) }}">
<i class="bi bi-chevron-double-left"></i>
</a>
</li>
<!-- Previous -->
<li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link bg-dark border-secondary"
href="{{ url_for('admin.bookings', status=status_filter, customer_id=customer_filter, kurs=kurs_filter, q=search_query, year=year_filter, month=month_filter, sort=sort_by, dir=sort_dir, page=page-1, per_page=per_page) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
<!-- Page numbers -->
{% set start_page = [page - 2, 1]|max %}
{% set end_page = [page + 2, total_pages]|min %}
{% if start_page > 1 %}
<li class="page-item disabled"><span class="page-link bg-dark border-secondary">...</span></li>
{% endif %}
{% for p in range(start_page, end_page + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link {% if p != page %}bg-dark border-secondary{% endif %}"
href="{{ url_for('admin.bookings', status=status_filter, customer_id=customer_filter, kurs=kurs_filter, q=search_query, year=year_filter, month=month_filter, sort=sort_by, dir=sort_dir, page=p, per_page=per_page) }}">
{{ p }}
</a>
</li>
{% endfor %}
{% if end_page < total_pages %}
<li class="page-item disabled"><span class="page-link bg-dark border-secondary">...</span></li>
{% endif %}
<!-- Next -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link bg-dark border-secondary"
href="{{ url_for('admin.bookings', status=status_filter, customer_id=customer_filter, kurs=kurs_filter, q=search_query, year=year_filter, month=month_filter, sort=sort_by, dir=sort_dir, page=page+1, per_page=per_page) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
<!-- Last -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link bg-dark border-secondary"
href="{{ url_for('admin.bookings', status=status_filter, customer_id=customer_filter, kurs=kurs_filter, q=search_query, year=year_filter, month=month_filter, sort=sort_by, dir=sort_dir, page=total_pages, per_page=per_page) }}">
<i class="bi bi-chevron-double-right"></i>
</a>
</li>
</ul>
</nav>
<!-- Per Page -->
<div>
<select class="form-select form-select-sm bg-dark border-secondary text-light" style="width: auto;"
onchange="window.location.href='{{ url_for('admin.bookings', status=status_filter, customer_id=customer_filter, kurs=kurs_filter, q=search_query, year=year_filter, month=month_filter, sort=sort_by, dir=sort_dir, page=1) }}&per_page=' + this.value">
<option value="25" {% if per_page == 25 %}selected{% endif %}>25 pro Seite</option>
<option value="50" {% if per_page == 50 %}selected{% endif %}>50 pro Seite</option>
<option value="100" {% if per_page == 100 %}selected{% endif %}>100 pro Seite</option>
<option value="200" {% if per_page == 200 %}selected{% endif %}>200 pro Seite</option>
</select>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-calendar-x text-muted" style="font-size: 3rem;"></i>
<p class="text-muted mt-3 mb-0">Keine Buchungen gefunden.</p>
{% if search_query or status_filter or customer_filter or kurs_filter or year_filter or month_filter %}
<a href="{{ url_for('admin.bookings') }}" class="btn btn-outline-secondary mt-3">
<i class="bi bi-x-lg me-1"></i>Filter zuruecksetzen
</a>
{% else %}
<a href="{{ url_for('admin.bookings_sync') }}" class="btn btn-primary mt-3">
<i class="bi bi-arrow-repeat me-1"></i>Buchungen synchronisieren
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('bookingsChart');
if (ctx) {
const bookingsData = {{ bookings_per_month | tojson | safe }};
new Chart(ctx, {
type: 'bar',
data: {
labels: bookingsData.map(d => d.month),
datasets: [{
label: 'Buchungen',
data: bookingsData.map(d => d.count),
backgroundColor: 'rgba(13, 202, 240, 0.6)',
borderColor: 'rgba(13, 202, 240, 1)',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: '#6c757d',
stepSize: 1
},
grid: {
color: 'rgba(108, 117, 125, 0.2)'
}
},
x: {
ticks: {
color: '#6c757d'
},
grid: {
display: false
}
}
}
}
});
}
});
</script>
{% endblock extra_js %}

View File

@@ -0,0 +1,221 @@
{% extends "admin/base.html" %}
{% block title %}Buchungen importieren{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-2">
<li class="breadcrumb-item"><a href="{{ url_for('admin.bookings') }}" class="text-info">Buchungen</a></li>
<li class="breadcrumb-item active">Import</li>
</ol>
</nav>
<h1><i class="bi bi-upload me-2"></i>Buchungen importieren</h1>
</div>
<div>
<a href="{{ url_for('admin.bookings') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Zurueck
</a>
</div>
</div>
<div class="row g-4">
<!-- Import Form -->
<div class="col-lg-7">
<div class="card">
<div class="card-header">
<i class="bi bi-file-earmark-arrow-up me-2"></i>Datei hochladen
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<div class="mb-4">
<label for="import_file" class="form-label">Import-Datei</label>
<input type="file" class="form-control bg-dark text-light border-secondary"
id="import_file" name="import_file"
accept=".csv,.json,.xlsx"
required>
<div class="form-text text-muted">
Unterstuetzte Formate: CSV, JSON (MEC-Format), Excel (.xlsx)
</div>
</div>
<div class="mb-4">
<label for="delimiter" class="form-label">CSV-Trennzeichen</label>
<select class="form-select bg-dark text-light border-secondary" id="delimiter" name="delimiter">
<option value=";" selected>Semikolon (;) - Standard fuer deutsches Excel</option>
<option value=",">Komma (,) - Internationales Format</option>
<option value="\t">Tabulator</option>
</select>
</div>
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="overwrite" name="overwrite">
<label class="form-check-label" for="overwrite">
<strong>Existierende Buchungen ueberschreiben</strong>
</label>
<div class="form-text text-muted">
Wenn deaktiviert, werden Buchungen uebersprungen, die bereits existieren (empfohlen).
</div>
</div>
</div>
<div class="alert alert-info d-flex align-items-center">
<i class="bi bi-shield-check me-2 fs-5"></i>
<div>
<strong>Ueberschreibungsschutz:</strong> Standardmaessig werden nur neue Buchungen importiert.
Existierende Buchungen (erkannt an WordPress-ID oder Buchungsnummer) werden uebersprungen.
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-success btn-lg">
<i class="bi bi-upload me-2"></i>Import starten
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Info & Templates -->
<div class="col-lg-5">
<!-- Stats -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>Aktuelle Daten
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted">Buchungen im System:</span>
<span class="badge bg-primary fs-6">{{ total_bookings }}</span>
</div>
</div>
</div>
<!-- Templates -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-file-earmark-text me-2"></i>Import-Vorlagen
</div>
<div class="card-body">
<p class="text-muted mb-3">
Laden Sie eine Vorlage herunter, um das korrekte Format zu sehen:
</p>
<div class="d-grid gap-2">
<a href="{{ url_for('admin.bookings_import_template', format='csv') }}"
class="btn btn-outline-info">
<i class="bi bi-filetype-csv me-2"></i>CSV-Vorlage herunterladen
</a>
<a href="{{ url_for('admin.bookings_import_template', format='json') }}"
class="btn btn-outline-info">
<i class="bi bi-filetype-json me-2"></i>JSON-Vorlage herunterladen
</a>
</div>
</div>
</div>
<!-- Field Mapping -->
<div class="card">
<div class="card-header">
<i class="bi bi-list-columns me-2"></i>Unterstuetzte Felder
</div>
<div class="card-body">
<table class="table table-dark table-sm mb-0">
<thead>
<tr>
<th>Spaltenname</th>
<th>Beschreibung</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>email</code></td>
<td>Kunden-E-Mail <span class="badge bg-danger">Pflicht</span></td>
</tr>
<tr>
<td><code>buchungsnummer</code></td>
<td>Eindeutige Buchungsnummer</td>
</tr>
<tr>
<td><code>id</code> / <code>wp_id</code></td>
<td>WordPress Buchungs-ID</td>
</tr>
<tr>
<td><code>kurs</code> / <code>kurs_title</code></td>
<td>Kurs-Titel</td>
</tr>
<tr>
<td><code>datum</code> / <code>date</code></td>
<td>Kursdatum (YYYY-MM-DD oder DD.MM.YYYY)</td>
</tr>
<tr>
<td><code>uhrzeit</code> / <code>time</code></td>
<td>Kurszeit (HH:MM)</td>
</tr>
<tr>
<td><code>ort</code> / <code>location</code></td>
<td>Kursort</td>
</tr>
<tr>
<td><code>status</code></td>
<td>confirmed/pending/cancelled</td>
</tr>
<tr>
<td><code>preis</code> / <code>price</code></td>
<td>Gesamtpreis (z.B. 150,00 oder 150.00)</td>
</tr>
<tr>
<td><code>name</code></td>
<td>Kundenname bei Buchung</td>
</tr>
<tr>
<td><code>telefon</code> / <code>phone</code></td>
<td>Telefonnummer</td>
</tr>
</tbody>
</table>
<p class="text-muted mt-3 mb-0 small">
<i class="bi bi-info-circle me-1"></i>
Nicht erkannte Spalten werden automatisch als Zusatzfelder importiert.
</p>
</div>
</div>
</div>
</div>
<!-- MEC Format Info -->
<div class="card mt-4">
<div class="card-header">
<i class="bi bi-wordpress me-2"></i>MEC WordPress Export-Format
</div>
<div class="card-body">
<p class="text-muted">
Beim Import aus dem MEC (Modern Events Calendar) System wird folgendes JSON-Format unterstuetzt:
</p>
<pre class="bg-dark p-3 rounded border border-secondary"><code>{
"bookings": [
{
"id": 1234,
"number": "KB-2024-0001",
"customer": {
"email": "kunde@example.com",
"name": "Max Mustermann",
"phone": "+43 123 456789"
},
"kurs_title": "Reitkurs Anfaenger",
"kurs_date": "2024-01-15",
"kurs_time": "10:00",
"kurs_location": "Reiterhof Wien",
"status": "confirmed",
"price": 150.00,
"custom_fields": {
"Pferdename": "Blitz",
"Reitniveau": "Anfaenger"
}
}
]
}</code></pre>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,134 @@
{% extends "admin/base.html" %}
{% block title %}Buchungen synchronisieren{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-2">
<li class="breadcrumb-item"><a href="{{ url_for('admin.bookings') }}" class="text-info">Buchungen</a></li>
<li class="breadcrumb-item active">Synchronisieren</li>
</ol>
</nav>
<h1><i class="bi bi-arrow-repeat me-2"></i>Buchungen synchronisieren</h1>
</div>
<a href="{{ url_for('admin.bookings') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Zurueck
</a>
</div>
<!-- Status Info -->
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>Sync-Status
</div>
<div class="card-body">
<table class="table table-dark table-borderless mb-0">
<tr>
<td class="text-muted">Buchungen im Portal</td>
<td><strong>{{ total_bookings }}</strong></td>
</tr>
<tr>
<td class="text-muted">Kunden gesamt</td>
<td><strong>{{ customers|length }}</strong></td>
</tr>
<tr>
<td class="text-muted">Letzter Sync</td>
<td>
{% if last_sync_time %}
{{ last_sync_time.strftime('%d.%m.%Y um %H:%M') }}
{% else %}
<span class="text-muted">Noch nie synchronisiert</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-question-circle me-2"></i>Hinweis
</div>
<div class="card-body">
<p class="mb-2">Die Synchronisation ruft alle Buchungen aus WordPress ab und speichert sie im Portal.</p>
<ul class="mb-0 small text-muted">
<li>Neue Buchungen werden erstellt</li>
<li>Bestehende Buchungen werden aktualisiert</li>
<li>Buchungen werden per E-Mail zugeordnet</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Sync Forms -->
<div class="row g-4">
<!-- Einzelnen Kunden synchronisieren -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-info bg-opacity-10">
<i class="bi bi-person me-2"></i>Einzelnen Kunden synchronisieren
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label class="form-label">Kunde auswaehlen</label>
<select name="customer_id" class="form-select bg-dark border-secondary text-light" required>
<option value="">-- Kunde waehlen --</option>
{% for customer in customers %}
<option value="{{ customer.id }}">
{{ customer.display_name }} ({{ customer.email }})
</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-info">
<i class="bi bi-arrow-repeat me-1"></i>Kunden synchronisieren
</button>
</form>
</div>
</div>
</div>
<!-- Alle Kunden synchronisieren -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-primary bg-opacity-10">
<i class="bi bi-people me-2"></i>Alle Kunden synchronisieren
</div>
<div class="card-body">
<p class="text-muted mb-3">
Synchronisiert die Buchungen aller {{ customers|length }} Kunden aus WordPress.
</p>
<div class="alert alert-warning mb-3">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Achtung:</strong> Dies kann bei vielen Kunden einige Zeit dauern.
</div>
<form method="POST">
<button type="submit" class="btn btn-primary">
<i class="bi bi-arrow-repeat me-1"></i>Alle synchronisieren
</button>
</form>
</div>
</div>
</div>
</div>
<!-- WordPress-Konfiguration pruefen -->
<div class="card mt-4">
<div class="card-header">
<i class="bi bi-gear me-2"></i>WordPress-Verbindung
</div>
<div class="card-body">
<p class="mb-2">Die Synchronisation nutzt die WordPress REST API unter:</p>
<code class="d-block p-2 bg-dark rounded mb-3">GET /wp-json/kurs-booking/v1/bookings?email=...</code>
<a href="{{ url_for('admin.settings_wordpress') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-gear me-1"></i>WordPress-Einstellungen pruefen
</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,88 @@
{% extends "admin/base.html" %}
{% block title %}Neuer Admin{% endblock %}
{% block content %}
<div class="mb-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('admin.admins') }}" class="text-info">Administratoren</a></li>
<li class="breadcrumb-item active">Neuer Admin</li>
</ol>
</nav>
<h1><i class="bi bi-shield-plus me-2"></i>Neuer Admin</h1>
<p class="text-muted">Erstellen Sie einen neuen Administrator-Account</p>
</div>
<div class="row">
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('admin.create_admin') }}">
<div class="mb-3">
<label for="username" class="form-label">Benutzername *</label>
<input type="text" class="form-control" id="username" name="username"
required autocomplete="off" pattern="[a-zA-Z0-9_]+"
title="Nur Buchstaben, Zahlen und Unterstriche">
<div class="form-text">Nur Buchstaben, Zahlen und Unterstriche erlaubt</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Anzeigename *</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">E-Mail</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort *</label>
<input type="password" class="form-control" id="password" name="password"
required minlength="8">
<div class="form-text">Mindestens 8 Zeichen</div>
</div>
<div class="mb-4">
<label for="password_confirm" class="form-label">Passwort bestaetigen *</label>
<input type="password" class="form-control" id="password_confirm" name="password_confirm"
required minlength="8">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="bi bi-check-lg me-1"></i>
Admin erstellen
</button>
<a href="{{ url_for('admin.admins') }}" class="btn btn-outline-secondary">
Abbrechen
</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card bg-dark border-secondary">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>
Hinweise
</div>
<div class="card-body">
<ul class="mb-0">
<li class="mb-2">
<strong>Benutzername</strong> wird fuer die Anmeldung verwendet und kann spaeter nicht geaendert werden.
</li>
<li class="mb-2">
<strong>Passwort</strong> wird sicher verschluesselt gespeichert.
</li>
<li>
Neue Admins sind sofort <span class="badge bg-success">Aktiv</span> und koennen sich einloggen.
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,273 @@
{% extends "admin/base.html" %}
{% block title %}Kunde: {{ customer.display_name }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-2">
<li class="breadcrumb-item"><a href="{{ url_for('admin.customers') }}" class="text-info">Kunden</a></li>
<li class="breadcrumb-item active">{{ customer.display_name }}</li>
</ol>
</nav>
<h1><i class="bi bi-person me-2"></i>{{ customer.display_name }}</h1>
</div>
<div>
<form method="POST" action="{{ url_for('admin.customer_sync', customer_id=customer.id) }}" class="d-inline">
<button type="submit" class="btn btn-outline-info" title="Von WordPress synchronisieren">
<i class="bi bi-arrow-repeat me-1"></i>Sync
</button>
</form>
<a href="{{ url_for('admin.customer_edit', customer_id=customer.id) }}" class="btn btn-warning ms-2">
<i class="bi bi-pencil me-1"></i>Bearbeiten
</a>
<a href="{{ url_for('admin.customers') }}" class="btn btn-outline-secondary ms-2">
<i class="bi bi-arrow-left me-1"></i>Zurueck
</a>
</div>
</div>
<!-- Prepare custom_fields data early for ordering -->
{% set custom_fields = customer.get_custom_fields() %}
{% set wp_labels = {} %}
{% if wp_schema and wp_schema.custom_fields %}
{% for field in wp_schema.custom_fields %}
{% if field.name %}
{% set saved = field_config.wp_fields.get(field.name, {}) %}
{% set _ = wp_labels.update({field.name: saved.get('label', field.label)}) %}
{% endif %}
{% endfor %}
{% endif %}
{# Sprint 13: Labels + Hidden aus customer_view_config #}
{% set view_labels = customer_view_config.field_labels if customer_view_config else {} %}
{% set hidden_fields = customer_view_config.hidden_fields if customer_view_config else [] %}
{# Skip fields already shown in Kontaktdaten section #}
{% set skip_fields = [
'mec_field_2', 'mec_field_3', 'mec_field_7', 'mec_field_8',
'name', 'vorname', 'nachname', 'first_name', 'last_name',
'email', 'e_mail', 'e-mail',
'telefon', 'phone', 'mobil', 'mobile', 'tel',
'adresse', 'strasse', 'straße', 'street', 'address_street',
'plz', 'postleitzahl', 'zip', 'address_zip',
'ort', 'stadt', 'city', 'address_city'
] %}
<div class="row g-4">
<!-- 1. Kontaktdaten -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-person-vcard me-2"></i>Kontaktdaten
</div>
<div class="card-body">
<table class="table table-dark table-borderless mb-0">
<tr>
<td class="text-muted" style="width: 140px;">E-Mail</td>
<td>
<a href="mailto:{{ customer.email }}" class="text-info">{{ customer.email }}</a>
</td>
</tr>
<tr>
<td class="text-muted">Telefon</td>
<td>
{% if customer.display_phone %}
<a href="tel:{{ customer.display_phone }}" class="text-info">{{ customer.display_phone }}</a>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
</tr>
<tr>
<td class="text-muted">Adresse</td>
<td>
{% set addr = customer.display_address %}
{% if addr.street %}
{{ addr.street }}<br>
{{ addr.zip }} {{ addr.city }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- 2. Personendaten (wichtig - direkt nach Kontaktdaten) -->
{% if custom_fields %}
{% set has_data = namespace(value=false) %}
{% for key, value in custom_fields.items() %}
{% set saved_field = field_config.wp_fields.get(key, {}) %}
{# Sprint 13: Check both field_config.visible AND hidden_fields list #}
{% set is_hidden = key in hidden_fields %}
{% if key not in skip_fields and saved_field.get('visible', true) and not is_hidden %}
{% set has_data.value = true %}
{% endif %}
{% endfor %}
{% if has_data.value %}
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-person-lines-fill me-2"></i>Personendaten
</div>
<div class="card-body">
<table class="table table-dark table-borderless table-sm mb-0">
{% for key, value in custom_fields.items() %}
{% set saved_field = field_config.wp_fields.get(key, {}) %}
{% set is_visible = saved_field.get('visible', true) %}
{% set is_hidden = key in hidden_fields %}
{% if key not in skip_fields and is_visible and not is_hidden %}
<tr>
<td class="text-muted" style="width: 180px;">
{# Sprint 13: Priority: view_labels > saved_field.label > wp_labels > key #}
{{ view_labels.get(key) or saved_field.get('label') or wp_labels.get(key) or key }}
</td>
<td>{{ value }}</td>
</tr>
{% endif %}
{% endfor %}
</table>
</div>
</div>
</div>
{% endif %}
{% endif %}
<!-- 3. E-Mail-Einstellungen -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-envelope me-2"></i>E-Mail-Einstellungen
</div>
<div class="card-body">
<div class="d-flex align-items-center mb-2">
{% if customer.email_notifications %}
<i class="bi bi-check-circle text-success me-2"></i>
{% else %}
<i class="bi bi-x-circle text-danger me-2"></i>
{% endif %}
<span>Buchungsbestaetigungen</span>
</div>
<div class="d-flex align-items-center mb-2">
{% if customer.email_reminders %}
<i class="bi bi-check-circle text-success me-2"></i>
{% else %}
<i class="bi bi-x-circle text-danger me-2"></i>
{% endif %}
<span>Kurserinnerungen</span>
</div>
<div class="d-flex align-items-center mb-2">
{% if customer.email_invoices %}
<i class="bi bi-check-circle text-success me-2"></i>
{% else %}
<i class="bi bi-x-circle text-danger me-2"></i>
{% endif %}
<span>Rechnungen</span>
</div>
<div class="d-flex align-items-center">
{% if customer.email_marketing %}
<i class="bi bi-check-circle text-success me-2"></i>
{% else %}
<i class="bi bi-x-circle text-danger me-2"></i>
{% endif %}
<span>Newsletter & Marketing</span>
</div>
</div>
</div>
</div>
<!-- 4. Konto-Infos -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>Konto-Informationen
</div>
<div class="card-body">
<table class="table table-dark table-borderless mb-0">
<tr>
<td class="text-muted" style="width: 140px;">Kunden-ID</td>
<td>#{{ customer.id }}</td>
</tr>
<tr>
<td class="text-muted">WP User-ID</td>
<td>{{ customer.wp_user_id or '-' }}</td>
</tr>
<tr>
<td class="text-muted">Registriert</td>
<td>{{ customer.created_at.strftime('%d.%m.%Y um %H:%M') if customer.created_at else '-' }}</td>
</tr>
<tr>
<td class="text-muted">Letzter Login</td>
<td>
{% if customer.last_login_at %}
{{ customer.last_login_at.strftime('%d.%m.%Y um %H:%M') }}
{% else %}
<span class="text-muted">Noch nie eingeloggt</span>
{% endif %}
</td>
</tr>
<tr>
<td class="text-muted">Aktualisiert</td>
<td>{{ customer.updated_at.strftime('%d.%m.%Y um %H:%M') if customer.updated_at else '-' }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<!-- Gefahrenzone -->
<div class="card border-danger mt-4">
<div class="card-header bg-danger bg-opacity-25 text-danger">
<i class="bi bi-exclamation-triangle me-2"></i>Gefahrenzone
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>Kunden loeschen</strong>
<p class="text-muted mb-0 small">
Loescht das Kundenkonto und alle zugehoerigen Daten unwiderruflich.
</p>
</div>
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="bi bi-trash me-1"></i>Kunde loeschen
</button>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content bg-dark">
<div class="modal-header border-secondary">
<h5 class="modal-title">Kunde loeschen</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Moechten Sie diesen Kunden wirklich loeschen?</p>
<p class="mb-0">
<strong>{{ customer.display_name }}</strong><br>
<span class="text-muted">{{ customer.email }}</span>
</p>
<div class="alert alert-danger mt-3 mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
Diese Aktion kann nicht rueckgaengig gemacht werden!
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<form method="POST" action="{{ url_for('admin.customer_delete', customer_id=customer.id) }}" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash me-1"></i>Endgueltig loeschen
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends "admin/base.html" %}
{% block title %}Kunde bearbeiten: {{ customer.display_name }}{% endblock %}
{% block content %}
{# Sprint 12: All customer data comes from custom_fields #}
{% set fields = customer.get_custom_fields() %}
{% set addr = customer.display_address %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-2">
<li class="breadcrumb-item"><a href="{{ url_for('admin.customers') }}" class="text-info">Kunden</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('admin.customer_detail', customer_id=customer.id) }}" class="text-info">{{ customer.display_name }}</a></li>
<li class="breadcrumb-item active">Bearbeiten</li>
</ol>
</nav>
<h1><i class="bi bi-pencil me-2"></i>Kunde bearbeiten</h1>
</div>
</div>
<form method="POST" action="{{ url_for('admin.customer_edit', customer_id=customer.id) }}">
<div class="row g-4">
<!-- Kontaktdaten -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-person-vcard me-2"></i>Kontaktdaten
</div>
<div class="card-body">
<div class="mb-3">
<label for="email" class="form-label">E-Mail</label>
<input type="email" class="form-control bg-dark border-secondary text-light"
id="email" value="{{ customer.email }}" disabled>
<div class="form-text">E-Mail kann nicht geaendert werden (Identifikator)</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control bg-dark border-secondary text-light"
id="name" name="name" value="{{ fields.get('name', '') }}" required>
</div>
<div class="mb-3">
<label for="phone" class="form-label">Telefon</label>
<input type="tel" class="form-control bg-dark border-secondary text-light"
id="phone" name="phone" value="{{ customer.display_phone }}"
placeholder="+43 ...">
</div>
</div>
</div>
</div>
<!-- Adresse -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-geo-alt me-2"></i>Adresse
</div>
<div class="card-body">
<div class="mb-3">
<label for="address_street" class="form-label">Strasse</label>
<input type="text" class="form-control bg-dark border-secondary text-light"
id="address_street" name="address_street"
value="{{ addr.street }}">
</div>
<div class="row">
<div class="col-4">
<div class="mb-3">
<label for="address_zip" class="form-label">PLZ</label>
<input type="text" class="form-control bg-dark border-secondary text-light"
id="address_zip" name="address_zip"
value="{{ addr.zip }}">
</div>
</div>
<div class="col-8">
<div class="mb-3">
<label for="address_city" class="form-label">Ort</label>
<input type="text" class="form-control bg-dark border-secondary text-light"
id="address_city" name="address_city"
value="{{ addr.city }}">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- E-Mail-Einstellungen -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-envelope me-2"></i>E-Mail-Einstellungen
</div>
<div class="card-body">
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="email_notifications"
name="email_notifications"
{{ 'checked' if customer.email_notifications else '' }}>
<label class="form-check-label" for="email_notifications">
Buchungsbestaetigungen
</label>
<div class="form-text">Erhaelt E-Mails bei neuen Buchungen</div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="email_reminders"
name="email_reminders"
{{ 'checked' if customer.email_reminders else '' }}>
<label class="form-check-label" for="email_reminders">
Kurserinnerungen
</label>
<div class="form-text">Erhaelt Erinnerungen vor Kursbeginn</div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="email_invoices"
name="email_invoices"
{{ 'checked' if customer.email_invoices else '' }}>
<label class="form-check-label" for="email_invoices">
Rechnungen
</label>
<div class="form-text">Erhaelt Rechnungen per E-Mail</div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="email_marketing"
name="email_marketing"
{{ 'checked' if customer.email_marketing else '' }}>
<label class="form-check-label" for="email_marketing">
Newsletter & Marketing
</label>
<div class="form-text">Erhaelt Newsletter und Angebote</div>
</div>
</div>
</div>
</div>
<!-- Konto-Infos (nur Anzeige) -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>Konto-Informationen
</div>
<div class="card-body">
<table class="table table-dark table-borderless table-sm mb-0">
<tr>
<td class="text-muted" style="width: 140px;">Kunden-ID</td>
<td>#{{ customer.id }}</td>
</tr>
<tr>
<td class="text-muted">WP User-ID</td>
<td>{{ customer.wp_user_id or '-' }}</td>
</tr>
<tr>
<td class="text-muted">Registriert</td>
<td>{{ customer.created_at.strftime('%d.%m.%Y um %H:%M') if customer.created_at else '-' }}</td>
</tr>
<tr>
<td class="text-muted">Letzter Login</td>
<td>
{% if customer.last_login_at %}
{{ customer.last_login_at.strftime('%d.%m.%Y um %H:%M') }}
{% else %}
<span class="text-muted">Noch nie eingeloggt</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Weitere Felder (aus custom_fields, ohne Kernfelder) -->
{% set skip_core = ['name', 'phone', 'address_street', 'address_zip', 'address_city', 'email'] %}
{% set extra_fields = {} %}
{% for key, value in fields.items() if key not in skip_core %}
{% set _ = extra_fields.update({key: value}) %}
{% endfor %}
{% if extra_fields %}
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-person-lines-fill me-2"></i>Weitere Felder
</div>
<div class="card-body">
{% for key, value in extra_fields.items() %}
{% set saved_field = field_config.wp_fields.get(key, {}) %}
{% set label = saved_field.get('label') or key %}
<div class="mb-3">
<label for="custom_{{ key }}" class="form-label">{{ label }}</label>
<input type="text" class="form-control bg-dark border-secondary text-light"
id="custom_{{ key }}" name="custom_{{ key }}"
value="{{ value }}">
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
<!-- Aktionen -->
<div class="d-flex justify-content-between mt-4">
<a href="{{ url_for('admin.customer_detail', customer_id=customer.id) }}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg me-1"></i>Abbrechen
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>Aenderungen speichern
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,157 @@
{% extends "admin/base.html" %}
{% block title %}Kunden{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1><i class="bi bi-people me-2"></i>Kunden</h1>
<p class="text-muted mb-0">{{ customers|length }} Kunden{% if search %} gefunden fuer "{{ search }}"{% endif %}</p>
</div>
<div class="btn-group">
<a href="{{ url_for('admin.customers_export') }}" class="btn btn-outline-success">
<i class="bi bi-download me-1"></i>CSV Export
</a>
<a href="{{ url_for('admin.customers_import') }}" class="btn btn-outline-info">
<i class="bi bi-upload me-1"></i>CSV Import
</a>
</div>
</div>
<!-- Suchfeld -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" action="{{ url_for('admin.customers') }}" class="row g-3">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text bg-dark border-secondary">
<i class="bi bi-search"></i>
</span>
<input type="text" name="search" class="form-control bg-dark border-secondary text-light"
placeholder="Name, E-Mail oder Telefon suchen..."
value="{{ search or '' }}">
</div>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary me-2">
<i class="bi bi-search me-1"></i>Suchen
</button>
{% if search %}
<a href="{{ url_for('admin.customers') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg me-1"></i>Filter loeschen
</a>
{% endif %}
</div>
</form>
</div>
</div>
<!-- Kundentabelle -->
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Name</th>
<th>E-Mail</th>
<th>Telefon</th>
<th>Registriert</th>
<th>Letzter Login</th>
<th class="text-end">Aktionen</th>
</tr>
</thead>
<tbody>
{% for customer in customers %}
<tr>
<td>
<a href="{{ url_for('admin.customer_detail', customer_id=customer.id) }}"
class="text-light text-decoration-none fw-medium">
{{ customer.display_name }}
</a>
</td>
<td>
<a href="mailto:{{ customer.email }}" class="text-info text-decoration-none">
{{ customer.email }}
</a>
</td>
<td>{{ customer.display_phone or '-' }}</td>
<td>{{ customer.created_at.strftime('%d.%m.%Y') if customer.created_at else '-' }}</td>
<td>
{% if customer.last_login_at %}
{{ customer.last_login_at.strftime('%d.%m.%Y %H:%M') }}
{% else %}
<span class="text-muted">Noch nie</span>
{% endif %}
</td>
<td class="text-end">
<div class="btn-group btn-group-sm">
<a href="{{ url_for('admin.customer_detail', customer_id=customer.id) }}"
class="btn btn-outline-info" title="Details">
<i class="bi bi-eye"></i>
</a>
<a href="{{ url_for('admin.customer_edit', customer_id=customer.id) }}"
class="btn btn-outline-warning" title="Bearbeiten">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-outline-danger" title="Loeschen"
data-bs-toggle="modal" data-bs-target="#deleteModal{{ customer.id }}">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal{{ customer.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content bg-dark">
<div class="modal-header border-secondary">
<h5 class="modal-title">Kunde loeschen</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-start">
<p>Moechten Sie diesen Kunden wirklich loeschen?</p>
<p class="mb-0">
<strong>{{ customer.display_name }}</strong><br>
<span class="text-muted">{{ customer.email }}</span>
</p>
<div class="alert alert-danger mt-3 mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
Diese Aktion kann nicht rueckgaengig gemacht werden!
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<form method="POST" action="{{ url_for('admin.customer_delete', customer_id=customer.id) }}" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash me-1"></i>Loeschen
</button>
</form>
</div>
</div>
</div>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center text-muted py-4">
{% if search %}
Keine Kunden fuer "{{ search }}" gefunden
{% else %}
Keine Kunden registriert
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% if customers|length > 20 %}
<div class="mt-3 text-muted small">
<i class="bi bi-info-circle me-1"></i>
Zeige {{ customers|length }} Kunden. Nutzen Sie die Suche, um die Liste zu filtern.
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,112 @@
{% extends "admin/base.html" %}
{% block title %}Kunden importieren{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-2">
<li class="breadcrumb-item"><a href="{{ url_for('admin.customers') }}" class="text-info">Kunden</a></li>
<li class="breadcrumb-item active">Importieren</li>
</ol>
</nav>
<h1><i class="bi bi-upload me-2"></i>Kunden importieren</h1>
</div>
<a href="{{ url_for('admin.settings_csv') }}" class="btn btn-outline-info">
<i class="bi bi-gear me-1"></i>Felder konfigurieren
</a>
</div>
<div class="row">
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-file-earmark-arrow-up me-2"></i>CSV-Datei hochladen
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="csv_file" class="form-label">CSV-Datei auswaehlen</label>
<input type="file" class="form-control" id="csv_file" name="csv_file"
accept=".csv" required>
<div class="form-text">
Trennzeichen: <strong>{{ csv_config.delimiter if csv_config.delimiter != '\t' else 'Tab' }}</strong>
(konfigurierbar unter <a href="{{ url_for('admin.settings_csv') }}" class="text-info">CSV-Einstellungen</a>)
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="bi bi-upload me-1"></i>Importieren
</button>
<a href="{{ url_for('admin.customers') }}" class="btn btn-outline-secondary">
Abbrechen
</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-info-circle me-2"></i>Erwartete Spalten</span>
<span class="badge bg-info">{{ csv_config.export_fields|selectattr('enabled')|list|length }} aktiv</span>
</div>
<div class="card-body">
<p class="text-muted small">Die CSV-Datei sollte folgende Spaltennamen verwenden:</p>
<table class="table table-dark table-sm">
<thead>
<tr>
<th>Spaltenname</th>
<th>Feld</th>
<th class="text-center">Pflicht</th>
</tr>
</thead>
<tbody>
{% for field in csv_config.export_fields %}
{% if field.enabled %}
<tr>
<td><code class="text-warning">{{ field.label }}</code></td>
<td class="text-muted small">{{ field.key }}</td>
<td class="text-center">
{% if field.key == 'email' or field.key == 'name' %}
<span class="badge bg-danger">Ja</span>
{% else %}
<span class="badge bg-secondary">Nein</span>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<div class="alert alert-info mb-0 mt-3">
<i class="bi bi-lightbulb me-2"></i>
<strong>Tipp:</strong>
<a href="{{ url_for('admin.customers_export') }}" class="text-info">Exportieren</a>
Sie zuerst die bestehenden Kunden als Vorlage.
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<i class="bi bi-exclamation-triangle me-2"></i>Hinweise
</div>
<div class="card-body">
<ul class="mb-0">
<li>Bestehende Kunden werden anhand der <strong>E-Mail</strong> erkannt und aktualisiert</li>
<li>Neue E-Mail-Adressen werden als neue Kunden angelegt</li>
<li>Die <strong>ID</strong>-Spalte wird beim Import ignoriert</li>
<li>Ja/Nein-Felder akzeptieren: <code>Ja</code>, <code>1</code>, <code>true</code>, <code>yes</code></li>
<li>Encoding: UTF-8 (mit oder ohne BOM)</li>
<li>Leere Zellen ueberschreiben keine bestehenden Werte</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,239 @@
{% extends "admin/base.html" %}
{% block title %}Feld-Konfiguration{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1><i class="bi bi-sliders me-2"></i>Feld-Konfiguration</h1>
<p class="text-muted">Legen Sie fest, welche Felder Kunden sehen und bearbeiten koennen</p>
</div>
</div>
<form method="POST" action="{{ url_for('admin.field_config') }}">
<!-- Sections -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-layout-text-window me-2"></i>
Sektionen
</div>
<div class="card-body">
<p class="text-muted small mb-3">Aktivieren oder deaktivieren Sie ganze Bereiche im Kundenprofil.</p>
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Sektion</th>
<th class="text-center" style="width: 100px;">Sichtbar</th>
<th style="width: 200px;">Bezeichnung</th>
</tr>
</thead>
<tbody>
{% for key, section in config.sections.items() %}
<tr>
<td>
<i class="bi bi-{{ 'person' if key == 'contact' else 'house' if key == 'address' else 'envelope' if key == 'email_settings' else 'shield-check' }} me-2 text-muted"></i>
{{ section.label }}
</td>
<td class="text-center">
<div class="form-check form-switch d-inline-block">
<input class="form-check-input" type="checkbox"
name="section_{{ key }}_visible"
id="section_{{ key }}_visible"
{% if section.visible %}checked{% endif %}>
</div>
</td>
<td>
<input type="text" class="form-control form-control-sm"
name="section_{{ key }}_label"
value="{{ section.label }}">
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Profile Fields (Standard) -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-person me-2"></i>
Standard-Felder
</div>
<div class="card-body">
<p class="text-muted small mb-3">Basis-Felder fuer Kontakt und Adresse.</p>
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Feld</th>
<th class="text-center" style="width: 100px;">Sichtbar</th>
<th class="text-center" style="width: 100px;">Editierbar</th>
<th style="width: 200px;">Bezeichnung</th>
</tr>
</thead>
<tbody>
{% for key, field in config.profile_fields.items() %}
<tr>
<td>
<code class="text-info">{{ key }}</code>
</td>
<td class="text-center">
<div class="form-check form-switch d-inline-block">
<input class="form-check-input" type="checkbox"
name="field_{{ key }}_visible"
id="field_{{ key }}_visible"
{% if field.visible %}checked{% endif %}>
</div>
</td>
<td class="text-center">
<div class="form-check form-switch d-inline-block">
<input class="form-check-input" type="checkbox"
name="field_{{ key }}_editable"
id="field_{{ key }}_editable"
{% if field.editable %}checked{% endif %}
{% if key in ['name', 'email'] %}disabled title="Dieses Feld kann nicht editierbar gemacht werden"{% endif %}>
</div>
</td>
<td>
<input type="text" class="form-control form-control-sm"
name="field_{{ key }}_label"
value="{{ field.label }}">
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- WordPress Booking Fields -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
<i class="bi bi-wordpress me-2"></i>
WordPress Buchungsfelder
{% if wp_schema and wp_schema.custom_fields %}
<span class="badge bg-success ms-2">{{ wp_schema.custom_fields|length }} Felder</span>
{% endif %}
</span>
<a href="{{ url_for('admin.field_config') }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-arrow-clockwise"></i>
</a>
</div>
<div class="card-body">
{% if wp_error %}
<div class="alert alert-warning mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>WordPress nicht erreichbar:</strong> {{ wp_error }}
<br><small class="text-muted">Bitte <a href="{{ url_for('admin.settings_wordpress') }}" class="text-info">WordPress-Einstellungen</a> pruefen.</small>
</div>
{% elif wp_schema and wp_schema.custom_fields %}
<p class="text-muted small mb-3">
Diese Felder sind in WordPress unter <strong>Kurs-Booking &rarr; Einstellungen &rarr; Buchungsfelder</strong> definiert.
</p>
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Feldname</th>
<th style="width: 80px;">Typ</th>
<th style="min-width: 180px;">Bezeichnung</th>
<th style="width: 50px;" class="text-center">Aktiv</th>
</tr>
</thead>
<tbody>
{% for field in wp_schema.custom_fields %}
{% set field_id = field.name or field.id or field.label|lower|replace(' ', '_') %}
{% set saved = config.wp_fields.get(field_id, {}) %}
<tr>
<td class="align-middle" style="word-break: break-word; max-width: 250px;">
<code class="text-warning" style="white-space: normal; word-break: break-all;">{{ field_id }}</code>
{% if field.mandatory or field.required %}
<span class="badge bg-danger ms-1">Pflicht</span>
{% endif %}
</td>
<td class="align-middle">
<span class="badge bg-secondary">{{ field.type }}</span>
</td>
<td class="align-middle">
<input type="text" class="form-control form-control-sm"
name="wp_{{ field_id }}_label"
value="{{ saved.get('label', field.label) }}"
placeholder="{{ field.label }}">
</td>
<td class="text-center align-middle">
<div class="form-check form-switch d-inline-block mb-0">
<input class="form-check-input" type="checkbox"
name="wp_{{ field_id }}_visible"
id="wp_{{ field_id }}_visible"
{% if saved.get('visible', true) %}checked{% endif %}>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% elif wp_schema %}
<div class="text-center text-muted py-4">
<i class="bi bi-inbox fs-1 mb-2"></i>
<p class="mb-0">Keine Buchungsfelder in WordPress definiert.</p>
<small>Felder koennen in WordPress unter Kurs-Booking &rarr; Einstellungen &rarr; Buchungsfelder hinzugefuegt werden.</small>
</div>
{% else %}
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-2"></i>
WordPress-Verbindung pruefen...
</div>
{% endif %}
</div>
</div>
{# Sprint 12: Legacy MEC Fields section removed - all fields now managed via
WordPress schema or dynamic custom_fields #}
<!-- Advanced Options -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-gear-wide-connected me-2"></i>
Erweiterte Optionen
</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox"
name="custom_fields_visible"
id="custom_fields_visible"
{% if config.custom_fields_visible %}checked{% endif %}>
<label class="form-check-label" for="custom_fields_visible">
<strong>Custom Fields anzeigen</strong>
</label>
<p class="text-muted small mb-0 mt-1">
Zeigt zusaetzliche Felder aus WordPress-Buchungen im Kundenprofil an.
</p>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
name="sync_button_visible"
id="sync_button_visible"
{% if config.sync_button_visible %}checked{% endif %}>
<label class="form-check-label" for="sync_button_visible">
<strong>Sync-Button anzeigen</strong>
</label>
<p class="text-muted small mb-0 mt-1">
Erlaubt Kunden, ihre Daten aus der letzten WordPress-Buchung zu synchronisieren.
</p>
</div>
</div>
</div>
<!-- Submit -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,110 @@
{% extends "admin/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="mb-4">
<h1><i class="bi bi-speedometer2 me-2"></i>Admin Dashboard</h1>
<p class="text-muted">Portal-Einstellungen und Benutzerverwaltung</p>
</div>
<div class="row g-4 mb-4">
<!-- Stats Cards -->
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-25 rounded p-3 me-3">
<i class="bi bi-people fs-3 text-primary"></i>
</div>
<div>
<h3 class="mb-0">{{ total_customers }}</h3>
<small class="text-muted">Kunden gesamt</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-danger bg-opacity-25 rounded p-3 me-3">
<i class="bi bi-shield-check fs-3 text-danger"></i>
</div>
<div>
<h3 class="mb-0">{{ admin_count }}</h3>
<small class="text-muted">Administratoren</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Links -->
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-start">
<div class="bg-info bg-opacity-25 rounded p-3 me-3">
<i class="bi bi-sliders fs-3 text-info"></i>
</div>
<div class="flex-grow-1">
<h5>Feld-Konfiguration</h5>
<p class="text-muted small mb-3">
Legen Sie fest, welche Felder Kunden im Profil sehen und bearbeiten koennen.
</p>
<a href="{{ url_for('admin.field_config') }}" class="btn btn-outline-info">
<i class="bi bi-gear me-1"></i>
Felder konfigurieren
</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-start">
<div class="bg-warning bg-opacity-25 rounded p-3 me-3">
<i class="bi bi-people-fill fs-3 text-warning"></i>
</div>
<div class="flex-grow-1">
<h5>Kundenverwaltung</h5>
<p class="text-muted small mb-3">
Alle registrierten Kunden einsehen und verwalten.
</p>
<a href="{{ url_for('admin.customers') }}" class="btn btn-outline-warning">
<i class="bi bi-person-gear me-1"></i>
Kunden anzeigen
</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-start">
<div class="bg-danger bg-opacity-25 rounded p-3 me-3">
<i class="bi bi-shield-lock fs-3 text-danger"></i>
</div>
<div class="flex-grow-1">
<h5>Administratoren</h5>
<p class="text-muted small mb-3">
Admin-Benutzer verwalten und neue Admins anlegen.
</p>
<a href="{{ url_for('admin.admins') }}" class="btn btn-outline-danger">
<i class="bi bi-shield-plus me-1"></i>
Admins verwalten
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - {{ branding.company_name }}</title>
{% if branding.favicon_url %}
<link rel="icon" href="{{ branding.favicon_url }}" type="image/x-icon">
{% endif %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, {{ branding.colors.background }} 0%, {{ branding.colors.border }} 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
background: {{ branding.colors.sidebar_bg }};
border: 1px solid {{ branding.colors.border }};
border-radius: 1rem;
max-width: 400px;
width: 100%;
}
.form-control {
background: #0d0f12;
border-color: {{ branding.colors.border }};
color: {{ branding.colors.text }};
}
.form-control:focus {
background: #0d0f12;
border-color: #dc3545;
color: {{ branding.colors.text }};
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}
.btn-danger {
background: #dc3545;
border-color: #dc3545;
}
.btn-danger:hover {
background: #bb2d3b;
border-color: #b02a37;
}
h3 {
color: {{ branding.colors.text }};
}
.text-muted {
color: {{ branding.colors.muted }} !important;
}
</style>
</head>
<body class="text-light">
<div class="login-card p-4 shadow-lg">
<div class="text-center mb-4">
{% if branding.logo_url %}
<img src="{{ branding.logo_url }}" alt="{{ branding.company_name }}" style="max-height: 50px; margin-bottom: 1rem;">
{% else %}
<div class="bg-danger bg-opacity-25 rounded-circle d-inline-flex p-3 mb-3">
<i class="bi bi-shield-lock fs-1 text-danger"></i>
</div>
{% endif %}
<h3>{{ branding.company_name }} Admin</h3>
<p class="text-muted mb-0">Bitte melden Sie sich an</p>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show py-2">
{{ message }}
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="post" autocomplete="off">
<div class="mb-3">
<label for="username" class="form-label text-light">Benutzername</label>
<div class="input-group">
<span class="input-group-text bg-dark border-secondary text-light">
<i class="bi bi-person"></i>
</span>
<input type="text" class="form-control" id="username" name="username"
required autofocus autocomplete="username">
</div>
</div>
<div class="mb-4">
<label for="password" class="form-label text-light">Passwort</label>
<div class="input-group">
<span class="input-group-text bg-dark border-secondary text-light">
<i class="bi bi-key"></i>
</span>
<input type="password" class="form-control" id="password" name="password"
required autocomplete="current-password">
</div>
</div>
<button type="submit" class="btn btn-danger w-100 py-2">
<i class="bi bi-box-arrow-in-right me-2"></i>
Anmelden
</button>
</form>
<div class="text-center mt-4">
<a href="{{ url_for('main.index') }}" class="text-muted text-decoration-none small">
<i class="bi bi-arrow-left me-1"></i>
Zurueck zum Kundenportal
</a>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,509 @@
{% extends "admin/base.html" %}
{% block title %}Branding{% endblock %}
{% block extra_css %}
<style>
.color-preview {
width: 40px;
height: 40px;
border-radius: 0.375rem;
border: 2px solid var(--sidebar-border);
cursor: pointer;
}
.color-input-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
.color-input-group input[type="color"] {
-webkit-appearance: none;
width: 50px;
height: 40px;
border: none;
border-radius: 0.375rem;
cursor: pointer;
padding: 0;
}
.color-input-group input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-input-group input[type="color"]::-webkit-color-swatch {
border: 2px solid var(--sidebar-border);
border-radius: 0.375rem;
}
.logo-preview {
max-width: 200px;
max-height: 60px;
background: var(--sidebar-bg);
padding: 0.5rem;
border-radius: 0.375rem;
border: 1px solid var(--sidebar-border);
}
.preview-box {
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.preview-header {
padding: 0.75rem 1rem;
border-radius: 0.375rem;
margin-bottom: 0.5rem;
}
.preview-button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem;
color: white;
cursor: pointer;
}
</style>
{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('admin.index') }}">Dashboard</a></li>
<li class="breadcrumb-item active">Branding</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-palette me-2"></i>Portal-Branding</h1>
</div>
<form method="post">
<div class="row">
<!-- Allgemein -->
<div class="col-lg-6 mb-4">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-building me-2"></i>Allgemein
</div>
<div class="card-body">
<div class="mb-3">
<label for="company_name" class="form-label">Firmenname</label>
<input type="text"
class="form-control"
id="company_name"
name="company_name"
value="{{ config.company_name }}"
placeholder="Kundenportal">
<div class="form-text">
Wird im Header, Login-Seite und E-Mails angezeigt.
</div>
</div>
<div class="mb-3">
<label class="form-label">Logo</label>
{% if config.logo_url %}
<div class="mb-2 p-2 rounded" style="background: var(--sidebar-bg); border: 1px solid var(--sidebar-border);">
<img src="{{ config.logo_url }}" alt="Logo-Vorschau" class="logo-preview" onerror="this.style.display='none'">
</div>
{% endif %}
<div class="input-group mb-2">
<input type="url"
class="form-control"
id="logo_url"
name="logo_url"
value="{{ config.logo_url }}"
placeholder="https://example.com/logo.png">
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#logoUploadModal">
<i class="bi bi-upload"></i>
</button>
</div>
<div class="form-text">
Empfohlen: 200x60px, PNG/SVG/WebP (max 500KB). Leer = Text-Logo.
</div>
</div>
<div class="mb-3">
<label class="form-label">Favicon</label>
{% if config.favicon_url %}
<div class="mb-2">
<img src="{{ config.favicon_url }}" alt="Favicon-Vorschau" style="max-width: 32px; max-height: 32px;">
</div>
{% endif %}
<div class="input-group mb-2">
<input type="url"
class="form-control"
id="favicon_url"
name="favicon_url"
value="{{ config.favicon_url }}"
placeholder="https://example.com/favicon.ico">
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#faviconUploadModal">
<i class="bi bi-upload"></i>
</button>
</div>
<div class="form-text">
Browser-Tab Icon. Empfohlen: 32x32px, ICO/PNG/SVG (max 100KB).
</div>
</div>
</div>
</div>
</div>
<!-- Farben -->
<div class="col-lg-6 mb-4">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-brush me-2"></i>Farben
</div>
<div class="card-body">
<!-- Primary Color -->
<div class="mb-3">
<label class="form-label">Primaerfarbe</label>
<div class="color-input-group">
<input type="color"
id="color_primary"
name="color_primary"
value="{{ config.colors.primary }}">
<input type="text"
class="form-control"
id="color_primary_text"
value="{{ config.colors.primary }}"
pattern="^#[0-9A-Fa-f]{6}$"
style="max-width: 120px;">
<span class="text-muted small">Buttons, Links</span>
</div>
</div>
<!-- Primary Hover -->
<div class="mb-3">
<label class="form-label">Primaerfarbe (Hover)</label>
<div class="color-input-group">
<input type="color"
id="color_primary_hover"
name="color_primary_hover"
value="{{ config.colors.primary_hover }}">
<input type="text"
class="form-control"
id="color_primary_hover_text"
value="{{ config.colors.primary_hover }}"
pattern="^#[0-9A-Fa-f]{6}$"
style="max-width: 120px;">
<span class="text-muted small">Hover-States</span>
</div>
</div>
<!-- Background -->
<div class="mb-3">
<label class="form-label">Hintergrund</label>
<div class="color-input-group">
<input type="color"
id="color_background"
name="color_background"
value="{{ config.colors.background }}">
<input type="text"
class="form-control"
id="color_background_text"
value="{{ config.colors.background }}"
pattern="^#[0-9A-Fa-f]{6}$"
style="max-width: 120px;">
<span class="text-muted small">Seiten-Hintergrund</span>
</div>
</div>
<!-- Header BG -->
<div class="mb-3">
<label class="form-label">Header-Hintergrund</label>
<div class="color-input-group">
<input type="color"
id="color_header_bg"
name="color_header_bg"
value="{{ config.colors.header_bg }}">
<input type="text"
class="form-control"
id="color_header_bg_text"
value="{{ config.colors.header_bg }}"
pattern="^#[0-9A-Fa-f]{6}$"
style="max-width: 120px;">
<span class="text-muted small">Topbar/Navigation</span>
</div>
</div>
<!-- Sidebar BG -->
<div class="mb-3">
<label class="form-label">Sidebar-Hintergrund</label>
<div class="color-input-group">
<input type="color"
id="color_sidebar_bg"
name="color_sidebar_bg"
value="{{ config.colors.sidebar_bg }}">
<input type="text"
class="form-control"
id="color_sidebar_bg_text"
value="{{ config.colors.sidebar_bg }}"
pattern="^#[0-9A-Fa-f]{6}$"
style="max-width: 120px;">
<span class="text-muted small">Admin-Sidebar</span>
</div>
</div>
<!-- Text Color -->
<div class="mb-3">
<label class="form-label">Textfarbe</label>
<div class="color-input-group">
<input type="color"
id="color_text"
name="color_text"
value="{{ config.colors.text }}">
<input type="text"
class="form-control"
id="color_text_text"
value="{{ config.colors.text }}"
pattern="^#[0-9A-Fa-f]{6}$"
style="max-width: 120px;">
<span class="text-muted small">Haupttext</span>
</div>
</div>
<!-- Muted Color -->
<div class="mb-3">
<label class="form-label">Gedaempfte Farbe</label>
<div class="color-input-group">
<input type="color"
id="color_muted"
name="color_muted"
value="{{ config.colors.muted }}">
<input type="text"
class="form-control"
id="color_muted_text"
value="{{ config.colors.muted }}"
pattern="^#[0-9A-Fa-f]{6}$"
style="max-width: 120px;">
<span class="text-muted small">Sekundaerer Text</span>
</div>
</div>
<!-- Border Color -->
<div class="mb-3">
<label class="form-label">Rahmenfarbe</label>
<div class="color-input-group">
<input type="color"
id="color_border"
name="color_border"
value="{{ config.colors.border }}">
<input type="text"
class="form-control"
id="color_border_text"
value="{{ config.colors.border }}"
pattern="^#[0-9A-Fa-f]{6}$"
style="max-width: 120px;">
<span class="text-muted small">Rahmen/Trennlinien</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Vorschau -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-eye me-2"></i>Vorschau
</div>
<div class="card-body">
<div class="preview-box" id="previewBox">
<div class="preview-header" id="previewHeader">
<span id="previewCompanyName">{{ config.company_name }}</span>
</div>
<p id="previewText" style="margin-bottom: 0.5rem;">Beispieltext in der Hauptfarbe</p>
<p id="previewMuted" class="small" style="margin-bottom: 1rem;">Gedaempfter Beispieltext</p>
<button type="button" class="preview-button" id="previewButton">Beispiel-Button</button>
</div>
</div>
</div>
<!-- Aktionen -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#resetModal">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Auf Standard zuruecksetzen
</button>
</div>
</form>
<!-- Reset Modal -->
<div class="modal fade" id="resetModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Branding zuruecksetzen?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Moechten Sie alle Branding-Einstellungen auf die Standardwerte zuruecksetzen?</p>
<p class="text-muted small mb-0">Diese Aktion kann nicht rueckgaengig gemacht werden.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<form method="post" action="{{ url_for('admin.settings_branding_reset') }}" style="display: inline;">
<button type="submit" class="btn btn-danger">Zuruecksetzen</button>
</form>
</div>
</div>
</div>
</div>
<!-- Logo Upload Modal -->
<div class="modal fade" id="logoUploadModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Logo hochladen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{{ url_for('admin.settings_branding_upload_logo') }}" enctype="multipart/form-data">
<div class="modal-body">
<div class="mb-3">
<label for="logo_file" class="form-label">Logo-Datei auswaehlen</label>
<input type="file"
class="form-control"
id="logo_file"
name="logo_file"
accept=".png,.jpg,.jpeg,.gif,.svg,.webp"
required>
<div class="form-text">
Erlaubte Formate: PNG, JPG, GIF, SVG, WebP<br>
Maximale Groesse: 500KB<br>
Empfohlene Abmessungen: 200x60 Pixel
</div>
</div>
<div id="logoPreviewContainer" class="text-center d-none">
<p class="text-muted small mb-2">Vorschau:</p>
<img id="logoPreviewImg" src="" alt="Vorschau" style="max-width: 200px; max-height: 80px;">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-upload me-1"></i>Hochladen
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Favicon Upload Modal -->
<div class="modal fade" id="faviconUploadModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Favicon hochladen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{{ url_for('admin.settings_branding_upload_favicon') }}" enctype="multipart/form-data">
<div class="modal-body">
<div class="mb-3">
<label for="favicon_file" class="form-label">Favicon-Datei auswaehlen</label>
<input type="file"
class="form-control"
id="favicon_file"
name="favicon_file"
accept=".ico,.png,.svg"
required>
<div class="form-text">
Erlaubte Formate: ICO, PNG, SVG<br>
Maximale Groesse: 100KB<br>
Empfohlene Abmessungen: 32x32 Pixel
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-upload me-1"></i>Hochladen
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Sync color picker with text input
document.querySelectorAll('input[type="color"]').forEach(colorInput => {
const textInputId = colorInput.id + '_text';
const textInput = document.getElementById(textInputId);
if (textInput) {
// Color picker -> Text input
colorInput.addEventListener('input', () => {
textInput.value = colorInput.value;
updatePreview();
});
// Text input -> Color picker
textInput.addEventListener('input', () => {
if (/^#[0-9A-Fa-f]{6}$/.test(textInput.value)) {
colorInput.value = textInput.value;
updatePreview();
}
});
}
});
// Company name preview
document.getElementById('company_name').addEventListener('input', function() {
document.getElementById('previewCompanyName').textContent = this.value || 'Kundenportal';
});
// Update preview with current colors
function updatePreview() {
const box = document.getElementById('previewBox');
const header = document.getElementById('previewHeader');
const text = document.getElementById('previewText');
const muted = document.getElementById('previewMuted');
const button = document.getElementById('previewButton');
box.style.background = document.getElementById('color_background').value;
box.style.borderColor = document.getElementById('color_border').value;
header.style.background = document.getElementById('color_header_bg').value;
header.style.color = document.getElementById('color_text').value;
text.style.color = document.getElementById('color_text').value;
muted.style.color = document.getElementById('color_muted').value;
button.style.background = document.getElementById('color_primary').value;
}
// Initial preview update
updatePreview();
// Logo file preview
document.getElementById('logo_file').addEventListener('change', function(e) {
const file = e.target.files[0];
const container = document.getElementById('logoPreviewContainer');
const img = document.getElementById('logoPreviewImg');
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
img.src = e.target.result;
container.classList.remove('d-none');
};
reader.readAsDataURL(file);
} else {
container.classList.add('d-none');
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,141 @@
{% extends "admin/base.html" %}
{% block title %}CSV Export/Import{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1><i class="bi bi-filetype-csv me-2"></i>CSV Export/Import</h1>
<p class="text-muted">Konfigurieren Sie, welche Felder exportiert und importiert werden</p>
</div>
<div class="btn-group">
<a href="{{ url_for('admin.customers_export') }}" class="btn btn-success">
<i class="bi bi-download me-1"></i>Export testen
</a>
</div>
</div>
<form method="POST" action="{{ url_for('admin.settings_csv') }}">
<!-- Export Fields -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-list-check me-2"></i>
Export-Felder
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Waehlen Sie aus, welche Felder in der CSV-Datei enthalten sein sollen.
Spaltenreihenfolge entspricht der Reihenfolge in dieser Liste.
</p>
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th class="text-center" style="width: 80px;">Aktiv</th>
<th>Feld</th>
<th style="width: 250px;">Spaltenname (CSV)</th>
</tr>
</thead>
<tbody>
{% for field in config.export_fields %}
<tr>
<td class="text-center">
<div class="form-check form-switch d-inline-block">
<input class="form-check-input" type="checkbox"
name="enabled_{{ field.key }}"
id="enabled_{{ field.key }}"
{% if field.enabled %}checked{% endif %}
{% if field.key == 'email' %}disabled title="E-Mail ist Pflichtfeld"{% endif %}>
</div>
{% if field.key == 'email' %}
<input type="hidden" name="enabled_{{ field.key }}" value="on">
{% endif %}
</td>
<td>
<code class="text-info">{{ field.key }}</code>
{% if field.key == 'email' %}
<span class="badge bg-danger ms-2">Pflicht</span>
{% endif %}
{% if field.key in ['created_at', 'updated_at'] %}
<span class="badge bg-secondary ms-2">System</span>
{% endif %}
</td>
<td>
<input type="text" class="form-control form-control-sm"
name="label_{{ field.key }}"
value="{{ field.label }}"
placeholder="{{ field.label }}">
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Advanced Options -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-gear me-2"></i>
Erweiterte Optionen
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="delimiter" class="form-label">Trennzeichen</label>
<select class="form-select" name="delimiter" id="delimiter">
<option value=";" {% if config.delimiter == ';' %}selected{% endif %}>Semikolon (;) - Excel-kompatibel</option>
<option value="," {% if config.delimiter == ',' %}selected{% endif %}>Komma (,) - Standard CSV</option>
<option value="\t" {% if config.delimiter == '\t' %}selected{% endif %}>Tabulator - TSV</option>
</select>
<div class="form-text">Semikolon wird fuer deutsche Excel-Versionen empfohlen.</div>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox"
name="include_custom_fields"
id="include_custom_fields"
{% if config.include_custom_fields %}checked{% endif %}>
<label class="form-check-label" for="include_custom_fields">
<strong>Zusatzfelder exportieren</strong>
</label>
<p class="text-muted small mb-0 mt-1">
Fuegt eine JSON-Spalte mit MEC/WordPress Zusatzfeldern hinzu.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Import Info -->
<div class="card mb-4 border-info">
<div class="card-header bg-info bg-opacity-10">
<i class="bi bi-info-circle me-2"></i>
Import-Hinweise
</div>
<div class="card-body">
<ul class="mb-0">
<li>Der Import erkennt Spalten automatisch anhand der <strong>Spaltennamen</strong> (erste Zeile)</li>
<li><strong>E-Mail</strong> ist das Schluesselfeld - bestehende Kunden werden aktualisiert</li>
<li>Werte fuer Ja/Nein-Felder: <code>Ja</code>, <code>1</code>, <code>true</code> = aktiviert</li>
<li>Leere Zellen werden beim Import uebersprungen (bestehende Werte bleiben)</li>
<li>Die <strong>ID</strong>-Spalte wird beim Import ignoriert</li>
</ul>
</div>
</div>
<!-- Submit -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
<a href="{{ url_for('admin.customers') }}" class="btn btn-outline-secondary">
Zurueck zur Kundenliste
</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,176 @@
{% extends "admin/base.html" %}
{% block title %}Kunden-Standardwerte{% endblock %}
{% block content %}
<div class="mb-4">
<h1><i class="bi bi-person-plus me-2"></i>Kunden-Standardwerte</h1>
<p class="text-muted">Standard E-Mail-Einstellungen fuer neue Kunden</p>
</div>
<div class="row">
<div class="col-lg-8">
<form method="POST" action="{{ url_for('admin.settings_customer_defaults') }}">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-envelope-check me-2"></i>
Standard E-Mail-Einstellungen
</div>
<div class="card-body">
<p class="text-muted mb-4">
Diese Einstellungen werden bei der Erstellung neuer Kunden als Standardwerte verwendet.
Kunden koennen ihre Praeferenzen spaeter im Portal aendern.
</p>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
id="email_notifications" name="email_notifications" value="1"
{% if config.email_notifications %}checked{% endif %}>
<label class="form-check-label" for="email_notifications">
<strong>Buchungsbestaetigungen</strong>
</label>
</div>
<div class="form-text ms-4">
E-Mail bei erfolgreicher Buchung
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
id="email_reminders" name="email_reminders" value="1"
{% if config.email_reminders %}checked{% endif %}>
<label class="form-check-label" for="email_reminders">
<strong>Kurserinnerungen</strong>
</label>
</div>
<div class="form-text ms-4">
Automatische Erinnerung vor Kursbeginn
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
id="email_invoices" name="email_invoices" value="1"
{% if config.email_invoices %}checked{% endif %}>
<label class="form-check-label" for="email_invoices">
<strong>Rechnungen</strong>
</label>
</div>
<div class="form-text ms-4">
Rechnungen und Zahlungsinformationen per E-Mail
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
id="email_marketing" name="email_marketing" value="1"
{% if config.email_marketing %}checked{% endif %}>
<label class="form-check-label" for="email_marketing">
<strong>Newsletter & Marketing</strong>
</label>
</div>
<div class="form-text ms-4">
Informationen zu neuen Kursen und Angeboten
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-danger">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
</form>
</div>
<div class="col-lg-4">
<!-- Info Card -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>
Hinweise
</div>
<div class="card-body small">
<p class="mb-2"><strong>Wann werden diese Werte angewendet?</strong></p>
<p class="text-muted mb-3">
Bei jeder Neuerstellung eines Kunden - egal ob durch Registrierung,
Buchung, CSV-Import oder Webhook.
</p>
<p class="mb-2"><strong>DSGVO-Hinweis:</strong></p>
<p class="text-muted mb-3">
Marketing-Einstellungen sollten standardmaessig deaktiviert sein (Opt-in).
Nur bei expliziter Zustimmung aktivieren.
</p>
<p class="mb-2"><strong>Bestehende Kunden:</strong></p>
<p class="text-muted mb-0">
Beim Speichern werden Kunden mit <strong>fehlenden</strong> Werten automatisch ergaenzt.
Explizit gesetzte Einstellungen bleiben unveraendert.
</p>
{% if null_count > 0 %}
<div class="alert alert-warning mt-3 mb-0 py-2">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>{{ null_count }}</strong> Kunden haben fehlende Werte
</div>
{% endif %}
</div>
</div>
<!-- Current Values -->
<div class="card">
<div class="card-header">
<i class="bi bi-gear me-2"></i>
Aktuelle Standardwerte
</div>
<div class="card-body">
<table class="table table-dark table-sm mb-0">
<tr>
<td class="text-muted">Buchungsbestaetigungen</td>
<td class="text-end">
{% if config.email_notifications %}
<i class="bi bi-check-circle text-success"></i>
{% else %}
<i class="bi bi-x-circle text-danger"></i>
{% endif %}
</td>
</tr>
<tr>
<td class="text-muted">Kurserinnerungen</td>
<td class="text-end">
{% if config.email_reminders %}
<i class="bi bi-check-circle text-success"></i>
{% else %}
<i class="bi bi-x-circle text-danger"></i>
{% endif %}
</td>
</tr>
<tr>
<td class="text-muted">Rechnungen</td>
<td class="text-end">
{% if config.email_invoices %}
<i class="bi bi-check-circle text-success"></i>
{% else %}
<i class="bi bi-x-circle text-danger"></i>
{% endif %}
</td>
</tr>
<tr>
<td class="text-muted">Newsletter & Marketing</td>
<td class="text-end">
{% if config.email_marketing %}
<i class="bi bi-check-circle text-success"></i>
{% else %}
<i class="bi bi-x-circle text-danger"></i>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,130 @@
{% extends "admin/base.html" %}
{% block title %}Kundenansicht{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('admin.index') }}">Dashboard</a></li>
<li class="breadcrumb-item active">Kundenansicht</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1><i class="bi bi-layout-text-sidebar me-2"></i>Kundenansicht konfigurieren</h1>
<p class="text-muted mb-0">Definieren Sie Labels und Sichtbarkeit fuer Kundenfelder im Admin-Bereich.</p>
</div>
</div>
<form method="post">
<!-- Info-Box -->
<div class="alert alert-info mb-4">
<i class="bi bi-info-circle me-2"></i>
<strong>{{ discovered_fields|length }} Felder</strong> wurden automatisch aus bestehenden Kundendaten erkannt.
Sie koennen fuer jedes Feld ein benutzerdefiniertes Label setzen oder es ausblenden.
</div>
{% if discovered_fields %}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-sliders me-2"></i>Feld-Konfiguration</span>
<span class="badge bg-secondary">{{ discovered_fields|length }} Felder</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th style="width: 200px;">Feldname (intern)</th>
<th>Anzeige-Label</th>
<th style="width: 120px;" class="text-center">Ausblenden</th>
</tr>
</thead>
<tbody>
{% for field_name in discovered_fields %}
{% set current_label = config.field_labels.get(field_name, '') %}
{% set default_label = default_config.field_labels.get(field_name, '') %}
{% set is_hidden = field_name in config.hidden_fields %}
<tr{% if is_hidden %} class="opacity-50"{% endif %}>
<td>
<code>{{ field_name }}</code>
</td>
<td>
<input type="text"
class="form-control form-control-sm"
name="label_{{ field_name }}"
value="{{ current_label }}"
placeholder="{{ default_label or field_name }}"
{% if is_hidden %}disabled{% endif %}>
{% if default_label and not current_label %}
<small class="text-muted">Standard: {{ default_label }}</small>
{% endif %}
</td>
<td class="text-center">
<div class="form-check form-switch d-flex justify-content-center">
<input type="checkbox"
class="form-check-input"
id="hidden_{{ field_name }}"
name="hidden_{{ field_name }}"
{% if is_hidden %}checked{% endif %}
onchange="toggleFieldRow(this, '{{ field_name }}')">
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
Keine Kundenfelder gefunden. Importieren Sie zuerst Kundendaten, damit Felder erkannt werden koennen.
</div>
{% endif %}
<!-- Aktionen -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
<a href="{{ url_for('admin.customers') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>
Zurueck zu Kunden
</a>
</div>
</form>
<!-- Hinweise -->
<div class="card mt-4">
<div class="card-header">
<i class="bi bi-lightbulb me-2"></i>Hinweise
</div>
<div class="card-body">
<ul class="mb-0">
<li><strong>Anzeige-Label:</strong> Wenn leer, wird der interne Feldname oder der Standard verwendet.</li>
<li><strong>Ausblenden:</strong> Versteckte Felder werden in der Kundendetailansicht nicht angezeigt.</li>
<li><strong>Auto-Discovery:</strong> Neue Felder werden automatisch erkannt, wenn Kundendaten importiert werden.</li>
</ul>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function toggleFieldRow(checkbox, fieldName) {
const row = checkbox.closest('tr');
const input = row.querySelector('input[type="text"]');
if (checkbox.checked) {
row.classList.add('opacity-50');
input.disabled = true;
} else {
row.classList.remove('opacity-50');
input.disabled = false;
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,343 @@
{% extends "admin/base.html" %}
{% block title %}Feld-Mapping{% endblock %}
{% block content %}
<style>
.sortable-row {
cursor: grab;
transition: background-color 0.2s;
}
.sortable-row:active {
cursor: grabbing;
}
.sortable-row.dragging {
opacity: 0.5;
background-color: rgba(220, 53, 69, 0.2) !important;
}
.sortable-row.drag-over {
border-top: 2px solid #dc3545;
}
.drag-handle {
cursor: grab;
color: #6c757d;
}
.drag-handle:hover {
color: #adb5bd;
}
</style>
<div class="mb-4">
<h1><i class="bi bi-arrow-left-right me-2"></i>Feld-Mapping</h1>
<p class="text-muted">Portal-Felder mit WordPress-Feldern verbinden (verschiebbar)</p>
</div>
<div class="row">
<div class="col-lg-9">
<form method="POST" action="{{ url_for('admin.settings_field_mapping') }}" id="mappingForm">
<!-- Datenbank-Felder -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-database me-2"></i>
Stammdaten (Datenbank)
</div>
<div class="card-body">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th style="width: 40px;"></th>
<th style="width: 180px;">Portal-Feld</th>
<th style="width: 70px;" class="text-center">Sync?</th>
<th>WordPress-Feld</th>
</tr>
</thead>
<tbody id="dbFieldsBody" class="sortable-body">
{% set db_fields = [
{'key': 'db.phone', 'label': 'Telefon'},
{'key': 'db.address_street', 'label': 'Strasse'},
{'key': 'db.address_zip', 'label': 'PLZ'},
{'key': 'db.address_city', 'label': 'Ort'}
] %}
{% for field in db_fields %}
{% set current_wp = config.mappings.get(field.key, '') %}
<tr class="sortable-row" draggable="true" data-field="{{ field.key }}">
<td class="align-middle text-center drag-handle">
<i class="bi bi-grip-vertical"></i>
</td>
<td class="align-middle">
<strong>{{ field.label }}</strong>
</td>
<td class="text-center align-middle">
<div class="form-check form-switch d-inline-block mb-0">
<input class="form-check-input sync-toggle" type="checkbox"
data-field="{{ field.key }}"
id="sync_{{ field.key }}"
{% if current_wp %}checked{% endif %}>
</div>
</td>
<td class="align-middle">
<select name="map_{{ field.key }}"
id="select_{{ field.key }}"
class="form-select form-select-sm bg-dark text-light border-secondary"
{% if not current_wp %}disabled{% endif %}>
<option value="">-- Waehlen --</option>
{% if wp_schema and wp_schema.custom_fields %}
{% for wp_field in wp_schema.custom_fields %}
{% set wp_id = wp_field.name or wp_field.id %}
<option value="{{ wp_id }}" {% if current_wp == wp_id %}selected{% endif %}>
{{ wp_field.label }}
</option>
{% endfor %}
{% endif %}
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Zusatzfelder -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-plus-circle me-2"></i>
Zusatzfelder (Custom)
</div>
<div class="card-body">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th style="width: 40px;"></th>
<th style="width: 180px;">Portal-Feld</th>
<th style="width: 70px;" class="text-center">Sync?</th>
<th>WordPress-Feld</th>
</tr>
</thead>
<tbody id="customFieldsBody" class="sortable-body">
{% set custom_fields = [
{'key': 'custom.vorname', 'label': 'Vorname'},
{'key': 'custom.nachname', 'label': 'Nachname'},
{'key': 'custom.geburtsdatum', 'label': 'Geburtsdatum'},
{'key': 'custom.pferdename', 'label': 'Pferdename'},
{'key': 'custom.geschlecht_pferd', 'label': 'Geschlecht Pferd'}
] %}
{% for field in custom_fields %}
{% set current_wp = config.mappings.get(field.key, '') %}
<tr class="sortable-row" draggable="true" data-field="{{ field.key }}">
<td class="align-middle text-center drag-handle">
<i class="bi bi-grip-vertical"></i>
</td>
<td class="align-middle">
<strong>{{ field.label }}</strong>
</td>
<td class="text-center align-middle">
<div class="form-check form-switch d-inline-block mb-0">
<input class="form-check-input sync-toggle" type="checkbox"
data-field="{{ field.key }}"
id="sync_{{ field.key }}"
{% if current_wp %}checked{% endif %}>
</div>
</td>
<td class="align-middle">
<select name="map_{{ field.key }}"
id="select_{{ field.key }}"
class="form-select form-select-sm bg-dark text-light border-secondary"
{% if not current_wp %}disabled{% endif %}>
<option value="">-- Waehlen --</option>
{% if wp_schema and wp_schema.custom_fields %}
{% for wp_field in wp_schema.custom_fields %}
{% set wp_id = wp_field.name or wp_field.id %}
<option value="{{ wp_id }}" {% if current_wp == wp_id %}selected{% endif %}>
{{ wp_field.label }}
</option>
{% endfor %}
{% endif %}
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Options -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-gear me-2"></i>
Optionen
</div>
<div class="card-body">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
id="auto_sync_on_booking" name="auto_sync_on_booking"
{% if config.auto_sync_on_booking %}checked{% endif %}>
<label class="form-check-label" for="auto_sync_on_booking">
<strong>Auto-Sync bei neuer Buchung</strong>
</label>
<div class="form-text">
Synchronisiert automatisch, wenn ein Kunde bucht.
</div>
</div>
</div>
</div>
<!-- Hidden field for order -->
<input type="hidden" name="field_order" id="fieldOrder" value="">
<button type="submit" class="btn btn-danger">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
</form>
</div>
<div class="col-lg-3">
<!-- WordPress Status -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-wordpress me-2"></i>
WordPress
</div>
<div class="card-body">
{% if wp_error %}
<div class="text-danger small">
<i class="bi bi-x-circle me-1"></i>
{{ wp_error }}
</div>
{% elif wp_schema and wp_schema.custom_fields %}
<div class="text-success small mb-2">
<i class="bi bi-check-circle me-1"></i>
Verbunden
</div>
<div class="text-muted small">
{{ wp_schema.custom_fields|length }} Felder verfuegbar
</div>
{% else %}
<div class="text-warning small">
<i class="bi bi-exclamation-triangle me-1"></i>
Keine Felder
</div>
{% endif %}
</div>
</div>
<!-- Bulk Sync -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-arrow-repeat me-2"></i>
Synchronisation
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('admin.customers_sync_all') }}"
onsubmit="return confirm('Alle Kunden synchronisieren?');">
<button type="submit" class="btn btn-outline-warning btn-sm w-100">
<i class="bi bi-arrow-repeat me-1"></i>
Alle synchronisieren
</button>
</form>
</div>
</div>
<!-- Info -->
<div class="card">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>
Anleitung
</div>
<div class="card-body small text-muted">
<p class="mb-2">
<i class="bi bi-grip-vertical me-1"></i>
<strong>Verschieben:</strong> Zeilen ziehen
</p>
<p class="mb-2">
<i class="bi bi-toggle-on me-1"></i>
<strong>Sync = An:</strong> Feld wird synchronisiert
</p>
<p class="mb-0">
<i class="bi bi-info-circle me-1"></i>
Nur leere Felder werden befuellt
</p>
</div>
</div>
</div>
</div>
<script>
// Toggle select when checkbox changes
document.querySelectorAll('.sync-toggle').forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
var fieldKey = this.dataset.field;
var select = document.getElementById('select_' + fieldKey);
if (select) {
select.disabled = !this.checked;
if (!this.checked) {
select.value = '';
}
}
});
});
// Drag and Drop
document.querySelectorAll('.sortable-body').forEach(function(tbody) {
var draggedRow = null;
tbody.addEventListener('dragstart', function(e) {
if (e.target.classList.contains('sortable-row')) {
draggedRow = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
});
tbody.addEventListener('dragend', function(e) {
if (e.target.classList.contains('sortable-row')) {
e.target.classList.remove('dragging');
tbody.querySelectorAll('.sortable-row').forEach(function(row) {
row.classList.remove('drag-over');
});
}
});
tbody.addEventListener('dragover', function(e) {
e.preventDefault();
var targetRow = e.target.closest('.sortable-row');
if (targetRow && targetRow !== draggedRow) {
tbody.querySelectorAll('.sortable-row').forEach(function(row) {
row.classList.remove('drag-over');
});
targetRow.classList.add('drag-over');
}
});
tbody.addEventListener('drop', function(e) {
e.preventDefault();
var targetRow = e.target.closest('.sortable-row');
if (targetRow && draggedRow && targetRow !== draggedRow) {
var rows = Array.from(tbody.querySelectorAll('.sortable-row'));
var draggedIndex = rows.indexOf(draggedRow);
var targetIndex = rows.indexOf(targetRow);
if (draggedIndex < targetIndex) {
targetRow.parentNode.insertBefore(draggedRow, targetRow.nextSibling);
} else {
targetRow.parentNode.insertBefore(draggedRow, targetRow);
}
}
tbody.querySelectorAll('.sortable-row').forEach(function(row) {
row.classList.remove('drag-over');
});
});
});
// Save order before submit
document.getElementById('mappingForm').addEventListener('submit', function() {
var order = [];
document.querySelectorAll('.sortable-row').forEach(function(row) {
order.push(row.dataset.field);
});
document.getElementById('fieldOrder').value = JSON.stringify(order);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,147 @@
{% extends "admin/base.html" %}
{% block title %}Mail-Konfiguration{% endblock %}
{% block content %}
<div class="mb-4">
<h1><i class="bi bi-envelope me-2"></i>Mail-Server Konfiguration</h1>
<p class="text-muted">SMTP-Einstellungen fuer E-Mail-Versand</p>
</div>
<div class="row">
<div class="col-lg-8">
<form method="POST" action="{{ url_for('admin.settings_mail') }}">
<!-- SMTP Settings -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-server me-2"></i>
SMTP-Einstellungen
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8 mb-3">
<label for="mail_server" class="form-label">SMTP-Server</label>
<input type="text" class="form-control" id="mail_server" name="mail_server"
value="{{ config.mail_server }}" placeholder="smtp.example.com">
</div>
<div class="col-md-4 mb-3">
<label for="mail_port" class="form-label">Port</label>
<input type="number" class="form-control" id="mail_port" name="mail_port"
value="{{ config.mail_port }}" min="1" max="65535">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="mail_username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="mail_username" name="mail_username"
value="{{ config.mail_username }}" autocomplete="off">
</div>
<div class="col-md-6 mb-3">
<label for="mail_password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="mail_password" name="mail_password"
placeholder="{% if config.mail_password %}(gespeichert){% else %}Passwort eingeben{% endif %}"
autocomplete="new-password">
<div class="form-text">Leer lassen um bestehendes Passwort beizubehalten</div>
</div>
</div>
<div class="mb-3">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="mail_use_tls"
id="mail_use_tls" {% if config.mail_use_tls %}checked{% endif %}>
<label class="form-check-label" for="mail_use_tls">TLS verwenden (STARTTLS)</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="mail_use_ssl"
id="mail_use_ssl" {% if config.mail_use_ssl %}checked{% endif %}>
<label class="form-check-label" for="mail_use_ssl">SSL verwenden</label>
</div>
</div>
</div>
</div>
<!-- Sender Settings -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-person-badge me-2"></i>
Absender
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="mail_default_sender" class="form-label">Absender E-Mail</label>
<input type="email" class="form-control" id="mail_default_sender" name="mail_default_sender"
value="{{ config.mail_default_sender }}" placeholder="portal@example.com">
</div>
<div class="col-md-6 mb-3">
<label for="mail_default_sender_name" class="form-label">Absender Name</label>
<input type="text" class="form-control" id="mail_default_sender_name" name="mail_default_sender_name"
value="{{ config.mail_default_sender_name }}">
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-danger">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
</form>
</div>
<div class="col-lg-4">
<!-- Test Email -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-send me-2"></i>
Test-E-Mail
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('admin.settings_mail_test') }}">
<div class="mb-3">
<label for="test_email" class="form-label">Empfaenger</label>
<input type="email" class="form-control" id="test_email" name="test_email"
placeholder="test@example.com" required>
</div>
<button type="submit" class="btn btn-outline-info w-100"
{% if not config.mail_server %}disabled{% endif %}>
<i class="bi bi-envelope-arrow-up me-1"></i>
Test senden
</button>
{% if not config.mail_server %}
<div class="form-text text-warning mt-2">
<i class="bi bi-exclamation-triangle me-1"></i>
Zuerst SMTP-Server konfigurieren
</div>
{% endif %}
</form>
</div>
</div>
<!-- Common SMTP Settings -->
<div class="card">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>
Haeufige Einstellungen
</div>
<div class="card-body small">
<p class="mb-2"><strong>Gmail:</strong></p>
<ul class="mb-3">
<li>Server: smtp.gmail.com</li>
<li>Port: 587 (TLS) oder 465 (SSL)</li>
</ul>
<p class="mb-2"><strong>Office 365:</strong></p>
<ul class="mb-3">
<li>Server: smtp.office365.com</li>
<li>Port: 587 (TLS)</li>
</ul>
<p class="mb-2"><strong>All-Inkl:</strong></p>
<ul class="mb-0">
<li>Server: smtp.all-inkl.de</li>
<li>Port: 587 (TLS)</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,129 @@
{% extends "admin/base.html" %}
{% block title %}OTP & Sicherheit{% endblock %}
{% block content %}
<div class="mb-4">
<h1><i class="bi bi-shield-lock me-2"></i>OTP & Sicherheit</h1>
<p class="text-muted">Einstellungen fuer Einmalpasswoerter und Authentifizierung</p>
</div>
<div class="row">
<div class="col-lg-8">
<form method="POST" action="{{ url_for('admin.settings_otp') }}">
<!-- OTP Settings -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-key me-2"></i>
Einmalpasswort (OTP)
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<label for="otp_expiry_minutes" class="form-label">Gueltigkeit (Minuten)</label>
<input type="number" class="form-control" id="otp_expiry_minutes" name="otp_expiry_minutes"
value="{{ config.otp_expiry_minutes }}" min="1" max="60">
<div class="form-text">Wie lange ist ein OTP-Code gueltig?</div>
</div>
<div class="col-md-4 mb-3">
<label for="otp_length" class="form-label">Code-Laenge (Ziffern)</label>
<input type="number" class="form-control" id="otp_length" name="otp_length"
value="{{ config.otp_length }}" min="4" max="8">
<div class="form-text">Anzahl der Ziffern im Code</div>
</div>
<div class="col-md-4 mb-3">
<label for="otp_max_attempts" class="form-label">Max. Fehlversuche</label>
<input type="number" class="form-control" id="otp_max_attempts" name="otp_max_attempts"
value="{{ config.otp_max_attempts }}" min="1" max="10">
<div class="form-text">Bevor Code ungueltig wird</div>
</div>
</div>
</div>
</div>
<!-- Prefill Token Settings -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-link-45deg me-2"></i>
Prefill-Token (WordPress-Integration)
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="prefill_token_expiry" class="form-label">Token-Gueltigkeit (Sekunden)</label>
<input type="number" class="form-control" id="prefill_token_expiry" name="prefill_token_expiry"
value="{{ config.prefill_token_expiry }}" min="60" max="3600">
<div class="form-text">
Standard: 300 (5 Minuten). Maximaler Wert: 3600 (1 Stunde)
</div>
</div>
</div>
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-2"></i>
Prefill-Tokens werden von WordPress generiert, um Kundendaten automatisch
im Login-Formular vorzufuellen.
</div>
</div>
</div>
<button type="submit" class="btn btn-danger">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
</form>
</div>
<div class="col-lg-4">
<!-- Info Card -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-lightbulb me-2"></i>
Empfehlungen
</div>
<div class="card-body small">
<p class="mb-2"><strong>OTP-Gueltigkeit:</strong></p>
<p class="text-muted mb-3">
10 Minuten sind ein guter Kompromiss zwischen Sicherheit und Benutzerfreundlichkeit.
</p>
<p class="mb-2"><strong>Code-Laenge:</strong></p>
<p class="text-muted mb-3">
6 Ziffern bieten ausreichende Sicherheit (1 Million Kombinationen).
</p>
<p class="mb-2"><strong>Fehlversuche:</strong></p>
<p class="text-muted mb-0">
3 Versuche schuetzen vor Brute-Force-Angriffen, ohne legitime Nutzer zu sehr einzuschraenken.
</p>
</div>
</div>
<!-- Current Values -->
<div class="card">
<div class="card-header">
<i class="bi bi-gear me-2"></i>
Aktuelle Werte
</div>
<div class="card-body">
<table class="table table-dark table-sm mb-0">
<tr>
<td class="text-muted">OTP gueltig</td>
<td class="text-end">{{ config.otp_expiry_minutes }} Min.</td>
</tr>
<tr>
<td class="text-muted">Code-Laenge</td>
<td class="text-end">{{ config.otp_length }} Ziffern</td>
</tr>
<tr>
<td class="text-muted">Max. Versuche</td>
<td class="text-end">{{ config.otp_max_attempts }}</td>
</tr>
<tr>
<td class="text-muted">Token-Gueltigkeit</td>
<td class="text-end">{{ config.prefill_token_expiry }} Sek.</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,185 @@
{% extends "admin/base.html" %}
{% block title %}WordPress-Integration{% endblock %}
{% block content %}
<div class="mb-4">
<h1><i class="bi bi-wordpress me-2"></i>WordPress-Integration</h1>
<p class="text-muted">Verbindungseinstellungen zum WordPress Kurs-Booking System</p>
</div>
<div class="row">
<div class="col-lg-8">
<form method="POST" action="{{ url_for('admin.settings_wordpress') }}">
<!-- API Connection -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-plug me-2"></i>
API-Verbindung
</div>
<div class="card-body">
<div class="mb-3">
<label for="wp_api_url" class="form-label">WordPress REST API URL</label>
<input type="url" class="form-control" id="wp_api_url" name="wp_api_url"
value="{{ config.wp_api_url }}"
placeholder="http://192.168.100.93:8300/wp-json/kurs-booking/v1">
<div class="form-text">
Vollstaendige URL zur kurs-booking REST API (ohne abschliessenden Slash)
</div>
</div>
<div class="mb-3">
<label for="wp_api_secret" class="form-label">API Secret</label>
<div class="input-group">
<input type="password" class="form-control" id="wp_api_secret" name="wp_api_secret"
value="{{ config.wp_api_secret }}">
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecret()">
<i class="bi bi-eye" id="toggleIcon"></i>
</button>
<button type="button" class="btn btn-outline-secondary" onclick="generateSecret()">
Generieren
</button>
</div>
<div class="form-text">
Muss identisch sein mit der WordPress-Einstellung "Portal API Secret" im Kurs-Booking Plugin
</div>
</div>
<div class="mb-0">
<label for="wp_booking_page_url" class="form-label">Buchungsseite URL (optional)</label>
<input type="url" class="form-control" id="wp_booking_page_url" name="wp_booking_page_url"
value="{{ config.wp_booking_page_url }}"
placeholder="http://192.168.100.93:8300/buchung/">
<div class="form-text">
Link zur Buchungsseite fuer "Neue Buchung" Button
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-danger">
<i class="bi bi-check-lg me-1"></i>
Speichern
</button>
</form>
</div>
<div class="col-lg-4">
<!-- Connection Test -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-broadcast me-2"></i>
Verbindungstest
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('admin.settings_wordpress_test') }}">
<p class="small text-muted mb-3">
Testet die Verbindung zur WordPress REST API mit den gespeicherten Einstellungen.
</p>
<button type="submit" class="btn btn-outline-info w-100"
{% if not config.wp_api_url %}disabled{% endif %}>
<i class="bi bi-arrow-repeat me-1"></i>
Verbindung testen
</button>
{% if not config.wp_api_url %}
<div class="form-text text-warning mt-2">
<i class="bi bi-exclamation-triangle me-1"></i>
Zuerst API URL konfigurieren
</div>
{% endif %}
</form>
</div>
</div>
<!-- Status Info -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i>
Konfigurationsstatus
</div>
<div class="card-body">
<table class="table table-dark table-sm mb-0">
<tr>
<td class="text-muted">API URL</td>
<td class="text-end">
{% if config.wp_api_url %}
<span class="badge bg-success">Gesetzt</span>
{% else %}
<span class="badge bg-warning">Nicht gesetzt</span>
{% endif %}
</td>
</tr>
<tr>
<td class="text-muted">API Secret</td>
<td class="text-end">
{% if config.wp_api_secret %}
<span class="badge bg-success">Gesetzt</span>
{% else %}
<span class="badge bg-warning">Nicht gesetzt</span>
{% endif %}
</td>
</tr>
<tr>
<td class="text-muted">Buchungsseite</td>
<td class="text-end">
{% if config.wp_booking_page_url %}
<span class="badge bg-success">Gesetzt</span>
{% else %}
<span class="badge bg-secondary">Optional</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
<!-- API Endpoints Info -->
<div class="card">
<div class="card-header">
<i class="bi bi-diagram-3 me-2"></i>
Verfuegbare Endpunkte
</div>
<div class="card-body small">
<code class="d-block mb-2">/bookings</code>
<p class="text-muted mb-2">Buchungen eines Kunden abrufen</p>
<code class="d-block mb-2">/invoices</code>
<p class="text-muted mb-2">Rechnungen via sevDesk abrufen</p>
<code class="d-block mb-2">/videos</code>
<p class="text-muted mb-2">Video-Zugang pruefen</p>
<code class="d-block mb-2">/test</code>
<p class="text-muted mb-0">Verbindungstest</p>
</div>
</div>
</div>
</div>
<script>
function toggleSecret() {
const input = document.getElementById('wp_api_secret');
const icon = document.getElementById('toggleIcon');
if (input.type === 'password') {
input.type = 'text';
icon.classList.remove('bi-eye');
icon.classList.add('bi-eye-slash');
} else {
input.type = 'password';
icon.classList.remove('bi-eye-slash');
icon.classList.add('bi-eye');
}
}
function generateSecret() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let secret = '';
for (let i = 0; i < 32; i++) {
secret += chars.charAt(Math.floor(Math.random() * chars.length));
}
document.getElementById('wp_api_secret').value = secret;
document.getElementById('wp_api_secret').type = 'text';
document.getElementById('toggleIcon').classList.remove('bi-eye');
document.getElementById('toggleIcon').classList.add('bi-eye-slash');
}
</script>
{% endblock %}