457 lines
29 KiB
PHP
457 lines
29 KiB
PHP
@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
|