14-04-2026
This commit is contained in:
parent
f58c709945
commit
0f82fea88a
72 changed files with 7414 additions and 148 deletions
|
|
@ -0,0 +1,45 @@
|
|||
<div class="timeline">
|
||||
@forelse($incident->activities as $activity)
|
||||
<div class="timeline-item {{ $activity->type === 'status_change' ? 'timeline-item-secondary' : '' }}">
|
||||
<div class="timeline-indicator bg-{{ match($activity->type) {
|
||||
'status_change' => 'info',
|
||||
'email' => 'primary',
|
||||
'call' => 'success',
|
||||
'ticket' => 'warning',
|
||||
'provider_response' => 'secondary',
|
||||
default => 'light border'
|
||||
} }}">
|
||||
<i class="ion {{ $activity->type_icon }}"></i>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="badge badge-light">{{ $activity->type_label }}</span>
|
||||
<strong class="ml-1">{{ $activity->title }}</strong>
|
||||
</div>
|
||||
<small class="text-muted text-nowrap ml-2">
|
||||
{{ $activity->created_at->format('d.m.Y H:i') }}
|
||||
— {{ $activity->author }}
|
||||
</small>
|
||||
</div>
|
||||
@if($activity->content)
|
||||
<div class="mt-1 text-muted">{{ $activity->content }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-muted">Noch keine Aktivitäten.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.timeline { position: relative; padding-left: 2.5rem; }
|
||||
.timeline::before { content: ''; position: absolute; left: 1rem; top: 0; bottom: 0; width: 2px; background: #e9ecef; }
|
||||
.timeline-item { position: relative; margin-bottom: 1.25rem; }
|
||||
.timeline-indicator {
|
||||
position: absolute; left: -2.5rem; width: 2rem; height: 2rem;
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.875rem; color: #fff;
|
||||
}
|
||||
.timeline-content { background: #f8f9fa; border-radius: 0.375rem; padding: 0.75rem 1rem; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<div class="modal fade" id="createIncidentModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Neuen Incident anlegen</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('admin.payment-dashboard.store') }}">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-8">
|
||||
<label>Titel <span class="text-danger">*</span></label>
|
||||
<input type="text" name="title" class="form-control" value="{{ old('title') }}" required>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label>Erkannt am <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" name="detected_at" class="form-control"
|
||||
value="{{ old('detected_at', now()->format('Y-m-d\TH:i')) }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label>Anbieter <span class="text-danger">*</span></label>
|
||||
<select name="provider" class="custom-select" required>
|
||||
<option value="payone" {{ old('provider', 'payone') === 'payone' ? 'selected' : '' }}>PAYONE</option>
|
||||
{{-- <option value="stripe" {{ old('provider') === 'stripe' ? 'selected' : '' }}>Stripe</option> --}}
|
||||
<option value="paypal" {{ old('provider') === 'paypal' ? 'selected' : '' }}>PayPal</option>
|
||||
{{-- <option value="mollie" {{ old('provider') === 'mollie' ? 'selected' : '' }}>Mollie</option> --}}
|
||||
<option value="other" {{ old('provider') === 'other' ? 'selected' : '' }}>Sonstige</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label>Typ <span class="text-danger">*</span></label>
|
||||
<select name="type" class="custom-select" required>
|
||||
<option value="outage" {{ old('type') === 'outage' ? 'selected' : '' }}>Ausfall</option>
|
||||
<option value="ipn_error" {{ old('type') === 'ipn_error' ? 'selected' : '' }}>IPN-Fehler</option>
|
||||
<option value="payment_failure" {{ old('type', 'payment_failure') === 'payment_failure' ? 'selected' : '' }}>Zahlungsfehler</option>
|
||||
<option value="slow_response" {{ old('type') === 'slow_response' ? 'selected' : '' }}>Langsame Antwort</option>
|
||||
<option value="other" {{ old('type') === 'other' ? 'selected' : '' }}>Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label>Schwere <span class="text-danger">*</span></label>
|
||||
<select name="severity" class="custom-select" required>
|
||||
<option value="low" {{ old('severity') === 'low' ? 'selected' : '' }}>Niedrig</option>
|
||||
<option value="medium" {{ old('severity', 'medium') === 'medium' ? 'selected' : '' }}>Mittel</option>
|
||||
<option value="high" {{ old('severity') === 'high' ? 'selected' : '' }}>Hoch</option>
|
||||
<option value="critical" {{ old('severity') === 'critical' ? 'selected' : '' }}>Kritisch</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label>Betroffene Bestellungen</label>
|
||||
<input type="number" name="affected_orders" class="form-control" min="0"
|
||||
value="{{ old('affected_orders', 0) }}">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label>Betroffener Umsatz (€)</label>
|
||||
<input type="number" name="affected_revenue" class="form-control" min="0" step="0.01"
|
||||
value="{{ old('affected_revenue', '0.00') }}">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label>Ticket-Nummer</label>
|
||||
<input type="text" name="ticket_number" class="form-control"
|
||||
value="{{ old('ticket_number') }}" placeholder="z.B. PAYONE-12345">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Beschreibung</label>
|
||||
<textarea name="description" class="form-control" rows="3"
|
||||
placeholder="Was ist passiert?">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="ion ion-md-alert"></i> Incident anlegen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Schwere</th>
|
||||
<th>Titel</th>
|
||||
<th>Anbieter</th>
|
||||
<th>Typ</th>
|
||||
<th>Status</th>
|
||||
<th>Erkannt</th>
|
||||
<th>Dauer</th>
|
||||
@if(isset($showActions) && $showActions)
|
||||
<th></th>
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($incidents as $incident)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge badge-{{ $incident->severity_color }}">
|
||||
{{ $incident->severity_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('admin.payment-dashboard.show', $incident) }}">
|
||||
{{ $incident->title }}
|
||||
</a>
|
||||
@if($incident->ticket_number)
|
||||
<span class="text-muted small ml-1">#{{ $incident->ticket_number }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td><span class="badge badge-secondary">{{ $incident->provider_label }}</span></td>
|
||||
<td>
|
||||
<i class="ion {{ $incident->type_icon }}"></i>
|
||||
{{ $incident->type_label }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ $incident->status_color }}">
|
||||
{{ $incident->status_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-nowrap">{{ $incident->detected_at->format('d.m.Y H:i') }}</td>
|
||||
<td class="text-nowrap">{{ $incident->duration }}</td>
|
||||
@if(isset($showActions) && $showActions)
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<form method="POST" action="{{ route('admin.payment-dashboard.status.update', $incident) }}" class="d-inline">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<select name="status" class="custom-select custom-select-sm" onchange="this.form.submit()" style="width:auto">
|
||||
@foreach(['open' => 'Offen', 'in_progress' => 'In Bearb.', 'waiting_provider' => 'Wartet', 'resolved' => 'Gelöst', 'closed' => 'Geschl.'] as $value => $label)
|
||||
<option value="{{ $value }}" {{ $incident->status === $value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
<a href="{{ route('admin.payment-dashboard.show', $incident) }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="ion ion-md-open"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-3">Keine Incidents vorhanden.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<div class="row mb-4">
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card {{ $stats['critical_open'] > 0 ? 'border-danger' : '' }}">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">Offene Incidents</div>
|
||||
<div class="display-4 font-weight-bold {{ $stats['open_incidents'] > 0 ? 'text-danger' : 'text-success' }}">
|
||||
{{ $stats['open_incidents'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">In Bearbeitung</div>
|
||||
<div class="display-4 font-weight-bold {{ $stats['in_progress'] > 0 ? 'text-warning' : 'text-muted' }}">
|
||||
{{ $stats['in_progress'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">PAYONE (30 Tage)</div>
|
||||
<div class="display-4 font-weight-bold text-info">
|
||||
{{ $stats['payone_incidents_30d'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">Gelöst (Monat)</div>
|
||||
<div class="display-4 font-weight-bold text-success">
|
||||
{{ $stats['resolved_this_month'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">Erfolgsrate Zahlung</div>
|
||||
<div class="display-4 font-weight-bold {{ $transactionStats['success_rate'] < 90 ? 'text-warning' : 'text-success' }}">
|
||||
{{ $transactionStats['success_rate'] }}%
|
||||
</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">letzte {{ $transactionStats['days'] }} Tage</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card {{ $transactionStats['failed'] > 0 ? 'border-warning' : '' }}">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">Fehlgeschlagen</div>
|
||||
<div class="display-4 font-weight-bold {{ $transactionStats['failed'] > 0 ? 'text-danger' : 'text-success' }}">
|
||||
{{ $transactionStats['failed'] }}
|
||||
</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">letzte {{ $transactionStats['days'] }} Tage</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
457
resources/views/admin/payment-dashboard/abandoned.blade.php
Normal file
457
resources/views/admin/payment-dashboard/abandoned.blade.php
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-sm btn-outline-secondary mr-2">
|
||||
<i class="ion ion-md-arrow-back"></i> Zurück
|
||||
</a>
|
||||
<strong>Abbruch-Analyse</strong>
|
||||
<small class="text-muted ml-2">Nicht gestartete, abgebrochene und technisch fehlerhafte Zahlungen</small>
|
||||
</div>
|
||||
{{-- Zeitraum-Filter --}}
|
||||
<form method="GET" class="form-inline">
|
||||
<label class="mr-2 text-muted small">Zeitraum:</label>
|
||||
<select name="days" class="form-control form-control-sm mr-2" onchange="this.form.submit()">
|
||||
@foreach ([7 => '7 Tage', 14 => '14 Tage', 30 => '30 Tage', 60 => '60 Tage', 90 => '90 Tage', 0 => 'Alle'] as $value => $label)
|
||||
<option value="{{ $value }}" {{ $days == $value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Stat-Karten --}}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body text-center py-3">
|
||||
<div class="text-muted small mb-1">Zahlung nie gestartet</div>
|
||||
<div class="h3 font-weight-bold mb-0 text-warning">{{ $abandonedStats['no_payment'] }}</div>
|
||||
<small class="text-muted">Orders mit txaction=prev ohne Payment</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body text-center py-3">
|
||||
<div class="text-muted small mb-1">Abgebrochen / Fehler</div>
|
||||
<div class="h3 font-weight-bold mb-0 text-danger">{{ $abandonedStats['cancelled'] }}</div>
|
||||
<small class="text-muted">cancel + error Payments</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-secondary">
|
||||
<div class="card-body text-center py-3">
|
||||
<div class="text-muted small mb-1">Kein PAYONE-Callback</div>
|
||||
<div class="h3 font-weight-bold mb-0 text-secondary">{{ $abandonedStats['no_callback'] }}</div>
|
||||
<small class="text-muted">Payments ohne Transaktion (>2h)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tabs --}}
|
||||
<ul class="nav nav-tabs mb-0" id="abandonedTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="tab-no-payment" data-toggle="tab" href="#no-payment" role="tab">
|
||||
Nie gestartet
|
||||
@if ($abandonedStats['no_payment'] > 0)
|
||||
<span class="badge badge-warning ml-1">{{ $abandonedStats['no_payment'] }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="tab-cancelled" data-toggle="tab" href="#cancelled" role="tab">
|
||||
Abgebrochen / Fehler
|
||||
@if ($abandonedStats['cancelled'] > 0)
|
||||
<span class="badge badge-danger ml-1">{{ $abandonedStats['cancelled'] }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="tab-no-callback" data-toggle="tab" href="#no-callback" role="tab">
|
||||
Kein Callback
|
||||
@if ($abandonedStats['no_callback'] > 0)
|
||||
<span class="badge badge-secondary ml-1">{{ $abandonedStats['no_callback'] }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content border border-top-0 rounded-bottom bg-white px-3 pt-3 pb-2 mb-4">
|
||||
|
||||
{{-- Tab 1: Orders ohne Payment --}}
|
||||
<div class="tab-pane fade show active" id="no-payment" role="tabpanel">
|
||||
<p class="text-muted small mt-1 mb-3">
|
||||
Bestellungen, bei denen der Benutzer den Checkout-Prozess zwar abgeschlossen hat (txaction=prev),
|
||||
aber die Zahlung nie initiiert wurde. Mindestens 30 Minuten alt.
|
||||
</p>
|
||||
@if ($ordersWithoutPayment->isEmpty())
|
||||
<div class="alert alert-success">Keine offenen Bestellungen ohne Zahlung im gewählten Zeitraum.</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Order-ID</th>
|
||||
<th>Kunde / Berater</th>
|
||||
<th>Typ</th>
|
||||
<th>Betrag</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Vor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($ordersWithoutPayment as $order)
|
||||
@php
|
||||
$isConsultant = $order->auth_user_id && $order->auth_user;
|
||||
if ($isConsultant) {
|
||||
$name = trim(
|
||||
($order->auth_user->firstname ?? '') .
|
||||
' ' .
|
||||
($order->auth_user->lastname ?? ''),
|
||||
);
|
||||
$email = $order->auth_user->email ?? '–';
|
||||
} else {
|
||||
$name = trim(
|
||||
($order->shopping_user->billing_firstname ?? '') .
|
||||
' ' .
|
||||
($order->shopping_user->billing_lastname ?? ''),
|
||||
);
|
||||
$email = $order->shopping_user->billing_email ?? '–';
|
||||
}
|
||||
@endphp
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ $order->auth_user_id ? route('admin_sales_users_detail', $order->id) : route('admin_sales_customers_detail', $order->id) }}"
|
||||
target="_blank" class="text-monospace">
|
||||
#{{ $order->id }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@if ($isConsultant)
|
||||
<span class="badge badge-primary badge-sm mr-1">Berater</span>
|
||||
@else
|
||||
<span class="badge badge-info badge-sm mr-1">Kunde</span>
|
||||
@endif
|
||||
<strong>{{ $name ?: '–' }}</strong>
|
||||
<br><small class="text-muted">{{ $email }}</small>
|
||||
</td>
|
||||
<td><small>{{ $order->payment_for ?? '–' }}</small></td>
|
||||
<td class="font-weight-bold">
|
||||
{{ $order->price_total ? number_format($order->price_total, 2, ',', '.') . ' €' : '–' }}
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
{{ $order->created_at ? $order->created_at->format('d.m.Y H:i') : '–' }}</td>
|
||||
<td class="text-muted small">
|
||||
{{ $order->created_at ? $order->created_at->diffForHumans() : '–' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $ordersWithoutPayment->links() }}
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Tab 2: Abgebrochene / Fehler --}}
|
||||
<div class="tab-pane fade" id="cancelled" role="tabpanel">
|
||||
<p class="text-muted small mt-1 mb-3">
|
||||
Zahlungen, bei denen der Nutzer aktiv abgebrochen hat (<code>cancel</code>) oder bei denen PAYONE
|
||||
einen Fehler zurückgemeldet hat (<code>error</code>). Zeile anklicken für PAYONE-Fehlerdetails.
|
||||
</p>
|
||||
@if ($cancelledPayments->isEmpty())
|
||||
<div class="alert alert-success">Keine abgebrochenen Zahlungen im gewählten Zeitraum.</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width:1rem"></th>
|
||||
<th>Referenz</th>
|
||||
<th>Order-ID</th>
|
||||
<th>Kunde / Berater</th>
|
||||
<th>Betrag</th>
|
||||
<th>Status</th>
|
||||
<th>Zahlungsart</th>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Vor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($cancelledPayments as $payment)
|
||||
@php
|
||||
$order = $payment->shopping_order;
|
||||
$isConsultant = $order && $order->auth_user_id && $order->auth_user;
|
||||
if ($order && $isConsultant) {
|
||||
$name = trim(
|
||||
($order->auth_user->firstname ?? '') .
|
||||
' ' .
|
||||
($order->auth_user->lastname ?? ''),
|
||||
);
|
||||
$email = $order->auth_user->email ?? '–';
|
||||
} elseif ($order && $order->shopping_user) {
|
||||
$name = trim(
|
||||
($order->shopping_user->billing_firstname ?? '') .
|
||||
' ' .
|
||||
($order->shopping_user->billing_lastname ?? ''),
|
||||
);
|
||||
$email = $order->shopping_user->billing_email ?? '–';
|
||||
} else {
|
||||
$name = '–';
|
||||
$email = '–';
|
||||
}
|
||||
$hasTransactions = $payment->payment_transactions->isNotEmpty();
|
||||
$collapseId = 'cancelled-tx-' . $payment->id;
|
||||
@endphp
|
||||
{{-- Hauptzeile --}}
|
||||
<tr class="{{ $hasTransactions ? 'cursor-pointer' : '' }}"
|
||||
@if($hasTransactions) data-toggle="collapse" data-target="#{{ $collapseId }}" @endif>
|
||||
<td class="text-center text-muted">
|
||||
@if($hasTransactions)
|
||||
<i class="ion ion-md-chevron-forward toggle-icon" style="font-size:0.85rem"></i>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-monospace small">{{ $payment->reference }}</td>
|
||||
<td>
|
||||
@if ($order)
|
||||
<a href="{{ $isConsultant ? route('admin_sales_users_detail', $order->id) : route('admin_sales_customers_detail', $order->id) }}"
|
||||
target="_blank" onclick="event.stopPropagation()">#{{ $order->id }}</a>
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($order)
|
||||
@if ($isConsultant)
|
||||
<span class="badge badge-primary badge-sm mr-1">Berater</span>
|
||||
@else
|
||||
<span class="badge badge-info badge-sm mr-1">Kunde</span>
|
||||
@endif
|
||||
<strong>{{ $name ?: '–' }}</strong>
|
||||
<br><small class="text-muted">{{ $email }}</small>
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="font-weight-bold">
|
||||
{{ $payment->amount ? number_format($payment->amount / 100, 2, ',', '.') . ' €' : '–' }}
|
||||
</td>
|
||||
<td>
|
||||
@if ($payment->status === 'cancel')
|
||||
<span class="badge badge-warning">Abgebrochen</span>
|
||||
@elseif($payment->status === 'error')
|
||||
<span class="badge badge-danger">Fehler</span>
|
||||
@else
|
||||
<span class="badge badge-secondary">{{ $payment->status }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="small">{{ $payment->payment_type ?? '–' }}</td>
|
||||
<td class="text-muted small">
|
||||
{{ $payment->created_at ? $payment->created_at->format('d.m.Y H:i') : '–' }}</td>
|
||||
<td class="text-muted small">
|
||||
{{ $payment->created_at ? $payment->created_at->diffForHumans() : '–' }}</td>
|
||||
</tr>
|
||||
|
||||
{{-- Aufklappbare Fehlerdetails --}}
|
||||
@if($hasTransactions)
|
||||
<tr class="collapse" id="{{ $collapseId }}">
|
||||
<td colspan="9" class="p-0">
|
||||
<div class="bg-light border-top border-bottom px-3 py-2">
|
||||
<small class="text-muted font-weight-bold d-block mb-2">
|
||||
PAYONE-Transaktionen ({{ $payment->payment_transactions->count() }})
|
||||
</small>
|
||||
@foreach($payment->payment_transactions as $tx)
|
||||
<div class="card card-body p-2 mb-2 {{ $tx->errorcode ? 'border-danger' : 'border-secondary' }}" style="font-size:0.78rem">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<span class="text-muted">TX-ID:</span>
|
||||
<strong>{{ $tx->txid ?? '–' }}</strong><br>
|
||||
<span class="text-muted">Action:</span>
|
||||
<code>{{ $tx->txaction ?? '–' }}</code><br>
|
||||
<span class="text-muted">Request:</span>
|
||||
<code>{{ $tx->request ?? '–' }}</code><br>
|
||||
<span class="text-muted">Status:</span>
|
||||
@if($tx->status === 'approved')
|
||||
<span class="badge badge-success">approved</span>
|
||||
@elseif($tx->status === 'error')
|
||||
<span class="badge badge-danger">error</span>
|
||||
@else
|
||||
<span class="badge badge-secondary">{{ $tx->status ?? '–' }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@php
|
||||
$errorcode = $tx->errorcode
|
||||
?? ($tx->transmitted_data['errorcode'] ?? null);
|
||||
$failedcause = $tx->transmitted_data['failedcause'] ?? null;
|
||||
$errormessage = $tx->errormessage
|
||||
?? ($tx->transmitted_data['errormessage'] ?? null);
|
||||
$customermessage = $tx->customermessage
|
||||
?? ($tx->transmitted_data['customermessage'] ?? null);
|
||||
$description = $tx->error_description;
|
||||
@endphp
|
||||
@if($errorcode)
|
||||
<span class="text-danger font-weight-bold">
|
||||
<i class="ion ion-md-warning"></i>
|
||||
Fehlercode {{ $errorcode }}
|
||||
</span><br>
|
||||
@if($description)
|
||||
<span class="font-weight-bold text-dark">{{ $description }}</span><br>
|
||||
@endif
|
||||
@if($errormessage)
|
||||
<span class="text-muted">PAYONE-Meldung:</span>
|
||||
<span class="text-danger">{{ $errormessage }}</span><br>
|
||||
@endif
|
||||
@if($failedcause && $failedcause != '-'.$errorcode)
|
||||
<span class="text-muted">Ursache:</span>
|
||||
<code>{{ $failedcause }}</code><br>
|
||||
@endif
|
||||
@if($customermessage)
|
||||
<span class="text-muted">Kundennachricht:</span>
|
||||
<em>{{ $customermessage }}</em>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-muted font-italic">
|
||||
@if($tx->txaction === 'failed')
|
||||
Fehlercode nicht übermittelt
|
||||
<br><small>(txaction=failed ohne Fehlercode)</small>
|
||||
@elseif($tx->status === 'REDIRECT')
|
||||
Nutzer zu PAYONE weitergeleitet
|
||||
<br><small>(kein Fehler, Redirect)</small>
|
||||
@else
|
||||
Kein Fehlercode in diesem Callback
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<span class="text-muted">Modus:</span>
|
||||
@if($tx->mode === 'test')
|
||||
<span class="badge badge-warning">TEST</span>
|
||||
@elseif($tx->mode === 'live')
|
||||
<span class="badge badge-success">LIVE</span>
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
<br>
|
||||
<span class="text-muted">Zeitpunkt:</span>
|
||||
{{ $tx->created_at ? $tx->created_at->format('d.m.Y H:i:s') : '–' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $cancelledPayments->links() }}
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Tab 3: Kein Callback --}}
|
||||
<div class="tab-pane fade" id="no-callback" role="tabpanel">
|
||||
<p class="text-muted small mt-1 mb-3">
|
||||
Zahlungen, die gestartet wurden (PAYONE-Redirect), aber nach mehr als 2 Stunden
|
||||
weder einen Callback noch eine Nutzer-Rückkehr registriert haben.
|
||||
Dies kann auf technische Probleme (Timeout, fehlgeschlagene Weiterleitung) hinweisen.
|
||||
</p>
|
||||
@if ($pendingPayments->isEmpty())
|
||||
<div class="alert alert-success">Keine offenen Zahlungen ohne Callback im gewählten Zeitraum.</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Referenz</th>
|
||||
<th>Order-ID</th>
|
||||
<th>Kunde / Berater</th>
|
||||
<th>Betrag</th>
|
||||
<th>Zahlungsart</th>
|
||||
<th>Modus</th>
|
||||
<th>Gestartet</th>
|
||||
<th>Vor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($pendingPayments as $payment)
|
||||
@php
|
||||
$order = $payment->shopping_order;
|
||||
$isConsultant = $order && $order->auth_user_id && $order->auth_user;
|
||||
if ($order && $isConsultant) {
|
||||
$name = trim(
|
||||
($order->auth_user->firstname ?? '') .
|
||||
' ' .
|
||||
($order->auth_user->lastname ?? ''),
|
||||
);
|
||||
$email = $order->auth_user->email ?? '–';
|
||||
} elseif ($order && $order->shopping_user) {
|
||||
$name = trim(
|
||||
($order->shopping_user->billing_firstname ?? '') .
|
||||
' ' .
|
||||
($order->shopping_user->billing_lastname ?? ''),
|
||||
);
|
||||
$email = $order->shopping_user->billing_email ?? '–';
|
||||
} else {
|
||||
$name = '–';
|
||||
$email = '–';
|
||||
}
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="text-monospace small">{{ $payment->reference }}</td>
|
||||
<td>
|
||||
@if ($order)
|
||||
<a href="{{ $isConsultant ? route('admin_sales_users_detail', $order->id) : route('admin_sales_customers_detail', $order->id) }}"
|
||||
target="_blank">#{{ $order->id }}</a>
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($order)
|
||||
@if ($isConsultant)
|
||||
<span class="badge badge-primary badge-sm mr-1">Berater</span>
|
||||
@else
|
||||
<span class="badge badge-info badge-sm mr-1">Kunde</span>
|
||||
@endif
|
||||
<strong>{{ $name ?: '–' }}</strong>
|
||||
<br><small class="text-muted">{{ $email }}</small>
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="font-weight-bold">
|
||||
{{ $payment->amount ? number_format($payment->amount / 100, 2, ',', '.') . ' €' : '–' }}
|
||||
</td>
|
||||
<td class="small">{{ $payment->payment_type ?? '–' }}</td>
|
||||
<td>
|
||||
@if (($payment->mode ?? '') === 'test')
|
||||
<span class="badge badge-warning">TEST</span>
|
||||
@elseif(($payment->mode ?? '') === 'live')
|
||||
<span class="badge badge-success">LIVE</span>
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
{{ $payment->created_at ? $payment->created_at->format('d.m.Y H:i') : '–' }}</td>
|
||||
<td class="text-muted small">
|
||||
{{ $payment->created_at ? $payment->created_at->diffForHumans() : '–' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $pendingPayments->links() }}
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
302
resources/views/admin/payment-dashboard/funnel.blade.php
Normal file
302
resources/views/admin/payment-dashboard/funnel.blade.php
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
<div class="mb-2">
|
||||
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-sm btn-outline-secondary mr-2">
|
||||
<i class="ion ion-md-arrow-back"></i> Zurück
|
||||
</a>
|
||||
<strong>Checkout-Funnel Tracking</strong>
|
||||
<small class="text-muted ml-2">Internes Tracking aller Checkout-Schritte</small>
|
||||
</div>
|
||||
<form method="GET" class="form-inline">
|
||||
<label class="mr-2 text-muted small">Zeitraum:</label>
|
||||
<select name="days" class="form-control form-control-sm" onchange="this.form.submit()">
|
||||
@foreach ([1 => 'Heute', 7 => '7 Tage', 14 => '14 Tage', 30 => '30 Tage', 60 => '60 Tage', 90 => '90 Tage', 0 => 'Alle'] as $value => $label)
|
||||
<option value="{{ $value }}" {{ $days == $value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@php $topCount = $funnelSteps[0]['count'] > 0 ? $funnelSteps[0]['count'] : 1; @endphp
|
||||
|
||||
<div class="row">
|
||||
{{-- ── Funnel ──────────────────────────────────────────────────────── --}}
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><strong>Checkout-Funnel</strong></div>
|
||||
<div class="card-body">
|
||||
@foreach ($funnelSteps as $i => $step)
|
||||
@php $barWidth = $topCount > 0 ? round($step['count'] / $topCount * 100) : 0; @endphp
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<div>
|
||||
<span class="badge badge-secondary mr-1">{{ $i + 1 }}</span>
|
||||
<strong>{{ $step['label'] }}</strong>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="font-weight-bold">{{ number_format($step['count'], 0, ',', '.') }}</span>
|
||||
@if ($step['conversion'] !== null)
|
||||
<span
|
||||
class="ml-2 small {{ $step['conversion'] >= 70 ? 'text-success' : ($step['conversion'] >= 40 ? 'text-warning' : 'text-danger') }}">
|
||||
↓ {{ $step['conversion'] }}%
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress" style="height:20px;">
|
||||
<div class="progress-bar {{ $i === 0 ? 'bg-primary' : ($i === count($funnelSteps) - 1 ? 'bg-success' : 'bg-info') }}"
|
||||
style="width:{{ max($barWidth, $step['count'] > 0 ? 2 : 0) }}%" role="progressbar">
|
||||
@if ($barWidth > 8)
|
||||
{{ $barWidth }}%
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@php
|
||||
$totalConversion =
|
||||
$funnelSteps[0]['count'] > 0
|
||||
? round(($funnelSteps[4]['count'] / $funnelSteps[0]['count']) * 100, 1)
|
||||
: 0;
|
||||
@endphp
|
||||
<div
|
||||
class="alert {{ $totalConversion >= 50 ? 'alert-success' : ($totalConversion >= 25 ? 'alert-warning' : 'alert-danger') }} mt-2 mb-0 py-2">
|
||||
<strong>Gesamt-Konversionsrate: {{ $totalConversion }}%</strong>
|
||||
<small class="text-muted ml-2">(Checkout aufgerufen → Zahlung bestätigt)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Rechte Spalte ────────────────────────────────────────────────── --}}
|
||||
<div class="col-lg-4">
|
||||
|
||||
{{-- Rückkehr-Status --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><strong>Rückkehr von PAYONE</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
@forelse($returnStats as $status => $count)
|
||||
<tr>
|
||||
<td>
|
||||
@if ($status === 'success')
|
||||
<span class="badge badge-success">success</span>
|
||||
@elseif($status === 'cancel')
|
||||
<span class="badge badge-warning">cancel</span>
|
||||
@elseif($status === 'error')
|
||||
<span class="badge badge-danger">error</span>
|
||||
@else
|
||||
<span class="badge badge-secondary">{{ $status ?? '?' }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-right font-weight-bold">{{ number_format($count, 0, ',', '.') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="2" class="text-muted text-center py-3 small">Noch keine Daten</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Quell-Kanal --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><strong>Quelle (Checkout-Aufrufe)</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
@forelse($sourceBreakdown as $key => $source)
|
||||
<tr>
|
||||
<td style="font-size:0.82rem">
|
||||
@if ($key === 'kundenshop')
|
||||
<i class="ion ion-md-cart text-success mr-1"></i>
|
||||
@elseif($key === 'salescenter')
|
||||
<i class="ion ion-md-briefcase text-primary mr-1"></i>
|
||||
@elseif($key === 'beraterzugang')
|
||||
<i class="ion ion-md-person text-info mr-1"></i>
|
||||
@elseif($key === 'testserver')
|
||||
<i class="ion ion-md-flask text-warning mr-1"></i>
|
||||
@else
|
||||
<i class="ion ion-md-help text-muted mr-1"></i>
|
||||
@endif
|
||||
{{ $source['label'] }}
|
||||
</td>
|
||||
<td class="text-right font-weight-bold">
|
||||
{{ number_format($source['count'], 0, ',', '.') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="2" class="text-muted text-center py-3 small">Noch keine Daten</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Ereignisse (gefiltert + paginiert) ─────────────────────────────────── --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap">
|
||||
<strong>Ereignisse</strong>
|
||||
{{-- Filter-Leiste --}}
|
||||
<form method="GET" class="form-inline mt-1 mt-md-0">
|
||||
<input type="hidden" name="days" value="{{ $days }}">
|
||||
<select name="event" class="form-control form-control-sm mr-1" onchange="this.form.submit()">
|
||||
<option value="">Alle Ereignisse</option>
|
||||
@foreach (\App\Models\CheckoutFunnelEvent::eventLabels() as $val => $label)
|
||||
<option value="{{ $val }}" {{ $filterEvent == $val ? 'selected' : '' }}>
|
||||
{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="return_status" class="form-control form-control-sm mr-1" onchange="this.form.submit()">
|
||||
<option value="">Alle Status</option>
|
||||
<option value="success" {{ $filterStatus == 'success' ? 'selected' : '' }}>success</option>
|
||||
<option value="cancel" {{ $filterStatus == 'cancel' ? 'selected' : '' }}>cancel</option>
|
||||
<option value="error" {{ $filterStatus == 'error' ? 'selected' : '' }}>error</option>
|
||||
</select>
|
||||
<select name="source" class="form-control form-control-sm mr-1" onchange="this.form.submit()">
|
||||
<option value="">Alle Quellen</option>
|
||||
@foreach (\App\Models\CheckoutFunnelEvent::sourceLabels() as $val => $label)
|
||||
<option value="{{ $val }}" {{ $filterSource == $val ? 'selected' : '' }}>
|
||||
{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if ($filterEvent || $filterStatus || $filterSource)
|
||||
<a href="{{ route('admin.payment-dashboard.funnel', ['days' => $days]) }}"
|
||||
class="btn btn-sm btn-outline-secondary">✕ Reset</a>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Ereignis</th>
|
||||
<th>Quelle</th>
|
||||
<th>Domain</th>
|
||||
<th>Berater</th>
|
||||
<th>Order-ID</th>
|
||||
<th>Zahlungsart</th>
|
||||
<th>Betrag</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($recentEvents as $event)
|
||||
<tr>
|
||||
<td class="text-muted small text-nowrap">
|
||||
{{ $event->created_at->format('d.m. H:i:s') }}<br>
|
||||
<small>{{ $event->created_at->diffForHumans() }}</small>
|
||||
</td>
|
||||
<td>
|
||||
@php
|
||||
$badgeClass = match ($event->event) {
|
||||
'checkout_visited' => 'badge-secondary',
|
||||
'form_submitted' => 'badge-info',
|
||||
'payment_initiated' => 'badge-primary',
|
||||
'payment_returned' => match ($event->return_status) {
|
||||
'success' => 'badge-success',
|
||||
'cancel' => 'badge-warning',
|
||||
default => 'badge-danger',
|
||||
},
|
||||
'payment_confirmed' => 'badge-success',
|
||||
default => 'badge-light',
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $badgeClass }}">{{ $event->event_label }}</span>
|
||||
@if ($event->metadata && isset($event->metadata['txaction']))
|
||||
<small class="text-muted ml-1">{{ $event->metadata['txaction'] }}</small>
|
||||
@endif
|
||||
</td>
|
||||
<td class="small">
|
||||
@php $src = $event->source_type; @endphp
|
||||
@if ($src === 'kundenshop')
|
||||
<span class="badge badge-success" title="Kundenshop">Shop</span>
|
||||
@elseif($src === 'salescenter')
|
||||
<span class="badge badge-primary" title="Salescenter">SC</span>
|
||||
@elseif($src === 'beraterzugang')
|
||||
<span class="badge badge-info" title="Beraterzugang">BZ</span>
|
||||
@elseif($src === 'testserver')
|
||||
<span class="badge badge-warning" title="Testserver">TEST</span>
|
||||
@else
|
||||
<span class="badge badge-secondary">?</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-muted small">{{ $event->domain ?? '–' }}</td>
|
||||
<td class="small">
|
||||
@if ($event->consultant)
|
||||
{{ $event->consultant->firstname }} {{ $event->consultant->lastname }}
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="small">
|
||||
@if ($event->shopping_order_id)
|
||||
<a href="{{ route('admin_sales_users_detail', $event->shopping_order_id) }}"
|
||||
target="_blank">
|
||||
#{{ $event->shopping_order_id }}
|
||||
</a>
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="small">{{ $event->payment_method ?? '–' }}</td>
|
||||
<td class="small">
|
||||
{{ $event->amount_cents ? number_format($event->amount_cents / 100, 2, ',', '.') . ' €' : '–' }}
|
||||
</td>
|
||||
<td class="small">
|
||||
@if ($event->return_status)
|
||||
@if ($event->return_status === 'success')
|
||||
<span class="badge badge-success">{{ $event->return_status }}</span>
|
||||
@elseif($event->return_status === 'cancel')
|
||||
<span class="badge badge-warning">{{ $event->return_status }}</span>
|
||||
@else
|
||||
<span class="badge badge-danger">{{ $event->return_status }}</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-muted">–</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted py-4">
|
||||
Keine Ereignisse gefunden.
|
||||
@if ($filterEvent || $filterStatus || $filterSource)
|
||||
<a href="{{ route('admin.payment-dashboard.funnel', ['days' => $days]) }}">Filter
|
||||
zurücksetzen</a>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if ($recentEvents->hasPages())
|
||||
<div class="card-footer">
|
||||
{{ $recentEvents->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Tracking-Hinweis --}}
|
||||
<div class="alert alert-info small py-2">
|
||||
<i class="ion ion-md-information-circle mr-1"></i>
|
||||
<strong>Hinweis:</strong> Das Tracking ist ab dem Aktivierungszeitpunkt aktiv. Ältere Checkouts sind nicht
|
||||
enthalten.
|
||||
Schritt 5 „PAYONE Callback" wird sowohl bei synchroner Bestätigung (<code>transactionApproved</code>)
|
||||
als auch bei asynchronem IPN-Callback (<code>txaction=paid</code> und <code>appointed</code>) erfasst.
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
143
resources/views/admin/payment-dashboard/index.blade.php
Normal file
143
resources/views/admin/payment-dashboard/index.blade.php
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0">
|
||||
<i class="ion ion-md-alert text-danger"></i> Payment Monitor
|
||||
</h4>
|
||||
<small class="text-muted">Entwickler-Ansicht — {{ now()->format('d.m.Y H:i') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-danger btn-sm" data-toggle="modal" data-target="#createIncidentModal">
|
||||
<i class="ion ion-md-add"></i> Neuer Incident
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Stat-Karten --}}
|
||||
@include('admin.payment-dashboard._partials.stats-cards')
|
||||
|
||||
{{-- Anbieter-Status --}}
|
||||
<div class="row mb-4">
|
||||
@foreach($providerStats as $key => $provider)
|
||||
@php $uptime = $uptimeStats[$key] ?? null; @endphp
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card {{ $provider['open_incidents'] > 0 ? 'border-danger' : '' }}">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ $provider['label'] }}</strong>
|
||||
<div>
|
||||
@if($uptime && $uptime['last_check'])
|
||||
@if($uptime['last_check']->is_up)
|
||||
<span class="badge badge-success mr-1" title="Letzte Prüfung: {{ $uptime['last_check']->checked_at->diffForHumans() }}">
|
||||
<i class="ion ion-md-checkmark"></i> Online
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-danger mr-1" title="{{ $uptime['last_check']->error_message }}">
|
||||
<i class="ion ion-md-close"></i> Offline
|
||||
</span>
|
||||
@endif
|
||||
@endif
|
||||
@if($provider['open_incidents'] > 0)
|
||||
<span class="badge badge-warning">{{ $provider['open_incidents'] }} Incident</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">{{ $provider['total_30d'] }} Incidents (30 Tage)</div>
|
||||
@if($uptime && $uptime['checks_24h'] > 0)
|
||||
<div class="text-muted" style="font-size:0.7rem">
|
||||
Uptime 24h: {{ $uptime['uptime_24h'] }}%
|
||||
@if($uptime['failures_24h'] > 0)
|
||||
— <span class="text-danger">{{ $uptime['failures_24h'] }} Ausfälle</span>
|
||||
@endif
|
||||
</div>
|
||||
@elseif($uptime)
|
||||
<div class="text-muted" style="font-size:0.7rem">Noch keine Uptime-Daten</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Tabs --}}
|
||||
<ul class="nav nav-tabs" id="dashboardTabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#tab-open">
|
||||
Offene Incidents
|
||||
@if($openIncidents->count() > 0)
|
||||
<span class="badge badge-danger ml-1">{{ $openIncidents->count() }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#tab-all">Alle Incidents</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('admin.payment-dashboard.payments') }}">
|
||||
<i class="ion ion-md-card"></i> Zahlungen & Transaktionen
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('admin.payment-dashboard.transactions') }}">
|
||||
<i class="ion ion-md-swap"></i> Rohe Transaktionen
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('admin.payment-dashboard.funnel') }}">
|
||||
<i class="ion ion-md-funnel"></i> Funnel-Tracking
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('admin.payment-dashboard.abandoned') }}">
|
||||
<i class="ion ion-md-alert"></i> Abbruch-Analyse
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('admin.payment-dashboard.logs') }}">
|
||||
<i class="ion ion-md-list"></i> PAYONE Logs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
{{-- Tab: Offene Incidents --}}
|
||||
<div class="tab-pane fade show active pt-3" id="tab-open">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
@include('admin.payment-dashboard._partials.incident-table', [
|
||||
'incidents' => $openIncidents,
|
||||
'showActions' => true,
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tab: Alle Incidents --}}
|
||||
<div class="tab-pane fade pt-3" id="tab-all">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
@include('admin.payment-dashboard._partials.incident-table', [
|
||||
'incidents' => $allIncidents,
|
||||
'showActions' => true,
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
{{ $allIncidents->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('admin.payment-dashboard._partials.create-incident-modal')
|
||||
|
||||
@endsection
|
||||
86
resources/views/admin/payment-dashboard/logs.blade.php
Normal file
86
resources/views/admin/payment-dashboard/logs.blade.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-sm btn-outline-secondary mr-2">
|
||||
<i class="ion ion-md-arrow-back"></i> Zurück
|
||||
</a>
|
||||
<strong>PAYONE Log-Viewer</strong>
|
||||
</div>
|
||||
@if(count($availableDates) > 1)
|
||||
<form method="GET" action="{{ route('admin.payment-dashboard.logs') }}">
|
||||
<select name="date" class="custom-select custom-select-sm" onchange="this.form.submit()">
|
||||
@foreach($availableDates as $date)
|
||||
<option value="{{ $date }}" {{ $selectedDate === $date ? 'selected' : '' }}>
|
||||
{{ \Carbon\Carbon::parse($date)->format('d.m.Y') }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if(count($entries) === 0)
|
||||
<div class="alert alert-info">
|
||||
<i class="ion ion-md-information-circle"></i>
|
||||
Keine Log-Einträge für {{ \Carbon\Carbon::parse($selectedDate)->format('d.m.Y') }} gefunden.
|
||||
@if(count($availableDates) > 0)
|
||||
Verfügbare Daten: {{ implode(', ', array_map(fn($d) => \Carbon\Carbon::parse($d)->format('d.m.Y'), $availableDates)) }}
|
||||
@else
|
||||
Der Log-Kanal <code>payone</code> hat noch keine Einträge geschrieben.
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="mb-2 text-muted small">
|
||||
<i class="ion ion-md-list"></i> {{ count($entries) }} Einträge (neueste zuerst)
|
||||
</div>
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="mb-3">
|
||||
<input type="text" id="logFilter" class="form-control form-control-sm"
|
||||
placeholder="Filter: Stichwort oder Fehlercode (z.B. Error:2003)..."
|
||||
oninput="filterLogs(this.value)">
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div id="logEntries">
|
||||
@foreach($entries as $entry)
|
||||
@php
|
||||
$levelColor = match($entry['level']) {
|
||||
'error' => 'danger',
|
||||
'warning' => 'warning',
|
||||
'info' => 'info',
|
||||
'notice' => 'secondary',
|
||||
default => 'secondary',
|
||||
};
|
||||
@endphp
|
||||
<div class="log-entry border-bottom px-3 py-2 {{ $entry['level'] === 'error' ? 'bg-light' : '' }}"
|
||||
data-search="{{ strtolower($entry['timestamp'] . ' ' . $entry['level'] . ' ' . $entry['message']) }}">
|
||||
<div class="d-flex align-items-start">
|
||||
<span class="badge badge-{{ $levelColor }} mr-2 mt-1 flex-shrink-0">{{ strtoupper($entry['level']) }}</span>
|
||||
<div style="min-width:0">
|
||||
<div class="text-muted small">{{ $entry['timestamp'] }}</div>
|
||||
<div class="small" style="word-break:break-all; font-family:monospace">{{ $entry['message'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
function filterLogs(term) {
|
||||
const entries = document.querySelectorAll('.log-entry');
|
||||
const search = term.toLowerCase();
|
||||
entries.forEach(el => {
|
||||
el.style.display = el.dataset.search.includes(search) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
111
resources/views/admin/payment-dashboard/management.blade.php
Normal file
111
resources/views/admin/payment-dashboard/management.blade.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="mb-0">
|
||||
<i class="ion ion-md-alert text-danger"></i> Payment Monitor
|
||||
</h4>
|
||||
<small class="text-muted">Stand: {{ now()->format('d.m.Y H:i') }}</small>
|
||||
</div>
|
||||
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="ion ion-md-code-working"></i> Entwickler-Ansicht
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Ampel-Karten --}}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
@php
|
||||
$level = $stats['critical_open'] > 0 ? 'danger' : ($stats['open_incidents'] > 0 ? 'warning' : 'success');
|
||||
$levelText = $stats['critical_open'] > 0 ? 'Kritische Störung!' : ($stats['open_incidents'] > 0 ? 'Offene Störungen' : 'Alles in Ordnung');
|
||||
@endphp
|
||||
<div class="card border-{{ $level }}">
|
||||
<div class="card-body text-center py-4">
|
||||
<i class="ion ion-md-{{ $level === 'success' ? 'checkmark-circle' : 'alert' }} text-{{ $level }}" style="font-size: 3rem"></i>
|
||||
<h5 class="mt-2 text-{{ $level }}">{{ $levelText }}</h5>
|
||||
<div class="display-4 font-weight-bold">{{ $stats['open_incidents'] }}</div>
|
||||
<div class="text-muted">Offene Störungen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card {{ $transactionStats['failed'] > 0 ? 'border-warning' : '' }}">
|
||||
<div class="card-body text-center py-4">
|
||||
<i class="ion ion-md-card text-{{ $transactionStats['success_rate'] >= 95 ? 'success' : ($transactionStats['success_rate'] >= 80 ? 'warning' : 'danger') }}" style="font-size: 3rem"></i>
|
||||
<h5 class="mt-2">Zahlungsquote</h5>
|
||||
<div class="display-4 font-weight-bold text-{{ $transactionStats['success_rate'] >= 95 ? 'success' : ($transactionStats['success_rate'] >= 80 ? 'warning' : 'danger') }}">
|
||||
{{ $transactionStats['success_rate'] }}%
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
{{ $transactionStats['failed'] }} fehlgeschlagen (30 Tage)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center py-4">
|
||||
<i class="ion ion-md-cash text-info" style="font-size: 3rem"></i>
|
||||
<h5 class="mt-2">Betroffener Umsatz</h5>
|
||||
<div class="display-4 font-weight-bold text-{{ $stats['total_affected_revenue'] > 0 ? 'warning' : 'success' }}">
|
||||
{{ number_format($stats['total_affected_revenue'], 0, ',', '.') }} €
|
||||
</div>
|
||||
<div class="text-muted">bei offenen Incidents</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Anbieter-Status --}}
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">Anbieter-Status</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
@foreach($providerStats as $key => $provider)
|
||||
<div class="col-sm-6 col-md-3 text-center mb-3">
|
||||
<div class="h5 mb-1">{{ $provider['label'] }}</div>
|
||||
@if($provider['open_incidents'] > 0)
|
||||
<span class="badge badge-danger badge-pill" style="font-size:1rem; padding: 0.5rem 1rem">
|
||||
<i class="ion ion-md-alert"></i> {{ $provider['open_incidents'] }} Störung(en)
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-success badge-pill" style="font-size:1rem; padding: 0.5rem 1rem">
|
||||
<i class="ion ion-md-checkmark"></i> OK
|
||||
</span>
|
||||
@endif
|
||||
<div class="text-muted small mt-1">{{ $provider['total_30d'] }} Incidents (30d)</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Aktive Störungen --}}
|
||||
@if($openIncidents->count() > 0)
|
||||
<div class="card mb-4 border-danger">
|
||||
<h6 class="card-header bg-danger text-white">
|
||||
<i class="ion ion-md-alert"></i> Aktive Störungen ({{ $openIncidents->count() }})
|
||||
</h6>
|
||||
<div class="card-body p-0">
|
||||
@include('admin.payment-dashboard._partials.incident-table', [
|
||||
'incidents' => $openIncidents,
|
||||
'showActions' => false,
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Letzte Vorfälle --}}
|
||||
<div class="card">
|
||||
<h6 class="card-header">Letzte Vorfälle</h6>
|
||||
<div class="card-body p-0">
|
||||
@include('admin.payment-dashboard._partials.incident-table', [
|
||||
'incidents' => $recentIncidents,
|
||||
'showActions' => false,
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
328
resources/views/admin/payment-dashboard/payments.blade.php
Normal file
328
resources/views/admin/payment-dashboard/payments.blade.php
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-sm btn-outline-secondary mr-2">
|
||||
<i class="ion ion-md-arrow-back"></i> Zurück
|
||||
</a>
|
||||
<strong>Zahlungs-Übersicht</strong>
|
||||
<small class="text-muted ml-2">ShoppingPayments mit Transaktionen und Bestellung</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Stat-Karten --}}
|
||||
<div class="row mb-4">
|
||||
<div class="col-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="text-muted small">Zahlungen gesamt</div>
|
||||
<div class="h4 font-weight-bold mb-0">{{ $paymentStats['total'] }}</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">{{ $paymentStats['days'] }} Tage</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="text-muted small">Bezahlt</div>
|
||||
<div class="h4 font-weight-bold mb-0 text-success">{{ $paymentStats['paid'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-2">
|
||||
<div class="card {{ $paymentStats['failed'] > 0 ? 'border-danger' : '' }}">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="text-muted small">Mit Fehler</div>
|
||||
<div class="h4 font-weight-bold mb-0 {{ $paymentStats['failed'] > 0 ? 'text-danger' : 'text-muted' }}">
|
||||
{{ $paymentStats['failed'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="text-muted small">Ausstehend</div>
|
||||
<div class="h4 font-weight-bold mb-0 text-warning">{{ $paymentStats['pending'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="text-muted small">Volumen gesamt</div>
|
||||
<div class="h5 font-weight-bold mb-0">{{ number_format($paymentStats['total_amount'], 2, ',', '.') }} €</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-2">
|
||||
<div class="card {{ $paymentStats['failed_amount'] > 0 ? 'border-danger' : '' }}">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="text-muted small">Fehlvolumen</div>
|
||||
<div class="h5 font-weight-bold mb-0 {{ $paymentStats['failed_amount'] > 0 ? 'text-danger' : 'text-muted' }}">
|
||||
{{ number_format($paymentStats['failed_amount'], 2, ',', '.') }} €
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" action="{{ route('admin.payment-dashboard.payments') }}" class="form-inline flex-wrap">
|
||||
<label class="mr-2 small">Zeitraum:</label>
|
||||
<select name="days" class="custom-select custom-select-sm mr-3 mb-1" onchange="this.form.submit()">
|
||||
@foreach([1 => 'Heute', 7 => '7 Tage', 14 => '14 Tage', 30 => '30 Tage', 0 => 'Alle'] as $val => $label)
|
||||
<option value="{{ $val }}" {{ (int)$days === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="mr-2 small">Status:</label>
|
||||
<select name="txaction" class="custom-select custom-select-sm mr-3 mb-1" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
@foreach(['paid' => 'Bezahlt', 'appointed' => 'Vorgemerkt', 'pending' => 'Ausstehend', 'failed' => 'Fehlgeschlagen'] as $val => $label)
|
||||
<option value="{{ $val }}" {{ $statusFilter === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="mr-2 small">Modus:</label>
|
||||
<select name="mode" class="custom-select custom-select-sm mr-3 mb-1" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
<option value="live" {{ $modeFilter === 'live' ? 'selected' : '' }}>Live</option>
|
||||
<option value="test" {{ $modeFilter === 'test' ? 'selected' : '' }}>Test</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Zahlungs-Tabelle --}}
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width:30px"></th>
|
||||
<th>Payment ID</th>
|
||||
<th>Bestellung</th>
|
||||
<th>Kunde</th>
|
||||
<th>Zahlart</th>
|
||||
<th>Betrag</th>
|
||||
<th>Status</th>
|
||||
<th>Modus</th>
|
||||
<th>Transaktionen</th>
|
||||
<th>Datum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($payments as $payment)
|
||||
@php
|
||||
$hasFailed = $payment->payment_transactions->where('txaction', 'failed')->count() > 0;
|
||||
$isPaid = $payment->txaction === 'paid';
|
||||
$rowClass = $hasFailed ? 'table-danger' : ($isPaid ? 'table-success' : '');
|
||||
@endphp
|
||||
<tr class="{{ $rowClass }}">
|
||||
<td class="text-center align-middle">
|
||||
@if($payment->payment_transactions->count() > 0)
|
||||
<button class="btn btn-sm btn-link p-0 text-muted"
|
||||
data-toggle="collapse"
|
||||
data-target="#tx-{{ $payment->id }}"
|
||||
title="{{ $payment->payment_transactions->count() }} Transaktionen">
|
||||
<i class="ion ion-md-arrow-dropdown"></i>
|
||||
</button>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<code class="small">{{ $payment->reference }}</code>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@if($payment->shopping_order)
|
||||
@php
|
||||
$isCustomerOrder = in_array($payment->shopping_order->payment_for, [6, 7]);
|
||||
$orderRoute = $isCustomerOrder
|
||||
? route('admin_sales_customers_detail', $payment->shopping_order->id)
|
||||
: route('admin_sales_users_detail', $payment->shopping_order->id);
|
||||
@endphp
|
||||
<a href="{{ $orderRoute }}" class="font-weight-bold" target="_blank">
|
||||
#{{ $payment->shopping_order->id }}
|
||||
</a>
|
||||
@if($payment->shopping_order->paid)
|
||||
<i class="ion ion-md-checkmark-circle text-success ml-1" title="Bezahlt"></i>
|
||||
@endif
|
||||
<div class="text-muted" style="font-size:0.7rem">
|
||||
{{ number_format($payment->shopping_order->total ?? 0, 2, ',', '.') }} €
|
||||
</div>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle small">
|
||||
@if($payment->shopping_order?->auth_user_id && $payment->shopping_order->auth_user)
|
||||
{{-- Berater-Bestellung --}}
|
||||
<span class="badge badge-primary mb-1">Berater</span>
|
||||
<div>{{ $payment->shopping_order->auth_user->name }}</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">
|
||||
{{ $payment->shopping_order->auth_user->email }}
|
||||
</div>
|
||||
@elseif($payment->shopping_order?->shopping_user)
|
||||
{{-- Kunden-Bestellung --}}
|
||||
@php $su = $payment->shopping_order->shopping_user; @endphp
|
||||
<span class="badge badge-info mb-1">Kunde</span>
|
||||
<div>
|
||||
{{ trim($su->billing_firstname . ' ' . $su->billing_lastname) ?: '—' }}
|
||||
</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">
|
||||
{{ $su->billing_email }}
|
||||
</div>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle small">
|
||||
{{ $payment->getPaymentType() }}
|
||||
@if($payment->clearingtype)
|
||||
<span class="badge badge-secondary ml-1">{{ $payment->clearingtype }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle text-nowrap">
|
||||
<strong>{{ number_format($payment->amount / 100, 2, ',', '.') }}</strong>
|
||||
<small class="text-muted">{{ $payment->currency }}</small>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@php
|
||||
$txColor = match($payment->txaction) {
|
||||
'paid' => 'success',
|
||||
'failed' => 'danger',
|
||||
'appointed' => 'info',
|
||||
'pending' => 'warning',
|
||||
default => 'secondary',
|
||||
};
|
||||
@endphp
|
||||
<span class="badge badge-{{ $txColor }}">{{ $payment->txaction ?? '—' }}</span>
|
||||
@if($hasFailed && $isPaid)
|
||||
<span class="badge badge-warning ml-1" title="Hatte fehlerhafte Transaktionen">
|
||||
<i class="ion ion-md-warning"></i>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@if($payment->mode)
|
||||
<span class="badge badge-{{ $payment->mode === 'test' ? 'warning' : 'light' }}">
|
||||
{{ $payment->mode }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
@if($payment->payment_transactions->count() > 0)
|
||||
<span class="badge badge-{{ $hasFailed ? 'danger' : 'secondary' }}">
|
||||
{{ $payment->payment_transactions->count() }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-muted">0</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle text-nowrap small text-muted">
|
||||
{{ $payment->created_at->format('d.m.Y H:i') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- Aufklappbare Transaktions-Sub-Tabelle --}}
|
||||
@if($payment->payment_transactions->count() > 0)
|
||||
<tr class="collapse" id="tx-{{ $payment->id }}">
|
||||
<td colspan="10" class="p-0">
|
||||
<div class="bg-light border-bottom px-3 py-2">
|
||||
<small class="font-weight-bold text-muted text-uppercase">
|
||||
Transaktionen zu Payment #{{ $payment->id }} / Referenz {{ $payment->reference }}
|
||||
</small>
|
||||
</div>
|
||||
<table class="table table-sm table-bordered mb-0" style="background:#fafafa">
|
||||
<thead>
|
||||
<tr class="bg-light">
|
||||
<th class="pl-4">TX-ID</th>
|
||||
<th>Aktion</th>
|
||||
<th>Status</th>
|
||||
<th>Fehlercode</th>
|
||||
<th>Fehlermeldung</th>
|
||||
<th>Kundennachricht</th>
|
||||
<th>Modus</th>
|
||||
<th>Datum</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($payment->payment_transactions as $tx)
|
||||
<tr class="{{ $tx->txaction === 'failed' ? 'table-danger' : ($tx->txaction === 'paid' ? 'table-success' : '') }}">
|
||||
<td class="pl-4 small">{{ $tx->txid ?? '—' }}</td>
|
||||
<td>
|
||||
@php
|
||||
$txaColor = match($tx->txaction) {
|
||||
'paid' => 'success', 'failed' => 'danger',
|
||||
'appointed' => 'info', 'pending' => 'warning',
|
||||
default => 'secondary',
|
||||
};
|
||||
@endphp
|
||||
<span class="badge badge-{{ $txaColor }}">{{ $tx->txaction ?? '—' }}</span>
|
||||
</td>
|
||||
<td class="small">{{ $tx->status ?? '—' }}</td>
|
||||
<td>
|
||||
@if($tx->errorcode)
|
||||
<span class="text-danger font-weight-bold">{{ $tx->errorcode }}</span>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="small text-muted" style="max-width:200px">
|
||||
{{ \Illuminate\Support\Str::limit($tx->errormessage, 60) }}
|
||||
</td>
|
||||
<td class="small text-muted" style="max-width:150px">
|
||||
{{ \Illuminate\Support\Str::limit($tx->customermessage, 50) }}
|
||||
</td>
|
||||
<td>
|
||||
@if($tx->mode)
|
||||
<span class="badge badge-{{ $tx->mode === 'test' ? 'warning' : 'light' }}">{{ $tx->mode }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-nowrap small text-muted">{{ $tx->created_at->format('d.m.Y H:i') }}</td>
|
||||
<td>
|
||||
@if($tx->transmitted_data)
|
||||
<button class="btn btn-xs btn-outline-secondary"
|
||||
data-toggle="collapse"
|
||||
data-target="#raw-{{ $tx->id }}"
|
||||
title="Rohdaten">
|
||||
<i class="ion ion-md-code"></i>
|
||||
</button>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@if($tx->transmitted_data)
|
||||
<tr class="collapse" id="raw-{{ $tx->id }}">
|
||||
<td colspan="9" class="bg-white">
|
||||
<pre class="mb-0 small" style="max-height:150px; overflow-y:auto; font-size:0.75rem">{{ json_encode($tx->transmitted_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="10" class="text-center text-muted py-4">
|
||||
Keine Zahlungen im gewählten Zeitraum gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $payments->links() }}
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
165
resources/views/admin/payment-dashboard/show.blade.php
Normal file
165
resources/views/admin/payment-dashboard/show.blade.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-sm btn-outline-secondary mr-2">
|
||||
<i class="ion ion-md-arrow-back"></i> Zurück
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge badge-{{ $incident->severity_color }} badge-pill px-3 py-2" style="font-size:0.9rem">
|
||||
{{ $incident->severity_label }}
|
||||
</span>
|
||||
<span class="badge badge-{{ $incident->status_color }} badge-pill px-3 py-2 ml-1" style="font-size:0.9rem">
|
||||
{{ $incident->status_label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{{-- Linke Spalte: Details + Timeline --}}
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<i class="ion {{ $incident->type_icon }} mr-2 text-secondary"></i>
|
||||
<h5 class="mb-0">{{ $incident->title }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4">
|
||||
<small class="text-muted d-block">Anbieter</small>
|
||||
<span class="badge badge-secondary">{{ $incident->provider_label }}</span>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<small class="text-muted d-block">Typ</small>
|
||||
{{ $incident->type_label }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<small class="text-muted d-block">Erkannt am</small>
|
||||
{{ $incident->detected_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4">
|
||||
<small class="text-muted d-block">Dauer</small>
|
||||
{{ $incident->duration }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<small class="text-muted d-block">Betroffene Bestellungen</small>
|
||||
{{ $incident->affected_orders }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<small class="text-muted d-block">Betroffener Umsatz</small>
|
||||
{{ number_format($incident->affected_revenue, 2, ',', '.') }} €
|
||||
</div>
|
||||
</div>
|
||||
@if($incident->ticket_number)
|
||||
<div class="mb-3">
|
||||
<small class="text-muted d-block">Ticket-Nummer</small>
|
||||
<code>{{ $incident->ticket_number }}</code>
|
||||
</div>
|
||||
@endif
|
||||
@if($incident->description)
|
||||
<div class="mb-3">
|
||||
<small class="text-muted d-block">Beschreibung</small>
|
||||
<p class="mb-0">{{ $incident->description }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($incident->notes)
|
||||
<div>
|
||||
<small class="text-muted d-block">Interne Notizen</small>
|
||||
<p class="mb-0 text-muted">{{ $incident->notes }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Aktivitäten-Timeline --}}
|
||||
<div class="card">
|
||||
<h6 class="card-header">Kommunikationsverlauf ({{ $incident->activities->count() }})</h6>
|
||||
<div class="card-body">
|
||||
@include('admin.payment-dashboard._partials.activity-timeline')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Rechte Spalte: Aktionen --}}
|
||||
<div class="col-lg-4">
|
||||
{{-- Status ändern --}}
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">Status ändern</h6>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.payment-dashboard.status.update', $incident) }}">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<div class="form-group">
|
||||
<select name="status" class="custom-select">
|
||||
@foreach(['open' => 'Offen', 'in_progress' => 'In Bearbeitung', 'waiting_provider' => 'Wartet auf Anbieter', 'resolved' => 'Gelöst', 'closed' => 'Geschlossen'] as $value => $label)
|
||||
<option value="{{ $value }}" {{ $incident->status === $value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Status speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Aktivität hinzufügen --}}
|
||||
<div class="card">
|
||||
<h6 class="card-header">Aktivität hinzufügen</h6>
|
||||
<div class="card-body">
|
||||
@if($errors->has('type') || $errors->has('title'))
|
||||
<div class="alert alert-danger small">
|
||||
@foreach($errors->all() as $error)
|
||||
<div>{{ $error }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<form method="POST" action="{{ route('admin.payment-dashboard.activity.store', $incident) }}">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label class="small">Typ</label>
|
||||
<select name="type" class="custom-select custom-select-sm">
|
||||
<option value="note">Notiz</option>
|
||||
<option value="email">E-Mail</option>
|
||||
<option value="call">Telefonat</option>
|
||||
<option value="ticket">Ticket</option>
|
||||
<option value="provider_response">Anbieter-Antwort</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="small">Titel</label>
|
||||
<input type="text" name="title" class="form-control form-control-sm"
|
||||
placeholder="Kurze Beschreibung" value="{{ old('title') }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="small">Details</label>
|
||||
<textarea name="content" class="form-control form-control-sm" rows="3"
|
||||
placeholder="Inhalt der Aktivität...">{{ old('content') }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary btn-block btn-sm">
|
||||
<i class="ion ion-md-add"></i> Aktivität speichern
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($incident->resolved_at)
|
||||
<div class="mt-3 text-center text-muted small">
|
||||
<i class="ion ion-md-checkmark-circle text-success"></i>
|
||||
Gelöst am {{ $incident->resolved_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
183
resources/views/admin/payment-dashboard/transactions.blade.php
Normal file
183
resources/views/admin/payment-dashboard/transactions.blade.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-sm btn-outline-secondary mr-2">
|
||||
<i class="ion ion-md-arrow-back"></i> Zurück
|
||||
</a>
|
||||
<a href="{{ route('admin.payment-dashboard.payments') }}" class="btn btn-sm btn-outline-primary mr-2">
|
||||
<i class="ion ion-md-card"></i> Zahlungen & Transaktionen
|
||||
</a>
|
||||
<strong>Rohe Transaktionen</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Stat-Karten --}}
|
||||
<div class="row mb-4">
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">Gesamt</div>
|
||||
<div class="display-4 font-weight-bold">{{ $transactionStats['total'] }}</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">letzte {{ $transactionStats['days'] }} Tage</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">Erfolgsrate</div>
|
||||
<div class="display-4 font-weight-bold text-{{ $transactionStats['success_rate'] >= 95 ? 'success' : ($transactionStats['success_rate'] >= 80 ? 'warning' : 'danger') }}">
|
||||
{{ $transactionStats['success_rate'] }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">Bezahlt</div>
|
||||
<div class="display-4 font-weight-bold text-success">{{ $transactionStats['paid'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card {{ $transactionStats['failed'] > 0 ? 'border-danger' : '' }}">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">Fehlgeschlagen</div>
|
||||
<div class="display-4 font-weight-bold {{ $transactionStats['failed'] > 0 ? 'text-danger' : 'text-muted' }}">
|
||||
{{ $transactionStats['failed'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($transactionStats['error_distribution']->count() > 0)
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">Fehlercodes (letzte {{ $transactionStats['days'] }} Tage)</h6>
|
||||
<div class="card-body py-2">
|
||||
@foreach($transactionStats['error_distribution'] as $error)
|
||||
<span class="badge badge-danger mr-2 mb-1" style="font-size:0.85rem; padding: 0.4rem 0.7rem">
|
||||
Code {{ $error->errorcode }}: {{ $error->count }}×
|
||||
@if($error->errormessage) — {{ \Illuminate\Support\Str::limit($error->errormessage, 60) }} @endif
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" action="{{ route('admin.payment-dashboard.transactions') }}" class="form-inline">
|
||||
<label class="mr-2 small">Zeitraum:</label>
|
||||
<select name="days" class="custom-select custom-select-sm mr-3" onchange="this.form.submit()">
|
||||
@foreach([1 => 'Heute', 7 => '7 Tage', 14 => '14 Tage', 30 => '30 Tage', 0 => 'Alle'] as $val => $label)
|
||||
<option value="{{ $val }}" {{ (int)$days === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="mr-2 small">Aktion:</label>
|
||||
<select name="txaction" class="custom-select custom-select-sm mr-3" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
@foreach(['paid' => 'Bezahlt', 'appointed' => 'Vorgemerkt', 'pending' => 'Ausstehend', 'failed' => 'Fehlgeschlagen'] as $val => $label)
|
||||
<option value="{{ $val }}" {{ $txactionFilter == $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Transaktions-Tabelle --}}
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Datum</th>
|
||||
<th>Aktion</th>
|
||||
<th>TX-ID</th>
|
||||
<th>Referenz</th>
|
||||
<th>Modus</th>
|
||||
<th>Fehlercode</th>
|
||||
<th>Fehlermeldung</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($transactions as $tx)
|
||||
<tr class="{{ $tx->txaction === 'failed' ? 'table-danger' : ($tx->txaction === 'paid' ? 'table-success' : '') }}">
|
||||
<td class="text-muted small">{{ $tx->id }}</td>
|
||||
<td class="text-nowrap small">{{ $tx->created_at->format('d.m.Y H:i') }}</td>
|
||||
<td>
|
||||
@php
|
||||
$actionColor = match($tx->txaction) {
|
||||
'paid' => 'success',
|
||||
'failed' => 'danger',
|
||||
'appointed' => 'info',
|
||||
'pending' => 'warning',
|
||||
default => 'secondary',
|
||||
};
|
||||
@endphp
|
||||
<span class="badge badge-{{ $actionColor }}">{{ $tx->txaction ?? '—' }}</span>
|
||||
</td>
|
||||
<td class="small">{{ $tx->txid ?? '—' }}</td>
|
||||
<td class="small">
|
||||
@if($tx->shopping_payment)
|
||||
<code>{{ $tx->shopping_payment->reference }}</code>
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($tx->mode)
|
||||
<span class="badge badge-{{ $tx->mode === 'test' ? 'warning' : 'secondary' }}">{{ $tx->mode }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($tx->errorcode)
|
||||
<span class="text-danger font-weight-bold">{{ $tx->errorcode }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="small text-muted" style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap">
|
||||
{{ $tx->errormessage }}
|
||||
@if($tx->customermessage)
|
||||
<span class="text-info">({{ \Illuminate\Support\Str::limit($tx->customermessage, 40) }})</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($tx->transmitted_data)
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button"
|
||||
data-toggle="collapse" data-target="#tx-data-{{ $tx->id }}">
|
||||
<i class="ion ion-md-code"></i>
|
||||
</button>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@if($tx->transmitted_data)
|
||||
<tr class="collapse" id="tx-data-{{ $tx->id }}">
|
||||
<td colspan="9" class="bg-light">
|
||||
<pre class="mb-0 small" style="max-height:200px; overflow-y:auto">{{ json_encode($tx->transmitted_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted py-3">Keine Transaktionen gefunden.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $transactions->links() }}
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
|
@ -138,6 +138,148 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Umsätze nach Ländern - Jährlich -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Umsätze nach Ländern {{ session('revenue_filter_year') }}</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if(isset($revenue_summary['country_yearly']) && $revenue_summary['country_yearly']->count() > 0)
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Land</th>
|
||||
<th class="text-right">Netto</th>
|
||||
<th class="text-right">Steuer</th>
|
||||
<th class="text-right">Brutto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($revenue_summary['country_yearly'] as $item)
|
||||
<tr>
|
||||
<td>{{ $item->country_name }}</td>
|
||||
<td class="text-right">{{ number_format($item->total_net, 2, ',', '.') }} €</td>
|
||||
<td class="text-right">{{ number_format($item->total_tax, 2, ',', '.') }} €</td>
|
||||
<td class="text-right"><strong>{{ number_format($item->total_gross, 2, ',', '.') }} €</strong></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div class="p-3"><p class="text-muted mb-0">Keine Umsätze nach Ländern für {{ session('revenue_filter_year') }} gefunden</p></div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gutschriften nach Ländern - Jährlich -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Gutschriften nach Ländern {{ session('revenue_filter_year') }}</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if(isset($credit_summary['country_yearly']) && $credit_summary['country_yearly']->count() > 0)
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Land</th>
|
||||
<th class="text-right">Netto</th>
|
||||
<th class="text-right">Steuer</th>
|
||||
<th class="text-right">Brutto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($credit_summary['country_yearly'] as $item)
|
||||
<tr>
|
||||
<td>{{ $item->country_name }}</td>
|
||||
<td class="text-right">{{ number_format($item->total_net, 2, ',', '.') }} €</td>
|
||||
<td class="text-right">{{ number_format($item->total_tax, 2, ',', '.') }} €</td>
|
||||
<td class="text-right"><strong>{{ number_format($item->total_gross, 2, ',', '.') }} €</strong></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div class="p-3"><p class="text-muted mb-0">Keine Gutschriften nach Ländern für {{ session('revenue_filter_year') }} gefunden</p></div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Umsätze nach Ländern - Monatlich -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Umsätze nach Ländern – monatliche Aufschlüsselung</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if(isset($revenue_summary['country_monthly']) && $revenue_summary['country_monthly']->count() > 0)
|
||||
@php $revenueByMonth = $revenue_summary['country_monthly']->groupBy('month'); @endphp
|
||||
@foreach($revenueByMonth as $month => $countries)
|
||||
<div class="px-3 pt-2 pb-1">
|
||||
<strong class="text-primary">{{ $countries->first()->month_label }}</strong>
|
||||
</div>
|
||||
<table class="table table-sm mb-1">
|
||||
<tbody>
|
||||
@foreach($countries as $item)
|
||||
<tr>
|
||||
<td class="pl-4">{{ $item->country_name }}</td>
|
||||
<td class="text-right text-muted"><small>{{ number_format($item->total_net, 2, ',', '.') }} €</small></td>
|
||||
<td class="text-right text-muted"><small>{{ number_format($item->total_tax, 2, ',', '.') }} €</small></td>
|
||||
<td class="text-right"><small><strong>{{ number_format($item->total_gross, 2, ',', '.') }} €</strong></small></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="p-3"><p class="text-muted mb-0">Keine monatlichen Umsätze nach Ländern gefunden</p></div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gutschriften nach Ländern - Monatlich -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Gutschriften nach Ländern – monatliche Aufschlüsselung</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if(isset($credit_summary['country_monthly']) && $credit_summary['country_monthly']->count() > 0)
|
||||
@php $creditByMonth = $credit_summary['country_monthly']->groupBy('month'); @endphp
|
||||
@foreach($creditByMonth as $month => $countries)
|
||||
<div class="px-3 pt-2 pb-1">
|
||||
<strong class="text-primary">{{ $countries->first()->month_label }}</strong>
|
||||
</div>
|
||||
<table class="table table-sm mb-1">
|
||||
<tbody>
|
||||
@foreach($countries as $item)
|
||||
<tr>
|
||||
<td class="pl-4">{{ $item->country_name }}</td>
|
||||
<td class="text-right text-muted"><small>{{ number_format($item->total_net, 2, ',', '.') }} €</small></td>
|
||||
<td class="text-right text-muted"><small>{{ number_format($item->total_tax, 2, ',', '.') }} €</small></td>
|
||||
<td class="text-right"><small><strong>{{ number_format($item->total_gross, 2, ',', '.') }} €</strong></small></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="p-3"><p class="text-muted mb-0">Keine monatlichen Gutschriften nach Ländern gefunden</p></div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue