ï»¿## ðŸ—ï¸ **Multi-Tenant System-Architektur**

Note: Event Type-aware UI
- Theme colors can come from `event_types.settings.palette` to skin headers/buttons per type.
- Emotion picker fetches type-filtered emotions via `/api/events/:slug/emotions`.
- Random task generator and gallery filters respect event type automatically via API.


```
â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
â”‚                         SYSTEM OVERVIEW (Multi-Tenant)                 â”‚
â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¤
â”‚                                                                         â”‚
â”‚  ðŸ”§ SUPER ADMIN          ðŸ‘°ðŸ¤µ BRAUTPAARE         ðŸ“± GÃ„STE PWA           â”‚
â”‚  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”      â”‚
â”‚  â”‚   System Admin  â”‚    â”‚  Customer Admin â”‚    â”‚   React PWA     â”‚      â”‚
â”‚  â”‚   Panel         â”‚â—„â”€â”€â–ºâ”‚     Panel       â”‚â—„â”€â”€â–ºâ”‚  Event Photos   â”‚      â”‚
â”‚  â”‚                 â”‚    â”‚                 â”‚    â”‚                 â”‚      â”‚
â”‚  â”‚ â€¢ User Mgmt     â”‚    â”‚ â€¢ Event Setup   â”‚    â”‚ â€¢ Take Photos   â”‚      â”‚
â”‚  â”‚ â€¢ System Config â”‚    â”‚ â€¢ Photo Review  â”‚    â”‚ â€¢ Upload Share  â”‚      â”‚
â”‚  â”‚ â€¢ Billing       â”‚    â”‚ â€¢ Gallery Build â”‚    â”‚ â€¢ Live Feed     â”‚      â”‚
â”‚  â”‚ â€¢ Analytics     â”‚    â”‚ â€¢ Export        â”‚    â”‚ â€¢ Like/Comment  â”‚      â”‚
â”‚  â”‚ â€¢ Monitoring    â”‚    â”‚                 â”‚    â”‚                 â”‚      â”‚
â”‚  â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜    â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜    â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜      â”‚
â”‚           â”‚                       â”‚                       â”‚             â”‚
â”‚           â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜             â”‚
â”‚                                   â”‚                                     â”‚
â”‚  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”â”‚
â”‚  â”‚              LARAVEL BACKEND API (Multi-Tenant)                    â”‚â”‚
â”‚  â”‚  â€¢ Tenant Isolation  â€¢ User Management  â€¢ Billing System          â”‚â”‚
â”‚  â”‚  â€¢ System Monitoring â€¢ Global Analytics â€¢ Platform Administration â”‚â”‚
â”‚  â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜â”‚
â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜
```

## ðŸ‘‘ **Super Admin System (Platform Management)**

### **Multi-Tenant Database Schema:**
```sql
-- Tenant/Customer Management
CREATE TABLE tenants (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(255) NOT NULL,           -- "Familie MÃ¼ller"
    slug VARCHAR(255) UNIQUE NOT NULL,    -- "familie-mueller"
    domain VARCHAR(255) UNIQUE,           -- Optional: custom domain
    
    -- Contact Information
    contact_name VARCHAR(255) NOT NULL,
    contact_email VARCHAR(255) NOT NULL,
    contact_phone VARCHAR(255),
    
    -- Event-basierte Monetarisierung (keine Subscriptions)
    event_credits_balance INTEGER DEFAULT 1,
    free_event_granted_at TIMESTAMP,
    
    -- Limits & Quotas
    max_photos_per_event INTEGER DEFAULT 500,
    max_storage_mb INTEGER DEFAULT 1024,  -- 1GB
    
    -- Feature Flags
    features JSON,                        -- {"custom_branding": true, "api_access": false}
    
    -- Metadata
    last_activity_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Erweiterte User-Tabelle fÃ¼r Multi-Tenancy
ALTER TABLE users ADD COLUMN tenant_id INTEGER;
ALTER TABLE users ADD COLUMN role ENUM('super_admin', 'tenant_admin', 'tenant_user') DEFAULT 'tenant_user';
ALTER TABLE users ADD CONSTRAINT fk_users_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenants(id);

-- Events gehÃ¶ren zu Tenants
ALTER TABLE events ADD COLUMN tenant_id INTEGER NOT NULL;
ALTER TABLE events ADD CONSTRAINT fk_events_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenants(id);

-- System-weite Konfiguration
CREATE TABLE system_settings (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    key VARCHAR(255) UNIQUE NOT NULL,
    value TEXT,
    description TEXT,
    is_public BOOLEAN DEFAULT 0,          -- FÃ¼r Ã¶ffentliche API settings
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Platform Analytics
CREATE TABLE platform_analytics (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tenant_id INTEGER,
    metric_name VARCHAR(100) NOT NULL,    -- 'photos_uploaded', 'events_created'
    metric_value INTEGER NOT NULL,
    metric_date DATE NOT NULL,
    metadata JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    INDEX idx_analytics_date_metric (metric_date, metric_name),
    INDEX idx_analytics_tenant (tenant_id, metric_date)
);

-- Event Purchases (one-time) und Guthaben-Ledger
CREATE TABLE event_purchases (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tenant_id INTEGER NOT NULL,
    events_purchased INTEGER NOT NULL DEFAULT 1,
    amount DECIMAL(10,2) NOT NULL,
    currency VARCHAR(3) DEFAULT 'EUR',
    provider ENUM('app_store', 'play_store', 'stripe', 'paypal') NOT NULL,
    external_receipt_id VARCHAR(255),
    status ENUM('pending', 'paid', 'failed', 'refunded', 'cancelled') DEFAULT 'pending',
    purchased_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    INDEX idx_event_purchases_tenant (tenant_id, purchased_at)
);

CREATE TABLE event_credits_ledger (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tenant_id INTEGER NOT NULL,
    delta INTEGER NOT NULL,
    reason ENUM('initial_free','purchase','manual_adjustment','event_created','refund') NOT NULL,
    related_purchase_id INTEGER,
    note TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    FOREIGN KEY (related_purchase_id) REFERENCES event_purchases(id),
    INDEX idx_ledger_tenant (tenant_id, created_at)
);
```

## ðŸ› ï¸ **Super Admin Panel (Separate Filament Panel)**

Note (Filament 4):
- Forms/Infolists components live under `Filament\Schema\Components` (updated below).
- Table row actions use `recordActions()`; actions are unified under `Filament\Actions`.
- Keep `Tables\Columns`/`Tables\Filters` as-is; columns/filters remain in Tables.

### **Super Admin Panel Provider:**
```php
<?php
// app/Providers/Filament/SuperAdminPanelProvider.php

namespace App\Providers\Filament;

use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Pages;

class SuperAdminPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->id('super-admin')
            ->path('super-admin')
            ->login()
            ->colors([
                'primary' => Color::Indigo,
                'secondary' => Color::Slate,
            ])
            ->brandName('Fotobox Platform Admin')
            ->brandLogo(asset('images/platform-logo.png'))
            ->discoverResources(in: app_path('Filament/SuperAdmin/Resources'), for: 'App\\Filament\\SuperAdmin\\Resources')
            ->discoverPages(in: app_path('Filament/SuperAdmin/Pages'), for: 'App\\Filament\\SuperAdmin\\Pages')
            ->pages([
                Pages\Dashboard::class,
            ])
            ->widgets([
                \App\Filament\SuperAdmin\Widgets\PlatformStatsWidget::class,
                \App\Filament\SuperAdmin\Widgets\RevenueChartWidget::class,
                \App\Filament\SuperAdmin\Widgets\TenantActivityWidget::class,
            ])
            ->navigationGroups([
                'Customer Management',
                'Platform Configuration',
                'Legal & Compliance',
                'Billing & Events',
                'System Monitoring',
                'Content Management'
            ])
            ->authMiddleware([
                \App\Http\Middleware\SuperAdminOnly::class,
            ]);
    }
}
```

## ðŸ‘¥ **Tenant Management Resource**

```php
<?php
// app/Filament/SuperAdmin/Resources/TenantResource.php

namespace App\Filament\SuperAdmin\Resources;

use App\Models\Tenant;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Schema as Schema;
use Filament\Actions;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;

class TenantResource extends Resource
{
    protected static ?string $model = Tenant::class;
    protected static ?string $navigationIcon = 'heroicon-o-building-office';
    protected static ?string $navigationGroup = 'Customer Management';
    protected static ?string $label = 'Kunden';

    public static function form(Form $form): Form
    {
        return $form->schema([
            Schema\\Components\\Section::make('Kunden-Details')
                ->schema([
                    Schema\\Components\\TextInput::make('name')
                        ->label('Kundenname')
                        ->required()
                        ->maxLength(255),

                    Schema\\Components\\TextInput::make('slug')
                        ->required()
                        ->unique(Tenant::class, 'slug', ignoreRecord: true)
                        ->helperText('Eindeutige URL fÃ¼r den Kunden'),

                    Schema\\Components\\TextInput::make('domain')
                        ->label('Custom Domain (optional)')
                        ->url()
                        ->unique(Tenant::class, 'domain', ignoreRecord: true),

                    Schema\\Components\\TextInput::make('contact_name')
                        ->label('Ansprechpartner')
                        ->required(),

                    Schema\\Components\\TextInput::make('contact_email')
                        ->label('E-Mail')
                        ->email()
                        ->required(),

                    Schema\\Components\\TextInput::make('contact_phone')
                        ->label('Telefon')
                        ->tel(),
                ])
                ->columns(2),

            Schema\\Components\\Section::make('Subscription Management')
                ->schema([
                    Schema\\Components\\Select::make('plan_type')
                        ->label('Plan')
                        ->options([
                            'free' => 'Free (Testversion)',
                            'basic' => 'Basic (29â‚¬/Monat)',
                            'premium' => 'Premium (49â‚¬/Monat)', 
                            'enterprise' => 'Enterprise (Custom)'
                        ])
                        ->required()
                        ->live(),

                    Schema\\Components\\Select::make('subscription_status')
                        ->options([
                            'trial' => 'Trial',
                            'active' => 'Aktiv',
                            'suspended' => 'Suspendiert',
                            'cancelled' => 'GekÃ¼ndigt'
                        ])
                        ->required(),

                    Schema\\Components\\DateTimePicker::make('subscription_starts_at')
                        ->label('Abo Start'),

                    Schema\\Components\\DateTimePicker::make('subscription_ends_at')
                        ->label('Abo Ende'),

                    Schema\\Components\\DateTimePicker::make('trial_ends_at')
                        ->label('Trial Ende')
                        ->visible(fn ($get) => $get('subscription_status') === 'trial'),
                ])
                ->columns(2),

            Schema\\Components\\Section::make('Limits & Kontingente')
                ->schema([
                    Schema\\Components\\TextInput::make('max_events')
                        ->label('Max. Events')
                        ->numeric()
                        ->default(1),

                    Schema\\Components\\TextInput::make('max_photos_per_event')
                        ->label('Max. Fotos pro Event')
                        ->numeric()
                        ->default(500),

                    Schema\\Components\\TextInput::make('max_storage_mb')
                        ->label('Max. Speicher (MB)')
                        ->numeric()
                        ->default(1024),
                ])
                ->columns(3),

            Schema\\Components\\Section::make('Feature Flags')
                ->schema([
                    Schema\\Components\\KeyValue::make('features')
                        ->label('Aktivierte Features')
                        ->keyLabel('Feature')
                        ->valueLabel('Aktiviert')
                        ->default([
                            'custom_branding' => false,
                            'api_access' => false,
                            'advanced_analytics' => false,
                            'white_label' => false
                        ]),
                ]),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('name')
                    ->searchable()
                    ->sortable(),

                Tables\Columns\TextColumn::make('contact_email')
                    ->label('Kontakt')
                    ->searchable(),

                Tables\Columns\TextColumn::make('event_credits_balance')
                    ->label('Event-Guthaben')
                    ->badge()
                    

                
                Tables\Columns\TextColumn::make('events_count')
                    ->label('Events')
                    ->counts('events'),

                Tables\Columns\TextColumn::make('total_photos_count')
                    ->label('Gesamt Fotos')
                    ->getStateUsing(fn ($record) => 
                        $record->events()->withCount('photos')->get()->sum('photos_count')
                    ),

                Tables\Columns\TextColumn::make('free_event_granted_at')
                    ->label('Kostenloses Event am')
                    ->date('d.m.Y')
                    ->sortable(),

                Tables\Columns\TextColumn::make('last_activity_at')
                    ->label('Letzte AktivitÃ¤t')
                    ->since()
                    ->sortable(),
            ])
            ->filters([
                Tables\Filters\SelectFilter::make('plan_type')
                    ->options([
                        'free' => 'Free',
                        'basic' => 'Basic',
                        'premium' => 'Premium',
                        'enterprise' => 'Enterprise'
                    ]),

                Tables\Filters\SelectFilter::make('subscription_status')
                    ->options([
                        'trial' => 'Trial',
                        'active' => 'Aktiv',
                        'suspended' => 'Suspendiert',
                        'cancelled' => 'GekÃ¼ndigt'
                    ]),

                Tables\Filters\Filter::make('expiring_soon')
                    ->label('LÃ¤uft bald ab')
                    ->query(fn ($query) => $query->where('subscription_ends_at', '<=', now()->addDays(30))),
            ])
            ->recordActions([
                Actions\\ViewAction::make(),
                Actions\\EditAction::make(),
                Actions\\Action::make('login_as')
                    ->label('Als Kunde anmelden')
                    ->icon('heroicon-o-arrow-right-on-rectangle')
                    ->url(fn ($record) => "/admin/impersonate/{$record->id}")
                    ->openUrlInNewTab(),
                
            ])
            ->bulkActions([
                Actions\\BulkAction::make('send_notification')
                    ->label('Benachrichtigung senden')
                    ->icon('heroicon-o-envelope')
                    ->form([
                        Schema\\Components\\TextInput::make('subject')
                            ->label('Betreff')
                            ->required(),
                        Schema\\Components\\Textarea::make('message')
                            ->label('Nachricht')
                            ->required()
                            ->rows(4),
                    ])
                    ->action(function (array $data, Collection $records) {
                        // Send bulk notification to selected customers
                        foreach ($records as $tenant) {
                            // Send email notification
                        }
                    }),
            ])
            ->defaultSort('created_at', 'desc');
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListTenants::route('/'),
            'create' => Pages\CreateTenant::route('/create'),
            'edit' => Pages\EditTenant::route('/{record}/edit'),
            'view' => Pages\ViewTenant::route('/{record}'),
        ];
    }
}
```

