tenant admin startseite schicker gestaltet und super-admin und tenant admin (filament) aufgesplittet.
Es gibt nun task collections und vordefinierte tasks für alle. Onboarding verfeinert und webseite-carousel gefixt (logging später entfernen!)
This commit is contained in:
@@ -3,45 +3,46 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\EventResource\Pages;
|
||||
use App\Support\JoinTokenLayoutRegistry;
|
||||
use App\Filament\Resources\EventResource\RelationManagers\EventPackagesRelationManager;
|
||||
use App\Models\Event;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\EventType;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\JoinTokenLayoutRegistry;
|
||||
use BackedEnum;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Select;
|
||||
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 UnitEnum;
|
||||
use BackedEnum;
|
||||
|
||||
use App\Filament\Resources\EventResource\RelationManagers\EventPackagesRelationManager;
|
||||
|
||||
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;
|
||||
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.platform');
|
||||
}
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
{
|
||||
return $form->schema([
|
||||
Select::make('tenant_id')
|
||||
->label(__('admin.events.fields.tenant'))
|
||||
->options(Tenant::all()->pluck('name', 'id'))
|
||||
->options(Tenant::query()->pluck('name', 'id'))
|
||||
->searchable()
|
||||
->required(),
|
||||
TextInput::make('name')
|
||||
@@ -58,11 +59,11 @@ class EventResource extends Resource
|
||||
->required(),
|
||||
Select::make('event_type_id')
|
||||
->label(__('admin.events.fields.type'))
|
||||
->options(EventType::all()->pluck('name', 'id'))
|
||||
->options(EventType::query()->pluck('name', 'id'))
|
||||
->searchable(),
|
||||
Select::make('package_id')
|
||||
->label(__('admin.events.fields.package'))
|
||||
->options(\App\Models\Package::where('type', 'endcustomer')->pluck('name', 'id'))
|
||||
->options(\App\Models\Package::query()->where('type', 'endcustomer')->pluck('name', 'id'))
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
@@ -85,7 +86,7 @@ class EventResource extends Resource
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('tenant_id')->label(__('admin.events.table.tenant'))->sortable(),
|
||||
Tables\Columns\TextColumn::make('tenant.name')->label(__('admin.events.table.tenant'))->searchable(),
|
||||
Tables\Columns\TextColumn::make('name')->limit(30),
|
||||
Tables\Columns\TextColumn::make('slug')->searchable(),
|
||||
Tables\Columns\TextColumn::make('date')->date(),
|
||||
@@ -106,9 +107,9 @@ class EventResource extends Resource
|
||||
Tables\Columns\TextColumn::make('primary_join_token')
|
||||
->label(__('admin.events.table.join'))
|
||||
->getStateUsing(function ($record) {
|
||||
$token = $record->joinTokens()->orderByDesc('created_at')->first();
|
||||
$token = $record->joinTokens()->latest()->first();
|
||||
|
||||
return $token ? url('/e/'.$token->token) : __('admin.events.table.no_join_tokens');
|
||||
return $token ? url('/e/' . $token->token) : __('admin.events.table.no_join_tokens');
|
||||
})
|
||||
->description(function ($record) {
|
||||
$total = $record->joinTokens()->count();
|
||||
@@ -127,7 +128,7 @@ class EventResource extends Resource
|
||||
Actions\Action::make('toggle')
|
||||
->label(__('admin.events.actions.toggle_active'))
|
||||
->icon('heroicon-o-power')
|
||||
->action(fn($record) => $record->update(['is_active' => !$record->is_active])),
|
||||
->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')
|
||||
@@ -152,7 +153,7 @@ class EventResource extends Resource
|
||||
'id' => $token->id,
|
||||
'label' => $token->label,
|
||||
'token' => $token->token,
|
||||
'url' => url('/e/'.$token->token),
|
||||
'url' => url('/e/' . $token->token),
|
||||
'usage_limit' => $token->usage_limit,
|
||||
'usage_count' => $token->usage_count,
|
||||
'expires_at' => optional($token->expires_at)->toIso8601String(),
|
||||
@@ -178,19 +179,20 @@ class EventResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListEvents::route('/'),
|
||||
'view' => Pages\ViewEvent::route('/{record}'),
|
||||
'edit' => Pages\EditEvent::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
EventPackagesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
11
app/Filament/Resources/EventResource/Pages/CreateEvent.php
Normal file
11
app/Filament/Resources/EventResource/Pages/CreateEvent.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EventResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EventResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateEvent extends CreateRecord
|
||||
{
|
||||
protected static string $resource = EventResource::class;
|
||||
}
|
||||
@@ -2,26 +2,24 @@
|
||||
|
||||
namespace App\Filament\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 App\Models\EventPackage;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
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
|
||||
{
|
||||
@@ -30,29 +28,32 @@ class EventPackagesRelationManager extends RelationManager
|
||||
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(),
|
||||
]);
|
||||
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(),
|
||||
DateTimePicker::make('expires_at')
|
||||
->label('Ablauf')
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
@@ -90,9 +91,7 @@ class EventPackagesRelationManager extends RelationManager
|
||||
->money('EUR')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->filters([])
|
||||
->headerActions([
|
||||
CreateAction::make(),
|
||||
])
|
||||
@@ -121,9 +120,8 @@ class EventPackagesRelationManager extends RelationManager
|
||||
return __('admin.events.relation_managers.event_packages.title');
|
||||
}
|
||||
|
||||
public function getTableQuery(): Builder | Relation
|
||||
public function getTableQuery(): Builder|Relation
|
||||
{
|
||||
return parent::getTableQuery()
|
||||
->with('package');
|
||||
return parent::getTableQuery()->with('package');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,46 +3,47 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\PhotoResource\Pages;
|
||||
use App\Models\Photo;
|
||||
use App\Models\Event;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use App\Models\Photo;
|
||||
use BackedEnum;
|
||||
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 Filament\Forms\Components\Select;
|
||||
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 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;
|
||||
|
||||
protected static ?int $navigationSort = 30;
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.content');
|
||||
}
|
||||
protected static ?int $navigationSort = 30;
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
{
|
||||
return $form->schema([
|
||||
Select::make('event_id')
|
||||
->label(__('admin.photos.fields.event'))
|
||||
->options(Event::all()->pluck('name', 'id'))
|
||||
->options(Event::query()->orderBy('name')->pluck('name', 'id'))
|
||||
->searchable()
|
||||
->required(),
|
||||
FileUpload::make('file_path')
|
||||
->label(__('admin.photos.fields.photo'))
|
||||
->image() // enable FilePond image preview
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('photos')
|
||||
->visibility('public')
|
||||
@@ -61,9 +62,14 @@ class PhotoResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('file_path')->label(__('admin.photos.table.photo'))->disk('public')->visibility('public'),
|
||||
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_id')->label(__('admin.photos.table.event')),
|
||||
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(),
|
||||
@@ -73,13 +79,13 @@ class PhotoResource extends Resource
|
||||
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]))
|
||||
->visible(fn (Photo $record) => ! $record->is_featured)
|
||||
->action(fn (Photo $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]))
|
||||
->visible(fn (Photo $record) => $record->is_featured)
|
||||
->action(fn (Photo $record) => $record->update(['is_featured' => false]))
|
||||
->icon('heroicon-o-star'),
|
||||
Actions\DeleteAction::make(),
|
||||
])
|
||||
@@ -87,11 +93,11 @@ class PhotoResource extends Resource
|
||||
Actions\BulkAction::make('feature')
|
||||
->label(__('admin.photos.actions.feature_selected'))
|
||||
->icon('heroicon-o-star')
|
||||
->action(fn($records) => $records->each->update(['is_featured' => true])),
|
||||
->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])),
|
||||
->action(fn ($records) => $records->each->update(['is_featured' => false])),
|
||||
Actions\DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -4,28 +4,25 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TenantPackageResource\Pages;
|
||||
use App\Models\TenantPackage;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Icons\Icon;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use UnitEnum;
|
||||
use BackedEnum;
|
||||
|
||||
class TenantPackageResource extends Resource
|
||||
{
|
||||
@@ -33,31 +30,23 @@ class TenantPackageResource extends Resource
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-shopping-bag';
|
||||
|
||||
protected static ?string $navigationLabel = 'Packages';
|
||||
|
||||
protected static ?string $slug = 'tenant-packages';
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('Package Details')
|
||||
->schema([
|
||||
Select::make('package_id')
|
||||
->relationship('package', 'name')
|
||||
->required()
|
||||
->searchable(),
|
||||
Select::make('tenant_id')
|
||||
->relationship('tenant', 'name')
|
||||
->required()
|
||||
->default(fn () => Auth::user()->tenant_id)
|
||||
->disabled(),
|
||||
DateTimePicker::make('expires_at')
|
||||
->required(),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
])
|
||||
->columns(1),
|
||||
Select::make('tenant_id')
|
||||
->relationship('tenant', 'name')
|
||||
->required()
|
||||
->searchable(),
|
||||
Select::make('package_id')
|
||||
->relationship('package', 'name')
|
||||
->required()
|
||||
->searchable(),
|
||||
DateTimePicker::make('purchased_at'),
|
||||
DateTimePicker::make('expires_at'),
|
||||
Toggle::make('active')->default(true),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -65,26 +54,13 @@ class TenantPackageResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('package.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('tenant.name')
|
||||
->badge()
|
||||
->color('success'),
|
||||
TextColumn::make('expires_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
IconColumn::make('is_active')
|
||||
->boolean(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
TextColumn::make('tenant.name')->searchable()->sortable(),
|
||||
TextColumn::make('package.name')->badge()->color('success'),
|
||||
TextColumn::make('purchased_at')->dateTime()->sortable(),
|
||||
TextColumn::make('expires_at')->dateTime()->sortable(),
|
||||
IconColumn::make('active')->boolean(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
ViewAction::make(),
|
||||
@@ -96,15 +72,12 @@ class TenantPackageResource extends Resource
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('tenant_id', Auth::user()->tenant_id));
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@@ -116,4 +89,4 @@ class TenantPackageResource extends Resource
|
||||
'edit' => Pages\EditTenantPackage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,9 @@
|
||||
namespace App\Filament\Resources\TenantPackageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TenantPackageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class CreateTenantPackage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = TenantPackageResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['tenant_id'] = Auth::user()->tenant_id;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,4 @@ class EditTenantPackage extends EditRecord
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ namespace App\Filament\Resources\TenantPackageResource\Pages;
|
||||
use App\Filament\Resources\TenantPackageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ListTenantPackages extends ListRecords
|
||||
{
|
||||
@@ -18,9 +16,4 @@ class ListTenantPackages extends ListRecords
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return parent::getTableQuery()->where('tenant_id', Auth::user()->tenant_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ class ViewTenantPackage extends ViewRecord
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,36 +4,30 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\UserResource\Pages;
|
||||
use App\Models\User;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Icons\Icon;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use BackedEnum;
|
||||
use UnitEnum;
|
||||
|
||||
class UserResource extends Resource
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-user-circle';
|
||||
|
||||
protected static ?string $navigationLabel = 'Users';
|
||||
|
||||
protected static ?string $slug = 'users';
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
@@ -76,8 +70,7 @@ class UserResource extends Resource
|
||||
->required(fn (string $operation): bool => $operation === 'create')
|
||||
->dehydrated(false),
|
||||
])
|
||||
->columns(1)
|
||||
->visible(fn (): bool => Auth::user()?->id === Request::route('record')),
|
||||
->columns(1),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -85,22 +78,18 @@ class UserResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('fullName')
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
->searchable(),
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('phone'),
|
||||
TextColumn::make('fullName')->sortable()->searchable(),
|
||||
TextColumn::make('email')->searchable(),
|
||||
TextColumn::make('username')->searchable(),
|
||||
TextColumn::make('tenant.name')
|
||||
->label('Tenant'),
|
||||
TextColumn::make('email_verified_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
->label(__('admin.common.tenant'))
|
||||
->badge(),
|
||||
TextColumn::make('phone'),
|
||||
IconColumn::make('email_verified_at')
|
||||
->label(__('admin.users.fields.verified'))
|
||||
->boolean(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
ViewAction::make(),
|
||||
@@ -111,23 +100,20 @@ class UserResource extends Resource
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('tenant_id', Auth::user()->tenant_id));
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
'create' => Pages\CreateUser::route('/create'),
|
||||
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
app/Filament/Resources/UserResource/Pages/CreateUser.php
Normal file
21
app/Filament/Resources/UserResource/Pages/CreateUser.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class CreateUser extends CreateRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,4 @@ class EditUser extends EditRecord
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ namespace App\Filament\Resources\UserResource\Pages;
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
@@ -18,11 +16,4 @@ class ListUsers extends ListRecords
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordClasses(fn (User $record) => $record->id === auth()->id() ? 'border-2 border-blue-500' : '')
|
||||
->poll('30s');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
186
app/Filament/Tenant/Pages/InviteStudio.php
Normal file
186
app/Filament/Tenant/Pages/InviteStudio.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?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 Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use BackedEnum;
|
||||
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('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();
|
||||
}
|
||||
}
|
||||
311
app/Filament/Tenant/Pages/TenantOnboarding.php
Normal file
311
app/Filament/Tenant/Pages/TenantOnboarding.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?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 Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use BackedEnum;
|
||||
use UnitEnum;
|
||||
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;
|
||||
|
||||
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('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');
|
||||
}
|
||||
}
|
||||
209
app/Filament/Tenant/Resources/EventResource.php
Normal file
209
app/Filament/Tenant/Resources/EventResource.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources;
|
||||
|
||||
use App\Filament\Tenant\Resources\EventResource\Pages;
|
||||
use App\Support\JoinTokenLayoutRegistry;
|
||||
use App\Support\TenantOnboardingState;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventType;
|
||||
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\DatePicker;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use UnitEnum;
|
||||
use BackedEnum;
|
||||
|
||||
use App\Filament\Tenant\Resources\EventResource\RelationManagers\EventPackagesRelationManager;
|
||||
|
||||
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()
|
||||
->map(function ($token) use ($record) {
|
||||
$layouts = JoinTokenLayoutRegistry::toResponse(function (string $layoutId, string $format) use ($record, $token) {
|
||||
return route('tenant.events.join-tokens.layouts.download', [
|
||||
'event' => $record->slug,
|
||||
'joinToken' => $token->getKey(),
|
||||
'layout' => $layoutId,
|
||||
'format' => $format,
|
||||
]);
|
||||
});
|
||||
|
||||
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('tenant.events.join-tokens.layouts.index', [
|
||||
'event' => $record->slug,
|
||||
'joinToken' => $token->getKey(),
|
||||
]),
|
||||
];
|
||||
});
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
126
app/Filament/Tenant/Resources/PhotoResource.php
Normal file
126
app/Filament/Tenant/Resources/PhotoResource.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
242
app/Filament/Tenant/Resources/TaskCollectionResource.php
Normal file
242
app/Filament/Tenant/Resources/TaskCollectionResource.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
206
app/Filament/Tenant/Resources/TaskResource.php
Normal file
206
app/Filament/Tenant/Resources/TaskResource.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
namespace App\Filament\Tenant\Widgets;
|
||||
|
||||
use Filament\Tables;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
namespace App\Filament\Tenant\Widgets;
|
||||
|
||||
use Filament\Tables;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
namespace App\Filament\Tenant\Widgets;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -53,4 +53,4 @@ class UploadsPerDayChart extends ChartWidget
|
||||
{
|
||||
return __('admin.widgets.uploads_per_day.heading');
|
||||
}
|
||||
}
|
||||
}
|
||||
157
app/Http/Controllers/Api/Tenant/EmotionController.php
Normal file
157
app/Http/Controllers/Api/Tenant/EmotionController.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Tenant;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Tenant\EmotionStoreRequest;
|
||||
use App\Http\Requests\Tenant\EmotionUpdateRequest;
|
||||
use App\Http\Resources\Tenant\EmotionResource;
|
||||
use App\Models\Emotion;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EmotionController extends Controller
|
||||
{
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$tenantId = $request->tenant->id;
|
||||
|
||||
$query = Emotion::query()
|
||||
->whereNull('tenant_id')
|
||||
->orWhere('tenant_id', $tenantId)
|
||||
->with('eventTypes');
|
||||
|
||||
if ($request->boolean('only_tenant')) {
|
||||
$query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
if ($request->boolean('only_global')) {
|
||||
$query->whereNull('tenant_id');
|
||||
}
|
||||
|
||||
$query->orderByRaw('tenant_id is null desc')->orderBy('sort_order')->orderBy('id');
|
||||
|
||||
$emotions = $query->paginate($request->integer('per_page', 25));
|
||||
|
||||
return EmotionResource::collection($emotions);
|
||||
}
|
||||
|
||||
public function store(EmotionStoreRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
$payload = [
|
||||
'tenant_id' => $request->tenant->id,
|
||||
'name' => $this->localizeValue($data['name']),
|
||||
'description' => $this->localizeValue($data['description'] ?? null, allowNull: true),
|
||||
'icon' => $data['icon'] ?? 'lucide-smile',
|
||||
'color' => $this->normalizeColor($data['color'] ?? '#6366f1'),
|
||||
'sort_order' => $data['sort_order'] ?? 0,
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
];
|
||||
|
||||
$emotion = null;
|
||||
|
||||
DB::transaction(function () use (&$emotion, $payload, $data) {
|
||||
$emotion = Emotion::create($payload);
|
||||
|
||||
if (! empty($data['event_type_ids'])) {
|
||||
$emotion->eventTypes()->sync($data['event_type_ids']);
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'message' => __('Emotion erfolgreich erstellt.'),
|
||||
'data' => new EmotionResource($emotion->fresh('eventTypes')),
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(EmotionUpdateRequest $request, Emotion $emotion): JsonResponse
|
||||
{
|
||||
if ($emotion->tenant_id && $emotion->tenant_id !== $request->tenant->id) {
|
||||
abort(403, 'Emotion gehört nicht zu diesem Tenant.');
|
||||
}
|
||||
|
||||
if (is_null($emotion->tenant_id) && $request->hasAny(['name', 'description', 'icon', 'color', 'sort_order'])) {
|
||||
abort(403, 'Globale Emotions können nicht bearbeitet werden.');
|
||||
}
|
||||
|
||||
$data = $request->validated();
|
||||
|
||||
DB::transaction(function () use ($emotion, $data) {
|
||||
$update = [];
|
||||
|
||||
if (array_key_exists('name', $data)) {
|
||||
$update['name'] = $this->localizeValue($data['name'], allowNull: false, fallback: $emotion->name);
|
||||
}
|
||||
|
||||
if (array_key_exists('description', $data)) {
|
||||
$update['description'] = $this->localizeValue($data['description'], allowNull: true, fallback: $emotion->description);
|
||||
}
|
||||
|
||||
if (array_key_exists('icon', $data)) {
|
||||
$update['icon'] = $data['icon'] ?? $emotion->icon;
|
||||
}
|
||||
|
||||
if (array_key_exists('color', $data)) {
|
||||
$update['color'] = $this->normalizeColor($data['color'] ?? $emotion->color);
|
||||
}
|
||||
|
||||
if (array_key_exists('sort_order', $data)) {
|
||||
$update['sort_order'] = $data['sort_order'] ?? 0;
|
||||
}
|
||||
|
||||
if (array_key_exists('is_active', $data)) {
|
||||
$update['is_active'] = $data['is_active'];
|
||||
}
|
||||
|
||||
if (! empty($update)) {
|
||||
$emotion->update($update);
|
||||
}
|
||||
|
||||
if (array_key_exists('event_type_ids', $data)) {
|
||||
$emotion->eventTypes()->sync($data['event_type_ids'] ?? []);
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'message' => __('Emotion aktualisiert.'),
|
||||
'data' => new EmotionResource($emotion->fresh('eventTypes')),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function localizeValue(mixed $value, bool $allowNull = false, ?array $fallback = null): ?array
|
||||
{
|
||||
if ($allowNull && ($value === null || $value === '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$filtered = array_filter($value, static fn ($text) => is_string($text) && $text !== '');
|
||||
if (! empty($filtered)) {
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
return $allowNull ? null : ($fallback ?? []);
|
||||
}
|
||||
|
||||
if (is_string($value) && $value !== '') {
|
||||
$locale = app()->getLocale() ?: 'de';
|
||||
return [$locale => $value];
|
||||
}
|
||||
|
||||
return $allowNull ? null : $fallback;
|
||||
}
|
||||
|
||||
protected function normalizeColor(string $color): string
|
||||
{
|
||||
$normalized = ltrim($color, '#');
|
||||
if (strlen($normalized) === 6) {
|
||||
return '#' . strtolower($normalized);
|
||||
}
|
||||
|
||||
return '#6366f1';
|
||||
}
|
||||
}
|
||||
94
app/Http/Controllers/Api/Tenant/TaskCollectionController.php
Normal file
94
app/Http/Controllers/Api/Tenant/TaskCollectionController.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Tenant;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Tenant\TaskCollectionResource;
|
||||
use App\Models\Event;
|
||||
use App\Models\TaskCollection;
|
||||
use App\Services\Tenant\TaskCollectionImportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TaskCollectionController extends Controller
|
||||
{
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$tenantId = $request->tenant->id;
|
||||
|
||||
$query = TaskCollection::query()
|
||||
->forTenant($tenantId)
|
||||
->with('eventType')
|
||||
->withCount('tasks')
|
||||
->orderBy('position')
|
||||
->orderBy('id');
|
||||
|
||||
if ($search = $request->query('search')) {
|
||||
$query->where(function ($inner) use ($search) {
|
||||
$inner->where('name_translations->de', 'like', "%{$search}%")
|
||||
->orWhere('name_translations->en', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($eventTypeSlug = $request->query('event_type')) {
|
||||
$query->whereHas('eventType', fn ($q) => $q->where('slug', $eventTypeSlug));
|
||||
}
|
||||
|
||||
if ($request->boolean('only_global')) {
|
||||
$query->whereNull('tenant_id');
|
||||
}
|
||||
|
||||
if ($request->boolean('only_tenant')) {
|
||||
$query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
$perPage = $request->integer('per_page', 15);
|
||||
|
||||
return TaskCollectionResource::collection(
|
||||
$query->paginate($perPage)
|
||||
);
|
||||
}
|
||||
|
||||
public function show(Request $request, TaskCollection $collection): JsonResponse
|
||||
{
|
||||
$this->authorizeAccess($request, $collection);
|
||||
|
||||
$collection->load(['eventType', 'tasks' => fn ($query) => $query->with('assignedEvents')]);
|
||||
|
||||
return response()->json(new TaskCollectionResource($collection));
|
||||
}
|
||||
|
||||
public function activate(
|
||||
Request $request,
|
||||
TaskCollection $collection,
|
||||
TaskCollectionImportService $importService
|
||||
): JsonResponse {
|
||||
$this->authorizeAccess($request, $collection);
|
||||
|
||||
$data = $request->validate([
|
||||
'event_slug' => ['required', 'string', Rule::exists('events', 'slug')->where('tenant_id', $request->tenant->id)],
|
||||
]);
|
||||
|
||||
$event = Event::where('slug', $data['event_slug'])
|
||||
->where('tenant_id', $request->tenant->id)
|
||||
->firstOrFail();
|
||||
|
||||
$result = $importService->import($collection, $event);
|
||||
|
||||
return response()->json([
|
||||
'message' => __('Task-Collection erfolgreich importiert.'),
|
||||
'collection' => new TaskCollectionResource($result['collection']->load('eventType')->loadCount('tasks')),
|
||||
'created_task_ids' => $result['created_task_ids'],
|
||||
'attached_task_ids' => $result['attached_task_ids'],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function authorizeAccess(Request $request, TaskCollection $collection): void
|
||||
{
|
||||
if ($collection->tenant_id && $collection->tenant_id !== $request->tenant->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,14 +23,27 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$query = Task::where('tenant_id', $request->tenant->id)
|
||||
$tenantId = $request->tenant->id;
|
||||
|
||||
$query = Task::query()
|
||||
->where(function ($inner) use ($tenantId) {
|
||||
$inner->whereNull('tenant_id')
|
||||
->orWhere('tenant_id', $tenantId);
|
||||
})
|
||||
->with(['taskCollection', 'assignedEvents'])
|
||||
->orderByRaw('tenant_id is null desc')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
// Search and filters
|
||||
if ($search = $request->get('search')) {
|
||||
$query->where('title', 'like', "%{$search}%")
|
||||
->orWhere('description', 'like', "%{$search}%");
|
||||
$query->where(function ($inner) use ($search) {
|
||||
$like = '%' . $search . '%';
|
||||
$inner->where('title->de', 'like', $like)
|
||||
->orWhere('title->en', 'like', $like)
|
||||
->orWhere('description->de', 'like', $like)
|
||||
->orWhere('description->en', 'like', $like);
|
||||
});
|
||||
}
|
||||
|
||||
if ($collectionId = $request->get('collection_id')) {
|
||||
@@ -55,15 +68,19 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function store(TaskStoreRequest $request): JsonResponse
|
||||
{
|
||||
$task = Task::create(array_merge($request->validated(), [
|
||||
'tenant_id' => $request->tenant->id,
|
||||
]));
|
||||
$collectionId = $request->input('collection_id');
|
||||
$collection = $collectionId ? $this->resolveAccessibleCollection($request, $collectionId) : null;
|
||||
|
||||
if ($collectionId = $request->input('collection_id')) {
|
||||
$task->taskCollection()->associate(TaskCollection::findOrFail($collectionId));
|
||||
$task->save();
|
||||
$payload = $this->prepareTaskPayload($request->validated(), $request->tenant->id);
|
||||
$payload['tenant_id'] = $request->tenant->id;
|
||||
|
||||
if ($collection) {
|
||||
$payload['collection_id'] = $collection->id;
|
||||
$payload['source_collection_id'] = $collection->source_collection_id ?? $collection->id;
|
||||
}
|
||||
|
||||
$task = Task::create($payload);
|
||||
|
||||
$task->load(['taskCollection', 'assignedEvents']);
|
||||
|
||||
return response()->json([
|
||||
@@ -81,7 +98,7 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function show(Request $request, Task $task): JsonResponse
|
||||
{
|
||||
if ($task->tenant_id !== $request->tenant->id) {
|
||||
if ($task->tenant_id && $task->tenant_id !== $request->tenant->id) {
|
||||
abort(404, 'Task nicht gefunden.');
|
||||
}
|
||||
|
||||
@@ -103,13 +120,18 @@ class TaskController extends Controller
|
||||
abort(404, 'Task nicht gefunden.');
|
||||
}
|
||||
|
||||
$task->update($request->validated());
|
||||
$collectionId = $request->input('collection_id');
|
||||
$collection = $collectionId ? $this->resolveAccessibleCollection($request, $collectionId) : null;
|
||||
|
||||
if ($collectionId = $request->input('collection_id')) {
|
||||
$task->taskCollection()->associate(TaskCollection::findOrFail($collectionId));
|
||||
$task->save();
|
||||
$payload = $this->prepareTaskPayload($request->validated(), $request->tenant->id, $task);
|
||||
|
||||
if ($collection) {
|
||||
$payload['collection_id'] = $collection->id;
|
||||
$payload['source_collection_id'] = $collection->source_collection_id ?? $collection->id;
|
||||
}
|
||||
|
||||
$task->update($payload);
|
||||
|
||||
$task->load(['taskCollection', 'assignedEvents']);
|
||||
|
||||
return response()->json([
|
||||
@@ -228,7 +250,7 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function fromCollection(Request $request, TaskCollection $collection): AnonymousResourceCollection
|
||||
{
|
||||
if ($collection->tenant_id !== $request->tenant->id) {
|
||||
if ($collection->tenant_id && $collection->tenant_id !== $request->tenant->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
@@ -239,4 +261,98 @@ class TaskController extends Controller
|
||||
|
||||
return TaskResource::collection($tasks);
|
||||
}
|
||||
}
|
||||
|
||||
protected function resolveAccessibleCollection(Request $request, int|string $collectionId): TaskCollection
|
||||
{
|
||||
return TaskCollection::where('id', $collectionId)
|
||||
->where(function ($query) use ($request) {
|
||||
$query->whereNull('tenant_id');
|
||||
|
||||
if ($request->tenant?->id) {
|
||||
$query->orWhere('tenant_id', $request->tenant->id);
|
||||
}
|
||||
})
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
protected function prepareTaskPayload(array $data, int $tenantId, ?Task $original = null): array
|
||||
{
|
||||
if (array_key_exists('title', $data)) {
|
||||
$data['title'] = $this->normalizeTranslations($data['title'], $original?->title);
|
||||
} elseif (array_key_exists('title_translations', $data)) {
|
||||
$data['title'] = $this->normalizeTranslations($data['title_translations'], $original?->title);
|
||||
}
|
||||
|
||||
if (array_key_exists('description', $data)) {
|
||||
$data['description'] = $this->normalizeTranslations($data['description'], $original?->description, true);
|
||||
} elseif (array_key_exists('description_translations', $data)) {
|
||||
$data['description'] = $this->normalizeTranslations(
|
||||
$data['description_translations'],
|
||||
$original?->description,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (array_key_exists('example_text', $data)) {
|
||||
$data['example_text'] = $this->normalizeTranslations($data['example_text'], $original?->example_text, true);
|
||||
} elseif (array_key_exists('example_text_translations', $data)) {
|
||||
$data['example_text'] = $this->normalizeTranslations(
|
||||
$data['example_text_translations'],
|
||||
$original?->example_text,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
unset(
|
||||
$data['title_translations'],
|
||||
$data['description_translations'],
|
||||
$data['example_text_translations']
|
||||
);
|
||||
|
||||
if (! array_key_exists('difficulty', $data) || $data['difficulty'] === null) {
|
||||
$data['difficulty'] = $original?->difficulty ?? 'easy';
|
||||
}
|
||||
|
||||
if (! array_key_exists('priority', $data) || $data['priority'] === null) {
|
||||
$data['priority'] = $original?->priority ?? 'medium';
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array<string, string>|null $fallback
|
||||
*
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
protected function normalizeTranslations(mixed $value, ?array $fallback = null, bool $allowNull = false): ?array
|
||||
{
|
||||
if ($allowNull && ($value === null || $value === '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$filtered = array_filter(
|
||||
$value,
|
||||
static fn ($text) => is_string($text) && $text !== ''
|
||||
);
|
||||
|
||||
if (! empty($filtered)) {
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
return $allowNull ? null : ($fallback ?? []);
|
||||
}
|
||||
|
||||
if (is_string($value) && $value !== '') {
|
||||
$locale = app()->getLocale() ?: 'de';
|
||||
|
||||
return [
|
||||
$locale => $value,
|
||||
];
|
||||
}
|
||||
|
||||
return $allowNull ? null : $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,13 @@ use App\Models\TenantPackage;
|
||||
use App\Models\PackagePurchase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\Table\TableExtension;
|
||||
use League\CommonMark\Extension\Autolink\AutolinkExtension;
|
||||
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
|
||||
use League\CommonMark\Extension\TaskList\TaskListExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
|
||||
class MarketingController extends Controller
|
||||
{
|
||||
@@ -429,7 +435,15 @@ class MarketingController extends Controller
|
||||
|
||||
// Transform to array with translated strings for the current locale
|
||||
$markdown = $postModel->getTranslation('content', $locale) ?? $postModel->getTranslation('content', 'de') ?? '';
|
||||
$converter = new CommonMarkConverter();
|
||||
|
||||
$environment = new Environment();
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
$environment->addExtension(new TableExtension());
|
||||
$environment->addExtension(new AutolinkExtension());
|
||||
$environment->addExtension(new StrikethroughExtension());
|
||||
$environment->addExtension(new TaskListExtension());
|
||||
|
||||
$converter = new MarkdownConverter($environment);
|
||||
$contentHtml = (string) $converter->convert($markdown);
|
||||
|
||||
// Debug log for content_html
|
||||
|
||||
27
app/Http/Requests/Tenant/EmotionStoreRequest.php
Normal file
27
app/Http/Requests/Tenant/EmotionStoreRequest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Tenant;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class EmotionStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'icon' => ['nullable', 'string', 'max:50'],
|
||||
'color' => ['nullable', 'string', 'regex:/^#?[0-9a-fA-F]{6}$/'],
|
||||
'sort_order' => ['nullable', 'integer'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
'event_type_ids' => ['nullable', 'array'],
|
||||
'event_type_ids.*' => ['integer', 'exists:event_types,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Http/Requests/Tenant/EmotionUpdateRequest.php
Normal file
27
app/Http/Requests/Tenant/EmotionUpdateRequest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Tenant;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class EmotionUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'description' => ['sometimes', 'nullable', 'string'],
|
||||
'icon' => ['sometimes', 'nullable', 'string', 'max:50'],
|
||||
'color' => ['sometimes', 'nullable', 'string', 'regex:/^#?[0-9a-fA-F]{6}$/'],
|
||||
'sort_order' => ['sometimes', 'nullable', 'integer'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
'event_type_ids' => ['sometimes', 'array'],
|
||||
'event_type_ids.*' => ['integer', 'exists:event_types,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,18 @@ class TaskStoreRequest extends FormRequest
|
||||
'description' => ['nullable', 'string'],
|
||||
'collection_id' => ['nullable', 'exists:task_collections,id', function ($attribute, $value, $fail) {
|
||||
$tenantId = request()->tenant?->id;
|
||||
if ($tenantId && !\App\Models\TaskCollection::where('id', $value)->where('tenant_id', $tenantId)->exists()) {
|
||||
|
||||
$accessible = \App\Models\TaskCollection::where('id', $value)
|
||||
->where(function ($query) use ($tenantId) {
|
||||
$query->whereNull('tenant_id');
|
||||
|
||||
if ($tenantId) {
|
||||
$query->orWhere('tenant_id', $tenantId);
|
||||
}
|
||||
})
|
||||
->exists();
|
||||
|
||||
if (! $accessible) {
|
||||
$fail('Die TaskCollection gehört nicht zu diesem Tenant.');
|
||||
}
|
||||
}],
|
||||
@@ -57,4 +68,4 @@ class TaskStoreRequest extends FormRequest
|
||||
'assigned_to.exists' => 'Der zugewiesene Benutzer existiert nicht.',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,18 @@ class TaskUpdateRequest extends FormRequest
|
||||
'description' => ['sometimes', 'nullable', 'string'],
|
||||
'collection_id' => ['sometimes', 'nullable', 'exists:task_collections,id', function ($attribute, $value, $fail) {
|
||||
$tenantId = request()->tenant?->id;
|
||||
if ($tenantId && !\App\Models\TaskCollection::where('id', $value)->where('tenant_id', $tenantId)->exists()) {
|
||||
|
||||
$accessible = \App\Models\TaskCollection::where('id', $value)
|
||||
->where(function ($query) use ($tenantId) {
|
||||
$query->whereNull('tenant_id');
|
||||
|
||||
if ($tenantId) {
|
||||
$query->orWhere('tenant_id', $tenantId);
|
||||
}
|
||||
})
|
||||
->exists();
|
||||
|
||||
if (! $accessible) {
|
||||
$fail('Die TaskCollection gehört nicht zu diesem Tenant.');
|
||||
}
|
||||
}],
|
||||
@@ -56,4 +67,4 @@ class TaskUpdateRequest extends FormRequest
|
||||
'assigned_to.exists' => 'Der zugewiesene Benutzer existiert nicht.',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Http/Resources/Tenant/EmotionResource.php
Normal file
64
app/Http/Resources/Tenant/EmotionResource.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Tenant;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class EmotionResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'tenant_id' => $this->tenant_id,
|
||||
'name' => $this->translatedText($this->name, 'Emotion'),
|
||||
'name_translations' => (array) $this->name,
|
||||
'description' => $this->description ? $this->translatedText($this->description, '') : null,
|
||||
'description_translations' => $this->description ? (array) $this->description : [],
|
||||
'icon' => $this->icon,
|
||||
'color' => $this->color,
|
||||
'sort_order' => $this->sort_order,
|
||||
'is_active' => (bool) $this->is_active,
|
||||
'is_global' => $this->tenant_id === null,
|
||||
'event_types' => $this->whenLoaded('eventTypes', function () {
|
||||
return $this->eventTypes->map(fn ($eventType) => [
|
||||
'id' => $eventType->id,
|
||||
'slug' => $eventType->slug,
|
||||
'name' => $this->translatedText($eventType->name, $eventType->slug ?? ''),
|
||||
'name_translations' => (array) $eventType->name,
|
||||
]);
|
||||
}),
|
||||
'created_at' => optional($this->created_at)->toISOString(),
|
||||
'updated_at' => optional($this->updated_at)->toISOString(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function translatedText(mixed $value, string $fallback): string
|
||||
{
|
||||
if (is_string($value) && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$locale = app()->getLocale();
|
||||
$locales = array_filter([
|
||||
$locale,
|
||||
$locale && str_contains($locale, '-') ? explode('-', $locale)[0] : null,
|
||||
'de',
|
||||
'en',
|
||||
]);
|
||||
|
||||
foreach ($locales as $code) {
|
||||
if ($code && isset($value[$code]) && $value[$code] !== '') {
|
||||
return $value[$code];
|
||||
}
|
||||
}
|
||||
|
||||
$first = reset($value);
|
||||
return $first !== false ? (string) $first : $fallback;
|
||||
}
|
||||
}
|
||||
42
app/Http/Resources/Tenant/TaskCollectionResource.php
Normal file
42
app/Http/Resources/Tenant/TaskCollectionResource.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Tenant;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class TaskCollectionResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'slug' => $this->slug,
|
||||
'name' => $this->name,
|
||||
'name_translations' => $this->name_translations,
|
||||
'description' => $this->description,
|
||||
'description_translations' => $this->description_translations,
|
||||
'tenant_id' => $this->tenant_id,
|
||||
'is_global' => $this->tenant_id === null,
|
||||
'event_type' => $this->whenLoaded('eventType', function () {
|
||||
return [
|
||||
'id' => $this->eventType->id,
|
||||
'slug' => $this->eventType->slug,
|
||||
'name' => $this->eventType->name,
|
||||
'icon' => $this->eventType->icon,
|
||||
];
|
||||
}),
|
||||
'tasks_count' => $this->whenCounted('tasks'),
|
||||
'is_default' => (bool) ($this->is_default ?? false),
|
||||
'position' => $this->position,
|
||||
'source_collection_id' => $this->source_collection_id,
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -18,15 +18,27 @@ class TaskResource extends JsonResource
|
||||
? $this->assignedEvents->count()
|
||||
: $this->assignedEvents()->count();
|
||||
|
||||
$titleTranslations = $this->normalizeTranslations($this->title);
|
||||
$descriptionTranslations = $this->normalizeTranslations($this->description, allowNull: true);
|
||||
$exampleTranslations = $this->normalizeTranslations($this->example_text, allowNull: true);
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'tenant_id' => $this->tenant_id,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'slug' => $this->slug,
|
||||
'title' => $this->translatedText($titleTranslations, 'Untitled task'),
|
||||
'title_translations' => $titleTranslations,
|
||||
'description' => $descriptionTranslations ? $this->translatedText($descriptionTranslations, '') : null,
|
||||
'description_translations' => $descriptionTranslations ?? [],
|
||||
'example_text' => $exampleTranslations ? $this->translatedText($exampleTranslations, '') : null,
|
||||
'example_text_translations' => $exampleTranslations ?? [],
|
||||
'priority' => $this->priority,
|
||||
'difficulty' => $this->difficulty,
|
||||
'due_date' => $this->due_date?->toISOString(),
|
||||
'is_completed' => (bool) $this->is_completed,
|
||||
'collection_id' => $this->collection_id,
|
||||
'source_task_id' => $this->source_task_id,
|
||||
'source_collection_id' => $this->source_collection_id,
|
||||
'assigned_events_count' => $assignedEventsCount,
|
||||
'assigned_events' => $this->whenLoaded(
|
||||
'assignedEvents',
|
||||
@@ -36,5 +48,60 @@ class TaskResource extends JsonResource
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
protected function normalizeTranslations(mixed $value, bool $allowNull = false): ?array
|
||||
{
|
||||
if ($allowNull && ($value === null || $value === '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$filtered = array_filter(
|
||||
$value,
|
||||
static fn ($text) => is_string($text) && $text !== ''
|
||||
);
|
||||
|
||||
return ! empty($filtered)
|
||||
? $filtered
|
||||
: ($allowNull ? null : []);
|
||||
}
|
||||
|
||||
if (is_string($value) && $value !== '') {
|
||||
$locale = app()->getLocale() ?: 'de';
|
||||
|
||||
return [
|
||||
$locale => $value,
|
||||
];
|
||||
}
|
||||
|
||||
return $allowNull ? null : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $translations
|
||||
*/
|
||||
protected function translatedText(array $translations, string $fallback): string
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
$locales = array_filter([
|
||||
$locale,
|
||||
$locale && str_contains($locale, '-') ? explode('-', $locale)[0] : null,
|
||||
'de',
|
||||
'en',
|
||||
]);
|
||||
|
||||
foreach ($locales as $code) {
|
||||
if ($code && isset($translations[$code]) && $translations[$code] !== '') {
|
||||
return $translations[$code];
|
||||
}
|
||||
}
|
||||
|
||||
$first = reset($translations);
|
||||
|
||||
return $first !== false ? $first : $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\Table\TableExtension;
|
||||
use League\CommonMark\Extension\Autolink\AutolinkExtension;
|
||||
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
|
||||
use League\CommonMark\Extension\TaskList\TaskListExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
|
||||
class BlogPost extends Model
|
||||
{
|
||||
@@ -54,7 +60,15 @@ class BlogPost extends Model
|
||||
{
|
||||
return Attribute::get(function () {
|
||||
$markdown = $this->getTranslation('content', app()->getLocale());
|
||||
$converter = new CommonMarkConverter();
|
||||
|
||||
$environment = new Environment();
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
$environment->addExtension(new TableExtension());
|
||||
$environment->addExtension(new AutolinkExtension());
|
||||
$environment->addExtension(new StrikethroughExtension());
|
||||
$environment->addExtension(new TaskListExtension());
|
||||
|
||||
$converter = new MarkdownConverter($environment);
|
||||
return $converter->convert($markdown);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
@@ -18,6 +19,11 @@ class Emotion extends Model
|
||||
'description' => 'array',
|
||||
];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function eventTypes(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(EventType::class, 'emotion_event_type', 'emotion_id', 'event_type_id');
|
||||
|
||||
@@ -6,10 +6,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Znck\Eloquent\Relations\BelongsToThrough as BelongsToThroughRelation;
|
||||
use Znck\Eloquent\Traits\BelongsToThrough;
|
||||
|
||||
class Photo extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use BelongsToThrough;
|
||||
|
||||
protected $table = 'photos';
|
||||
protected $guarded = [];
|
||||
@@ -47,5 +50,12 @@ class Photo extends Model
|
||||
{
|
||||
return $this->hasMany(PhotoLike::class);
|
||||
}
|
||||
}
|
||||
|
||||
public function tenant(): BelongsToThroughRelation
|
||||
{
|
||||
return $this->belongsToThrough(
|
||||
Tenant::class,
|
||||
Event::class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Task extends Model
|
||||
{
|
||||
@@ -38,6 +41,59 @@ class Task extends Model
|
||||
return $this->belongsTo(TaskCollection::class, 'collection_id');
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function sourceTask(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Task::class, 'source_task_id');
|
||||
}
|
||||
|
||||
public function derivedTasks(): HasMany
|
||||
{
|
||||
return $this->hasMany(Task::class, 'source_task_id');
|
||||
}
|
||||
|
||||
public function sourceCollection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(TaskCollection::class, 'source_collection_id');
|
||||
}
|
||||
|
||||
public function scopeForTenant(Builder $query, ?int $tenantId): Builder
|
||||
{
|
||||
return $query->where(function (Builder $innerQuery) use ($tenantId) {
|
||||
$innerQuery->whereNull('tenant_id');
|
||||
|
||||
if ($tenantId) {
|
||||
$innerQuery->orWhere('tenant_id', $tenantId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (Task $task) {
|
||||
if (! $task->slug) {
|
||||
$task->slug = static::generateSlug(
|
||||
$task->title['en'] ?? $task->title['de'] ?? 'task'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static function generateSlug(string $base): string
|
||||
{
|
||||
$slugBase = Str::slug($base) ?: 'task';
|
||||
|
||||
do {
|
||||
$slug = $slugBase . '-' . Str::random(6);
|
||||
} while (static::where('slug', $slug)->exists());
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
public function assignedEvents(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Event::class, 'event_task', 'task_id', 'event_id')
|
||||
|
||||
@@ -4,7 +4,10 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class TaskCollection extends Model
|
||||
{
|
||||
@@ -14,10 +17,40 @@ class TaskCollection extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'name',
|
||||
'description',
|
||||
'slug',
|
||||
'name_translations',
|
||||
'description_translations',
|
||||
'event_type_id',
|
||||
'source_collection_id',
|
||||
'is_default',
|
||||
'position',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'name_translations' => 'array',
|
||||
'description_translations' => 'array',
|
||||
];
|
||||
|
||||
public function eventType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EventType::class);
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function sourceCollection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(TaskCollection::class, 'source_collection_id');
|
||||
}
|
||||
|
||||
public function derivedCollections(): HasMany
|
||||
{
|
||||
return $this->hasMany(TaskCollection::class, 'source_collection_id');
|
||||
}
|
||||
|
||||
public function tasks(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
@@ -25,7 +58,7 @@ class TaskCollection extends Model
|
||||
'task_collection_task',
|
||||
'task_collection_id',
|
||||
'task_id'
|
||||
);
|
||||
)->withPivot(['sort_order']);
|
||||
}
|
||||
|
||||
public function events(): BelongsToMany
|
||||
@@ -35,7 +68,49 @@ class TaskCollection extends Model
|
||||
'event_task_collection',
|
||||
'task_collection_id',
|
||||
'event_id'
|
||||
);
|
||||
)->withPivot(['sort_order'])->withTimestamps();
|
||||
}
|
||||
|
||||
public function scopeGlobal($query)
|
||||
{
|
||||
return $query->whereNull('tenant_id');
|
||||
}
|
||||
|
||||
public function scopeForTenant($query, ?int $tenantId)
|
||||
{
|
||||
return $query->where(function ($inner) use ($tenantId) {
|
||||
$inner->whereNull('tenant_id');
|
||||
|
||||
if ($tenantId) {
|
||||
$inner->orWhere('tenant_id', $tenantId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function getNameAttribute(): string
|
||||
{
|
||||
return $this->resolveTranslation('name_translations');
|
||||
}
|
||||
|
||||
public function getDescriptionAttribute(): ?string
|
||||
{
|
||||
$value = $this->resolveTranslation('description_translations');
|
||||
|
||||
return $value ?: null;
|
||||
}
|
||||
|
||||
protected function resolveTranslation(string $attribute, ?string $locale = null): string
|
||||
{
|
||||
$translations = $this->{$attribute} ?? [];
|
||||
|
||||
if (is_string($translations)) {
|
||||
$translations = json_decode($translations, true) ?: [];
|
||||
}
|
||||
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
|
||||
return $translations[$locale]
|
||||
?? Arr::first($translations)
|
||||
?? '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,11 @@ class User extends Authenticatable implements MustVerifyEmail, HasName, Filament
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($this->role, ['tenant_admin', 'super_admin'], true);
|
||||
return match ($panel->getId()) {
|
||||
'superadmin' => $this->role === 'super_admin',
|
||||
'admin' => $this->role === 'tenant_admin',
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
public function canAccessTenant(Model $tenant): bool
|
||||
|
||||
@@ -17,34 +17,26 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use Stephenjude\FilamentBlog\Filament\Resources\CategoryResource;
|
||||
use Stephenjude\FilamentBlog\Filament\Resources\PostResource;
|
||||
use Stephenjude\FilamentBlog\Filament\Resources\TagResource;
|
||||
use App\Models\BlogCategory;
|
||||
use App\Models\BlogPost;
|
||||
use App\Models\BlogTag;
|
||||
|
||||
class AdminPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->default()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->brandName('Fotospiel Studio')
|
||||
->login(\App\Filament\Pages\Auth\Login::class)
|
||||
->colors([
|
||||
'primary' => Color::Pink,
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
|
||||
->pages([
|
||||
Pages\Dashboard::class,
|
||||
])
|
||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
||||
->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,
|
||||
Widgets\FilamentInfoWidget::class,
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
@@ -60,12 +52,8 @@ class AdminPanelProvider extends PanelProvider
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
])
|
||||
->resources([
|
||||
\App\Filament\Resources\UserResource::class,
|
||||
\App\Filament\Resources\TenantPackageResource::class,
|
||||
])
|
||||
->tenant(\App\Models\Tenant::class)
|
||||
// Remove blog models as they are global and handled in SuperAdmin
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Widgets;
|
||||
use App\Filament\Resources\LegalPageResource;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
@@ -17,21 +18,12 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use App\Filament\Resources\LegalPageResource;
|
||||
use App\Filament\Resources\TenantResource;
|
||||
use App\Models\User;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\BlogPost;
|
||||
use App\Models\BlogCategory;
|
||||
use App\Models\BlogTag;
|
||||
use App\Filament\Widgets\PlatformStatsWidget;
|
||||
use App\Filament\Widgets\TopTenantsByUploads;
|
||||
use App\Filament\Blog\Resources\PostResource;
|
||||
use App\Filament\Blog\Resources\CategoryResource;
|
||||
use App\Filament\Blog\Resources\AuthorResource;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SuperAdminPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
@@ -49,7 +41,7 @@ class SuperAdminPanelProvider extends PanelProvider
|
||||
->pages([
|
||||
Pages\Dashboard::class,
|
||||
])
|
||||
->login(\App\Filament\SuperAdmin\Pages\Auth\Login::class)
|
||||
->login(\App\Filament\Pages\Auth\Login::class)
|
||||
/*->plugin(
|
||||
BlogPlugin::make()
|
||||
)*/
|
||||
@@ -76,7 +68,11 @@ class SuperAdminPanelProvider extends PanelProvider
|
||||
Authenticate::class,
|
||||
])
|
||||
->resources([
|
||||
|
||||
\App\Filament\Resources\EventResource::class,
|
||||
\App\Filament\Resources\PhotoResource::class,
|
||||
\App\Filament\Resources\UserResource::class,
|
||||
\App\Filament\Resources\TenantPackageResource::class,
|
||||
\App\Filament\Resources\TaskResource::class,
|
||||
PostResource::class,
|
||||
CategoryResource::class,
|
||||
LegalPageResource::class,
|
||||
|
||||
161
app/Services/Tenant/TaskCollectionImportService.php
Normal file
161
app/Services/Tenant/TaskCollectionImportService.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Tenant;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskCollection;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
|
||||
class TaskCollectionImportService
|
||||
{
|
||||
public function __construct(private readonly DatabaseManager $db)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{collection: TaskCollection, created_task_ids: array<int>, attached_task_ids: array<int>}
|
||||
*/
|
||||
public function import(TaskCollection $collection, Event $event): array
|
||||
{
|
||||
if ($collection->tenant_id && $collection->tenant_id !== $event->tenant_id) {
|
||||
throw new RuntimeException('Task collection is not accessible for this tenant.');
|
||||
}
|
||||
|
||||
$collection->loadMissing('tasks');
|
||||
|
||||
return $this->db->transaction(function () use ($collection, $event) {
|
||||
$tenantId = $event->tenant_id;
|
||||
|
||||
$targetCollection = $this->resolveTenantCollection($collection, $tenantId);
|
||||
|
||||
$createdTaskIds = [];
|
||||
$attachedTaskIds = [];
|
||||
|
||||
foreach ($collection->tasks as $task) {
|
||||
$tenantTask = $this->resolveTenantTask($task, $targetCollection, $tenantId);
|
||||
|
||||
if ($tenantTask->wasRecentlyCreated) {
|
||||
$createdTaskIds[] = $tenantTask->id;
|
||||
}
|
||||
|
||||
if (! $tenantTask->assignedEvents()->where('event_id', $event->id)->exists()) {
|
||||
$tenantTask->assignedEvents()->attach($event->id);
|
||||
$attachedTaskIds[] = $tenantTask->id;
|
||||
}
|
||||
}
|
||||
|
||||
$event->taskCollections()->syncWithoutDetaching([
|
||||
$targetCollection->id => ['sort_order' => $targetCollection->position ?? 0],
|
||||
]);
|
||||
|
||||
return [
|
||||
'collection' => $targetCollection->fresh(),
|
||||
'created_task_ids' => $createdTaskIds,
|
||||
'attached_task_ids' => $attachedTaskIds,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
protected function resolveTenantCollection(TaskCollection $collection, int $tenantId): TaskCollection
|
||||
{
|
||||
if ($collection->tenant_id === $tenantId) {
|
||||
return $collection;
|
||||
}
|
||||
|
||||
$existing = TaskCollection::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('source_collection_id', $collection->id)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
return TaskCollection::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'source_collection_id' => $collection->id,
|
||||
'event_type_id' => $collection->event_type_id,
|
||||
'slug' => $this->buildCollectionSlug($collection->slug, $tenantId),
|
||||
'name_translations' => $collection->name_translations,
|
||||
'description_translations' => $collection->description_translations,
|
||||
'is_default' => false,
|
||||
'position' => $collection->position,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function resolveTenantTask(Task $templateTask, TaskCollection $targetCollection, int $tenantId): Task
|
||||
{
|
||||
if ($templateTask->tenant_id === $tenantId) {
|
||||
if ($templateTask->collection_id !== $targetCollection->id) {
|
||||
$templateTask->update(['collection_id' => $targetCollection->id]);
|
||||
}
|
||||
|
||||
return $templateTask;
|
||||
}
|
||||
|
||||
$sourceId = $templateTask->source_task_id ?: $templateTask->id;
|
||||
|
||||
$existing = Task::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('source_task_id', $sourceId)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->update([
|
||||
'collection_id' => $targetCollection->id,
|
||||
'source_collection_id' => $templateTask->source_collection_id ?: $targetCollection->source_collection_id ?: $targetCollection->id,
|
||||
]);
|
||||
|
||||
return tap($existing)->refresh();
|
||||
}
|
||||
|
||||
$slugBase = $templateTask->slug ?: ($templateTask->title['en'] ?? $templateTask->title['de'] ?? 'task');
|
||||
$slug = $this->buildTaskSlug($slugBase);
|
||||
|
||||
$cloned = Task::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'slug' => $slug,
|
||||
'emotion_id' => $templateTask->emotion_id,
|
||||
'event_type_id' => $templateTask->event_type_id,
|
||||
'title' => $templateTask->title,
|
||||
'description' => $templateTask->description,
|
||||
'example_text' => $templateTask->example_text,
|
||||
'due_date' => null,
|
||||
'is_completed' => false,
|
||||
'priority' => $templateTask->priority,
|
||||
'collection_id' => $targetCollection->id,
|
||||
'difficulty' => $templateTask->difficulty,
|
||||
'sort_order' => $templateTask->sort_order,
|
||||
'is_active' => true,
|
||||
'source_task_id' => $sourceId,
|
||||
'source_collection_id' => $templateTask->source_collection_id ?: $templateTask->collection_id,
|
||||
]);
|
||||
|
||||
return $cloned;
|
||||
}
|
||||
|
||||
protected function buildCollectionSlug(?string $slug, int $tenantId): string
|
||||
{
|
||||
$base = Str::slug(($slug ?: 'collection') . '-' . $tenantId);
|
||||
|
||||
do {
|
||||
$candidate = $base . '-' . Str::random(4);
|
||||
} while (TaskCollection::where('slug', $candidate)->exists());
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
protected function buildTaskSlug(string $base): string
|
||||
{
|
||||
$slugBase = Str::slug($base) ?: 'task';
|
||||
|
||||
do {
|
||||
$candidate = $slugBase . '-' . Str::random(6);
|
||||
} while (Task::where('slug', $candidate)->exists());
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
87
app/Support/TenantOnboardingState.php
Normal file
87
app/Support/TenantOnboardingState.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\EventJoinToken;
|
||||
use App\Models\TaskCollection;
|
||||
use App\Models\Tenant;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class TenantOnboardingState
|
||||
{
|
||||
public static function tenant(?Tenant $tenant = null): ?Tenant
|
||||
{
|
||||
if ($tenant) {
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
/** @var Tenant|null $tenant */
|
||||
$tenant = Filament::getTenant();
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
public static function status(?Tenant $tenant = null): array
|
||||
{
|
||||
$tenant = self::tenant($tenant);
|
||||
|
||||
if (! $tenant) {
|
||||
return [
|
||||
'packages' => false,
|
||||
'event' => false,
|
||||
'palette' => false,
|
||||
'invite' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$hasCustomCollections = TaskCollection::query()
|
||||
->where('tenant_id', $tenant->id)
|
||||
->exists();
|
||||
|
||||
$hasEvent = $tenant->events()->exists();
|
||||
|
||||
$palette = Arr::get($tenant->settings ?? [], 'branding.palette');
|
||||
|
||||
$hasInvite = EventJoinToken::query()
|
||||
->whereHas('event', fn ($query) => $query->where('tenant_id', $tenant->id))
|
||||
->exists();
|
||||
|
||||
return [
|
||||
'packages' => $hasCustomCollections,
|
||||
'event' => $hasEvent,
|
||||
'palette' => filled($palette),
|
||||
'invite' => $hasInvite,
|
||||
];
|
||||
}
|
||||
|
||||
public static function completed(?Tenant $tenant = null): bool
|
||||
{
|
||||
$status = self::status($tenant);
|
||||
|
||||
return collect($status)->every(fn ($done) => $done === true)
|
||||
|| Arr::has(self::tenant($tenant)?->settings ?? [], 'onboarding.completed_at');
|
||||
}
|
||||
|
||||
public static function markCompleted(Tenant $tenant, array $data = []): void
|
||||
{
|
||||
$settings = $tenant->settings ?? [];
|
||||
|
||||
Arr::set($settings, 'onboarding.completed_at', Carbon::now()->toIso8601String());
|
||||
|
||||
if (Arr::has($data, 'primary_event_id')) {
|
||||
Arr::set($settings, 'onboarding.primary_event_id', Arr::get($data, 'primary_event_id'));
|
||||
}
|
||||
|
||||
if (Arr::has($data, 'selected_packages')) {
|
||||
Arr::set($settings, 'onboarding.selected_packages', Arr::get($data, 'selected_packages'));
|
||||
}
|
||||
|
||||
if (Arr::has($data, 'qr_layout')) {
|
||||
Arr::set($settings, 'onboarding.qr_layout', Arr::get($data, 'qr_layout'));
|
||||
}
|
||||
|
||||
$tenant->forceFill(['settings' => $settings])->save();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user