- 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>
1024 lines
57 KiB
PHP
1024 lines
57 KiB
PHP
<?php
|
||
|
||
use App\Enums\InvoiceStatus;
|
||
use App\Enums\UserPaymentOptionStatus;
|
||
use App\Exceptions\ManualInvoiceActionException;
|
||
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 App\Services\Billing\ManualInvoiceDunningService;
|
||
use App\Services\Billing\ManualInvoiceService;
|
||
use Flux\Flux;
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Support\Carbon;
|
||
use Laravel\Cashier\Subscription;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Title;
|
||
use Livewire\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';
|
||
|
||
/** Rechnungsausgang: `all` oder `man_open` (offene MAN-Rechnungen zuerst bearbeiten). */
|
||
public string $invoiceFilter = 'man_open';
|
||
|
||
/** Eingaben der Aktions-Modals (eine Rechnung zur Zeit). */
|
||
public string $paidAtInput = '';
|
||
|
||
public string $terminateNote = '';
|
||
|
||
public bool $terminateNotify = true;
|
||
|
||
public string $abandonNote = '';
|
||
|
||
public function mount(): void
|
||
{
|
||
$this->paidAtInput = today()->toDateString();
|
||
}
|
||
|
||
public function updatedSearch(): void
|
||
{
|
||
$this->resetPage('subscriptionsPage');
|
||
$this->resetPage('purchasesPage');
|
||
$this->resetPage('invoicesPage');
|
||
$this->resetPage('bestandsschutzPage');
|
||
}
|
||
|
||
public function updatedInvoiceFilter(): void
|
||
{
|
||
$this->resetPage('invoicesPage');
|
||
}
|
||
|
||
/**
|
||
* MAN-Rechnung per Mail (mit PDF) an den Kunden senden; Zahlungsziel
|
||
* läuft ab heute.
|
||
*/
|
||
public function sendInvoice(int $invoiceId, ManualInvoiceDunningService $dunning): void
|
||
{
|
||
$this->runInvoiceAction($invoiceId, fn (Invoice $invoice): string => __(
|
||
'Rechnung :number an :email gesendet — zahlbar bis :date.',
|
||
$this->messageParts($dunning->send($invoice, auth()->user())),
|
||
), 'send-invoice-'.$invoiceId);
|
||
}
|
||
|
||
public function remindInvoice(int $invoiceId, ManualInvoiceDunningService $dunning): void
|
||
{
|
||
$this->runInvoiceAction($invoiceId, fn (Invoice $invoice): string => __(
|
||
'Zahlungserinnerung zu :number an :email gesendet — zahlbar bis :date.',
|
||
$this->messageParts($dunning->remind($invoice, auth()->user())),
|
||
), 'remind-invoice-'.$invoiceId);
|
||
}
|
||
|
||
public function markInvoicePaid(int $invoiceId, ManualInvoiceDunningService $dunning): void
|
||
{
|
||
$this->validate(['paidAtInput' => ['required', 'date']]);
|
||
|
||
$this->runInvoiceAction($invoiceId, function (Invoice $invoice) use ($dunning): string {
|
||
$paid = $dunning->markPaid($invoice, Carbon::parse($this->paidAtInput)->startOfDay(), auth()->user());
|
||
|
||
return __('Rechnung :number als bezahlt markiert (Zahlungseingang :date).', [
|
||
'number' => $paid->number,
|
||
'date' => $paid->paid_at->format('d.m.Y'),
|
||
]);
|
||
}, 'paid-invoice-'.$invoiceId);
|
||
}
|
||
|
||
public function terminateInvoice(int $invoiceId, ManualInvoiceDunningService $dunning): void
|
||
{
|
||
$this->runInvoiceAction($invoiceId, function (Invoice $invoice) use ($dunning): string {
|
||
$cancelled = $dunning->terminateForNonPayment(
|
||
$invoice,
|
||
auth()->user(),
|
||
$this->terminateNotify,
|
||
filled($this->terminateNote) ? trim($this->terminateNote) : null,
|
||
);
|
||
|
||
$this->reset(['terminateNote', 'terminateNotify']);
|
||
$this->resetPage('bestandsschutzPage');
|
||
|
||
return trans_choice(
|
||
'Rechnung :number storniert, :count Bestandsschutz-Posten gekündigt.|Rechnung :number storniert, :count Bestandsschutz-Posten gekündigt.',
|
||
$cancelled,
|
||
['number' => $invoice->number, 'count' => $cancelled],
|
||
);
|
||
}, 'terminate-invoice-'.$invoiceId);
|
||
}
|
||
|
||
/**
|
||
* Fällige, aber blockierte Vereinbarung sofort abrechnen (nachdem z. B.
|
||
* die Rechnungsadresse nachgetragen wurde) statt auf den 04:30-Lauf zu warten.
|
||
*/
|
||
public function invoiceNow(int $optionId, ManualInvoiceService $manualInvoices): void
|
||
{
|
||
$this->authorize('billing:manage');
|
||
|
||
$option = UserPaymentOption::query()->find($optionId);
|
||
$invoice = $option ? $manualInvoices->invoiceFor($option) : null;
|
||
|
||
if (! $invoice) {
|
||
$this->notify(__('Die Vereinbarung konnte nicht abgerechnet werden — fehlt die Rechnungsadresse des Kunden?'), 'error');
|
||
|
||
return;
|
||
}
|
||
|
||
$this->notify(__('Rechnung :number ausgestellt — jetzt an den Kunden senden.', ['number' => $invoice->number]));
|
||
}
|
||
|
||
/**
|
||
* Blockierte Vereinbarung ohne Abrechnung entfernen (Kunde nicht
|
||
* erreichbar / keine Adresse): Bündel gekündigt, Alt-Rechnung abgeschrieben.
|
||
*/
|
||
public function abandonAgreement(int $optionId, ManualInvoiceDunningService $dunning): void
|
||
{
|
||
$this->authorize('billing:manage');
|
||
|
||
$option = UserPaymentOption::query()->with('user')->find($optionId);
|
||
|
||
if (! $option) {
|
||
$this->notify(__('Die Vereinbarung wurde nicht gefunden.'), 'error');
|
||
|
||
return;
|
||
}
|
||
|
||
try {
|
||
$cancelled = $dunning->abandonAgreement($option, auth()->user(), filled($this->abandonNote) ? trim($this->abandonNote) : null);
|
||
$this->reset('abandonNote');
|
||
$this->resetPage('bestandsschutzPage');
|
||
$this->notify(trans_choice(
|
||
'Vereinbarung von :user entfernt — :count Bestandsschutz-Posten gekündigt, keine Rechnung gestellt.|Vereinbarung von :user entfernt — :count Bestandsschutz-Posten gekündigt, keine Rechnung gestellt.',
|
||
$cancelled,
|
||
['user' => $option->user?->name ?? '–', 'count' => $cancelled],
|
||
));
|
||
} catch (ManualInvoiceActionException $exception) {
|
||
$this->notify($exception->getMessage(), 'error');
|
||
}
|
||
|
||
Flux::modal('abandon-agreement-'.$optionId)->close();
|
||
}
|
||
|
||
/**
|
||
* Kündigt das gesamte Bestandsschutz-Bündel eines Kunden gemeinsam
|
||
* (M2: keine Einzelkündigung).
|
||
*/
|
||
public function cancelBundle(int $userId, LegacySubscriptionService $legacy): void
|
||
{
|
||
$this->authorize('billing:manage');
|
||
|
||
$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');
|
||
}
|
||
|
||
/**
|
||
* Gemeinsamer Rahmen der Rechnungsaktionen: Berechtigung, Laden,
|
||
* fachliche Fehlermeldung statt Exception, Modal schließen.
|
||
*
|
||
* @param \Closure(Invoice): string $action
|
||
*/
|
||
private function runInvoiceAction(int $invoiceId, \Closure $action, string $modal): void
|
||
{
|
||
$this->authorize('billing:manage');
|
||
|
||
$invoice = Invoice::query()->with('user')->find($invoiceId);
|
||
|
||
if (! $invoice) {
|
||
$this->notify(__('Die Rechnung wurde nicht gefunden.'), 'error');
|
||
|
||
return;
|
||
}
|
||
|
||
try {
|
||
$this->notify($action($invoice));
|
||
} catch (ManualInvoiceActionException $exception) {
|
||
$this->notify($exception->getMessage(), 'error');
|
||
}
|
||
|
||
Flux::modal($modal)->close();
|
||
}
|
||
|
||
/**
|
||
* @return array{number: string, email: string, date: string}
|
||
*/
|
||
private function messageParts(Invoice $invoice): array
|
||
{
|
||
return [
|
||
'number' => (string) $invoice->number,
|
||
'email' => (string) $invoice->user?->email,
|
||
'date' => (string) $invoice->due_date?->format('d.m.Y'),
|
||
];
|
||
}
|
||
|
||
private function notify(string $message, string $type = 'success'): void
|
||
{
|
||
$this->notification = $message;
|
||
$this->notificationType = $type;
|
||
}
|
||
|
||
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');
|
||
|
||
// Offene MAN-Rechnungen (Überweisungskunden): der Bearbeitungsstapel.
|
||
$openManual = Invoice::query()->manual()->open();
|
||
|
||
// Fällige Vereinbarungen ohne Rechnung = vom 04:30-Lauf übersprungen
|
||
// (fehlende Rechnungsadresse o. ä.) — sonst wäre die Periode weitergeschaltet.
|
||
$blockedAgreements = app(ManualInvoiceService::class)->duePaymentOptions(limit: 100);
|
||
|
||
// Letzte MAN-Rechnung je Bestandsschutz-Posten (für die Inline-Anzeige).
|
||
$latestInvoiceByOption = Invoice::query()
|
||
->manual()
|
||
->whereNotNull('user_payment_option_id')
|
||
->orderByDesc('invoice_date')
|
||
->orderByDesc('id')
|
||
->get()
|
||
->unique('user_payment_option_id')
|
||
->keyBy('user_payment_option_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(),
|
||
'open_manual_count' => (clone $openManual)->count(),
|
||
'open_manual_cents' => (int) (clone $openManual)->sum('total_cents'),
|
||
'overdue_manual_count' => (clone $openManual)->whereDate('due_date', '<', today())->count(),
|
||
'unsent_manual_count' => (clone $openManual)->whereNull('sent_at')->count(),
|
||
],
|
||
'blockedAgreements' => $blockedAgreements,
|
||
'latestInvoiceByOption' => $latestInvoiceByOption,
|
||
'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'))
|
||
->when($this->invoiceFilter === 'man_open', fn (Builder $query) => $query->manual()->open())
|
||
->when($this->invoiceFilter === 'man_open', fn (Builder $query) => $query->orderBy('due_date'), fn (Builder $query) => $query->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 ============== --}}
|
||
<x-portal.page-header
|
||
:badge="__('Admin Backend')"
|
||
:eyebrow="__('Administration · Finanzen')"
|
||
:title="__('Zahlungen')"
|
||
:description="__('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.')"
|
||
description-width="720px"
|
||
>
|
||
<x-slot:actions>
|
||
<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>
|
||
</x-slot:actions>
|
||
</x-portal.page-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-5">
|
||
<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>
|
||
<x-portal.stat-card :variant="$stats['overdue_manual_count'] > 0 ? 'warn' : 'muted'" :label="__('Offene MAN-Rechnungen')" :value="number_format($stats['open_manual_count'], 0, ',', '.')">
|
||
<x-slot:meta>
|
||
{{ __(':sum € · :overdue überfällig · :unsent ungesendet', [
|
||
'sum' => number_format($stats['open_manual_cents'] / 100, 2, ',', '.'),
|
||
'overdue' => $stats['overdue_manual_count'],
|
||
'unsent' => $stats['unsent_manual_count'],
|
||
]) }}
|
||
</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>
|
||
|
||
{{-- ============== BLOCKIERTE VEREINBARUNGEN (fällig, aber ohne Rechnung) ============== --}}
|
||
@if ($blockedAgreements->isNotEmpty())
|
||
<article class="panel overflow-hidden border-[color:var(--color-warn)]/40">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Blockierte Vereinbarungen') }}</span>
|
||
<span class="badge warn dot">{{ trans_choice(':count fällig ohne Rechnung|:count fällig ohne Rechnung', $blockedAgreements->count(), ['count' => $blockedAgreements->count()]) }}</span>
|
||
</div>
|
||
<div class="px-5 pt-4 text-[12px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Die Fälligkeit ist erreicht, aber der tägliche Rechnungslauf konnte keine Rechnung ausstellen — in der Regel fehlt die Rechnungsadresse. Entweder Adresse im User-Profil nachtragen und „Jetzt abrechnen", oder die Vereinbarung ohne Abrechnung „Entfernen" (Kunde nicht erreichbar).') }}
|
||
</div>
|
||
<flux:table>
|
||
<flux:table.columns>
|
||
<flux:table.column>{{ __('User') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Kondition') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Fällig seit') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Grund') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Aktion') }}</flux:table.column>
|
||
</flux:table.columns>
|
||
@foreach ($blockedAgreements as $blocked)
|
||
<flux:table.row wire:key="blocked-{{ $blocked->id }}">
|
||
<flux:table.cell>
|
||
@if ($blocked->user)
|
||
<div class="space-y-0.5">
|
||
<flux:button size="xs" variant="filled" :href="route('admin.users.edit', $blocked->user)" wire:navigate>
|
||
{{ $blocked->user->name }}
|
||
</flux:button>
|
||
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $blocked->user->email }}</div>
|
||
</div>
|
||
@else
|
||
<span class="text-[12px] text-[color:var(--color-ink-3)]">–</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<div class="text-[12px] text-[color:var(--color-ink)]">{{ $blocked->legacy_conditions['name'] ?? ($blocked->paymentOption?->article_number ?? '–') }}</div>
|
||
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ number_format((int) ($blocked->legacy_conditions['net_cents'] ?? $blocked->paymentOption?->price_cents ?? 0) / 100, 2, ',', '.') }} € netto</div>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<span class="text-[12px] text-[color:var(--color-ink-2)]">{{ $blocked->current_period_end?->format('d.m.Y') }}</span>
|
||
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $blocked->current_period_end?->diffForHumans() }}</div>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
@if ($blocked->billing_hold_reason)
|
||
<span class="badge warn dot">{{ $blocked->billing_hold_reason }}</span>
|
||
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('seit :date', ['date' => $blocked->billing_hold_at?->format('d.m.Y')]) }}</div>
|
||
@elseif (! $blocked->user?->billingAddress)
|
||
<span class="badge warn dot">{{ __('Keine Rechnungsadresse') }}</span>
|
||
@else
|
||
<span class="badge">{{ __('Noch nicht geprüft') }}</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<div class="flex flex-wrap items-center gap-1.5">
|
||
<flux:button size="sm" variant="primary" icon="document-plus" wire:click="invoiceNow({{ $blocked->id }})">
|
||
{{ __('Jetzt abrechnen') }}
|
||
</flux:button>
|
||
<flux:modal.trigger name="abandon-agreement-{{ $blocked->id }}">
|
||
<flux:button size="sm" variant="danger" icon="trash">{{ __('Entfernen') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
</div>
|
||
|
||
<flux:modal name="abandon-agreement-{{ $blocked->id }}" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Vereinbarung von :user ohne Abrechnung entfernen?', ['user' => $blocked->user?->name ?? '–']) }}</flux:heading>
|
||
<flux:subheading>
|
||
{{ __('Es wird keine Rechnung gestellt. Alle Bestandsschutz-Posten des Kunden werden gekündigt, eine noch offene Alt-Rechnung im Archiv wird abgeschrieben. Der Kunde erhält keine E-Mail; für neue Veröffentlichungen gelten die aktuellen Tarife.') }}
|
||
</flux:subheading>
|
||
</div>
|
||
<flux:field>
|
||
<flux:label>{{ __('Interner Vermerk (optional)') }}</flux:label>
|
||
<flux:input wire:model="abandonNote" placeholder="{{ __('z. B. E-Mail nicht erreichbar, keine Adresse') }}" />
|
||
</flux:field>
|
||
<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" icon="trash" wire:click="abandonAgreement({{ $blocked->id }})">{{ __('Entfernen') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@endforeach
|
||
</flux:table>
|
||
</article>
|
||
@endif
|
||
|
||
{{-- ============== 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>
|
||
@if ($lastInvoice = $latestInvoiceByOption[$post->id] ?? null)
|
||
<div class="text-[11px] mt-0.5 flex items-center gap-1.5">
|
||
<span class="font-mono text-[color:var(--color-ink-2)]">{{ $lastInvoice->number }}</span>
|
||
@if ($lastInvoice->status === \App\Enums\InvoiceStatus::Paid)
|
||
<span class="badge ok">{{ __('bezahlt') }}</span>
|
||
@elseif ($lastInvoice->status === \App\Enums\InvoiceStatus::Open)
|
||
<span class="badge warn">{{ $lastInvoice->isOverdue() ? __('überfällig') : ($lastInvoice->sent_at ? __('offen') : __('ungesendet')) }}</span>
|
||
@else
|
||
<span class="badge">{{ $lastInvoice->status->label() }}</span>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</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>
|
||
|
||
<x-portal.table-pagination :paginator="$bestandsschutz" />
|
||
</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>
|
||
<x-portal.table-pagination :paginator="$subscriptions" />
|
||
</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>
|
||
<x-portal.table-pagination :paginator="$purchases" />
|
||
</article>
|
||
|
||
{{-- ============== RECHNUNGEN (STR/MAN) ============== --}}
|
||
<article class="panel overflow-hidden">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Rechnungsausgang (STR/MAN)') }}</span>
|
||
<div class="flex items-center gap-3">
|
||
<flux:select wire:model.live="invoiceFilter" size="sm" class="min-w-[220px]">
|
||
<option value="man_open">{{ __('Offene MAN-Rechnungen (zu bearbeiten)') }}</option>
|
||
<option value="all">{{ __('Alle Rechnungen (STR + MAN)') }}</option>
|
||
</flux:select>
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __(':count Einträge', ['count' => number_format($invoices->total(), 0, ',', '.')]) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
@if ($invoiceFilter === 'man_open')
|
||
<div class="px-5 pt-4 text-[12px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Überweisungskunden (Bestandsschutz): Rechnung senden → Zahlung abwarten → als bezahlt markieren. Ohne Zahlung bis zum Zahlungsziel: Bestandsschutz beenden (Rechnung wird storniert, Bündel gekündigt).') }}
|
||
</div>
|
||
@endif
|
||
<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.column>{{ __('Aktionen') }}</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>
|
||
@php($isManualOpen = $invoice->isManual() && $invoice->status === \App\Enums\InvoiceStatus::Open)
|
||
<div class="space-y-1">
|
||
@if ($invoice->status === \App\Enums\InvoiceStatus::Paid)
|
||
<span class="badge ok dot">{{ $invoice->status->label() }}</span>
|
||
@elseif ($isManualOpen && $invoice->isOverdue())
|
||
<span class="badge err dot">{{ __('Überfällig') }}</span>
|
||
@elseif ($invoice->status === \App\Enums\InvoiceStatus::Open)
|
||
<span class="badge warn dot">{{ $invoice->status->label() }}</span>
|
||
@elseif ($invoice->status === \App\Enums\InvoiceStatus::Void)
|
||
<span class="badge" title="{{ $invoice->void_reason }}">{{ $invoice->status->label() }}</span>
|
||
@else
|
||
<span class="badge">{{ $invoice->status->label() }}</span>
|
||
@endif
|
||
@if ($invoice->isManual())
|
||
<div class="text-[11px] text-[color:var(--color-ink-3)] leading-[1.5]">
|
||
@if ($invoice->sent_at)
|
||
{{ __('gesendet :date', ['date' => $invoice->sent_at->format('d.m.Y')]) }}
|
||
@elseif ($invoice->status === \App\Enums\InvoiceStatus::Open)
|
||
{{ __('noch nicht gesendet') }}
|
||
@endif
|
||
@if ($invoice->reminded_at)
|
||
· {{ __('erinnert :date (:n×)', ['date' => $invoice->reminded_at->format('d.m.Y'), 'n' => $invoice->reminder_count]) }}
|
||
@endif
|
||
@if ($isManualOpen && $invoice->due_date)
|
||
· {{ __('zahlbar bis :date', ['date' => $invoice->due_date->format('d.m.Y')]) }}
|
||
@endif
|
||
@if ($invoice->replaces_legacy_invoice_number)
|
||
· {{ __('ersetzt :number', ['number' => $invoice->replaces_legacy_invoice_number]) }}
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</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.cell>
|
||
@if ($invoice->isManual())
|
||
<div class="flex flex-wrap items-center gap-1.5">
|
||
<flux:button size="xs" variant="filled" icon="document-text" :href="route('admin.invoices.pdf', $invoice)" target="_blank">
|
||
{{ __('PDF') }}
|
||
</flux:button>
|
||
|
||
@if ($isManualOpen)
|
||
@if (! $invoice->sent_at)
|
||
<flux:modal.trigger name="send-invoice-{{ $invoice->id }}">
|
||
<flux:button size="xs" variant="primary" icon="paper-airplane">{{ __('Rechnung senden') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
@else
|
||
<flux:modal.trigger name="remind-invoice-{{ $invoice->id }}">
|
||
<flux:button size="xs" variant="filled" icon="bell-alert">{{ __('Erinnerung') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
@endif
|
||
<flux:modal.trigger name="paid-invoice-{{ $invoice->id }}">
|
||
<flux:button size="xs" variant="filled" icon="check-circle">{{ __('Bezahlt') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
<flux:modal.trigger name="terminate-invoice-{{ $invoice->id }}">
|
||
<flux:button size="xs" variant="danger" icon="x-circle">{{ __('Bestandsschutz beenden') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
@endif
|
||
</div>
|
||
|
||
@if ($isManualOpen)
|
||
{{-- Senden --}}
|
||
<flux:modal name="send-invoice-{{ $invoice->id }}" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Rechnung :number senden?', ['number' => $invoice->number]) }}</flux:heading>
|
||
<flux:subheading>
|
||
{{ __('Die Rechnung geht als PDF an :email — mit Bankverbindung, Zahlungsziel :date (:days Tage ab heute) und dem Hinweis, dass der Bestandsschutz ohne Zahlung zu diesem Datum endet.', [
|
||
'email' => $invoice->user?->email ?? '–',
|
||
'date' => today()->addDays((int) config('billing.manual_due_days', 14))->format('d.m.Y'),
|
||
'days' => (int) config('billing.manual_due_days', 14),
|
||
]) }}
|
||
</flux:subheading>
|
||
</div>
|
||
<dl class="grid grid-cols-2 gap-y-1 text-[12.5px]">
|
||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Betrag (brutto)') }}</dt>
|
||
<dd class="font-semibold tabular-nums">{{ number_format($invoice->total_cents / 100, 2, ',', '.') }} €</dd>
|
||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Rechnungsdatum') }}</dt>
|
||
<dd>{{ $invoice->invoice_date?->format('d.m.Y') }}</dd>
|
||
</dl>
|
||
<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="primary" icon="paper-airplane" wire:click="sendInvoice({{ $invoice->id }})">{{ __('Jetzt senden') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
|
||
{{-- Erinnern --}}
|
||
<flux:modal name="remind-invoice-{{ $invoice->id }}" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Zahlungserinnerung zu :number senden?', ['number' => $invoice->number]) }}</flux:heading>
|
||
<flux:subheading>
|
||
{{ __('Gleiche Rechnung als PDF, Betreff „Zahlungserinnerung". Ist das Zahlungsziel verstrichen, gilt eine Nachfrist von :days Tagen ab heute; sonst bleibt es bestehen.', ['days' => (int) config('billing.reminder_due_days', 7)]) }}
|
||
</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="primary" icon="bell-alert" wire:click="remindInvoice({{ $invoice->id }})">{{ __('Erinnerung senden') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
|
||
{{-- Bezahlt --}}
|
||
<flux:modal name="paid-invoice-{{ $invoice->id }}" class="max-w-md">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Zahlungseingang zu :number verbuchen', ['number' => $invoice->number]) }}</flux:heading>
|
||
<flux:subheading>{{ __('Betrag laut Kontoauszug: :amount €. Die Rechnung wird auf „Bezahlt" gesetzt.', ['amount' => number_format($invoice->total_cents / 100, 2, ',', '.')]) }}</flux:subheading>
|
||
</div>
|
||
<flux:field>
|
||
<flux:label>{{ __('Zahlungseingang am') }}</flux:label>
|
||
<flux:input type="date" wire:model="paidAtInput" max="{{ today()->toDateString() }}" />
|
||
<flux:error name="paidAtInput" />
|
||
</flux:field>
|
||
<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="primary" icon="check-circle" wire:click="markInvoicePaid({{ $invoice->id }})">{{ __('Als bezahlt markieren') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
|
||
{{-- Bestandsschutz beenden --}}
|
||
<flux:modal name="terminate-invoice-{{ $invoice->id }}" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Bestandsschutz von :user beenden?', ['user' => $invoice->user?->name ?? '–']) }}</flux:heading>
|
||
<flux:subheading>
|
||
{{ __('Rechnung :number wird storniert (keine Forderung mehr), alle Bestandsschutz-Posten des Kunden werden gekündigt und weitere offene MAN-Rechnungen des Kunden ebenfalls storniert. Veröffentlichte PMs bleiben online; neue Veröffentlichungen nur noch über die aktuellen Tarife.', ['number' => $invoice->number]) }}
|
||
</flux:subheading>
|
||
</div>
|
||
<flux:field>
|
||
<flux:label>{{ __('Interner Vermerk (optional)') }}</flux:label>
|
||
<flux:input wire:model="terminateNote" placeholder="{{ __('z. B. telefonisch nicht erreichbar') }}" />
|
||
</flux:field>
|
||
<flux:checkbox wire:model="terminateNotify" :label="__('Kunden per E-Mail informieren (Rechnung storniert, Bestandsschutz beendet, Hinweis auf Tarife)')" />
|
||
<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" icon="x-circle" wire:click="terminateInvoice({{ $invoice->id }})">{{ __('Bestandsschutz beenden') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
@endif
|
||
@else
|
||
<span class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('Stripe') }}</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@empty
|
||
<flux:table.row>
|
||
<flux:table.cell colspan="7">
|
||
<div class="px-4 py-8 text-center text-[13px] text-[color:var(--color-ink-3)]">
|
||
{{ $invoiceFilter === 'man_open' ? __('Keine offenen MAN-Rechnungen — alles abgearbeitet.') : __('Noch keine Rechnungen im neuen Rechnungsausgang.') }}
|
||
</div>
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@endforelse
|
||
</flux:table>
|
||
<x-portal.table-pagination :paginator="$invoices" />
|
||
</article>
|
||
</div>
|