## ðŸ’° **Subscription Plans Resource**

```php
<?php
// app/Filament/SuperAdmin/Resources/SubscriptionPlanResource.php

namespace App\Filament\SuperAdmin\Resources;

use App\Models\SubscriptionPlan;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\\Schema as Schema;
use Filament\\Actions;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;

class SubscriptionPlanResource extends Resource
{
    protected static ?string $model = SubscriptionPlan::class;
    protected static ?string $navigationIcon = 'heroicon-o-credit-card';
    protected static ?string $navigationGroup = 'Billing & Subscriptions';
    protected static ?string $label = 'Abo-PlÃ¤ne';

    public static function form(Form $form): Form
    {
        return $form->schema([
            Schema\\Components\\Section::make('Plan Details')
                ->schema([
                    Schema\\Components\\TextInput::make('name')
                        ->required()
                        ->maxLength(100),

                    Schema\\Components\\TextInput::make('slug')
                        ->required()
                        ->unique(SubscriptionPlan::class, 'slug', ignoreRecord: true),

                    Schema\\Components\\TextInput::make('price_monthly')
                        ->label('Monatspreis (â‚¬)')
                        ->numeric()
                        ->prefix('â‚¬'),

                    Schema\\Components\\TextInput::make('price_yearly')
                        ->label('Jahrespreis (â‚¬)')
                        ->numeric()
                        ->prefix('â‚¬'),

                    Schema\\Components\\Toggle::make('is_active')
                        ->label('Plan aktiv')
                        ->default(true),
                ])
                ->columns(2),

            Schema\\Components\\Section::make('Plan Limits')
                ->schema([
                    Schema\\Components\\TextInput::make('max_events')
                        ->label('Max. Events')
                        ->numeric()
                        ->required(),

                    Schema\\Components\\TextInput::make('max_photos_per_event')
                        ->label('Max. Fotos pro Event')
                        ->numeric()
                        ->required(),

                    Schema\\Components\\TextInput::make('max_storage_gb')
                        ->label('Max. Speicher (GB)')
                        ->numeric()
                        ->required(),
                ])
                ->columns(3),

            Schema\\Components\\Section::make('Features')
                ->schema([
                    Schema\\Components\\KeyValue::make('features')
                        ->label('Plan Features')
                        ->keyLabel('Feature')
                        ->valueLabel('Enthalten')
                        ->default([
                            'custom_branding' => false,
                            'api_access' => false,
                            'advanced_analytics' => false,
                            'priority_support' => false,
                            'white_label' => false,
                            'custom_domain' => false
                        ]),
                ]),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('name')
                    ->searchable()
                    ->sortable(),

                Tables\Columns\TextColumn::make('price_monthly')
                    ->label('Monatlich')
                    ->money('EUR'),

                Tables\Columns\TextColumn::make('price_yearly')
                    ->label('JÃ¤hrlich')
                    ->money('EUR'),

                Tables\Columns\TextColumn::make('max_events')
                    ->label('Events'),

                Tables\Columns\TextColumn::make('max_photos_per_event')
                    ->label('Fotos/Event'),

                Tables\Columns\TextColumn::make('max_storage_gb')
                    ->label('Storage (GB)'),

                Tables\Columns\TextColumn::make('subscribers_count')
                    ->label('Kunden')
                    ->getStateUsing(fn ($record) => $record->tenants()->count()),

                Tables\Columns\IconColumn::make('is_active')
                    ->boolean(),
            ])
            ->recordActions([
                Actions\\EditAction::make(),
                Actions\\Action::make('duplicate')
                    ->label('Duplizieren')
                    ->icon('heroicon-o-document-duplicate')
                    ->action(function ($record) {
                        $newPlan = $record->replicate();
                        $newPlan->name = $record->name . ' (Kopie)';
                        $newPlan->slug = $record->slug . '-copy';
                        $newPlan->save();
                    }),
            ]);
    }
}
```

## ðŸ“Š **System Settings Resource**

```php
<?php
// app/Filament/SuperAdmin/Resources/SystemSettingResource.php

namespace App\Filament\SuperAdmin\Resources;

use App\Models\SystemSetting;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\\Schema as Schema;
use Filament\\Actions;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;

class SystemSettingResource extends Resource
{
    protected static ?string $model = SystemSetting::class;
    protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
    protected static ?string $navigationGroup = 'Platform Configuration';
    protected static ?string $label = 'System-Einstellungen';

    public static function form(Form $form): Form
    {
        return $form->schema([
            Schema\\Components\\Section::make('Setting Details')
                ->schema([
                    Schema\\Components\\TextInput::make('key')
                        ->required()
                        ->unique(SystemSetting::class, 'key', ignoreRecord: true)
                        ->helperText('Eindeutiger Key fÃ¼r die Einstellung'),

                    Schema\\Components\\Textarea::make('value')
                        ->required()
                        ->rows(3)
                        ->helperText('JSON, String oder andere Werte'),

                    Schema\\Components\\Textarea::make('description')
                        ->rows(2)
                        ->helperText('Beschreibung der Einstellung'),

                    Schema\\Components\\Toggle::make('is_public')
                        ->label('Ã–ffentlich verfÃ¼gbar')
                        ->helperText('Ãœber Ã¶ffentliche API abrufbar'),
                ]),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('key')
                    ->searchable()
                    ->sortable(),

                Tables\Columns\TextColumn::make('value')
                    ->limit(50)
                    ->searchable(),

                Tables\Columns\TextColumn::make('description')
                    ->limit(100),

                Tables\Columns\IconColumn::make('is_public')
                    ->boolean(),

                Tables\Columns\TextColumn::make('updated_at')
                    ->label('Zuletzt geÃ¤ndert')
                    ->dateTime('d.m.Y H:i')
                    ->sortable(),
            ])
            ->filters([
                Tables\Filters\TernaryFilter::make('is_public')
                    ->label('Ã–ffentliche Einstellungen'),
            ])
            ->actions([
                Actions\\EditAction::make(),
            ])
            ->groups([
                'category' => Tables\Grouping\Group::make('key')
                    ->label('Kategorie')
                    ->getDescriptionFromRecordUsing(fn ($record) => explode('.', $record->key)[0])
            ]);
    }

    // Predefined System Settings
    public static function getDefaultSettings(): array
    {
        return [
            'platform.max_file_size' => '10485760', // 10MB
            'platform.allowed_image_types' => 'jpeg,jpg,png,webp',
            // Event-basierte Preisgestaltung
            'pricing.event_price_eur' => '9.99',
            'pricing.free_event_on_app_purchase' => 'true',
            'email.from_address' => 'noreply@fotobox-platform.de',
            'email.support_address' => 'support@fotobox-platform.de',
            'analytics.google_analytics_id' => '',
            'features.maintenance_mode' => 'false',
            'features.new_registrations' => 'true',
            'storage.default_disk' => 'local',
            'storage.cdn_url' => '',
        ];
    }
}
```

## ðŸ“Š **Super Admin Widgets**

### **Platform Statistics Widget:**
```php
<?php
// app/Filament/SuperAdmin/Widgets/PlatformStatsWidget.php

namespace App\Filament\SuperAdmin\Widgets;

use App\Models\{Tenant, Event, Photo, User};
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;

class PlatformStatsWidget extends BaseWidget
{
    protected function getStats(): array
    {
        return [
            Stat::make('Kunden mit Events', Tenant::whereHas('events')->count())
                ->description('Mindestens ein Event vorhanden')
                ->descriptionIcon('heroicon-m-building-office')
                ->color('success'),

            Stat::make('Gesamt Events', Event::count())
                ->description('Alle Events auf der Platform')
                ->descriptionIcon('heroicon-m-calendar-days')
                ->color('primary'),

            Stat::make('Gesamt Fotos', Photo::count())
                ->description('Hochgeladene Fotos')
                ->descriptionIcon('heroicon-m-photo')
                ->color('warning'),

            Stat::make('Monthly Recurring Revenue', 
                Tenant::where('subscription_status', 'active')
                    ->whereIn('plan_type', ['basic', 'premium'])
                    ->get()
                    ->sum(fn($tenant) => match($tenant->plan_type) {
                        'basic' => 29,
                        'premium' => 49,
                        default => 0
                    })
            )
                ->description('â‚¬ MRR')
                ->descriptionIcon('heroicon-m-currency-euro')
                ->color('success'),
        ];
    }
}
```

### **Revenue Chart Widget:**
```php
<?php
// app/Filament/SuperAdmin/Widgets/RevenueChartWidget.php

namespace App\Filament\SuperAdmin\Widgets;

use App\Models\EventPurchase;
use Filament\Widgets\ChartWidget;

class RevenueChartWidget extends ChartWidget
{
    protected static ?string $heading = 'Umsatz (Letzte 12 Monate)';

    protected function getData(): array
    {
        $data = EventPurchase::where('status', 'paid')
            ->where('created_at', '>=', now()->subYear())
            ->selectRaw('DATE_FORMAT(created_at, "%Y-%m") as month, SUM(amount) as revenue')
            ->groupBy('month')
            ->orderBy('month')
            ->get();

        return [
            'datasets' => [
                [
                    'label' => 'Umsatz (â‚¬)',
                    'data' => $data->pluck('revenue'),
                    'borderColor' => 'rgb(99, 102, 241)',
                    'backgroundColor' => 'rgba(99, 102, 241, 0.1)',
                ],
            ],
            'labels' => $data->pluck('month'),
        ];
    }

    protected function getType(): string
    {
        return 'line';
    }
}
```

## ðŸ” **Super Admin Middleware**

```php
<?php
// app/Http/Middleware/SuperAdminOnly.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SuperAdminOnly
{
    public function handle(Request $request, Closure $next)
    {
        if (!auth()->check() || auth()->user()->role !== 'super_admin') {
            abort(403, 'Access denied. Super Admin privileges required.');
        }

        return $next($request);
    }
}
```

## ðŸ¢ **Tenant Scoping Middleware**

```php
<?php
// app/Http/Middleware/TenantScope.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use App\Models\Tenant;

class TenantScope
{
    public function handle(Request $request, Closure $next)
    {
        if (auth()->user()->role === 'super_admin') {
            return $next($request); // Super Admin bypasses tenant scoping
        }

        $tenant = auth()->user()->tenant;
        
        if (!$tenant) {
            abort(403, 'No tenant associated with user.');
        }

        // Set global tenant scope
        config(['app.current_tenant' => $tenant]);
        
        return $next($request);
    }
}
```

## ðŸŽ¯ **Super Admin Navigation Struktur**

```
ðŸ  Dashboard
â”œâ”€â”€ ðŸ“Š Platform Overview
â”œâ”€â”€ ðŸ’° Revenue Summary
â””â”€â”€ ðŸš¨ System Alerts

ðŸ‘¥ Customer Management
â”œâ”€â”€ ðŸ¢ Tenants/Kunden
â”œâ”€â”€ ðŸ‘¤ User Management  
â”œâ”€â”€ ðŸ“ž Support Tickets
â””â”€â”€ ðŸ“§ Bulk Communications

ðŸ’³ Billing & Subscriptions
â”œâ”€â”€ ðŸ’° Subscription Plans
â”œâ”€â”€ ðŸ§¾ Invoices & Payments
â”œâ”€â”€ ðŸ“Š Revenue Analytics
â””â”€â”€ ðŸŽ Promotions & Discounts

âš™ï¸ Platform Configuration
â”œâ”€â”€ ðŸ”§ System Settings
â”œâ”€â”€ ðŸŽ¨ Global Themes
â”œâ”€â”€ # Hochzeits-Fotobox App - Project Requirements & Prompt (PRP)

## ðŸ“‹ Projekt-Ãœbersicht

**Projektname:** Emotionen-Fotobox fÃ¼r Deutsche Hochzeiten  
**Ziel:** Eine Progressive Web App (PWA), die HochzeitsgÃ¤ste spielerisch dazu motiviert, emotionale Fotos zu erstellen und zu teilen  
**Tech-Stack:** Laravel 12.x + Filament 4.x (Admin) + Vite + React + SQLite + Tailwind CSS  
> Versions as of 2025-09-01: Laravel 12 (latest 12.21.x), Filament 4 (stable). If you must stay on Filament v3 for compatibility, pin to 3.3.x.
**Zielgruppe:** Deutsche HochzeitsgÃ¤ste aller Altersgruppen  

## ðŸŽ¯ Kern-FunktionalitÃ¤ten

### 1. Emotions-basiertes Foto-System
- **6 Haupt-Emotionen:** Liebe ðŸ’•, Freude ðŸ˜‚, RÃ¼hrung ðŸ¥º, Nostalgie ðŸ“¸, Ãœberraschung ðŸ˜², Stolz ðŸ†
- **Aufgaben-Generator:** Pro Emotion 8-12 verschiedene Foto-Aufgaben
- **Schwierigkeitsgrade:** Easy (Einzelperson), Medium (Gruppe), Hard (Kreativ/Nachstellung)

### 2. Social Media-inspiriertes UI
- **Instagram Stories-Layout:** Horizontale Emotion-Bubbles
- **TikTok-Style Kamera:** Vollbild mit Overlay-Aufgaben
- **Feed-Design:** Kachel-Grid mit Like-Funktion
- **Swipe-Navigation:** Intuitive Bedienung

### 3. Real-time Features
- **Live-Photo-Feed:** Neue Fotos erscheinen sofort bei allen GÃ¤sten
- **Kollaborative Galerie:** Gemeinsame Fotosortierung nach Emotionen
- **Like-System:** Herzchen fÃ¼r beliebte Fotos

## ðŸ—ï¸ Technische Architektur

### Backend: Laravel 12.x (PHP 8.4)

#### Database Schema (SQLite)
```sql
-- Event Types (wedding, christmas, birthday, corporate, ...)
CREATE TABLE event_types (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,           -- JSON (translatable: {"de":"Hochzeit","en":"Wedding"})
    slug VARCHAR(100) UNIQUE NOT NULL,
    icon VARCHAR(64),
    settings JSON, -- e.g., {"palette": {...}}
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Events (Veranstaltungen)
CREATE TABLE events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,           -- JSON (translatable)
    date DATE NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    description TEXT,            -- JSON (translatable)
    settings JSON, -- Konfiguration fÃ¼r Emotionen, Farben etc.
    event_type_id INTEGER NOT NULL,
    is_active BOOLEAN DEFAULT 1,
    default_locale VARCHAR(5) DEFAULT 'de',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (event_type_id) REFERENCES event_types(id)
);

