Remove legacy tenant Filament panel
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-04 18:47:36 +01:00
parent 15e19d4e8b
commit 8805c8264c
30 changed files with 4 additions and 2267 deletions

View File

@@ -1,187 +0,0 @@
<?php
namespace App\Filament\Tenant\Pages;
use App\Models\Event;
use App\Models\EventJoinToken;
use App\Services\EventJoinTokenService;
use App\Support\JoinTokenLayoutRegistry;
use App\Support\TenantOnboardingState;
use BackedEnum;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\URL;
class InviteStudio extends Page
{
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-qr-code';
protected string $view = 'filament.tenant.pages.invite-studio';
protected static ?string $navigationLabel = 'Einladungen & QR';
protected static ?string $slug = 'invite-studio';
protected static ?string $title = 'Einladungen & QR-Codes';
protected static ?int $navigationSort = 50;
public ?int $selectedEventId = null;
public string $tokenLabel = '';
public array $tokens = [];
public array $layouts = [];
protected static bool $shouldRegisterNavigation = true;
public function mount(): void
{
$tenant = TenantOnboardingState::tenant();
abort_if(! $tenant, 403);
if (! TenantOnboardingState::completed($tenant)) {
$this->redirect(TenantOnboarding::getUrl());
return;
}
$firstEventId = $tenant->events()->orderBy('date')->value('id');
$this->selectedEventId = $firstEventId;
$this->layouts = $this->buildLayouts();
if ($this->selectedEventId) {
$this->loadEventContext();
}
}
public static function shouldRegisterNavigation(): bool
{
return TenantOnboardingState::completed();
}
public function updatedSelectedEventId(): void
{
$this->loadEventContext();
}
public function createInvite(EventJoinTokenService $service): void
{
$this->validate([
'selectedEventId' => ['required', 'exists:events,id'],
'tokenLabel' => ['nullable', 'string', 'max:120'],
]);
$tenant = TenantOnboardingState::tenant();
abort_if(! $tenant, 403);
$event = $tenant->events()->whereKey($this->selectedEventId)->first();
if (! $event) {
Notification::make()
->title('Event konnte nicht gefunden werden')
->danger()
->send();
return;
}
$label = $this->tokenLabel ?: 'Einladung '.now()->format('d.m.');
$layoutPreference = Arr::get($tenant->settings ?? [], 'branding.preferred_invite_layout');
$service->createToken($event, [
'label' => $label,
'metadata' => [
'preferred_layout' => $layoutPreference,
],
'created_by' => auth()->id(),
]);
$this->tokenLabel = '';
$this->loadEventContext();
Notification::make()
->title('Neuer Einladungslink erstellt')
->success()
->send();
}
protected function loadEventContext(): void
{
$tenant = TenantOnboardingState::tenant();
if (! $tenant || ! $this->selectedEventId) {
$this->tokens = [];
return;
}
$event = $tenant->events()->whereKey($this->selectedEventId)->first();
if (! $event) {
$this->tokens = [];
return;
}
$this->tokens = $event->joinTokens()
->orderByDesc('created_at')
->get()
->map(fn (EventJoinToken $token) => $this->mapToken($event, $token))
->toArray();
}
protected function mapToken(Event $event, EventJoinToken $token): array
{
$downloadUrls = JoinTokenLayoutRegistry::toResponse(function (string $layoutId, string $format) use ($event, $token) {
return route('api.v1.tenant.events.join-tokens.layouts.download', [
'event' => $event->slug,
'joinToken' => $token->getKey(),
'layout' => $layoutId,
'format' => $format,
]);
});
return [
'id' => $token->getKey(),
'label' => $token->label ?? 'Einladungslink',
'url' => URL::to('/e/'.$token->token),
'created_at' => optional($token->created_at)->format('d.m.Y H:i'),
'usage_count' => $token->usage_count,
'usage_limit' => $token->usage_limit,
'active' => $token->isActive(),
'downloads' => $downloadUrls,
];
}
protected function buildLayouts(): array
{
return collect(JoinTokenLayoutRegistry::all())
->map(fn (array $layout) => [
'id' => $layout['id'],
'name' => $layout['name'],
'subtitle' => $layout['subtitle'] ?? '',
'description' => $layout['description'] ?? '',
])
->toArray();
}
public function getEventsProperty(): Collection
{
$tenant = TenantOnboardingState::tenant();
if (! $tenant) {
return collect();
}
return $tenant->events()->orderBy('date')->get();
}
}

View File

