1254 lines
56 KiB
PHP
1254 lines
56 KiB
PHP
<?php
|
||
|
||
use App\Models\RegistrationCode;
|
||
use Spatie\Permission\Models\Role;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Validation\Rule;
|
||
use Livewire\Volt\Component;
|
||
use Livewire\WithPagination;
|
||
use function Livewire\Volt\{layout, title};
|
||
|
||
layout('components.layouts.app');
|
||
title('Registrierungscodes verwalten');
|
||
|
||
new class extends Component {
|
||
use WithPagination;
|
||
|
||
public string $role = 'broker';
|
||
public string $codePrefix = 'M';
|
||
public string $codePart1 = '';
|
||
public string $codePart2 = '';
|
||
public string $codePart3 = '';
|
||
public string $codePart4 = '';
|
||
public ?string $name = null;
|
||
public string $customerBrokerType = 'broker'; // Tab-Auswahl für Kunden: broker oder retailer
|
||
public ?int $brokerPartnerId = null;
|
||
public ?string $expiresAt = null;
|
||
public ?string $note = null;
|
||
public string $rangeFrom = '';
|
||
public string $rangeTo = '';
|
||
public string $rangeFromPart1 = '';
|
||
public string $rangeFromPart2 = '';
|
||
public string $rangeFromPart3 = '';
|
||
public string $rangeFromPart4 = '';
|
||
public string $rangeToPart1 = '';
|
||
public string $rangeToPart2 = '';
|
||
public string $rangeToPart3 = '';
|
||
public string $rangeToPart4 = '';
|
||
|
||
// Filter und Sortierung für Tabelle
|
||
public string $tableRoleFilter = '';
|
||
public string $tableStatusFilter = '';
|
||
public string $tableAssignmentFilter = '';
|
||
public string $search = '';
|
||
public string $sortField = 'created_at';
|
||
public string $sortDirection = 'desc';
|
||
|
||
// roleOptions werden dynamisch aus der Datenbank geladen
|
||
public array $roleOptions = [];
|
||
|
||
public function mount(): void
|
||
{
|
||
$this->loadRoleOptions();
|
||
$this->codePrefix = $this->roleOptions[$this->role]['prefix'];
|
||
$this->expiresAt = now()->addDays(7)->format('Y-m-d');
|
||
if ($this->role === 'customer') {
|
||
$this->regenerateCustomerRange();
|
||
} else {
|
||
$this->regenerateNumber();
|
||
}
|
||
}
|
||
|
||
private function loadRoleOptions(): void
|
||
{
|
||
$roles = Role::whereNotNull('reg_prefix')->orderBy('id', 'asc')->get();
|
||
|
||
foreach ($roles as $role) {
|
||
// Konvertiere Role name zu lowercase für Konsistenz mit registration_codes.role
|
||
$key = strtolower(str_replace('-', '', $role->name)); // "Broker" → "broker"
|
||
|
||
// Für Broker spezielle Behandlung
|
||
$this->roleOptions[$key] = [
|
||
'label' => $role->display_name ?? $role->name,
|
||
'prefix' => $role->reg_prefix,
|
||
'startNumber' => $role->reg_start_number ?? 10000000,
|
||
'description' => $role->reg_description,
|
||
'color' => $role->color ?? 'zinc',
|
||
'icon' => $role->icon ?? 'key',
|
||
];
|
||
}
|
||
}
|
||
|
||
public function updatedRole(): void
|
||
{
|
||
$this->codePrefix = $this->roleOptions[$this->role]['prefix'];
|
||
$this->name = null;
|
||
$this->brokerPartnerId = null;
|
||
$this->customerBrokerType = 'broker';
|
||
$this->rangeFrom = '';
|
||
$this->rangeTo = '';
|
||
$this->rangeFromPart1 = '';
|
||
$this->rangeFromPart2 = '';
|
||
$this->rangeFromPart3 = '';
|
||
$this->rangeFromPart4 = '';
|
||
$this->rangeToPart1 = '';
|
||
$this->rangeToPart2 = '';
|
||
$this->rangeToPart3 = '';
|
||
$this->rangeToPart4 = '';
|
||
if ($this->role === 'customer') {
|
||
$this->regenerateCustomerRange();
|
||
} else {
|
||
$this->regenerateNumber();
|
||
}
|
||
}
|
||
|
||
public function updatedRangeFromPart1(): void
|
||
{
|
||
$this->updateRangeFrom();
|
||
}
|
||
|
||
public function updatedRangeFromPart2(): void
|
||
{
|
||
$this->updateRangeFrom();
|
||
}
|
||
|
||
public function updatedRangeFromPart3(): void
|
||
{
|
||
$this->updateRangeFrom();
|
||
}
|
||
|
||
public function updatedRangeFromPart4(): void
|
||
{
|
||
$this->updateRangeFrom();
|
||
}
|
||
|
||
public function updatedRangeToPart1(): void
|
||
{
|
||
$this->updateRangeTo();
|
||
}
|
||
|
||
public function updatedRangeToPart2(): void
|
||
{
|
||
$this->updateRangeTo();
|
||
}
|
||
|
||
public function updatedRangeToPart3(): void
|
||
{
|
||
$this->updateRangeTo();
|
||
}
|
||
|
||
public function updatedRangeToPart4(): void
|
||
{
|
||
$this->updateRangeTo();
|
||
}
|
||
|
||
protected function updateRangeFrom(): void
|
||
{
|
||
$this->rangeFrom = trim($this->rangeFromPart1 . ' ' . $this->rangeFromPart2 . ' ' . $this->rangeFromPart3 . ' ' . $this->rangeFromPart4);
|
||
}
|
||
|
||
protected function updateRangeTo(): void
|
||
{
|
||
$this->rangeTo = trim($this->rangeToPart1 . ' ' . $this->rangeToPart2 . ' ' . $this->rangeToPart3 . ' ' . $this->rangeToPart4);
|
||
}
|
||
|
||
public function updatedCustomerBrokerType(): void
|
||
{
|
||
$this->brokerPartnerId = null;
|
||
}
|
||
|
||
public function regenerateNumber(): void
|
||
{
|
||
$number = $this->getNextAvailableNumber();
|
||
$this->codePart1 = substr($number, 0, 2);
|
||
$this->codePart2 = substr($number, 2, 2);
|
||
$this->codePart3 = substr($number, 4, 2);
|
||
$this->codePart4 = substr($number, 6, 2);
|
||
}
|
||
|
||
public function regenerateCustomerRange(): void
|
||
{
|
||
$fromNumber = $this->getNextAvailableNumber();
|
||
|
||
// Setze "Code von" Parts
|
||
$this->rangeFromPart1 = substr($fromNumber, 0, 2);
|
||
$this->rangeFromPart2 = substr($fromNumber, 2, 2);
|
||
$this->rangeFromPart3 = substr($fromNumber, 4, 2);
|
||
$this->rangeFromPart4 = substr($fromNumber, 6, 2);
|
||
|
||
// Berechne "Code bis" (+10)
|
||
$fromNumberInt = (int) $fromNumber;
|
||
$toNumberInt = $fromNumberInt + 10;
|
||
$toNumber = str_pad((string) $toNumberInt, 8, '0', STR_PAD_LEFT);
|
||
|
||
// Setze "Code bis" Parts
|
||
$this->rangeToPart1 = substr($toNumber, 0, 2);
|
||
$this->rangeToPart2 = substr($toNumber, 2, 2);
|
||
$this->rangeToPart3 = substr($toNumber, 4, 2);
|
||
$this->rangeToPart4 = substr($toNumber, 6, 2);
|
||
|
||
// Aktualisiere rangeFrom und rangeTo
|
||
$this->updateRangeFrom();
|
||
$this->updateRangeTo();
|
||
}
|
||
|
||
public function updatedCodePart1(): void
|
||
{
|
||
$this->validateNumberUniqueness();
|
||
}
|
||
|
||
public function updatedCodePart2(): void
|
||
{
|
||
$this->validateNumberUniqueness();
|
||
}
|
||
|
||
public function updatedCodePart3(): void
|
||
{
|
||
$this->validateNumberUniqueness();
|
||
}
|
||
|
||
public function updatedCodePart4(): void
|
||
{
|
||
$this->validateNumberUniqueness();
|
||
}
|
||
|
||
protected function validateNumberUniqueness(): void
|
||
{
|
||
// Prüfe nur wenn alle Teile ausgefüllt sind
|
||
if (strlen($this->codePart1) < 2 || strlen($this->codePart2) < 2 ||
|
||
strlen($this->codePart3) < 2 || strlen($this->codePart4) < 2) {
|
||
$this->resetErrorBag(['codePart1', 'codePart2', 'codePart3', 'codePart4']);
|
||
return;
|
||
}
|
||
|
||
$number = $this->normalizeNumber($this->codePart1 . $this->codePart2 . $this->codePart3 . $this->codePart4);
|
||
|
||
// Prüfe ob diese Nummer bereits verwendet wird (unabhängig von Prefix/Rolle)
|
||
$exists = RegistrationCode::whereRaw('SUBSTRING(code, 2) = ?', [$number])->exists();
|
||
|
||
if ($exists) {
|
||
$this->addError('codePart1', __('Diese Nummer wird bereits verwendet. Jede Nummer ist einzigartig über alle Rollen hinweg.'));
|
||
} else {
|
||
$this->resetErrorBag(['codePart1', 'codePart2', 'codePart3', 'codePart4']);
|
||
}
|
||
}
|
||
|
||
public function createCode(): void
|
||
{
|
||
$prefix = $this->normalizedPrefix();
|
||
|
||
// Range-Modus für Kunden (immer)
|
||
if ($this->role === 'customer') {
|
||
$this->createCodeRange($prefix);
|
||
return;
|
||
}
|
||
|
||
// Einzelner Code
|
||
$number = $this->normalizeNumber($this->codePart1 . $this->codePart2 . $this->codePart3 . $this->codePart4);
|
||
$fullCode = $prefix . $number;
|
||
|
||
$this->validate($this->rules($prefix), $this->messages());
|
||
|
||
// Prüfe auf doppelten Code (Nummer unabhängig von Prefix/Rolle)
|
||
$numberExists = RegistrationCode::whereRaw('SUBSTRING(code, 2) = ?', [$number])->exists();
|
||
if ($numberExists) {
|
||
$this->addError('codePart1', __('Diese Nummer wird bereits verwendet. Jede Nummer ist einzigartig über alle Rollen hinweg.'));
|
||
return;
|
||
}
|
||
|
||
// Aktualisiere die Teile für die Anzeige
|
||
$this->codePart1 = substr($number, 0, 2);
|
||
$this->codePart2 = substr($number, 2, 2);
|
||
$this->codePart3 = substr($number, 4, 2);
|
||
$this->codePart4 = substr($number, 6, 2);
|
||
|
||
$metadata = [];
|
||
if ($this->note) {
|
||
$metadata['note'] = $this->note;
|
||
}
|
||
|
||
RegistrationCode::create([
|
||
'code' => $fullCode,
|
||
'role' => $this->role,
|
||
'name' => in_array($this->role, ['broker', 'retailer', 'manufacturer'], true) ? $this->name : null,
|
||
'status' => RegistrationCode::STATUS_AVAILABLE,
|
||
'broker_partner_id' => null,
|
||
'partner_id' => null,
|
||
'assigned_to_code_id' => $this->role === 'customer' ? $this->brokerPartnerId : null,
|
||
'expires_at' => $this->expiresAt ? Carbon::parse($this->expiresAt)->endOfDay() : null,
|
||
'metadata' => !empty($metadata) ? $metadata : null,
|
||
]);
|
||
|
||
Flux::toast(
|
||
heading: 'Success!',
|
||
text: 'Registrierungscode wurde angelegt.',
|
||
variant: 'success',
|
||
position: "top end",
|
||
duration: 3000
|
||
);
|
||
$this->reset(['name', 'brokerPartnerId', 'expiresAt', 'note', 'rangeFrom', 'rangeTo', 'rangeFromPart1', 'rangeFromPart2', 'rangeFromPart3', 'rangeFromPart4', 'rangeToPart1', 'rangeToPart2', 'rangeToPart3', 'rangeToPart4']);
|
||
$this->codePrefix = $this->roleOptions[$this->role]['prefix'];
|
||
$this->expiresAt = now()->addDays(7)->format('Y-m-d');
|
||
$this->regenerateNumber();
|
||
}
|
||
|
||
protected function createCodeRange(string $prefix): void
|
||
{
|
||
// Aktualisiere rangeFrom und rangeTo aus den Parts
|
||
$this->updateRangeFrom();
|
||
$this->updateRangeTo();
|
||
|
||
$this->validate([
|
||
'rangeFromPart1' => ['required', 'digits:2'],
|
||
'rangeFromPart2' => ['required', 'digits:2'],
|
||
'rangeFromPart3' => ['required', 'digits:2'],
|
||
'rangeFromPart4' => ['required', 'digits:2'],
|
||
'rangeToPart1' => ['required', 'digits:2'],
|
||
'rangeToPart2' => ['required', 'digits:2'],
|
||
'rangeToPart3' => ['required', 'digits:2'],
|
||
'rangeToPart4' => ['required', 'digits:2'],
|
||
'brokerPartnerId' => ['required', 'exists:registration_codes,id'],
|
||
], [
|
||
'rangeFromPart1.required' => __('Bitte geben Sie den Startwert der Range ein.'),
|
||
'rangeFromPart1.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'rangeFromPart2.required' => __('Bitte geben Sie den Startwert der Range ein.'),
|
||
'rangeFromPart2.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'rangeFromPart3.required' => __('Bitte geben Sie den Startwert der Range ein.'),
|
||
'rangeFromPart3.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'rangeFromPart4.required' => __('Bitte geben Sie den Startwert der Range ein.'),
|
||
'rangeFromPart4.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'rangeToPart1.required' => __('Bitte geben Sie den Endwert der Range ein.'),
|
||
'rangeToPart1.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'rangeToPart2.required' => __('Bitte geben Sie den Endwert der Range ein.'),
|
||
'rangeToPart2.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'rangeToPart3.required' => __('Bitte geben Sie den Endwert der Range ein.'),
|
||
'rangeToPart3.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'rangeToPart4.required' => __('Bitte geben Sie den Endwert der Range ein.'),
|
||
'rangeToPart4.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'brokerPartnerId.required' => __('Bitte wählen Sie einen Makler oder Händler aus.'),
|
||
]);
|
||
|
||
// Normalisiere die Eingabe (zusammenfügen der Parts)
|
||
$fromNumberStr = $this->normalizeNumber($this->rangeFromPart1 . $this->rangeFromPart2 . $this->rangeFromPart3 . $this->rangeFromPart4);
|
||
$toNumberStr = $this->normalizeNumber($this->rangeToPart1 . $this->rangeToPart2 . $this->rangeToPart3 . $this->rangeToPart4);
|
||
|
||
// Validiere Format (8 Ziffern)
|
||
if (!preg_match('/^\d{8}$/', $fromNumberStr)) {
|
||
$this->addError('rangeFrom', __('Format: 40 00 00 01 (8 Ziffern)'));
|
||
return;
|
||
}
|
||
|
||
if (!preg_match('/^\d{8}$/', $toNumberStr)) {
|
||
$this->addError('rangeTo', __('Format: 40 00 00 50 (8 Ziffern)'));
|
||
return;
|
||
}
|
||
|
||
$fromNumber = (int) $fromNumberStr;
|
||
$toNumber = (int) $toNumberStr;
|
||
|
||
if ($fromNumber >= $toNumber) {
|
||
$this->addError('rangeFrom', __('Der Startwert muss kleiner als der Endwert sein.'));
|
||
return;
|
||
}
|
||
|
||
if (($toNumber - $fromNumber) > 1000) {
|
||
$this->addError('rangeTo', __('Die Range darf maximal 1000 Codes umfassen.'));
|
||
return;
|
||
}
|
||
|
||
$created = 0;
|
||
$skipped = 0;
|
||
|
||
for ($num = $fromNumber; $num <= $toNumber; $num++) {
|
||
$numberStr = str_pad((string) $num, 8, '0', STR_PAD_LEFT);
|
||
$fullCode = $prefix . $numberStr;
|
||
|
||
// Prüfe ob bereits vorhanden
|
||
if (RegistrationCode::whereRaw('SUBSTRING(code, 2) = ?', [$numberStr])->exists()) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$metadata = [];
|
||
if ($this->note) {
|
||
$metadata['note'] = $this->note;
|
||
}
|
||
|
||
RegistrationCode::create([
|
||
'code' => $fullCode,
|
||
'role' => $this->role,
|
||
'name' => null,
|
||
'status' => RegistrationCode::STATUS_AVAILABLE,
|
||
'broker_partner_id' => null,
|
||
'partner_id' => null,
|
||
'assigned_to_code_id' => $this->brokerPartnerId,
|
||
'expires_at' => $this->expiresAt ? Carbon::parse($this->expiresAt)->endOfDay() : null,
|
||
'metadata' => !empty($metadata) ? $metadata : null,
|
||
]);
|
||
|
||
$created++;
|
||
}
|
||
|
||
Flux::toast(
|
||
heading: 'Success!',
|
||
text: __(':created Codes wurden angelegt. :skipped wurden übersprungen (bereits vorhanden).', ['created' => $created, 'skipped' => $skipped]),
|
||
variant: 'success',
|
||
position: "top end",
|
||
duration: 3000
|
||
);
|
||
|
||
|
||
$this->reset(['brokerPartnerId', 'expiresAt', 'note', 'rangeFrom', 'rangeTo', 'rangeFromPart1', 'rangeFromPart2', 'rangeFromPart3', 'rangeFromPart4', 'rangeToPart1', 'rangeToPart2', 'rangeToPart3', 'rangeToPart4']);
|
||
$this->expiresAt = now()->addDays(7)->format('Y-m-d');
|
||
if ($this->role === 'customer') {
|
||
$this->regenerateCustomerRange();
|
||
} else {
|
||
$this->regenerateNumber();
|
||
}
|
||
}
|
||
|
||
public function expire(int $codeId): void
|
||
{
|
||
/** @var RegistrationCode|null $code */
|
||
$code = RegistrationCode::find($codeId);
|
||
if (!$code) {
|
||
return;
|
||
}
|
||
|
||
if ($code->status === RegistrationCode::STATUS_USED) {
|
||
$this->addError('code', __('Dieser Code wurde bereits genutzt und kann nicht deaktiviert werden.'));
|
||
return;
|
||
}
|
||
|
||
$code->update([
|
||
'status' => RegistrationCode::STATUS_EXPIRED,
|
||
'expires_at' => $code->expires_at ?? now(),
|
||
]);
|
||
|
||
session()->flash('message', __('Code wurde deaktiviert.'));
|
||
}
|
||
|
||
protected function rules(string $prefix): array
|
||
{
|
||
$rules = [
|
||
'role' => ['required', Rule::in(array_keys($this->roleOptions))],
|
||
'codePrefix' => ['required', 'alpha', 'size:1'],
|
||
'name' => [
|
||
Rule::requiredIf(in_array($this->role, ['broker', 'retailer', 'manufacturer'], true)),
|
||
'nullable',
|
||
'string',
|
||
'max:255',
|
||
],
|
||
'expiresAt' => ['nullable', 'date'],
|
||
'note' => ['nullable', 'string', 'max:255'],
|
||
];
|
||
|
||
// Für Kunden gelten Range-Regeln
|
||
if ($this->role === 'customer') {
|
||
$rules['rangeFromPart1'] = ['required', 'digits:2'];
|
||
$rules['rangeFromPart2'] = ['required', 'digits:2'];
|
||
$rules['rangeFromPart3'] = ['required', 'digits:2'];
|
||
$rules['rangeFromPart4'] = ['required', 'digits:2'];
|
||
$rules['rangeToPart1'] = ['required', 'digits:2'];
|
||
$rules['rangeToPart2'] = ['required', 'digits:2'];
|
||
$rules['rangeToPart3'] = ['required', 'digits:2'];
|
||
$rules['rangeToPart4'] = ['required', 'digits:2'];
|
||
$rules['brokerPartnerId'] = ['required', 'exists:registration_codes,id'];
|
||
} else {
|
||
// Normale Code-Felder für andere Rollen
|
||
$rules['codePart1'] = ['required', 'digits:2'];
|
||
$rules['codePart2'] = ['required', 'digits:2'];
|
||
$rules['codePart3'] = ['required', 'digits:2'];
|
||
$rules['codePart4'] = ['required', 'digits:2'];
|
||
}
|
||
|
||
return $rules;
|
||
}
|
||
|
||
protected function messages(): array
|
||
{
|
||
return [
|
||
'role.required' => __('Bitte Rolle wählen.'),
|
||
'codePrefix.required' => __('Prefix ist erforderlich.'),
|
||
'codePrefix.alpha' => __('Prefix darf nur Buchstaben enthalten.'),
|
||
'codePrefix.size' => __('Prefix muss genau 1 Zeichen lang sein.'),
|
||
'codePart1.required' => __('Erster Block ist erforderlich.'),
|
||
'codePart1.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'codePart2.required' => __('Zweiter Block ist erforderlich.'),
|
||
'codePart2.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'codePart3.required' => __('Dritter Block ist erforderlich.'),
|
||
'codePart3.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'codePart4.required' => __('Vierter Block ist erforderlich.'),
|
||
'codePart4.digits' => __('Bitte genau 2 Ziffern eingeben.'),
|
||
'name.required' => __('Für Makler-, Händler- und Herstellercodes ist ein Name erforderlich.'),
|
||
'name.max' => __('Der Name darf maximal 255 Zeichen lang sein.'),
|
||
'brokerPartnerId.required' => __('Für Kundencodes muss ein Makler oder Händler gewählt werden.'),
|
||
'brokerPartnerId.exists' => __('Bitte einen gültigen Makler- oder Händlercode auswählen.'),
|
||
'expiresAt.date' => __('Bitte ein gültiges Datum wählen.'),
|
||
'note.max' => __('Notiz darf max. 255 Zeichen lang sein.'),
|
||
];
|
||
}
|
||
|
||
protected function normalizedPrefix(): string
|
||
{
|
||
$prefix = strtoupper(trim($this->codePrefix));
|
||
if ($prefix === '') {
|
||
$prefix = $this->roleOptions[$this->role]['prefix'] ?? 'X';
|
||
}
|
||
|
||
return substr($prefix, 0, 1);
|
||
}
|
||
|
||
protected function normalizeNumber(string $number): string
|
||
{
|
||
$digits = preg_replace('/\D+/', '', $number);
|
||
$digits = str_pad(substr($digits, 0, 8), 8, '0', STR_PAD_LEFT);
|
||
|
||
return $digits;
|
||
}
|
||
|
||
protected function getNextAvailableNumber(): string
|
||
{
|
||
$startNumber = $this->roleOptions[$this->role]['startNumber'] ?? '10000001';
|
||
$currentNumber = (int) $startNumber;
|
||
|
||
// Finde die nächste verfügbare Nummer im Bereich dieser Rolle
|
||
$maxAttempts = 10000; // Sicherheitslimit
|
||
$attempts = 0;
|
||
|
||
while ($attempts < $maxAttempts) {
|
||
$numberStr = str_pad((string) $currentNumber, 8, '0', STR_PAD_LEFT);
|
||
|
||
// Prüfe ob diese Nummer bereits verwendet wird (unabhängig von Prefix/Rolle)
|
||
$exists = RegistrationCode::whereRaw('SUBSTRING(code, 2) = ?', [$numberStr])->exists();
|
||
|
||
if (!$exists) {
|
||
return $numberStr;
|
||
}
|
||
|
||
$currentNumber++;
|
||
$attempts++;
|
||
}
|
||
|
||
// Fallback: zufällige Nummer wenn Bereich voll ist
|
||
return $this->generateRandomUniqueNumber();
|
||
}
|
||
|
||
protected function generateRandomUniqueNumber(): string
|
||
{
|
||
$attempts = 0;
|
||
do {
|
||
$attempts++;
|
||
$number = str_pad((string) random_int(10000000, 99999999), 8, '0', STR_PAD_LEFT);
|
||
$exists = RegistrationCode::whereRaw('SUBSTRING(code, 2) = ?', [$number])->exists();
|
||
} while ($exists && $attempts < 100);
|
||
|
||
return $number;
|
||
}
|
||
|
||
public function getNextAvailableNumberForDisplay(): string
|
||
{
|
||
$number = $this->getNextAvailableNumber();
|
||
return substr($number, 0, 2) . ' ' . substr($number, 2, 2) . ' ' . substr($number, 4, 2) . ' ' . substr($number, 6, 2);
|
||
}
|
||
|
||
public function sortBy(string $field): void
|
||
{
|
||
if ($this->sortField === $field) {
|
||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
$this->sortField = $field;
|
||
$this->sortDirection = 'asc';
|
||
}
|
||
}
|
||
|
||
public function updatingSearch(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatingTableRoleFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatingTableStatusFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatingTableAssignmentFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedTableRoleFilter(): void
|
||
{
|
||
// Reset assignment filter wenn Rolle geändert wird
|
||
$this->tableAssignmentFilter = '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function with(): array
|
||
{
|
||
$stats = [
|
||
'available' => RegistrationCode::where('status', RegistrationCode::STATUS_AVAILABLE)->count(),
|
||
'used' => RegistrationCode::where('status', RegistrationCode::STATUS_USED)->count(),
|
||
'expired' => RegistrationCode::where('status', RegistrationCode::STATUS_EXPIRED)->count(),
|
||
];
|
||
|
||
$recentCodes = RegistrationCode::with(['usedBy'])
|
||
->latest()
|
||
->take(25)
|
||
->get();
|
||
|
||
// Für Kunden: Filtere Codes basierend auf customerBrokerType
|
||
$brokerAndRetailerCodes = RegistrationCode::whereIn('role', $this->role === 'customer' ? [$this->customerBrokerType] : ['broker', 'retailer'])
|
||
->where('status', RegistrationCode::STATUS_AVAILABLE)
|
||
->orderBy('code')
|
||
->get();
|
||
|
||
// Query für Tabelle mit Filtern und Sortierung
|
||
$tableQuery = RegistrationCode::with(['usedBy', 'assignedToCode'])
|
||
->when($this->tableRoleFilter, fn($q) => $q->where('role', $this->tableRoleFilter))
|
||
->when($this->tableStatusFilter, fn($q) => $q->where('status', $this->tableStatusFilter))
|
||
->when($this->tableAssignmentFilter && $this->tableRoleFilter === 'customer', fn($q) =>
|
||
$q->where('assigned_to_code_id', $this->tableAssignmentFilter)
|
||
)
|
||
->when($this->search, fn($q) =>
|
||
$q->where(function($query) {
|
||
$searchTerm = '%' . $this->search . '%';
|
||
$query->where('code', 'like', $searchTerm)
|
||
->orWhere('name', 'like', $searchTerm)
|
||
->orWhereHas('usedBy', fn($userQuery) =>
|
||
$userQuery->where('name', 'like', $searchTerm)
|
||
->orWhere('email', 'like', $searchTerm)
|
||
);
|
||
})
|
||
);
|
||
|
||
$codes = $tableQuery->orderBy($this->sortField, $this->sortDirection)->paginate(25);
|
||
|
||
// Finde alle Makler/Händler, die Kunden zugeordnet haben (mit neuer Spalte)
|
||
$assignmentIds = RegistrationCode::where('role', 'customer')
|
||
->whereNotNull('assigned_to_code_id')
|
||
->distinct()
|
||
->pluck('assigned_to_code_id');
|
||
|
||
$availableAssignments = RegistrationCode::whereIn('id', $assignmentIds)
|
||
->whereIn('role', ['broker', 'retailer'])
|
||
->orderBy('code')
|
||
->get();
|
||
|
||
return [
|
||
'stats' => $stats,
|
||
'recentCodes' => $recentCodes,
|
||
'roleOptions' => $this->roleOptions,
|
||
'nextAvailableNumber' => $this->getNextAvailableNumberForDisplay(),
|
||
'brokerAndRetailerCodes' => $brokerAndRetailerCodes,
|
||
'codes' => $codes,
|
||
'availableAssignments' => $availableAssignments ?? collect(),
|
||
];
|
||
}
|
||
}; ?>
|
||
|
||
<div class="space-y-6 p-6">
|
||
<flux:toast />
|
||
{{-- Header --}}
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<flux:heading size="xl" class="mb-2">{{ __('Registrierungscodes') }}</flux:heading>
|
||
<flux:subheading>{{ __('Codes erzeugen, zuordnen und deaktivieren') }}</flux:subheading>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Statistik --}}
|
||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||
<flux:card class="shadow-elegant">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<flux:subheading>{{ __('Verfügbar') }}</flux:subheading>
|
||
<flux:heading size="2xl" class="mt-2">{{ $stats['available'] }}</flux:heading>
|
||
</div>
|
||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-green-100 dark:bg-green-900/20">
|
||
<flux:icon.key class="h-6 w-6 text-green-600 dark:text-green-400" />
|
||
</div>
|
||
</div>
|
||
</flux:card>
|
||
|
||
<flux:card class="shadow-elegant">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<flux:subheading>{{ __('Verbraucht') }}</flux:subheading>
|
||
<flux:heading size="2xl" class="mt-2">{{ $stats['used'] }}</flux:heading>
|
||
</div>
|
||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
|
||
<flux:icon.user class="h-6 w-6 text-zinc-700 dark:text-zinc-300" />
|
||
</div>
|
||
</div>
|
||
</flux:card>
|
||
|
||
<flux:card class="shadow-elegant">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<flux:subheading>{{ __('Deaktiviert/abgelaufen') }}</flux:subheading>
|
||
<flux:heading size="2xl" class="mt-2">{{ $stats['expired'] }}</flux:heading>
|
||
</div>
|
||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/20">
|
||
<flux:icon.x-circle class="h-6 w-6 text-orange-600 dark:text-orange-400" />
|
||
</div>
|
||
</div>
|
||
</flux:card>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||
{{-- Formular --}}
|
||
<flux:card class="shadow-elegant">
|
||
<form wire:submit="createCode" class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg" class="mb-2">{{ __('Neuen Code anlegen') }}</flux:heading>
|
||
<flux:subheading>{{ __('Rolle wählen, Nummer generieren, optional zuordnen') }}</flux:subheading>
|
||
</div>
|
||
|
||
<flux:separator />
|
||
|
||
<flux:field>
|
||
<flux:label>{{ __('Rolle') }}</flux:label>
|
||
<flux:select wire:model.live="role">
|
||
@foreach($roleOptions as $key => $meta)
|
||
<flux:select.option :value="$key">
|
||
{{ $meta['label'] }} — {{ $meta['description'] }}
|
||
</flux:select.option>
|
||
@endforeach
|
||
</flux:select>
|
||
@error('role') <flux:error>{{ $message }}</flux:error> @enderror
|
||
</flux:field>
|
||
|
||
@if($role === 'customer')
|
||
{{-- Code von --}}
|
||
<flux:field>
|
||
<flux:label>{{ __('Code von') }}</flux:label>
|
||
<div class="flex items-center gap-2">
|
||
<div class="flex items-center gap-1">
|
||
<flux:input
|
||
wire:model="codePrefix"
|
||
maxlength="1"
|
||
readonly
|
||
class="w-16 text-center font-mono text-lg font-semibold"
|
||
/>
|
||
<span class="text-zinc-400">|</span>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="rangeFromPart1"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="rangeFromPart2"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="rangeFromPart3"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="rangeFromPart4"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
</div>
|
||
</div>
|
||
@error('rangeFrom') <flux:error>{{ $message }}</flux:error> @enderror
|
||
<p class="text-xs text-zinc-500 mt-1">{{ __('Format: Prefix + 4×2 Ziffern') }}</p>
|
||
</flux:field>
|
||
|
||
{{-- Code bis --}}
|
||
<flux:field>
|
||
<flux:label>{{ __('Code bis') }}</flux:label>
|
||
<div class="flex items-center gap-2">
|
||
<div class="flex items-center gap-1">
|
||
<flux:input
|
||
wire:model="codePrefix"
|
||
maxlength="1"
|
||
readonly
|
||
class="w-16 text-center font-mono text-lg font-semibold"
|
||
/>
|
||
<span class="text-zinc-400">|</span>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="rangeToPart1"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="rangeToPart2"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="rangeToPart3"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="rangeToPart4"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
</div>
|
||
</div>
|
||
@error('rangeTo') <flux:error>{{ $message }}</flux:error> @enderror
|
||
<p class="text-xs text-zinc-500 mt-1">{{ __('Format: Prefix + 4×2 Ziffern') }}</p>
|
||
</flux:field>
|
||
@else
|
||
{{-- Normale Code-Eingabe für andere Rollen --}}
|
||
<flux:field>
|
||
<flux:label>{{ __('Code') }}</flux:label>
|
||
<div class="flex items-center gap-2">
|
||
<div class="flex items-center gap-1">
|
||
<flux:input
|
||
wire:model="codePrefix"
|
||
maxlength="1"
|
||
readonly
|
||
class="w-16 text-center font-mono text-lg font-semibold"
|
||
/>
|
||
<span class="text-zinc-400">|</span>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="codePart1"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="codePart2"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="codePart3"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
<flux:input
|
||
wire:model.live.debounce.500ms="codePart4"
|
||
maxlength="2"
|
||
placeholder="00"
|
||
class="w-16 text-center font-mono"
|
||
/>
|
||
</div>
|
||
</div>
|
||
@error('codePrefix') <flux:error>{{ $message }}</flux:error> @enderror
|
||
@error('codePart1') <flux:error>{{ $message }}</flux:error> @enderror
|
||
@error('codePart2') <flux:error>{{ $message }}</flux:error> @enderror
|
||
@error('codePart3') <flux:error>{{ $message }}</flux:error> @enderror
|
||
@error('codePart4') <flux:error>{{ $message }}</flux:error> @enderror
|
||
<div class="flex items-center justify-between mt-1">
|
||
<p class="text-xs text-zinc-500">{{ __('Format: Prefix + 4×2 Ziffern') }}</p>
|
||
<p class="text-xs text-zinc-400">{{ __('Nächste:') }} {{ $nextAvailableNumber }}</p>
|
||
</div>
|
||
</flux:field>
|
||
@endif
|
||
|
||
@if(in_array($role, ['broker', 'retailer', 'manufacturer'], true))
|
||
<flux:field>
|
||
<flux:label>{{ __('Name') }} <span class="text-red-500">*</span></flux:label>
|
||
<flux:description>
|
||
@if($role === 'manufacturer')
|
||
{{ __('Name des Herstellers') }}
|
||
@else
|
||
{{ __('Name für die Zuordnung von Kunden') }}
|
||
@endif
|
||
</flux:description>
|
||
<flux:input
|
||
wire:model="name"
|
||
placeholder="{{ __('z.B. Max Mustermann') }}"
|
||
icon="user"
|
||
/>
|
||
@error('name') <flux:error>{{ $message }}</flux:error> @enderror
|
||
</flux:field>
|
||
@endif
|
||
|
||
@if($role === 'customer')
|
||
<flux:field>
|
||
<flux:label>{{ __('Makler oder Händler (Pflicht für Kundencodes)') }}</flux:label>
|
||
<flux:description>{{ __('Wählen Sie zwischen Makler oder Händler und ordnen Sie dann einen Code zu') }}</flux:description>
|
||
|
||
{{-- Tabs für Makler/Händler-Auswahl --}}
|
||
<div class="mb-4">
|
||
<div class="flex gap-2 border-b border-zinc-200 dark:border-zinc-700">
|
||
<button
|
||
type="button"
|
||
wire:click="$set('customerBrokerType', 'broker')"
|
||
class="px-4 py-2 text-sm font-medium transition-colors {{ $customerBrokerType === 'broker' ? 'border-b-2 border-primary-500 text-primary-600 dark:text-primary-400' : 'text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300' }}"
|
||
>
|
||
{{ __('Makler') }}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
wire:click="$set('customerBrokerType', 'retailer')"
|
||
class="px-4 py-2 text-sm font-medium transition-colors {{ $customerBrokerType === 'retailer' ? 'border-b-2 border-primary-500 text-primary-600 dark:text-primary-400' : 'text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300' }}"
|
||
>
|
||
{{ __('Händler') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Zuordnung --}}
|
||
<flux:field>
|
||
<flux:label>{{ __('Zuordnen zu') }}</flux:label>
|
||
<flux:select variant="listbox" searchable wire:model="brokerPartnerId" placeholder="{{ __('Bitte wählen') }}">
|
||
<flux:select.option :value="null">{{ __('Bitte wählen') }}</flux:select.option>
|
||
@foreach($brokerAndRetailerCodes as $code)
|
||
@php
|
||
$displayName = $code->name ?? $code->code;
|
||
$roleLabel = $roleOptions[$code->role]['label'] ?? ucfirst($code->role);
|
||
@endphp
|
||
<flux:select.option :value="$code->id">
|
||
{{ $displayName }} ({{ $code->code }})
|
||
</flux:select.option>
|
||
@endforeach
|
||
</flux:select>
|
||
@error('brokerPartnerId') <flux:error>{{ $message }}</flux:error> @enderror
|
||
</flux:field>
|
||
</flux:field>
|
||
@endif
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<flux:field>
|
||
<flux:label>{{ __('Ablaufdatum') }}</flux:label>
|
||
<flux:date-picker wire:model="expiresAt" locale="de" />
|
||
@error('expiresAt') <flux:error>{{ $message }}</flux:error> @enderror
|
||
</flux:field>
|
||
|
||
<flux:field>
|
||
<flux:label>{{ __('Notiz (optional)') }}</flux:label>
|
||
<flux:input wire:model.defer="note" placeholder="{{ __('z.B. Messe Hamburg 2025') }}" />
|
||
@error('note') <flux:error>{{ $message }}</flux:error> @enderror
|
||
</flux:field>
|
||
</div>
|
||
|
||
<flux:separator />
|
||
|
||
<x-error-alert />
|
||
|
||
<div class="flex justify-end">
|
||
<flux:button
|
||
type="submit"
|
||
variant="primary"
|
||
icon="key"
|
||
wire:loading.attr="disabled"
|
||
wire:target="createCode"
|
||
>
|
||
<span wire:loading.remove wire:target="createCode">
|
||
{{ __('Code speichern') }}
|
||
</span>
|
||
<span wire:loading wire:target="createCode">
|
||
<flux:icon.arrow-path class="animate-spin inline-block mr-2 h-4 w-4" />
|
||
{{ __('Speichern...') }}
|
||
</span>
|
||
</flux:button>
|
||
</div>
|
||
</form>
|
||
</flux:card>
|
||
|
||
{{-- Liste --}}
|
||
<flux:card class="shadow-elegant">
|
||
<div class="mb-4">
|
||
<flux:heading size="lg" class="mb-2">{{ __('Letzte Codes') }}</flux:heading>
|
||
<flux:subheading>{{ __('Zuletzt angelegte oder verbrauchte Codes') }}</flux:subheading>
|
||
</div>
|
||
|
||
<flux:separator class="mb-4" />
|
||
|
||
<div class="space-y-3 max-h-[600px] overflow-y-auto">
|
||
@forelse($recentCodes as $code)
|
||
@php
|
||
$statusColor = match($code->status) {
|
||
'available' => 'green',
|
||
'used' => 'zinc',
|
||
'expired' => 'orange',
|
||
default => 'zinc',
|
||
};
|
||
$roleLabel = $roleOptions[$code->role]['label'] ?? ucfirst($code->role);
|
||
@endphp
|
||
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div class="flex-1 space-y-1">
|
||
<div class="font-mono font-semibold text-lg text-zinc-900 dark:text-white">
|
||
{{ $code->code }}
|
||
</div>
|
||
@if($code->name)
|
||
<div class="text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
||
{{ __('Name:') }} {{ $code->name }}
|
||
</div>
|
||
@endif
|
||
@if($code->role === 'customer' && $code->assignedToCode)
|
||
@php
|
||
$brokerName = $code->assignedToCode->name ?? $code->assignedToCode->code;
|
||
$brokerRoleLabel = $roleOptions[$code->assignedToCode->role]['label'] ?? ucfirst($code->assignedToCode->role);
|
||
@endphp
|
||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||
{{ __('Zugeordnet zu:') }} {{ $brokerRoleLabel }} {{ $brokerName }} ({{ $code->assignedToCode->code }})
|
||
</div>
|
||
@endif
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<flux:badge size="sm" color="{{ $statusColor }}">
|
||
@if($code->status === 'available')
|
||
{{ __('Verfügbar') }}
|
||
@elseif($code->status === 'used')
|
||
{{ __('Verbraucht') }}
|
||
@else
|
||
{{ __('Abgelaufen') }}
|
||
@endif
|
||
</flux:badge>
|
||
<flux:badge size="sm" color="zinc">
|
||
{{ $roleLabel }}
|
||
</flux:badge>
|
||
</div>
|
||
@if($code->usedBy)
|
||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||
{{ __('Verbraucht von:') }} {{ $code->usedBy->name ?? $code->usedBy->email }}
|
||
@if($code->used_at)
|
||
<span class="text-xs">({{ $code->used_at->format('d.m.Y H:i') }})</span>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
@if($code->metadata && isset($code->metadata['note']))
|
||
<div class="text-xs text-zinc-400 italic">
|
||
{{ $code->metadata['note'] }}
|
||
</div>
|
||
@endif
|
||
<div class="text-xs text-zinc-400">
|
||
{{ __('Angelegt:') }} {{ $code->created_at->format('d.m.Y H:i') }}
|
||
@if($code->expires_at)
|
||
<br>{{ __('Gültig bis:') }} {{ $code->expires_at->format('d.m.Y H:i') }}
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@if($code->status === RegistrationCode::STATUS_AVAILABLE)
|
||
<flux:button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
icon="x-circle"
|
||
wire:click="expire({{ $code->id }})"
|
||
wire:confirm="{{ __('Möchten Sie diesen Code wirklich deaktivieren?') }}"
|
||
>
|
||
{{ __('Deaktivieren') }}
|
||
</flux:button>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@empty
|
||
<div class="py-8 text-center text-zinc-500">
|
||
<flux:icon.key class="mx-auto h-12 w-12 text-zinc-400" />
|
||
<div class="mt-2">{{ __('Noch keine Codes angelegt') }}</div>
|
||
</div>
|
||
@endforelse
|
||
</div>
|
||
</flux:card>
|
||
</div>
|
||
|
||
{{-- Codes Tabelle --}}
|
||
<flux:card class="shadow-elegant">
|
||
<div class="mb-6">
|
||
<flux:heading size="lg" class="mb-2">{{ __('Alle Registrierungscodes') }}</flux:heading>
|
||
<flux:subheading>{{ __('Übersicht aller Codes mit Filtern und Sortierung') }}</flux:subheading>
|
||
</div>
|
||
|
||
{{-- Filter --}}
|
||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-{{ $tableRoleFilter === 'customer' ? '5' : '4' }}">
|
||
<flux:input
|
||
wire:model.live.debounce.300ms="search"
|
||
icon="magnifying-glass"
|
||
placeholder="{{ __('Code oder Name suchen...') }}"
|
||
/>
|
||
|
||
<flux:select wire:model.live="tableRoleFilter">
|
||
<flux:select.option value="">{{ __('Alle Rollen') }}</flux:select.option>
|
||
@foreach($roleOptions as $key => $meta)
|
||
<flux:select.option :value="$key">{{ $meta['label'] }}</flux:select.option>
|
||
@endforeach
|
||
</flux:select>
|
||
|
||
@if($tableRoleFilter === 'customer')
|
||
<flux:select variant="listbox" searchable wire:model.live="tableAssignmentFilter" placeholder="{{ __('Alle Zuordnungen') }}">
|
||
<flux:select.option value="">{{ __('Alle Zuordnungen') }}</flux:select.option>
|
||
@foreach($availableAssignments as $assignment)
|
||
@php
|
||
$assignmentName = $assignment->name ?? $assignment->code;
|
||
$assignmentRoleLabel = $roleOptions[$assignment->role]['label'] ?? ucfirst($assignment->role);
|
||
@endphp
|
||
<flux:select.option :value="$assignment->id">
|
||
{{ $assignmentRoleLabel }}: {{ $assignmentName }} ({{ $assignment->code }})
|
||
</flux:select.option>
|
||
@endforeach
|
||
</flux:select>
|
||
@endif
|
||
|
||
<flux:select wire:model.live="tableStatusFilter">
|
||
<flux:select.option value="">{{ __('Alle Status') }}</flux:select.option>
|
||
<flux:select.option value="available">{{ __('Verfügbar') }}</flux:select.option>
|
||
<flux:select.option value="used">{{ __('Verbraucht') }}</flux:select.option>
|
||
<flux:select.option value="expired">{{ __('Abgelaufen') }}</flux:select.option>
|
||
</flux:select>
|
||
|
||
@if($search || $tableRoleFilter || $tableStatusFilter || $tableAssignmentFilter)
|
||
<flux:button wire:click="$set('search', ''); $set('tableRoleFilter', ''); $set('tableStatusFilter', ''); $set('tableAssignmentFilter', '')" variant="ghost" icon="x-mark">
|
||
{{ __('Filter zurücksetzen') }}
|
||
</flux:button>
|
||
@endif
|
||
</div>
|
||
|
||
<flux:table>
|
||
<flux:table.columns sticky>
|
||
<flux:table.column wire:click="sortBy('code')" class="cursor-pointer">
|
||
<div class="flex items-center gap-2">
|
||
{{ __('Code') }}
|
||
@if($sortField === 'code')
|
||
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
|
||
@endif
|
||
</div>
|
||
</flux:table.column>
|
||
<flux:table.column wire:click="sortBy('role')" class="cursor-pointer">
|
||
<div class="flex items-center gap-2">
|
||
{{ __('Rolle') }}
|
||
@if($sortField === 'role')
|
||
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
|
||
@endif
|
||
</div>
|
||
</flux:table.column>
|
||
<flux:table.column>{{ __('Name') }}</flux:table.column>
|
||
<flux:table.column wire:click="sortBy('status')" class="cursor-pointer">
|
||
<div class="flex items-center gap-2">
|
||
{{ __('Status') }}
|
||
@if($sortField === 'status')
|
||
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
|
||
@endif
|
||
</div>
|
||
</flux:table.column>
|
||
<flux:table.column>{{ __('Zuordnung') }}</flux:table.column>
|
||
<flux:table.column wire:click="sortBy('created_at')" class="cursor-pointer">
|
||
<div class="flex items-center gap-2">
|
||
{{ __('Angelegt') }}
|
||
@if($sortField === 'created_at')
|
||
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
|
||
@endif
|
||
</div>
|
||
</flux:table.column>
|
||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||
</flux:table.columns>
|
||
|
||
<flux:table.rows>
|
||
@forelse($codes as $code)
|
||
@php
|
||
$statusColor = match($code->status) {
|
||
'available' => 'green',
|
||
'used' => 'zinc',
|
||
'expired' => 'orange',
|
||
default => 'zinc',
|
||
};
|
||
$roleLabel = $roleOptions[$code->role]['label'] ?? ucfirst($code->role);
|
||
$name = $code->name;
|
||
@endphp
|
||
<flux:table.row :key="$code->id">
|
||
<flux:table.cell variant="strong">
|
||
<span class="font-mono">{{ $code->code }}</span>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<flux:badge size="sm" color="zinc">{{ $roleLabel }}</flux:badge>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
@if($name)
|
||
<span class="font-medium">{{ $name }}</span>
|
||
@else
|
||
<span class="text-zinc-400">—</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<flux:badge size="sm" color="{{ $statusColor }}">
|
||
@if($code->status === 'available')
|
||
{{ __('Verfügbar') }}
|
||
@elseif($code->status === 'used')
|
||
{{ __('Verbraucht') }}
|
||
@else
|
||
{{ __('Abgelaufen') }}
|
||
@endif
|
||
</flux:badge>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
@if($code->role === 'customer' && $code->assignedToCode)
|
||
@php
|
||
$brokerName = $code->assignedToCode->name ?? $code->assignedToCode->code;
|
||
$brokerRoleLabel = $roleOptions[$code->assignedToCode->role]['label'] ?? ucfirst($code->assignedToCode->role);
|
||
@endphp
|
||
<div class="text-sm">
|
||
<span class="text-zinc-500">{{ $brokerRoleLabel }}:</span>
|
||
<span class="font-medium">{{ $brokerName }}</span>
|
||
</div>
|
||
@elseif($code->usedBy)
|
||
<div class="text-sm">
|
||
<span class="text-zinc-500">{{ __('Verbraucht von:') }}</span>
|
||
<span class="font-medium">{{ $code->usedBy->name ?? $code->usedBy->email }}</span>
|
||
</div>
|
||
@else
|
||
<span class="text-zinc-400">—</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<div class="text-sm text-zinc-500">
|
||
{{ $code->created_at->format('d.m.Y H:i') }}
|
||
</div>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
@if($code->status === RegistrationCode::STATUS_AVAILABLE)
|
||
<flux:button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
icon="x-circle"
|
||
wire:click="expire({{ $code->id }})"
|
||
wire:confirm="{{ __('Möchten Sie diesen Code wirklich deaktivieren?') }}"
|
||
>
|
||
{{ __('Deaktivieren') }}
|
||
</flux:button>
|
||
@endif
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@empty
|
||
<flux:table.row>
|
||
<flux:table.cell colspan="8">
|
||
<div class="py-12 text-center">
|
||
<flux:icon.key class="mx-auto h-12 w-12 text-zinc-400" />
|
||
<flux:heading size="lg" class="mt-4">{{ __('Keine Codes gefunden') }}</flux:heading>
|
||
<flux:subheading class="mt-2">
|
||
@if($search || $tableRoleFilter || $tableStatusFilter)
|
||
{{ __('Versuchen Sie, Ihre Filter anzupassen.') }}
|
||
@else
|
||
{{ __('Erstellen Sie einen neuen Code, um zu beginnen.') }}
|
||
@endif
|
||
</flux:subheading>
|
||
</div>
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@endforelse
|
||
</flux:table.rows>
|
||
</flux:table>
|
||
|
||
{{-- Pagination --}}
|
||
<div class="mt-6">
|
||
{{ $codes->links() }}
|
||
</div>
|
||
</flux:card>
|
||
|
||
</div>
|