presseportale/resources/views/livewire/customer/invoices.blade.php
Kevin Adametz 505852f29e 5.4: <x-portal.table-pagination> + 13 Wrapper ersetzt
Buendelt den 13-fach byte-identischen Paginierungs-Fuss (border-t + p-4 um
->links('components.portal.pagination')) in eine Component mit :paginator-
Prop. Erzeugt identisches HTML -> kein visuelles Risiko.

Ersetzt in: users, invoices/index, payments/index (4x: Bestandsschutz,
Subscriptions, Purchases, Invoices), contacts/index, legal-requests/index,
presets/index, companies/index, footer-codes/index, customer/invoices,
customer/press-releases/index.

Bewusst BARE gelassen (kein Wrapper im Original): press-releases/index,
categories/index, press-kits/index. Alle Views kompilieren, 251 Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:35:37 +00:00

228 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php
use App\Models\LegacyInvoice;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Rechnungen')] class extends Component
{
use WithPagination;
public string $search = '';
public string $statusFilter = 'all';
public ?string $notification = null;
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function with(): array
{
$baseQuery = LegacyInvoice::query()
->where('user_id', auth()->id());
$invoices = (clone $baseQuery)
->when(filled($this->search), function ($query): void {
$query->where('number', 'like', '%'.$this->search.'%');
})
->when($this->statusFilter !== 'all', fn ($query) => $query->where('status', $this->statusFilter))
->latest('invoice_date')
->paginate(100);
return [
'invoices' => $invoices,
'statusOptions' => (clone $baseQuery)
->whereNotNull('status')
->distinct()
->orderBy('status')
->pluck('status')
->filter()
->values(),
'stats' => [
'count' => (clone $baseQuery)->count(),
'total_cents' => (int) (clone $baseQuery)->sum('total_cents'),
'paid_count' => (clone $baseQuery)->whereNotNull('paid_at')->count(),
'downloadable_count' => (clone $baseQuery)->count(),
],
];
}
}; ?>
<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">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Finanzen') }}</span>
<span class="badge hub">{{ __('Archivdaten') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Rechnungen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Ihr Rechnungsarchiv im User Backend. PDFs werden bei Bedarf aus den Archivdaten erzeugt.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button size="sm" variant="filled" icon="user" href="{{ route('me.profile') }}#rechnungsadresse" wire:navigate>
{{ __('Rechnungsadresse im Profil pflegen') }}
</flux:button>
</div>
</header>
@if ($notification)
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">{{ $notification }}</div>
</div>
@endif
{{-- ============== HINWEIS-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Hinweis zu Rechnungen') }}</span>
</div>
<div class="p-5">
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('Aktuell sehen Sie hier die aus dem Legacy-System übernommenen Rechnungen. Neue Abrechnungen werden später in dieselbe Finanznavigation integriert.') }}
</p>
</div>
</article>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Rechnungen')" :value="number_format($stats['count'])">
<x-slot:meta>{{ __('Archivdatensätze') }}</x-slot:meta>
<x-slot:trend>{{ __('User-spezifisch') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Archivsumme')" :value="number_format($stats['total_cents'] / 100, 2, ',', '.').' €'">
<x-slot:meta>{{ __('historisch') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Bezahlt')" :value="number_format($stats['paid_count'])">
<x-slot:meta>{{ __('vollständig') }}</x-slot:meta>
<x-slot:trend>{{ __('mit Zahlungsdatum') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('PDF-Download')" :value="number_format($stats['downloadable_count'])">
<x-slot:meta>{{ __('verfügbar') }}</x-slot:meta>
<x-slot:trend>{{ __('aus Archivdaten') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5 flex flex-col gap-3 sm:flex-row">
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Rechnungsnummer suchen…') }}" icon="magnifying-glass" class="flex-1" />
<flux:select wire:model.live="statusFilter" class="sm:w-48">
<option value="all">{{ __('Alle Status') }}</option>
@foreach ($statusOptions as $status)
<option value="{{ $status }}">{{ $status }}</option>
@endforeach
</flux:select>
</div>
</article>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Rechnungen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $invoices->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Rechnungsnr.') }}</flux:table.column>
<flux:table.column>{{ __('Portal') }}</flux:table.column>
<flux:table.column>{{ __('Betrag') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Rechnungsdatum') }}</flux:table.column>
<flux:table.column>{{ __('Bezahlt am') }}</flux:table.column>
<flux:table.column>{{ __('PDF') }}</flux:table.column>
</flux:table.columns>
@forelse ($invoices as $invoice)
<flux:table.row wire:key="legacy-invoice-{{ $invoice->id }}">
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $invoice->number ?? ('#'.$invoice->legacy_id) }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="badge hub">{{ $invoice->legacy_portal?->label() }}</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ number_format($invoice->total_cents / 100, 2, ',', '.') }} €
</span>
</flux:table.cell>
<flux:table.cell>
@if ($invoice->paid_at)
<span class="badge ok dot">{{ $invoice->status ?? __('Bezahlt') }}</span>
@else
<span class="badge warn dot">{{ $invoice->status ?? __('Offen') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $invoice->invoice_date?->format('d.m.Y') ?? '–' }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $invoice->paid_at?->format('d.m.Y') ?? '–' }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:button
size="sm"
variant="filled"
icon="arrow-top-right-on-square"
:href="route('me.invoices.pdf', $invoice)"
target="_blank"
>
{{ __('Öffnen') }}
</flux:button>
</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="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.document-text class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Rechnungen gefunden') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Sobald Rechnungen aus dem Archiv oder aus neuen Buchungen vorhanden sind, erscheinen sie hier.') }}
</p>
<flux:button class="mt-4" size="sm" variant="filled" icon="user" href="{{ route('me.profile') }}#rechnungsadresse" wire:navigate>
{{ __('Rechnungsadresse prüfen') }}
</flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
<x-portal.table-pagination :paginator="$invoices" />
</article>
</div>