@@ -1,311 +0,0 @@
<?php
namespace App\Filament\Tenant\Pages;
use App\Filament\Tenant\Resources\EventResource;
use App\Models\Event;
use App\Models\EventType;
use App\Models\TaskCollection;
use App\Services\EventJoinTokenService;
use App\Services\Tenant\TaskCollectionImportService;
use App\Support\JoinTokenLayoutRegistry;
use App\Support\TenantOnboardingState;
use BackedEnum;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Throwable;
use UnitEnum;
class TenantOnboarding extends Page
{
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-sparkles';
protected string $view = 'filament.tenant.pages.onboarding';
protected static ?string $navigationLabel = 'Willkommen';
protected static ?string $slug = 'willkommen';
protected static ?string $title = 'Euer Start mit Fotospiel';
protected static UnitEnum|string|null $navigationGroup = null;
public string $step = 'intro';
public array $status = [];
public array $inviteDownloads = [];
public array $selectedPackages = [];
public string $eventName = '';
public ?string $eventDate = null;
public ?int $eventTypeId = null;
public ?string $palette = null;
public ?string $inviteLayout = null;
public bool $isProcessing = false;
protected static bool $shouldRegisterNavigation = true;
public function mount(): void
{
$tenant = TenantOnboardingState::tenant();
abort_if(! $tenant, 403);
$this->status = TenantOnboardingState::status($tenant);
if (TenantOnboardingState::completed($tenant)) {
$this->redirect(EventResource::getUrl());
return;
}
$this->eventDate = Carbon::now()->addWeeks(2)->format('Y-m-d');
$this->eventTypeId = $this->getDefaultEventTypeId();
}
public static function shouldRegisterNavigation(): bool
{
$tenant = TenantOnboardingState::tenant();
return ! TenantOnboardingState::completed($tenant);
}
public function start(): void
{
$this->step = 'packages';
}
public function savePackages(): void
{
$this->validate([
'selectedPackages' => ['required', 'array', 'min:1'],
'selectedPackages.*' => ['integer', 'exists:task_collections,id'],
], [
'selectedPackages.required' => 'Bitte wählt mindestens ein Aufgabenpaket aus.',
]);
$this->step = 'event';
}
public function saveEvent(): void
{
$this->validate([
'eventName' => ['required', 'string', 'max:255'],
'eventDate' => ['required', 'date'],
'eventTypeId' => ['required', 'exists:event_types,id'],
]);
$this->step = 'palette';
}
public function savePalette(): void
{
$this->validate([
'palette' => ['required', 'string'],
]);
$this->step = 'invite';
}
public function finish(
TaskCollectionImportService $importService,
EventJoinTokenService $joinTokenService
): void {
$this->validate([
'inviteLayout' => ['required', 'string'],
], [
'inviteLayout.required' => 'Bitte wählt ein Layout aus.',
]);
$tenant = TenantOnboardingState::tenant();
abort_if(! $tenant, 403);
$this->isProcessing = true;
try {
DB::transaction(function () use ($tenant, $importService, $joinTokenService) {
$event = $this->createEvent($tenant);
$this->importPackages($importService, $this->selectedPackages, $event);
$token = $joinTokenService->createToken($event, [
'label' => 'Fotospiel Einladung',
'metadata' => [
'preferred_layout' => $this->inviteLayout,
],
]);
$settings = $tenant->settings ?? [];
Arr::set($settings, 'branding.palette', $this->palette);
Arr::set($settings, 'branding.primary_event_id', $event->id);
Arr::set($settings, 'branding.preferred_invite_layout', $this->inviteLayout);
$tenant->forceFill(['settings' => $settings])->save();
TenantOnboardingState::markCompleted($tenant, [
'primary_event_id' => $event->id,
'selected_packages' => $this->selectedPackages,
'qr_layout' => $this->inviteLayout,
]);
$this->inviteDownloads = $this->buildInviteDownloads($event, $token);
$this->status = TenantOnboardingState::status($tenant);
Notification::make()
->title('Euer Setup ist bereit!')
->body('Wir haben euer Event erstellt, Aufgaben importiert und euren Einladungslink vorbereitet.')
->success()
->send();
$this->redirect(EventResource::getUrl('view', ['record' => $event]));
});
} catch (Throwable $exception) {
report($exception);
Notification::make()
->title('Setup konnte nicht abgeschlossen werden')
->body('Bitte prüft eure Eingaben oder versucht es später erneut.')
->danger()
->send();
} finally {
$this->isProcessing = false;
}
}
protected function createEvent($tenant): Event
{
$slugBase = Str::slug($this->eventName) ?: 'event';
do {
$slug = Str::of($slugBase)->append('-', Str::random(6))->lower();
} while (Event::where('slug', $slug)->exists());
return Event::create([
'tenant_id' => $tenant->id,
'name' => [
app()->getLocale() => $this->eventName,
'de' => $this->eventName,
],
'description' => null,
'date' => $this->eventDate,
'slug' => (string) $slug,
'event_type_id' => $this->eventTypeId,
'is_active' => true,
'default_locale' => app()->getLocale(),
'status' => 'draft',
'settings' => [
'appearance' => [
'palette' => $this->palette,
],
],
]);
}
protected function importPackages(
TaskCollectionImportService $importService,
array $packageIds,
Event $event
): void {
if (empty($packageIds)) {
return;
}
/** @var EloquentCollection<TaskCollection> $collections */
$collections = TaskCollection::query()
->whereIn('id', $packageIds)
->get();
$collections->each(function (TaskCollection $collection) use ($importService, $event) {
$importService->import($collection, $event);
});
}
protected function buildInviteDownloads(Event $event, $token): array
{
return JoinTokenLayoutRegistry::toResponse(function (string $layoutId, string $format) use ($event, $token) {
return route('api.v1.tenant.events.join-tokens.layouts.download', [
'event' => $event->slug,
'joinToken' => $token->getKey(),
'layout' => $layoutId,
'format' => $format,
]);
});
}
public function getPackageListProperty(): array
{
return TaskCollection::query()
->whereNull('tenant_id')
->orderBy('position')
->get()
->map(fn (TaskCollection $collection) => [
'id' => $collection->getKey(),
'name' => $collection->name,
'description' => $collection->description,
])
->toArray();
}
public function getEventTypeOptionsProperty(): array
{
return EventType::query()
->orderBy('name->'.app()->getLocale())
->get()
->mapWithKeys(function (EventType $type) {
$name = $type->name[app()->getLocale()] ?? $type->name['de'] ?? Arr::first($type->name);
return [$type->getKey() => $name];
})
->toArray();
}
public function getPaletteOptionsProperty(): array
{
return [
'romance' => [
'label' => 'Rosé & Gold',
'description' => 'Warme Rosé-Töne mit goldenen Akzenten romantisch und elegant.',
],
'sunset' => [
'label' => 'Sonnenuntergang',
'description' => 'Leuchtende Orange- und Pink-Verläufe für lebhafte Partys.',
],
'evergreen' => [
'label' => 'Evergreen',
'description' => 'Sanfte Grüntöne und Naturakzente für Boho- & Outdoor-Events.',
],
'midnight' => [
'label' => 'Midnight',
'description' => 'Tiefes Navy und Flieder perfekt für elegante Abendveranstaltungen.',
],
];
}
public function getLayoutOptionsProperty(): array
{
return collect(JoinTokenLayoutRegistry::all())
->map(fn ($layout) => [
'id' => $layout['id'],
'name' => $layout['name'],
'subtitle' => $layout['subtitle'] ?? '',
'description' => $layout['description'] ?? '',
])
->toArray();
}
protected function getDefaultEventTypeId(): ?int
{
return EventType::query()->orderBy('name->'.app()->getLocale())->value('id');
}
}