-- Emotionen-Kategorien (reusable; linked to event types via pivot)
CREATE TABLE emotions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,           -- JSON (translatable)
    icon VARCHAR(50) NOT NULL, -- Emoji oder Icon-Klasse
    color VARCHAR(7) NOT NULL, -- Hex-Farbe
    description TEXT,            -- JSON (translatable)
    sort_order INTEGER DEFAULT 0,
    is_active BOOLEAN DEFAULT 1
);

-- Emotionâ†”EventType Zuordnung (many-to-many)
CREATE TABLE emotion_event_type (
    emotion_id INTEGER NOT NULL,
    event_type_id INTEGER NOT NULL,
    PRIMARY KEY (emotion_id, event_type_id),
    FOREIGN KEY (emotion_id) REFERENCES emotions(id),
    FOREIGN KEY (event_type_id) REFERENCES event_types(id)
);

CREATE TABLE tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    emotion_id INTEGER NOT NULL,
    event_type_id INTEGER, -- optional: directly target a type
    title TEXT NOT NULL,          -- JSON (translatable)
    description TEXT NOT NULL,    -- JSON (translatable)
    difficulty ENUM('easy', 'medium', 'hard') DEFAULT 'easy',
    example_text TEXT,            -- JSON (translatable)
    sort_order INTEGER DEFAULT 0,
    is_active BOOLEAN DEFAULT 1,
    -- multi-tenant scoping additions (see Tenant-Defined Tasks section)
    tenant_id INTEGER,
    scope TEXT DEFAULT 'global', -- 'global' | 'tenant' | 'event'
    event_id INTEGER,
    FOREIGN KEY (emotion_id) REFERENCES emotions(id)
    -- optional FKs for scoping
    ,FOREIGN KEY (event_type_id) REFERENCES event_types(id)
    ,FOREIGN KEY (tenant_id) REFERENCES tenants(id)
    ,FOREIGN KEY (event_id) REFERENCES events(id)
);

-- Hochgeladene Fotos
CREATE TABLE photos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    event_id INTEGER NOT NULL,
    emotion_id INTEGER NOT NULL,
    task_id INTEGER,
    guest_name VARCHAR(255) NOT NULL,
    file_path VARCHAR(255) NOT NULL,
    thumbnail_path VARCHAR(255) NOT NULL,
    likes_count INTEGER DEFAULT 0,
    is_featured BOOLEAN DEFAULT 0, -- FÃ¼r Highlights
    metadata JSON, -- EXIF-Daten, GerÃ¤tinfo etc.
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (event_id) REFERENCES events(id),
    FOREIGN KEY (emotion_id) REFERENCES emotions(id),
    FOREIGN KEY (task_id) REFERENCES tasks(id)
);

-- Photo-Likes (fÃ¼r Gast-Interaktion)
CREATE TABLE photo_likes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    photo_id INTEGER NOT NULL,
    guest_name VARCHAR(255) NOT NULL,
    ip_address VARCHAR(45),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (photo_id) REFERENCES photos(id),
    UNIQUE(photo_id, guest_name, ip_address)
);
```

#### Laravel Models

**Event Model:**
```php

Event model additions (event type):
```php
// In App\Models\Event
protected $fillable = ["name","date","slug","description","settings","event_type_id","is_active"];
public function type(){ return $this->belongsTo(\App\Models\EventType::class, "event_type_id"); }
```

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Event extends Model
{
    protected $fillable = ['name', 'date', 'slug', 'description', 'settings', 'is_active'];
    
    protected $casts = [
        'date' => 'date',
        'settings' => 'array',
        'is_active' => 'boolean'
    ];

    public function photos()
    {
        return $this->hasMany(Photo::class)->latest();
    }
    
    public function getRouteKeyName()
    {
        return 'slug';
    }
}
```

**Photo Model:**
```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Photo extends Model
{
    protected $fillable = [
        'event_id', 'emotion_id', 'task_id', 'guest_name', 
        'file_path', 'thumbnail_path', 'metadata'
    ];
    
    protected $casts = [
        'metadata' => 'array'
    ];

    public function event()
    {
        return $this->belongsTo(Event::class);
    }
    
    public function emotion()
    {
        return $this->belongsTo(Emotion::class);
    }
    
    public function task()
    {
        return $this->belongsTo(Task::class);
    }
    
    public function likes()
    {
        return $this->hasMany(PhotoLike::class);
    }
}
```

#### API Routes (routes/api.php)
```php
<?php

use App\Http\Controllers\Api\{
    EventController,
    EmotionController, 
    TaskController,
    PhotoController,
    LiveFeedController
};

// Event-spezifische Routes (Ã¼ber Slug-Parameter)
Route::prefix('events/{event:slug}')->middleware(['throttle:60,1'])->group(function () {
    // Event-Details
    Route::get('/', [EventController::class, 'show']);
    
    // Emotionen laden (per Event-Typ gefiltert + universal)
    Route::get('/emotions', [EmotionController::class, 'index']);
    
    // Aufgaben pro Emotion
    Route::get('/tasks/{emotion}', [TaskController::class, 'getByEmotion']);
    Route::get('/tasks/random/{emotion}', [TaskController::class, 'getRandomTask']);
    
    // Foto-Management
    Route::get('/photos', [PhotoController::class, 'index']);
    Route::post('/photos', [PhotoController::class, 'store']);
    Route::get('/photos/{photo}', [PhotoController::class, 'show']);
    Route::post('/photos/{photo}/like', [PhotoController::class, 'toggleLike']);
    
    // Live-Feed fÃ¼r Real-time Updates
    Route::get('/feed/latest', [LiveFeedController::class, 'getLatest']);
    Route::get('/feed/stream', [LiveFeedController::class, 'stream']); // SSE
});

// Admin Routes (fÃ¼r Hochzeits-Setup)
Route::prefix('admin')->middleware(['auth:sanctum'])->group(function () {
    Route::apiResource('events', EventController::class);
    Route::apiResource('emotions', EmotionController::class);
    Route::apiResource('tasks', TaskController::class);
});
```

#### Controller-Beispiele

**PhotoController:**
```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\{Event, Photo, PhotoLike};
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;

class PhotoController extends Controller
{
    public function store(Request $request, Event $event)
    {
        $request->validate([
            'photo' => 'required|image|mimes:jpeg,png,jpg|max:10240', // 10MB
            'emotion_id' => 'required|exists:emotions,id',
            'task_id' => 'nullable|exists:tasks,id',
            'guest_name' => 'required|string|max:100|min:2'
        ]);

        $photo = $request->file('photo');
        $filename = uniqid('photo_') . '.' . $photo->getClientOriginalExtension();
        
        // Original-Bild speichern
        $originalPath = $photo->storeAs('photos', $filename, 'public');
        
        // Thumbnail erstellen (300x300 quadratisch)
        $thumbnailPath = 'thumbnails/' . $filename;
        $thumbnail = Image::make($photo)
            ->fit(300, 300, function ($constraint) {
                $constraint->upsize();
            })
            ->encode('jpg', 85);
        
        Storage::disk('public')->put($thumbnailPath, $thumbnail);

        // Metadata sammeln
        $metadata = [
            'original_name' => $photo->getClientOriginalName(),
            'size' => $photo->getSize(),
            'mime_type' => $photo->getMimeType(),
            'ip_address' => $request->ip(),
            'user_agent' => $request->userAgent()
        ];

        // In Datenbank speichern
        $photoRecord = Photo::create([
            'event_id' => $event->id,
            'emotion_id' => $request->emotion_id,
            'task_id' => $request->task_id,
            'guest_name' => $request->guest_name,
            'file_path' => $originalPath,
            'thumbnail_path' => $thumbnailPath,
            'metadata' => $metadata
        ]);

        // Response mit Relations
        $photoRecord->load(['emotion', 'task']);
        
        // Optional: Broadcasting fÃ¼r Live-Updates
        // broadcast(new PhotoUploaded($photoRecord));

        return response()->json([
            'success' => true,
            'data' => $photoRecord,
            'message' => 'Foto erfolgreich hochgeladen!'
        ], 201);
    }

    public function index(Request $request, Event $event)
    {
        $query = $event->photos()->with(['emotion', 'task', 'likes']);
        
        // Filter nach Emotion
        if ($request->has('emotion_id')) {
            $query->where('emotion_id', $request->emotion_id);
        }
        
        // Sortierung
        $sortBy = $request->get('sort', 'latest');
        switch ($sortBy) {
            case 'popular':
                $query->withCount('likes')->orderByDesc('likes_count');
                break;
            case 'oldest':
                $query->oldest();
                break;
            default:
                $query->latest();
        }
        
        $photos = $query->paginate(20);
        
        return response()->json($photos);
    }

    public function toggleLike(Request $request, Event $event, Photo $photo)
    {
        $request->validate([
            'guest_name' => 'required|string|max:100'
        ]);

        $like = PhotoLike::where([
            'photo_id' => $photo->id,
            'guest_name' => $request->guest_name,
            'ip_address' => $request->ip()
        ])->first();

        if ($like) {
            $like->delete();
            $photo->decrement('likes_count');
            $liked = false;
        } else {
            PhotoLike::create([
                'photo_id' => $photo->id,
                'guest_name' => $request->guest_name,
                'ip_address' => $request->ip()
            ]);
            $photo->increment('likes_count');
            $liked = true;
        }

        return response()->json([
            'liked' => $liked,
            'likes_count' => $photo->fresh()->likes_count
        ]);
    }
}
```

### Frontend: React + Vite PWA

Note: Event Type-aware UI
- Theme colors can come from `event_types.settings.palette` to skin headers/buttons per type.
- Emotion picker fetches type-filtered emotions via `/api/events/:slug/emotions`.
- Random task generator and gallery filters respect event type automatically via API.

#### Projekt-Struktur
```
resources/js/
â”œâ”€â”€ src/
â”‚   â”œâ”€â”€ components/
â”‚   â”‚   â”œâ”€â”€ Camera/
â”‚   â”‚   â”‚   â”œâ”€â”€ CameraCapture.jsx
â”‚   â”‚   â”‚   â”œâ”€â”€ PhotoPreview.jsx
â”‚   â”‚   â”‚   â””â”€â”€ TaskOverlay.jsx
â”‚   â”‚   â”œâ”€â”€ Emotions/
â”‚   â”‚   â”‚   â”œâ”€â”€ EmotionPicker.jsx
â”‚   â”‚   â”‚   â”œâ”€â”€ EmotionBubble.jsx
â”‚   â”‚   â”‚   â””â”€â”€ TaskGenerator.jsx
â”‚   â”‚   â”œâ”€â”€ Gallery/
â”‚   â”‚   â”‚   â”œâ”€â”€ PhotoGrid.jsx
â”‚   â”‚   â”‚   â”œâ”€â”€ PhotoModal.jsx
â”‚   â”‚   â”‚   â””â”€â”€ PhotoCard.jsx
â”‚   â”‚   â”œâ”€â”€ Layout/
â”‚   â”‚   â”‚   â”œâ”€â”€ Header.jsx
â”‚   â”‚   â”‚   â”œâ”€â”€ Navigation.jsx
â”‚   â”‚   â”‚   â””â”€â”€ LoadingSpinner.jsx
â”‚   â”‚   â””â”€â”€ Forms/
â”‚   â”‚       â”œâ”€â”€ GuestNameForm.jsx
â”‚   â”‚       â””â”€â”€ PhotoUpload.jsx
â”‚   â”œâ”€â”€ pages/
â”‚   â”‚   â”œâ”€â”€ GeneralLandingPage.jsx
â”‚   â”‚   â”œâ”€â”€ EventSpecificLandingPage.jsx
â”‚   â”‚   â”œâ”€â”€ HomePage.jsx
â”‚   â”‚   â”œâ”€â”€ CameraPage.jsx
â”‚   â”‚   â”œâ”€â”€ GalleryPage.jsx
â”‚   â”œâ”€â”€ hooks/
â”‚   â”‚   â”œâ”€â”€ useCamera.js
â”‚   â”‚   â”œâ”€â”€ usePhotoUpload.js
â”‚   â”‚   â”œâ”€â”€ useLocalStorage.js
â”‚   â”‚   â””â”€â”€ useRealTimeFeed.js
â”‚   â”œâ”€â”€ services/
â”‚   â”‚   â”œâ”€â”€ api.js
â”‚   â”‚   â”œâ”€â”€ imageUtils.js
â”‚   â”‚   â””â”€â”€ cacheManager.js
â”‚   â”œâ”€â”€ context/
â”‚   â”‚   â”œâ”€â”€ AppContext.jsx
â”‚   â”‚   â””â”€â”€ EventContext.jsx
â”‚   â””â”€â”€ utils/
â”‚       â”œâ”€â”€ constants.js
â”‚       â”œâ”€â”€ helpers.js
â”‚       â””â”€â”€ validation.js
```

#### Vite Configuration (vite.config.js)
```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
import path from 'path'

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'prompt',
      includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'safari-pinned-tab.svg'],
      manifest: {
        name: 'Hochzeits-Fotobox',
        short_name: 'Fotobox',
        description: 'Emotionale Hochzeitsfotos fÃ¼r unvergessliche Momente',
        theme_color: '#ff6b9d',
        background_color: '#ffffff',
        display: 'standalone',
        orientation: 'portrait-primary',
        start_url: '/',
        scope: '/',
        icons: [
          {
            src: '/icons/pwa-192x192.png',
            sizes: '192x192',
            type: 'image/png'
          },
          {
            src: '/icons/pwa-512x512.png',
            sizes: '512x512',
            type: 'image/png'
          }
        ]
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
        runtimeCaching: [
          {
            urlPattern: /^\/api\//,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              expiration: {
                maxEntries: 100,
                maxAgeSeconds: 60 * 60 * 24 // 24 hours
              }
            }
          },
          {
            urlPattern: /\.(png|jpg|jpeg|svg|gif)$/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'images-cache',
              expiration: {
                maxEntries: 200,
                maxAgeSeconds: 60 * 60 * 24 * 7 // 1 week
              }
            }
          }
        ]
      }
    })
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './resources/js/src')
    }
  },
  build: {
    outDir: 'public/build',
    emptyOutDir: true,
    manifest: true,
    rollupOptions: {
      input: 'resources/js/app.jsx'
    }
  },
  server: {
    host: true,
    hmr: {
      host: 'localhost'
    }
  }
})
```

#### React Haupt-Komponenten

**App.jsx (Routing & Context)**
```jsx
import React from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { EventProvider } from '@/context/EventContext'
import { AppProvider } from '@/context/AppContext'

// Pages
import EventLanding from '@/pages/EventLanding'
import HomePage from '@/pages/HomePage'
import CameraPage from '@/pages/CameraPage'
import GalleryPage from '@/pages/GalleryPage'

