497 lines
26 KiB
Twig
497 lines
26 KiB
Twig
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Zahlungstransaktionen - Webshop Admin</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<!-- Sidebar -->
|
|
<nav class="col-md-3 col-lg-2 d-md-block bg-dark sidebar collapse">
|
|
<div class="position-sticky pt-3">
|
|
<ul class="nav flex-column">
|
|
<li class="nav-item">
|
|
<a class="nav-link text-white" href="/admin/dashboard">
|
|
<i class="bi bi-speedometer2"></i> Dashboard
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link text-white" href="/admin/payment">
|
|
<i class="bi bi-credit-card"></i> Zahlungen
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link active text-white" href="/admin/payment/transactions">
|
|
<i class="bi bi-list"></i> Transaktionen
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link text-white" href="/admin/payment/paypal">
|
|
<i class="bi bi-paypal"></i> PayPal
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link text-white" href="/admin/payment/stripe">
|
|
<i class="bi bi-credit-card"></i> Stripe
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link text-white" href="/admin/payment/sepa">
|
|
<i class="bi bi-bank"></i> SEPA
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main content -->
|
|
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
|
<h1 class="h2">Zahlungstransaktionen</h1>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<div class="btn-group me-2">
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="exportTransactions()">
|
|
<i class="bi bi-download"></i> Export
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="refreshTransactions()">
|
|
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Flash Messages -->
|
|
{% if success_messages %}
|
|
{% for message in success_messages %}
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
{% if error_messages %}
|
|
{% for message in error_messages %}
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card text-white bg-primary">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Gesamt Transaktionen</h6>
|
|
<h4 class="mb-0">{{ total }}</h4>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="bi bi-credit-card fs-1"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-white bg-success">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Erfolgreich</h6>
|
|
<h4 class="mb-0" id="successfulCount">-</h4>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="bi bi-check-circle fs-1"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-white bg-warning">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Ausstehend</h6>
|
|
<h4 class="mb-0" id="pendingCount">-</h4>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="bi bi-clock fs-1"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-white bg-danger">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="card-title">Fehlgeschlagen</h6>
|
|
<h4 class="mb-0" id="failedCount">-</h4>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="bi bi-x-circle fs-1"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<form id="filterForm" class="row g-3">
|
|
<div class="col-md-3">
|
|
<label for="provider" class="form-label">Zahlungsanbieter</label>
|
|
<select class="form-select" id="provider" name="provider">
|
|
<option value="">Alle Anbieter</option>
|
|
<option value="paypal">PayPal</option>
|
|
<option value="stripe">Stripe</option>
|
|
<option value="sepa">SEPA</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="status" class="form-label">Status</label>
|
|
<select class="form-select" id="status" name="status">
|
|
<option value="">Alle Status</option>
|
|
<option value="pending">Ausstehend</option>
|
|
<option value="completed">Erfolgreich</option>
|
|
<option value="failed">Fehlgeschlagen</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="date_from" class="form-label">Von Datum</label>
|
|
<input type="date" class="form-control" id="date_from" name="date_from">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="date_to" class="form-label">Bis Datum</label>
|
|
<input type="date" class="form-control" id="date_to" name="date_to">
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-search"></i> Filtern
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" onclick="resetFilters()">
|
|
<i class="bi bi-arrow-clockwise"></i> Zurücksetzen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transactions Table -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">Transaktionen</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Bestellung</th>
|
|
<th>Anbieter</th>
|
|
<th>Betrag</th>
|
|
<th>Status</th>
|
|
<th>Datum</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="transactionsTable">
|
|
{% for transaction in transactions %}
|
|
<tr>
|
|
<td>
|
|
<span class="badge bg-secondary">{{ transaction.id }}</span>
|
|
</td>
|
|
<td>
|
|
<a href="/admin/orders/view/{{ transaction.order_id }}" class="text-decoration-none">
|
|
#{{ transaction.order_number }}
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-primary">{{ transaction.provider|upper }}</span>
|
|
</td>
|
|
<td>
|
|
<strong>{{ transaction.amount|number_format(2, ',', '.') }} €</strong>
|
|
<br>
|
|
<small class="text-muted">{{ transaction.currency }}</small>
|
|
</td>
|
|
<td>
|
|
{% if transaction.status == 'completed' %}
|
|
<span class="badge bg-success">
|
|
<i class="bi bi-check-circle"></i> Erfolgreich
|
|
</span>
|
|
{% elseif transaction.status == 'pending' %}
|
|
<span class="badge bg-warning">
|
|
<i class="bi bi-clock"></i> Ausstehend
|
|
</span>
|
|
{% elseif transaction.status == 'failed' %}
|
|
<span class="badge bg-danger">
|
|
<i class="bi bi-x-circle"></i> Fehlgeschlagen
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ transaction.status }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{{ transaction.created_at|date('d.m.Y H:i') }}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" onclick="viewTransaction({{ transaction.id }})">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-info" onclick="retryTransaction({{ transaction.id }})">
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if total_pages > 1 %}
|
|
<nav aria-label="Transaktionen Navigation">
|
|
<ul class="pagination justify-content-center">
|
|
{% if page > 1 %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page - 1 }}">Zurück</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for p in range(1, total_pages) %}
|
|
{% if p == page %}
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ p }}</span>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ p }}">{{ p }}</a>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if page < total_pages %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page + 1 }}">Weiter</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transaction Details Modal -->
|
|
<div class="modal fade" id="transactionModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Transaktionsdetails</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="transactionModalBody">
|
|
<!-- Content will be loaded here -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
// Update statistics
|
|
function updateStatistics() {
|
|
const transactions = document.querySelectorAll('#transactionsTable tr');
|
|
let successful = 0, pending = 0, failed = 0;
|
|
|
|
transactions.forEach(row => {
|
|
const statusCell = row.querySelector('td:nth-child(5)');
|
|
if (statusCell) {
|
|
const status = statusCell.textContent.toLowerCase();
|
|
if (status.includes('erfolgreich')) successful++;
|
|
else if (status.includes('ausstehend')) pending++;
|
|
else if (status.includes('fehlgeschlagen')) failed++;
|
|
}
|
|
});
|
|
|
|
document.getElementById('successfulCount').textContent = successful;
|
|
document.getElementById('pendingCount').textContent = pending;
|
|
document.getElementById('failedCount').textContent = failed;
|
|
}
|
|
|
|
// View transaction details
|
|
function viewTransaction(transactionId) {
|
|
fetch(`/admin/payment/transaction/${transactionId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const modalBody = document.getElementById('transactionModalBody');
|
|
modalBody.innerHTML = `
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Transaktionsinformationen</h6>
|
|
<table class="table table-sm">
|
|
<tr><td>ID:</td><td>${data.id}</td></tr>
|
|
<tr><td>Anbieter:</td><td>${data.provider}</td></tr>
|
|
<tr><td>Status:</td><td>${data.status}</td></tr>
|
|
<tr><td>Betrag:</td><td>${data.amount} ${data.currency}</td></tr>
|
|
<tr><td>Erstellt:</td><td>${data.created_at}</td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Bestellinformationen</h6>
|
|
<table class="table table-sm">
|
|
<tr><td>Bestellnummer:</td><td>${data.order_number}</td></tr>
|
|
<tr><td>Kunde:</td><td>${data.customer_name}</td></tr>
|
|
<tr><td>Bestelldatum:</td><td>${data.order_date}</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<h6>Payment Data</h6>
|
|
<pre class="bg-light p-3 rounded">${JSON.stringify(JSON.parse(data.payment_data), null, 2)}</pre>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('transactionModal'));
|
|
modal.show();
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Laden der Transaktionsdetails:', error);
|
|
alert('Fehler beim Laden der Transaktionsdetails');
|
|
});
|
|
}
|
|
|
|
// Retry transaction
|
|
function retryTransaction(transactionId) {
|
|
if (confirm('Möchten Sie diese Transaktion erneut versuchen?')) {
|
|
fetch(`/admin/payment/retry-transaction/${transactionId}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `csrf_token={{ csrf_token }}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Transaktion wird erneut versucht...', 'success');
|
|
setTimeout(() => location.reload(), 2000);
|
|
} else {
|
|
showAlert('Fehler beim erneuten Versuch: ' + data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Retry Fehler:', error);
|
|
showAlert('Fehler beim erneuten Versuch', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
// Export transactions
|
|
function exportTransactions() {
|
|
const form = document.getElementById('filterForm');
|
|
const formData = new FormData(form);
|
|
const params = new URLSearchParams(formData);
|
|
|
|
const date = new Date().toISOString().split('T')[0];
|
|
const filename = `transactions_${date}.csv`;
|
|
|
|
fetch(`/admin/payment/export-transactions?${params.toString()}`)
|
|
.then(response => response.blob())
|
|
.then(blob => {
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
})
|
|
.catch(error => {
|
|
console.error('Export Fehler:', error);
|
|
showAlert('Fehler beim Export der Transaktionen', 'danger');
|
|
});
|
|
}
|
|
|
|
// Refresh transactions
|
|
function refreshTransactions() {
|
|
location.reload();
|
|
}
|
|
|
|
// Reset filters
|
|
function resetFilters() {
|
|
document.getElementById('filterForm').reset();
|
|
document.getElementById('filterForm').submit();
|
|
}
|
|
|
|
// Show alert message
|
|
function showAlert(message, type) {
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
|
alertDiv.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
const container = document.querySelector('main');
|
|
container.insertBefore(alertDiv, container.firstChild);
|
|
|
|
// Auto-remove after 5 seconds
|
|
setTimeout(() => {
|
|
if (alertDiv.parentNode) {
|
|
alertDiv.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateStatistics();
|
|
|
|
// Auto-refresh every 30 seconds
|
|
setInterval(() => {
|
|
fetch('/admin/payment/transactions-data')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Update table if new transactions
|
|
if (data.new_transactions > 0) {
|
|
location.reload();
|
|
}
|
|
})
|
|
.catch(error => console.error('Auto-refresh Fehler:', error));
|
|
}, 30000);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |