Files

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 %}