// Layout
import Layout from '@/components/Layout/Layout'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      cacheTime: 1000 * 60 * 30, // 30 minutes
      retry: 2
    }
  }
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AppProvider>
        <Router>
          <Routes>
            {/* Event Selection */}
            <Route path="/" element={<EventLanding />} />
            
            {/* Main App (requires event context) */}
            <Route path="/event/:eventSlug/*" element={
              <EventProvider>
                <Layout>
                  <Routes>
                    <Route index element={<HomePage />} />
                    <Route path="camera" element={<CameraPage />} />
                    <Route path="camera/:emotionId" element={<CameraPage />} />
                    <Route path="gallery" element={<GalleryPage />} />
                    <Route path="gallery/:emotionId" element={<GalleryPage />} />
                    <Route path="*" element={<Navigate to="../" replace />} />
                  </Routes>
                </Layout>
              </EventProvider>
            } />
          </Routes>
        </Router>
      </AppProvider>
    </QueryClientProvider>
  )
}

export default App
```


**GeneralLandingPage.jsx**
```jsx
import React from 'react';
import { useNavigate } from 'react-router-dom';

function GeneralLandingPage() {
  const navigate = useNavigate();

  const handleQRScan = () => {
    // Trigger device camera to scan QR code
    // This would use a library like jsQR or react-qr-reader
    console.log("Opening QR scanner...");
  };

  const handleManualEntry = () => {
    // Open modal for manual code entry
    const code = prompt("Bitte gib den Event-Code ein:");
    if (code) {
      // Validate and redirect to event
      navigate(`/event/${code}`);
    }
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-pink-50 via-white to-purple-50 flex flex-col">
      {/* Minimalist Header */}
      <header className="p-4 flex justify-between items-center">
        <div className="text-sm text-gray-500">Event Date</div>
        <div className="text-sm text-gray-500">Branding Logo</div>
      </header>

      {/* Main Content */}
      <main className="flex-grow flex flex-col items-center justify-center px-4">
        <div className="text-center mb-8">
          <h1 className="text-3xl font-bold text-gray-800 mb-2">Willkommen bei der Fotobox! ðŸŽ‰</h1>
          <p className="text-gray-600">Dein SchlÃ¼ssel zu unvergesslichen Hochzeitsmomenten.</p>
        </div>

        {/* Central Card for Actions */}
        <div className="bg-white rounded-xl shadow-lg p-6 w-full max-w-md">
          {/* QR Code Scanner Emphasis */}
          <div className="mb-6">
            <div className="bg-gray-200 border-2 border-dashed rounded-xl w-32 h-32 mx-auto mb-4" />
            <button
              onClick={handleQRScan}
              className="w-full py-3 px-4 bg-pink-500 text-white font-semibold rounded-lg shadow-md hover:bg-pink-600 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:ring-opacity-75 transition-all"
            >
              QR-Code scannen
            </button>
          </div>

          {/* Manual Code Entry (Fallback) */}
          <div className="text-center">
            <button
              onClick={handleManualEntry}
              className="text-pink-500 hover:text-pink-700 font-medium"
            >
              Oder Code manuell eingeben
            </button>
          </div>
        </div>
      </main>

      {/* Minimalist Footer */}
      <footer className="p-4 text-center text-sm text-gray-500">
        Â© {new Date().getFullYear()} Emotionen-Fotobox
      </footer>
    </div>
  );
}

export default GeneralLandingPage;
```

**EventSpecificLandingPage.jsx**
```jsx
import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';

function EventSpecificLandingPage() {
  const { eventSlug } = useParams();
  const navigate = useNavigate();
  const [guestName, setGuestName] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (guestName.trim()) {
      // Store guest name in context or localStorage
      localStorage.setItem('guestName', guestName);
      // Redirect to main event page
      navigate(`/event/${eventSlug}`);
    }
  };

  return (
    <div className="min-h-screen bg-gradient-to-b from-pink-100 to-purple-100 flex flex-col">
      {/* Header */}
      <header className="p-4 flex justify-between items-start">
        <div className="text-2xl">â¤ï¸ðŸ“¸</div> {/* Heart with Camera Icon */}
        <div className="text-lg font-sans text-gray-700">01.01.2025</div> {/* Event Date */}
      </header>

      {/* Main Content - Vertically Centered */}
      <main className="flex-grow flex flex-col items-center justify-center px-4">
        {/* Title Section */}
        <div className="text-center mb-8">
          <p className="text-gray-700 uppercase tracking-wider font-sans mb-1">HOCHZEIT VON</p>
          <h1 className="text-4xl font-bold italic font-handwriting text-gray-800 mb-2">MARIE & LUKAS</h1>
          <p className="text-gray-600 text-sm font-sans">Fangt die schÃ¶nsten Momente ein!</p>
        </div>

        {/* Input Section */}
        <div className="bg-white bg-opacity-70 backdrop-blur-sm rounded-xl shadow-lg p-6 w-full max-w-md">
          <p className="text-center text-gray-700 mb-4 font-medium">Bereit fÃ¼r Emotionen?</p>
          <form onSubmit={handleSubmit}>
            <input
              type="text"
              placeholder="Dein Name (z.B. Anna)"
              value={guestName}
              onChange={(e) => setGuestName(e.target.value)}
              className="w-full p-3 mb-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-300"
              required
            />
            <button
              type="submit"
              className="w-full py-3 px-4 bg-[#F85B73] text-white font-semibold rounded-lg shadow-md hover:bg-[#e05067] focus:outline-none focus:ring-2 focus:ring-pink-400 focus:ring-opacity-75 transition-all transform hover:scale-105 active:scale-95"
            >
              LETâ€™S GO! ðŸŽ‰
            </button>
          </form>
        </div>
      </main>

      {/* Footer */}
      <footer className="p-6 text-center">
        <div className="text-2xl mb-1">â¤ï¸</div>
        <p className="text-gray-700 font-sans">Emotionen-Fotobox</p>
      </footer>
    </div>
  );
}

export default EventSpecificLandingPage;
```

**SettingsSheet.jsx (gear icon â†’ bottom sheet)**
```jsx
import React from "react"
import { Link } from "react-router-dom"
import { useTranslation } from "react-i18next"

export default function SettingsSheet({ open, onClose, onLangChange, currentLang }) {
  const { t } = useTranslation()
  if (!open) return null
  return (
    <div className="fixed inset-0 z-50">
      <div className="absolute inset-0 bg-black/40" onClick={onClose} />
      <div className="absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4 shadow-xl">
        <div className="mx-auto w-full max-w-md">
          <div className="flex items-center justify-between mb-3">
            <h3 className="text-lg font-semibold">{t("ui:settings")}</h3>
            <button onClick={onClose} className="text-gray-500">âœ•</button>
          </div>
          <div className="space-y-4">
            <div>
              <div className="text-sm text-gray-500 mb-1">{t("ui:language")}</div>
              <div className="flex gap-2">
                <button onClick={() => onLangChange("de")} className={`px-3 py-1 rounded border ${currentLang==="de"?"bg-gray-900 text-white":""}`}>DE</button>
                <button onClick={() => onLangChange("en")} className={`px-3 py-1 rounded border ${currentLang==="en"?"bg-gray-900 text-white":""}`}>EN</button>
              </div>
            </div>
            <div>
              <div className="text-sm text-gray-500 mb-1">{t("ui:legal")}</div>
              <ul className="space-y-2">
                <li><Link to="/legal/imprint" onClick={onClose} className="text-pink-600">Impressum</Link></li>
                <li><Link to="/legal/privacy" onClick={onClose} className="text-pink-600">DatenschutzerklÃ¤rung</Link></li>
                <li><Link to="/legal/terms" onClick={onClose} className="text-pink-600">AGB</Link></li>
              </ul>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}
```

**LegalPage.jsx (renders platform-managed markdown)**
```jsx
import React from "react"
import { useParams } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
import ReactMarkdown from "react-markdown"
import rehypeSanitize from "rehype-sanitize"
import { useTranslation } from "react-i18next"

export default function LegalPage(){
  const { slug } = useParams()
  const { i18n } = useTranslation()
  const lang = i18n.language || "de"

  const { data, isLoading, error } = useQuery({
    queryKey: ["legal", slug, lang],
    queryFn: async () => {
      const res = await fetch(`/api/legal/${slug}?lang=${lang}`)
      if(!res.ok) throw new Error("Failed to load legal page")
      return res.json()
    }
  })

  if (isLoading) return <div className="p-4">Loadingâ€¦</div>
  if (error) return <div className="p-4 text-red-600">Error loading content.</div>

  return (
    <div className="max-w-3xl mx-auto p-4 prose prose-pink">
      <h1 className="mb-2">{data.title}</h1>
      <div className="text-sm text-gray-500 mb-6">
        {data.updated_at ? new Date(data.updated_at).toLocaleDateString(lang) : null}
      </div>
      <ReactMarkdown rehypePlugins={[rehypeSanitize]}>
        {data.body_markdown || ""}
      </ReactMarkdown>
    </div>
  )
}
```
**Header.jsx (gear button opens SettingsSheet and changes language)**
```jsx
import React from "react"
import { Settings } from "lucide-react"
import { useTranslation } from "react-i18next"
import SettingsSheet from "@/components/Layout/SettingsSheet"

export default function Header() {
  const { i18n } = useTranslation()
  const [open, setOpen] = React.useState(false)

  const onLangChange = (lng) => {
    i18n.changeLanguage(lng)
    localStorage.setItem("lang", lng)
    setOpen(false)
  }

  return (
    <header className="sticky top-0 z-40 bg-white/70 backdrop-blur border-b">
      <div className="max-w-4xl mx-auto px-4 h-12 flex items-center justify-between">
        <div className="font-semibold">Fotobox</div>
        <button
          aria-label="Settings"
          onClick={() => setOpen(true)}
          className="p-2 rounded-full hover:bg-gray-100"
        >
          <Settings className="w-5 h-5 text-gray-700" />
        </button>
      </div>
      <SettingsSheet
        open={open}
        onClose={() => setOpen(false)}
        onLangChange={onLangChange}
        currentLang={i18n.language || "de"}
      />
    </header>
  )
}
```
## ðŸŒ Internationalization (i18n)

### Goals & Locales
- Support multi-lingual content across admin, API, and PWA.
- Initial locales: `de` (default) and `en`; easily extendable.

### Backend (Laravel 12 + PHP 8.4)
- Dependency: `spatie/laravel-translatable` for JSON translations.
- App config (`config/app.php`): set `locale`, `fallback_locale`, and optionally `APP_SUPPORTED_LOCALES=de,en`.

#### i18n Data Model
- Store translatable fields as JSON:
  - `event_types.name`
  - `events.name`, `events.description`
  - `emotions.name`, `emotions.description`
  - `tasks.title`, `tasks.description`, `tasks.example_text`

#### Model Setup
```php
<?php
use Spatie\Translatable\HasTranslations;

class Task extends Model {
    use HasTranslations;
    public $translatable = ['title','description','example_text'];
}

class Emotion extends Model {
    use HasTranslations;
    public $translatable = ['name','description'];
}

class EventType extends Model {
    use HasTranslations;
    public $translatable = ['name'];
}

class Event extends Model {
    use HasTranslations;
    public $translatable = ['name','description'];
}
```

## ðŸ“œ Legal Content Management (Impressum/Privacy/AGB)

### Schema
```sql
CREATE TABLE legal_pages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tenant_id INTEGER NULL,                 -- null = platform default; future: tenant override
    slug VARCHAR(32) NOT NULL,              -- 'imprint' | 'privacy' | 'terms' | 'custom'
    title TEXT NOT NULL,                    -- JSON translatable: {"de":"Impressum","en":"Imprint"}
    body_markdown TEXT NOT NULL,            -- JSON translatable; Markdown content
    locale_fallback VARCHAR(5) DEFAULT 'de',
    version INTEGER DEFAULT 1,
    effective_from TIMESTAMP NULL,
    is_published BOOLEAN DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(slug, tenant_id, version),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
```

### Filament v4 â€” LegalPageResource (Super Admin)
```php
<?php
// app/Filament/SuperAdmin/Resources/LegalPageResource.php

namespace App\Filament\SuperAdmin\Resources;

use App\Models\LegalPage;
use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables\Table;
use Filament\Forms\Form; use Filament\Schema as Schema; use Filament\Actions;

class LegalPageResource extends Resource
{
    protected static ?string $model = LegalPage::class;
    protected static ?string $navigationIcon = 'heroicon-o-document-text';
    protected static ?string $navigationGroup = 'Legal & Compliance';
    protected static ?string $label = 'Legal Pages';

    public static function form(Form $form): Form
    {
        return $form->schema([
            Schema\Components\Select::make('slug')
                ->label('Type')->options([
                    'imprint' => 'Impressum',
                    'privacy' => 'DatenschutzerklÃ¤rung',
                    'terms' => 'AGB',
                    'custom' => 'Custom',
                ])->required(),

            Schema\Components\Tabs::make('Translations')->tabs([
                Schema\Components\Tabs\Tab::make('Deutsch')->schema([
                    Schema\Components\TextInput::make('title.de')->label('Titel (DE)')->required(),
                    Schema\Components\Textarea::make('body_markdown.de')->label('Inhalt (DE)')->rows(12)->required(),
                ]),
                Schema\Components\Tabs\Tab::make('English')->schema([
                    Schema\Components\TextInput::make('title.en')->label('Title (EN)')->required(),
                    Schema\Components\Textarea::make('body_markdown.en')->label('Content (EN)')->rows(12)->required(),
                ]),
            ])->columnSpanFull(),

            Schema\Components\Grid::make(3)->schema([
                Schema\Components\TextInput::make('version')->numeric()->default(1),
                Schema\Components\DateTimePicker::make('effective_from'),
                Schema\Components\Toggle::make('is_published')->label('Published'),
            ]),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\BadgeColumn::make('slug')->label('Type')->colors([
                    'primary' => 'imprint','success'=>'privacy','warning'=>'terms','gray'=>'custom'
                ]),
                Tables\Columns\TextColumn::make('title')->label('Title')->formatStateUsing(fn($r)=>$r->getTranslation('title', app()->getLocale()))->limit(40),
                Tables\Columns\TextColumn::make('version')->sortable(),
                Tables\Columns\IconColumn::make('is_published')->boolean(),
                Tables\Columns\TextColumn::make('updated_at')->dateTime('d.m.Y H:i')->sortable(),
            ])
            ->headerActions([
                Actions\CreateAction::make(),
            ])
            ->recordActions([
                Actions\EditAction::make(),
                Actions\Action::make('preview')->label('Preview')->url(fn($r)=>url("/legal/{$r->slug}?lang=".app()->getLocale()))->openUrlInNewTab(),
                Actions\DeleteAction::make(),
            ]);
    }
}
```

### API (public)
```php
// routes/api.php
use App\Http\Controllers\Api\LegalController;
Route::get('/legal/{slug}', [LegalController::class, 'show']);
```

```php
<?php
// app/Http/Controllers/Api/LegalController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\LegalPage;
use Illuminate\Http\Request;

class LegalController extends Controller
{
    public function show(Request $request, string $slug)
    {
        $locale = app()->getLocale();
        $page = LegalPage::query()
            ->where('slug', $slug)
            ->where('is_published', true)
            ->orderByDesc('version')
            ->firstOrFail();

        return response()->json([
            'slug' => $page->slug,
            'title' => $page->getTranslation('title', $locale),
            'body_markdown' => $page->getTranslation('body_markdown', $locale),
            'version' => $page->version,
            'effective_from' => $page->effective_from,
            'updated_at' => $page->updated_at,
            'locale' => $locale,
        ]);
    }
}
```

### PWA UX
- Header gear icon opens a Settings sheet with:
  - Language: DE/EN toggle
  - Legal: Impressum, DatenschutzerklÃ¤rung, AGB (links)
- Footer links (public pages): â€œImpressum Â· Datenschutz Â· AGBâ€ (DE labels in DE locale) for easy/direct access.
- Routes: `/legal/imprint`, `/legal/privacy`, `/legal/terms` â†’ render via API.

React components (sketch):
```jsx
// routes
<Route path="/legal/:slug" element={<LegalPage />} />

// components/SettingsSheet.jsx (triggered by gear button)
// shows language selector and links to /legal/... routes
```

Workbox caching (vite-plugin-pwa): add runtime cache for `/api/legal/*` (CacheFirst with revalidate).

Compliance (DE): ensure â€œImpressumâ€ is labeled in German and reachable within â‰¤2 clicks; show last updated date; include provider details (name, address, contact, VAT/registry) in content.

#### Locale Resolution (API)
- Precedence: `?lang` â†’ `Accept-Language` â†’ `event.default_locale` â†’ fallback.
- Set `Content-Language` header.
```php
class SetLocale { /* see middleware example above */ }
```

### Admin (Filament v4) Forms
- Use Tabs for per-locale inputs (Spatie accepts arrays):
```php
Schema\Components\Tabs::make('Translations')->tabs([
  Schema\Components\Tabs\Tab::make('Deutsch')->schema([
    Schema\Components\TextInput::make('title.de')->label('Titel (DE)')->required(),
    Schema\Components\Textarea::make('description.de')->label('Beschreibung (DE)')->rows(3),
    Schema\Components\TextInput::make('example_text.de')->label('Beispiel (DE)'),
  ]),
  Schema\Components\Tabs\Tab::make('English')->schema([
    Schema\Components\TextInput::make('title.en')->label('Title (EN)'),
    Schema\Components\Textarea::make('description.en')->label('Description (EN)')->rows(3),
    Schema\Components\TextInput::make('example_text.en')->label('Example (EN)'),
  ]),
])
```

### CSV (Localized Fields)
- Extra headers allowed: `title:de`, `title:en`, `description:de`, `description:en`, `example_text:de`, `example_text:en`.
- If only base columns exist, treat them as the active import locale.
```csv
emotion,title:de,title:en,description:de,description:en,difficulty,is_active,scope,event_type
Freude,Sprung-Foto,Jump Photo,Alle springen!,Everyone jump!,medium,1,tenant,corporate
```

### Frontend (React PWA)
- Dependencies: `i18next`, `react-i18next`, `i18next-browser-languagedetector`.
- Initialize in `resources/js/src/i18n.js` and pass locale to API via `?lang=`.

**HomePage.jsx (Emotions-Picker)**
```jsx
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { useEvent } from '@/context/EventContext'
import { useApp } from '@/context/AppContext'
import EmotionBubble from '@/components/Emotions/EmotionBubble'
import PhotoGrid from '@/components/Gallery/PhotoGrid'
import LoadingSpinner from '@/components/Layout/LoadingSpinner'

