342 lines
15 KiB
PHP
342 lines
15 KiB
PHP
<?php
|
|
|
|
use App\Models\PartnerInvitation;
|
|
use App\Mail\PartnerInvitationMail;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Livewire\Volt\Component;
|
|
use Spatie\Permission\Models\Role;
|
|
use function Livewire\Volt\{layout, title, state};
|
|
|
|
layout('components.layouts.app');
|
|
title('Partner einladen');
|
|
|
|
new class extends Component {
|
|
public string $companyName = '';
|
|
public string $contactFirstName = '';
|
|
public string $contactLastName = '';
|
|
public ?int $roleId = null;
|
|
public string $email = '';
|
|
public bool $showSuccessMessage = false;
|
|
public ?PartnerInvitation $lastInvitation = null;
|
|
public int $expiryWeeks = 1;
|
|
|
|
public function getPartnerRoles()
|
|
{
|
|
// Lade nur Rollen die eingeladen werden können
|
|
return Role::where('can_be_invited', true)
|
|
->orderBy('name')
|
|
->get();
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
// Setze default auf die erste einladbare Rolle
|
|
$firstRole = $this->getPartnerRoles()->first();
|
|
$this->roleId = $firstRole?->id;
|
|
}
|
|
|
|
public function sendInvitation(): void
|
|
{
|
|
$availableRoleIds = $this->getPartnerRoles()->pluck('id')->toArray();
|
|
|
|
$this->validate([
|
|
'companyName' => 'required|string|max:255',
|
|
'contactFirstName' => 'nullable|string|max:255',
|
|
'contactLastName' => 'nullable|string|max:255',
|
|
'roleId' => 'required|exists:roles,id|in:' . implode(',', $availableRoleIds),
|
|
'email' => 'required|email|max:255|unique:users,email',
|
|
'expiryWeeks' => 'required|integer|in:1,2,3,4',
|
|
], [
|
|
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
|
'companyName.max' => __('Der Firmenname darf maximal 255 Zeichen lang sein.'),
|
|
'contactFirstName.max' => __('Der Vorname darf maximal 255 Zeichen lang sein.'),
|
|
'contactLastName.max' => __('Der Nachname darf maximal 255 Zeichen lang sein.'),
|
|
'roleId.required' => __('Bitte wählen Sie einen Partner-Typ aus.'),
|
|
'roleId.exists' => __('Der gewählte Partner-Typ ist ungültig.'),
|
|
'email.required' => __('Bitte geben Sie eine E-Mail-Adresse ein.'),
|
|
'email.email' => __('Bitte geben Sie eine gültige E-Mail-Adresse ein.'),
|
|
'email.max' => __('Die E-Mail-Adresse darf maximal 255 Zeichen lang sein.'),
|
|
'email.unique' => __('Diese E-Mail-Adresse ist bereits als Benutzer registriert.'),
|
|
'expiryWeeks.required' => __('Bitte wählen Sie eine Gültigkeitsdauer aus.'),
|
|
'expiryWeeks.in' => __('Die Gültigkeitsdauer muss zwischen 1 und 4 Wochen liegen.'),
|
|
]);
|
|
|
|
// Prüfe ob bereits eine aktive Einladung existiert
|
|
$existingInvitation = PartnerInvitation::where('email', $this->email)
|
|
->pending()
|
|
->first();
|
|
|
|
if ($existingInvitation) {
|
|
$this->addError('email', __('Es existiert bereits eine aktive Einladung für diese E-Mail-Adresse.'));
|
|
return;
|
|
}
|
|
|
|
// Erstelle Einladung
|
|
$invitation = PartnerInvitation::create([
|
|
'company_name' => $this->companyName,
|
|
'contact_first_name' => $this->contactFirstName ?: null,
|
|
'contact_last_name' => $this->contactLastName ?: null,
|
|
'role_id' => $this->roleId,
|
|
'email' => $this->email,
|
|
'token' => PartnerInvitation::generateToken(),
|
|
'status' => 'pending',
|
|
'expires_at' => now()->addWeeks($this->expiryWeeks),
|
|
'invited_by' => auth()->id(),
|
|
]);
|
|
|
|
// Generiere Einladungs-URL
|
|
$invitationUrl = route('partner.invitation.accept', ['token' => $invitation->token]);
|
|
|
|
// Sende E-Mail
|
|
try {
|
|
Mail::to($this->email)->send(new PartnerInvitationMail($invitation, $invitationUrl));
|
|
|
|
$this->lastInvitation = $invitation;
|
|
$this->showSuccessMessage = true;
|
|
|
|
// Reset Form
|
|
$this->reset(['companyName', 'contactFirstName', 'contactLastName', 'roleId', 'email', 'expiryWeeks']);
|
|
|
|
// Setze default Rolle wieder
|
|
$firstRole = $this->getPartnerRoles()->first();
|
|
$this->roleId = $firstRole?->id;
|
|
$this->expiryWeeks = 1;
|
|
|
|
session()->flash('message', __('Einladung erfolgreich versendet!'));
|
|
} catch (\Exception $e) {
|
|
$this->addError('email', __('Fehler beim Versenden der E-Mail: ') . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'partnerRoles' => $this->getPartnerRoles(),
|
|
'recentInvitations' => PartnerInvitation::with(['invitedBy', 'role'])
|
|
->latest()
|
|
->take(10)
|
|
->get(),
|
|
'pendingCount' => PartnerInvitation::pending()->count(),
|
|
'acceptedCount' => PartnerInvitation::where('status', 'accepted')->count(),
|
|
'expiredCount' => PartnerInvitation::expired()->count(),
|
|
];
|
|
}
|
|
}; ?>
|
|
|
|
<div class="space-y-6 p-6">
|
|
{{-- Header --}}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<flux:heading size="xl" class="mb-2">{{ __('Partner einladen') }}</flux:heading>
|
|
<flux:subheading>{{ __('Laden Sie neue Partner zu Ihrer Plattform ein') }}</flux:subheading>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Statistics --}}
|
|
<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>{{ __('Ausstehend') }}</flux:subheading>
|
|
<flux:heading size="2xl" class="mt-2">{{ $pendingCount }}</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.clock class="h-6 w-6 text-orange-600 dark:text-orange-400" />
|
|
</div>
|
|
</div>
|
|
</flux:card>
|
|
|
|
<flux:card class="shadow-elegant">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<flux:subheading>{{ __('Akzeptiert') }}</flux:subheading>
|
|
<flux:heading size="2xl" class="mt-2">{{ $acceptedCount }}</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.check-circle 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>{{ __('Abgelaufen') }}</flux:subheading>
|
|
<flux:heading size="2xl" class="mt-2">{{ $expiredCount }}</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.x-circle class="h-6 w-6 text-zinc-600 dark:text-zinc-400" />
|
|
</div>
|
|
</div>
|
|
</flux:card>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
{{-- Invitation Form --}}
|
|
<flux:card class="shadow-elegant">
|
|
<form wire:submit="sendInvitation" class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg" class="mb-2">{{ __('Neue Einladung senden') }}</flux:heading>
|
|
<flux:subheading>{{ __('Füllen Sie die Felder aus, um einen neuen Partner einzuladen') }}</flux:subheading>
|
|
</div>
|
|
|
|
<flux:separator />
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Firmenname') }}</flux:label>
|
|
<flux:input
|
|
wire:model="companyName"
|
|
placeholder="{{ __('z.B. Möbelhaus Mustermann') }}"
|
|
icon="building-office"
|
|
/>
|
|
@error('companyName') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<flux:field>
|
|
<flux:label>{{ __('Vorname (optional)') }}</flux:label>
|
|
<flux:input
|
|
wire:model="contactFirstName"
|
|
placeholder="{{ __('z.B. Max') }}"
|
|
icon="user"
|
|
/>
|
|
@error('contactFirstName') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Nachname (optional)') }}</flux:label>
|
|
<flux:input
|
|
wire:model="contactLastName"
|
|
placeholder="{{ __('z.B. Mustermann') }}"
|
|
icon="user"
|
|
/>
|
|
@error('contactLastName') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
</div>
|
|
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Partner-Typ') }}</flux:label>
|
|
<flux:description>{{ __('Wählen Sie den Typ des Partners aus') }}</flux:description>
|
|
<flux:select wire:model="roleId">
|
|
@foreach($partnerRoles as $role)
|
|
<flux:select.option :value="$role->id">
|
|
@if($role->icon)
|
|
<flux:icon.{{ $role->icon }} class="mr-2" />
|
|
@endif
|
|
{{ $role->display_name ?? $role->name }}
|
|
</flux:select.option>
|
|
@endforeach
|
|
</flux:select>
|
|
@error('roleId') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Gültigkeit der Einladung') }}</flux:label>
|
|
<flux:description>{{ __('Wählen Sie zwischen 1 und 4 Wochen') }}</flux:description>
|
|
<flux:select wire:model="expiryWeeks">
|
|
@for($i = 1; $i <= 4; $i++)
|
|
<flux:select.option :value="$i">
|
|
{{ $i }} {{ $i === 1 ? __('Woche') : __('Wochen') }}
|
|
</flux:select.option>
|
|
@endfor
|
|
</flux:select>
|
|
@error('expiryWeeks') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('E-Mail Adresse') }}</flux:label>
|
|
<flux:description>{{ __('E-Mail des Ansprechpartners') }}</flux:description>
|
|
<flux:input
|
|
type="email"
|
|
wire:model="email"
|
|
placeholder="{{ __('z.B. einkauf@mustermann.de') }}"
|
|
icon="envelope"
|
|
/>
|
|
@error('email') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
<flux:separator />
|
|
|
|
{{-- Error Alert --}}
|
|
<x-error-alert />
|
|
|
|
<div class="flex justify-end">
|
|
<flux:button
|
|
type="submit"
|
|
variant="primary"
|
|
icon="paper-airplane"
|
|
wire:loading.attr="disabled"
|
|
wire:target="sendInvitation"
|
|
>
|
|
<span wire:loading.remove wire:target="sendInvitation">
|
|
{{ __('Einladung senden') }}
|
|
</span>
|
|
<span wire:loading wire:target="sendInvitation">
|
|
<flux:icon.arrow-path class="animate-spin inline-block mr-2 h-4 w-4" />
|
|
{{ __('Wird gesendet...') }}
|
|
</span>
|
|
</flux:button>
|
|
</div>
|
|
</form>
|
|
</flux:card>
|
|
|
|
{{-- Recent Invitations --}}
|
|
<flux:card class="shadow-elegant">
|
|
<div class="mb-4">
|
|
<flux:heading size="lg" class="mb-2">{{ __('Letzte Einladungen') }}</flux:heading>
|
|
<flux:subheading>{{ __('Übersicht der zuletzt versendeten Einladungen') }}</flux:subheading>
|
|
</div>
|
|
|
|
<flux:separator class="mb-4" />
|
|
|
|
<div class="space-y-3">
|
|
@forelse($recentInvitations as $invitation)
|
|
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="font-semibold text-zinc-900 dark:text-white">
|
|
{{ $invitation->company_name }}
|
|
@if($invitation->contact_full_name)
|
|
<span class="text-sm font-normal text-zinc-500">• {{ $invitation->contact_full_name }}</span>
|
|
@endif
|
|
</div>
|
|
<div class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
|
{{ $invitation->email }}
|
|
</div>
|
|
<div class="mt-2 flex items-center gap-2">
|
|
<flux:badge size="sm"
|
|
color="{{ $invitation->status === 'pending' ? 'orange' : ($invitation->status === 'accepted' ? 'green' : 'zinc') }}">
|
|
{{ ucfirst($invitation->status) }}
|
|
</flux:badge>
|
|
<flux:badge size="sm" color="{{ $invitation->role?->color ?? 'zinc' }}">
|
|
{{ $invitation->role?->display_name ?? $invitation->role?->name }}
|
|
</flux:badge>
|
|
</div>
|
|
<div class="mt-2 text-xs text-zinc-400">
|
|
{{ __('Eingeladen am:') }} {{ $invitation->created_at->format('d.m.Y H:i') }}
|
|
@if($invitation->status === 'pending')
|
|
<br>{{ __('Gültig bis:') }} {{ $invitation->expires_at->format('d.m.Y H:i') }}
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="py-8 text-center text-zinc-500">
|
|
<flux:icon.envelope class="mx-auto h-12 w-12 text-zinc-400" />
|
|
<div class="mt-2">{{ __('Noch keine Einladungen versendet') }}</div>
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
</flux:card>
|
|
</div>
|
|
|
|
{{-- Success Toast --}}
|
|
@if (session()->has('message'))
|
|
<flux:toast :variant="'success'">
|
|
{{ session('message') }}
|
|
</flux:toast>
|
|
@endif
|
|
</div>
|
|
|