presseportale/resources/views/livewire/admin/payments/index.blade.php
Kevin Adametz be7d1799a5 WS-4: Bestandsschutz-Abrechnung – gekoppelte Kündigung + Admin-Sichtbarkeit
M1 (portalübergreifend) und M5 (04:30-Worker) verifiziert; M2 + M3 umgesetzt.

- M2 (Entscheidung: getrennte Rechnungen, gekoppelte Kündigung): neuer
  LegacySubscriptionService::cancelBundleFor() kündigt das gesamte
  Bestandsschutz-Bündel eines Kunden gemeinsam – keine Einzelkündigung. Kein
  Rechnungs-Schema-Umbau (jede Kondition behält ihre MAN-Rechnung).
- M3: Admin-Payments-Ansicht zeigt eine "Bestandsschutz-Posten"-Tabelle
  (Badge, Portal, Netto, nächste Fälligkeit, "Bündel (N)") + Bündel-Kündigung
  mit Bestätigungsmodal.
- Tests: tests/Feature/Billing/LegacyBundleTest.php (M1 cross-portal, M2 Bündel-
  Kündigung + Gate, M3 Admin-Liste + Kündigung). Detailplan WS-4 dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:20:18 +00:00

577 lines
30 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Enums\InvoiceStatus;
use App\Enums\UserPaymentOptionStatus;
use App\Models\Invoice;
use App\Models\Plan;
use App\Models\SinglePurchase;
use App\Models\User;
use App\Models\UserPaymentOption;
use App\Services\Billing\LegacySubscriptionService;
use Illuminate\Database\Eloquent\Builder;
use Laravel\Cashier\Subscription;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Zahlungen')] class extends Component
{
use WithPagination;
public string $search = '';
public string $notification = '';
public string $notificationType = 'success';
public function updatedSearch(): void
{
$this->resetPage('subscriptionsPage');
$this->resetPage('purchasesPage');
$this->resetPage('invoicesPage');
$this->resetPage('bestandsschutzPage');
}
/**
* Kündigt das gesamte Bestandsschutz-Bündel eines Kunden gemeinsam
* (M2: keine Einzelkündigung).
*/
public function cancelBundle(int $userId, LegacySubscriptionService $legacy): void
{
$user = User::query()->find($userId);
if (! $user) {
$this->notification = __('Der angeforderte Kunde wurde nicht gefunden.');
$this->notificationType = 'error';
return;
}
$count = $legacy->cancelBundleFor($user);
$this->notification = trans_choice(
':count Bestandsschutz-Posten gemeinsam gekündigt.|:count Bestandsschutz-Posten gemeinsam gekündigt.',
$count,
['count' => $count],
);
$this->notificationType = 'success';
$this->resetPage('bestandsschutzPage');
}
public function with(): array
{
$plans = Plan::query()->get();
/** @var array<string, array{plan: Plan, interval: string}> $plansByPriceId */
$plansByPriceId = [];
foreach ($plans as $plan) {
if ($plan->stripe_price_id_monthly) {
$plansByPriceId[$plan->stripe_price_id_monthly] = ['plan' => $plan, 'interval' => __('monatlich')];
}
if ($plan->stripe_price_id_yearly) {
$plansByPriceId[$plan->stripe_price_id_yearly] = ['plan' => $plan, 'interval' => __('jährlich')];
}
}
$activeSubscriptions = Subscription::query()->active()->get();
$monthlyRecurringCents = $activeSubscriptions->sum(function (Subscription $subscription) use ($plansByPriceId): int {
$entry = $plansByPriceId[$subscription->stripe_price] ?? null;
if (! $entry) {
return 0;
}
return $entry['interval'] === __('jährlich')
? (int) round($entry['plan']->yearly_price_cents / 12)
: $entry['plan']->monthly_price_cents;
});
// Bestandsschutz-Posten (M3): laufende Grandfathered-Vereinbarungen.
$grandfatheredBase = fn (): Builder => UserPaymentOption::query()
->where('status', UserPaymentOptionStatus::Grandfathered->value)
->whereNull('cancelled_at');
// Bündel-Größe je Kunde (für die „Bündel (N)"-Anzeige) eine günstige
// Aggregat-Query statt N+1.
$bundleCounts = $grandfatheredBase()
->selectRaw('user_id, count(*) as aggregate_count')
->groupBy('user_id')
->pluck('aggregate_count', 'user_id');
return [
'plansByPriceId' => $plansByPriceId,
'stats' => [
'active_subscriptions' => $activeSubscriptions->count(),
'mrr_cents' => $monthlyRecurringCents,
'revenue_30d_cents' => (int) Invoice::query()
->where('status', InvoiceStatus::Paid->value)
->where('paid_at', '>=', now()->subDays(30))
->sum('total_cents'),
'open_purchases' => SinglePurchase::query()->grantingSubmission()->count(),
'grandfathered_posts' => $bundleCounts->sum(),
'grandfathered_customers' => $bundleCounts->count(),
],
'bestandsschutz' => $this->searchByUser($grandfatheredBase()->with('user:id,name,email'))
->orderBy('user_id')
->orderBy('id')
->paginate(25, pageName: 'bestandsschutzPage'),
'bundleCounts' => $bundleCounts,
'subscriptions' => $this->searchByUser(Subscription::query()->with('owner'))
->latest('created_at')
->paginate(25, pageName: 'subscriptionsPage'),
'purchases' => $this->searchByUser(SinglePurchase::query()->with(['user', 'pressRelease']))
->latest('created_at')
->paginate(25, pageName: 'purchasesPage'),
'invoices' => $this->searchByUser(Invoice::query()->with('user'))
->latest('invoice_date')
->latest('id')
->paginate(25, pageName: 'invoicesPage'),
];
}
/**
* Wendet die User-Suche (Name oder E-Mail) auf eine der drei
* Zahlungs-Tabellen an. Abos hängen über `owner` am User, Käufe und
* Rechnungen über `user`.
*/
private function searchByUser(Builder $query): Builder
{
if (! filled($this->search)) {
return $query;
}
$search = trim($this->search);
$relation = $query->getModel() instanceof Subscription ? 'owner' : 'user';
return $query->whereHas($relation, function (Builder $query) use ($search): void {
$query
->where('name', 'like', '%'.$search.'%')
->orWhere('email', 'like', '%'.$search.'%');
});
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="page-header">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Finanzen') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Zahlungen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ __('Stripe-Abos, Einmalkäufe und der lokale Rechnungsausgang (STR-/MAN-Kreis) auf einen Blick. Stripe bleibt Zahlungs- und Belegquelle — diese Übersicht spiegelt die per Webhook synchronisierten Daten.') }}
</p>
</div>
<div class="flex items-center gap-2">
<flux:button size="sm" variant="filled" icon="archive-box" :href="route('admin.invoices.index')" wire:navigate>
{{ __('Legacy-Rechnungen') }}
</flux:button>
<flux:button size="sm" variant="primary" icon="rectangle-stack" :href="route('admin.payments.plans')" wire:navigate>
{{ __('Tarife & Pakete') }}
</flux:button>
</div>
</header>
@if ($notification)
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 3500)" x-show="show" x-transition
@class([
'px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2',
'bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]' => $notificationType === 'error',
'bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]' => $notificationType !== 'error',
])>
@if ($notificationType === 'error')
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0" />
@else
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
@endif
{{ $notification }}
</div>
@endif
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Aktive Abos')" :value="number_format($stats['active_subscriptions'], 0, ',', '.')">
<x-slot:meta>{{ __('Stripe-Subscriptions') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('MRR (netto)')" :value="number_format($stats['mrr_cents'] / 100, 2, ',', '.').' €'">
<x-slot:meta>{{ __('monatlich wiederkehrend') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Umsatz 30 Tage')" :value="number_format($stats['revenue_30d_cents'] / 100, 2, ',', '.').' €'">
<x-slot:meta>{{ __('bezahlte Rechnungen, brutto') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Offene Einzel-PMs')" :value="number_format($stats['open_purchases'], 0, ',', '.')">
<x-slot:meta>{{ __('bezahlt, noch nicht eingelöst') }}</x-slot:meta>
</x-portal.stat-card>
</section>
{{-- ============== SUCHE ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Suche') }}</span>
</div>
<div class="p-5">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Nach User-Name oder E-Mail suchen — filtert Abos, Käufe und Rechnungen...') }}"
icon="magnifying-glass"
class="max-w-xl"
/>
</div>
</article>
{{-- ============== BESTANDSSCHUTZ (Legacy-Posten, M3/M2) ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Bestandsschutz-Posten') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':posts Posten · :customers Kunden', [
'posts' => number_format($stats['grandfathered_posts'], 0, ',', '.'),
'customers' => number_format($stats['grandfathered_customers'], 0, ',', '.'),
]) }}
</span>
</div>
<div class="px-5 pt-4 text-[12px] text-[color:var(--color-ink-3)]">
{{ __('Migrierte Legacy-Vereinbarungen (kein Tarif-Merge). Posten je Portal getrennt, aber als Bündel nur gemeinsam kündbar keine Einzelkündigung.') }}
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('User') }}</flux:table.column>
<flux:table.column>{{ __('Kondition') }}</flux:table.column>
<flux:table.column>{{ __('Portal') }}</flux:table.column>
<flux:table.column>{{ __('Netto') }}</flux:table.column>
<flux:table.column>{{ __('Nächste Fälligkeit') }}</flux:table.column>
<flux:table.column>{{ __('Bündel') }}</flux:table.column>
<flux:table.column>{{ __('Aktion') }}</flux:table.column>
</flux:table.columns>
@forelse ($bestandsschutz as $post)
@php
$conditions = $post->legacy_conditions ?? [];
$netCents = (int) ($conditions['net_cents'] ?? 0);
$portalValue = $conditions['legacy_portal'] ?? null;
$bundleSize = (int) ($bundleCounts[$post->user_id] ?? 1);
@endphp
<flux:table.row wire:key="bestandsschutz-{{ $post->id }}">
<flux:table.cell>
@if ($post->user)
<div class="space-y-0.5">
<flux:button size="xs" variant="filled" :href="route('admin.users.show', $post->user)" wire:navigate>
{{ $post->user->name }}
</flux:button>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $post->user->email }}</div>
</div>
@else
<span class="text-[12px] text-[color:var(--color-ink-3)]"></span>
@endif
</flux:table.cell>
<flux:table.cell>
<span class="badge ok dot">{{ __('Bestandsschutz') }}</span>
<div class="text-[12px] text-[color:var(--color-ink)] mt-1">
{{ $conditions['name'] ?? ($post->paymentOption?->article_number ?? '') }}
</div>
</flux:table.cell>
<flux:table.cell>
<span class="badge hub">{{ $portalValue ? \Illuminate\Support\Str::headline($portalValue) : __('Unbekannt') }}</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink)]">
{{ number_format($netCents / 100, 2, ',', '.') }} €
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $post->current_period_end?->format('d.m.Y') ?? '' }}
</span>
</flux:table.cell>
<flux:table.cell>
@if ($bundleSize > 1)
<span class="badge warn">{{ __('Bündel (:n)', ['n' => $bundleSize]) }}</span>
@else
<span class="badge">{{ __('Einzeln') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:modal.trigger name="cancel-bundle-{{ $post->user_id }}">
<flux:button size="sm" variant="filled" icon="x-circle" type="button"
x-on:click.prevent="$dispatch('open-modal', 'cancel-bundle-{{ $post->user_id }}')">
{{ __('Bündel kündigen') }}
</flux:button>
</flux:modal.trigger>
<flux:modal name="cancel-bundle-{{ $post->user_id }}" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Bestandsschutz-Bündel kündigen?') }}</flux:heading>
<flux:subheading>
{{ trans_choice(
'Es wird :n Bestandsschutz-Posten von :user gekündigt. Eine Einzelkündigung ist nicht vorgesehen.|Es werden alle :n Bestandsschutz-Posten von :user gemeinsam gekündigt. Eine Einzelkündigung ist nicht vorgesehen.',
$bundleSize,
['n' => $bundleSize, 'user' => $post->user?->name ?? ''],
) }}
</flux:subheading>
</div>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="danger" wire:click="cancelBundle({{ $post->user_id }})">
{{ __('Bündel kündigen') }}
</flux:button>
</div>
</div>
</flux:modal>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="7">
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine laufenden Bestandsschutz-Posten') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $bestandsschutz->links('components.portal.pagination') }}
</div>
</article>
{{-- ============== ABOS ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Abos') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => number_format($subscriptions->total(), 0, ',', '.')]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('User') }}</flux:table.column>
<flux:table.column>{{ __('Tarif') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Seit') }}</flux:table.column>
<flux:table.column>{{ __('Endet') }}</flux:table.column>
</flux:table.columns>
@forelse ($subscriptions as $subscription)
<flux:table.row wire:key="admin-subscription-{{ $subscription->id }}">
<flux:table.cell>
@if ($subscription->owner)
<div class="space-y-0.5">
<flux:button size="xs" variant="filled" :href="route('admin.users.show', $subscription->owner)" wire:navigate>
{{ $subscription->owner->name }}
</flux:button>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $subscription->owner->email }}</div>
</div>
@else
<span class="text-[12px] text-[color:var(--color-ink-3)]"></span>
@endif
</flux:table.cell>
<flux:table.cell>
@php($planEntry = $plansByPriceId[$subscription->stripe_price] ?? null)
@if ($planEntry)
<div class="space-y-0.5">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $planEntry['plan']->name }}</div>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $planEntry['interval'] }}</div>
</div>
@else
<div class="text-[11px] text-[color:var(--color-ink-3)] font-mono">{{ $subscription->stripe_price ?? '' }}</div>
@endif
</flux:table.cell>
<flux:table.cell>
@if (in_array($subscription->stripe_status, ['active', 'trialing'], true))
<span class="badge ok dot">{{ $subscription->stripe_status === 'trialing' ? __('Testphase') : __('Aktiv') }}</span>
@elseif (in_array($subscription->stripe_status, ['past_due', 'unpaid', 'incomplete'], true))
<span class="badge warn dot">{{ $subscription->stripe_status }}</span>
@else
<span class="badge">{{ $subscription->stripe_status }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-2)]">{{ $subscription->created_at?->format('d.m.Y') ?? '' }}</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-2)]">{{ $subscription->ends_at?->format('d.m.Y') ?? '' }}</span>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="5">
<div class="px-4 py-8 text-center text-[13px] text-[color:var(--color-ink-3)]">
{{ __('Noch keine Stripe-Abos vorhanden.') }}
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $subscriptions->links('components.portal.pagination') }}
</div>
</article>
{{-- ============== EINMALKÄUFE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Einmalkäufe') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => number_format($purchases->total(), 0, ',', '.')]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('User') }}</flux:table.column>
<flux:table.column>{{ __('Typ') }}</flux:table.column>
<flux:table.column>{{ __('Betrag (netto)') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Bezahlt am') }}</flux:table.column>
<flux:table.column>{{ __('Eingelöst für') }}</flux:table.column>
</flux:table.columns>
@forelse ($purchases as $purchase)
<flux:table.row wire:key="admin-purchase-{{ $purchase->id }}">
<flux:table.cell>
@if ($purchase->user)
<div class="space-y-0.5">
<flux:button size="xs" variant="filled" :href="route('admin.users.show', $purchase->user)" wire:navigate>
{{ $purchase->user->name }}
</flux:button>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $purchase->user->email }}</div>
</div>
@else
<span class="text-[12px] text-[color:var(--color-ink-3)]"></span>
@endif
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink-2)]">{{ $purchase->type->label() }}</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($purchase->price_cents / 100, 2, ',', '.') }} €</span>
</flux:table.cell>
<flux:table.cell>
@if ($purchase->status === \App\Enums\SinglePurchaseStatus::Paid)
<span class="badge ok dot">{{ $purchase->status->label() }}</span>
@elseif ($purchase->status === \App\Enums\SinglePurchaseStatus::Consumed)
<span class="badge hub dot">{{ $purchase->status->label() }}</span>
@elseif ($purchase->status === \App\Enums\SinglePurchaseStatus::Pending)
<span class="badge warn dot">{{ $purchase->status->label() }}</span>
@else
<span class="badge">{{ $purchase->status->label() }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-2)]">{{ $purchase->paid_at?->format('d.m.Y H:i') ?? '' }}</span>
</flux:table.cell>
<flux:table.cell>
@if ($purchase->pressRelease)
<flux:button size="xs" variant="filled" :href="route('admin.press-releases.show', $purchase->pressRelease->id)" wire:navigate>
{{ \Illuminate\Support\Str::limit($purchase->pressRelease->title, 40) }}
</flux:button>
@else
<span class="text-[12px] text-[color:var(--color-ink-3)]"></span>
@endif
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="6">
<div class="px-4 py-8 text-center text-[13px] text-[color:var(--color-ink-3)]">
{{ __('Noch keine Einmalkäufe vorhanden.') }}
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $purchases->links('components.portal.pagination') }}
</div>
</article>
{{-- ============== RECHNUNGEN (STR/MAN) ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechnungsausgang (STR/MAN)') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => number_format($invoices->total(), 0, ',', '.')]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Nummer') }}</flux:table.column>
<flux:table.column>{{ __('Kreis') }}</flux:table.column>
<flux:table.column>{{ __('User') }}</flux:table.column>
<flux:table.column>{{ __('Betrag (brutto)') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Rechnungsdatum') }}</flux:table.column>
</flux:table.columns>
@forelse ($invoices as $invoice)
<flux:table.row wire:key="admin-invoice-{{ $invoice->id }}">
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)] font-mono">{{ $invoice->number }}</span>
</flux:table.cell>
<flux:table.cell>
@if ($invoice->stripe_invoice_id)
<span class="badge hub">{{ __('Stripe (STR)') }}</span>
@else
<span class="badge">{{ __('Manuell (MAN)') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
@if ($invoice->user)
<div class="space-y-0.5">
<flux:button size="xs" variant="filled" :href="route('admin.users.show', $invoice->user)" wire:navigate>
{{ $invoice->user->name }}
</flux:button>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $invoice->user->email }}</div>
</div>
@else
<span class="text-[12px] text-[color:var(--color-ink-3)]"></span>
@endif
</flux:table.cell>
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($invoice->total_cents / 100, 2, ',', '.') }} €</span>
</flux:table.cell>
<flux:table.cell>
@if ($invoice->status === \App\Enums\InvoiceStatus::Paid)
<span class="badge ok dot">{{ $invoice->status->label() }}</span>
@elseif ($invoice->status === \App\Enums\InvoiceStatus::Open)
<span class="badge warn dot">{{ $invoice->status->label() }}</span>
@else
<span class="badge">{{ $invoice->status->label() }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<div class="space-y-0.5">
<div class="text-[12px] text-[color:var(--color-ink-2)]">{{ $invoice->invoice_date?->format('d.m.Y') ?? '' }}</div>
@if ($invoice->paid_at)
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('bezahlt: :date', ['date' => $invoice->paid_at->format('d.m.Y')]) }}</div>
@endif
</div>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="6">
<div class="px-4 py-8 text-center text-[13px] text-[color:var(--color-ink-3)]">
{{ __('Noch keine Rechnungen im neuen Rechnungsausgang.') }}
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $invoices->links('components.portal.pagination') }}
</div>
</article>
</div>