function HomePage() {
  const navigate = useNavigate()
  const { eventSlug, apiService } = useEvent()
  const { guestName, setGuestName } = useApp()

  // Emotionen laden
  const { data: emotions, isLoading: emotionsLoading } = useQuery({
    queryKey: ['emotions', eventSlug],
    queryFn: () => apiService.getEmotions(),
    enabled: !!eventSlug
  })

  // Neueste Fotos laden
  const { data: recentPhotos, isLoading: photosLoading } = useQuery({
    queryKey: ['photos', eventSlug, 'recent'],
    queryFn: () => apiService.getPhotos({ limit: 8, sort: 'latest' }),
    enabled: !!eventSlug,
    refetchInterval: 30000 // Alle 30 Sekunden aktualisieren
  })

  const handleEmotionClick = (emotionId) => {
    if (!guestName) {
      // Gast-Namen abfragen wenn noch nicht gesetzt
      const name = prompt('Wie sollen wir dich nennen?', '')
      if (name?.trim()) {
        setGuestName(name.trim())
      } else {
        return
      }
    }
    navigate(`/event/${eventSlug}/camera/${emotionId}`)
  }

  if (emotionsLoading) {
    return <LoadingSpinner message="Emotionen werden geladen..." />
  }

  return (
    <div className="min-h-screen bg-gradient-to-br from-pink-50 via-white to-purple-50 bg-[url('/images/wedding-lights-background.svg')] bg-cover bg-center bg-no-repeat">
      {/* Header */}
      <div className="px-4 py-6 text-center">
        <h1 className="text-3xl font-bold text-gray-800 mb-2">
          Willkommen zur Fotobox! ðŸ“¸
        </h1>
        {guestName && (
          <p className="text-sm text-pink-600 mt-2">
            Hallo, {guestName}! ðŸ‘‹
          </p>
        )}
      </div>

      {/* Emotions-Picker */}
      <div className="px-4 mb-8">
        <div className="flex overflow-x-auto pb-4 gap-4 px-2">
          {emotions?.map((emotion) => (
            <EmotionBubble
              key={emotion.id}
              emotion={emotion}
              onClick={() => handleEmotionClick(emotion.id)}
              className="flex-shrink-0"
            />
          ))}
        </div>
      </div>

      {/* "Alle anzeigen" Button */}
      <div className="px-4 mb-8 text-center">
        <button
          onClick={() => navigate(`/event/${eventSlug}/gallery`)}
          className="text-pink-600 text-sm font-medium hover:text-pink-700"
        >
          Alle anzeigen â†’
        </button>
      </div>

      {/* Recent Photos Feed */}
      <div className="px-4">
        <h2 className="text-xl font-semibold text-gray-700 mb-4">
          Neueste Momente
        </h2>
        
        {photosLoading ? (
          <div className="grid grid-cols-2 gap-4">
            {[...Array(4)].map((_, i) => (
              <div key={i} className="aspect-square bg-gray-200 rounded-lg animate-pulse" />
            ))}
          </div>
        ) : (
          <PhotoGrid
            photos={recentPhotos?.data || []}
            columns={2}
            showEmotion={true}
          />
        )}
      </div>
    </div>
  )
}

export default HomePage
```

**CameraPage.jsx (Foto-Aufnahme)**
```jsx
import React, { useState, useRef, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useEvent } from '@/context/EventContext'
import { useApp } from '@/context/AppContext'
import CameraCapture from '@/components/Camera/CameraCapture'
import TaskOverlay from '@/components/Camera/TaskOverlay'
import PhotoPreview from '@/components/Camera/PhotoPreview'

function CameraPage() {
  const { emotionId } = useParams()
  const navigate = useNavigate()
  const { eventSlug, apiService } = useEvent()
  const { guestName } = useApp()
  const queryClient = useQueryClient()
  
  const [capturedPhoto, setCapturedPhoto] = useState(null)
  const [currentTask, setCurrentTask] = useState(null)
  const cameraRef = useRef(null)

  // Aktueller Task laden
  const { data: tasks, refetch: refetchTasks } = useQuery({
    queryKey: ['tasks', eventSlug, emotionId],
    queryFn: () => apiService.getRandomTask(emotionId),
    enabled: !!emotionId && !!eventSlug,
    onSuccess: (data) => {
      if (data && !currentTask) {
        setCurrentTask(data)
      }
    }
  })

  // Foto-Upload Mutation
  const uploadMutation = useMutation({
    mutationFn: (photoData) => apiService.uploadPhoto(photoData),
    onSuccess: () => {
      // Cache invalidieren fÃ¼r sofortige Updates
      queryClient.invalidateQueries(['photos', eventSlug])
      
      // ZurÃ¼ck zur Startseite
      navigate(`/event/${eventSlug}`)
    },
    onError: (error) => {
      console.error('Upload failed:', error)
      alert('Upload fehlgeschlagen. Bitte versuche es erneut.')
    }
  })

  const handleCapture = useCallback((photoBlob) => {
    setCapturedPhoto(photoBlob)
  }, [])

  const handleRetake = useCallback(() => {
    setCapturedPhoto(null)
  }, [])

  const handleUpload = useCallback(() => {
    if (!capturedPhoto || !guestName || !currentTask) return

    const formData = new FormData()
    formData.append('photo', capturedPhoto, 'photo.jpg')
    formData.append('emotion_id', emotionId)
    formData.append('task_id', currentTask.id)
    formData.append('guest_name', guestName)

    uploadMutation.mutate(formData)
  }, [capturedPhoto, guestName, emotionId, currentTask, uploadMutation])

  const handleNewTask = useCallback(() => {
    refetchTasks().then((result) => {
      if (result.data) {
        setCurrentTask(result.data)
      }
    })
  }, [refetchTasks])

  // Redirect wenn kein Gast-Name gesetzt
  React.useEffect(() => {
    if (!guestName) {
      navigate(`/event/${eventSlug}`)
    }
  }, [guestName, navigate, eventSlug])

  if (!currentTask) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-center">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-pink-600 mx-auto mb-4"></div>
          <p>Aufgabe wird geladen...</p>
        </div>
      </div>
    )
  }

  return (
    <div className="relative h-screen bg-black overflow-hidden">
      {!capturedPhoto ? (
        <>
          {/* Live-Kamera */}
          <CameraCapture
            ref={cameraRef}
            onCapture={handleCapture}
          />
          
          {/* Task-Overlay */}
          <TaskOverlay
            task={currentTask}
            onNewTask={handleNewTask}
            onBack={() => navigate(`/event/${eventSlug}`)}
          />
        </>
      ) : (
        /* Foto-Vorschau */
        <PhotoPreview
          photo={capturedPhoto}
          task={currentTask}
          onRetake={handleRetake}
          onUpload={handleUpload}
          isUploading={uploadMutation.isLoading}
        />
      )}
    </div>
  )
}

export default CameraPage
```

#### PWA Service Worker Features

**Offline-FunktionalitÃ¤t:**
```javascript
// public/sw.js (Custom Service Worker)
const CACHE_NAME = 'fotobox-v1'
const OFFLINE_URL = '/offline.html'

// Offline-Queue fÃ¼r Photo-Uploads
class PhotoUploadQueue {
  constructor() {
    this.queue = []
    this.isProcessing = false
  }

  async add(photoData) {
    this.queue.push(photoData)
    await this.process()
  }

  async process() {
    if (this.isProcessing || this.queue.length === 0) return
    
    this.isProcessing = true
    
    while (this.queue.length > 0) {
      const photoData = this.queue.shift()
      
      try {
        const response = await fetch(photoData.url, {
          method: 'POST',
          body: photoData.formData
        })
        
        if (response.ok) {
          // Upload erfolgreich - Cache invalidieren
          await caches.delete('api-cache')
          
          // Client benachrichtigen
          self.clients.matchAll().then(clients => {
            clients.forEach(client => {
              client.postMessage({
                type: 'PHOTO_UPLOADED',
                data: photoData
              })
            })
          })
        } else {
          // Fehlgeschlagen - zurÃ¼ck in die Queue
          this.queue.unshift(photoData)
          break
        }
      } catch (error) {
        // Netzwerkfehler - zurÃ¼ck in die Queue
        this.queue.unshift(photoData)
        break
      }
    }
    
    this.isProcessing = false
  }
}

const uploadQueue = new PhotoUploadQueue()

// Network-first fÃ¼r API-Calls
self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          // Response klonen fÃ¼r Cache
          const responseClone = response.clone()
          caches.open(CACHE_NAME).then(cache => {
            cache.put(event.request, responseClone)
          })
          return response
        })
        .catch(() => {
          // Fallback auf Cache
          return caches.match(event.request)
        })
    )
  }
})
```

## ðŸŽ¨ UI/UX Design-Spezifikationen

### Farbschema
```javascript
// tailwind.config.js - Custom Colors
module.exports = {
  theme: {
    extend: {
      colors: {
        emotions: {
          love: '#ff6b9d',      // Liebe - Pink
          joy: '#ffd93d',       // Freude - Gelb  
          touched: '#6bcf7f',   // RÃ¼hrung - GrÃ¼n
          nostalgia: '#a78bfa', // Nostalgie - Lila
          surprise: '#fb7185',   // Ãœberraschung - Rosa
          pride: '#34d399'      // Stolz - TÃ¼rkis
        },
        wedding: {
          primary: '#ff6b9d',
          secondary: '#a78bfa',
          accent: '#ffd93d',
          neutral: '#6b7280',
          success: '#10b981',
          error: '#ef4444'
        }
      },
      fontFamily: {
        'wedding': ['Poppins', 'Inter', 'sans-serif']
      },
      animation: {
        'bounce-soft': 'bounce 2s infinite',
        'pulse-slow': 'pulse 3s infinite',
        'wiggle': 'wiggle 1s ease-in-out infinite',
        'fade-in': 'fadeIn 0.5s ease-in-out',
        'slide-up': 'slideUp 0.3s ease-out'
      }
    }
  }
}
```

### Responsive Breakpoints
- **Mobile First:** 320px - 768px (Hauptzielgruppe)
- **Tablet:** 768px - 1024px (Landscape-Modus)
- **Desktop:** 1024px+ (Admin-Interface)

### Gestaltungs-Prinzipien
1. **Thumb-Friendly:** Alle wichtigen Buttons in Daumen-Reichweite
2. **High Contrast:** Gute Lesbarkeit auch bei schlechtem Licht
3. **Emotional Colors:** Jede Emotion hat eigene Farb-IdentitÃ¤t
4. **Micro-Animations:** Subtile Feedback-Animationen
5. **Progressive Disclosure:** KomplexitÃ¤t schrittweise aufbauen

## ðŸ“± Komponenten-Spezifikationen

### EmotionBubble Component
```jsx
import React from 'react'
import { motion } from 'framer-motion'