View File

@@ -1,264 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources;
use App\Filament\Tenant\Resources\EventResource\Pages;
use App\Filament\Tenant\Resources\EventResource\RelationManagers\EventPackagesRelationManager;
use App\Models\Event;
use App\Models\EventJoinTokenEvent;
use App\Models\EventType;
use App\Support\JoinTokenLayoutRegistry;
use App\Support\TenantOnboardingState;
use BackedEnum;
use Carbon\Carbon;
use Filament\Actions;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use UnitEnum;
class EventResource extends Resource
{
protected static ?string $model = Event::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-calendar';
protected static UnitEnum|string|null $navigationGroup = null;
public static function getNavigationGroup(): UnitEnum|string|null
{
return __('admin.nav.platform');
}
protected static ?int $navigationSort = 20;
public static function shouldRegisterNavigation(): bool
{
return TenantOnboardingState::completed();
}
public static function form(Schema $form): Schema
{
$tenantId = Auth::user()?->tenant_id;
return $form->schema([
Hidden::make('tenant_id')
->default($tenantId)
->dehydrated(),
TextInput::make('name')
->label(__('admin.events.fields.name'))
->required()
->maxLength(255),
TextInput::make('slug')
->label(__('admin.events.fields.slug'))
->required()
->unique(ignoreRecord: true)
->maxLength(255),
DatePicker::make('date')
->label(__('admin.events.fields.date'))
->required(),
Select::make('event_type_id')
->label(__('admin.events.fields.type'))
->options(EventType::all()->pluck('name', 'id'))
->searchable(),
Select::make('package_id')
->label(__('admin.events.fields.package'))
->options(\App\Models\Package::where('type', 'endcustomer')->pluck('name', 'id'))
->searchable()
->preload()
->required(),
TextInput::make('default_locale')
->label(__('admin.events.fields.default_locale'))
->default('de')
->maxLength(5),
Toggle::make('is_active')
->label(__('admin.events.fields.is_active'))
->default(true),
KeyValue::make('settings')
->label(__('admin.events.fields.settings'))
->keyLabel(__('admin.common.key'))
->valueLabel(__('admin.common.value')),
])->columns(2);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('id')->sortable(),
Tables\Columns\TextColumn::make('eventPackage.package.name')
->label(__('admin.events.table.package'))
->badge()
->color('success'),
Tables\Columns\TextColumn::make('name')->limit(30),
Tables\Columns\TextColumn::make('slug')->searchable(),
Tables\Columns\TextColumn::make('date')->date(),
Tables\Columns\IconColumn::make('is_active')->boolean(),
Tables\Columns\TextColumn::make('default_locale'),
Tables\Columns\TextColumn::make('eventPackage.used_photos')
->label(__('admin.events.table.used_photos'))
->badge(),
Tables\Columns\TextColumn::make('eventPackage.remaining_photos')
->label(__('admin.events.table.remaining_photos'))
->badge()
->color(fn ($state) => $state < 1 ? 'danger' : 'success')
->getStateUsing(fn ($record) => $record->eventPackage?->remaining_photos ?? 0),
Tables\Columns\TextColumn::make('primary_join_token')
->label(__('admin.events.table.join'))
->getStateUsing(function ($record) {
$token = $record->joinTokens()->orderByDesc('created_at')->first();
return $token ? url('/e/'.$token->token) : __('admin.events.table.no_join_tokens');
})
->description(function ($record) {
$total = $record->joinTokens()->count();
return $total > 0
? __('admin.events.table.join_tokens_total', ['count' => $total])
: __('admin.events.table.join_tokens_missing');
})
->copyable()
->copyMessage(__('admin.events.messages.join_link_copied')),
Tables\Columns\TextColumn::make('created_at')->since(),
])
->modifyQueryUsing(function (Builder $query) {
if ($tenantId = Auth::user()?->tenant_id) {
$query->where('tenant_id', $tenantId);
}
})
->filters([])
->actions([
Actions\EditAction::make(),
Actions\Action::make('toggle')
->label(__('admin.events.actions.toggle_active'))
->icon('heroicon-o-power')
->action(fn ($record) => $record->update(['is_active' => ! $record->is_active])),
Actions\Action::make('join_tokens')
->label(__('admin.events.actions.join_link_qr'))
->icon('heroicon-o-qr-code')
->modalHeading(__('admin.events.modal.join_link_heading'))
->modalSubmitActionLabel(__('admin.common.close'))
->modalWidth('xl')
->modalContent(function ($record) {
$tokens = $record->joinTokens()
->orderByDesc('created_at')
->get();
if ($tokens->isEmpty()) {
return view('filament.events.join-link', [
'event' => $record,
'tokens' => collect(),
]);
}
$tokenIds = $tokens->pluck('id');
$now = now();
$totals = EventJoinTokenEvent::query()
->selectRaw('event_join_token_id, event_type, COUNT(*) as total')
->whereIn('event_join_token_id', $tokenIds)
->groupBy('event_join_token_id', 'event_type')
->get()
->groupBy('event_join_token_id');
$recent24h = EventJoinTokenEvent::query()
->selectRaw('event_join_token_id, COUNT(*) as total')
->whereIn('event_join_token_id', $tokenIds)
->where('occurred_at', '>=', $now->copy()->subHours(24))
->groupBy('event_join_token_id')
->pluck('total', 'event_join_token_id');
$lastSeen = EventJoinTokenEvent::query()
->whereIn('event_join_token_id', $tokenIds)
->selectRaw('event_join_token_id, MAX(occurred_at) as last_at')
->groupBy('event_join_token_id')
->pluck('last_at', 'event_join_token_id');
$tokens = $tokens->map(function ($token) use ($record, $totals, $recent24h, $lastSeen) {
$layouts = JoinTokenLayoutRegistry::toResponse(function (string $layoutId, string $format) use ($record, $token) {
return route('api.v1.tenant.events.join-tokens.layouts.download', [
'event' => $record->slug,
'joinToken' => $token->getKey(),
'layout' => $layoutId,
'format' => $format,
]);
});
$analyticsGroup = $totals->get($token->id, collect());
$analytics = $analyticsGroup->mapWithKeys(function ($row) {
return [$row->event_type => (int) $row->total];
});
$successCount = (int) ($analytics['access_granted'] ?? 0) + (int) ($analytics['gallery_access_granted'] ?? 0);
$failureCount = (int) ($analytics['invalid_token'] ?? 0)
+ (int) ($analytics['token_expired'] ?? 0)
+ (int) ($analytics['token_revoked'] ?? 0)
+ (int) ($analytics['token_rate_limited'] ?? 0)
+ (int) ($analytics['event_not_public'] ?? 0)
+ (int) ($analytics['gallery_expired'] ?? 0);
$lastSeenAt = $lastSeen->get($token->id);
return [
'id' => $token->id,
'label' => $token->label,
'token' => $token->token,
'url' => url('/e/'.$token->token),
'usage_limit' => $token->usage_limit,
'usage_count' => $token->usage_count,
'expires_at' => optional($token->expires_at)->toIso8601String(),
'revoked_at' => optional($token->revoked_at)->toIso8601String(),
'is_active' => $token->isActive(),
'created_at' => optional($token->created_at)->toIso8601String(),
'layouts' => $layouts,
'layouts_url' => route('api.v1.tenant.events.join-tokens.layouts.index', [
'event' => $record->slug,
'joinToken' => $token->getKey(),
]),
'analytics' => [
'success_total' => $successCount,
'failure_total' => $failureCount,
'rate_limited_total' => (int) ($analytics['token_rate_limited'] ?? 0),
'recent_24h' => (int) $recent24h->get($token->id, 0),
'last_seen_at' => $lastSeenAt ? Carbon::parse($lastSeenAt)->toIso8601String() : null,
],
];
});
return view('filament.events.join-link', [
'event' => $record,
'tokens' => $tokens,
]);
}),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListEvents::route('/'),
'create' => Pages\CreateEvent::route('/create'),
'view' => Pages\ViewEvent::route('/{record}'),
'edit' => Pages\EditEvent::route('/{record}/edit'),
];
}
public static function getRelations(): array
{
return [
EventPackagesRelationManager::class,
];
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\EventResource\Pages;
use App\Filament\Tenant\Resources\EventResource;
use Filament\Resources\Pages\CreateRecord;
class CreateEvent extends CreateRecord
{
protected static string $resource = EventResource::class;
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\EventResource\Pages;
use App\Filament\Tenant\Resources\EventResource;
use Filament\Resources\Pages\EditRecord;
class EditEvent extends EditRecord
{
protected static string $resource = EventResource::class;
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\EventResource\Pages;
use App\Filament\Tenant\Resources\EventResource;
use Filament\Resources\Pages\ListRecords;
class ListEvents extends ListRecords
{
protected static string $resource = EventResource::class;
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\EventResource\Pages;
use App\Filament\Tenant\Resources\EventResource;
use Filament\Resources\Pages\ViewRecord;
class ViewEvent extends ViewRecord
{
protected static string $resource = EventResource::class;
}

View File

@@ -1,129 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\EventResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Actions\CreateAction;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use App\Models\EventPackage;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DateTimePicker;
use Filament\Tables\Columns\TextColumn;
use Filament\Schemas\Schema;
class EventPackagesRelationManager extends RelationManager
{
protected static string $relationship = 'eventPackages';
public function form(Schema $schema): Schema
{
return $schema->schema([
Select::make('package_id')
->label('Package')
->relationship('package', 'name')
->searchable()
->preload()
->required(),
TextInput::make('purchased_price')
->label('Kaufpreis')
->prefix('€')
->numeric()
->step(0.01)
->required(),
TextInput::make('used_photos')
->label('Verwendete Fotos')
->numeric()
->default(0)
->readOnly(),
TextInput::make('used_guests')
->label('Verwendete Gäste')
->numeric()
->default(0)
->readOnly(),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('package.name')
->columns([
TextColumn::make('package.name')
->label('Package')
->badge()
->color('success'),
TextColumn::make('used_photos')
->label('Verwendete Fotos')
->badge(),
TextColumn::make('remaining_photos')
->label('Verbleibende Fotos')
->badge()
->color(fn ($state) => $state < 1 ? 'danger' : 'success')
->getStateUsing(fn (EventPackage $record) => $record->remaining_photos),
TextColumn::make('used_guests')
->label('Verwendete Gäste')
->badge(),
TextColumn::make('remaining_guests')
->label('Verbleibende Gäste')
->badge()
->color(fn ($state) => $state < 1 ? 'danger' : 'success')
->getStateUsing(fn (EventPackage $record) => $record->remaining_guests),
TextColumn::make('expires_at')
->label('Ablauf')
->dateTime()
->badge()
->color(fn ($state) => $state && $state->isPast() ? 'danger' : 'success'),
TextColumn::make('price')
->label('Preis')
->money('EUR')
->sortable(),
])
->filters([
//
])
->headerActions([
CreateAction::make(),
])
->actions([
EditAction::make(),
DeleteAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
public function getRelationExistenceQuery(
Builder $query,
string $relationshipName,
?string $ownerKeyName,
mixed $ownerKeyValue,
): Builder {
return $query;
}
public static function getTitle(Model $ownerRecord, string $pageClass): string
{
return __('admin.events.relation_managers.event_packages.title');
}
public function getTableQuery(): Builder | Relation
{
return parent::getTableQuery()
->with('package');
}
}

View File

@@ -1,126 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources;
use App\Filament\Tenant\Resources\PhotoResource\Pages;
use App\Models\Photo;
use App\Models\Event;
use App\Support\TenantOnboardingState;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\KeyValue;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use UnitEnum;
use BackedEnum;
class PhotoResource extends Resource
{
protected static ?string $model = Photo::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-photo';
protected static UnitEnum|string|null $navigationGroup = null;
public static function getNavigationGroup(): UnitEnum|string|null
{
return __('admin.nav.content');
}
protected static ?int $navigationSort = 30;
public static function shouldRegisterNavigation(): bool
{
return TenantOnboardingState::completed();
}
public static function form(Schema $form): Schema
{
$tenantId = Auth::user()?->tenant_id;
return $form->schema([
Select::make('event_id')
->label(__('admin.photos.fields.event'))
->options(
Event::query()
->when($tenantId, fn ($query) => $query->where('tenant_id', $tenantId))
->pluck('name', 'id')
)
->searchable()
->required(),
FileUpload::make('file_path')
->label(__('admin.photos.fields.photo'))
->image() // enable FilePond image preview
->disk('public')
->directory('photos')
->visibility('public')
->required(),
Toggle::make('is_featured')
->label(__('admin.photos.fields.is_featured'))
->default(false),
KeyValue::make('metadata')
->label(__('admin.photos.fields.metadata'))
->keyLabel(__('admin.common.key'))
->valueLabel(__('admin.common.value')),
])->columns(2);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\ImageColumn::make('file_path')->label(__('admin.photos.table.photo'))->disk('public')->visibility('public'),
Tables\Columns\TextColumn::make('id')->sortable(),
Tables\Columns\TextColumn::make('event.name')->label(__('admin.photos.table.event'))->searchable(),
Tables\Columns\TextColumn::make('likes_count')->label(__('admin.photos.table.likes')),
Tables\Columns\IconColumn::make('is_featured')->boolean(),
Tables\Columns\TextColumn::make('created_at')->since(),
])
->modifyQueryUsing(function (Builder $query) {
if ($tenantId = Auth::user()?->tenant_id) {
$query->whereHas('event', fn (Builder $eventQuery) => $eventQuery->where('tenant_id', $tenantId));
}
})
->filters([])
->actions([
Actions\EditAction::make(),
Actions\Action::make('feature')
->label(__('admin.photos.actions.feature'))
->visible(fn($record) => !$record->is_featured)
->action(fn($record) => $record->update(['is_featured' => true]))
->icon('heroicon-o-star'),
Actions\Action::make('unfeature')
->label(__('admin.photos.actions.unfeature'))
->visible(fn($record) => $record->is_featured)
->action(fn($record) => $record->update(['is_featured' => false]))
->icon('heroicon-o-star'),
Actions\DeleteAction::make(),
])
->bulkActions([
Actions\BulkAction::make('feature')
->label(__('admin.photos.actions.feature_selected'))
->icon('heroicon-o-star')
->action(fn($records) => $records->each->update(['is_featured' => true])),
Actions\BulkAction::make('unfeature')
->label(__('admin.photos.actions.unfeature_selected'))
->icon('heroicon-o-star')
->action(fn($records) => $records->each->update(['is_featured' => false])),
Actions\DeleteBulkAction::make(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListPhotos::route('/'),
'view' => Pages\ViewPhoto::route('/{record}'),
'edit' => Pages\EditPhoto::route('/{record}/edit'),
];
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\PhotoResource\Pages;
use App\Filament\Tenant\Resources\PhotoResource;
use Filament\Resources\Pages\EditRecord;
class EditPhoto extends EditRecord
{
protected static string $resource = PhotoResource::class;
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\PhotoResource\Pages;
use App\Filament\Tenant\Resources\PhotoResource;
use Filament\Resources\Pages\ListRecords;
class ListPhotos extends ListRecords
{
protected static string $resource = PhotoResource::class;
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\PhotoResource\Pages;
use App\Filament\Tenant\Resources\PhotoResource;
use Filament\Resources\Pages\ViewRecord;
class ViewPhoto extends ViewRecord
{
protected static string $resource = PhotoResource::class;
}

View File

@@ -1,242 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources;
use App\Filament\Tenant\Resources\TaskCollectionResource\Pages;
use App\Models\Event;
use App\Models\EventType;
use App\Models\TaskCollection;
use App\Services\Tenant\TaskCollectionImportService;
use Filament\Facades\Filament;
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Actions;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Filament\Notifications\Notification;
use Illuminate\Support\Str;
use BackedEnum;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use App\Support\TenantOnboardingState;
class TaskCollectionResource extends Resource
{
protected static ?string $model = TaskCollection::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-folder';
protected static ?int $navigationSort = 50;
public static function shouldRegisterNavigation(): bool
{
return TenantOnboardingState::completed();
}
public static function getNavigationGroup(): string
{
return __('admin.nav.library');
}
public static function form(Schema $schema): Schema
{
$tenantId = auth()->user()?->tenant_id;
return $schema->components([
Section::make(__('Task Collection Details'))
->schema([
TextInput::make('name_translations.de')
->label(__('Name (DE)'))
->required()
->maxLength(255)
->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null),
TextInput::make('name_translations.en')
->label(__('Name (EN)'))
->maxLength(255)
->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null),
Select::make('event_type_id')
->label(__('Event Type'))
->options(fn () => EventType::orderBy('name->' . app()->getLocale())
->get()
->mapWithKeys(function (EventType $type) {
$name = $type->name[app()->getLocale()] ?? $type->name['de'] ?? reset($type->name);
return [$type->id => $name];
})->toArray())
->searchable()
->required()
->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null),
Textarea::make('description_translations.de')
->label(__('Description (DE)'))
->rows(3)
->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null),
Textarea::make('description_translations.en')
->label(__('Description (EN)'))
->rows(3)
->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null),
])->columns(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('Name'))
->searchable(['name_translations->de', 'name_translations->en'])
->sortable(),
BadgeColumn::make('eventType.name')
->label(__('Event Type'))
->color('info'),
IconColumn::make('tenant_id')
->label(__('Scope'))
->boolean()
->trueIcon('heroicon-o-user-group')
->falseIcon('heroicon-o-globe-alt')
->state(fn (TaskCollection $record) => $record->tenant_id !== null)
->tooltip(fn (TaskCollection $record) => $record->tenant_id ? __('Tenant-only') : __('Global template')),
TextColumn::make('tasks_count')
->label(__('Tasks'))
->counts('tasks')
->sortable(),
])
->filters([
SelectFilter::make('event_type_id')
->label(__('Event Type'))
->relationship('eventType', 'name->' . app()->getLocale()),
SelectFilter::make('scope')
->options([
'global' => __('Global template'),
'tenant' => __('Tenant-owned'),
])
->query(function ($query, $value) {
$tenantId = auth()->user()?->tenant_id;
if ($value === 'global') {
$query->whereNull('tenant_id');
}
if ($value === 'tenant') {
$query->where('tenant_id', $tenantId);
}
}),
])
->actions([
\Filament\Actions\Action::make('import')
->label(__('Import to Event'))
->icon('heroicon-o-cloud-arrow-down')
->form([
Select::make('event_slug')
->label(__('Select Event'))
->options(function () {
$tenantId = auth()->user()?->tenant_id;
return Event::where('tenant_id', $tenantId)
->orderBy('date', 'desc')
->get()
->mapWithKeys(function (Event $event) {
$name = $event->name[app()->getLocale()] ?? $event->name['de'] ?? reset($event->name);
return [
$event->slug => sprintf('%s (%s)', $name, $event->date?->format('d.m.Y')),
];
})->toArray();
})
->required()
->searchable(),
])
->action(function (TaskCollection $record, array $data) {
$event = Event::where('slug', $data['event_slug'])
->where('tenant_id', auth()->user()?->tenant_id)
->firstOrFail();
/** @var TaskCollectionImportService $service */
$service = app(TaskCollectionImportService::class);
$service->import($record, $event);
Notification::make()
->title(__('Task collection imported'))
->body(__('The collection :name has been imported.', ['name' => $record->name]))
->success()
->send();
}),
Actions\EditAction::make()
->label(__('Edit'))
->visible(fn (TaskCollection $record) => $record->tenant_id === auth()->user()?->tenant_id),
])
->headerActions([
Actions\CreateAction::make()
->label(__('Create Task Collection'))
->mutateFormDataUsing(function (array $data) {
$tenantId = auth()->user()?->tenant_id;
$data['tenant_id'] = $tenantId;
$data['slug'] = static::generateSlug($data['name_translations']['en'] ?? $data['name_translations']['de'] ?? 'collection', $tenantId);
return $data;
}),
])
->bulkActions([
Actions\DeleteBulkAction::make()
->visible(fn () => false),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListTaskCollections::route('/'),
'create' => Pages\CreateTaskCollection::route('/create'),
'edit' => Pages\EditTaskCollection::route('/{record}/edit'),
];
}
public static function getEloquentQuery(): Builder
{
$tenantId = auth()->user()?->tenant_id;
return parent::getEloquentQuery()
->forTenant($tenantId)
->with('eventType')
->withCount('tasks');
}
public static function getGloballySearchableAttributes(): array
{
return ['name_translations->de', 'name_translations->en'];
}
public static function generateSlug(string $base, int $tenantId): string
{
$slugBase = Str::slug($base) ?: 'collection';
do {
$candidate = $slugBase . '-' . $tenantId . '-' . Str::random(4);
} while (TaskCollection::where('slug', $candidate)->exists());
return $candidate;
}
public static function scopeEloquentQueryToTenant(Builder $query, ?Model $tenant): Builder
{
$tenant ??= Filament::getTenant();
if (! $tenant) {
return $query;
}
return $query->where(function (Builder $innerQuery) use ($tenant) {
$innerQuery->whereNull('tenant_id')
->orWhere('tenant_id', $tenant->getKey());
});
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\TaskCollectionResource\Pages;
use App\Filament\Tenant\Resources\TaskCollectionResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
class CreateTaskCollection extends CreateRecord
{
protected static string $resource = TaskCollectionResource::class;
protected function mutateFormDataBeforeCreate(array $data): array
{
$tenantId = Auth::user()?->tenant_id;
$data['tenant_id'] = $tenantId;
$data['slug'] = TaskCollectionResource::generateSlug(
$data['name_translations']['en'] ?? $data['name_translations']['de'] ?? 'collection',
$tenantId
);
return $data;
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\TaskCollectionResource\Pages;
use App\Filament\Tenant\Resources\TaskCollectionResource;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Auth;
class EditTaskCollection extends EditRecord
{
protected static string $resource = TaskCollectionResource::class;
protected function authorizeAccess(): void
{
parent::authorizeAccess();
$record = $this->getRecord();
if ($record->tenant_id !== Auth::user()?->tenant_id) {
abort(403);
}
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\TaskCollectionResource\Pages;
use App\Filament\Tenant\Resources\TaskCollectionResource;
use Filament\Resources\Pages\ListRecords;
class ListTaskCollections extends ListRecords
{
protected static string $resource = TaskCollectionResource::class;
}

View File

@@ -1,206 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources;
use App\Filament\Tenant\Resources\TaskResource\Pages;
use App\Models\Event;
use App\Models\Task;
use App\Support\TenantOnboardingState;
use BackedEnum;
use Filament\Facades\Filament;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Tabs as SchemaTabs;
use Filament\Schemas\Components\Tabs\Tab as SchemaTab;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Actions;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Model;
use UnitEnum;
class TaskResource extends Resource
{
protected static ?string $model = Task::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-check';
protected static ?int $navigationSort = 40;
public static function shouldRegisterNavigation(): bool
{
return TenantOnboardingState::completed();
}
public static function getNavigationGroup(): UnitEnum|string|null
{
return __('admin.nav.library');
}
public static function form(Schema $form): Schema
{
$tenantId = Auth::user()?->tenant_id;
return $form->schema([
Select::make('emotion_id')
->relationship('emotion', 'name')
->required()
->searchable()
->preload(),
Select::make('event_type_id')
->relationship('eventType', 'name')
->searchable()
->preload()
->label(__('admin.tasks.fields.event_type_optional')),
SchemaTabs::make('content_tabs')
->label(__('admin.tasks.fields.content_localization'))
->tabs([
SchemaTab::make(__('admin.common.german'))
->icon('heroicon-o-language')
->schema([
TextInput::make('title.de')
->label(__('admin.tasks.fields.title_de'))
->required(),
MarkdownEditor::make('description.de')
->label(__('admin.tasks.fields.description_de'))
->columnSpanFull(),
MarkdownEditor::make('example_text.de')
->label(__('admin.tasks.fields.example_de'))
->columnSpanFull(),
]),
SchemaTab::make(__('admin.common.english'))
->icon('heroicon-o-language')
->schema([
TextInput::make('title.en')
->label(__('admin.tasks.fields.title_en'))
->required(),
MarkdownEditor::make('description.en')
->label(__('admin.tasks.fields.description_en'))
->columnSpanFull(),
MarkdownEditor::make('example_text.en')
->label(__('admin.tasks.fields.example_en'))
->columnSpanFull(),
]),
])
->columnSpanFull(),
Select::make('difficulty')
->label(__('admin.tasks.fields.difficulty.label'))
->options([
'easy' => __('admin.tasks.fields.difficulty.easy'),
'medium' => __('admin.tasks.fields.difficulty.medium'),
'hard' => __('admin.tasks.fields.difficulty.hard'),
])
->default('easy'),
TextInput::make('sort_order')
->numeric()
->default(0),
Toggle::make('is_active')
->default(true),
Select::make('assigned_events')
->label(__('admin.tasks.fields.events'))
->multiple()
->relationship(
'assignedEvents',
'name',
fn (Builder $query) => $tenantId
? $query->where('tenant_id', $tenantId)
: $query
)
->searchable()
->preload()
->getOptionLabelFromRecordUsing(fn (Event $record) => $record->name)
->helperText(__('admin.tasks.fields.events_helper')),
])->columns(2);
}
public static function table(Table $table): Table
{
$tenantId = Auth::user()?->tenant_id;
return $table
->columns([
Tables\Columns\TextColumn::make('id')
->label('#')
->sortable(),
Tables\Columns\TextColumn::make('title')
->label(__('admin.tasks.table.title'))
->getStateUsing(function ($record) {
$value = $record->title;
if (is_array($value)) {
$loc = app()->getLocale();
return $value[$loc] ?? ($value['de'] ?? ($value['en'] ?? ''));
}
return (string) $value;
})
->limit(60)
->searchable(['title->de', 'title->en']),
Tables\Columns\TextColumn::make('emotion.name')
->label(__('admin.tasks.fields.emotion'))
->toggleable(),
Tables\Columns\TextColumn::make('eventType.name')
->label(__('admin.tasks.fields.event_type'))
->toggleable(),
Tables\Columns\TextColumn::make('assignedEvents.name')
->label(__('admin.tasks.table.events'))
->badge()
->separator(', ')
->limitList(2),
Tables\Columns\TextColumn::make('difficulty')
->label(__('admin.tasks.fields.difficulty.label'))
->badge(),
Tables\Columns\IconColumn::make('is_active')
->label(__('admin.tasks.table.is_active'))
->boolean(),
Tables\Columns\TextColumn::make('sort_order')
->label(__('admin.tasks.table.sort_order'))
->sortable(),
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\DeleteAction::make(),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
])
->modifyQueryUsing(function (Builder $query) use ($tenantId) {
if (! $tenantId) {
return $query;
}
$query->forTenant($tenantId);
return $query;
});
}
public static function getPages(): array
{
return [
'index' => Pages\ListTasks::route('/'),
'create' => Pages\CreateTask::route('/create'),
'edit' => Pages\EditTask::route('/{record}/edit'),
];
}
public static function scopeEloquentQueryToTenant(Builder $query, ?Model $tenant): Builder
{
$tenant ??= Filament::getTenant();
if (! $tenant) {
return $query;
}
return $query->where(function (Builder $innerQuery) use ($tenant) {
$innerQuery->whereNull('tenant_id')
->orWhere('tenant_id', $tenant->getKey());
});
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\TaskResource\Pages;
use App\Filament\Tenant\Resources\TaskResource;
use Filament\Resources\Pages\CreateRecord;
class CreateTask extends CreateRecord
{
protected static string $resource = TaskResource::class;
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\TaskResource\Pages;
use App\Filament\Tenant\Resources\TaskResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTask extends EditRecord
{
protected static string $resource = TaskResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Filament\Tenant\Resources\TaskResource\Pages;
use App\Filament\Tenant\Resources\TaskResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListTasks extends ListRecords
{
protected static string $resource = TaskResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Filament\Tenant\Widgets;
use Filament\Tables;
use Filament\Widgets\TableWidget as BaseWidget;
use Illuminate\Support\Carbon;
use App\Models\Event;
class EventsActiveToday extends BaseWidget
{
protected static ?string $heading = null;
public function getHeading()
{
return __('admin.widgets.events_active_today.heading');
}
protected ?string $pollingInterval = '60s';
public function table(Tables\Table $table): Tables\Table
{
$today = Carbon::today()->toDateString();
return $table
->query(
Event::query()
->where('is_active', true)
->whereDate('date', '<=', $today)
->withCount([
'photos as uploads_today' => function ($q) use ($today) {
$q->whereDate('created_at', $today);
},
])
->orderByDesc('date')
->limit(10)
)
->columns([
Tables\Columns\TextColumn::make('id')->label(__('admin.common.hash'))->width('60px'),
Tables\Columns\TextColumn::make('slug')->label(__('admin.common.slug'))->searchable(),
Tables\Columns\TextColumn::make('date')->date(),
Tables\Columns\TextColumn::make('uploads_today')->label(__('admin.common.uploads_today'))->numeric(),
])
->paginated(false);
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Filament\Tenant\Widgets;
use Filament\Tables;
use Filament\Widgets\TableWidget as BaseWidget;
use App\Models\Photo;
use Filament\Actions;
class RecentPhotosTable extends BaseWidget
{
protected static ?string $heading = null;
public function getHeading()
{
return __('admin.widgets.recent_uploads.heading');
}
protected int|string|array $columnSpan = 'full';
public function table(Tables\Table $table): Tables\Table
{
return $table
->query(
Photo::query()
->orderByDesc('created_at')
->limit(10)
)
->columns([
Tables\Columns\ImageColumn::make('thumbnail_path')->label(__('admin.common.thumb'))->circular(),
Tables\Columns\TextColumn::make('id')->label(__('admin.common.hash')),
Tables\Columns\TextColumn::make('event_id')->label(__('admin.common.event')),
Tables\Columns\TextColumn::make('likes_count')->label(__('admin.common.likes')),
Tables\Columns\TextColumn::make('created_at')->since(),
])
->actions([
Actions\Action::make('feature')
->label(__('admin.photos.actions.feature'))
->visible(fn(Photo $record) => ! (bool)($record->is_featured ?? 0))
->action(fn(Photo $record) => $record->update(['is_featured' => 1])),
Actions\Action::make('unfeature')
->label(__('admin.photos.actions.unfeature'))
->visible(fn(Photo $record) => (bool)($record->is_featured ?? 0))
->action(fn(Photo $record) => $record->update(['is_featured' => 0])),
])
->paginated(false);
}
}

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Filament\Tenant\Widgets;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;
class UploadsPerDayChart extends ChartWidget
{
protected ?string $heading = null;
protected ?string $maxHeight = '220px';
protected ?string $pollingInterval = '60s';
protected function getData(): array
{
// Build last 14 days labels
$labels = [];
$start = Carbon::now()->startOfDay()->subDays(13);
for ($i = 0; $i < 14; $i++) {
$labels[] = $start->copy()->addDays($i)->format('Y-m-d');
}
// SQLite-friendly group by date
$rows = DB::table('photos')
->selectRaw("strftime('%Y-%m-%d', created_at) as d, count(*) as c")
->where('created_at', '>=', $start)
->groupBy('d')
->orderBy('d')
->get();
$map = collect($rows)->keyBy('d');
$data = array_map(fn ($d) => (int) ($map[$d]->c ?? 0), $labels);
return [
'labels' => $labels,
'datasets' => [
[
'label' => __('admin.common.uploads'),
'data' => $data,
'borderColor' => '#f59e0b',
'backgroundColor' => 'rgba(245, 158, 11, 0.2)',
'tension' => 0.3,
],
],
];
}
protected function getType(): string
{
return 'line';
}
public function getHeading(): string|\Illuminate\Contracts\Support\Htmlable|null
{
return __('admin.widgets.uploads_per_day.heading');
}
}

View File

@@ -156,7 +156,7 @@ class AuthenticatedSessionController extends Controller
// Super admins go to Filament superadmin panel
if ($user && $user->role === 'super_admin') {
return '/admin';
return '/super-admin';
}
// Tenant admins go to their PWA dashboard

View File

@@ -113,7 +113,7 @@ class RedirectIfAuthenticated extends BaseMiddleware
}
if ($user && $user->role === 'super_admin') {
return '/admin';
return '/super-admin';
}
if ($user && $user->role === 'user') {

View File

@@ -301,8 +301,5 @@ class AppServiceProvider extends ServiceProvider
]);
});
if ($this->app->runningInConsole()) {
$this->app->register(\App\Providers\Filament\AdminPanelProvider::class);
}
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->path('admin')
->brandName('Fotospiel Studio')
->login(\App\Filament\Pages\Auth\Login::class)
->colors([
'primary' => Color::Pink,
])
->homeUrl(fn () => \App\Filament\Tenant\Pages\TenantOnboarding::getUrl())
->discoverResources(in: app_path('Filament/Tenant/Resources'), for: 'App\\Filament\\Tenant\\Resources')
->discoverPages(in: app_path('Filament/Tenant/Pages'), for: 'App\\Filament\\Tenant\\Pages')
->pages([])
->discoverWidgets(in: app_path('Filament/Tenant/Widgets'), for: 'App\\Filament\\Tenant\\Widgets')
->widgets([
Widgets\AccountWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->tenant(\App\Models\Tenant::class)
// Remove blog models as they are global and handled in SuperAdmin
;
}
}