admin: Credits und freie PMs ohne Zahlung gutschreiben
Neues Panel "Guthaben & Kontingent" auf der Benutzer-Detailseite (users:manage): Credits gehen als Grant-Buchung ins Wallet-Ledger, freie Pressemitteilungen als bezahlte 0-€-Einzelkäufe, die das Buchungs-Gate erfüllen und beim Veröffentlichen regulär eingelöst werden — Stripe bleibt unberührt. Jede Gutschrift landet mit Vergeber und Grund als neuer Typ "Gutschrift" im Aktivitäts-Verlauf. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
80c5f03e6f
commit
3f4679fbb2
4 changed files with 322 additions and 0 deletions
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Billing\AdminGrantService;
|
||||
use App\Services\Billing\CreditWalletService;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Title;
|
||||
|
|
@ -11,6 +14,14 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
|
|||
#[Locked]
|
||||
public int $id;
|
||||
|
||||
public int $grantCreditsAmount = 100;
|
||||
|
||||
public string $grantCreditsNote = '';
|
||||
|
||||
public int $grantPmCount = 1;
|
||||
|
||||
public string $grantPmNote = '';
|
||||
|
||||
public function mount(int $id): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
|
|
@ -25,6 +36,54 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
|
|||
}
|
||||
}
|
||||
|
||||
public function grantCredits(): void
|
||||
{
|
||||
$this->validate([
|
||||
'grantCreditsAmount' => ['required', 'integer', 'min:1', 'max:100000'],
|
||||
'grantCreditsNote' => ['nullable', 'string', 'max:240'],
|
||||
]);
|
||||
|
||||
$user = User::query()->findOrFail($this->id);
|
||||
|
||||
app(AdminGrantService::class)->grantCredits(
|
||||
$user,
|
||||
$this->grantCreditsAmount,
|
||||
auth()->user(),
|
||||
trim($this->grantCreditsNote) ?: null,
|
||||
);
|
||||
|
||||
Flux::toast(
|
||||
text: __(':count Credits gutgeschrieben.', ['count' => $this->grantCreditsAmount]),
|
||||
variant: 'success',
|
||||
);
|
||||
|
||||
$this->reset('grantCreditsNote');
|
||||
}
|
||||
|
||||
public function grantFreePressReleases(): void
|
||||
{
|
||||
$this->validate([
|
||||
'grantPmCount' => ['required', 'integer', 'min:1', 'max:100'],
|
||||
'grantPmNote' => ['nullable', 'string', 'max:240'],
|
||||
]);
|
||||
|
||||
$user = User::query()->findOrFail($this->id);
|
||||
|
||||
app(AdminGrantService::class)->grantFreePressReleases(
|
||||
$user,
|
||||
$this->grantPmCount,
|
||||
auth()->user(),
|
||||
trim($this->grantPmNote) ?: null,
|
||||
);
|
||||
|
||||
Flux::toast(
|
||||
text: __(':count freie Pressemitteilung(en) gutgeschrieben.', ['count' => $this->grantPmCount]),
|
||||
variant: 'success',
|
||||
);
|
||||
|
||||
$this->reset('grantPmNote');
|
||||
}
|
||||
|
||||
public function companyUserRoleLabel(?string $role): string
|
||||
{
|
||||
return match ($role ?? 'member') {
|
||||
|
|
@ -55,6 +114,9 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
|
|||
|
||||
return [
|
||||
'user' => $user,
|
||||
'creditBalance' => $user ? app(CreditWalletService::class)->balance($user) : 0,
|
||||
'openPmPurchases' => $user?->openSinglePurchaseCount() ?? 0,
|
||||
'quotaRemaining' => $user?->pressReleaseQuotaRemaining(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
|
@ -138,6 +200,75 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
|
|||
</div>
|
||||
</article>
|
||||
|
||||
{{-- ============== GUTHABEN & KONTINGENT ============== --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Guthaben & Kontingent') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('Gutschriften ohne Zahlung — landen im Verlauf') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-5">
|
||||
<div class="grid gap-2 sm:grid-cols-3">
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Credit-Saldo') }}</div>
|
||||
<div class="text-[15px] font-semibold text-[color:var(--color-ink)] mt-1 tabular-nums">
|
||||
{{ number_format($creditBalance, 0, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Offene PM-Käufe/Gutschriften') }}</div>
|
||||
<div class="text-[15px] font-semibold text-[color:var(--color-ink)] mt-1 tabular-nums">
|
||||
{{ number_format($openPmPurchases, 0, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('PM-Kontingent verbleibend') }}</div>
|
||||
<div class="text-[15px] font-semibold text-[color:var(--color-ink)] mt-1 tabular-nums">
|
||||
{{ $quotaRemaining === null ? __('Unbegrenzt') : number_format($quotaRemaining, 0, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] p-4 space-y-3">
|
||||
<div class="text-[12.5px] font-semibold text-[color:var(--color-ink)]">{{ __('Credits gutschreiben') }}</div>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anzahl Credits') }}</flux:label>
|
||||
<flux:input type="number" min="1" max="100000" wire:model="grantCreditsAmount" />
|
||||
<flux:error name="grantCreditsAmount" />
|
||||
</flux:field>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Grund (optional, erscheint im Ledger & Verlauf)') }}</flux:label>
|
||||
<flux:input wire:model="grantCreditsNote" maxlength="240" :placeholder="__('z. B. interne Nutzung, Kulanz…')" />
|
||||
<flux:error name="grantCreditsNote" />
|
||||
</flux:field>
|
||||
<flux:button variant="primary" icon="banknotes" wire:click="grantCredits">
|
||||
{{ __('Credits gutschreiben') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] p-4 space-y-3">
|
||||
<div class="text-[12.5px] font-semibold text-[color:var(--color-ink)]">{{ __('Freie Pressemitteilungen gutschreiben') }}</div>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anzahl PMs') }}</flux:label>
|
||||
<flux:input type="number" min="1" max="100" wire:model="grantPmCount" />
|
||||
<flux:error name="grantPmCount" />
|
||||
</flux:field>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Grund (optional, erscheint im Verlauf)') }}</flux:label>
|
||||
<flux:input wire:model="grantPmNote" maxlength="240" :placeholder="__('z. B. eigene PMs über das Admin-Konto…')" />
|
||||
<flux:error name="grantPmNote" />
|
||||
</flux:field>
|
||||
<flux:button variant="primary" icon="newspaper" wire:click="grantFreePressReleases">
|
||||
{{ __('Freie PMs gutschreiben') }}
|
||||
</flux:button>
|
||||
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
|
||||
{{ __('Wird als bezahlter 0-€-Einzelkauf verbucht und beim Veröffentlichen regulär eingelöst.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Rechnungsadresse') }}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue