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

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

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

93 lines
4.2 KiB
PHP

@extends('layouts.layout-2')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-sm btn-outline-secondary mr-2">
<i class="ion ion-md-arrow-back"></i> Zurück
</a>
<strong>Provider-Log-Viewer</strong>
</div>
<form method="GET" action="{{ route('admin.payment-dashboard.logs') }}" class="form-inline">
<select name="channel" class="custom-select custom-select-sm mr-2" onchange="this.form.submit()">
@foreach($availableChannels as $channel)
<option value="{{ $channel }}" {{ $selectedChannel === $channel ? 'selected' : '' }}>
{{ strtoupper($channel) }}
</option>
@endforeach
</select>
@if(count($availableDates) > 1)
<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>
@endif
</form>
</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>{{ $selectedChannel }}</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