diff --git a/app/Filament/Resources/EmotionResource.php b/app/Filament/Resources/EmotionResource.php index ad8309d..b580791 100644 --- a/app/Filament/Resources/EmotionResource.php +++ b/app/Filament/Resources/EmotionResource.php @@ -4,29 +4,36 @@ namespace App\Filament\Resources; use App\Filament\Resources\EmotionResource\Pages; use App\Models\Emotion; -use Filament\Schemas\Schema as Schema; -use Filament\Schemas\Components as SC; +use Filament\Actions; +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\Schemas\Schema; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; +use UnitEnum; +use BackedEnum; class EmotionResource extends Resource { protected static ?string $model = Emotion::class; - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-face-smile'; - protected static string|\UnitEnum|null $navigationGroup = 'Library'; + protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-face-smile'; + protected static UnitEnum|string|null $navigationGroup = 'Library'; protected static ?int $navigationSort = 10; - public static function form(Schema $schema): Schema + public static function form(Schema $form): Schema { - return $schema->components([ - SC\KeyValue::make('name')->label('Name (de/en)')->keyLabel('locale')->valueLabel('value')->default(['de' => '', 'en' => ''])->required(), - SC\TextInput::make('icon')->label('Icon/Emoji')->maxLength(50), - SC\TextInput::make('color')->maxLength(7)->helperText('#RRGGBB'), - SC\KeyValue::make('description')->label('Description (de/en)')->keyLabel('locale')->valueLabel('value'), - SC\TextInput::make('sort_order')->numeric()->default(0), - SC\Toggle::make('is_active')->default(true), - SC\Select::make('eventTypes') + return $form->schema([ + KeyValue::make('name')->label('Name (de/en)')->keyLabel('locale')->valueLabel('value')->default(['de' => '', 'en' => ''])->required(), + TextInput::make('icon')->label('Icon/Emoji')->maxLength(50), + TextInput::make('color')->maxLength(7)->helperText('#RRGGBB'), + KeyValue::make('description')->label('Description (de/en)')->keyLabel('locale')->valueLabel('value'), + TextInput::make('sort_order')->numeric()->default(0), + Toggle::make('is_active')->default(true), + Select::make('eventTypes') ->label('Event Types') ->multiple() ->searchable() @@ -48,12 +55,10 @@ class EmotionResource extends Resource ]) ->filters([]) ->actions([ - Tables\Actions\EditAction::make(), + Actions\EditAction::make(), ]) ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), + Actions\DeleteBulkAction::make(), ]); } @@ -65,128 +70,3 @@ class EmotionResource extends Resource ]; } } - -namespace App\Filament\Resources\EmotionResource\Pages; - -use App\Filament\Resources\EmotionResource; -use Filament\Resources\Pages\ManageRecords; -use Filament\Actions; -use Filament\Resources\Pages\Page; -use Filament\Forms; -use Filament\Notifications\Notification; -use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Facades\DB; - -class ManageEmotions extends ManageRecords -{ - protected static string $resource = EmotionResource::class; - - protected function getHeaderActions(): array - { - return [ - Actions\Action::make('import') - ->label('Import CSV') - ->icon('heroicon-o-arrow-up-tray') - ->url(EmotionResource::getUrl('import')), - Actions\Action::make('template') - ->label('Download CSV Template') - ->icon('heroicon-o-document-arrow-down') - ->url(url('/super-admin/templates/emotions.csv')), - ]; - } -} - -class ImportEmotions extends Page -{ - protected static string $resource = EmotionResource::class; - protected string $view = 'filament.pages.blank'; - protected ?string $heading = 'Import Emotions (CSV)'; - - public ?string $file = null; - - protected function getFormSchema(): array - { - return [ - Forms\Components\FileUpload::make('file') - ->label('CSV file') - ->acceptedFileTypes(['text/csv', 'text/plain']) - ->directory('imports') - ->required(), - ]; - } - - protected function getFormActions(): array - { - return [ - Forms\Components\Actions\Action::make('import') - ->label('Import') - ->action('doImport') - ->color('primary') - ]; - } - - public function doImport(): void - { - $state = $this->form->getState(); - $path = $state['file'] ?? null; - if (! $path || ! Storage::disk('public')->exists($path)) { - Notification::make()->danger()->title('File not found')->send(); - return; - } - $full = Storage::disk('public')->path($path); - [$ok, $fail] = $this->importEmotionsCsv($full); - Notification::make()->success()->title("Imported {$ok} rows")->body($fail ? "{$fail} failed" : null)->send(); - } - - private function importEmotionsCsv(string $file): array - { - $h = fopen($file, 'r'); - if (! $h) return [0,0]; - $ok = 0; $fail = 0; - // Expected headers: name_de,name_en,icon,color,description_de,description_en,sort_order,is_active,event_types - $headers = fgetcsv($h, 0, ','); - if (! $headers) return [0,0]; - $map = array_flip($headers); - while (($row = fgetcsv($h, 0, ',')) !== false) { - try { - $nameDe = trim($row[$map['name_de']] ?? ''); - $nameEn = trim($row[$map['name_en']] ?? ''); - $name = $nameDe ?: $nameEn; - if ($name === '') { $fail++; continue; } - $data = [ - 'name' => ['de' => $nameDe, 'en' => $nameEn], - 'icon' => $row[$map['icon']] ?? null, - 'color' => $row[$map['color']] ?? null, - 'description' => [ - 'de' => $row[$map['description_de']] ?? null, - 'en' => $row[$map['description_en']] ?? null, - ], - 'sort_order' => (int)($row[$map['sort_order']] ?? 0), - 'is_active' => (int)($row[$map['is_active']] ?? 1) ? 1 : 0, - ]; - $id = DB::table('emotions')->insertGetId(array_merge($data, [ - 'created_at' => now(), 'updated_at' => now(), - ])); - // Attach event types if provided (by slug list separated by '|') - $et = $row[$map['event_types']] ?? ''; - if ($et) { - $slugs = array_filter(array_map('trim', explode('|', $et))); - if ($slugs) { - $ids = DB::table('event_types')->whereIn('slug', $slugs)->pluck('id')->all(); - foreach ($ids as $eid) { - DB::table('emotion_event_type')->insertOrIgnore([ - 'emotion_id' => $id, - 'event_type_id' => $eid, - ]); - } - } - } - $ok++; - } catch (\Throwable $e) { - $fail++; - } - } - fclose($h); - return [$ok, $fail]; - } -} diff --git a/app/Filament/Resources/EmotionResource/Pages/ImportEmotions.php b/app/Filament/Resources/EmotionResource/Pages/ImportEmotions.php new file mode 100644 index 0000000..fa937ed --- /dev/null +++ b/app/Filament/Resources/EmotionResource/Pages/ImportEmotions.php @@ -0,0 +1,109 @@ +schema([ + FileUpload::make('file') + ->label('CSV file') + ->acceptedFileTypes(['text/csv', 'text/plain']) + ->directory('imports') + ->required(), + ]); + } + + public function doImport(): void + { + $this->validate(); + + $path = $this->form->getState()['file'] ?? null; + if (!$path || !Storage::disk('public')->exists($path)) { + Notification::make()->danger()->title('File not found')->send(); + return; + } + + $fullPath = Storage::disk('public')->path($path); + [$ok, $fail] = $this->importEmotionsCsv($fullPath); + + Notification::make() + ->success() + ->title("Imported {$ok} rows") + ->body($fail ? "{$fail} failed" : null) + ->send(); + } + + private function importEmotionsCsv(string $file): array + { + $handle = fopen($file, 'r'); + if (!$handle) { + return [0, 0]; + } + + $ok = 0; + $fail = 0; + $headers = fgetcsv($handle, 0, ','); + if (!$headers) { + return [0, 0]; + } + + $map = array_flip($headers); + + while (($row = fgetcsv($handle, 0, ',')) !== false) { + try { + DB::transaction(function () use ($row, $map, &$ok) { + $nameDe = trim($row[$map['name_de']] ?? ''); + $nameEn = trim($row[$map['name_en']] ?? ''); + + if (empty($nameDe) && empty($nameEn)) { + throw new \Exception('Name is required.'); + } + + $emotion = Emotion::create([ + 'name' => ['de' => $nameDe, 'en' => $nameEn], + 'icon' => $row[$map['icon']] ?? null, + 'color' => $row[$map['color']] ?? null, + 'description' => [ + 'de' => $row[$map['description_de']] ?? null, + 'en' => $row[$map['description_en']] ?? null, + ], + 'sort_order' => (int)($row[$map['sort_order']] ?? 0), + 'is_active' => (int)($row[$map['is_active']] ?? 1) ? 1 : 0, + ]); + + $eventTypes = $row[$map['event_types']] ?? ''; + if ($eventTypes) { + $slugs = array_filter(array_map('trim', explode('|', $eventTypes))); + if ($slugs) { + $eventTypeIds = DB::table('event_types')->whereIn('slug', $slugs)->pluck('id')->all(); + $emotion->eventTypes()->attach($eventTypeIds); + } + } + $ok++; + }); + } catch (\Throwable $e) { + $fail++; + } + } + + fclose($handle); + return [$ok, $fail]; + } +} diff --git a/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php b/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php new file mode 100644 index 0000000..944adb4 --- /dev/null +++ b/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php @@ -0,0 +1,27 @@ +label('Import CSV') + ->icon('heroicon-o-arrow-up-tray') + ->url(EmotionResource::getUrl('import')), + Actions\Action::make('template') + ->label('Download CSV Template') + ->icon('heroicon-o-document-arrow-down') + ->url(url('/super-admin/templates/emotions.csv')), + ]; + } +} diff --git a/app/Filament/Resources/EventResource.php b/app/Filament/Resources/EventResource.php index dc84b03..ce8e4e2 100644 --- a/app/Filament/Resources/EventResource.php +++ b/app/Filament/Resources/EventResource.php @@ -7,12 +7,15 @@ use App\Models\Event; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; +use Filament\Actions; +use UnitEnum; +use BackedEnum; class EventResource extends Resource { protected static ?string $model = Event::class; - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-calendar'; - protected static string|\UnitEnum|null $navigationGroup = 'Platform'; + protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-calendar'; + protected static UnitEnum|string|null $navigationGroup = 'Platform'; protected static ?int $navigationSort = 20; public static function table(Table $table): Table @@ -34,13 +37,12 @@ class EventResource extends Resource ]) ->filters([]) ->actions([ - Tables\Actions\ViewAction::make(), - Tables\Actions\EditAction::make(), - Tables\Actions\Action::make('toggle') + Actions\EditAction::make(), + Actions\Action::make('toggle') ->label('Toggle Active') ->icon('heroicon-o-power') - ->action(fn($record) => $record->update(['is_active' => ! (bool)$record->is_active])), - Tables\Actions\Action::make('join_link') + ->action(fn($record) => $record->update(['is_active' => !$record->is_active])), + Actions\Action::make('join_link') ->label('Join Link / QR') ->icon('heroicon-o-qr-code') ->modalHeading('Event Join Link') @@ -50,9 +52,7 @@ class EventResource extends Resource ])), ]) ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), + Actions\DeleteBulkAction::make(), ]); } @@ -64,26 +64,4 @@ class EventResource extends Resource 'edit' => Pages\EditEvent::route('/{record}/edit'), ]; } -} - -namespace App\Filament\Resources\EventResource\Pages; - -use App\Filament\Resources\EventResource; -use Filament\Resources\Pages\ListRecords; -use Filament\Resources\Pages\ViewRecord; -use Filament\Resources\Pages\EditRecord; - -class ListEvents extends ListRecords -{ - protected static string $resource = EventResource::class; -} - -class ViewEvent extends ViewRecord -{ - protected static string $resource = EventResource::class; -} - -class EditEvent extends EditRecord -{ - protected static string $resource = EventResource::class; -} +} \ No newline at end of file diff --git a/app/Filament/Resources/EventResource/Pages/EditEvent.php b/app/Filament/Resources/EventResource/Pages/EditEvent.php new file mode 100644 index 0000000..a1ef6d8 --- /dev/null +++ b/app/Filament/Resources/EventResource/Pages/EditEvent.php @@ -0,0 +1,11 @@ +components([ - SC\KeyValue::make('name')->label('Name (de/en)')->default(['de' => '', 'en' => ''])->required(), - SC\TextInput::make('slug')->required()->unique(ignoreRecord: true), - SC\TextInput::make('icon')->maxLength(64), - SC\KeyValue::make('settings')->label('Settings')->keyLabel('key')->valueLabel('value'), - SC\Select::make('emotions') + return $form->schema([ + KeyValue::make('name')->label('Name (de/en)')->default(['de' => '', 'en' => ''])->required(), + TextInput::make('slug')->required()->unique(ignoreRecord: true), + TextInput::make('icon')->maxLength(64), + KeyValue::make('settings')->label('Settings')->keyLabel('key')->valueLabel('value'), + Select::make('emotions') ->label('Emotions') ->multiple() ->searchable() @@ -45,12 +50,10 @@ class EventTypeResource extends Resource ]) ->filters([]) ->actions([ - Tables\Actions\EditAction::make(), + Actions\EditAction::make(), ]) ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), + Actions\DeleteBulkAction::make(), ]); } @@ -60,14 +63,4 @@ class EventTypeResource extends Resource 'index' => Pages\ManageEventTypes::route('/'), ]; } -} - -namespace App\Filament\Resources\EventTypeResource\Pages; - -use App\Filament\Resources\EventTypeResource; -use Filament\Resources\Pages\ManageRecords; - -class ManageEventTypes extends ManageRecords -{ - protected static string $resource = EventTypeResource::class; -} +} \ No newline at end of file diff --git a/app/Filament/Resources/EventTypeResource/Pages/ManageEventTypes.php b/app/Filament/Resources/EventTypeResource/Pages/ManageEventTypes.php new file mode 100644 index 0000000..571b378 --- /dev/null +++ b/app/Filament/Resources/EventTypeResource/Pages/ManageEventTypes.php @@ -0,0 +1,19 @@ +filters([]) ->actions([ - Tables\Actions\ViewAction::make(), - Tables\Actions\EditAction::make(), + Actions\EditAction::make(), ]) ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), + Actions\DeleteBulkAction::make(), ]); } @@ -47,25 +47,3 @@ class LegalPageResource extends Resource ]; } } - -namespace App\Filament\Resources\LegalPageResource\Pages; - -use App\Filament\Resources\LegalPageResource; -use Filament\Resources\Pages\ListRecords; -use Filament\Resources\Pages\ViewRecord; -use Filament\Resources\Pages\EditRecord; - -class ListLegalPages extends ListRecords -{ - protected static string $resource = LegalPageResource::class; -} - -class ViewLegalPage extends ViewRecord -{ - protected static string $resource = LegalPageResource::class; -} - -class EditLegalPage extends EditRecord -{ - protected static string $resource = LegalPageResource::class; -} diff --git a/app/Filament/Resources/LegalPageResource/Pages/EditLegalPage.php b/app/Filament/Resources/LegalPageResource/Pages/EditLegalPage.php new file mode 100644 index 0000000..08e716c --- /dev/null +++ b/app/Filament/Resources/LegalPageResource/Pages/EditLegalPage.php @@ -0,0 +1,11 @@ +filters([]) ->actions([ - Tables\Actions\ViewAction::make(), - Tables\Actions\EditAction::make(), - Tables\Actions\Action::make('feature') + Actions\EditAction::make(), + Actions\Action::make('feature') ->label('Feature') - ->visible(fn($record) => ! (bool)$record->is_featured) - ->action(fn($record) => $record->update(['is_featured' => 1])) + ->visible(fn($record) => !$record->is_featured) + ->action(fn($record) => $record->update(['is_featured' => true])) ->icon('heroicon-o-star'), - Tables\Actions\Action::make('unfeature') + Actions\Action::make('unfeature') ->label('Unfeature') - ->visible(fn($record) => (bool)$record->is_featured) - ->action(fn($record) => $record->update(['is_featured' => 0])) + ->visible(fn($record) => $record->is_featured) + ->action(fn($record) => $record->update(['is_featured' => false])) ->icon('heroicon-o-star'), - Tables\Actions\DeleteAction::make(), + Actions\DeleteAction::make(), ]) ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\BulkAction::make('feature') - ->label('Feature selected') - ->icon('heroicon-o-star') - ->action(fn($records) => $records->each->update(['is_featured' => 1])), - Tables\Actions\BulkAction::make('unfeature') - ->label('Unfeature selected') - ->icon('heroicon-o-star') - ->action(fn($records) => $records->each->update(['is_featured' => 0])), - Tables\Actions\DeleteBulkAction::make(), - ]), + Actions\BulkAction::make('feature') + ->label('Feature selected') + ->icon('heroicon-o-star') + ->action(fn($records) => $records->each->update(['is_featured' => true])), + Actions\BulkAction::make('unfeature') + ->label('Unfeature selected') + ->icon('heroicon-o-star') + ->action(fn($records) => $records->each->update(['is_featured' => false])), + Actions\DeleteBulkAction::make(), ]); } @@ -65,26 +65,4 @@ class PhotoResource extends Resource 'edit' => Pages\EditPhoto::route('/{record}/edit'), ]; } -} - -namespace App\Filament\Resources\PhotoResource\Pages; - -use App\Filament\Resources\PhotoResource; -use Filament\Resources\Pages\ListRecords; -use Filament\Resources\Pages\ViewRecord; -use Filament\Resources\Pages\EditRecord; - -class ListPhotos extends ListRecords -{ - protected static string $resource = PhotoResource::class; -} - -class ViewPhoto extends ViewRecord -{ - protected static string $resource = PhotoResource::class; -} - -class EditPhoto extends EditRecord -{ - protected static string $resource = PhotoResource::class; -} +} \ No newline at end of file diff --git a/app/Filament/Resources/PhotoResource/Pages/EditPhoto.php b/app/Filament/Resources/PhotoResource/Pages/EditPhoto.php new file mode 100644 index 0000000..878082c --- /dev/null +++ b/app/Filament/Resources/PhotoResource/Pages/EditPhoto.php @@ -0,0 +1,11 @@ +components([ - SC\Select::make('emotion_id')->relationship('emotion', 'name')->required()->searchable()->preload(), - SC\Select::make('event_type_id')->relationship('eventType', 'name')->searchable()->preload()->label('Event Type (optional)'), - SC\KeyValue::make('title')->label('Title (de/en)')->default(['de' => '', 'en' => ''])->required(), - SC\KeyValue::make('description')->label('Description (de/en)'), - SC\Select::make('difficulty')->options([ + return $form->schema([ + Select::make('emotion_id')->relationship('emotion', 'name')->required()->searchable()->preload(), + Select::make('event_type_id')->relationship('eventType', 'name')->searchable()->preload()->label('Event Type (optional)'), + KeyValue::make('title')->label('Title (de/en)')->default(['de' => '', 'en' => ''])->required(), + KeyValue::make('description')->label('Description (de/en)'), + Select::make('difficulty')->options([ 'easy' => 'Easy', 'medium' => 'Medium', 'hard' => 'Hard', ])->default('easy'), - SC\KeyValue::make('example_text')->label('Example (de/en)'), - SC\TextInput::make('sort_order')->numeric()->default(0), - SC\Toggle::make('is_active')->default(true), + KeyValue::make('example_text')->label('Example (de/en)'), + TextInput::make('sort_order')->numeric()->default(0), + Toggle::make('is_active')->default(true), ])->columns(2); } @@ -49,13 +55,11 @@ class TaskResource extends Resource ]) ->filters([]) ->actions([ - Tables\Actions\EditAction::make(), - Tables\Actions\DeleteAction::make(), + Actions\EditAction::make(), + Actions\DeleteAction::make(), ]) ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), + Actions\DeleteBulkAction::make(), ]); } @@ -66,138 +70,4 @@ class TaskResource extends Resource 'import' => Pages\ImportTasks::route('/import'), ]; } -} - -namespace App\Filament\Resources\TaskResource\Pages; - -use App\Filament\Resources\TaskResource; -use Filament\Resources\Pages\ManageRecords; -use Filament\Actions; -use Filament\Resources\Pages\Page; -use Filament\Forms; -use Filament\Notifications\Notification; -use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Facades\DB; - -class ManageTasks extends ManageRecords -{ - protected static string $resource = TaskResource::class; - - protected function getHeaderActions(): array - { - return [ - Actions\Action::make('import') - ->label('Import CSV') - ->icon('heroicon-o-arrow-up-tray') - ->url(TaskResource::getUrl('import')), - Actions\Action::make('template') - ->label('Download CSV Template') - ->icon('heroicon-o-document-arrow-down') - ->url(url('/super-admin/templates/tasks.csv')), - ]; - } -} - -class ImportTasks extends Page -{ - protected static string $resource = TaskResource::class; - protected string $view = 'filament.pages.blank'; - protected ?string $heading = 'Import Tasks (CSV)'; - - public ?string $file = null; - - protected function getFormSchema(): array - { - return [ - Forms\Components\FileUpload::make('file') - ->label('CSV file') - ->acceptedFileTypes(['text/csv', 'text/plain']) - ->directory('imports') - ->required(), - ]; - } - - protected function getFormActions(): array - { - return [ - Forms\Components\Actions\Action::make('import') - ->label('Import') - ->action('doImport') - ->color('primary') - ]; - } - - public function doImport(): void - { - $state = $this->form->getState(); - $path = $state['file'] ?? null; - if (! $path || ! Storage::disk('public')->exists($path)) { - Notification::make()->danger()->title('File not found')->send(); - return; - } - $full = Storage::disk('public')->path($path); - [$ok, $fail] = $this->importTasksCsv($full); - Notification::make()->success()->title("Imported {$ok} rows")->body($fail ? "${fail} failed" : null)->send(); - } - - private function importTasksCsv(string $file): array - { - $h = fopen($file, 'r'); - if (! $h) return [0,0]; - $ok = 0; $fail = 0; - // Expected headers: emotion_name,emotion_name_de,emotion_name_en,event_type_slug,title_de,title_en,description_de,description_en,difficulty,example_text_de,example_text_en,sort_order,is_active - $headers = fgetcsv($h, 0, ','); - if (! $headers) return [0,0]; - $map = array_flip($headers); - while (($row = fgetcsv($h, 0, ',')) !== false) { - try { - $emotionName = trim($row[$map['emotion_name']] ?? ''); - $emotionNameDe = trim($row[$map['emotion_name_de']] ?? ''); - $emotionNameEn = trim($row[$map['emotion_name_en']] ?? ''); - $emotionId = null; - if ($emotionName !== '') { - $emotionId = DB::table('emotions')->where('name', $emotionName)->value('id'); - } - if (! $emotionId && $emotionNameDe !== '') { - $emotionId = DB::table('emotions')->where('name', 'like', '%"de":"'.str_replace('"','""',$emotionNameDe).'"%')->value('id'); - } - if (! $emotionId && $emotionNameEn !== '') { - $emotionId = DB::table('emotions')->where('name', 'like', '%"en":"'.str_replace('"','""',$emotionNameEn).'"%')->value('id'); - } - if (! $emotionId) { $fail++; continue; } - $eventTypeSlug = trim($row[$map['event_type_slug']] ?? ''); - $eventTypeId = null; - if ($eventTypeSlug !== '') { - $eventTypeId = DB::table('event_types')->where('slug', $eventTypeSlug)->value('id'); - } - $data = [ - 'emotion_id' => $emotionId, - 'event_type_id' => $eventTypeId, - 'title' => [ - 'de' => $row[$map['title_de']] ?? null, - 'en' => $row[$map['title_en']] ?? null, - ], - 'description' => [ - 'de' => $row[$map['description_de']] ?? null, - 'en' => $row[$map['description_en']] ?? null, - ], - 'difficulty' => $row[$map['difficulty']] ?? 'easy', - 'example_text' => [ - 'de' => $row[$map['example_text_de']] ?? null, - 'en' => $row[$map['example_text_en']] ?? null, - ], - 'sort_order' => (int)($row[$map['sort_order']] ?? 0), - 'is_active' => (int)($row[$map['is_active']] ?? 1) ? 1 : 0, - 'created_at' => now(), - 'updated_at' => now(), - ]; - DB::table('tasks')->insert($data); - $ok++; - } catch (\Throwable $e) { - $fail++; - } - } - fclose($h); - return [$ok, $fail]; - } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/TaskResource/Pages/ImportTasks.php b/app/Filament/Resources/TaskResource/Pages/ImportTasks.php new file mode 100644 index 0000000..3675457 --- /dev/null +++ b/app/Filament/Resources/TaskResource/Pages/ImportTasks.php @@ -0,0 +1,125 @@ +schema([ + FileUpload::make('file') + ->label('CSV file') + ->acceptedFileTypes(['text/csv', 'text/plain']) + ->directory('imports') + ->required(), + ]); + } + + public function doImport(): void + { + $this->validate(); + + $path = $this->form->getState()['file'] ?? null; + if (!$path || !Storage::disk('public')->exists($path)) { + Notification::make()->danger()->title('File not found')->send(); + return; + } + + $fullPath = Storage::disk('public')->path($path); + [$ok, $fail] = $this->importTasksCsv($fullPath); + + Notification::make() + ->success() + ->title("Imported {$ok} rows") + ->body($fail ? "{$fail} failed" : null) + ->send(); + } + + private function importTasksCsv(string $file): array + { + $handle = fopen($file, 'r'); + if (!$handle) { + return [0, 0]; + } + + $ok = 0; + $fail = 0; + $headers = fgetcsv($handle, 0, ','); + if (!$headers) { + return [0, 0]; + } + + $map = array_flip($headers); + + while (($row = fgetcsv($handle, 0, ',')) !== false) { + try { + DB::transaction(function () use ($row, $map, &$ok) { + $emotionName = trim($row[$map['emotion_name']] ?? ''); + $emotionNameDe = trim($row[$map['emotion_name_de']] ?? ''); + $emotionNameEn = trim($row[$map['emotion_name_en']] ?? ''); + $emotionId = null; + + if ($emotionName) { + $emotionId = DB::table('emotions')->where('name', $emotionName)->value('id'); + } elseif ($emotionNameDe) { + $emotionId = DB::table('emotions')->where('name->de', $emotionNameDe)->value('id'); + } elseif ($emotionNameEn) { + $emotionId = DB::table('emotions')->where('name->en', $emotionNameEn)->value('id'); + } + + if (!$emotionId) { + throw new \Exception('Emotion not found.'); + } + + $eventTypeId = null; + $eventTypeSlug = trim($row[$map['event_type_slug']] ?? ''); + if ($eventTypeSlug) { + $eventTypeId = DB::table('event_types')->where('slug', $eventTypeSlug)->value('id'); + } + + Task::create([ + 'emotion_id' => $emotionId, + 'event_type_id' => $eventTypeId, + 'title' => [ + 'de' => $row[$map['title_de']] ?? null, + 'en' => $row[$map['title_en']] ?? null, + ], + 'description' => [ + 'de' => $row[$map['description_de']] ?? null, + 'en' => $row[$map['description_en']] ?? null, + ], + 'difficulty' => $row[$map['difficulty']] ?? 'easy', + 'example_text' => [ + 'de' => $row[$map['example_text_de']] ?? null, + 'en' => $row[$map['example_text_en']] ?? null, + ], + 'sort_order' => (int)($row[$map['sort_order']] ?? 0), + 'is_active' => (int)($row[$map['is_active']] ?? 1) ? 1 : 0, + ]); + + $ok++; + }); + } catch (\Throwable $e) { + $fail++; + } + } + + fclose($handle); + return [$ok, $fail]; + } +} diff --git a/app/Filament/Resources/TaskResource/Pages/ManageTasks.php b/app/Filament/Resources/TaskResource/Pages/ManageTasks.php new file mode 100644 index 0000000..9d96669 --- /dev/null +++ b/app/Filament/Resources/TaskResource/Pages/ManageTasks.php @@ -0,0 +1,27 @@ +label('Import CSV') + ->icon('heroicon-o-arrow-up-tray') + ->url(TaskResource::getUrl('import')), + Actions\Action::make('template') + ->label('Download CSV Template') + ->icon('heroicon-o-document-arrow-down') + ->url(url('/super-admin/templates/tasks.csv')), + ]; + } +} diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 6b23555..3b052b2 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -7,12 +7,15 @@ use App\Models\Tenant; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; +use UnitEnum; +use BackedEnum; +use Filament\Actions; class TenantResource extends Resource { protected static ?string $model = Tenant::class; - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-building-office'; - protected static string|\UnitEnum|null $navigationGroup = 'Platform'; + protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-building-office'; + protected static UnitEnum|string|null $navigationGroup = 'Platform'; protected static ?int $navigationSort = 10; public static function table(Table $table): Table @@ -29,13 +32,10 @@ class TenantResource extends Resource ]) ->filters([]) ->actions([ - Tables\Actions\ViewAction::make(), - Tables\Actions\EditAction::make(), + Actions\EditAction::make(), ]) ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), + Actions\DeleteBulkAction::make(), ]); } @@ -48,25 +48,3 @@ class TenantResource extends Resource ]; } } - -namespace App\Filament\Resources\TenantResource\Pages; - -use App\Filament\Resources\TenantResource; -use Filament\Resources\Pages\ListRecords; -use Filament\Resources\Pages\ViewRecord; -use Filament\Resources\Pages\EditRecord; - -class ListTenants extends ListRecords -{ - protected static string $resource = TenantResource::class; -} - -class ViewTenant extends ViewRecord -{ - protected static string $resource = TenantResource::class; -} - -class EditTenant extends EditRecord -{ - protected static string $resource = TenantResource::class; -} diff --git a/app/Filament/Resources/TenantResource/Pages/EditTenant.php b/app/Filament/Resources/TenantResource/Pages/EditTenant.php new file mode 100644 index 0000000..605a710 --- /dev/null +++ b/app/Filament/Resources/TenantResource/Pages/EditTenant.php @@ -0,0 +1,11 @@ + { + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); const res = await fetch('/api/v1/tenant/login', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken || '', + }, body: JSON.stringify({ email, password }), }); if (!res.ok) throw new Error('Login failed'); diff --git a/resources/views/admin.blade.php b/resources/views/admin.blade.php index 60905dc..cabb7e2 100644 --- a/resources/views/admin.blade.php +++ b/resources/views/admin.blade.php @@ -3,6 +3,7 @@ + Tenant Admin @vite('resources/js/admin/main.tsx') diff --git a/resources/views/filament/resources/emotion-resource/pages/import-emotions.blade.php b/resources/views/filament/resources/emotion-resource/pages/import-emotions.blade.php new file mode 100644 index 0000000..67dd9bd --- /dev/null +++ b/resources/views/filament/resources/emotion-resource/pages/import-emotions.blade.php @@ -0,0 +1,14 @@ + + + {{ $this->form }} + + + + Import + + + diff --git a/resources/views/filament/resources/task-resource/pages/import-tasks.blade.php b/resources/views/filament/resources/task-resource/pages/import-tasks.blade.php new file mode 100644 index 0000000..67dd9bd --- /dev/null +++ b/resources/views/filament/resources/task-resource/pages/import-tasks.blade.php @@ -0,0 +1,14 @@ + + + {{ $this->form }} + + + + Import + + + diff --git a/tests/Feature/EmotionResourceTest.php b/tests/Feature/EmotionResourceTest.php new file mode 100644 index 0000000..2d06d5d --- /dev/null +++ b/tests/Feature/EmotionResourceTest.php @@ -0,0 +1,41 @@ +create(); + $this->actingAs($user); + + $csvData = "name_de,name_en,icon,color,description_de,description_en,sort_order,is_active,event_types\nGlück,Joy,😊,#FFD700,Gefühl des Glücks,Feeling of joy,1,1,wedding|birthday"; + $csvFile = UploadedFile::fake()->createWithContent('emotions.csv', $csvData); + + Livewire::test(ImportEmotions::class) + ->set('file', $csvFile->getRealPath()) + ->call('doImport'); + + $this->assertDatabaseHas('emotions', [ + 'name' => json_encode(['de' => 'Glück', 'en' => 'Joy']), + 'icon' => '😊', + 'color' => '#FFD700', + 'description' => json_encode(['de' => 'Gefühl des Glücks', 'en' => 'Feeling of joy']), + 'sort_order' => 1, + 'is_active' => 1, + ]); + + $emotion = Emotion::where('name->de', 'Glück')->first(); + $this->assertNotNull($emotion); + } +}