14-04-2026
This commit is contained in:
parent
f58c709945
commit
0f82fea88a
72 changed files with 7414 additions and 148 deletions
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue