14-04-2026

This commit is contained in:
Kevin Adametz 2026-04-14 18:07:45 +02:00
parent f58c709945
commit 0f82fea88a
72 changed files with 7414 additions and 148 deletions

View 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