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,45 @@
<div class="timeline">
@forelse($incident->activities as $activity)
<div class="timeline-item {{ $activity->type === 'status_change' ? 'timeline-item-secondary' : '' }}">
<div class="timeline-indicator bg-{{ match($activity->type) {
'status_change' => 'info',
'email' => 'primary',
'call' => 'success',
'ticket' => 'warning',
'provider_response' => 'secondary',
default => 'light border'
} }}">
<i class="ion {{ $activity->type_icon }}"></i>
</div>
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start">
<div>
<span class="badge badge-light">{{ $activity->type_label }}</span>
<strong class="ml-1">{{ $activity->title }}</strong>
</div>
<small class="text-muted text-nowrap ml-2">
{{ $activity->created_at->format('d.m.Y H:i') }}
&mdash; {{ $activity->author }}
</small>
</div>
@if($activity->content)
<div class="mt-1 text-muted">{{ $activity->content }}</div>
@endif
</div>
</div>
@empty
<p class="text-muted">Noch keine Aktivitäten.</p>
@endforelse
</div>
<style>
.timeline { position: relative; padding-left: 2.5rem; }
.timeline::before { content: ''; position: absolute; left: 1rem; top: 0; bottom: 0; width: 2px; background: #e9ecef; }
.timeline-item { position: relative; margin-bottom: 1.25rem; }
.timeline-indicator {
position: absolute; left: -2.5rem; width: 2rem; height: 2rem;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-size: 0.875rem; color: #fff;
}
.timeline-content { background: #f8f9fa; border-radius: 0.375rem; padding: 0.75rem 1rem; }
</style>

View file

@ -0,0 +1,100 @@
<div class="modal fade" id="createIncidentModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Neuen Incident anlegen</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form method="POST" action="{{ route('admin.payment-dashboard.store') }}">
@csrf
<div class="modal-body">
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="form-row">
<div class="form-group col-md-8">
<label>Titel <span class="text-danger">*</span></label>
<input type="text" name="title" class="form-control" value="{{ old('title') }}" required>
</div>
<div class="form-group col-md-4">
<label>Erkannt am <span class="text-danger">*</span></label>
<input type="datetime-local" name="detected_at" class="form-control"
value="{{ old('detected_at', now()->format('Y-m-d\TH:i')) }}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label>Anbieter <span class="text-danger">*</span></label>
<select name="provider" class="custom-select" required>
<option value="payone" {{ old('provider', 'payone') === 'payone' ? 'selected' : '' }}>PAYONE</option>
{{-- <option value="stripe" {{ old('provider') === 'stripe' ? 'selected' : '' }}>Stripe</option> --}}
<option value="paypal" {{ old('provider') === 'paypal' ? 'selected' : '' }}>PayPal</option>
{{-- <option value="mollie" {{ old('provider') === 'mollie' ? 'selected' : '' }}>Mollie</option> --}}
<option value="other" {{ old('provider') === 'other' ? 'selected' : '' }}>Sonstige</option>
</select>
</div>
<div class="form-group col-md-4">
<label>Typ <span class="text-danger">*</span></label>
<select name="type" class="custom-select" required>
<option value="outage" {{ old('type') === 'outage' ? 'selected' : '' }}>Ausfall</option>
<option value="ipn_error" {{ old('type') === 'ipn_error' ? 'selected' : '' }}>IPN-Fehler</option>
<option value="payment_failure" {{ old('type', 'payment_failure') === 'payment_failure' ? 'selected' : '' }}>Zahlungsfehler</option>
<option value="slow_response" {{ old('type') === 'slow_response' ? 'selected' : '' }}>Langsame Antwort</option>
<option value="other" {{ old('type') === 'other' ? 'selected' : '' }}>Sonstiges</option>
</select>
</div>
<div class="form-group col-md-4">
<label>Schwere <span class="text-danger">*</span></label>
<select name="severity" class="custom-select" required>
<option value="low" {{ old('severity') === 'low' ? 'selected' : '' }}>Niedrig</option>
<option value="medium" {{ old('severity', 'medium') === 'medium' ? 'selected' : '' }}>Mittel</option>
<option value="high" {{ old('severity') === 'high' ? 'selected' : '' }}>Hoch</option>
<option value="critical" {{ old('severity') === 'critical' ? 'selected' : '' }}>Kritisch</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label>Betroffene Bestellungen</label>
<input type="number" name="affected_orders" class="form-control" min="0"
value="{{ old('affected_orders', 0) }}">
</div>
<div class="form-group col-md-4">
<label>Betroffener Umsatz ()</label>
<input type="number" name="affected_revenue" class="form-control" min="0" step="0.01"
value="{{ old('affected_revenue', '0.00') }}">
</div>
<div class="form-group col-md-4">
<label>Ticket-Nummer</label>
<input type="text" name="ticket_number" class="form-control"
value="{{ old('ticket_number') }}" placeholder="z.B. PAYONE-12345">
</div>
</div>
<div class="form-group">
<label>Beschreibung</label>
<textarea name="description" class="form-control" rows="3"
placeholder="Was ist passiert?">{{ old('description') }}</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-danger">
<i class="ion ion-md-alert"></i> Incident anlegen
</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,71 @@
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead class="thead-light">
<tr>
<th>Schwere</th>
<th>Titel</th>
<th>Anbieter</th>
<th>Typ</th>
<th>Status</th>
<th>Erkannt</th>
<th>Dauer</th>
@if(isset($showActions) && $showActions)
<th></th>
@endif
</tr>
</thead>
<tbody>
@forelse($incidents as $incident)
<tr>
<td>
<span class="badge badge-{{ $incident->severity_color }}">
{{ $incident->severity_label }}
</span>
</td>
<td>
<a href="{{ route('admin.payment-dashboard.show', $incident) }}">
{{ $incident->title }}
</a>
@if($incident->ticket_number)
<span class="text-muted small ml-1">#{{ $incident->ticket_number }}</span>
@endif
</td>
<td><span class="badge badge-secondary">{{ $incident->provider_label }}</span></td>
<td>
<i class="ion {{ $incident->type_icon }}"></i>
{{ $incident->type_label }}
</td>
<td>
<span class="badge badge-{{ $incident->status_color }}">
{{ $incident->status_label }}
</span>
</td>
<td class="text-nowrap">{{ $incident->detected_at->format('d.m.Y H:i') }}</td>
<td class="text-nowrap">{{ $incident->duration }}</td>
@if(isset($showActions) && $showActions)
<td>
<div class="d-flex gap-1">
<form method="POST" action="{{ route('admin.payment-dashboard.status.update', $incident) }}" class="d-inline">
@csrf
@method('PATCH')
<select name="status" class="custom-select custom-select-sm" onchange="this.form.submit()" style="width:auto">
@foreach(['open' => 'Offen', 'in_progress' => 'In Bearb.', 'waiting_provider' => 'Wartet', 'resolved' => 'Gelöst', 'closed' => 'Geschl.'] as $value => $label)
<option value="{{ $value }}" {{ $incident->status === $value ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
</form>
<a href="{{ route('admin.payment-dashboard.show', $incident) }}" class="btn btn-sm btn-outline-secondary">
<i class="ion ion-md-open"></i>
</a>
</div>
</td>
@endif
</tr>
@empty
<tr>
<td colspan="8" class="text-center text-muted py-3">Keine Incidents vorhanden.</td>
</tr>
@endforelse
</tbody>
</table>
</div>

View file

@ -0,0 +1,64 @@
<div class="row mb-4">
<div class="col-sm-6 col-xl-2">
<div class="card {{ $stats['critical_open'] > 0 ? 'border-danger' : '' }}">
<div class="card-body text-center">
<div class="text-muted small">Offene Incidents</div>
<div class="display-4 font-weight-bold {{ $stats['open_incidents'] > 0 ? 'text-danger' : 'text-success' }}">
{{ $stats['open_incidents'] }}
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-2">
<div class="card">
<div class="card-body text-center">
<div class="text-muted small">In Bearbeitung</div>
<div class="display-4 font-weight-bold {{ $stats['in_progress'] > 0 ? 'text-warning' : 'text-muted' }}">
{{ $stats['in_progress'] }}
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-2">
<div class="card">
<div class="card-body text-center">
<div class="text-muted small">PAYONE (30 Tage)</div>
<div class="display-4 font-weight-bold text-info">
{{ $stats['payone_incidents_30d'] }}
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-2">
<div class="card">
<div class="card-body text-center">
<div class="text-muted small">Gelöst (Monat)</div>
<div class="display-4 font-weight-bold text-success">
{{ $stats['resolved_this_month'] }}
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-2">
<div class="card">
<div class="card-body text-center">
<div class="text-muted small">Erfolgsrate Zahlung</div>
<div class="display-4 font-weight-bold {{ $transactionStats['success_rate'] < 90 ? 'text-warning' : 'text-success' }}">
{{ $transactionStats['success_rate'] }}%
</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-2">
<div class="card {{ $transactionStats['failed'] > 0 ? 'border-warning' : '' }}">
<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-success' }}">
{{ $transactionStats['failed'] }}
</div>
<div class="text-muted" style="font-size:0.7rem">letzte {{ $transactionStats['days'] }} Tage</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,457 @@
@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

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

View file

@ -0,0 +1,143 @@
@extends('layouts.layout-2')
@section('content')
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show">
{{ session('success') }}
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
@endif
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h4 class="mb-0">
<i class="ion ion-md-alert text-danger"></i> Payment Monitor
</h4>
<small class="text-muted">Entwickler-Ansicht &mdash; {{ now()->format('d.m.Y H:i') }}</small>
</div>
<div>
<button type="button" class="btn btn-danger btn-sm" data-toggle="modal" data-target="#createIncidentModal">
<i class="ion ion-md-add"></i> Neuer Incident
</button>
</div>
</div>
{{-- Stat-Karten --}}
@include('admin.payment-dashboard._partials.stats-cards')
{{-- Anbieter-Status --}}
<div class="row mb-4">
@foreach($providerStats as $key => $provider)
@php $uptime = $uptimeStats[$key] ?? null; @endphp
<div class="col-sm-6 col-xl-3">
<div class="card {{ $provider['open_incidents'] > 0 ? 'border-danger' : '' }}">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center">
<strong>{{ $provider['label'] }}</strong>
<div>
@if($uptime && $uptime['last_check'])
@if($uptime['last_check']->is_up)
<span class="badge badge-success mr-1" title="Letzte Prüfung: {{ $uptime['last_check']->checked_at->diffForHumans() }}">
<i class="ion ion-md-checkmark"></i> Online
</span>
@else
<span class="badge badge-danger mr-1" title="{{ $uptime['last_check']->error_message }}">
<i class="ion ion-md-close"></i> Offline
</span>
@endif
@endif
@if($provider['open_incidents'] > 0)
<span class="badge badge-warning">{{ $provider['open_incidents'] }} Incident</span>
@endif
</div>
</div>
<div class="text-muted small mt-1">{{ $provider['total_30d'] }} Incidents (30 Tage)</div>
@if($uptime && $uptime['checks_24h'] > 0)
<div class="text-muted" style="font-size:0.7rem">
Uptime 24h: {{ $uptime['uptime_24h'] }}%
@if($uptime['failures_24h'] > 0)
&mdash; <span class="text-danger">{{ $uptime['failures_24h'] }} Ausfälle</span>
@endif
</div>
@elseif($uptime)
<div class="text-muted" style="font-size:0.7rem">Noch keine Uptime-Daten</div>
@endif
</div>
</div>
</div>
@endforeach
</div>
{{-- Tabs --}}
<ul class="nav nav-tabs" id="dashboardTabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#tab-open">
Offene Incidents
@if($openIncidents->count() > 0)
<span class="badge badge-danger ml-1">{{ $openIncidents->count() }}</span>
@endif
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#tab-all">Alle Incidents</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.payment-dashboard.payments') }}">
<i class="ion ion-md-card"></i> Zahlungen &amp; Transaktionen
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.payment-dashboard.transactions') }}">
<i class="ion ion-md-swap"></i> Rohe Transaktionen
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.payment-dashboard.funnel') }}">
<i class="ion ion-md-funnel"></i> Funnel-Tracking
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.payment-dashboard.abandoned') }}">
<i class="ion ion-md-alert"></i> Abbruch-Analyse
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.payment-dashboard.logs') }}">
<i class="ion ion-md-list"></i> PAYONE Logs
</a>
</li>
</ul>
<div class="tab-content">
{{-- Tab: Offene Incidents --}}
<div class="tab-pane fade show active pt-3" id="tab-open">
<div class="card">
<div class="card-body p-0">
@include('admin.payment-dashboard._partials.incident-table', [
'incidents' => $openIncidents,
'showActions' => true,
])
</div>
</div>
</div>
{{-- Tab: Alle Incidents --}}
<div class="tab-pane fade pt-3" id="tab-all">
<div class="card">
<div class="card-body p-0">
@include('admin.payment-dashboard._partials.incident-table', [
'incidents' => $allIncidents,
'showActions' => true,
])
</div>
</div>
<div class="mt-3">
{{ $allIncidents->links() }}
</div>
</div>
</div>
@include('admin.payment-dashboard._partials.create-incident-modal')
@endsection

View file

@ -0,0 +1,86 @@
@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>PAYONE Log-Viewer</strong>
</div>
@if(count($availableDates) > 1)
<form method="GET" action="{{ route('admin.payment-dashboard.logs') }}">
<select name="date" class="custom-select custom-select-sm" onchange="this.form.submit()">
@foreach($availableDates as $date)
<option value="{{ $date }}" {{ $selectedDate === $date ? 'selected' : '' }}>
{{ \Carbon\Carbon::parse($date)->format('d.m.Y') }}
</option>
@endforeach
</select>
</form>
@endif
</div>
@if(count($entries) === 0)
<div class="alert alert-info">
<i class="ion ion-md-information-circle"></i>
Keine Log-Einträge für {{ \Carbon\Carbon::parse($selectedDate)->format('d.m.Y') }} gefunden.
@if(count($availableDates) > 0)
Verfügbare Daten: {{ implode(', ', array_map(fn($d) => \Carbon\Carbon::parse($d)->format('d.m.Y'), $availableDates)) }}
@else
Der Log-Kanal <code>payone</code> hat noch keine Einträge geschrieben.
@endif
</div>
@else
<div class="mb-2 text-muted small">
<i class="ion ion-md-list"></i> {{ count($entries) }} Einträge (neueste zuerst)
</div>
{{-- Filter --}}
<div class="mb-3">
<input type="text" id="logFilter" class="form-control form-control-sm"
placeholder="Filter: Stichwort oder Fehlercode (z.B. Error:2003)..."
oninput="filterLogs(this.value)">
</div>
<div class="card">
<div class="card-body p-0">
<div id="logEntries">
@foreach($entries as $entry)
@php
$levelColor = match($entry['level']) {
'error' => 'danger',
'warning' => 'warning',
'info' => 'info',
'notice' => 'secondary',
default => 'secondary',
};
@endphp
<div class="log-entry border-bottom px-3 py-2 {{ $entry['level'] === 'error' ? 'bg-light' : '' }}"
data-search="{{ strtolower($entry['timestamp'] . ' ' . $entry['level'] . ' ' . $entry['message']) }}">
<div class="d-flex align-items-start">
<span class="badge badge-{{ $levelColor }} mr-2 mt-1 flex-shrink-0">{{ strtoupper($entry['level']) }}</span>
<div style="min-width:0">
<div class="text-muted small">{{ $entry['timestamp'] }}</div>
<div class="small" style="word-break:break-all; font-family:monospace">{{ $entry['message'] }}</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
<script>
function filterLogs(term) {
const entries = document.querySelectorAll('.log-entry');
const search = term.toLowerCase();
entries.forEach(el => {
el.style.display = el.dataset.search.includes(search) ? '' : 'none';
});
}
</script>
@endsection

View file

@ -0,0 +1,111 @@
@extends('layouts.layout-2')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-0">
<i class="ion ion-md-alert text-danger"></i> Payment Monitor
</h4>
<small class="text-muted">Stand: {{ now()->format('d.m.Y H:i') }}</small>
</div>
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-outline-secondary btn-sm">
<i class="ion ion-md-code-working"></i> Entwickler-Ansicht
</a>
</div>
{{-- Ampel-Karten --}}
<div class="row mb-4">
<div class="col-md-4">
@php
$level = $stats['critical_open'] > 0 ? 'danger' : ($stats['open_incidents'] > 0 ? 'warning' : 'success');
$levelText = $stats['critical_open'] > 0 ? 'Kritische Störung!' : ($stats['open_incidents'] > 0 ? 'Offene Störungen' : 'Alles in Ordnung');
@endphp
<div class="card border-{{ $level }}">
<div class="card-body text-center py-4">
<i class="ion ion-md-{{ $level === 'success' ? 'checkmark-circle' : 'alert' }} text-{{ $level }}" style="font-size: 3rem"></i>
<h5 class="mt-2 text-{{ $level }}">{{ $levelText }}</h5>
<div class="display-4 font-weight-bold">{{ $stats['open_incidents'] }}</div>
<div class="text-muted">Offene Störungen</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card {{ $transactionStats['failed'] > 0 ? 'border-warning' : '' }}">
<div class="card-body text-center py-4">
<i class="ion ion-md-card text-{{ $transactionStats['success_rate'] >= 95 ? 'success' : ($transactionStats['success_rate'] >= 80 ? 'warning' : 'danger') }}" style="font-size: 3rem"></i>
<h5 class="mt-2">Zahlungsquote</h5>
<div class="display-4 font-weight-bold text-{{ $transactionStats['success_rate'] >= 95 ? 'success' : ($transactionStats['success_rate'] >= 80 ? 'warning' : 'danger') }}">
{{ $transactionStats['success_rate'] }}%
</div>
<div class="text-muted">
{{ $transactionStats['failed'] }} fehlgeschlagen (30 Tage)
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body text-center py-4">
<i class="ion ion-md-cash text-info" style="font-size: 3rem"></i>
<h5 class="mt-2">Betroffener Umsatz</h5>
<div class="display-4 font-weight-bold text-{{ $stats['total_affected_revenue'] > 0 ? 'warning' : 'success' }}">
{{ number_format($stats['total_affected_revenue'], 0, ',', '.') }}
</div>
<div class="text-muted">bei offenen Incidents</div>
</div>
</div>
</div>
</div>
{{-- Anbieter-Status --}}
<div class="card mb-4">
<h6 class="card-header">Anbieter-Status</h6>
<div class="card-body">
<div class="row">
@foreach($providerStats as $key => $provider)
<div class="col-sm-6 col-md-3 text-center mb-3">
<div class="h5 mb-1">{{ $provider['label'] }}</div>
@if($provider['open_incidents'] > 0)
<span class="badge badge-danger badge-pill" style="font-size:1rem; padding: 0.5rem 1rem">
<i class="ion ion-md-alert"></i> {{ $provider['open_incidents'] }} Störung(en)
</span>
@else
<span class="badge badge-success badge-pill" style="font-size:1rem; padding: 0.5rem 1rem">
<i class="ion ion-md-checkmark"></i> OK
</span>
@endif
<div class="text-muted small mt-1">{{ $provider['total_30d'] }} Incidents (30d)</div>
</div>
@endforeach
</div>
</div>
</div>
{{-- Aktive Störungen --}}
@if($openIncidents->count() > 0)
<div class="card mb-4 border-danger">
<h6 class="card-header bg-danger text-white">
<i class="ion ion-md-alert"></i> Aktive Störungen ({{ $openIncidents->count() }})
</h6>
<div class="card-body p-0">
@include('admin.payment-dashboard._partials.incident-table', [
'incidents' => $openIncidents,
'showActions' => false,
])
</div>
</div>
@endif
{{-- Letzte Vorfälle --}}
<div class="card">
<h6 class="card-header">Letzte Vorfälle</h6>
<div class="card-body p-0">
@include('admin.payment-dashboard._partials.incident-table', [
'incidents' => $recentIncidents,
'showActions' => false,
])
</div>
</div>
@endsection

View file

@ -0,0 +1,328 @@
@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>Zahlungs-Übersicht</strong>
<small class="text-muted ml-2">ShoppingPayments mit Transaktionen und Bestellung</small>
</div>
</div>
{{-- Stat-Karten --}}
<div class="row mb-4">
<div class="col-6 col-xl-2">
<div class="card">
<div class="card-body text-center py-2">
<div class="text-muted small">Zahlungen gesamt</div>
<div class="h4 font-weight-bold mb-0">{{ $paymentStats['total'] }}</div>
<div class="text-muted" style="font-size:0.7rem">{{ $paymentStats['days'] }} Tage</div>
</div>
</div>
</div>
<div class="col-6 col-xl-2">
<div class="card">
<div class="card-body text-center py-2">
<div class="text-muted small">Bezahlt</div>
<div class="h4 font-weight-bold mb-0 text-success">{{ $paymentStats['paid'] }}</div>
</div>
</div>
</div>
<div class="col-6 col-xl-2">
<div class="card {{ $paymentStats['failed'] > 0 ? 'border-danger' : '' }}">
<div class="card-body text-center py-2">
<div class="text-muted small">Mit Fehler</div>
<div class="h4 font-weight-bold mb-0 {{ $paymentStats['failed'] > 0 ? 'text-danger' : 'text-muted' }}">
{{ $paymentStats['failed'] }}
</div>
</div>
</div>
</div>
<div class="col-6 col-xl-2">
<div class="card">
<div class="card-body text-center py-2">
<div class="text-muted small">Ausstehend</div>
<div class="h4 font-weight-bold mb-0 text-warning">{{ $paymentStats['pending'] }}</div>
</div>
</div>
</div>
<div class="col-6 col-xl-2">
<div class="card">
<div class="card-body text-center py-2">
<div class="text-muted small">Volumen gesamt</div>
<div class="h5 font-weight-bold mb-0">{{ number_format($paymentStats['total_amount'], 2, ',', '.') }} </div>
</div>
</div>
</div>
<div class="col-6 col-xl-2">
<div class="card {{ $paymentStats['failed_amount'] > 0 ? 'border-danger' : '' }}">
<div class="card-body text-center py-2">
<div class="text-muted small">Fehlvolumen</div>
<div class="h5 font-weight-bold mb-0 {{ $paymentStats['failed_amount'] > 0 ? 'text-danger' : 'text-muted' }}">
{{ number_format($paymentStats['failed_amount'], 2, ',', '.') }}
</div>
</div>
</div>
</div>
</div>
{{-- Filter --}}
<div class="card mb-3">
<div class="card-body py-2">
<form method="GET" action="{{ route('admin.payment-dashboard.payments') }}" class="form-inline flex-wrap">
<label class="mr-2 small">Zeitraum:</label>
<select name="days" class="custom-select custom-select-sm mr-3 mb-1" 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">Status:</label>
<select name="txaction" class="custom-select custom-select-sm mr-3 mb-1" onchange="this.form.submit()">
<option value="">Alle</option>
@foreach(['paid' => 'Bezahlt', 'appointed' => 'Vorgemerkt', 'pending' => 'Ausstehend', 'failed' => 'Fehlgeschlagen'] as $val => $label)
<option value="{{ $val }}" {{ $statusFilter === $val ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
<label class="mr-2 small">Modus:</label>
<select name="mode" class="custom-select custom-select-sm mr-3 mb-1" onchange="this.form.submit()">
<option value="">Alle</option>
<option value="live" {{ $modeFilter === 'live' ? 'selected' : '' }}>Live</option>
<option value="test" {{ $modeFilter === 'test' ? 'selected' : '' }}>Test</option>
</select>
</form>
</div>
</div>
{{-- Zahlungs-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 style="width:30px"></th>
<th>Payment ID</th>
<th>Bestellung</th>
<th>Kunde</th>
<th>Zahlart</th>
<th>Betrag</th>
<th>Status</th>
<th>Modus</th>
<th>Transaktionen</th>
<th>Datum</th>
</tr>
</thead>
<tbody>
@forelse($payments as $payment)
@php
$hasFailed = $payment->payment_transactions->where('txaction', 'failed')->count() > 0;
$isPaid = $payment->txaction === 'paid';
$rowClass = $hasFailed ? 'table-danger' : ($isPaid ? 'table-success' : '');
@endphp
<tr class="{{ $rowClass }}">
<td class="text-center align-middle">
@if($payment->payment_transactions->count() > 0)
<button class="btn btn-sm btn-link p-0 text-muted"
data-toggle="collapse"
data-target="#tx-{{ $payment->id }}"
title="{{ $payment->payment_transactions->count() }} Transaktionen">
<i class="ion ion-md-arrow-dropdown"></i>
</button>
@endif
</td>
<td class="align-middle">
<code class="small">{{ $payment->reference }}</code>
</td>
<td class="align-middle">
@if($payment->shopping_order)
@php
$isCustomerOrder = in_array($payment->shopping_order->payment_for, [6, 7]);
$orderRoute = $isCustomerOrder
? route('admin_sales_customers_detail', $payment->shopping_order->id)
: route('admin_sales_users_detail', $payment->shopping_order->id);
@endphp
<a href="{{ $orderRoute }}" class="font-weight-bold" target="_blank">
#{{ $payment->shopping_order->id }}
</a>
@if($payment->shopping_order->paid)
<i class="ion ion-md-checkmark-circle text-success ml-1" title="Bezahlt"></i>
@endif
<div class="text-muted" style="font-size:0.7rem">
{{ number_format($payment->shopping_order->total ?? 0, 2, ',', '.') }}
</div>
@else
<span class="text-muted"></span>
@endif
</td>
<td class="align-middle small">
@if($payment->shopping_order?->auth_user_id && $payment->shopping_order->auth_user)
{{-- Berater-Bestellung --}}
<span class="badge badge-primary mb-1">Berater</span>
<div>{{ $payment->shopping_order->auth_user->name }}</div>
<div class="text-muted" style="font-size:0.7rem">
{{ $payment->shopping_order->auth_user->email }}
</div>
@elseif($payment->shopping_order?->shopping_user)
{{-- Kunden-Bestellung --}}
@php $su = $payment->shopping_order->shopping_user; @endphp
<span class="badge badge-info mb-1">Kunde</span>
<div>
{{ trim($su->billing_firstname . ' ' . $su->billing_lastname) ?: '—' }}
</div>
<div class="text-muted" style="font-size:0.7rem">
{{ $su->billing_email }}
</div>
@else
<span class="text-muted"></span>
@endif
</td>
<td class="align-middle small">
{{ $payment->getPaymentType() }}
@if($payment->clearingtype)
<span class="badge badge-secondary ml-1">{{ $payment->clearingtype }}</span>
@endif
</td>
<td class="align-middle text-nowrap">
<strong>{{ number_format($payment->amount / 100, 2, ',', '.') }}</strong>
<small class="text-muted">{{ $payment->currency }}</small>
</td>
<td class="align-middle">
@php
$txColor = match($payment->txaction) {
'paid' => 'success',
'failed' => 'danger',
'appointed' => 'info',
'pending' => 'warning',
default => 'secondary',
};
@endphp
<span class="badge badge-{{ $txColor }}">{{ $payment->txaction ?? '—' }}</span>
@if($hasFailed && $isPaid)
<span class="badge badge-warning ml-1" title="Hatte fehlerhafte Transaktionen">
<i class="ion ion-md-warning"></i>
</span>
@endif
</td>
<td class="align-middle">
@if($payment->mode)
<span class="badge badge-{{ $payment->mode === 'test' ? 'warning' : 'light' }}">
{{ $payment->mode }}
</span>
@endif
</td>
<td class="align-middle text-center">
@if($payment->payment_transactions->count() > 0)
<span class="badge badge-{{ $hasFailed ? 'danger' : 'secondary' }}">
{{ $payment->payment_transactions->count() }}
</span>
@else
<span class="text-muted">0</span>
@endif
</td>
<td class="align-middle text-nowrap small text-muted">
{{ $payment->created_at->format('d.m.Y H:i') }}
</td>
</tr>
{{-- Aufklappbare Transaktions-Sub-Tabelle --}}
@if($payment->payment_transactions->count() > 0)
<tr class="collapse" id="tx-{{ $payment->id }}">
<td colspan="10" class="p-0">
<div class="bg-light border-bottom px-3 py-2">
<small class="font-weight-bold text-muted text-uppercase">
Transaktionen zu Payment #{{ $payment->id }} / Referenz {{ $payment->reference }}
</small>
</div>
<table class="table table-sm table-bordered mb-0" style="background:#fafafa">
<thead>
<tr class="bg-light">
<th class="pl-4">TX-ID</th>
<th>Aktion</th>
<th>Status</th>
<th>Fehlercode</th>
<th>Fehlermeldung</th>
<th>Kundennachricht</th>
<th>Modus</th>
<th>Datum</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach($payment->payment_transactions as $tx)
<tr class="{{ $tx->txaction === 'failed' ? 'table-danger' : ($tx->txaction === 'paid' ? 'table-success' : '') }}">
<td class="pl-4 small">{{ $tx->txid ?? '—' }}</td>
<td>
@php
$txaColor = match($tx->txaction) {
'paid' => 'success', 'failed' => 'danger',
'appointed' => 'info', 'pending' => 'warning',
default => 'secondary',
};
@endphp
<span class="badge badge-{{ $txaColor }}">{{ $tx->txaction ?? '—' }}</span>
</td>
<td class="small">{{ $tx->status ?? '—' }}</td>
<td>
@if($tx->errorcode)
<span class="text-danger font-weight-bold">{{ $tx->errorcode }}</span>
@else
<span class="text-muted"></span>
@endif
</td>
<td class="small text-muted" style="max-width:200px">
{{ \Illuminate\Support\Str::limit($tx->errormessage, 60) }}
</td>
<td class="small text-muted" style="max-width:150px">
{{ \Illuminate\Support\Str::limit($tx->customermessage, 50) }}
</td>
<td>
@if($tx->mode)
<span class="badge badge-{{ $tx->mode === 'test' ? 'warning' : 'light' }}">{{ $tx->mode }}</span>
@endif
</td>
<td class="text-nowrap small text-muted">{{ $tx->created_at->format('d.m.Y H:i') }}</td>
<td>
@if($tx->transmitted_data)
<button class="btn btn-xs btn-outline-secondary"
data-toggle="collapse"
data-target="#raw-{{ $tx->id }}"
title="Rohdaten">
<i class="ion ion-md-code"></i>
</button>
@endif
</td>
</tr>
@if($tx->transmitted_data)
<tr class="collapse" id="raw-{{ $tx->id }}">
<td colspan="9" class="bg-white">
<pre class="mb-0 small" style="max-height:150px; overflow-y:auto; font-size:0.75rem">{{ json_encode($tx->transmitted_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
</td>
</tr>
@endif
@endforeach
</tbody>
</table>
</td>
</tr>
@endif
@empty
<tr>
<td colspan="10" class="text-center text-muted py-4">
Keine Zahlungen im gewählten Zeitraum gefunden.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="mt-3">
{{ $payments->links() }}
</div>
@endsection

View file

@ -0,0 +1,165 @@
@extends('layouts.layout-2')
@section('content')
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show">
{{ session('success') }}
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
@endif
<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>
</div>
<div>
<span class="badge badge-{{ $incident->severity_color }} badge-pill px-3 py-2" style="font-size:0.9rem">
{{ $incident->severity_label }}
</span>
<span class="badge badge-{{ $incident->status_color }} badge-pill px-3 py-2 ml-1" style="font-size:0.9rem">
{{ $incident->status_label }}
</span>
</div>
</div>
<div class="row">
{{-- Linke Spalte: Details + Timeline --}}
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-header d-flex align-items-center">
<i class="ion {{ $incident->type_icon }} mr-2 text-secondary"></i>
<h5 class="mb-0">{{ $incident->title }}</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-sm-4">
<small class="text-muted d-block">Anbieter</small>
<span class="badge badge-secondary">{{ $incident->provider_label }}</span>
</div>
<div class="col-sm-4">
<small class="text-muted d-block">Typ</small>
{{ $incident->type_label }}
</div>
<div class="col-sm-4">
<small class="text-muted d-block">Erkannt am</small>
{{ $incident->detected_at->format('d.m.Y H:i') }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4">
<small class="text-muted d-block">Dauer</small>
{{ $incident->duration }}
</div>
<div class="col-sm-4">
<small class="text-muted d-block">Betroffene Bestellungen</small>
{{ $incident->affected_orders }}
</div>
<div class="col-sm-4">
<small class="text-muted d-block">Betroffener Umsatz</small>
{{ number_format($incident->affected_revenue, 2, ',', '.') }}
</div>
</div>
@if($incident->ticket_number)
<div class="mb-3">
<small class="text-muted d-block">Ticket-Nummer</small>
<code>{{ $incident->ticket_number }}</code>
</div>
@endif
@if($incident->description)
<div class="mb-3">
<small class="text-muted d-block">Beschreibung</small>
<p class="mb-0">{{ $incident->description }}</p>
</div>
@endif
@if($incident->notes)
<div>
<small class="text-muted d-block">Interne Notizen</small>
<p class="mb-0 text-muted">{{ $incident->notes }}</p>
</div>
@endif
</div>
</div>
{{-- Aktivitäten-Timeline --}}
<div class="card">
<h6 class="card-header">Kommunikationsverlauf ({{ $incident->activities->count() }})</h6>
<div class="card-body">
@include('admin.payment-dashboard._partials.activity-timeline')
</div>
</div>
</div>
{{-- Rechte Spalte: Aktionen --}}
<div class="col-lg-4">
{{-- Status ändern --}}
<div class="card mb-4">
<h6 class="card-header">Status ändern</h6>
<div class="card-body">
<form method="POST" action="{{ route('admin.payment-dashboard.status.update', $incident) }}">
@csrf
@method('PATCH')
<div class="form-group">
<select name="status" class="custom-select">
@foreach(['open' => 'Offen', 'in_progress' => 'In Bearbeitung', 'waiting_provider' => 'Wartet auf Anbieter', 'resolved' => 'Gelöst', 'closed' => 'Geschlossen'] as $value => $label)
<option value="{{ $value }}" {{ $incident->status === $value ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-primary btn-block">Status speichern</button>
</form>
</div>
</div>
{{-- Aktivität hinzufügen --}}
<div class="card">
<h6 class="card-header">Aktivität hinzufügen</h6>
<div class="card-body">
@if($errors->has('type') || $errors->has('title'))
<div class="alert alert-danger small">
@foreach($errors->all() as $error)
<div>{{ $error }}</div>
@endforeach
</div>
@endif
<form method="POST" action="{{ route('admin.payment-dashboard.activity.store', $incident) }}">
@csrf
<div class="form-group">
<label class="small">Typ</label>
<select name="type" class="custom-select custom-select-sm">
<option value="note">Notiz</option>
<option value="email">E-Mail</option>
<option value="call">Telefonat</option>
<option value="ticket">Ticket</option>
<option value="provider_response">Anbieter-Antwort</option>
</select>
</div>
<div class="form-group">
<label class="small">Titel</label>
<input type="text" name="title" class="form-control form-control-sm"
placeholder="Kurze Beschreibung" value="{{ old('title') }}" required>
</div>
<div class="form-group">
<label class="small">Details</label>
<textarea name="content" class="form-control form-control-sm" rows="3"
placeholder="Inhalt der Aktivität...">{{ old('content') }}</textarea>
</div>
<button type="submit" class="btn btn-secondary btn-block btn-sm">
<i class="ion ion-md-add"></i> Aktivität speichern
</button>
</form>
</div>
</div>
@if($incident->resolved_at)
<div class="mt-3 text-center text-muted small">
<i class="ion ion-md-checkmark-circle text-success"></i>
Gelöst am {{ $incident->resolved_at->format('d.m.Y H:i') }}
</div>
@endif
</div>
</div>
@endsection

View file

@ -0,0 +1,183 @@
@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>
@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">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>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>
@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">{{ $tx->txid ?? '—' }}</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="9" 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="9" 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

View file

@ -138,6 +138,148 @@
</div>
</div>
</div>
<hr>
<!-- Umsätze nach Ländern - Jährlich -->
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Umsätze nach Ländern {{ session('revenue_filter_year') }}</h6>
</div>
<div class="card-body p-0">
@if(isset($revenue_summary['country_yearly']) && $revenue_summary['country_yearly']->count() > 0)
<table class="table table-sm table-hover mb-0">
<thead class="thead-light">
<tr>
<th>Land</th>
<th class="text-right">Netto</th>
<th class="text-right">Steuer</th>
<th class="text-right">Brutto</th>
</tr>
</thead>
<tbody>
@foreach($revenue_summary['country_yearly'] as $item)
<tr>
<td>{{ $item->country_name }}</td>
<td class="text-right">{{ number_format($item->total_net, 2, ',', '.') }} </td>
<td class="text-right">{{ number_format($item->total_tax, 2, ',', '.') }} </td>
<td class="text-right"><strong>{{ number_format($item->total_gross, 2, ',', '.') }} </strong></td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="p-3"><p class="text-muted mb-0">Keine Umsätze nach Ländern für {{ session('revenue_filter_year') }} gefunden</p></div>
@endif
</div>
</div>
</div>
<!-- Gutschriften nach Ländern - Jährlich -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Gutschriften nach Ländern {{ session('revenue_filter_year') }}</h6>
</div>
<div class="card-body p-0">
@if(isset($credit_summary['country_yearly']) && $credit_summary['country_yearly']->count() > 0)
<table class="table table-sm table-hover mb-0">
<thead class="thead-light">
<tr>
<th>Land</th>
<th class="text-right">Netto</th>
<th class="text-right">Steuer</th>
<th class="text-right">Brutto</th>
</tr>
</thead>
<tbody>
@foreach($credit_summary['country_yearly'] as $item)
<tr>
<td>{{ $item->country_name }}</td>
<td class="text-right">{{ number_format($item->total_net, 2, ',', '.') }} </td>
<td class="text-right">{{ number_format($item->total_tax, 2, ',', '.') }} </td>
<td class="text-right"><strong>{{ number_format($item->total_gross, 2, ',', '.') }} </strong></td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="p-3"><p class="text-muted mb-0">Keine Gutschriften nach Ländern für {{ session('revenue_filter_year') }} gefunden</p></div>
@endif
</div>
</div>
</div>
</div>
<!-- Umsätze nach Ländern - Monatlich -->
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Umsätze nach Ländern monatliche Aufschlüsselung</h6>
</div>
<div class="card-body p-0">
@if(isset($revenue_summary['country_monthly']) && $revenue_summary['country_monthly']->count() > 0)
@php $revenueByMonth = $revenue_summary['country_monthly']->groupBy('month'); @endphp
@foreach($revenueByMonth as $month => $countries)
<div class="px-3 pt-2 pb-1">
<strong class="text-primary">{{ $countries->first()->month_label }}</strong>
</div>
<table class="table table-sm mb-1">
<tbody>
@foreach($countries as $item)
<tr>
<td class="pl-4">{{ $item->country_name }}</td>
<td class="text-right text-muted"><small>{{ number_format($item->total_net, 2, ',', '.') }} </small></td>
<td class="text-right text-muted"><small>{{ number_format($item->total_tax, 2, ',', '.') }} </small></td>
<td class="text-right"><small><strong>{{ number_format($item->total_gross, 2, ',', '.') }} </strong></small></td>
</tr>
@endforeach
</tbody>
</table>
@endforeach
@else
<div class="p-3"><p class="text-muted mb-0">Keine monatlichen Umsätze nach Ländern gefunden</p></div>
@endif
</div>
</div>
</div>
<!-- Gutschriften nach Ländern - Monatlich -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Gutschriften nach Ländern monatliche Aufschlüsselung</h6>
</div>
<div class="card-body p-0">
@if(isset($credit_summary['country_monthly']) && $credit_summary['country_monthly']->count() > 0)
@php $creditByMonth = $credit_summary['country_monthly']->groupBy('month'); @endphp
@foreach($creditByMonth as $month => $countries)
<div class="px-3 pt-2 pb-1">
<strong class="text-primary">{{ $countries->first()->month_label }}</strong>
</div>
<table class="table table-sm mb-1">
<tbody>
@foreach($countries as $item)
<tr>
<td class="pl-4">{{ $item->country_name }}</td>
<td class="text-right text-muted"><small>{{ number_format($item->total_net, 2, ',', '.') }} </small></td>
<td class="text-right text-muted"><small>{{ number_format($item->total_tax, 2, ',', '.') }} </small></td>
<td class="text-right"><small><strong>{{ number_format($item->total_gross, 2, ',', '.') }} </strong></small></td>
</tr>
@endforeach
</tbody>
</table>
@endforeach
@else
<div class="p-3"><p class="text-muted mb-0">Keine monatlichen Gutschriften nach Ländern gefunden</p></div>
@endif
</div>
</div>
</div>
</div>
</div>
<script>