465 lines
23 KiB
HTML
Executable File
465 lines
23 KiB
HTML
Executable File
{% 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 %}
|