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(), ]; } }; ?>