presseportale/resources/views/livewire/customer/invoices.blade.php
Kevin Adametz 2cde7284a1 livewire: Volt entfernt — Komponenten als Livewire-4-Single-File-Components, Route::livewire(), Livewire::test(); livewire/volt deinstalliert
- 83 Komponenten: Livewire\Volt\Component → Livewire\Component
- 73 Routen (admin, auth, customer): Volt::route() → Route::livewire()
- 88 Testdateien: Volt::test/actingAs → Livewire::test/actingAs
- VoltServiceProvider gelöscht; composer remove livewire/volt
- config/livewire.php auf v4-Schlüssel (component_layout, component_placeholder,
  component_locations, make_command: SFC ohne ⚡). Der alte Schlüssel layout war
  wirkungslos; alle gerouteten Komponenten setzen ihr Layout selbst.
- CLAUDE.md, AGENTS.md, boost.json, .cursorrules, Skill volt-development
- Roadmap: erledigt; Larastan erfasst SFCs weiterhin nicht (Blade-Dateien)

Suite 2.325 grün, einer übersprungen; Larastan und Pint ohne Meldung;
route:cache und view:cache laufen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:59:36 +00:00

280 lines
14 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\Enums\InvoiceStatus;
use App\Models\Invoice;
use App\Models\LegacyInvoice;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Rechnungen')] class extends Component
{
use WithPagination;
public ?string $notification = null;
public function with(): array
{
// Nie bezahlte Alt-Rechnungen (hold) sind erledigt und erscheinen
// dem Kunden nicht mehr als „offen".
$baseQuery = LegacyInvoice::query()
->where('user_id', auth()->id())
->withoutHold();
// Neue Rechnungen (STR = Stripe-Spiegelung, MAN = Bestandskreis).
$currentQuery = Invoice::query()->where('user_id', auth()->id());
// Das Altsystem-Archiv erscheint nur, wenn der User dort überhaupt
// Rechnungen hat (QA-Befund 07.07.) — Neukunden sehen es nie.
$invoices = (clone $baseQuery)
->latest('invoice_date')
->paginate(100);
return [
'currentInvoices' => (clone $currentQuery)
->latest('invoice_date')
->latest('id')
->limit(100)
->get(),
'invoices' => $invoices,
'stats' => [
'count' => (clone $baseQuery)->count() + (clone $currentQuery)->count(),
'total_cents' => (int) (clone $baseQuery)->sum('total_cents') + (int) (clone $currentQuery)->sum('total_cents'),
'paid_count' => (clone $baseQuery)->whereNotNull('paid_at')->count()
+ (clone $currentQuery)->where('status', InvoiceStatus::Paid->value)->count(),
'downloadable_count' => (clone $baseQuery)->count()
+ (clone $currentQuery)->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>
</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)]">
{{ $invoices->total() > 0
? __('Aktuelle Rechnungen Ihrer Buchungen sowie das Archiv aus dem Altsystem.')
: __('Aktuelle Rechnungen Ihrer Buchungen.') }}
</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
@if (session('invoice-error'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
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" />
{{ session('invoice-error') }}
</div>
@endif
{{-- ============== 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>{{ __('neu + Archiv') }}</x-slot:meta>
<x-slot:trend>{{ __('User-spezifisch') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Gesamtsumme')" :value="number_format($stats['total_cents'] / 100, 2, ',', '.').' €'">
<x-slot:meta>{{ __('alle Rechnungen') }}</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="__('Beleg verfügbar')" :value="number_format($stats['downloadable_count'])">
<x-slot:meta>{{ __('PDF / Stripe') }}</x-slot:meta>
<x-slot:trend>{{ __('direkt abrufbar') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== AKTUELLE RECHNUNGEN (STR/MAN) ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktuelle Rechnungen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $currentInvoices->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Rechnungsnr.') }}</flux:table.column>
<flux:table.column>{{ __('Netto') }}</flux:table.column>
<flux:table.column>{{ __('USt.') }}</flux:table.column>
<flux:table.column>{{ __('Gesamt') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Rechnungsdatum') }}</flux:table.column>
<flux:table.column>{{ __('Beleg') }}</flux:table.column>
</flux:table.columns>
@forelse ($currentInvoices as $invoice)
<flux:table.row wire:key="invoice-{{ $invoice->id }}">
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $invoice->number }}</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink-2)] tabular-nums">
{{ number_format($invoice->amount_cents / 100, 2, ',', '.') }} €
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink-2)] tabular-nums">
{{ number_format($invoice->tax_cents / 100, 2, ',', '.') }} €
</span>
</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>
@if ($invoice->due_date)
<div class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5">{{ __('zahlbar bis :date', ['date' => $invoice->due_date->format('d.m.Y')]) }}</div>
@endif
@else
<span class="badge">{{ $invoice->status->label() }}</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>
@if ($invoice->stripe_invoice_id)
<flux:button
size="sm"
variant="filled"
icon="arrow-top-right-on-square"
:href="route('me.invoices.stripe', $invoice)"
target="_blank"
>
{{ __('Bei Stripe öffnen') }}
</flux:button>
@else
<flux:button
size="sm"
variant="filled"
icon="document-text"
:href="route('me.invoices.manual-pdf', $invoice)"
target="_blank"
>
{{ __('PDF öffnen') }}
</flux:button>
@endif
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="7">
<div class="px-4 py-6 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Noch keine Rechnungen aus neuen Buchungen — sie erscheinen hier automatisch nach der ersten Abrechnung.') }}
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
</article>
{{-- ============== RECHNUNGSARCHIV (ALTSYSTEM) ==============
Nur für Bestandskunden mit importierten Alt-Rechnungen. --}}
@if ($invoices->total() > 0)
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechnungsarchiv (Altsystem)') }}</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>
@foreach ($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">{{ __('Bezahlt') }}</span>
@elseif ($invoice->status === \App\Models\LegacyInvoice::STATUS_SUPERSEDED)
<span class="badge hub">{{ __('Ersetzt durch :number', ['number' => $invoice->raw_snapshot['superseded_by_invoice_number'] ?? '–']) }}</span>
@elseif ($invoice->status === \App\Models\LegacyInvoice::STATUS_UNCOLLECTIBLE)
<span class="badge">{{ __('Erledigt') }}</span>
@else
<span class="badge warn dot">{{ __('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>
@endforeach
</flux:table>
<x-portal.table-pagination :paginator="$invoices" />
</article>
@endif
</div>