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

@ -63,8 +63,12 @@ return [
'no_participants' => 'Noch keine Teilnehmer.',
'no_participants_with_points' => 'Noch keine Teilnehmer mit Punkten.',
'anonymous_consultant' => 'Anonymer Berater',
'ranking_all_active' => 'Alle Aktiven',
'vip_view_notice' => 'VIP-Ansicht: Klarnamen aller Teilnehmer werden angezeigt.',
'vip_terms_accepted' => 'Teilnahmebedingungen akzeptiert',
'vip_terms_pending' => 'Teilnahmebedingungen noch nicht akzeptiert',
'ranking_anonymous_hint' => 'Namen erscheinen erst, wenn die Teilnahme am Incentive bestätigt wurde.',
'ranking_extended_hint' => 'Die Liste zeigt die Plätze 130. Die besten :n qualifizierten Berater (hervorgehoben) gewinnen; die Plätze danach zeigen, wer noch nachlegen kann.',
'ranking_extended_hint' => 'Die Liste zeigt alle Berater mit mehr als 0 Punkten. Die besten :n qualifizierten Berater (hervorgehoben) gewinnen; die Plätze danach zeigen, wer noch nachlegen kann.',
'calculation_details' => 'Berechnungsdetails',
'close' => 'Schliessen',
@ -136,6 +140,14 @@ return [
'you_participate' => 'Du nimmst teil!',
'your_rank' => 'Dein aktueller Rang',
'participate_intro' => 'Bist du bereit für die Challenge? Melde dich einmalig an, um im offiziellen Ranking gelistet zu werden.',
'dash_notice_unregistered_title' => 'Noch nicht angemeldet',
'dash_notice_unregistered_body' => 'Du nimmst am Incentive noch nicht offiziell teil. Ohne Bestätigung werden deine Punkte nicht gewertet und du erscheinst nicht in der Rangliste.',
'dash_notice_unconfirmed_title' => 'Teilnahme noch nicht bestätigt',
'dash_notice_unconfirmed_body' => 'Deine Punkte laufen bereits mit aber ohne Bestätigung der Teilnahmebedingungen wirst du in der Rangliste anonym angezeigt und kannst nicht gewinnen.',
'dash_notice_btn' => 'Jetzt Teilnahme bestätigen',
'dash_modal_title' => 'Teilnahme bestätigen',
'dash_modal_intro' => 'Bitte lies die Informationen und Teilnahmebedingungen sorgfältig durch und bestätige anschließend deine Teilnahme.',
'dash_modal_cancel' => 'Schließen',
'pending_confirmation_banner' => 'Deine Punkte werden bereits im Qualifikationszeitraum mitgerechnet. Bitte bestätige die Teilnahme, damit dein Name in der Rangliste sichtbar wird und du alle Funktionen nutzen kannst.',
'details_requires_confirmation' => 'Die Detailansicht ist erst nach Bestätigung der Teilnahme verfügbar.',
'participate_abo_hint' => 'Es liegt mindestens ein für die Wertung relevantes Abo vor (aktives Berater-Abo oder Kundenabo im Qualifikationszeitraum). Mit dem Teilnehmen werden die Punkte dafür direkt nach den aktuellen Regeln übernommen.',

View file

@ -86,4 +86,6 @@ return [
'my_abo' => 'Mein Abo',
'my_subscriptions' => 'Meine Abos',
'team_customers' => 'Team Kunden',
'payment_monitor' => 'Payment Monitor',
'payment_monitor_management' => 'Payment Monitor GF',
];

View file

@ -0,0 +1,6 @@
<?php
return [
'previous' => '&laquo; Zurück',
'next' => 'Weiter &raquo;',
];

View file

@ -161,12 +161,32 @@ return [
'payment_not_found' => 'Zahlung nicht gefunden',
'payment_not_found_description' => 'Die Zahlung mit der Referenz :reference konnte nicht gefunden werden. Bitte kontaktieren Sie uns, falls Sie bereits bezahlt haben.',
'payment_canceled' => 'Zahlung abgebrochen',
'payment_canceled_description' => 'Der Zahlungsvorgang wurde abgebrochen. Ihre Bestellung wurde nicht ausgeführt.',
'payment_error' => 'Zahlungsfehler',
'payment_error_description' => 'Bei der Zahlungsabwicklung ist ein Fehler aufgetreten. Ihre Bestellung konnte nicht abgeschlossen werden.',
'payment_canceled_description' => 'Sie haben den Zahlungsvorgang abgebrochen. Ihre Bestellung wurde nicht ausgeführt und es wurde nichts belastet.',
'payment_canceled_hint' => 'Sie können jederzeit einen neuen Zahlungsversuch starten.',
'payment_error' => 'Zahlung fehlgeschlagen',
'payment_error_description' => 'Die Zahlung konnte leider nicht abgeschlossen werden.',
'payment_error_hint' => 'Bitte prüfen Sie Ihre Zahlungsdaten und versuchen Sie es erneut — oder wählen Sie eine andere Zahlungsart.',
'payment_error_retry' => 'Erneut versuchen',
'payment_error_code' => 'Fehlercode',
'payment_error_what_to_do' => 'Was kann ich tun?',
'payment_unknown_status' => 'Unbekannter Zahlungsstatus',
'payment_unknown_status_description' => 'Der Zahlungsstatus konnte nicht ermittelt werden. Bitte kontaktieren Sie uns für weitere Informationen.',
'contact_support_if_needed' => 'Bei Fragen wenden Sie sich bitte an unseren Kundenservice.',
'contact_support_if_needed' => 'Bei weiteren Fragen wenden Sie sich bitte an unseren Kundenservice.',
'try_again' => 'Erneut versuchen',
'choose_different_payment' => 'Andere Zahlungsart wählen',
'nothing_was_charged' => 'Es wurde nichts von Ihrem Konto abgebucht.',
'payment_error_reasons' => [
'card_expired' => 'Ihre Karte ist abgelaufen. Bitte verwenden Sie eine gültige Karte oder wählen Sie eine andere Zahlungsart.',
'card_blocked' => 'Ihre Karte ist gesperrt. Bitte wenden Sie sich an Ihre Bank oder wählen Sie eine andere Zahlungsart.',
'card_invalid' => 'Die Kartendaten sind ungültig. Bitte überprüfen Sie Ihre Eingabe.',
'card_declined' => 'Ihre Bank hat die Zahlung abgelehnt. Bitte wenden Sie sich an Ihre Bank oder wählen Sie eine andere Zahlungsart.',
'insufficient_funds' => 'Das Kartenlimit wurde überschritten. Bitte wenden Sie sich an Ihre Bank oder wählen Sie eine andere Zahlungsart.',
'cvv_invalid' => 'Die Prüfziffer (CVV) ist nicht korrekt. Bitte überprüfen Sie die 3-stellige Zahl auf der Rückseite Ihrer Karte.',
'3ds_failed' => 'Die 3D-Secure-Authentifizierung ist fehlgeschlagen. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsart.',
'timeout' => 'Die Verbindung zur Bank ist unterbrochen (Timeout). Bitte versuchen Sie es in wenigen Minuten erneut.',
'fraud' => 'Die Zahlung wurde aus Sicherheitsgründen abgelehnt. Bitte wenden Sie sich an Ihre Bank.',
'general' => 'Bitte überprüfen Sie Ihre Zahlungsdaten und versuchen Sie es erneut. Falls das Problem weiterhin besteht, wählen Sie eine andere Zahlungsart.',
],
// DHL Packstation/Paketbox
'packstation_delivery' => 'Lieferung an Packstation/Paketbox',

View file

@ -63,8 +63,12 @@ return [
'no_participants' => 'No participants yet.',
'no_participants_with_points' => 'No participants with points yet.',
'anonymous_consultant' => 'Anonymous consultant',
'ranking_all_active' => 'All Active',
'vip_view_notice' => 'VIP view: Real names of all participants are shown.',
'vip_terms_accepted' => 'Terms accepted',
'vip_terms_pending' => 'Terms not yet accepted',
'ranking_anonymous_hint' => 'Names appear only after participation in the incentive has been confirmed.',
'ranking_extended_hint' => 'The list shows ranks 130. The best :n qualified consultants (highlighted) win; the ranks below show who can still push ahead.',
'ranking_extended_hint' => 'The list shows all consultants with more than 0 points. The best :n qualified consultants (highlighted) win; the ranks below show who can still push ahead.',
'calculation_details' => 'Calculation Details',
'close' => 'Close',
@ -136,6 +140,14 @@ return [
'you_participate' => 'You are participating!',
'your_rank' => 'Your current rank',
'participate_intro' => 'Ready for the challenge? Register once to be listed in the official ranking.',
'dash_notice_unregistered_title' => 'Not yet registered',
'dash_notice_unregistered_body' => 'You are not yet officially participating. Without confirmation, your points won\'t count and you won\'t appear in the ranking.',
'dash_notice_unconfirmed_title' => 'Participation not yet confirmed',
'dash_notice_unconfirmed_body' => 'Your points are already tracked but without accepting the terms you will appear anonymously in the ranking and cannot win.',
'dash_notice_btn' => 'Confirm participation now',
'dash_modal_title' => 'Confirm participation',
'dash_modal_intro' => 'Please read the information and terms carefully, then confirm your participation.',
'dash_modal_cancel' => 'Close',
'pending_confirmation_banner' => 'Your points are already counted for the qualification period. Please confirm participation so your name appears in the ranking and you can use all features.',
'details_requires_confirmation' => 'The detail view is available only after you confirm participation.',
'participate_abo_hint' => 'You already have at least one subscription that counts (active consultant subscription or a customer subscription started in the qualification period). When you join, points for it are applied immediately according to the current rules.',

View file

@ -86,4 +86,6 @@ return [
'my_abo' => 'My Abo',
'my_subscriptions' => 'My Subscriptions',
'team_customers' => 'Team Customers',
'payment_monitor' => 'Payment Monitor',
'payment_monitor_management' => 'Payment Monitor (Mgmt)',
];

View file

@ -0,0 +1,6 @@
<?php
return [
'previous' => '&laquo; Previous',
'next' => 'Next &raquo;',
];

View file

@ -154,12 +154,32 @@ return [
'payment_not_found' => 'Payment not found',
'payment_not_found_description' => 'The payment with reference :reference could not be found. Please contact us if you have already paid.',
'payment_canceled' => 'Payment canceled',
'payment_canceled_description' => 'The payment process was canceled. Your order was not processed.',
'payment_error' => 'Payment error',
'payment_error_description' => 'An error occurred during payment processing. Your order could not be completed.',
'payment_canceled_description' => 'You have canceled the payment process. Your order was not processed and nothing has been charged.',
'payment_canceled_hint' => 'You can start a new payment attempt at any time.',
'payment_error' => 'Payment failed',
'payment_error_description' => 'Unfortunately, the payment could not be completed.',
'payment_error_hint' => 'Please check your payment details and try again — or choose a different payment method.',
'payment_error_retry' => 'Try again',
'payment_error_code' => 'Error code',
'payment_error_what_to_do' => 'What can I do?',
'payment_unknown_status' => 'Unknown payment status',
'payment_unknown_status_description' => 'The payment status could not be determined. Please contact us for more information.',
'contact_support_if_needed' => 'If you have any questions, please contact our customer service.',
'contact_support_if_needed' => 'If you have further questions, please contact our customer service.',
'try_again' => 'Try again',
'choose_different_payment' => 'Choose a different payment method',
'nothing_was_charged' => 'Nothing has been charged to your account.',
'payment_error_reasons' => [
'card_expired' => 'Your card has expired. Please use a valid card or choose a different payment method.',
'card_blocked' => 'Your card is blocked. Please contact your bank or choose a different payment method.',
'card_invalid' => 'The card details are invalid. Please check your input.',
'card_declined' => 'Your bank declined the payment. Please contact your bank or choose a different payment method.',
'insufficient_funds' => 'The card limit has been exceeded. Please contact your bank or choose a different payment method.',
'cvv_invalid' => 'The security code (CVV) is incorrect. Please check the 3-digit code on the back of your card.',
'3ds_failed' => '3D Secure authentication failed. Please try again or choose a different payment method.',
'timeout' => 'The connection to the bank was interrupted (timeout). Please try again in a few minutes.',
'fraud' => 'The payment was declined for security reasons. Please contact your bank.',
'general' => 'Please check your payment details and try again. If the problem persists, please choose a different payment method.',
],
// DHL Packstation/Parcel Box
'packstation_delivery' => 'Delivery to Packstation/Parcel Box',

View file

@ -63,8 +63,12 @@ return [
'no_participants' => 'Aun no hay participantes.',
'no_participants_with_points' => 'Aun no hay participantes con puntos.',
'anonymous_consultant' => 'Consultor anonimo',
'ranking_all_active' => 'Todos los activos',
'vip_view_notice' => 'Vista VIP: Se muestran los nombres reales de todos los participantes.',
'vip_terms_accepted' => 'Condiciones aceptadas',
'vip_terms_pending' => 'Condiciones aun no aceptadas',
'ranking_anonymous_hint' => 'Los nombres solo se muestran despues de confirmar la participacion en el incentivo.',
'ranking_extended_hint' => 'La lista muestra los puestos 130. Los mejores :n consultores calificados (marcados) ganan; los puestos siguientes muestran quien aun puede reforzar.',
'ranking_extended_hint' => 'La lista muestra todos los consultores con mas de 0 puntos. Los mejores :n consultores calificados (marcados) ganan; los puestos siguientes muestran quien aun puede reforzar.',
'calculation_details' => 'Detalles del calculo',
'close' => 'Cerrar',
@ -136,6 +140,14 @@ return [
'you_participate' => 'Estas participando!',
'your_rank' => 'Tu puesto actual',
'participate_intro' => 'Listo para el desafio? Registrate una vez para aparecer en el ranking oficial.',
'dash_notice_unregistered_title' => 'Aun no registrado',
'dash_notice_unregistered_body' => 'Aun no participas oficialmente. Sin confirmacion, tus puntos no contaran y no aparecereas en el ranking.',
'dash_notice_unconfirmed_title' => 'Participacion aun no confirmada',
'dash_notice_unconfirmed_body' => 'Tus puntos ya se estan registrando, pero sin aceptar las condiciones aparecereas de forma anonima en el ranking y no podras ganar.',
'dash_notice_btn' => 'Confirmar participacion ahora',
'dash_modal_title' => 'Confirmar participacion',
'dash_modal_intro' => 'Por favor lee la informacion y las condiciones con atencion y confirma tu participacion.',
'dash_modal_cancel' => 'Cerrar',
'pending_confirmation_banner' => 'Tus puntos ya cuentan en el periodo de calificacion. Confirma la participacion para que tu nombre sea visible en el ranking y puedas usar todas las funciones.',
'details_requires_confirmation' => 'La vista detallada solo esta disponible despues de confirmar la participacion.',
'participate_abo_hint' => 'Ya tienes al menos una suscripcion relevante (suscripcion de consultor activa o suscripcion de cliente en el periodo de calificacion). Al participar, los puntos se aplican de inmediato segun las reglas vigentes.',

View file

@ -86,4 +86,6 @@ return [
'my_abo' => 'Mi Suscripción',
'my_subscriptions' => 'Mis Suscripciones',
'team_customers' => 'Clientes del Equipo',
'payment_monitor' => 'Monitor de Pagos',
'payment_monitor_management' => 'Monitor de Pagos (Dir.)',
];

View file

@ -0,0 +1,6 @@
<?php
return [
'previous' => '&laquo; Anterior',
'next' => 'Siguiente &raquo;',
];

View file

@ -155,12 +155,32 @@ return [
'payment_not_found' => 'Pago no encontrado',
'payment_not_found_description' => 'No se pudo encontrar el pago con la referencia :reference. Por favor contáctenos si ya ha realizado el pago.',
'payment_canceled' => 'Pago cancelado',
'payment_canceled_description' => 'El proceso de pago fue cancelado. Su pedido no fue procesado.',
'payment_error' => 'Error de pago',
'payment_error_description' => 'Se produjo un error durante el procesamiento del pago. Su pedido no pudo completarse.',
'payment_canceled_description' => 'Ha cancelado el proceso de pago. Su pedido no fue procesado y no se realizó ningún cargo.',
'payment_canceled_hint' => 'Puede iniciar un nuevo intento de pago en cualquier momento.',
'payment_error' => 'Pago fallido',
'payment_error_description' => 'Lamentablemente, el pago no pudo completarse.',
'payment_error_hint' => 'Por favor, revise sus datos de pago e inténtelo de nuevo — o elija otro método de pago.',
'payment_error_retry' => 'Intentar de nuevo',
'payment_error_code' => 'Código de error',
'payment_error_what_to_do' => '¿Qué puedo hacer?',
'payment_unknown_status' => 'Estado de pago desconocido',
'payment_unknown_status_description' => 'No se pudo determinar el estado del pago. Por favor contáctenos para más información.',
'contact_support_if_needed' => 'Si tiene alguna pregunta, por favor contacte a nuestro servicio de atención al cliente.',
'contact_support_if_needed' => 'Si tiene más preguntas, por favor contacte a nuestro servicio de atención al cliente.',
'try_again' => 'Intentar de nuevo',
'choose_different_payment' => 'Elegir otro método de pago',
'nothing_was_charged' => 'No se ha realizado ningún cargo en su cuenta.',
'payment_error_reasons' => [
'card_expired' => 'Su tarjeta ha caducado. Por favor use una tarjeta válida o elija otro método de pago.',
'card_blocked' => 'Su tarjeta está bloqueada. Por favor contacte a su banco o elija otro método de pago.',
'card_invalid' => 'Los datos de la tarjeta son inválidos. Por favor verifique su entrada.',
'card_declined' => 'Su banco rechazó el pago. Por favor contacte a su banco o elija otro método de pago.',
'insufficient_funds' => 'El límite de la tarjeta fue superado. Por favor contacte a su banco o elija otro método de pago.',
'cvv_invalid' => 'El código de seguridad (CVV) es incorrecto. Por favor verifique los 3 dígitos en el reverso de su tarjeta.',
'3ds_failed' => 'La autenticación 3D Secure falló. Por favor inténtelo de nuevo o elija otro método de pago.',
'timeout' => 'La conexión con el banco se interrumpió (tiempo de espera). Por favor inténtelo de nuevo en unos minutos.',
'fraud' => 'El pago fue rechazado por razones de seguridad. Por favor contacte a su banco.',
'general' => 'Por favor verifique sus datos de pago e inténtelo de nuevo. Si el problema persiste, elija otro método de pago.',
],
// DHL Packstation/Paketbox
'packstation_delivery' => 'Entrega a Packstation/Paketbox',

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>

View file

@ -1,4 +1,7 @@
@if (isset($activeIncentive) && $activeIncentive)
@php
$hasConfirmedDash = $incentiveParticipant && $incentiveParticipant->accepted_terms_at !== null;
@endphp
<div class="d-flex col-xl-12 align-items-stretch">
<div class="w-100 mb-4 inc-dash-widget">
@ -60,6 +63,33 @@
</div>
@endif
{{-- Notice: Teilnahme noch nicht bestätigt --}}
@if (!$hasConfirmedDash)
<div class="inc-dash-notice mb-3">
<div class="d-flex align-items-start">
<i class="ion ion-md-alert inc-dash-notice-icon mr-2 mt-1"></i>
<div class="flex-grow-1">
@if ($incentiveParticipant)
<strong
class="inc-dash-notice-title">{{ __('incentive.dash_notice_unconfirmed_title') }}</strong>
<p class="mb-2 small">{{ __('incentive.dash_notice_unconfirmed_body') }}</p>
@else
<strong
class="inc-dash-notice-title">{{ __('incentive.dash_notice_unregistered_title') }}</strong>
<p class="mb-2 small">{{ __('incentive.dash_notice_unregistered_body') }}</p>
@endif
@if ($activeIncentive->isActive())
<button type="button" class="btn inc-dash-btn-notice" data-toggle="modal"
data-target="#incParticipateModal">
<i class="ion ion-md-checkmark-circle mr-1"></i>
{{ __('incentive.dash_notice_btn') }}
</button>
@endif
</div>
</div>
</div>
@endif
{{-- Bilder-Leiste --}}
@php
$dashGallery = [];
@ -99,6 +129,13 @@
<i class="ion ion-md-list mr-1"></i>
{{ __('incentive.dashboard_btn_ranking') }}
</a>
@if ($incentiveParticipant)
<a href="{{ route('user_incentive_details', [$activeIncentive->slug]) }}"
class="btn inc-dash-btn-secondary">
<i class="ion ion-md-list mr-1"></i>
{{ __('incentive.my_calculation') }}
</a>
@endif
</div>
</div>
@ -106,7 +143,117 @@
</div>
</div>
{{-- Teilnahme-Modal --}}
@if (!$hasConfirmedDash && $activeIncentive->isActive())
<div class="modal fade" id="incParticipateModal" tabindex="-1" role="dialog"
aria-labelledby="incParticipateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #6b7758, #4a5340);">
<h5 class="modal-title text-white" id="incParticipateModalLabel">
<i class="ion ion-md-trophy mr-2" style="color: #d7d700;"></i>
{{ __('incentive.dash_modal_title') }}
</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="incDashParticipateForm"
action="{{ route('user_incentive_participate', [$activeIncentive->slug]) }}" method="POST">
@csrf
<div class="modal-body">
<p class="mb-3 small">{{ __('incentive.dash_modal_intro') }}</p>
@if ($activeIncentive->getLang('description'))
<div class="mb-3" style="line-height: 1.6;">
{!! $activeIncentive->getLang('description') !!}
</div>
<hr>
@endif
@if ($activeIncentive->getLang('terms'))
<div class="card mb-4" style="border: 1px solid #e0e0d8;">
<div class="card-header py-2 px-3" style="cursor: pointer; background: #f4f5f0;"
data-toggle="collapse" data-target="#dashTermsCollapse">
<div class="d-flex align-items-center">
<i class="ion ion-md-document mr-2" style="color: #6b7758;"></i>
<strong class="small">{{ __('incentive.terms') }}</strong>
<i class="ion ion-md-chevron-down ml-auto text-muted"></i>
</div>
</div>
<div id="dashTermsCollapse" class="collapse">
<div class="card-body small"
style="max-height: 280px; overflow-y: auto; line-height: 1.6;">
{!! $activeIncentive->getLang('terms') !!}
</div>
</div>
</div>
@endif
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="dashAcceptTerms"
name="accept_terms" value="1" required>
<label class="custom-control-label" for="dashAcceptTerms">
{{ __('incentive.accept_terms') }}
@if ($activeIncentive->getLang('terms'))
(<a href="#dashTermsCollapse" data-toggle="collapse"
class="text-muted">{{ __('incentive.show_terms') }}</a>)
@endif
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
{{ __('incentive.dash_modal_cancel') }}
</button>
<button type="submit" class="btn inc-dash-btn-primary px-4">
<i class="ion ion-md-checkmark mr-1"></i>
{{ __('incentive.participate_now') }}
</button>
</div>
</form>
</div>
</div>
</div>
@endif
<style>
.inc-dash-notice {
background: rgba(215, 185, 0, 0.10);
border: 1px solid rgba(215, 185, 0, 0.35);
border-radius: .6rem;
padding: .9rem 1rem;
color: #555;
}
.inc-dash-notice-icon {
color: #c8a000;
font-size: 1.2rem;
}
.inc-dash-notice-title {
display: block;
color: #444;
margin-bottom: .2rem;
font-size: .9rem;
}
.inc-dash-btn-notice {
background: linear-gradient(135deg, #6b7758, #4a5340);
color: #fff !important;
border: none;
border-radius: 50px;
padding: .4rem 1.2rem;
font-weight: 700;
font-size: .82rem;
transition: transform .2s, box-shadow .2s;
}
.inc-dash-btn-notice:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(107, 119, 88, 0.35);
}
.inc-dash-widget {
border-radius: .75rem;
overflow: hidden;

View file

@ -0,0 +1,114 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Payment Incident Alert mivita.care</title>
<style type="text/css">
td, h1, h2, h3 { font-family: Helvetica, Verdana, Arial, sans-serif; font-weight: 400; }
body { -webkit-font-smoothing: antialiased; width: 100%; height: 100%; color: #37302d; background: #ffffff; font-size: 15px; line-height: 26px; }
table { border-collapse: separate !important; }
.severity-critical { color: #dc3545; }
.severity-high { color: #fd7e14; }
.severity-medium { color: #ffc107; }
.severity-low { color: #28a745; }
.label { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; }
.label-danger { background: #dc3545; color: #fff; }
.label-warning { background: #ffc107; color: #212529; }
.label-success { background: #28a745; color: #fff; }
a { color: #919f7a; text-decoration: none; }
</style>
</head>
<body style="padding:0; margin:0; display:block; background:#f8f8f8;">
<table align="left" cellpadding="0" cellspacing="0" width="100%" height="100%">
<tr>
<td align="left" valign="top" bgcolor="#f8f8f8" width="100%">
<br>
<table style="margin: 0 auto;" cellpadding="0" cellspacing="0" width="700">
<tr>
<td align="center" style="padding-bottom: 20px;">
<img src="https://my.mivita.care/images/logo_mivita.png" alt="mivita.care" width="200">
</td>
</tr>
<tr>
<td>
<table cellpadding="10" cellspacing="0" border="0" width="100%" bgcolor="#ffffff">
<tr>
<td style="font-family: Helvetica, sans-serif; font-size: 18px; font-weight: bold; padding-bottom: 5px;">
🚨 {{ $title }}
</td>
</tr>
<tr>
<td>
<table style="padding: 20px; border: 1px solid #eee; background-color: #fff8f8; line-height: 1.8em; width: 100%;" cellpadding="4" cellspacing="0">
<tr>
<td width="160"><strong>Titel</strong></td>
<td>{{ $incident->title }}</td>
</tr>
<tr>
<td><strong>Schwere</strong></td>
<td>
<span class="label label-{{ $incident->severity === 'critical' ? 'danger' : ($incident->severity === 'high' ? 'warning' : 'success') }}">
{{ $incident->severity_label }}
</span>
</td>
</tr>
<tr>
<td><strong>Anbieter</strong></td>
<td>{{ $incident->provider_label }}</td>
</tr>
<tr>
<td><strong>Typ</strong></td>
<td>{{ $incident->type_label }}</td>
</tr>
<tr>
<td><strong>Erkannt am</strong></td>
<td>{{ $incident->detected_at->format('d.m.Y H:i') }} Uhr</td>
</tr>
@if($incident->description)
<tr>
<td><strong>Beschreibung</strong></td>
<td>{{ $incident->description }}</td>
</tr>
@endif
@if($incident->affected_orders > 0)
<tr>
<td><strong>Betroffene Bestellungen</strong></td>
<td>{{ $incident->affected_orders }}</td>
</tr>
@endif
@if($incident->affected_revenue > 0)
<tr>
<td><strong>Betroffener Umsatz</strong></td>
<td>{{ number_format($incident->affected_revenue, 2, ',', '.') }} </td>
</tr>
@endif
</table>
</td>
</tr>
<tr>
<td style="text-align: center; padding: 20px 0;">
<a href="{{ $dashboardUrl }}"
style="display: inline-block; padding: 10px 24px; background-color: #dc3545; color: #ffffff; font-family: Helvetica, sans-serif; font-size: 14px; font-weight: bold; border-radius: 4px; text-decoration: none;">
Incident im Dashboard öffnen
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="color:#7B7B7E; font-size:13px; text-align: center; padding: 20px 0;">
Diese Nachricht wurde automatisch von mivita.care generiert.<br>
<a href="https://www.mivita.care" style="color: #7B7B7E; text-decoration: underline;">www.mivita.care</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -617,6 +617,25 @@
<div>{{ __('navigation.tools') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('admin/payment-dashboard*') ? ' active' : '' }}">
<a href="{{ route('admin.payment-dashboard.index') }}" class="sidenav-link">
<i class="sidenav-icon ion ion-md-alert"></i>
<div>{{ __('navigation.payment_monitor') }}</div>
@php $openIncidentCount = Cache::remember('open_incident_count', 60, fn() => \App\Models\PaymentIncident::whereIn('status', ['open','in_progress','waiting_provider'])->count()); @endphp
@if($openIncidentCount > 0)
<div class="pl-1 ml-auto">
<div class="badge badge-danger">{{ $openIncidentCount }}</div>
</div>
@endif
</a>
</li>
{{-- GF-Ansicht: vorerst auskommentiert --}}
{{-- <li class="sidenav-item{{ Request::is('admin/payment-dashboard/management') ? ' active' : '' }}">
<a href="{{ route('admin.payment-dashboard.management') }}" class="sidenav-link">
<i class="sidenav-icon ion ion-md-stats"></i>
<div>{{ __('navigation.payment_monitor_management') }}</div>
</a>
</li> --}}
@endif
@if (Auth::user()->isSySAdmin())
<li class="sidenav-divider mb-1"></li>

View file

@ -386,6 +386,49 @@
font-weight: 600;
}
.vip-ranking-notice {
background: rgba(107, 119, 88, 0.08);
border-bottom: 1px solid rgba(107, 119, 88, 0.15);
padding: .6rem 1.5rem;
font-size: .8rem;
color: #6b7758;
font-weight: 600;
}
.vip-terms-accepted {
color: #5a8a5a;
font-size: 1rem;
vertical-align: middle;
}
.vip-terms-pending {
color: #c0392b;
font-size: 1rem;
vertical-align: middle;
}
.inc-ranking-card .pagination {
margin: 0;
justify-content: center;
}
.inc-ranking-card .page-item .page-link {
color: #6b7758;
border-color: #e0e0d8;
font-size: .85rem;
}
.inc-ranking-card .page-item.active .page-link {
background-color: #6b7758;
border-color: #6b7758;
color: #fff;
}
.inc-ranking-card .page-item.disabled .page-link {
color: #bbb;
border-color: #e0e0d8;
}
.pending-banner {
background: rgba(215, 215, 0, 0.12);
border: 1px solid rgba(215, 215, 0, 0.3);
@ -857,7 +900,7 @@
<div class="d-flex align-items-center">
<i class="ion ion-md-list mr-2" style="font-size: 1.2rem; color: #6b7758;"></i>
<span class="ranking-title">{{ __('incentive.section_ranking') }}</span>
<span class="badge-top ml-2">Top {{ $rankingDisplayLimit }}</span>
<span class="badge-top ml-2">{{ __('incentive.ranking_all_active') }}</span>
</div>
<span
class="hint-text">{{ __('incentive.ranking_winners_hint', ['n' => $incentive->max_winners]) }}</span>
@ -868,6 +911,12 @@
</div>
</div>
<div class="card-body p-0">
@if ($isVipView)
<div class="vip-ranking-notice">
<i class="ion ion-md-eye mr-1"></i>
{{ __('incentive.vip_view_notice') }}
</div>
@endif
@if ($ranking->isEmpty())
<div class="p-4 text-center text-muted">
<i class="ion ion-md-people mb-2 d-block" style="font-size: 2.5rem; opacity: .4;"></i>
@ -904,7 +953,16 @@
@endif
</td>
<td>
@if ($p->accepted_terms_at)
@if ($p->accepted_terms_at || $isVipView)
@if ($isVipView)
@if ($p->accepted_terms_at)
<i class="ion ion-md-checkmark-circle vip-terms-accepted ml-1"
title="{{ __('incentive.vip_terms_accepted') }}"></i>&nbsp;
@else
<i class="ion ion-md-close-circle vip-terms-pending ml-1"
title="{{ __('incentive.vip_terms_pending') }}"></i>&nbsp;
@endif
@endif
@if ($p->user && $p->user->account)
{{ $p->user->account->first_name }} {{ $p->user->account->last_name }}
@else
@ -947,6 +1005,11 @@
</table>
</div>
@endif
@if ($ranking->hasPages())
<div class="p-3">
{{ $ranking->links() }}
</div>
@endif
</div>
</div>

View file

@ -1,73 +1,148 @@
@extends($user_shop ?'web.user.layouts.layout' : 'web.layouts.layout')
@extends($user_shop ? 'web.user.layouts.layout' : 'web.layouts.layout')
@section('content')
@php
$isCancel = ($error_type ?? 'error') === 'cancel';
$errorcode = $errorcode ?? null;
$errorDescription = $error_description ?? null;
// Fehlercode → konkreten Hinweistext ermitteln
$errorReason = null;
if (!$isCancel && $errorcode) {
$code = (int) $errorcode;
if (in_array($code, [33])) {
$errorReason = __('payment.payment_error_reasons.card_expired');
} elseif (in_array($code, [4, 34])) {
$errorReason = __('payment.payment_error_reasons.card_blocked');
} elseif (in_array($code, [12, 14, 105])) {
$errorReason = __('payment.payment_error_reasons.card_invalid');
} elseif (in_array($code, [5, 902, 4219])) {
$errorReason = __('payment.payment_error_reasons.card_declined');
} elseif (in_array($code, [130])) {
$errorReason = __('payment.payment_error_reasons.insufficient_funds');
} elseif (in_array($code, [120])) {
$errorReason = __('payment.payment_error_reasons.cvv_invalid');
} elseif (in_array($code, [900])) {
$errorReason = __('payment.payment_error_reasons.3ds_failed');
} elseif (in_array($code, [970, 135])) {
$errorReason = __('payment.payment_error_reasons.timeout');
} elseif (in_array($code, [4218])) {
$errorReason = __('payment.payment_error_reasons.fraud');
} else {
$errorReason = __('payment.payment_error_reasons.general');
}
} elseif (!$isCancel) {
$errorReason = __('payment.payment_error_reasons.general');
}
@endphp
<section class="page-header page-header-xlg parallax parallax-3"
style="background-image:url('/assets/images/vision-min.jpg')">
<div class="overlay dark-1"><!-- dark overlay [1 to 9 opacity] --></div>
<div class="container">
</div>
style="background-image:url('/assets/images/vision-min.jpg')">
<div class="overlay dark-1"></div>
<div class="container"></div>
</section>
<!-- /PAGE HEADER -->
<style>
div.shop-item {
margin-bottom:30px;
border: 1px solid #ddd;
}
div.shop-item > .thumbnail, .thumbnail {
border: none;
}
div.shop-item-summary {
padding: 8px;
}
div.shop-item-summary h2 a {
color: #9aa983;
font-size: 1.2em;
margin: 0 0 10px 0;
}
div.shop-item-buttons {
padding: 0 8px 10px 8px;
}
div.shop-item-buttons .btn-xs{
padding: 4px;
}
</style>
<!-- -->
<!-- -->
<section>
<section class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-2 col-md-12"></div>
<div class="col-md-12 col-lg-8">
<!-- CHECKOUT ERROR MESSAGE -->
<div class="panel panel-default">
<div class="panel-body">
<div class="alert alert-danger">
<h3><i class="fa fa-exclamation-triangle"></i> {{ $error_title ?? __('payment.payment_error') }}</h3>
<p>{{ $error_message ?? __('payment.payment_error_description') }}</p>
</div>
<hr />
@if ($isCancel)
{{-- ── ABGEBROCHEN ──────────────────────────────── --}}
<div class="panel panel-default">
<div class="panel-body text-center py-4">
<div style="font-size:3rem; color:#f0ad4e;" class="mb-3">
<i class="fa fa-ban"></i>
</div>
<h3 class="mb-2">{{ $error_title }}</h3>
<p class="text-muted mb-1">{{ $error_message }}</p>
<p class="text-muted small mb-4">{{ __('payment.nothing_was_charged') }}</p>
<p>{{ __('payment.contact_support_if_needed') }}</p>
<p>
<strong>{{ __('payment.your_mivita_team') }}</strong>
</p>
<div class="alert alert-info text-left py-2 mb-4" style="font-size:0.9rem;">
<i class="fa fa-info-circle mr-1"></i>
{{ __('payment.payment_canceled_hint') }}
</div>
@if($user_shop)
<div class="mt-4">
<a href="{{ config('app.protocol') . $user_shop->slug . '.' . config('app.domain') . config('app.tld_care') }}" class="btn btn-primary">
<i class="fa fa-arrow-left"></i> {{ __('payment.back_to_shop') }}
</a>
</div>
@if (isset($checkout_url))
<a href="{{ $checkout_url }}" class="btn btn-primary btn-lg btn-block mb-2">
<i class="fa fa-refresh mr-1"></i>
{{ __('payment.try_again') }}
</a>
@endif
@if ($user_shop)
<a href="{{ config('app.protocol') . $user_shop->slug . '.' . config('app.domain') . config('app.tld_care') }}"
class="btn btn-default btn-block">
<i class="fa fa-arrow-left mr-1"></i> {{ __('payment.back_to_shop') }}
</a>
@endif
</div>
</div>
@else
{{-- ── FEHLER ───────────────────────────────────── --}}
<div class="panel panel-default">
<div class="panel-body py-4">
<div class="text-center mb-3">
<div style="font-size:3rem; color:#d9534f;">
<i class="fa fa-exclamation-circle"></i>
</div>
<h3 class="mt-2 mb-1">{{ $error_title }}</h3>
<p class="text-muted mb-0">{{ $error_message }}</p>
<small class="text-muted">{{ __('payment.nothing_was_charged') }}</small>
</div>
<hr>
{{-- Konkreter Hinweis basierend auf Fehlercode --}}
<div class="alert alert-warning mb-3" style="font-size:0.9rem;">
<strong><i
class="fa fa-lightbulb-o mr-1"></i>{{ __('payment.payment_error_what_to_do') }}</strong><br>
{{ $errorReason }}
</div>
{{-- Fehlerbeschreibung + Code (für Transparenz) --}}
@if ($errorcode || $errorDescription)
<div class="panel panel-default mb-3" style="font-size:0.82rem;">
<div class="panel-heading py-1 px-3" style="font-size:0.82rem;">
<strong>{{ __('payment.payment_error_code') }}</strong>
</div>
<div class="panel-body py-2 px-3">
@if ($errorcode)
<span class="label label-danger mr-2">{{ $errorcode }}</span>
@endif
@if ($errorDescription)
<span class="text-muted">{{ $errorDescription }}</span>
@endif
</div>
</div>
@endif
{{-- Aktionsbuttons --}}
@if (isset($checkout_url))
<a href="{{ $checkout_url }}" class="btn btn-primary btn-block btn-lg mb-2">
<i class="fa fa-refresh mr-1"></i>
{{ __('payment.payment_error_retry') }}
</a>
@endif
@if ($user_shop)
<a href="{{ config('app.protocol') . $user_shop->slug . '.' . config('app.domain') . config('app.tld_care') }}"
class="btn btn-default btn-block mb-3">
<i class="fa fa-arrow-left mr-1"></i> {{ __('payment.back_to_shop') }}
</a>
@endif
<p class="text-muted text-center small mb-0">
<i class="fa fa-envelope-o mr-1"></i>
{{ __('payment.contact_support_if_needed') }}
</p>
</div>
</div>
@endif
</div>
</div>
<!-- /CHECKOUT ERROR MESSAGE -->
</div>
</section>
<!-- / -->
@endsection

View file

@ -210,8 +210,16 @@
@if (\Session::has('errormessage'))
<div class="row">
<div class="col-sm-12">
<div class="alert alert-danger">
{{ \Session::get('customermessage') }}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading mb-1">
<i class="fa fa-exclamation-circle mr-1"></i>
{{ __('payment.payment_error') }}
</h5>
@if(\Session::get('customermessage'))
<p class="mb-1">{{ \Session::get('customermessage') }}</p>
@endif
<hr class="my-2">
<p class="mb-0 small">{{ __('payment.payment_error_hint') }}</p>
</div>
</div>
</div>