21-11-2025
This commit is contained in:
parent
fa2ebd457d
commit
07959c0ba2
113 changed files with 4730 additions and 898 deletions
|
|
@ -5,7 +5,44 @@
|
|||
<livewire:notifications />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>Customer</flux:table.column>
|
||||
<flux:table.column>Date</flux:table.column>
|
||||
<flux:table.column>Status</flux:table.column>
|
||||
<flux:table.column>Amount</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
<flux:table.row>
|
||||
<flux:table.cell>Lindsey Aminoff</flux:table.cell>
|
||||
<flux:table.cell>Jul 29, 10:45 AM</flux:table.cell>
|
||||
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
|
||||
<flux:table.cell variant="strong">$49.00</flux:table.cell>
|
||||
</flux:table.row>
|
||||
|
||||
<flux:table.row>
|
||||
<flux:table.cell>Hanna Lubin</flux:table.cell>
|
||||
<flux:table.cell>Jul 28, 2:15 PM</flux:table.cell>
|
||||
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
|
||||
<flux:table.cell variant="strong">$312.00</flux:table.cell>
|
||||
</flux:table.row>
|
||||
|
||||
<flux:table.row>
|
||||
<flux:table.cell>Kianna Bushevi</flux:table.cell>
|
||||
<flux:table.cell>Jul 30, 4:05 PM</flux:table.cell>
|
||||
<flux:table.cell><flux:badge color="zinc" size="sm" inset="top bottom">Refunded</flux:badge></flux:table.cell>
|
||||
<flux:table.cell variant="strong">$132.00</flux:table.cell>
|
||||
</flux:table.row>
|
||||
|
||||
<flux:table.row>
|
||||
<flux:table.cell>Gustavo Geidt</flux:table.cell>
|
||||
<flux:table.cell>Jul 27, 9:30 AM</flux:table.cell>
|
||||
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
|
||||
<flux:table.cell variant="strong">$31.00</flux:table.cell>
|
||||
</flux:table.row>
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
|
|
@ -16,3 +53,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {{ $attributes }}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}" alt="B2IN Logo" class="h-10 w-auto dark:hidden" />
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}" alt="B2IN Logo" class="h-10 w-auto hidden dark:block" />
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 257 B |
|
|
@ -1,6 +1,5 @@
|
|||
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
|
||||
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
|
||||
</div>
|
||||
<div class="ms-1 grid flex-1 text-start text-sm">
|
||||
<span class="mb-0.5 truncate leading-none font-semibold">B2IN</span>
|
||||
<div class="flex size-16 items-center justify-center ml-2 ">
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}" alt="B2IN Logo" class="h-10 w-auto dark:hidden" />
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}" alt="B2IN Logo" class="h-10 w-auto hidden dark:block" />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
20
resources/views/components/error-alert.blade.php
Normal file
20
resources/views/components/error-alert.blade.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
@props(['title' => null])
|
||||
|
||||
@if ($errors->any())
|
||||
<div {{ $attributes->merge(['class' => 'rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4']) }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:icon.exclamation-circle class="h-5 w-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-red-800 dark:text-red-200 mb-2">
|
||||
{{ $title ?? __('Bitte korrigieren Sie folgende Fehler:') }}
|
||||
</h3>
|
||||
<ul class="text-sm text-red-700 dark:text-red-300 space-y-1 list-disc list-inside">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-800">
|
||||
<body class="min-h-screen bg-zinc-50 dark:bg-zinc-800 antialiased">
|
||||
<flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
|
||||
<a href="{{ config('domains.domain_main_url') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse">
|
||||
|
|
@ -11,21 +11,33 @@
|
|||
</a>
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.group :heading="__('Trader')" class="grid mb-4">
|
||||
|
||||
<flux:navlist.group :heading="__('Customer')" class="grid mb-4">
|
||||
<flux:navlist.item icon="user" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
<flux:navlist.group :heading="__('Retailer')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.group :heading="__('Customer')" class="grid mb-4">
|
||||
<flux:navlist.group :heading="__('Manufacturer')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.group :heading="__('Estate-Agent')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
|
||||
|
||||
<flux:navlist.group :heading="__('Admin')" class="grid mb-4">
|
||||
<flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Users') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="user-group" :href="route('admin.users.table')" :current="request()->routeIs('admin.users.table')" wire:navigate>{{ __('Users Table') }}</flux:navlist.item>
|
||||
<flux:navlist.group expandable expanded="false" heading="Favorites" class="hidden lg:grid">
|
||||
|
||||
<flux:navlist.group expandable expanded="false" heading="Users" class="hidden lg:grid">
|
||||
<flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Users') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="user-group" :href="route('admin.users.table')" :current="request()->routeIs('admin.users.table')" wire:navigate>{{ __('Users Table') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="shield-check" :href="route('admin.users.permissions')" :current="request()->routeIs('admin.users.permissions')" wire:navigate>{{ __('Permissions') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.item icon="envelope" :href="route('admin.partners.invite')" :current="request()->routeIs('admin.partners.invite')" wire:navigate>{{ __('Partner einladen') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.group :heading="__('CMS')" class="grid mb-4">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<x-layouts.auth.simple :title="$title ?? null">
|
||||
<x-layouts.auth.card :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts.auth.simple>
|
||||
</x-layouts.auth.card>
|
||||
|
|
|
|||
|
|
@ -2,25 +2,31 @@
|
|||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@include('partials.theme-init-script')
|
||||
</head>
|
||||
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<body class="min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-zinc-900 dark:via-zinc-900 dark:to-zinc-800 px-4 py-12">
|
||||
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div class="flex w-full max-w-md flex-col gap-6">
|
||||
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
<span class="flex h-20 w-20 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon />
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
|
||||
<flux:card class="shadow-2xl">
|
||||
<div class="px-10 py-8">{{ $slot }}</div>
|
||||
</flux:card>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<x-theme-toggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@livewireScripts
|
||||
@fluxScripts
|
||||
@include('partials.theme-toggle-script')
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,23 +2,30 @@
|
|||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@include('partials.theme-init-script')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<body class="min-h-screen bg-zinc-50 antialiased dark:bg-gradient-to-b dark:from-zinc-950 dark:to-zinc-900">
|
||||
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div class="flex w-full max-w-sm flex-col gap-2">
|
||||
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
|
||||
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
<span class="flex h-12 w-12 mb-1 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon />
|
||||
</span>
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
<div class="flex flex-col gap-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<x-theme-toggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@livewireScripts
|
||||
@fluxScripts
|
||||
@include('partials.theme-toggle-script')
|
||||
<script src="{{ asset('vendor/livewire/livewire.js') }}"></script>
|
||||
|
||||
<!-- Debug: Script-Status -->
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@include('partials.theme-init-script')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<body class="min-h-screen bg-zinc-50 antialiased dark:bg-gradient-to-b dark:from-zinc-950 dark:to-zinc-900">
|
||||
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
|
||||
<div class="absolute inset-0 bg-neutral-900"></div>
|
||||
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
|
||||
<x-app-logo-icon class="me-2 h-7" />
|
||||
</span>
|
||||
{{ config('app.name', 'Laravel') }}
|
||||
</a>
|
||||
|
|
@ -29,15 +30,20 @@
|
|||
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
<x-app-logo-icon />
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
{{ $slot }}
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<x-theme-toggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@fluxScripts
|
||||
@include('partials.theme-toggle-script')
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
25
resources/views/components/layouts/guest.blade.php
Normal file
25
resources/views/components/layouts/guest.blade.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@include('partials.theme-init-script')
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-zinc-900 dark:via-zinc-900 dark:to-zinc-800 px-4 py-12">
|
||||
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<x-theme-toggle />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@livewireScripts
|
||||
@fluxScripts
|
||||
@include('partials.theme-toggle-script')
|
||||
</body>
|
||||
|
||||
</html>
|
||||
15
resources/views/components/theme-toggle.blade.php
Normal file
15
resources/views/components/theme-toggle.blade.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<button
|
||||
id="theme-toggle"
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-colors"
|
||||
aria-label="Theme umschalten"
|
||||
>
|
||||
<svg id="theme-toggle-light-icon" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<svg id="theme-toggle-dark-icon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
</svg>
|
||||
<span id="theme-toggle-text">Hell</span>
|
||||
</button>
|
||||
|
||||
49
resources/views/components/wizard-progress.blade.php
Normal file
49
resources/views/components/wizard-progress.blade.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
@props(['currentStep', 'totalSteps', 'steps' => []])
|
||||
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-center">
|
||||
@foreach($steps as $index => $step)
|
||||
@php
|
||||
$stepNumber = $index + 1;
|
||||
$isActive = $stepNumber === $currentStep;
|
||||
$isCompleted = $stepNumber < $currentStep;
|
||||
@endphp
|
||||
|
||||
{{-- Step Circle --}}
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-full border-2
|
||||
{{ $isCompleted ? 'bg-accent-600 border-accent-600' : '' }}
|
||||
{{ $isActive ? 'bg-accent-600 border-accent-600' : '' }}
|
||||
{{ !$isActive && !$isCompleted ? 'bg-white dark:bg-zinc-800 border-zinc-300 dark:border-zinc-600' : '' }}">
|
||||
@if($isCompleted)
|
||||
<flux:icon.check class="h-5 w-5 text-white" />
|
||||
@else
|
||||
<span class="text-sm font-semibold
|
||||
{{ $isActive ? 'text-white' : 'text-zinc-500 dark:text-zinc-400' }}">
|
||||
{{ $stepNumber }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Step Label --}}
|
||||
<div class="mt-2 text-center">
|
||||
<p class="text-xs font-medium
|
||||
{{ $isActive ? 'text-accent-600 dark:text-accent-400' : 'text-zinc-500 dark:text-zinc-400' }}">
|
||||
{{ $step }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Connector Line --}}
|
||||
@if(!$loop->last)
|
||||
<div class="flex-1 h-0.5 mx-4
|
||||
{{ $stepNumber < $currentStep ? 'bg-accent-600' : 'bg-zinc-300 dark:bg-zinc-600' }}"
|
||||
style="min-width: 60px; max-width: 120px;">
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
47
resources/views/emails/partner-invitation.blade.php
Normal file
47
resources/views/emails/partner-invitation.blade.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<x-mail::message>
|
||||
# Willkommen bei B2In!
|
||||
|
||||
@if($contactFullName)
|
||||
Hallo {{ $contactFullName }},
|
||||
|
||||
Sie wurden eingeladen, Teil unserer Plattform zu werden.
|
||||
@else
|
||||
Sie wurden eingeladen, Teil unserer Plattform zu werden.
|
||||
@endif
|
||||
|
||||
## Firmeninformationen
|
||||
|
||||
**Firmenname:** {{ $companyName }}
|
||||
|
||||
**Partner-Typ:** {{ $partnerType }}
|
||||
|
||||
Wir freuen uns, Sie als **{{ $partnerType }}** in unserem Netzwerk begrüßen zu dürfen!
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
Klicken Sie auf den Button unten, um Ihre Registrierung abzuschließen. Sie können dann:
|
||||
|
||||
- Ihr Unternehmensprofil vervollständigen
|
||||
- Zugriff auf unser Partner-Portal erhalten
|
||||
- Mit anderen Partnern im Netzwerk interagieren
|
||||
|
||||
<x-mail::button :url="$invitationUrl" color="primary">
|
||||
Registrierung abschließen
|
||||
</x-mail::button>
|
||||
|
||||
<x-mail::panel>
|
||||
**Wichtig:** Diese Einladung ist gültig bis zum {{ $expiresAt->format('d.m.Y H:i') }} Uhr.
|
||||
</x-mail::panel>
|
||||
|
||||
Falls Sie Fragen haben, können Sie uns jederzeit kontaktieren.
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
{{ config('app.name') }} Team
|
||||
|
||||
---
|
||||
|
||||
<small>
|
||||
Falls der Button nicht funktioniert, kopieren Sie bitte den folgenden Link in Ihren Browser:
|
||||
{{ $invitationUrl }}
|
||||
</small>
|
||||
</x-mail::message>
|
||||
324
resources/views/livewire/admin/partners/invite.blade.php
Normal file
324
resources/views/livewire/admin/partners/invite.blade.php
Normal 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>
|
||||
|
||||
352
resources/views/livewire/admin/users.blade.php
Normal file
352
resources/views/livewire/admin/users.blade.php
Normal 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') {{ $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>
|
||||
478
resources/views/livewire/admin/users/permissions.blade.php
Normal file
478
resources/views/livewire/admin/users/permissions.blade.php
Normal 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>
|
||||
254
resources/views/livewire/partner/invitation-accept.blade.php
Normal file
254
resources/views/livewire/partner/invitation-accept.blade.php
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Partner;
|
||||
use App\Models\PartnerInvitation;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
new #[Layout('components.layouts.guest'), Title('Willkommen bei B2In')] class extends Component {
|
||||
public PartnerInvitation $invitation;
|
||||
|
||||
public string $firstName = '';
|
||||
public string $lastName = '';
|
||||
public string $email = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
public bool $acceptTerms = false;
|
||||
|
||||
public function mount(string $token): void
|
||||
{
|
||||
$this->invitation = PartnerInvitation::with('role')->where('token', $token)->firstOrFail();
|
||||
|
||||
// Prüfe ob Einladung abgelaufen ist
|
||||
if ($this->invitation->isExpired()) {
|
||||
$this->invitation->markAsExpired();
|
||||
$this->redirect('/partner/invitation/expired/' . $token);
|
||||
}
|
||||
|
||||
// Prüfe ob Einladung bereits verwendet wurde
|
||||
if ($this->invitation->status !== 'pending') {
|
||||
$this->redirect('/partner/invitation/used/' . $token);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$this->email = $this->invitation->email;
|
||||
$this->firstName = $this->invitation->contact_first_name ?? '';
|
||||
$this->lastName = $this->invitation->contact_last_name ?? '';
|
||||
}
|
||||
|
||||
public function createAccount(): void
|
||||
{
|
||||
$this->validate([
|
||||
'firstName' => 'required|string|max:255',
|
||||
'lastName' => 'required|string|max:255',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'acceptTerms' => 'accepted',
|
||||
], [
|
||||
'firstName.required' => __('Bitte geben Sie Ihren Vornamen ein.'),
|
||||
'firstName.max' => __('Der Vorname darf maximal 255 Zeichen lang sein.'),
|
||||
'lastName.required' => __('Bitte geben Sie Ihren Nachnamen ein.'),
|
||||
'lastName.max' => __('Der Nachname darf maximal 255 Zeichen lang sein.'),
|
||||
'email.required' => __('Bitte geben Sie Ihre E-Mail-Adresse ein.'),
|
||||
'email.email' => __('Bitte geben Sie eine gültige E-Mail-Adresse ein.'),
|
||||
'password.required' => __('Bitte geben Sie ein Passwort ein.'),
|
||||
'password.min' => __('Das Passwort muss mindestens 8 Zeichen lang sein.'),
|
||||
'password.confirmed' => __('Die Passwörter stimmen nicht überein.'),
|
||||
'acceptTerms.accepted' => __('Sie müssen die AGB und Datenschutzbestimmungen akzeptieren.'),
|
||||
]);
|
||||
|
||||
try {
|
||||
\DB::beginTransaction();
|
||||
|
||||
// 1. Erstelle Partner-Firma
|
||||
$partner = Partner::create([
|
||||
'company_name' => $this->invitation->company_name,
|
||||
'slug' => Str::slug($this->invitation->company_name),
|
||||
'type' => $this->invitation->role->name,
|
||||
'is_active' => false, // Wird später im Setup-Wizard aktiviert
|
||||
]);
|
||||
|
||||
// 2. Erstelle User
|
||||
$user = User::create([
|
||||
'partner_id' => $partner->id,
|
||||
'name' => $this->firstName . ' ' . $this->lastName,
|
||||
'email' => $this->email,
|
||||
'password' => Hash::make($this->password),
|
||||
'email_verified_at' => now(), // Auto-verifiziert durch Einladung
|
||||
]);
|
||||
|
||||
// 3. Weise Rolle zu
|
||||
$user->assignRole($this->invitation->role);
|
||||
|
||||
// 4. Markiere Einladung als akzeptiert
|
||||
$this->invitation->markAsAccepted($partner);
|
||||
|
||||
// 5. Logge User ein
|
||||
Auth::login($user);
|
||||
|
||||
\DB::commit();
|
||||
|
||||
// 6. Weiterleitung zum Setup-Wizard
|
||||
session()->flash('message', __('Willkommen bei B2In! Vervollständigen Sie nun Ihr Profil.'));
|
||||
$this->redirect(route('partner.setup.wizard'), navigate: true);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\DB::rollBack();
|
||||
$this->addError('email', __('Fehler beim Erstellen des Kontos: ') . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="w-full max-w-2xl">
|
||||
{{-- Header --}}
|
||||
<div class="text-center mb-8">
|
||||
@include('partials.logo-head')
|
||||
<h1 class="text-4xl font-bold text-zinc-900 dark:text-white mb-2">
|
||||
{{ __('Willkommen, :company!', ['company' => $invitation->company_name]) }}
|
||||
</h1>
|
||||
<p class="text-lg text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Erstellen Sie Ihr persönliches Konto, um Ihr Partner-Profil einzurichten.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- Card mit Formular --}}
|
||||
<flux:card class="shadow-2xl">
|
||||
<form wire:submit="createAccount" class="space-y-6">
|
||||
{{-- Partner Info Badge --}}
|
||||
<div class="flex items-center justify-center gap-3 p-4 bg-accent-50 dark:bg-accent-900/20 rounded-lg">
|
||||
<flux:icon.briefcase class="h-6 w-6 text-accent-600 dark:text-accent-400" />
|
||||
<div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Partner-Typ') }}</div>
|
||||
<div class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ $invitation->role?->display_name ?? $invitation->role?->name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Name Felder --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Ihr Vorname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input
|
||||
wire:model="firstName"
|
||||
placeholder="{{ __('z.B. Max') }}"
|
||||
icon="user"
|
||||
autofocus
|
||||
/>
|
||||
@error('firstName') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Ihr Nachname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input
|
||||
wire:model="lastName"
|
||||
placeholder="{{ __('z.B. Mustermann') }}"
|
||||
icon="user"
|
||||
/>
|
||||
@error('lastName') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
{{-- E-Mail (gesperrt) --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ __('E-Mail-Adresse') }}</flux:label>
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
type="email"
|
||||
icon="envelope"
|
||||
disabled
|
||||
/>
|
||||
<flux:description>{{ __('Diese E-Mail-Adresse wurde in der Einladung festgelegt und kann nicht geändert werden.') }}</flux:description>
|
||||
@error('email') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Passwort --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Passwort festlegen') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
type="password"
|
||||
placeholder="{{ __('Mindestens 8 Zeichen') }}"
|
||||
icon="lock-closed"
|
||||
/>
|
||||
@error('password') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Passwort bestätigen') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
type="password"
|
||||
placeholder="{{ __('Passwort wiederholen') }}"
|
||||
icon="lock-closed"
|
||||
/>
|
||||
@error('password_confirmation') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
{{-- AGB Checkbox --}}
|
||||
<flux:field>
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:checkbox wire:model="acceptTerms" />
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Ich akzeptiere die') }}
|
||||
<a href="#" class="text-accent-600 hover:text-accent-700 dark:text-accent-400">{{ __('AGB') }}</a>
|
||||
{{ __('und') }}
|
||||
<a href="#" class="text-accent-600 hover:text-accent-700 dark:text-accent-400">{{ __('Datenschutzbestimmungen') }}</a>.
|
||||
</div>
|
||||
</div>
|
||||
@error('acceptTerms') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Error Alert --}}
|
||||
<x-error-alert />
|
||||
|
||||
{{-- Submit Button --}}
|
||||
<div class="flex justify-end">
|
||||
<flux:button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
icon="arrow-right"
|
||||
class="w-full md:w-auto"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="createAccount"
|
||||
>
|
||||
<span wire:loading.remove wire:target="createAccount">
|
||||
{{ __('Konto erstellen & Setup starten') }}
|
||||
</span>
|
||||
<span wire:loading wire:target="createAccount">
|
||||
<flux:icon.arrow-path class="animate-spin inline-block mr-2 h-4 w-4" />
|
||||
{{ __('Wird erstellt...') }}
|
||||
</span>
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</flux:card>
|
||||
|
||||
{{-- Footer Hinweis --}}
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('Diese Einladung ist gültig bis zum') }}
|
||||
@if($invitation->expires_at)
|
||||
<strong>{{ $invitation->expires_at->format('d.m.Y H:i') }}</strong> {{ __('Uhr') }}.
|
||||
@else
|
||||
<strong>{{ __('unbegrenzt') }}</strong> {{ __('Uhr') }}.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
549
resources/views/livewire/partner/setup-wizard.blade.php
Normal file
549
resources/views/livewire/partner/setup-wizard.blade.php
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Partner;
|
||||
use App\Models\Brand;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends Component {
|
||||
use WithFileUploads;
|
||||
|
||||
public Partner $partner;
|
||||
public string $partnerType;
|
||||
public int $currentStep = 1;
|
||||
public int $totalSteps;
|
||||
public array $steps = [];
|
||||
|
||||
// Schritt 1: Stammdaten (alle Rollen)
|
||||
public string $companyName = '';
|
||||
public $logo = null;
|
||||
public string $description = '';
|
||||
public string $street = '';
|
||||
public string $zip = '';
|
||||
public string $city = '';
|
||||
public string $website = '';
|
||||
|
||||
// Schritt 2: Retailer - Liefergebiete
|
||||
public ?int $deliveryRadius = null;
|
||||
public ?int $assemblyRadius = null;
|
||||
|
||||
// Schritt 2: Manufacturer - Marke
|
||||
public string $brandName = '';
|
||||
public $brandLogo = null;
|
||||
public string $brandDescription = '';
|
||||
|
||||
public string $roleIcon = 'shield-check';
|
||||
public string $roleName = '-';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user->partner_id) {
|
||||
$this->redirect(route('dashboard'), navigate: true);
|
||||
return;
|
||||
}
|
||||
|
||||
$role = $user->roles->first();
|
||||
if ($role) {
|
||||
$this->roleIcon = $role->icon ?? 'shield-check';
|
||||
$this->roleName = $role->display_name ?? $role->name;
|
||||
}
|
||||
|
||||
$this->partner = Partner::with('users')->findOrFail($user->partner_id);
|
||||
$this->partnerType = $this->partner->type;
|
||||
|
||||
// Vorausfüllen
|
||||
$this->companyName = $this->partner->company_name;
|
||||
$this->description = $this->partner->description ?? '';
|
||||
$this->website = '';
|
||||
|
||||
// Definiere Schritte basierend auf Rolle
|
||||
$this->defineSteps();
|
||||
}
|
||||
|
||||
protected function defineSteps(): void
|
||||
{
|
||||
switch ($this->partnerType) {
|
||||
case 'Retailer':
|
||||
$this->steps = ['Stammdaten', 'Liefergebiete', 'Fertig'];
|
||||
$this->totalSteps = 3;
|
||||
break;
|
||||
case 'Manufacturer':
|
||||
$this->steps = ['Stammdaten', 'Marke anlegen', 'Fertig'];
|
||||
$this->totalSteps = 3;
|
||||
break;
|
||||
case 'Estate-Agent':
|
||||
$this->steps = ['Profil', 'Fertig'];
|
||||
$this->totalSteps = 2;
|
||||
break;
|
||||
default:
|
||||
$this->steps = ['Stammdaten', 'Fertig'];
|
||||
$this->totalSteps = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function saveStep1(): void
|
||||
{
|
||||
$this->validate(
|
||||
[
|
||||
'companyName' => 'required|string|max:255',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'street' => 'required|string|max:255',
|
||||
'zip' => 'required|string|max:10',
|
||||
'city' => 'required|string|max:255',
|
||||
'website' => 'nullable|url|max:255',
|
||||
'logo' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
||||
],
|
||||
[
|
||||
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
||||
'street.required' => __('Bitte geben Sie eine Straße ein.'),
|
||||
'zip.required' => __('Bitte geben Sie eine Postleitzahl ein.'),
|
||||
'city.required' => __('Bitte geben Sie eine Stadt ein.'),
|
||||
'website.url' => __('Bitte geben Sie eine gültige URL ein.'),
|
||||
'logo.image' => __('Das Logo muss eine Bilddatei sein.'),
|
||||
'logo.mimes' => __('Das Logo muss im Format JPG, PNG oder WebP sein.'),
|
||||
'logo.max' => __('Das Logo darf maximal 2 MB groß sein.'),
|
||||
],
|
||||
);
|
||||
|
||||
// Speichere Logo falls hochgeladen
|
||||
$logoPath = null;
|
||||
if ($this->logo) {
|
||||
$logoPath = $this->logo->store('partner-logos', 'public');
|
||||
}
|
||||
|
||||
// Update Partner
|
||||
$this->partner->update([
|
||||
'company_name' => $this->companyName,
|
||||
'description' => $this->description,
|
||||
'logo_url' => $logoPath ?? $this->partner->logo_url,
|
||||
]);
|
||||
|
||||
// TODO: Adresse speichern (separates Address-Model oder JSON-Feld)
|
||||
|
||||
$this->currentStep = 2;
|
||||
}
|
||||
|
||||
public function saveStep2Retailer(): void
|
||||
{
|
||||
$this->validate(
|
||||
[
|
||||
'deliveryRadius' => 'required|integer|min:1|max:500',
|
||||
'assemblyRadius' => 'required|integer|min:1|max:500',
|
||||
],
|
||||
[
|
||||
'deliveryRadius.required' => __('Bitte geben Sie einen Lieferradius ein.'),
|
||||
'deliveryRadius.min' => __('Der Lieferradius muss mindestens 1 km betragen.'),
|
||||
'assemblyRadius.required' => __('Bitte geben Sie einen Montageradius ein.'),
|
||||
'assemblyRadius.min' => __('Der Montageradius muss mindestens 1 km betragen.'),
|
||||
],
|
||||
);
|
||||
|
||||
$this->partner->update([
|
||||
'delivery_radius_km' => $this->deliveryRadius,
|
||||
'assembly_radius_km' => $this->assemblyRadius,
|
||||
]);
|
||||
|
||||
$this->completeSetup();
|
||||
}
|
||||
|
||||
public function saveStep2Manufacturer(): void
|
||||
{
|
||||
$this->validate(
|
||||
[
|
||||
'brandName' => 'required|string|max:255',
|
||||
'brandDescription' => 'nullable|string|max:1000',
|
||||
'brandLogo' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
||||
],
|
||||
[
|
||||
'brandName.required' => __('Bitte geben Sie einen Markennamen ein.'),
|
||||
'brandLogo.image' => __('Das Marken-Logo muss eine Bilddatei sein.'),
|
||||
'brandLogo.mimes' => __('Das Marken-Logo muss im Format JPG, PNG oder WebP sein.'),
|
||||
'brandLogo.max' => __('Das Marken-Logo darf maximal 2 MB groß sein.'),
|
||||
],
|
||||
);
|
||||
|
||||
// Speichere Brand-Logo falls hochgeladen
|
||||
$brandLogoPath = null;
|
||||
if ($this->brandLogo) {
|
||||
$brandLogoPath = $this->brandLogo->store('brand-logos', 'public');
|
||||
}
|
||||
|
||||
// Erstelle Brand
|
||||
Brand::create([
|
||||
'partner_id' => $this->partner->id,
|
||||
'name' => $this->brandName,
|
||||
'slug' => Str::slug($this->brandName),
|
||||
'description' => $this->brandDescription,
|
||||
'logo_url' => $brandLogoPath,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->completeSetup();
|
||||
}
|
||||
|
||||
protected function completeSetup(): void
|
||||
{
|
||||
$this->partner->update([
|
||||
'is_active' => true,
|
||||
'setup_completed' => true,
|
||||
'setup_completed_at' => now(),
|
||||
]);
|
||||
|
||||
$this->currentStep = $this->totalSteps;
|
||||
}
|
||||
|
||||
public function goToDashboard(): void
|
||||
{
|
||||
$this->redirect(route('dashboard'), navigate: true);
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
request()->session()->invalidate();
|
||||
request()->session()->regenerateToken();
|
||||
|
||||
$this->redirect(route('home'), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="w-full max-w-3xl">
|
||||
{{-- Header --}}
|
||||
<div class="text-center mb-8">
|
||||
@include('partials.logo-head')
|
||||
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white mb-2">
|
||||
{{ __('Vervollständigen Sie Ihr Profil') }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{{-- Progress Indicator --}}
|
||||
<x-wizard-progress :currentStep="$currentStep" :totalSteps="$totalSteps" :steps="$steps" />
|
||||
|
||||
{{-- Wizard Content --}}
|
||||
<flux:card class="shadow-2xl">
|
||||
{{-- Step 1: Stammdaten (für alle Rollen) --}}
|
||||
@if ($currentStep === 1)
|
||||
<form wire:submit="saveStep1" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
@svg('heroicon-o-'.$roleIcon, 'w-5 h-5')
|
||||
|
||||
@if ($partnerType === 'Retailer')
|
||||
{{ __('Ihre Stammdaten') }}
|
||||
@elseif($partnerType === 'Manufacturer')
|
||||
{{ __('Ihre Stammdaten') }}
|
||||
@else
|
||||
{{ __('Ihr Profil') }}
|
||||
@endif
|
||||
/ {{ $roleName }}
|
||||
</div>
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
{{ __('Diese Informationen helfen uns, Ihr Profil zu vervollständigen.') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="companyName" icon="building-office" />
|
||||
@error('companyName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Logo (optional)') }}</flux:label>
|
||||
<flux:description>{{ __('Laden Sie Ihr Firmenlogo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
|
||||
|
||||
<div class="space-y-2">
|
||||
<input type="file" wire:model.live="logo" accept="image/jpeg,image/png,image/jpg,image/webp"
|
||||
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
|
||||
|
||||
@error('logo')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
|
||||
<div wire:loading wire:target="logo" class="text-sm text-accent-600 dark:text-accent-400">
|
||||
<flux:icon.arrow-path class="inline-block h-4 w-4 animate-spin mr-2" />
|
||||
{{ __('Wird hochgeladen...') }}
|
||||
</div>
|
||||
|
||||
@if ($logo)
|
||||
<div wire:loading.remove wire:target="logo">
|
||||
@try
|
||||
<div class="p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="{{ $logo->temporaryUrl() }}" class="h-16 w-16 object-contain rounded border"
|
||||
alt="Logo Preview">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Logo erfolgreich hochgeladen') }}</p>
|
||||
<p class="text-xs text-green-600 dark:text-green-400">{{ $logo->getClientOriginalName() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@catch(\Exception $e)
|
||||
<div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p class="text-sm text-red-800 dark:text-red-200">{{ __('Fehler beim Laden der Vorschau') }}</p>
|
||||
</div>
|
||||
@endtry
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="description" rows="4"
|
||||
placeholder="{{ __('Ein kurzer Text über Ihr Unternehmen...') }}" />
|
||||
@error('description')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field class="md:col-span-2">
|
||||
<flux:label>{{ __('Straße') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="street" wire:/>
|
||||
@error('street')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('PLZ') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="zip" />
|
||||
@error('zip')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Stadt') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="city" icon="map-pin" />
|
||||
@error('city')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Website (optional)') }}</flux:label>
|
||||
<flux:input wire:model="website" type="url" icon="globe-alt" placeholder="https://..." />
|
||||
@error('website')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-between">
|
||||
|
||||
<flux:button type="button" variant="outline" icon="arrow-left-start-on-rectangle"
|
||||
wire:click="logout">
|
||||
{{ __('Logout') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary" icon="arrow-right">
|
||||
@if ($partnerType === 'Retailer')
|
||||
{{ __('Weiter zu Liefergebiete') }}
|
||||
@elseif($partnerType === 'Manufacturer')
|
||||
{{ __('Weiter zu Marke') }}
|
||||
@else
|
||||
{{ __('Setup abschließen') }}
|
||||
@endif
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
{{-- Step 2: Retailer - Liefergebiete --}}
|
||||
@if ($currentStep === 2 && $partnerType === 'Retailer')
|
||||
<form wire:submit="saveStep2Retailer" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
🚚 {{ __('Ihre Liefergebiete') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
{{ __('Wie weit liefern und montieren Sie von Ihrer Adresse (:zip :city) aus?', ['zip' => $zip, 'city' => $city]) }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Lieferradius') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich liefere im Umkreis von ... km') }}</flux:description>
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:input wire:model="deliveryRadius" type="number" min="1" max="500"
|
||||
class="flex-1" />
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
|
||||
</div>
|
||||
@error('deliveryRadius')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Montageradius') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich montiere im Umkreis von ... km') }}</flux:description>
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:input wire:model="assemblyRadius" type="number" min="1" max="500"
|
||||
class="flex-1" />
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
|
||||
</div>
|
||||
@error('assemblyRadius')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="check">
|
||||
{{ __('Setup abschließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
{{-- Step 2: Manufacturer - Marke anlegen --}}
|
||||
@if ($currentStep === 2 && $partnerType === 'Manufacturer')
|
||||
<form wire:submit="saveStep2Manufacturer" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
™️ {{ __('Ihre Marke') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. (Sie können später weitere Marken hinzufügen)') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Markenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="brandName" icon="tag"
|
||||
placeholder="{{ __('z.B. Möbelwerke Premium') }}" />
|
||||
@error('brandName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Marken-Logo (optional)') }}</flux:label>
|
||||
<flux:description>{{ __('Laden Sie Ihr Marken-Logo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
|
||||
|
||||
<div class="space-y-2">
|
||||
<input type="file" wire:model.live="brandLogo" accept="image/jpeg,image/png,image/jpg,image/webp"
|
||||
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
|
||||
|
||||
@error('brandLogo')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
|
||||
<div wire:loading wire:target="brandLogo" class="text-sm text-accent-600 dark:text-accent-400">
|
||||
<flux:icon.arrow-path class="inline-block h-4 w-4 animate-spin mr-2" />
|
||||
{{ __('Wird hochgeladen...') }}
|
||||
</div>
|
||||
|
||||
@if ($brandLogo)
|
||||
<div wire:loading.remove wire:target="brandLogo">
|
||||
@try
|
||||
<div class="p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="{{ $brandLogo->temporaryUrl() }}"
|
||||
class="h-16 w-16 object-contain rounded border" alt="Brand Logo Preview">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Logo erfolgreich hochgeladen') }}</p>
|
||||
<p class="text-xs text-green-600 dark:text-green-400">{{ $brandLogo->getClientOriginalName() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@catch(\Exception $e)
|
||||
<div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p class="text-sm text-red-800 dark:text-red-200">{{ __('Fehler beim Laden der Vorschau') }}</p>
|
||||
</div>
|
||||
@endtry
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Marken-Beschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="brandDescription" rows="4"
|
||||
placeholder="{{ __('Ein kurzer Text über Ihre Marke...') }}" />
|
||||
@error('brandDescription')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="check">
|
||||
{{ __('Setup abschließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
{{-- Final Step: Fertig! --}}
|
||||
@if ($currentStep === $totalSteps)
|
||||
<div class="text-center space-y-6 py-8">
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="flex items-center justify-center w-20 h-20 rounded-full bg-green-100 dark:bg-green-900/20">
|
||||
<flux:icon.check-circle class="h-12 w-12 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:heading size="xl" class="mb-2">
|
||||
✅ {{ __('Einrichtung abgeschlossen!') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
@if ($partnerType === 'Retailer')
|
||||
{{ __('Sie sind nun ein aktiver Händler. Sie können jetzt Ihr Sortiment pflegen.') }}
|
||||
@elseif($partnerType === 'Manufacturer')
|
||||
{{ __('Sie sind nun ein aktiver Hersteller. Sie können jetzt Ihre Produkte anlegen.') }}
|
||||
@else
|
||||
{{ __('Ihr Profil ist aktiv. In Ihrem Dashboard finden Sie Ihre persönlichen Einladungslinks.') }}
|
||||
@endif
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
@if ($partnerType !== 'Estate-Agent')
|
||||
<flux:button variant="primary" icon="plus" size="lg">
|
||||
{{ __('Erstes Produkt anlegen') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
<flux:button wire:click="goToDashboard" variant="outline" icon="home" size="lg">
|
||||
{{ __('Zum Dashboard') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
</div>
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination; // Wichtig für Paginierung
|
||||
|
||||
|
||||
new class extends Component {
|
||||
use WithPagination;
|
||||
|
||||
// Optional: Such- und Filter-Properties
|
||||
public string $search = '';
|
||||
public string $statusFilter = '';
|
||||
public string $roleFilter = '';
|
||||
|
||||
// Optional: Sortierung
|
||||
public string $sortField = 'name';
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Standardwerte für Sortierung setzen
|
||||
$this->sortField = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
public function sortBy(string $field): void
|
||||
{
|
||||
if ($this->sortField === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->sortField = $field;
|
||||
}
|
||||
|
||||
// Die Hauptmethode, um Daten zu laden
|
||||
public function users()
|
||||
{
|
||||
return User::query()
|
||||
->when($this->search, fn($query, $search) =>
|
||||
$query->where('name', 'like', '%' . $search . '%')
|
||||
->orWhere('email', 'like', '%' . $search . '%')
|
||||
)
|
||||
->when($this->statusFilter, fn($query, $status) =>
|
||||
$query->where('status', $status)
|
||||
)
|
||||
->when($this->roleFilter, fn($query, $role) =>
|
||||
$query->where('role', $role) // oder 'role_id' wenn du IDs verwendest
|
||||
)
|
||||
->orderBy($this->sortField, $this->sortDirection)
|
||||
->paginate(10); // 10 Einträge pro Seite
|
||||
}
|
||||
|
||||
// Optional: Lifecycle hook für das Zurücksetzen der Paginierung bei Suche/Filterung
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
public function updatedStatusFilter() { $this->resetPage(); }
|
||||
public function updatedRoleFilter() { $this->resetPage(); }
|
||||
|
||||
// Wird für die Paginierung mit Tailwind benötigt (Standard in Livewire 3)
|
||||
// Wenn FluxUI Bootstrap-basierte Paginierung braucht, musst du das anpassen
|
||||
// public function paginationView()
|
||||
// {
|
||||
// return 'vendor.livewire.tailwind'; // oder 'livewire::bootstrap'
|
||||
// }
|
||||
|
||||
// Wenn du mit Relationen arbeitest (z.B. user->role->name)
|
||||
// public function with(): array
|
||||
// {
|
||||
// return [
|
||||
// 'users' => User::with(['role', 'group']) // Eager loading
|
||||
// // ... deine Query-Logik von oben ...
|
||||
// ->paginate(10),
|
||||
// ];
|
||||
// }
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
{{-- Filter und Suchleiste (Beispiel, FluxUI Klassen anpassen) --}}
|
||||
<div class="mb-4 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-100 rounded">
|
||||
<div>
|
||||
<label for="search" class="block text-sm font-medium text-gray-700">Suche</label>
|
||||
<input wire:model.live.debounce.300ms="search" id="search" type="text" placeholder="Name oder E-Mail..."
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
</div>
|
||||
<div>
|
||||
<label for="statusFilter" class="block text-sm font-medium text-gray-700">Status</label>
|
||||
<select wire:model.live="statusFilter" id="statusFilter"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<option value="">Alle</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
<option value="pending">Ausstehend</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="roleFilter" class="block text-sm font-medium text-gray-700">Rolle</label>
|
||||
<input wire:model.live.debounce.300ms="roleFilter" id="roleFilter" type="text" placeholder="Rolle..."
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tabelle (FluxUI Klassen anpassen!) --}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 shadow">
|
||||
<thead class="bg-gray-50"> {{-- FluxUI Klasse für Thead --}}
|
||||
<tr>
|
||||
<th scope="col" wire:click="sortBy('name')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
|
||||
Name @if($sortField === 'name')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
|
||||
</th>
|
||||
<th scope="col" wire:click="sortBy('email')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
|
||||
E-Mail @if($sortField === 'email')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
|
||||
</th>
|
||||
<th scope="col" wire:click="sortBy('status')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
|
||||
Status @if($sortField === 'status')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
|
||||
</th>
|
||||
<th scope="col" wire:click="sortBy('role')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
|
||||
Rolle @if($sortField === 'role')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Gruppe
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">
|
||||
Rechte
|
||||
</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">Aktionen</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200"> {{-- FluxUI Klasse für Tbody --}}
|
||||
@forelse ($this->users() as $user)
|
||||
<tr wire:key="{{ $user->id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ $user->name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $user->email }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
{{ $user->status === 'active' ? 'bg-green-100 text-green-800' : ($user->status === 'inactive' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800') }}">
|
||||
{{ ucfirst($user->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->role }} {{-- Oder $user->role->name, wenn es eine Relation ist --}}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->group }} {{-- Oder $user->group->name --}}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{-- Darstellung der Rechte. Wenn es eine Many-to-Many Relation ist: --}}
|
||||
{{-- @foreach($user->permissions as $permission)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
{{ $permission->name }}
|
||||
</span>
|
||||
@endforeach --}}
|
||||
{{-- Oder wenn es ein JSON-Feld ist, musst du es parsen und anzeigen --}}
|
||||
Einfache Rechte-Anzeige (Todo)
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="#" class="text-indigo-600 hover:text-indigo-900">Bearbeiten</a> {{-- FluxUI Button-Klassen --}}
|
||||
<button wire:click="$dispatch('openModal', { component: 'admin.delete-user-modal', arguments: { userId: {{ $user->id }} }})"
|
||||
class="text-red-600 hover:text-red-900 ml-2">Löschen</button> {{-- FluxUI Button-Klassen --}}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">
|
||||
Keine Benutzer gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $this->users()->links() }} {{-- Stellt sicher, dass die Paginierungs-Views für dein UI-Kit konfiguriert sind --}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination; // Wichtig für Paginierung
|
||||
|
||||
|
||||
new class extends Component {
|
||||
use WithPagination;
|
||||
|
||||
// Optional: Such- und Filter-Properties
|
||||
public string $search = '';
|
||||
public string $statusFilter = '';
|
||||
public string $roleFilter = '';
|
||||
|
||||
// Optional: Sortierung
|
||||
public $sortBy = 'name';
|
||||
public $sortDirection = 'asc';
|
||||
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Standardwerte für Sortierung setzen
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
|
||||
public function sort($column) {
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*public function orders()
|
||||
{
|
||||
return \App\Models\Order::query()
|
||||
->tap(fn ($query) => $this->sortBy ? $query->orderBy($this->sortBy, $this->sortDirection) : $query)
|
||||
->paginate(5);
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
// Die Hauptmethode, um Daten zu laden
|
||||
#[\Livewire\Attributes\Computed]
|
||||
public function users()
|
||||
{
|
||||
return User::query()
|
||||
->when($this->search, fn($query, $search) =>
|
||||
$query->where('name', 'like', '%' . $search . '%')
|
||||
->orWhere('email', 'like', '%' . $search . '%')
|
||||
)
|
||||
->when($this->statusFilter, fn($query, $status) =>
|
||||
$query->where('status', $status)
|
||||
)
|
||||
->when($this->roleFilter, fn($query, $role) =>
|
||||
$query->where('role', $role) // oder 'role_id' wenn du IDs verwendest
|
||||
)
|
||||
->orderBy($this->sortBy, $this->sortDirection)
|
||||
->paginate(10); // 10 Einträge pro Seite
|
||||
}
|
||||
|
||||
// Optional: Lifecycle hook für das Zurücksetzen der Paginierung bei Suche/Filterung
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
public function updatedStatusFilter() { $this->resetPage(); }
|
||||
public function updatedRoleFilter() { $this->resetPage(); }
|
||||
|
||||
// Wird für die Paginierung mit Tailwind benötigt (Standard in Livewire 3)
|
||||
// Wenn FluxUI Bootstrap-basierte Paginierung braucht, musst du das anpassen
|
||||
// public function paginationView()
|
||||
// {
|
||||
// return 'vendor.livewire.tailwind'; // oder 'livewire::bootstrap'
|
||||
// }
|
||||
|
||||
// Wenn du mit Relationen arbeitest (z.B. user->role->name)
|
||||
// public function with(): array
|
||||
// {
|
||||
// return [
|
||||
// 'users' => User::with(['role', 'group']) // Eager loading
|
||||
// // ... deine Query-Logik von oben ...
|
||||
// ->paginate(10),
|
||||
// ];
|
||||
// }
|
||||
}; ?>
|
||||
<flux:table :paginate="$this->users">
|
||||
<flux:table.columns>
|
||||
<flux:table.column>Customer</flux:table.column>
|
||||
<flux:table.column sortable :sorted="$sortBy === 'date'" :direction="$sortDirection" wire:click="sort('date')">Date</flux:table.column>
|
||||
<flux:table.column sortable :sorted="$sortBy === 'status'" :direction="$sortDirection" wire:click="sort('status')">Status</flux:table.column>
|
||||
<flux:table.column sortable :sorted="$sortBy === 'amount'" :direction="$sortDirection" wire:click="sort('amount')">Amount</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@foreach ($this->users as $user)
|
||||
<flux:table.row :key="$user->id">
|
||||
<flux:table.cell class="flex items-center gap-3">
|
||||
<flux:avatar size="xs" src="{{ $user->avatar }}" />
|
||||
|
||||
{{ $user->name }}
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell class="whitespace-nowrap">{{ $user->email }}</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:badge size="sm" color="lime" inset="top bottom">RTest</flux:badge>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell variant="strong">{{ $user->role }}</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:dropdown>
|
||||
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal" inset="top bottom"></flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="plus">New post</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.submenu heading="Sort by">
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.radio checked>Name</flux:menu.radio>
|
||||
<flux:menu.radio>Date</flux:menu.radio>
|
||||
<flux:menu.radio>Popularity</flux:menu.radio>
|
||||
</flux:menu.radio.group>
|
||||
</flux:menu.submenu>
|
||||
<flux:menu.submenu heading="Filter">
|
||||
<flux:menu.checkbox checked>Draft</flux:menu.checkbox>
|
||||
<flux:menu.checkbox checked>Published</flux:menu.checkbox>
|
||||
<flux:menu.checkbox>Archived</flux:menu.checkbox>
|
||||
</flux:menu.submenu>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.item variant="danger" icon="trash">Delete</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
|
||||
|
|
@ -69,46 +69,46 @@ new class extends Component {
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
|
||||
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
|
||||
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
|
||||
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
|
||||
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
|
||||
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
|
||||
|
||||
<div>
|
||||
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
|
||||
<div>
|
||||
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
|
||||
|
||||
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail())
|
||||
<div>
|
||||
<flux:text class="mt-4">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail())
|
||||
<div>
|
||||
<flux:text class="mt-4">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</flux:link>
|
||||
</flux:text>
|
||||
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</flux:link>
|
||||
</flux:text>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="profile-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
|
||||
</div>
|
||||
|
||||
<livewire:settings.delete-user-form />
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
<x-action-message class="me-3" on="profile-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<livewire:settings.delete-user-form />
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<section class="section-padding flex items-center relative overflow-hidden">
|
||||
<section class="section-padding flex items-center relative border-b border-border/30">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 bg-[hsl(var(--hero-container))] rounded-[20px] w-[95%]">
|
||||
<div class="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-8 slide-right delay-200">
|
||||
<h1 class="text-hero">
|
||||
{!! $content['title'] !!}
|
||||
</h1>
|
||||
|
||||
<blockquote class="text-large text-muted-foreground italic leading-relaxed border-l-4 border-secondary pl-6">
|
||||
{{ $content['quote'] }}
|
||||
{!! $content['quote'] !!}
|
||||
</blockquote>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
|
|
@ -20,17 +20,19 @@
|
|||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="card-elevated rounded-3xl overflow-hidden">
|
||||
<img
|
||||
src="{{ asset('img/assets/' . $content['image']) }}"
|
||||
<div class="relative rounded-3xl overflow-hidden shadow-elevated slide-left delay-300">
|
||||
<img src="{{ asset('img/assets/' . $content['image']) }}"
|
||||
alt="{{ $content['image_alt'] }}"
|
||||
class="w-full h-96 lg:h-[500px] object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute -bottom-6 -right-6 bg-secondary text-secondary-foreground p-6 rounded-2xl">
|
||||
<div class="text-3xl font-bold">{{ $content['year'] }}</div>
|
||||
<p class="text-sm opacity-90">{{ $content['year_text'] }}</p>
|
||||
class="w-full h-[600px] object-cover" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
{{-- Floating info card --}}
|
||||
<div
|
||||
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400">
|
||||
<div class="text-xl font-medium text-muted-foreground">{{ $content['card_title'] }}</div>
|
||||
<div class="text-lg font-medium font-secondary">{!! $content['card_text'] !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@foreach($content['features'] as $index => $feature)
|
||||
<div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}">
|
||||
<div class="card-elevated p-8 overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col slide-up delay-{{ $index * 200 }}">
|
||||
<div class="group h-full {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}">
|
||||
<div class="card-elevated p-8 overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">
|
||||
<div class="text-center space-y-6">
|
||||
<div class="mx-auto w-16 h-16 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
|
||||
@if($feature['icon'])
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
{{-- Left Content --}}
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-6 slide-right delay-200">
|
||||
<h1 class="text-hero">
|
||||
<h1 class="text-hero-alt">
|
||||
{!! $content['title'] !!}
|
||||
</h1>
|
||||
<p class="text-lg text-muted-foreground max-w-md leading-relaxed">
|
||||
<p class="text-xl text-muted-foreground max-w-md leading-relaxed">
|
||||
{!! $content['subtitle'] !!}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -21,15 +21,16 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-6 pt-10 border-t border-border/80 slide-right delay-300">
|
||||
@foreach ($content['stats'] as $stat)
|
||||
<div class="flex items-center gap-2 text-md text-muted-foreground">
|
||||
@svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary')
|
||||
<span>{{ $stat }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(isset($content['stats']))
|
||||
<div class="flex flex-wrap items-center gap-6 pt-10 mt-10 border-t border-border/80 slide-right delay-300">
|
||||
@foreach ($content['stats'] as $stat)
|
||||
<div class="flex items-center gap-2 text-md text-muted-foreground font-light">
|
||||
@svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary')
|
||||
<span>{{ $stat }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -43,11 +44,10 @@
|
|||
|
||||
{{-- Floating info card --}}
|
||||
<div
|
||||
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400">
|
||||
<div class="text-sm text-muted-foreground">{{ $content['card_title'] }}</div>
|
||||
<div class="text-lg font-medium font-secondary">{!! $content['card_text'] !!}
|
||||
</div>
|
||||
</div>
|
||||
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400">
|
||||
<div class="text-xl font-medium text-muted-foreground">{{ $content['card_title'] }}</div>
|
||||
<div class="text-lg font-medium font-secondary">{!! $content['card_text'] !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div>
|
||||
<!-- Back Navigation -->
|
||||
<div class="pt-20 pb-4">
|
||||
<div class="container-narrow">
|
||||
<div class="pt-4 pb-4 border-b border-border/30 ">
|
||||
<div class="container-narrow ">
|
||||
<a href="/magazin" class="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
|
|
@ -10,13 +10,12 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article Header -->
|
||||
<article class="pb-16">
|
||||
<article class="pb-16 pt-16">
|
||||
<div class="container-narrow">
|
||||
<header class="text-center mb-12">
|
||||
<header class="text-center mb-12 slide-up delay-200">
|
||||
<h1 class="text-section-title mb-6 leading-tight">
|
||||
{{ $article['title'] }}
|
||||
{!! $article['title'] !!}
|
||||
</h1>
|
||||
<p class="text-large text-muted-foreground mb-8 max-w-3xl mx-auto">
|
||||
{{ $article['subtitle'] }}
|
||||
|
|
@ -38,7 +37,7 @@
|
|||
</header>
|
||||
|
||||
<!-- Featured Image -->
|
||||
<div class="mb-12 overflow-hidden rounded-lg">
|
||||
<div class="mb-12 overflow-hidden rounded-lg shadow-md slide-up delay-200">
|
||||
<img
|
||||
src="{{ asset('img/assets/' . $article['image']) }}"
|
||||
alt="{{ $article['title'] }}"
|
||||
|
|
@ -50,12 +49,12 @@
|
|||
<!-- Main Content -->
|
||||
<div class="md:col-span-3">
|
||||
<div class="prose prose-lg max-w-none">
|
||||
<p class="text-large text-muted-foreground leading-relaxed mb-8">
|
||||
<p class="text-lg text-muted-foreground leading-relaxed mb-8 slide-up delay-200">
|
||||
{{ $article['content']['intro'] }}
|
||||
</p>
|
||||
|
||||
@foreach($article['content']['sections'] as $index => $section)
|
||||
<section class="mb-10" id="section-{{ $index }}">
|
||||
<section class="mb-10 bg-light-muted shadow-md p-6 rounded-lg slide-up delay-400" id="section-{{ $index }}">
|
||||
<h2 class="text-2xl font-medium text-foreground mb-4">
|
||||
{{ $index + 1 }}. {{ $section['title'] }}
|
||||
</h2>
|
||||
|
|
@ -71,21 +70,25 @@
|
|||
<div class="md:col-span-1">
|
||||
<div class="sticky top-24">
|
||||
|
||||
<div class="card-elevated rounded-lg p-6">
|
||||
<h3 class="font-medium text-foreground mb-4">{{ $content['share_article'] }}</h3>
|
||||
<div class="card-elevated rounded-lg p-2 slide-left delay-400">
|
||||
<h3 class="font-medium text-foreground mb-4 text-center font-2xl py-2">{{ $content['share_article'] }}</h3>
|
||||
<div class="space-y-3">
|
||||
<button class="btn-secondary-accent w-full">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
<span class="font-medium">LinkedIn</span>
|
||||
<span class="font-medium text-sm">LinkedIn</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button class="btn-secondary-accent w-full">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
<span class="font-medium text-sm">Facebook</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<div class="space-y-8">
|
||||
@foreach($this->posts as $post)
|
||||
<article class="group">
|
||||
<div class="card-elevated rounded-3xl overflow-hidden h-full hover:scale-[1.02] transition-all duration-300">
|
||||
<div class="card-elevated rounded-3xl overflow-hidden h-full transition-all duration-300">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<div class="relative md:w-3/4 aspect-[2/1] md:aspect-[2/1]">
|
||||
<img
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<h3 class="text-xl lg:text-2xl font-semibold text-foreground leading-tight group-hover:text-secondary transition-colors duration-200">
|
||||
<a href="/magazin/{{ $post['id'] }}" class="stretched-link">
|
||||
{{ $post['title'] }}
|
||||
{!! $post['title'] !!}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,42 @@
|
|||
<section class="section-padding">
|
||||
<div class="container-padding text-center">
|
||||
<h2 class="text-section-title text-foreground mb-12">
|
||||
{!! $content['title'] !!}
|
||||
</h2>
|
||||
<div class="text-center mb-16 slide-up delay-200">
|
||||
<h2 class="text-section-title text-foreground mb-12">
|
||||
{!! $content['title'] !!}
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@foreach($content['timeline'] as $index => $item)
|
||||
@foreach($content['timeline'] as $index => $card)
|
||||
<div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}">
|
||||
<div class="card-elevated p-8 rounded-3xl h-full hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
||||
<div class="text-center space-y-6">
|
||||
<div class="w-12 h-12 mx-auto bg-secondary/20 rounded-full flex items-center justify-center">
|
||||
<div class="w-6 h-6 bg-secondary rounded-full"></div>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-foreground">{{ $item['title'] }}</h3>
|
||||
<div class="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">
|
||||
|
||||
|
||||
@if(isset($card['icon']))
|
||||
<div class="relative pt-12 pb-8">
|
||||
<div class="mx-auto w-20 h-20 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
|
||||
@svg('heroicon-o-'.$card['icon'], 'w-10 h-10 text-secondary-foreground')
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="p-6 spacing-small flex flex-col justify-between flex-grow">
|
||||
<div class="mb-4">
|
||||
@if (isset($card['logo']))
|
||||
<img src="{{ asset($card['logo']) }}" alt="{{ $card['title'] }}"
|
||||
class="{{ $card['logo_width'] }} h-18 object-contain" />
|
||||
@endif
|
||||
@if(isset($card['title']))
|
||||
<h3 class="text-2xl font-medium text-center">{{ $card['title'] }}</h3>
|
||||
@endif
|
||||
@if(isset($card['description']))
|
||||
<p class="text-muted-foreground leading-relaxed mt-4 text-center">
|
||||
{{ $card['description'] }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<p class="text-muted-foreground text-sm leading-relaxed">
|
||||
{{ $item['description'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-secondary/20 via-secondary to-secondary/20 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300"></div>
|
||||
|
|
|
|||
|
|
@ -21,35 +21,41 @@ function renderHeroIcon($iconName, $style = 'outline') {
|
|||
@endphp
|
||||
<section class="section-padding">
|
||||
<div class="container-padding">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-section-title mb-6">
|
||||
{!! $content['title'] !!}
|
||||
</h2>
|
||||
<p class="text-large text-muted-foreground max-w-2xl mx-auto">
|
||||
{{ $content['subtitle'] }}
|
||||
<div class="text-center mb-16 slide-up delay-300">
|
||||
<h2 class="text-section-title">{!! $content['title'] !!}</h2>
|
||||
<p class="text-large text-muted-foreground mt-4 max-w-3xl mx-auto">
|
||||
{!! $content['subtitle'] !!}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@foreach($content['values'] as $index => $value)
|
||||
<div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}">
|
||||
<div class="card-elevated p-8 rounded-3xl h-full hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
||||
<div class="text-center space-y-6">
|
||||
<div class="mx-auto w-20 h-20 bg-secondary/10 rounded-2xl flex items-center justify-center group-hover:bg-secondary/20 transition-colors duration-300">
|
||||
{!! renderHeroIcon($value['icon'], $value['icon_style'] ?? 'outline') !!}
|
||||
<div class="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">
|
||||
@if(isset($value['icon']))
|
||||
<div class="relative pt-12 pb-8">
|
||||
<div class="mx-auto w-20 h-20 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
|
||||
@svg('heroicon-o-'.$value['icon'], 'w-10 h-10 text-secondary-foreground')
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="p-6 spacing-small flex flex-col justify-between flex-grow">
|
||||
<div class="mb-4">
|
||||
@if (isset($value['logo']))
|
||||
<img src="{{ asset($value['logo']) }}" alt="{{ $value['title'] }}"
|
||||
class="{{ $value['logo_width'] }} h-18 object-contain" />
|
||||
@endif
|
||||
@if(isset($value['title']))
|
||||
<h3 class="text-xl font-medium text-center">{{ $value['title'] }}</h3>
|
||||
@endif
|
||||
@if(isset($value['description']))
|
||||
<p class="text-muted-foreground leading-relaxed mt-2 text-center">
|
||||
{{ $value['description'] }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-foreground">
|
||||
{{ $value['title'] }}
|
||||
</h3>
|
||||
|
||||
<div class="w-12 h-px bg-secondary mx-auto"></div>
|
||||
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{{ $value['description'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-secondary/20 via-secondary to-secondary/20 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,16 @@
|
|||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@if(isset($content['stats']))
|
||||
<div class="flex flex-wrap items-center gap-6 pt-10 border-t border-border/80 slide-right delay-300">
|
||||
@foreach ($content['stats'] as $stat)
|
||||
<div class="flex items-center gap-2 text-md text-muted-foreground">
|
||||
@svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary')
|
||||
<span>{{ $stat }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -39,15 +49,13 @@
|
|||
</div>
|
||||
|
||||
{{-- Floating info card --}}
|
||||
|
||||
<div
|
||||
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400">
|
||||
<div class="text-center slide-left delay-400">
|
||||
<h3 class="text-xl font-semibold text-foreground">{{ $content['hub']['title'] }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ $content['hub']['subtitle'] }}</p>
|
||||
</div>
|
||||
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400">
|
||||
<div class="text-xl font-medium text-muted-foreground">{!! $content['hub']['title'] !!}</div>
|
||||
<div class="text-lg font-medium font-secondary">{!! $content['hub']['subtitle'] !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,7 @@
|
|||
<section class="section-padding {{ $bg }}">
|
||||
<div class="container-padding">
|
||||
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
||||
{{-- Content --}}
|
||||
<div class="spacing-section">
|
||||
<div class="spacing-content slide-right delay-300">
|
||||
<h2 class="text-section-title">{{ $content['title'] }}</h2>
|
||||
<div class="spacing-small text-large text-muted-foreground leading-relaxed">
|
||||
@foreach ($content['paragraphs'] as $paragraph)
|
||||
<p>{!! $paragraph !!}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{-- Image --}}
|
||||
<div class="relative">
|
||||
|
|
@ -24,6 +14,19 @@
|
|||
{{ $content['image_caption'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
<div class="spacing-section">
|
||||
<div class="spacing-content slide-right delay-300">
|
||||
<h2 class="text-section-title">{{ $content['title'] }}</h2>
|
||||
<div class="spacing-small text-large text-muted-foreground leading-relaxed">
|
||||
@foreach ($content['paragraphs'] as $paragraph)
|
||||
<p>{!! $paragraph !!}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 bg-[hsl(var(--hero-container))] rounded-[20px] w-[95%]">
|
||||
<div class="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<!-- Left Side - Hero Text -->
|
||||
<div>
|
||||
<div class="slide-right delay-200">
|
||||
<h1 class="text-hero mb-6 tracking-wide">
|
||||
{!! $content['hero']['title'] ?? 'Send us a<br /><span class="text-secondary font-medium">message.</span>' !!}
|
||||
</h1>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Right Side - Contact Form -->
|
||||
<div class="card-elevated p-8">
|
||||
<div class="card-elevated p-8 slide-left delay-200">
|
||||
@if (session()->has('message'))
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-secondary/20 rounded-lg text-secondary">
|
||||
{{ session('message') }}
|
||||
|
|
@ -153,8 +153,8 @@
|
|||
<section class="section-padding">
|
||||
<div class="container-padding">
|
||||
<div class="grid md:grid-cols-3 gap-8">
|
||||
@foreach($this->contactInfo as $info)
|
||||
<div class="card-elevated p-8 rounded-xl text-center">
|
||||
@foreach($this->contactInfo as $index => $info)
|
||||
<div class="card-elevated p-8 rounded-xl text-center slide-up delay-{{ $index * 200 }}">
|
||||
<div class="w-12 h-12 bg-secondary/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
@if($info['icon'] === 'map-pin')
|
||||
<svg class="w-6 h-6 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
<section class="section-padding bg-accent">
|
||||
<div class="container-padding">
|
||||
<div class="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<div class="slide-right delay-200">
|
||||
<h2 class="text-section-title mb-6">
|
||||
{!! $content['social_media']['title'] ?? 'Follow for<br /><span class="text-secondary font-medium">exclusives</span>' !!}
|
||||
</h2>
|
||||
|
|
@ -197,8 +197,8 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach($this->socialMedia as $social)
|
||||
<div class="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors cursor-pointer">
|
||||
@foreach($this->socialMedia as $index => $social)
|
||||
<div class="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors slide-left delay-{{ $index * 200 }} cursor-pointer">
|
||||
<div>
|
||||
<h3 class="text-xl text-foreground">{{ $social['name'] }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ $social['handle'] }}</p>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ $title ?? config('app.name') }}</title>
|
||||
|
||||
|
|
@ -8,9 +9,13 @@
|
|||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
|
||||
|
||||
|
||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
|
||||
rel="stylesheet" />
|
||||
|
||||
|
||||
@vite(['resources/css/portal.css', 'resources/js/app.js'], 'build/portal')
|
||||
|
||||
@livewireStyles
|
||||
@fluxAppearance
|
||||
|
|
|
|||
8
resources/views/partials/logo-head.blade.php
Normal file
8
resources/views/partials/logo-head.blade.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<div class="mb-6 mx-auto flex justify-center">
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}"
|
||||
alt="B2IN Logo"
|
||||
class="h-14 w-auto dark:hidden" />
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}"
|
||||
alt="B2IN Logo"
|
||||
class="h-14 w-auto hidden dark:block" />
|
||||
</div>
|
||||
9
resources/views/partials/theme-init-script.blade.php
Normal file
9
resources/views/partials/theme-init-script.blade.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<!-- Theme Initialisierung (vor dem Rendern, um Flackern zu vermeiden) -->
|
||||
<script>
|
||||
if (localStorage.getItem('theme') === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
|
||||
38
resources/views/partials/theme-toggle-script.blade.php
Normal file
38
resources/views/partials/theme-toggle-script.blade.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<script>
|
||||
// Theme Toggle Funktionalität
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleText = document.getElementById('theme-toggle-text');
|
||||
const html = document.documentElement;
|
||||
|
||||
// Theme aus localStorage laden oder Standard verwenden
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
|
||||
function updateThemeUI(theme) {
|
||||
if (theme === 'dark') {
|
||||
html.classList.add('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
themeToggleDarkIcon.classList.add('hidden');
|
||||
themeToggleText.textContent = 'Hell';
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.add('hidden');
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
themeToggleText.textContent = 'Dunkel';
|
||||
}
|
||||
}
|
||||
|
||||
// Initiales Theme setzen
|
||||
updateThemeUI(savedTheme);
|
||||
|
||||
// Toggle Button Event
|
||||
themeToggleBtn.addEventListener('click', () => {
|
||||
const currentTheme = html.classList.contains('dark') ? 'dark' : 'light';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeUI(newTheme);
|
||||
});
|
||||
</script>
|
||||
|
||||
41
resources/views/partner/invitation-expired.blade.php
Normal file
41
resources/views/partner/invitation-expired.blade.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<x-layouts.guest>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 via-white to-orange-50 dark:from-zinc-900 dark:via-zinc-900 dark:to-zinc-800 px-4 py-12">
|
||||
<div class="w-full max-w-md text-center">
|
||||
<div class="mb-6 mx-auto flex justify-center">
|
||||
@include('partials.logo-head')
|
||||
</div>
|
||||
|
||||
<flux:card class="shadow-2xl">
|
||||
<div class="py-8">
|
||||
<flux:icon.exclamation-triangle class="h-16 w-16 text-red-500 mx-auto mb-4" />
|
||||
|
||||
<flux:heading size="xl" class="mb-4">
|
||||
{{ __('Einladung abgelaufen') }}
|
||||
</flux:heading>
|
||||
|
||||
<flux:subheading class="mb-6">
|
||||
{{ __('Diese Einladung ist am :date abgelaufen.', ['date' => $invitation->expires_at->format('d.m.Y H:i')]) }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-4 mb-6">
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Firma') }}: <strong class="text-zinc-900 dark:text-white">{{ $invitation->company_name }}</strong><br>
|
||||
{{ __('E-Mail') }}: <strong class="text-zinc-900 dark:text-white">{{ $invitation->email }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<flux:separator class="my-6" />
|
||||
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-6">
|
||||
{{ __('Bitte kontaktieren Sie Ihren Ansprechpartner, um eine neue Einladung zu erhalten.') }}
|
||||
</p>
|
||||
|
||||
<flux:button variant="primary" href="mailto:support@b2in.de" icon="envelope">
|
||||
{{ __('Support kontaktieren') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.guest>
|
||||
|
||||
38
resources/views/partner/invitation-used.blade.php
Normal file
38
resources/views/partner/invitation-used.blade.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<x-layouts.guest>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-zinc-900 dark:via-zinc-900 dark:to-zinc-800 px-4 py-12">
|
||||
<div class="w-full max-w-md text-center">
|
||||
@include('partials.logo-head')
|
||||
<flux:card class="shadow-2xl">
|
||||
<div class="py-8">
|
||||
<flux:icon.check-circle class="h-16 w-16 text-green-500 mx-auto mb-4" />
|
||||
|
||||
<flux:heading size="xl" class="mb-4">
|
||||
{{ __('Einladung bereits verwendet') }}
|
||||
</flux:heading>
|
||||
|
||||
<flux:subheading class="mb-6">
|
||||
{{ __('Diese Einladung wurde bereits am :date akzeptiert.', ['date' => $invitation->accepted_at?->format('d.m.Y H:i') ?? '']) }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-4 mb-6">
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Firma') }}: <strong class="text-zinc-900 dark:text-white">{{ $invitation->company_name }}</strong><br>
|
||||
{{ __('E-Mail') }}: <strong class="text-zinc-900 dark:text-white">{{ $invitation->email }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<flux:separator class="my-6" />
|
||||
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-6">
|
||||
{{ __('Sie können sich mit Ihren Zugangsdaten anmelden.') }}
|
||||
</p>
|
||||
|
||||
<flux:button variant="primary" href="{{ route('login') }}" icon="arrow-right">
|
||||
{{ __('Zur Anmeldung') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.guest>
|
||||
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.about-hero />
|
||||
<livewire:web.components.sections.our-story />
|
||||
<livewire:web.components.sections.leadership-team />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.ui.contact-form />
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.hero />
|
||||
<livewire:web.components.sections.ecosystem-core />
|
||||
<livewire:web.components.sections.vision-section bg="bg-accent" />
|
||||
<livewire:web.components.sections.vision-section />
|
||||
|
||||
<livewire:web.components.sections.ecosystem-core bg="bg-accent" />
|
||||
<livewire:web.components.sections.brand-worlds />
|
||||
<livewire:web.components.sections.content-section layout="left" bg="bg-accent"
|
||||
section="integriertes_modell_b2in" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.magazin-detail :id="$id ?? 1" />
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.magazin-list />
|
||||
</main>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue