Initial commit - Customer Portal for Coolify
This commit is contained in:
91
customer_portal/web/templates/admin/admins.html
Executable file
91
customer_portal/web/templates/admin/admins.html
Executable 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 %}
|
||||
456
customer_portal/web/templates/admin/base.html
Executable file
456
customer_portal/web/templates/admin/base.html
Executable 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>
|
||||
200
customer_portal/web/templates/admin/booking_detail.html
Executable file
200
customer_portal/web/templates/admin/booking_detail.html
Executable 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 %}
|
||||
464
customer_portal/web/templates/admin/bookings.html
Executable file
464
customer_portal/web/templates/admin/bookings.html
Executable 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 %}
|
||||
221
customer_portal/web/templates/admin/bookings_import.html
Executable file
221
customer_portal/web/templates/admin/bookings_import.html
Executable 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 %}
|
||||
134
customer_portal/web/templates/admin/bookings_sync.html
Executable file
134
customer_portal/web/templates/admin/bookings_sync.html
Executable 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 %}
|
||||
88
customer_portal/web/templates/admin/create_admin.html
Executable file
88
customer_portal/web/templates/admin/create_admin.html
Executable 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 %}
|
||||
273
customer_portal/web/templates/admin/customer_detail.html
Executable file
273
customer_portal/web/templates/admin/customer_detail.html
Executable 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 %}
|
||||
215
customer_portal/web/templates/admin/customer_edit.html
Executable file
215
customer_portal/web/templates/admin/customer_edit.html
Executable 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 %}
|
||||
157
customer_portal/web/templates/admin/customers.html
Executable file
157
customer_portal/web/templates/admin/customers.html
Executable 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 %}
|
||||
112
customer_portal/web/templates/admin/customers_import.html
Executable file
112
customer_portal/web/templates/admin/customers_import.html
Executable 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 %}
|
||||
239
customer_portal/web/templates/admin/field_config.html
Executable file
239
customer_portal/web/templates/admin/field_config.html
Executable 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 → Einstellungen → 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 → Einstellungen → 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 %}
|
||||
110
customer_portal/web/templates/admin/index.html
Executable file
110
customer_portal/web/templates/admin/index.html
Executable 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 %}
|
||||
118
customer_portal/web/templates/admin/login.html
Executable file
118
customer_portal/web/templates/admin/login.html
Executable 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>
|
||||
509
customer_portal/web/templates/admin/settings_branding.html
Executable file
509
customer_portal/web/templates/admin/settings_branding.html
Executable 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 %}
|
||||
141
customer_portal/web/templates/admin/settings_csv.html
Executable file
141
customer_portal/web/templates/admin/settings_csv.html
Executable 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 %}
|
||||
176
customer_portal/web/templates/admin/settings_customer_defaults.html
Executable file
176
customer_portal/web/templates/admin/settings_customer_defaults.html
Executable 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 %}
|
||||
130
customer_portal/web/templates/admin/settings_customer_view.html
Executable file
130
customer_portal/web/templates/admin/settings_customer_view.html
Executable 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 %}
|
||||
343
customer_portal/web/templates/admin/settings_field_mapping.html
Executable file
343
customer_portal/web/templates/admin/settings_field_mapping.html
Executable 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 %}
|
||||
147
customer_portal/web/templates/admin/settings_mail.html
Executable file
147
customer_portal/web/templates/admin/settings_mail.html
Executable 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 %}
|
||||
129
customer_portal/web/templates/admin/settings_otp.html
Executable file
129
customer_portal/web/templates/admin/settings_otp.html
Executable 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 %}
|
||||
185
customer_portal/web/templates/admin/settings_wordpress.html
Executable file
185
customer_portal/web/templates/admin/settings_wordpress.html
Executable 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 %}
|
||||
Reference in New Issue
Block a user