mivita/resources/views/admin/payment-dashboard/transactions.blade.php
Kevin 8e09aaefe8 Stripe-Integration (S7.4): Payment-Dashboard für Stripe
- Provider-Status/Uptime: Stripe-Kachel aktiviert; payment:check-uptime prüft
  zusätzlich api.stripe.com/v1/ (401 ohne Key = erreichbar) inkl. Auto-Incident
- Zahlungen: Anbieter-Filter, Provider-Badges, Umsatz je Provider,
  Stripe-Dashboard-Deep-Links (ShoppingPayment/PaymentTransaction::
  stripeDashboardUrl, pi_-Guard, testmodus-bewusst)
- Rohe Transaktionen: Anbieter-Filter/-Spalte, Erfolgsraten je Anbieter;
  Bugfix: Stringable-Vergleich ließ den txaction-Filter immer mit leerem Wert
  greifen — Liste war dauerhaft leer
- Neue Kachel „Abos je Anbieter" (Index + GF-Ansicht) mit Fehlerzählern und
  Stripe-Anteil-Fortschrittsbalken (Auslaufmodell PAYONE → Stripe ablesbar)
- Incident-Kachel weist Stripe/PAYONE getrennt aus; Log-Viewer mit
  Kanal-Umschalter stripe/payone
- Plan-Doku: S7.4-Umsetzungsstatus, Abo-Go-Live 2026-07-10 vermerkt

Tests: PaymentDashboardProviderTest (9 neu), Dashboard-Suite 49 grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:57:50 +00:00

228 lines
12 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">
<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 &amp; 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>
{{-- Erfolgsraten je Provider --}}
@if(count($transactionStats['providers']) > 0)
<div class="card mb-4">
<h6 class="card-header">Erfolgsraten je Anbieter (letzte {{ $transactionStats['days'] }} Tage)</h6>
<div class="card-body py-2">
<div class="row">
@foreach($transactionStats['providers'] as $providerKey => $providerRow)
<div class="col-sm-6 col-md-3 mb-2">
<div class="d-flex justify-content-between align-items-center">
<strong>{{ $providerRow['label'] }}</strong>
<span class="badge badge-{{ $providerRow['success_rate'] >= 95 ? 'success' : ($providerRow['success_rate'] >= 80 ? 'warning' : 'danger') }}">
{{ $providerRow['success_rate'] }}%
</span>
</div>
<div class="text-muted small">
{{ $providerRow['paid'] }} bezahlt / {{ $providerRow['failed'] }} fehlgeschlagen / {{ $providerRow['total'] }} gesamt
</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
@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) &mdash; {{ \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">Anbieter:</label>
<select name="provider" class="custom-select custom-select-sm mr-3" onchange="this.form.submit()">
<option value="">Alle</option>
@foreach(['stripe' => 'Stripe', 'payone' => 'PAYONE'] as $val => $label)
<option value="{{ $val }}" {{ $providerFilter === $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>Anbieter</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>
@if($tx->provider === 'stripe')
<span class="badge badge-primary">Stripe</span>
@else
<span class="badge badge-secondary">PAYONE</span>
@endif
</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">
@if($txStripeUrl = $tx->stripeDashboardUrl())
<a href="{{ $txStripeUrl }}" target="_blank" rel="noopener" title="Im Stripe-Dashboard öffnen">{{ $tx->txid }}</a>
@else
{{ $tx->txid ?? '—' }}
@endif
</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="10" 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="10" 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