const EmotionBubble = ({ emotion, onClick, isActive, className }) => {
  return (
    <motion.div
      whileTap={{ scale: 0.95 }}
      whileHover={{ scale: 1.05 }}
      className={`
        relative min-w-[100px] h-[100px] rounded-full
        flex flex-col items-center justify-center
        cursor-pointer transition-all duration-300
        shadow-lg hover:shadow-xl
        ${isActive ? 'ring-4 ring-offset-2' : ''}
        ${className}
      `}
      style={{ 
        backgroundColor: emotion.color,
        ringColor: emotion.color 
      }}
      onClick={() => onClick(emotion.id)}
    >
      {/* Emoji Icon */}
      <span className="text-2xl mb-1">{emotion.icon}</span>
      
      {/* Name */}
      <span className="text-xs font-medium text-white text-center px-2">
        {emotion.name}
      </span>
      
      {/* Pulse Animation fÃ¼r aktive Emotion */}
      {isActive && (
        <motion.div
          className="absolute inset-0 rounded-full border-2 border-white"
          animate={{ scale: [1, 1.2, 1], opacity: [0.7, 0, 0.7] }}
          transition={{ repeat: Infinity, duration: 2 }}
        />
      )}
    </motion.div>
  )
}
```

### CameraCapture Component
```jsx
import React, { useRef, useEffect, useState } from 'react'
import { Camera, RotateCcw, Zap, ZapOff } from 'lucide-react'

const CameraCapture = React.forwardRef(({ onCapture }, ref) => {
  const videoRef = useRef(null)
  const canvasRef = useRef(null)
  const [stream, setStream] = useState(null)
  const [hasFlash, setHasFlash] = useState(false)
  const [isFlashOn, setIsFlashOn] = useState(false)
  const [facingMode, setFacingMode] = useState('user') // 'user' oder 'environment'

  // Kamera initialisieren
  useEffect(() => {
    startCamera()
    return () => {
      stopCamera()
    }
  }, [facingMode])

  const startCamera = async () => {
    try {
      const constraints = {
        video: {
          facingMode: facingMode,
          width: { ideal: 1080 },
          height: { ideal: 1920 }
        }
      }

      const mediaStream = await navigator.mediaDevices.getUserMedia(constraints)
      setStream(mediaStream)
      
      if (videoRef.current) {
        videoRef.current.srcObject = mediaStream
      }

      // Flash-UnterstÃ¼tzung prÃ¼fen
      const videoTrack = mediaStream.getVideoTracks()[0]
      const capabilities = videoTrack.getCapabilities()
      setHasFlash(capabilities.torch === true)

    } catch (error) {
      console.error('Kamera-Zugriff fehlgeschlagen:', error)
      alert('Kamera-Zugriff nicht mÃ¶glich. Bitte Berechtigungen prÃ¼fen.')
    }
  }

  const stopCamera = () => {
    if (stream) {
      stream.getTracks().forEach(track => track.stop())
      setStream(null)
    }
  }

  const toggleFlash = async () => {
    if (!hasFlash || !stream) return

    const videoTrack = stream.getVideoTracks()[0]
    try {
      await videoTrack.applyConstraints({
        advanced: [{ torch: !isFlashOn }]
      })
      setIsFlashOn(!isFlashOn)
    } catch (error) {
      console.error('Flash toggle fehlgeschlagen:', error)
    }
  }

  const switchCamera = () => {
    setFacingMode(prev => prev === 'user' ? 'environment' : 'user')
  }

  const capturePhoto = () => {
    if (!videoRef.current || !canvasRef.current) return

    const video = videoRef.current
    const canvas = canvasRef.current
    const ctx = canvas.getContext('2d')

    // Canvas-GrÃ¶ÃŸe an Video anpassen
    canvas.width = video.videoWidth
    canvas.height = video.videoHeight

    // Video-Frame auf Canvas zeichnen
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height)

    // Canvas zu Blob konvertieren
    canvas.toBlob((blob) => {
      if (blob && onCapture) {
        onCapture(blob)
      }
    }, 'image/jpeg', 0.9)
  }

  return (
    <div className="relative w-full h-full">
      {/* Video Stream */}
      <video
        ref={videoRef}
        autoPlay
        playsInline
        muted
        className="w-full h-full object-cover"
      />

      {/* Hidden Canvas fÃ¼r Capture */}
      <canvas ref={canvasRef} className="hidden" />

      {/* Camera Controls */}
      <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-6">
        <div className="flex items-center justify-between">
          {/* Flash Toggle */}
          <button
            onClick={toggleFlash}
            disabled={!hasFlash}
            className={`
              p-3 rounded-full transition-all
              ${hasFlash 
                ? (isFlashOn ? 'bg-yellow-500' : 'bg-white/20') 
                : 'bg-gray-500/50 cursor-not-allowed'
              }
            `}
          >
            {isFlashOn ? (
              <Zap className="w-6 h-6 text-white" />
            ) : (
              <ZapOff className="w-6 h-6 text-white" />
            )}
          </button>

          {/* Capture Button */}
          <button
            onClick={capturePhoto}
            className="w-20 h-20 bg-white rounded-full border-4 border-pink-500 
                       flex items-center justify-center transition-all
                       hover:scale-105 active:scale-95"
          >
            <div className="w-16 h-16 bg-pink-500 rounded-full flex items-center justify-center">
              <Camera className="w-8 h-8 text-white" />
            </div>
          </button>

          {/* Camera Switch */}
          <button
            onClick={switchCamera}
            className="p-3 rounded-full bg-white/20 transition-all hover:bg-white/30"
          >
            <RotateCcw className="w-6 h-6 text-white" />
          </button>
        </div>
      </div>
    </div>
  )
})

export default CameraCapture
```

### TaskOverlay Component
```jsx
import React from 'react'
import { motion } from 'framer-motion'
import { Shuffle, ArrowLeft, HelpCircle } from 'lucide-react'

const TaskOverlay = ({ task, onNewTask, onBack, onShowHelp }) => {
  if (!task) return null

  return (
    <motion.div
      initial={{ opacity: 0, y: -50 }}
      animate={{ opacity: 1, y: 0 }}
      className="absolute top-0 left-0 right-0 z-10"
    >
      {/* Background Gradient */}
      <div className="bg-gradient-to-b from-black/70 via-black/50 to-transparent p-6">
        
        {/* Header Controls */}
        <div className="flex items-center justify-between mb-4">
          <button
            onClick={onBack}
            className="p-2 rounded-full bg-white/20 hover:bg-white/30 transition-all"
          >
            <ArrowLeft className="w-6 h-6 text-white" />
          </button>

          <div className="flex items-center gap-2">
            <button
              onClick={onShowHelp}
              className="p-2 rounded-full bg-white/20 hover:bg-white/30 transition-all"
            >
              <HelpCircle className="w-6 h-6 text-white" />
            </button>
            <button
              onClick={onNewTask}
              className="p-2 rounded-full bg-white/20 hover:bg-white/30 transition-all"
            >
              <Shuffle className="w-6 h-6 text-white" />
            </button>
          </div>
        </div>

        {/* Task Content */}
        <div className="text-center">
          {/* Emotion Badge */}
          <div 
            className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-4"
            style={{ backgroundColor: task.emotion?.color || '#ff6b9d' }}
          >
            <span className="text-xl">{task.emotion?.icon}</span>
            <span className="text-white font-medium">{task.emotion?.name}</span>
          </div>

          {/* Task Title */}
          <h2 className="text-2xl font-bold text-white mb-3 leading-tight">
            {task.title}
          </h2>

          {/* Task Description */}
          <p className="text-lg text-white/90 leading-relaxed max-w-sm mx-auto">
            {task.description}
          </p>

          {/* Difficulty Indicator */}
          <div className="flex items-center justify-center gap-1 mt-4">
            {[...Array(3)].map((_, i) => (
              <div
                key={i}
                className={`
                  w-2 h-2 rounded-full
                  ${i < getDifficultyLevel(task.difficulty) 
                    ? 'bg-yellow-400' 
                    : 'bg-white/30'
                  }
                `}
              />
            ))}
            <span className="text-white/70 text-sm ml-2 capitalize">
              {task.difficulty}
            </span>
          </div>

          {/* Example Hint */}
          {task.example_text && (
            <motion.div
              initial={{ height: 0, opacity: 0 }}
              animate={{ height: 'auto', opacity: 1 }}
              className="mt-4 p-3 bg-white/10 rounded-lg"
            >
              <p className="text-sm text-white/80 italic">
                ðŸ’¡ {task.example_text}
              </p>
            </motion.div>
          )}
        </div>
      </div>
    </motion.div>
  )
}

const getDifficultyLevel = (difficulty) => {
  switch (difficulty) {
    case 'easy': return 1
    case 'medium': return 2
    case 'hard': return 3
    default: return 1
  }
}

export default TaskOverlay
```

## ðŸ§© Tenant-Defined Tasks & CSV Import

### Goals
- Allow tenants (customers) to create and manage their own photo tasks.
- Support CSV import for bulk task creation for both Tenant Admins and Super Admin.
- Keep global (platform) task templates that tenants can also use.

### Schema Changes (Tenant Scoping for Tasks)
```sql
-- Extend tasks to support tenant/event scoping
ALTER TABLE tasks ADD COLUMN tenant_id INTEGER NULL;              -- owner tenant for custom tasks
ALTER TABLE tasks ADD COLUMN scope TEXT DEFAULT 'global' CHECK (scope IN ('global','tenant','event'));
ALTER TABLE tasks ADD COLUMN event_id INTEGER NULL;               -- optional link for event-specific tasks
ALTER TABLE tasks ADD CONSTRAINT fk_tasks_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenants(id);
ALTER TABLE tasks ADD CONSTRAINT fk_tasks_event_id FOREIGN KEY (event_id) REFERENCES events(id);

-- Prevent duplicate titles per emotion within the same tenant (optional)
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_unique_per_tenant ON tasks(tenant_id, emotion_id, title);
```

Notes:
- Use `scope='global'` for Super Admin library tasks, `tenant` for customer-defined, and `event` for per-event overrides.
- `tenant_id`/`event_id` are NULL for `global` tasks.

### CSV Format
- Encoding: UTF-8, delimiter: comma.
- Header columns:
  - emotion (name or ID)
  - title
  - description
  - difficulty (easy|medium|hard)
  - example_text (optional)
  - is_active (1|0, optional; default 1)
  - sort_order (integer, optional)
  - scope (global|tenant|event, optional; default depends on UI context)
  - event_slug (required if scope=event)`n  - event_type (slug, optional; defaults to current event type or chosen library type)

Example:
```csv
emotion,title,description,difficulty,example_text,is_active,sort_order,scope,event_type,event_slug
Freude,Sprung-Foto,Alle springen gleichzeitig in die Luft!,medium,,1,10,tenant,corporate,
Liebe,Kuss-Foto,Macht ein romantisches Kuss-Foto,easy,Einfach kÃ¼ssen und lÃ¤cheln!,1,5,global,wedding,
Ãœberraschung,PlÃ¶tzlicher Jubel,Erschreckt das Brautpaar mit Jubel,hard,,1,20,event,,mueller-hochzeit-2025
```

Validation rules:
- emotion must exist (map by name â†’ ID considering tenant visibility).
- difficulty in allowed set; length limits per model.
- If scope=event, event_slug must resolve to a tenant-owned event.

### Filament v4 â€” Tenant Admin Task Resource (with CSV Import)
```php
<?php
// app/Filament/Tenant/Resources/TaskResource.php

namespace App\Filament\Tenant\Resources;

use App\Models\{Task, Emotion, Event, EventType};
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Actions;
use Filament\Forms\Form;
use Filament\Schema as Schema;
use Illuminate\Support\Facades\DB;
use League\Csv\Reader;

class TaskResource extends Resource
{
    protected static ?string $model = Task::class;
    protected static ?string $navigationIcon = 'heroicon-o-queue-list';
    protected static ?string $navigationGroup = 'Content';
    protected static ?string $label = 'Aufgaben';

    public static function form(Form $form): Form
    {
        return $form->schema([
            Schema\Components\Section::make('Aufgabe')
                ->schema([
                    Schema\Components\Select::make('emotion_id')
                        ->label('Emotion')
                        ->relationship('emotion', 'name')
                        ->required(),
                    Schema\Components\TextInput::make('title')->required()->maxLength(255),
                    Schema\Components\Textarea::make('description')->required()->rows(3),
                    Schema\Components\Select::make('difficulty')
                        ->options(['easy'=>'Easy','medium'=>'Medium','hard'=>'Hard'])->required(),
                    Schema\Components\TextInput::make('example_text'),
                    Schema\Components\Toggle::make('is_active')->default(true),
                    Schema\Components\TextInput::make('sort_order')->numeric()->default(0),
                ])->columns(2),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('emotion.name')->label('Emotion')->sortable()->searchable(),
                Tables\Columns\TextColumn::make('title')->searchable(),
                Tables\Columns\BadgeColumn::make('difficulty')->colors(['easy'=>'success','medium'=>'warning','hard'=>'danger']),
                Tables\Columns\IconColumn::make('is_active')->boolean(),
                Tables\Columns\TextColumn::make('sort_order')->sortable(),
            ])
            ->filters([
                Tables\Filters\SelectFilter::make('event_type_id')
                    ->label('Event Type')
                    ->options(fn () => \App\Models\EventType::orderBy('name')->pluck('name', 'id')->all())
                    ->query(function ($query, $state) {
                        if (! $state) { return; }
                        $query->where(function($q) use ($state) {
                            $q->where('event_type_id', $state)
                              ->orWhereExists(function($sub) use ($state) {
                                  $sub->selectRaw(1)
                                      ->from('emotion_event_type as eet')
                                      ->whereColumn('eet.emotion_id', 'tasks.emotion_id')
                                      ->where('eet.event_type_id', $state);
                              });
                        });
                    }),
            ])
            ->headerActions([
                Actions\CreateAction::make(),
                Actions\Action::make('importCsv')
                    ->label('CSV Import')
                    ->icon('heroicon-o-arrow-up-tray')
                    ->form([
                        Schema\Components\FileUpload::make('csv')->acceptedFileTypes(['text/csv','text/plain'])->required(),
                        Schema\Components\Select::make('scope')->options(['tenant'=>'Tenant','event'=>'Event'])->default('tenant')->required(),
                        Schema\Components\Select::make('event_id')
                            ->label('Event (fÃ¼r Scope=Event)')
                            ->relationship('event', 'name')
                            ->searchable()
                            ->visible(fn ($get) => $get('scope') === 'event'),
                    ])
                    ->action(function(array $data) {
                        $path = $data['csv'];
                        $full = storage_path('app/public/'.$path);
                        $csv = Reader::createFromPath($full, 'r');
                        $csv->setHeaderOffset(0);
                        DB::transaction(function() use ($csv, $data) {
                            foreach ($csv->getRecords() as $row) {
                                $emotion = Emotion::query()->where('name', $row['emotion'] ?? '')->first();
                                if (! $emotion) { continue; }
                                Task::create([
                                    'tenant_id' => auth()->user()->tenant_id,
                                    'event_id' => $data['scope'] === 'event' ? ($data['event_id'] ?? null) : null,
                                    'scope' => $data['scope'],
                                    'emotion_id' => $emotion->id,
                                    'title' => trim($row['title'] ?? ''),
                                    'description' => trim($row['description'] ?? ''),
                                    'difficulty' => $row['difficulty'] ?? 'easy',
                                    'example_text' => $row['example_text'] ?? null,
                                    'is_active' => (int)($row['is_active'] ?? 1) === 1,
                                    'sort_order' => (int)($row['sort_order'] ?? 0),
                                ]);
                            }
                        });
                    })
                    ->requiresConfirmation(),
            ])
            ->recordActions([
                Actions\EditAction::make(),
                Actions\DeleteAction::make(),
            ]);
    }

    public static function getEloquentQuery()
    {
        // Tenant scoping for tasks: show global + tenant-owned
        return parent::getEloquentQuery()->where(function($q){
            $tenantId = auth()->user()->tenant_id;
            $q->whereNull('tenant_id')->where('scope','global')
              ->orWhere('tenant_id', $tenantId);
        });
    }
}
```

