21-11-2025

This commit is contained in:
Kevin Adametz 2025-11-21 18:21:23 +01:00
parent fa2ebd457d
commit 07959c0ba2
113 changed files with 4730 additions and 898 deletions

View file

@ -0,0 +1,324 @@
<?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 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',
], [
'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.'),
]);
// 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()->addDays(7), // 7 Tage gültig
'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']);
// Setze default Rolle wieder
$firstRole = $this->getPartnerRoles()->first();
$this->roleId = $firstRole?->id;
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>{{ __('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>

View file

@ -0,0 +1,352 @@
<?php
use App\Models\User;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use function Livewire\Volt\{layout, title, state};
layout('components.layouts.app');
title('Users Management');
new class extends Component {
use WithPagination;
public string $search = '';
public string $roleFilter = '';
public string $sortField = 'name';
public string $sortDirection = 'asc';
// Modal state
public bool $showRoleModal = false;
public ?int $selectedUserId = null;
public array $selectedRoles = [];
public function with(): array
{
$query = User::with('roles')
->when($this->search, fn($q, $search) =>
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
)
->when($this->roleFilter, fn($q, $role) =>
$q->whereHas('roles', fn($roleQuery) =>
$roleQuery->where('name', $role)
)
);
return [
'users' => $query->orderBy($this->sortField, $this->sortDirection)->paginate(15),
'totalUsers' => User::count(),
'verifiedUsers' => User::whereNotNull('email_verified_at')->count(),
'availableRoles' => \Spatie\Permission\Models\Role::orderBy('name')->get(),
'selectedUser' => $this->selectedUserId ? User::find($this->selectedUserId) : null,
];
}
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 updatingRoleFilter(): void
{
$this->resetPage();
}
public function openRoleModal(int $userId): void
{
$user = User::with('roles')->findOrFail($userId);
$this->selectedUserId = $userId;
$this->selectedRoles = $user->roles->pluck('name')->toArray();
$this->showRoleModal = true;
}
public function saveRoles(): void
{
if (!$this->selectedUserId) {
return;
}
$user = User::findOrFail($this->selectedUserId);
$user->syncRoles($this->selectedRoles);
$this->showRoleModal = false;
$this->selectedUserId = null;
$this->selectedRoles = [];
// Optional: Flash message
session()->flash('message', __('Roles updated successfully!'));
}
public function closeRoleModal(): void
{
$this->showRoleModal = false;
$this->selectedUserId = null;
$this->selectedRoles = [];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="mb-2">{{ __('Users Management') }}</flux:heading>
<flux:subheading>{{ __('Manage users and their roles in your application') }}</flux:subheading>
</div>
<div class="flex gap-2">
<flux:button variant="primary" icon="plus">{{ __('Create User') }}</flux:button>
</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>{{ __('Total Users') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $totalUsers }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.users class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Verified Users') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $verifiedUsers }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.shield-check class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Active Roles') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $availableRoles->count() }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.user-group class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
</div>
{{-- Filters --}}
<flux:card class="shadow-elegant">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="{{ __('Search users...') }}" />
<flux:select wire:model.live="roleFilter" placeholder="{{ __('All Roles') }}">
<flux:select.option value="">{{ __('All Roles') }}</flux:select.option>
@foreach($availableRoles as $role)
<flux:select.option :value="$role->name">{{ $role->display_name ?? $role->name }}</flux:select.option>
@endforeach
</flux:select>
@if($search || $roleFilter)
<flux:button wire:click="$set('search', ''); $set('roleFilter', '')" variant="ghost" icon="x-mark">
{{ __('Clear Filters') }}
</flux:button>
@endif
</div>
</flux:card>
{{-- Users Table --}}
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column class="w-1/4" wire:click="sortBy('name')" class="cursor-pointer">
<div class="flex items-center gap-2">
{{ __('User') }}
@if($sortField === 'name')
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
@endif
</div>
</flux:table.column>
<flux:table.column wire:click="sortBy('email')" class="cursor-pointer">
<div class="flex items-center gap-2">
{{ __('Email') }}
@if($sortField === 'email')
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
@endif
</div>
</flux:table.column>
<flux:table.column>{{ __('Roles') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Status') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($users as $user)
<flux:table.row :key="$user->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
<flux:table.cell>
<div class="flex items-center gap-3 pl-2">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-100 text-accent-600 dark:bg-accent-900/20 dark:text-accent-400 font-semibold">
{{ $user->initials() }}
</div>
<div>
<div class="font-semibold text-zinc-900 dark:text-white">{{ $user->name }}</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ __('ID:') }} {{ $user->id }}
</div>
</div>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex items-center gap-2">
<flux:icon.envelope variant="micro" class="text-zinc-400" />
<span>{{ $user->email }}</span>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1.5">
<button
type="button"
wire:click="openRoleModal({{ $user->id }})"
class="flex flex-wrap gap-1.5 cursor-pointer hover:opacity-80 transition-opacity"
>
@forelse($user->roles as $role)
<flux:badge size="sm" :color="$role->color ?? 'zinc'">
@svg('heroicon-o-'.$role->icon, 'w-5 h-5') &nbsp; {{ $role->display_name ?? $role->name }}
</flux:badge>
@empty
<flux:badge size="sm" color="zinc" icon="plus">{{ __('Assign Role') }}</flux:badge>
@endforelse
</button>
</div>
</flux:table.cell>
<flux:table.cell>
@if($user->email_verified_at)
<flux:badge size="sm" color="green" icon="check-circle">
{{ __('Verified') }}
</flux:badge>
@else
<flux:badge size="sm" color="zinc" icon="exclamation-circle">
{{ __('Unverified') }}
</flux:badge>
@endif
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
<flux:button size="sm" variant="ghost" icon="pencil"
tooltip="{{ __('Edit User') }}"></flux:button>
<flux:button size="sm" variant="ghost" icon="trash"
tooltip="{{ __('Delete User') }}"></flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="5">
<div class="py-12 text-center">
<flux:icon.users variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
<flux:heading size="lg" class="mt-4">{{ __('No users found') }}</flux:heading>
<flux:subheading class="mt-2">
@if($search || $roleFilter)
{{ __('Try adjusting your filters.') }}
@else
{{ __('Get started by creating a new user.') }}
@endif
</flux:subheading>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
{{-- Pagination --}}
@if($users->hasPages())
<div class="border-t border-zinc-200 px-6 py-4 dark:border-zinc-700">
{{ $users->links() }}
</div>
@endif
</flux:card>
{{-- Role Assignment Modal --}}
<flux:modal name="role-modal" :variant="'flyout'" wire:model="showRoleModal">
<form wire:submit="saveRoles" class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Assign Roles') }}</flux:heading>
<flux:subheading>
@if($selectedUser)
{{ __('Managing roles for') }} <strong>{{ $selectedUser->name }}</strong>
@endif
</flux:subheading>
</div>
<flux:separator />
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Roles') }}</flux:label>
<flux:description>{{ __('Select one or multiple roles for this user') }}</flux:description>
<div class="space-y-2 mt-3">
@foreach($availableRoles as $role)
<flux:checkbox
wire:model="selectedRoles"
:value="$role->name"
:label="$role->display_name ?? $role->name"
/>
@endforeach
</div>
</flux:field>
@if(!empty($selectedRoles))
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
<flux:subheading class="mb-2">{{ __('Selected Roles:') }}</flux:subheading>
<div class="flex flex-wrap gap-2">
@foreach($selectedRoles as $roleName)
@php
$roleObj = $availableRoles->firstWhere('name', $roleName);
@endphp
<flux:badge size="sm" :color="$roleObj?->color ?? 'zinc'">
{{ $roleName }}
</flux:badge>
@endforeach
</div>
</div>
@endif
</div>
<flux:separator />
<div class="flex justify-between gap-2">
<flux:button type="button" variant="ghost" wire:click="closeRoleModal">
{{ __('Cancel') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Save Roles') }}
</flux:button>
</div>
</form>
</flux:modal>
{{-- Success Message --}}
@if (session()->has('message'))
<flux:toast :variant="'success'">
{{ session('message') }}
</flux:toast>
@endif
</div>

View file

@ -0,0 +1,478 @@
<?php
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Livewire\Volt\Component;
use function Livewire\Volt\{layout, title};
title('Permissions & Roles');
new class extends Component {
public $activeTab = 'roles';
// Modal state for editing roles
public bool $showEditModal = false;
public ?int $selectedRoleId = null;
public string $roleName = '';
public string $roleDisplayName = '';
public string $roleIcon = 'shield-check';
public string $roleColor = 'zinc';
public array $rolePermissions = [];
public function with(): array
{
return [
'roles' => Role::with('permissions')->orderBy('name')->get(),
'permissions' => Permission::with('roles')->orderBy('name')->get(),
'allPermissions' => Permission::orderBy('name')->get(),
'selectedRole' => $this->selectedRoleId ? Role::find($this->selectedRoleId) : null,
];
}
public function switchTab(string $tab): void
{
\Log::info('Switching tab to: ' . $tab);
$this->activeTab = $tab;
\Log::info('Active tab: ' . $this->activeTab);
}
public function openEditModal(int $roleId): void
{
$role = Role::with('permissions')->findOrFail($roleId);
$this->selectedRoleId = $roleId;
$this->roleName = $role->name;
$this->roleDisplayName = $role->display_name ?? $role->name;
$this->roleIcon = $role->icon ?? 'shield-check';
$this->roleColor = $role->color ?? 'zinc';
$this->rolePermissions = $role->permissions->pluck('name')->toArray();
$this->roleIcon = $role->icon ?? 'shield-check';
$this->showEditModal = true;
}
public function saveRole(): void
{
if (!$this->selectedRoleId) {
return;
}
$this->validate([
'roleName' => 'required|string|max:255',
'roleColor' => 'required|string|max:50',
]);
$role = Role::findOrFail($this->selectedRoleId);
$role->update([
'name' => $this->roleName,
'display_name' => $this->roleDisplayName,
'icon' => $this->roleIcon,
'color' => $this->roleColor,
]);
$role->syncPermissions($this->rolePermissions);
$this->showEditModal = false;
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
session()->flash('message', __('Role updated successfully!'));
}
public function closeEditModal(): void
{
$this->showEditModal = false;
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="mb-2">{{ __('Permissions & Roles Management') }}</flux:heading>
<flux:subheading>{{ __('Manage roles and permissions for your application') }}</flux:subheading>
</div>
<div class="flex gap-2">
<flux:button variant="primary" icon="plus">{{ __('Create Role') }}</flux:button>
<flux:button variant="ghost" icon="shield-check">{{ __('Create Permission') }}</flux:button>
</div>
</div>
{{-- Tabs --}}
<flux:tabs wire:model.live="activeTab">
<flux:tab name="roles" icon="user-group">{{ __('Roles Overview') }}</flux:tab>
<flux:tab name="permissions" icon="shield-check">{{ __('Permissions Overview') }}</flux:tab>
</flux:tabs>
{{-- Roles Tab Content --}}
@if($activeTab === 'roles')
<div class="space-y-6">
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column class="w-1/5">{{ __('Role') }}</flux:table.column>
<flux:table.column>{{ __('Permissions') }}</flux:table.column>
<flux:table.column class="w-24">{{ __('Count') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($roles as $role)
<flux:table.row :key="$role->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
<flux:table.cell>
<div class="flex items-center gap-3 pl-2">
@php
$colorClasses = match($role->color ?? 'zinc') {
'red' => 'bg-red-100 text-red-600 dark:bg-red-900/20 dark:text-red-400',
'accent' => 'bg-accent-100 text-accent-600 dark:bg-accent-900/20 dark:text-accent-400',
'blue' => 'bg-blue-100 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400',
'lime' => 'bg-lime-100 text-lime-600 dark:bg-lime-900/20 dark:text-lime-400',
'teal' => 'bg-teal-100 text-teal-600 dark:bg-teal-900/20 dark:text-teal-400',
'orange' => 'bg-orange-100 text-orange-600 dark:bg-orange-900/20 dark:text-orange-400',
'purple' => 'bg-purple-100 text-purple-600 dark:bg-purple-900/20 dark:text-purple-400',
'indigo' => 'bg-indigo-100 text-indigo-600 dark:bg-indigo-900/20 dark:text-indigo-400',
'pink' => 'bg-pink-100 text-pink-600 dark:bg-pink-900/20 dark:text-pink-400',
'green' => 'bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400',
'yellow' => 'bg-yellow-100 text-yellow-600 dark:bg-yellow-900/20 dark:text-yellow-400',
default => 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400',
};
@endphp
<div class="flex h-10 w-10 items-center justify-center rounded-lg {{ $colorClasses }}">
@if($role->icon)
@svg('heroicon-o-'.$role->icon, 'w-5 h-5')
@else
@svg('heroicon-o-shield-check', 'w-5 h-5')
@endif
</div>
<div>
<div class="font-semibold text-zinc-900 dark:text-white">
{{ $role->display_name ?? $role->name }}
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $role->name }} {{ __('Guard:') }} {{ $role->guard_name }}
</div>
</div>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1.5">
@if($role->name === 'Super-Admin')
<flux:badge size="sm" :color="$role->color ?? 'red'" icon="star">{{ __('All Permissions') }}</flux:badge>
@elseif($role->permissions->isEmpty())
<flux:badge size="sm" color="zinc">{{ __('No permissions') }}</flux:badge>
@else
@foreach($role->permissions->take(5) as $permission)
<flux:badge size="sm" color="zinc">{{ $permission->name }}</flux:badge>
@endforeach
@if($role->permissions->count() > 5)
<flux:badge size="sm" color="accent">
+{{ $role->permissions->count() - 5 }} {{ __('more') }}
</flux:badge>
@endif
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="text-center font-mono text-sm font-semibold text-zinc-900 dark:text-white">
@if($role->name === 'Super-Admin')
@else
{{ $role->permissions->count() }}
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
<flux:button size="sm" variant="ghost" icon="eye"
tooltip="{{ __('View Details') }}"></flux:button>
<flux:button size="sm" variant="ghost" icon="pencil"
wire:click="openEditModal({{ $role->id }})"
tooltip="{{ __('Edit Role') }}"></flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="4">
<div class="py-12 text-center">
<flux:icon.shield-exclamation variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
<flux:heading size="lg" class="mt-4">{{ __('No roles found') }}</flux:heading>
<flux:subheading class="mt-2">
{{ __('Get started by creating a new role.') }}
</flux:subheading>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
{{-- Role 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>{{ __('Total Roles') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $roles->count() }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.user-group class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Total Permissions') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $permissions->count() }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.shield-check class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Avg. Permissions/Role') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">
{{ $roles->count() > 0 ? number_format($roles->sum(fn($r) => $r->permissions->count()) / $roles->count(), 1) : 0 }}
</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.chart-bar class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
</div>
</div>
@endif
{{-- Permissions Tab Content --}}
@if($activeTab === 'permissions')
<div class="space-y-6">
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column class="w-1/4">{{ __('Permission') }}</flux:table.column>
<flux:table.column>{{ __('Assigned to Roles') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Role Count') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@php
$groupedPermissions = $permissions->groupBy(function($permission) {
return explode(' ', $permission->name)[1] ?? 'other';
});
@endphp
@forelse($groupedPermissions as $group => $groupPermissions)
{{-- Group Header --}}
<flux:table.row>
<flux:table.cell colspan="4" class="bg-zinc-50 dark:bg-zinc-800/50">
<div class="flex items-center gap-2 py-1 pl-2">
<flux:icon.folder class="h-5 w-5 text-zinc-500" />
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
{{ ucfirst($group) }} {{ __('Permissions') }} ({{ $groupPermissions->count() }})
</flux:heading>
</div>
</flux:table.cell>
</flux:table.row>
{{-- Group Permissions --}}
@foreach($groupPermissions as $permission)
<flux:table.row :key="$permission->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
<flux:table.cell>
<div class="flex items-center gap-3 pl-8">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.key variant="micro" class="text-accent-600 dark:text-accent-400" />
</div>
<div>
<div class="font-medium text-zinc-900 dark:text-white">{{ $permission->name }}</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ __('Guard:') }} {{ $permission->guard_name }}
</div>
</div>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1.5">
@if($permission->roles->isEmpty())
<flux:badge size="sm" color="zinc" icon="exclamation-triangle">
{{ __('Not assigned') }}
</flux:badge>
@else
@foreach($permission->roles as $role)
<flux:badge size="sm" :color="$role->color ?? 'zinc'">
{{ $role->name }}
</flux:badge>
@endforeach
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="text-center font-mono text-sm font-semibold text-zinc-900 dark:text-white">
{{ $permission->roles->count() }}
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
<flux:button size="sm" variant="ghost" icon="pencil"
tooltip="{{ __('Edit Permission') }}"></flux:button>
<flux:button size="sm" variant="ghost" icon="trash"
tooltip="{{ __('Delete Permission') }}"></flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@endforeach
@empty
<flux:table.row>
<flux:table.cell colspan="4">
<div class="py-12 text-center">
<flux:icon.shield-exclamation variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
<flux:heading size="lg" class="mt-4">{{ __('No permissions found') }}</flux:heading>
<flux:subheading class="mt-2">
{{ __('Get started by creating a new permission.') }}
</flux:subheading>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
</div>
@endif
{{-- Edit Role Modal --}}
<flux:modal name="edit-role-modal" :variant="'flyout'" wire:model="showEditModal">
<form wire:submit="saveRole" class="space-y-6 max-w-2xl">
<div>
<flux:heading size="lg">{{ __('Edit Role') }}</flux:heading>
<flux:subheading>
@if($selectedRole)
{{ __('Editing role') }}: <strong>{{ $selectedRole->name }}</strong>
@endif
</flux:subheading>
</div>
<flux:separator />
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Role Name') }}</flux:label>
<flux:input wire:model="roleName" placeholder="{{ __('Enter role name') }}" />
@error('roleName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Role Icon') }}</flux:label>
<flux:input wire:model="roleIcon" placeholder="{{ __('Enter role icon') }}" />
@error('roleIcon') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Role Display Name') }}</flux:label>
<flux:input wire:model="roleDisplayName" placeholder="{{ __('Enter role display name') }}" />
@error('roleDisplayName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Color') }}</flux:label>
<flux:description>{{ __('Select a color for this role') }}</flux:description>
<flux:select wire:model.live="roleColor">
<flux:select.option value="red">{{ __('Red') }}</flux:select.option>
<flux:select.option value="orange">{{ __('Orange') }}</flux:select.option>
<flux:select.option value="lime">{{ __('Lime') }}</flux:select.option>
<flux:select.option value="teal">{{ __('Teal') }}</flux:select.option>
<flux:select.option value="indigo">{{ __('Indigo') }}</flux:select.option>
<flux:select.option value="purple">{{ __('Purple') }}</flux:select.option>
<flux:select.option value="pink">{{ __('Pink') }}</flux:select.option>
<flux:select.option value="accent">{{ __('Accent (Cyan)') }}</flux:select.option>
<flux:select.option value="yellow">{{ __('Yellow') }}</flux:select.option>
<flux:select.option value="green">{{ __('Green') }}</flux:select.option>
<flux:select.option value="zinc">{{ __('Gray') }}</flux:select.option>
</flux:select>
@error('roleColor') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
@if($roleColor)
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
<flux:subheading class="mb-2">{{ __('Preview:') }}</flux:subheading>
<flux:badge size="md" :color="$roleColor">
{{ $roleName ?: __('Role Name') }}
</flux:badge>
</div>
@endif
<flux:field>
<flux:label>{{ __('Permissions') }}</flux:label>
<flux:description>{{ __('Select permissions for this role') }}</flux:description>
<div class="mt-3 max-h-96 space-y-2 overflow-y-auto rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
@php
$groupedPerms = $allPermissions->groupBy(function($permission) {
return explode(' ', $permission->name)[1] ?? 'other';
});
@endphp
@foreach($groupedPerms as $group => $perms)
<div class="mb-4">
<flux:subheading class="mb-2 uppercase text-xs">{{ ucfirst($group) }}</flux:subheading>
<div class="ml-2 space-y-2">
@foreach($perms as $permission)
<flux:checkbox
wire:model="rolePermissions"
:value="$permission->name"
:label="$permission->name"
/>
@endforeach
</div>
</div>
@endforeach
</div>
</flux:field>
@if(!empty($rolePermissions))
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
<flux:subheading class="mb-2">{{ __('Selected Permissions:') }} ({{ count($rolePermissions) }})</flux:subheading>
<div class="flex flex-wrap gap-2">
@foreach($rolePermissions as $permName)
<flux:badge size="sm" color="zinc">
{{ $permName }}
</flux:badge>
@endforeach
</div>
</div>
@endif
</div>
<flux:separator />
<div class="flex justify-between gap-2">
<flux:button type="button" variant="ghost" wire:click="closeEditModal">
{{ __('Cancel') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Save Role') }}
</flux:button>
</div>
</form>
</flux:modal>
{{-- Success Message --}}
@if (session()->has('message'))
<flux:toast :variant="'success'">
{{ session('message') }}
</flux:toast>
@endif
</div>