mivita/resources/views/admin/payment-dashboard/funnel.blade.php
2026-04-14 18:07:45 +02:00

302 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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