**EmotionController (filtered by event type):**
```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\{Event, Emotion};

class EmotionController extends Controller
{
    public function index(Event $event)
    {
        $typeId = $event->event_type_id;
        $emotions = Emotion::query()
            ->where('is_active', true)
            ->where(function($q) use ($typeId) {
                $q->whereIn('id', function($sub) use ($typeId) {
                    $sub->from('emotion_event_type')
                        ->select('emotion_id')
                        ->where('event_type_id', $typeId);
                });
            })
            ->orderBy('sort_order')
            ->get();

        return response()->json($emotions);
    }
}
```

**TaskController (respect event type and scope precedence):**
```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\{Event, Task};
use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function getByEmotion(Request $request, Event $event, $emotionId)
    {
        $tenantId = optional($request->user())->tenant_id; // optional auth
        $typeId = $event->event_type_id;

        $query = Task::query()
            ->where('emotion_id', $emotionId)
            ->where('is_active', true)
            ->where(function($q) use ($event, $tenantId, $typeId) {
                $q->where('scope', 'event')->where('event_id', $event->id)
                  ->orWhere(function($q2) use ($tenantId, $typeId) {
                      $q2->where('scope','tenant')
                         ->where('tenant_id', $tenantId)
                         ->where(function($qq) use ($typeId){
                             $qq->whereNull('event_type_id')->orWhere('event_type_id', $typeId);
                         });
                  })
                  ->orWhere(function($q3) use ($typeId) {
                      $q3->where('scope','global')
                         ->where(function($qq) use ($typeId){
                             $qq->whereNull('event_type_id')->orWhere('event_type_id', $typeId);
                         });
                  });
            })
            ->orderByRaw("CASE WHEN scope='event' THEN 1 WHEN scope='tenant' THEN 2 ELSE 3 END")
            ->orderBy('sort_order')
            ->get();

        return response()->json($query);
    }

    public function getRandomTask(Request $request, Event $event, $emotionId)
    {
        $request->merge(['limit' => 1]);
        $tasks = $this->getByEmotion($request, $event, $emotionId)->getData();
        // pick a random from top precedence group
        $collection = collect($tasks);
        $topScope = $collection->min(fn($t) => $t->scope === 'event' ? 1 : ($t->scope === 'tenant' ? 2 : 3));
        $pool = $collection->filter(fn($t) => ($t->scope === 'event' ? 1 : ($t->scope === 'tenant' ? 2 : 3)) === $topScope);
        return response()->json($pool->random());
    }
}
```


### Super Admin â€” Event Types Resource
```php
<?php

### Super Admin â€” Emotion Resource (Event Type Filter)
```php
<?php
// app/Filament/SuperAdmin/Resources/EmotionResource.php

namespace App\\Filament\\SuperAdmin\\Resources;

use App\\Models\\{Emotion, EventType};
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Filament\\Actions;
use Filament\\Forms\\Form;
use Filament\\Schema as Schema;

class EmotionResource extends Resource
{
    protected static ?string $model = Emotion::class;
    protected static ?string $navigationIcon = "heroicon-o-face-smile";
    protected static ?string $navigationGroup = "Content Management";
    protected static ?string $label = "Emotions";

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make("name")->searchable()->sortable(),
                Tables\\Columns\\TextColumn::make("icon"),
                Tables\\Columns\\TextColumn::make("color"),
            ])
            ->filters([
                Tables\\Filters\\SelectFilter::make("event_type")
                    ->label("Event Type")
                    ->options(fn () => EventType::orderBy("name")->pluck("name","id")->all())
                    ->query(function($query, $state){
                        if (! $state) { return; }
                        $query->whereIn("id", function($sub) use ($state) {
                            $sub->from("emotion_event_type")
                                ->select("emotion_id")
                                ->where("event_type_id", $state);
                        });
                    }),
            ])
            ->recordActions([
                Actions\\EditAction::make(),
                Actions\\DeleteAction::make(),
            ])
            ->headerActions([ Actions\\CreateAction::make() ]);
    }
}
```

// app/Filament/SuperAdmin/Resources/EventTypeResource.php

namespace App\\Filament\\SuperAdmin\\Resources;

use App\\Models\\EventType;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Filament\\Forms\\Form;
use Filament\\Schema as Schema;
use Filament\\Actions;

class EventTypeResource extends Resource
{
    protected static ?string $model = EventType::class;
    protected static ?string $navigationIcon = "heroicon-o-rectangle-stack";
    protected static ?string $navigationGroup = "Platform Configuration";
    protected static ?string $label = "Event Types";

    public static function form(Form $form): Form
    {
        return $form->schema([
            Schema\\Components\\TextInput::make("name")->required()->maxLength(100),
            Schema\\Components\\TextInput::make("slug")->required()->maxLength(100)->unique(ignoreRecord: true),
            Schema\\Components\\TextInput::make("icon")->maxLength(64),
            Schema\\Components\\KeyValue::make("settings")->keyLabel("Key")->valueLabel("Value"),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make("name")->searchable()->sortable(),
                Tables\\Columns\\TextColumn::make("slug")->searchable(),
                Tables\\Columns\\TextColumn::make("icon"),
            ])
            ->recordActions([
                Actions\\EditAction::make(),
                Actions\\DeleteAction::make(),
            ])
            ->headerActions([ Actions\\CreateAction::make() ]);
    }
}
```

### Filament v4 â€” Super Admin Global Task Library (with CSV Import)
```php
<?php
// app/Filament/SuperAdmin/Resources/TaskLibraryResource.php

namespace App\Filament\SuperAdmin\Resources;

use App\Models\{Task, Emotion, Tenant, Event, EventType};
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Actions;
use Filament\Forms\Form;
use Filament\Schema as Schema;
use League\Csv\Reader;
use Illuminate\Support\Facades\DB;

class TaskLibraryResource extends Resource
{
    protected static ?string $model = Task::class;
    protected static ?string $navigationIcon = 'heroicon-o-sparkles';
    protected static ?string $navigationGroup = 'Content Management';
    protected static ?string $label = 'Task Library';

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('scope')->badge(),
                Tables\Columns\TextColumn::make('tenant.name')->label('Tenant')->toggleable(isToggledHidden: true),
                Tables\Columns\TextColumn::make('emotion.name')->label('Emotion'),
                Tables\Columns\TextColumn::make('title')->limit(60),
                Tables\Columns\BadgeColumn::make('difficulty'),
                Tables\Columns\IconColumn::make('is_active')->boolean(),
            ]            ->filters([
                Tables\\Filters\\SelectFilter::make('event_type_id')
                    ->label('Event Type')
                    ->options(fn () => \\App\\Models\\EventType::orderBy('name')->pluck('name','id')->all())
                    ->query(function (,){
                        if(! ){ return; }
                        ->where(function() use (){
                            ->where('event_type_id',)
                              ->orWhereExists(function() use (){
                                  ->selectRaw(1)
                                      ->from('emotion_event_type as eet')
                                      ->whereColumn('eet.emotion_id','tasks.emotion_id')
                                      ->where('eet.event_type_id',);
                              });
                        });
                    }),
            ])
)
            ->headerActions([
                Actions\Action::make('importCsv')
                    ->label('CSV Import')
                    ->icon('heroicon-o-arrow-up-tray')
                    ->form([
                        Schema\Components\FileUpload::make('csv')->acceptedFileTypes(['text/csv','text/plain'])->required(),
                        Schema\Components\Select::make('scope')->options(['global'=>'Global','tenant'=>'Tenant','event'=>'Event'])->default('global')->required(),
                        Schema\Components\Select::make('tenant_id')->relationship('tenant','name')->visible(fn($get)=>$get('scope')!=='global'),
                        Schema\Components\Select::make('event_id')->label('Event')->relationship('event','name')->visible(fn($get)=>$get('scope')==='event'),
                    ])
                    ->action(function(array $data){
                        $path = $data['csv'];
                        $full = storage_path('app/public/'.$path);
                        $csv = Reader::createFromPath($full, 'r');
                        $csv->setHeaderOffset(0);
                        DB::transaction(function() use ($csv, $data) {
                            foreach ($csv->getRecords() as $row) {
                                $emotion = Emotion::query()->where('name', $row['emotion'] ?? '')->first();
                                if (! $emotion) { continue; }
                                Task::create([
                                    'scope' => $data['scope'],
                                    'tenant_id' => $data['scope']!=='global' ? ($data['tenant_id'] ?? null) : null,
                                    'event_id' => $data['scope']==='event' ? ($data['event_id'] ?? null) : null,
                                    'emotion_id' => $emotion->id,
                                    'title' => trim($row['title'] ?? ''),
                                    'description' => trim($row['description'] ?? ''),
                                    'difficulty' => $row['difficulty'] ?? 'easy',
                                    'example_text' => $row['example_text'] ?? null,
                                    'is_active' => (int)($row['is_active'] ?? 1) === 1,
                                    'sort_order' => (int)($row['sort_order'] ?? 0),
                                ]);
                            }
                        });
                    })
                    ->requiresConfirmation(),
            ]);
    }
}
```

Implementation notes:
- Add policies to restrict Tenant Admins from editing global tasks.
- When importing with scope=tenant/event in Super Admin, enforce that referenced tenant/event exist.
- Consider a background Job for large CSV files and a progress UI.

## ðŸš€ Deployment & Setup-Anweisungen

### Entwicklungs-Setup

#### 1. Laravel Backend Setup
```bash
# Neues Laravel-Projekt erstellen
composer create-project laravel/laravel:^12.0 hochzeits-fotobox
cd hochzeits-fotobox

# AbhÃ¤ngigkeiten installieren
composer require intervention/image
composer require pusher/pusher-php-server
composer require laravel/sanctum
composer require filament/filament:^4.0 -W
# Falls Filament v3 benoetigt wird (Kompatibilitaet):
# composer require filament/filament:^3.3 -W
composer require league/csv:^9.15  # CSV parsing for task imports
composer require spatie/laravel-translatable  # JSON-based field translations (i18n)

# Environment konfigurieren
cp .env.example .env
# Datenbankverbindung auf SQLite Ã¤ndern:
# DB_CONNECTION=sqlite
# DB_DATABASE=/absolute/path/to/database/database.sqlite

# SQLite-Datenbank erstellen
touch database/database.sqlite

# Migrations erstellen und ausfÃ¼hren
php artisan make:migration create_events_table
php artisan make:migration create_emotions_table
php artisan make:migration create_event_types_table
php artisan make:migration create_emotion_event_type_table
php artisan make:migration add_event_type_id_to_events
php artisan make:migration add_event_type_and_scoping_to_tasks
php artisan make:migration add_i18n_translatables_json --table=events
php artisan make:migration add_i18n_translatables_json_to_emotions --table=emotions
php artisan make:migration add_i18n_translatables_json_to_tasks --table=tasks
php artisan make:migration create_tasks_table
php artisan make:migration create_photos_table
php artisan make:migration create_photo_likes_table

php artisan migrate

# Models und Controller generieren
php artisan make:model Event -c
php artisan make:model Emotion -c
php artisan make:model Task -c
php artisan make:model Photo -c
php artisan make:model PhotoLike

# Seeder fÃ¼r Demo-Daten
php artisan make:seeder EmotionsSeeder
php artisan make:seeder TasksSeeder
php artisan make:seeder DemoEventSeeder

# Storage-Link erstellen
php artisan storage:link

# App-Key generieren
php artisan key:generate
```

#### 2. Frontend Setup
```bash
# NPM-AbhÃ¤ngigkeiten installieren
npm install

# React und Tools
npm install react react-dom @vitejs/plugin-react
npm install @tanstack/react-query
npm install react-router-dom
npm install framer-motion
npm install lucide-react

# Styling
npm install tailwindcss postcss autoprefixer
npm install @tailwindcss/forms @tailwindcss/aspect-ratio

# PWA
npm install vite-plugin-pwa workbox-window

# Utils
npm install date-fns
npm install clsx
npm install compressorjs
npm install react-markdown rehype-sanitize

# Tailwind initialisieren
npx tailwindcss init -p

# Verzeichnis-Struktur erstellen
mkdir -p resources/js/src/{components,pages,hooks,services,context,utils}
mkdir -p resources/js/src/components/{Camera,Emotions,Gallery,Layout,Forms}
mkdir -p public/icons
```

#### 3. Seeder-Daten

**EmotionsSeeder.php:**
```php
<?php

use Illuminate\Database\Seeder;
use App\Models\Emotion;

class EmotionsSeeder extends Seeder
{
    public function run()
    {
        $emotions = [
            [
                'name' => 'Liebe',
                'icon' => 'ðŸ’•',
                'color' => '#ff6b9d',
                'description' => 'Romantische und liebevolle Momente',
                'sort_order' => 1
            ],
            [
                'name' => 'Freude',
                'icon' => 'ðŸ˜‚',
                'color' => '#ffd93d',
                'description' => 'Lustige und frÃ¶hliche Augenblicke',
                'sort_order' => 2
            ],
            [
                'name' => 'RÃ¼hrung',
                'icon' => 'ðŸ¥º',
                'color' => '#6bcf7f',
                'description' => 'Emotionale und berÃ¼hrende Szenen',
                'sort_order' => 3
            ],
            [
                'name' => 'Nostalgie',
                'icon' => 'ðŸ“¸',
                'color' => '#a78bfa',
                'description' => 'Erinnerungen und nostalgische GefÃ¼hle',
                'sort_order' => 4
            ],
            [
                'name' => 'Ãœberraschung',
                'icon' => 'ðŸ˜²',
                'color' => '#fb7185',
                'description' => 'Unerwartete und Ã¼berraschende Momente',
                'sort_order' => 5
            ],
            [
                'name' => 'Stolz',
                'icon' => 'ðŸ†',
                'color' => '#34d399',
                'description' => 'Stolze und triumphale Augenblicke',
                'sort_order' => 6
            ]
        ];

        foreach ($emotions as $emotionData) {
            Emotion::create($emotionData);
        }
    }
}
```

**TasksSeeder.php:**
```php
<?php

use Illuminate\Database\Seeder;
use App\Models\{Emotion, Task};

class TasksSeeder extends Seeder
{
    public function run()
    {
        $tasks = [
            // Liebe-Tasks
            [
                'emotion' => 'Liebe',
                'tasks' => [
                    [
                        'title' => 'Kuss-Foto',
                        'description' => 'Macht ein romantisches Kuss-Foto',
                        'difficulty' => 'easy',
                        'example_text' => 'Einfach kÃ¼ssen und lÃ¤cheln!'
                    ],
                    [
                        'title' => 'HÃ¤ndchen halten',
                        'description' => 'Zeigt eure HÃ¤nde - verheiratet und verliebt',
                        'difficulty' => 'easy',
                        'example_text' => 'Ringe in Szene setzen'
                    ],
                    [
                        'title' => 'Liebes-Pose',
                        'description' => 'Stellt die romantischste Szene aus einem Film nach',
                        'difficulty' => 'medium',
                        'example_text' => 'Titanic, Dirty Dancing oder eure Lieblings-Romanze'
                    ],
                    [
                        'title' => 'Herz formen',
                        'description' => 'Formt mit euren HÃ¤nden ein groÃŸes Herz',
                        'difficulty' => 'easy',
                        'example_text' => 'Gemeinsam Ã¼ber dem Kopf oder vor der Brust'
                    ]
                ]
            ],

            // Freude-Tasks  
            [
                'emotion' => 'Freude',
                'tasks' => [
                    [
                        'title' => 'Sprung-Foto',
                        'description' => 'Alle springen gleichzeitig in die Luft!',
                        'difficulty' => 'medium',
                        'example_text' => 'Auf drei: eins, zwei, drei - SPRUNG!'
                    ],
                    [
                        'title' => 'Grimassen-Contest',
                        'description' => 'Macht die verrÃ¼cktesten Gesichter',
                        'difficulty' => 'easy',
                        'example_text' => 'Je alberner, desto besser!'
                    ],
                    [
                        'title' => 'Lach-Attacke',
                        'description' => 'Alle lachen so laut sie kÃ¶nnen',
                        'difficulty' => 'easy',
                        'example_text' => 'Echtes Lachen ist am schÃ¶nsten'
                    ],
                    [
                        'title' => 'Tanz-Pose',
                        'description' => 'Zeigt euren besten Tanz-Move',
                        'difficulty' => 'medium',
                        'example_text' => 'Disco, Hip-Hop oder klassischer Walzer'
                    ]
                ]
            ],

            // RÃ¼hrung-Tasks
            [
                'emotion' => 'RÃ¼hrung',
                'tasks' => [
                    [
                        'title' => 'TrÃ¤nen der Freude',
                        'description' => 'Zeigt eure emotionalen Gesichter',
                        'difficulty' => 'easy',
                        'example_text' => 'Echte GefÃ¼hle sind die schÃ¶nsten'
                    ],
                    [
                        'title' => 'Familien-Umarmung',
                        'description' => 'GroÃŸe Gruppenumarmung mit der ganzen Familie',
                        'difficulty' => 'medium',
                        'example_text' => 'Alle kommen zusammen fÃ¼r eine warme Umarmung'
                    ],
                    [
                        'title' => 'Stille Momente',
                        'description' => 'Ein ruhiges, besinnliches Foto',
                        'difficulty' => 'medium',
                        'example_text' => 'Manchmal sagt ein stiller Blick mehr als tausend Worte'
                    ]
                ]
            ],

            // Weitere Emotionen...
        ];

        foreach ($tasks as $emotionGroup) {
            $emotion = Emotion::where('name', $emotionGroup['emotion'])->first();
            
            if ($emotion) {
                foreach ($emotionGroup['tasks'] as $taskData) {
                    Task::create([
                        'emotion_id' => $emotion->id,
                        'title' => $taskData['title'],
                        'description' => $taskData['description'],
                        'difficulty' => $taskData['difficulty'],
                        'example_text' => $taskData['example_text'] ?? null
                    ]);
                }
            }
        }
    }
}
```

### Production Deployment

#### 1. Shared Hosting Setup
```bash
# .htaccess fÃ¼r Laravel in public_html
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(.*)$ /public/$1 [L]

# Laravel-Optimierungen
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Frontend Build
npm run build
```

#### 2. VPS/Cloud Deployment (mit Docker)
```dockerfile
# Dockerfile
FROM php:8.4-apache

# PHP Extensions
RUN docker-php-ext-install pdo pdo_sqlite gd

# Apache Konfiguration
RUN a2enmod rewrite
COPY . /var/www/html
COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf

# Permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

# Frontend Build
RUN npm ci && npm run build

EXPOSE 80
```

#### 3. Produktions-Optimierungen

**Laravel Optimierungen:**
```php
// config/app.php - Produktionseinstellungen
'debug' => false,
'log_level' => 'error',

// config/cache.php - File-Cache fÃ¼r bessere Performance
'default' => 'file',

// config/session.php - Datei-basierte Sessions
'driver' => 'file',
'lifetime' => 720, // 12 Stunden fÃ¼r Hochzeitsfeier
```

**Nginx Konfiguration:**
```nginx
server {
    listen 80;
    server_name your-domain.com;
    root /var/www/hochzeits-fotobox/public;
    
    index index.php;
    
    # Gzip Kompression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
    
    # Caching fÃ¼r statische Assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # PHP Processing
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
    
    # Laravel Routes
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}
```

## ðŸ§ª Testing & QualitÃ¤tssicherung

### Automatisierte Tests

#### 1. Backend Tests (PHPUnit)
```php
<?php
// tests/Feature/PhotoUploadTest.php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\{Event, Emotion, Task};
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

class PhotoUploadTest extends TestCase
{
    public function test_photo_upload_successful()
    {
        Storage::fake('public');
        
        $event = Event::factory()->create();
        $emotion = Emotion::factory()->create();
        $task = Task::factory()->create(['emotion_id' => $emotion->id]);
        
        $file = UploadedFile::fake()->image('test-photo.jpg', 1200, 1600);
        
        $response = $this->postJson("/api/events/{$event->slug}/photos", [
            'photo' => $file,
            'emotion_id' => $emotion->id,
            'task_id' => $task->id,
            'guest_name' => 'Test Gast'
        ]);
        
        $response->assertStatus(201)
                ->assertJsonStructure([
                    'success',
                    'data' => [
                        'id',
                        'file_path',
                        'thumbnail_path',
                        'guest_name',
                        'emotion',
                        'task'
                    ]
                ]);
        
        // Dateien wurden erstellt
        Storage::disk('public')->assertExists('photos/' . $file->hashName());
        Storage::disk('public')->assertExists('thumbnails/' . $file->hashName());
        
        // Datenbank-Eintrag
        $this->assertDatabaseHas('photos', [
            'event_id' => $event->id,
            'guest_name' => 'Test Gast',
            'emotion_id' => $emotion->id
        ]);
    }
}
```

#### 2. Frontend Tests (Jest + React Testing Library)
```javascript
// src/__tests__/EmotionBubble.test.jsx

import React from 'react'
import { render, fireEvent, screen } from '@testing-library/react'
import EmotionBubble from '../components/Emotions/EmotionBubble'

const mockEmotion = {
  id: 1,
  name: 'Freude',
  icon: 'ðŸ˜‚',
  color: '#ffd93d'
}

describe('EmotionBubble', () => {
  test('renders emotion correctly', () => {
    render(
      <EmotionBubble emotion={mockEmotion} onClick={jest.fn()} />
    )
    
    expect(screen.getByText('ðŸ˜‚')).toBeInTheDocument()
    expect(screen.getByText('Freude')).toBeInTheDocument()
  })
  
  test('calls onClick when clicked', () => {
    const handleClick = jest.fn()
    
    render(
      <EmotionBubble emotion={mockEmotion} onClick={handleClick} />
    )
    
    fireEvent.click(screen.getByRole('button'))
    expect(handleClick).toHaveBeenCalledWith(1)
  })
  
  test('applies correct styling when active', () => {
    render(
      <EmotionBubble 
        emotion={mockEmotion} 
        onClick={jest.fn()} 
        isActive={true} 
      />
    )
    
    const bubble = screen.getByRole('button')
    expect(bubble).toHaveClass('ring-4')
  })
})
```

### Performance-Monitoring

#### 1. Laravel Telescope (Development)
```bash
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
```

#### 2. Frontend Performance (Web Vitals)
```javascript
// src/utils/analytics.js

import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

function sendToAnalytics(metric) {
  // Production Analytics Service
  if (process.env.NODE_ENV === 'production') {
    console.log('Web Vital:', metric)
    
    // Beispiel: Sende zu Google Analytics
    // gtag('event', 'web_vitals', {
    //   event_category: 'Web Vitals',
    //   event_action: metric.name,
    //   event_value: Math.round(metric.value),
    //   non_interaction: true,
    // })
  }
}

export function initializeWebVitals() {
  getCLS(sendToAnalytics)
  getFID(sendToAnalytics)
  getFCP(sendToAnalytics)
  getLCP(sendToAnalytics)
  getTTFB(sendToAnalytics)
}
```

## ðŸ“Š Analytics & Monitoring

### Event-Tracking
```javascript
// src/services/eventTracking.js

class EventTracker {
  constructor(eventSlug) {
    this.eventSlug = eventSlug
    this.events = []
  }

  track(action, data = {}) {
    const event = {
      timestamp: new Date().toISOString(),
      action,
      data,
      eventSlug: this.eventSlug,
      sessionId: this.getSessionId(),
      userAgent: navigator.userAgent
    }
    
    this.events.push(event)
    this.sendToServer(event)
  }

  // Wichtige Events
  trackPhotoCapture(emotionId, taskId) {
    this.track('photo_capture', { emotionId, taskId })
  }

  trackPhotoUpload(photoId, success) {
    this.track('photo_upload', { photoId, success })
  }

  trackEmotionSelect(emotionId) {
    this.track('emotion_select', { emotionId })
  }

  trackTaskSkip(taskId) {
    this.track('task_skip', { taskId })
  }

  sendToServer(event) {
    // API-Endpoint fÃ¼r Analytics
    fetch(`/api/events/${this.eventSlug}/analytics`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event)

## Frontend PWA — Status Update (Sep 2025)

Implemented guest PWA per plan with React/Vite/Tailwind and PWA plugin. Highlights:

- App Structure
  - Routes: /event/:slug/home, /event/:slug/camera, /event/:slug/gallery, /event/:slug/achievements, /legal/:slug.
  - Event home adopts the event landing romance gradient; dark-mode override added.
  - Offline fonts bundled (Inter 400/600, Playfair Display 400/700, Great Vibes 400).

- UI/UX
  - Header settings as a sheet (language + theme). Dark/light/system theme with class-based Tailwind and no-flash boot.
  - Camera page with floating capture bar: compact emotion scroller, large shutter with animation, last-capture thumbnail (opens Gallery), task lock badge.
  - Gallery: grid, modal (Dialog), likes, skeleton placeholders, emotion filter and deep link.
  - Achievements page (new): tabs People/Groups/Live. People aggregates claimed tasks by guest; Live shows recent task-claimed photos. i18n included.
  - Navigation: replaced "Camera" tab with "Achievements" (trophy). Camera reachable via Home CTA.

- Data & APIs (current usage)
  - Events: GET /api/events/{slug} (returns localized fields and palette when available).
  - Emotions: GET /api/events/{slug}/emotions.
  - Tasks: GET /api/events/{slug}/tasks/random/{emotion}.
  - Photos: GET /api/events/{slug}/photos, POST /api/events/{slug}/photos (compressed < 1.5MB), POST /api/events/{slug}/photos/{photo}/like.
  - Legal: GET /api/legal/{slug} (markdown rendered on client).

- PWA & Offline
  - Compression to < 1.5MB before upload, EXIF stripped; offline queue with retry on "online"; toasts for success/queued/error.
  - Workbox via vite-plugin-pwa; further SW Background Sync planned.

- shadcn-style Layer
  - Added Button, Card, Dialog, Sheet, Skeleton, Badge primitives for consistency.

- Super Admin Login
  - Switched to classic Laravel session login at /super-admin/login; Filament panel guarded by middleware.

- QR & Landings
  - Root landing with QR scanner (zxing) + manual code entry; event landing redesigned (typography + romance gradient).

- Demo Data
  - Seeder DemoAchievementsSeeder creates demo photos with 	ask_id for the Achievements page.

### Gamification Direction (Short-Event)
- Focus on “solved tasks”: photo claims a task (client), future: peer votes to verify.
- Achievements ideas saved: docs/achievements/ideas.md.
- Planned endpoint: POST /api/events/{slug}/photos/{photo}/task-vote { verdict: 'pass'|'fail', guest_name } ? returns { pass, fail, verified }.

### Open Items / Next
- Task verification voting (backend + modal UI) and badges (First/Finisher/Sampler/Assist etc.).
- SW Background Sync for uploads; install prompt + shortcuts.
- Admin: palette editor; (optional) groups/teams for Achievements.
- Accessibility polish, reduced-motion paths.

