From 21c9391e2cf3dfcb1bb36b04196e185e3c79c7c7 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 29 Sep 2025 22:16:12 +0200 Subject: [PATCH] webseite funktioniert, pay sdk, blog backend funktioniert --- .../Blog/Resources/CategoryResource.php | 217 +++++ .../CategoryResource/Pages/CreateCategory.php | 33 + .../CategoryResource/Pages/EditCategory.php | 51 ++ .../CategoryResource/Pages/ListCategories.php | 19 + app/Filament/Blog/Resources/PostResource.php | 304 +++++++ .../PostResource/Pages/CreatePost.php | 11 + .../Resources/PostResource/Pages/EditPost.php | 20 + .../PostResource/Pages/ListPosts.php | 19 + .../Resources/PostResource/Pages/ViewPost.php | 11 + app/Filament/Blog/Traits/HasContentEditor.php | 29 + app/Filament/Resources/PackageResource.php | 132 ++- .../PackageResource/Pages/CreatePackage.php | 10 +- .../PackageResource/Pages/EditPackage.php | 17 +- .../Resources/TenantPackageResource.php | 12 +- app/Filament/Resources/TenantResource.php | 9 +- .../PackagePurchasesRelationManager.php | 9 +- .../PurchasesRelationManager.php | 8 +- app/Filament/Resources/UserResource.php | 13 +- app/Http/Controllers/MarketingController.php | 263 +++++- .../Controllers/PayPalWebhookController.php | 153 ++-- .../Controllers/StripeWebhookController.php | 77 ++ app/Models/BlogCategory.php | 38 +- app/Models/BlogPost.php | 39 +- app/Models/BlogTag.php | 4 +- app/Models/Package.php | 29 +- .../Filament/SuperAdminPanelProvider.php | 19 +- composer.json | 1 - composer.lock | 830 +----------------- config/filament-blog.php | 7 +- ...id_required_in_package_purchases_table.php | 30 - ...ltilanguage_columns_to_blog_categories.php | 23 + ...name_description_from_blog_categories.php} | 11 +- ...nd_description_json_to_blog_categories.php | 27 + ..._columns_to_json_and_drop_translations.php | 32 + resources/lang/de/legal.php | 35 + resources/lang/de/marketing.php | 109 +++ resources/lang/en/legal.php | 32 + resources/lang/en/marketing.php | 149 ++++ resources/views/layouts/marketing.blade.php | 47 +- resources/views/legal/datenschutz.blade.php | 54 +- resources/views/legal/impressum.blade.php | 44 +- resources/views/marketing/blog-show.blade.php | 68 +- resources/views/marketing/blog.blade.php | 22 +- resources/views/marketing/occasions.blade.php | 136 ++- resources/views/marketing/packages.blade.php | 6 +- resources/views/marketing/register.blade.php | 2 +- resources/views/marketing/success.blade.php | 4 +- resources/views/partials/footer.blade.php | 10 + resources/views/partials/header.blade.php | 30 + routes/web.php | 65 +- tests/Feature/PurchaseTest.php | 66 ++ 51 files changed, 2093 insertions(+), 1293 deletions(-) create mode 100644 app/Filament/Blog/Resources/CategoryResource.php create mode 100644 app/Filament/Blog/Resources/CategoryResource/Pages/CreateCategory.php create mode 100644 app/Filament/Blog/Resources/CategoryResource/Pages/EditCategory.php create mode 100644 app/Filament/Blog/Resources/CategoryResource/Pages/ListCategories.php create mode 100644 app/Filament/Blog/Resources/PostResource.php create mode 100644 app/Filament/Blog/Resources/PostResource/Pages/CreatePost.php create mode 100644 app/Filament/Blog/Resources/PostResource/Pages/EditPost.php create mode 100644 app/Filament/Blog/Resources/PostResource/Pages/ListPosts.php create mode 100644 app/Filament/Blog/Resources/PostResource/Pages/ViewPost.php create mode 100644 app/Filament/Blog/Traits/HasContentEditor.php delete mode 100644 database/migrations/2025_09_27_110200_make_tenant_id_required_in_package_purchases_table.php create mode 100644 database/migrations/2025_09_29_155553_add_multilanguage_columns_to_blog_categories.php rename database/migrations/{2025_09_27_110300_add_unique_index_to_username_in_users_table.php => 2025_09_29_162547_drop_old_name_description_from_blog_categories.php} (54%) create mode 100644 database/migrations/2025_09_29_164248_add_name_and_description_json_to_blog_categories.php create mode 100644 database/migrations/2025_09_29_204232_alter_blog_categories_columns_to_json_and_drop_translations.php create mode 100644 resources/lang/de/legal.php create mode 100644 resources/lang/en/legal.php create mode 100644 resources/lang/en/marketing.php create mode 100644 resources/views/partials/footer.blade.php create mode 100644 resources/views/partials/header.blade.php diff --git a/app/Filament/Blog/Resources/CategoryResource.php b/app/Filament/Blog/Resources/CategoryResource.php new file mode 100644 index 0000000..b6d59d0 --- /dev/null +++ b/app/Filament/Blog/Resources/CategoryResource.php @@ -0,0 +1,217 @@ +schema([ + SchemaTabs::make('Übersetzungen') + ->tabs([ + SchemaTab::make('Deutsch') + ->schema([ + TextInput::make('name_de') + ->label('Name') + ->validationAttribute('name_de') + ->live() + ->afterStateUpdated(function (Get $get, Set $set, ?string $state) { + if (($get('slug') ?? '') !== Str::slug($state)) { + return; + } + + $set('slug', Str::slug($state)); + }), + MarkdownEditor::make('description_de') + ->label('Beschreibung') + ->validationAttribute('description_de'), + ]), + SchemaTab::make('Englisch') + ->schema([ + TextInput::make('name_en') + ->label('Name'), + MarkdownEditor::make('description_en') + ->label('Beschreibung'), + ]), + ]), + TextInput::make('slug') + ->label('Slug') + ->required() + ->unique(BlogCategory::class, 'slug', ignoreRecord: true), + Section::make('Sichtbarkeit') + ->schema([ + Toggle::make('is_visible') + ->label('Sichtbar für Gäste') + ->default(true), + ]), + Section::make('Metadaten') + ->schema([ + TextEntry::make('created_at') + ->label('Erstellt am') + ->default('—') + ->state(fn (?BlogCategory $record) => $record?->created_at?->diffForHumans()), + TextEntry::make('updated_at') + ->label('Zuletzt geändert') + ->default('—') + ->state(fn (?BlogCategory $record) => $record?->updated_at?->diffForHumans()), + ]), + ]) + ->columns(3); + } + + public static function mutateFormDataBeforeFill(array $data): array + { + $nameJson = $data['name'] ?? '[]'; + $nameArray = json_decode($nameJson, true) ?: []; + $data['name_de'] = $nameArray['de'] ?? ''; + $data['name_en'] = $nameArray['en'] ?? ''; + + $descJson = $data['description'] ?? '[]'; + $descArray = json_decode($descJson, true) ?: []; + $data['description_de'] = $descArray['de'] ?? ''; + $data['description_en'] = $descArray['en'] ?? ''; + + \Illuminate\Support\Facades\Log::info('BeforeFill Description Extraction:', [ + 'descJson' => $descJson, + 'descArray' => $descArray, + 'description_de' => $data['description_de'], + 'description_en' => $data['description_en'], + ]); + + return $data; + } + + public static function mutateFormDataBeforeCreate(array $data): array + { + \Illuminate\Support\Facades\Log::info('mutateFormDataBeforeCreate Input Data:', ['data' => $data]); + + $nameData = [ + 'de' => $data['name_de'] ?? '', + 'en' => $data['name_en'] ?? '', + ]; + $data['name'] = json_encode($nameData); + \Illuminate\Support\Facades\Log::info('mutateFormDataBeforeCreate Name JSON:', ['name' => $nameData]); + + $descData = [ + 'de' => $data['description_de'] ?? '', + 'en' => $data['description_en'] ?? '', + ]; + $data['description'] = json_encode($descData); + \Illuminate\Support\Facades\Log::info('mutateFormDataBeforeCreate Description JSON:', ['description' => $descData]); + + unset($data['name_de'], $data['name_en'], $data['description_de'], $data['description_en']); + + \Illuminate\Support\Facades\Log::info('mutateFormDataBeforeCreate Final Data:', $data); + + return $data; + } + + public static function mutateFormDataBeforeSave(array $data): array + { + $transformed = static::mutateFormDataBeforeCreate($data); + + return $transformed; + } + + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('name') + ->label('Name (DE)') + ->getStateUsing(fn ($record) => json_decode($record->name ?? '[]', true)['de'] ?? '—') + ->searchable() + ->sortable(), + TextColumn::make('slug') + ->label('Slug') + ->searchable() + ->sortable(), + IconColumn::make('is_visible') + ->label('Sichtbar') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle'), + TextColumn::make('updated_at') + ->label('Zuletzt geändert') + ->date() + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + EditAction::make(), + ]) + ->bulkActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListCategories::route('/'), + 'create' => Pages\CreateCategory::route('/create'), + 'edit' => Pages\EditCategory::route('/{record}/edit'), + ]; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} \ No newline at end of file diff --git a/app/Filament/Blog/Resources/CategoryResource/Pages/CreateCategory.php b/app/Filament/Blog/Resources/CategoryResource/Pages/CreateCategory.php new file mode 100644 index 0000000..a668dc1 --- /dev/null +++ b/app/Filament/Blog/Resources/CategoryResource/Pages/CreateCategory.php @@ -0,0 +1,33 @@ + 'required|string|max:255', + 'description_de' => 'nullable|string', + 'name_en' => 'nullable|string|max:255', + 'description_en' => 'nullable|string', + 'slug' => 'required|string|max:255|unique:blog_categories,slug', + 'is_visible' => 'boolean', + ]; + } + + protected function store() + { + $state = $this->form->getState(); + $data = $state['data'] ?? $state; + + $data = \App\Filament\Blog\Resources\CategoryResource::mutateFormDataBeforeCreate($data); + + $this->record = static::getResource()::getModel()::create($data); + } +} \ No newline at end of file diff --git a/app/Filament/Blog/Resources/CategoryResource/Pages/EditCategory.php b/app/Filament/Blog/Resources/CategoryResource/Pages/EditCategory.php new file mode 100644 index 0000000..7028b51 --- /dev/null +++ b/app/Filament/Blog/Resources/CategoryResource/Pages/EditCategory.php @@ -0,0 +1,51 @@ + 'required|string|max:255', + 'description_de' => 'nullable|string', + 'name_en' => 'nullable|string|max:255', + 'description_en' => 'nullable|string', + 'slug' => 'required|string|max:255|unique:blog_categories,slug,' . $this->record->id, + 'is_visible' => 'boolean', + ]; + } + + public function mount($record): void + { + parent::mount($record); + $data = $this->record->toArray(); + $data = \App\Filament\Blog\Resources\CategoryResource::mutateFormDataBeforeFill($data); + $this->form->fill($data); + } + + public function save(bool $shouldRedirect = true, bool $shouldSendSavedNotification = true): void + { + $state = $this->form->getState(); + \Illuminate\Support\Facades\Log::info('EditCategory Save - Full State:', $state); + + $data = $state['data'] ?? $state; + + $data = \App\Filament\Blog\Resources\CategoryResource::mutateFormDataBeforeSave($data); + + $this->record->update($data); + } +} \ No newline at end of file diff --git a/app/Filament/Blog/Resources/CategoryResource/Pages/ListCategories.php b/app/Filament/Blog/Resources/CategoryResource/Pages/ListCategories.php new file mode 100644 index 0000000..7da37af --- /dev/null +++ b/app/Filament/Blog/Resources/CategoryResource/Pages/ListCategories.php @@ -0,0 +1,19 @@ +schema([ + SchemaTabs::make('Übersetzungen') + ->tabs([ + SchemaTab::make('Deutsch') + ->schema([ + TextInput::make('title_de') + ->label('Titel') + ->required() + ->maxLength(255) + ->live() + ->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) { + if (($get('slug') ?? '') !== Str::slug($state)) { + return; + } + + $set('slug', Str::slug($state)); + }), + MarkdownEditor::make('content_de') + ->label('Inhalt') + ->required() + ->columnSpanFull(), + TextInput::make('excerpt_de') + ->label('Auszug') + ->maxLength(255), + TextInput::make('meta_title_de') + ->label('Meta-Titel') + ->maxLength(255), + Textarea::make('meta_description_de') + ->label('Meta-Beschreibung') + ->maxLength(65535) + ->columnSpanFull(), + ]) + ->columns(2), + SchemaTab::make('Englisch') + ->schema([ + TextInput::make('title_en') + ->label('Titel') + ->maxLength(255), + MarkdownEditor::make('content_en') + ->label('Inhalt') + ->columnSpanFull(), + TextInput::make('excerpt_en') + ->label('Auszug') + ->maxLength(255), + TextInput::make('meta_title_en') + ->label('Meta-Titel') + ->maxLength(255), + Textarea::make('meta_description_en') + ->label('Meta-Beschreibung') + ->maxLength(65535) + ->columnSpanFull(), + ]) + ->columns(2), + ]), + TextInput::make('slug') + ->label('Slug') + ->required() + ->unique(BlogPost::class, 'slug', ignoreRecord: true) + ->maxLength(255), + Section::make('Bild und Kategorie') + ->schema([ + FileUpload::make('featured_image') + ->label('Featured Image') + ->image() + ->directory('blog') + ->visibility('public'), + Select::make('category_id') + ->label('Kategorie') + ->relationship('category', 'name_de') + ->required() + ->preload() + ->createOptionForm([ + TextInput::make('name_de') + ->label('Name (DE)') + ->required() + ->maxLength(255) + ->afterStateUpdated(fn (Set $set, $state) => $set('name_en', $state)), + TextInput::make('slug') + ->label('Slug') + ->required() + ->unique(\App\Models\BlogCategory::class, 'slug', ignoreRecord: true) + ->maxLength(255), + ]), + ]) + ->columns(2), + Section::make('Veröffentlichung') + ->schema([ + Toggle::make('is_published') + ->label('Veröffentlicht'), + DateTimePicker::make('published_at') + ->label('Veröffentlicht am') + ->displayFormat('Y-m-d H:i:s') + ->default(now()), + ]), + ]); + } + + public static function mutateFormDataBeforeCreate(array $data): array + { + $data['translations'] = [ + 'title' => [ + 'de' => $data['title_de'] ?? '', + 'en' => $data['title_en'] ?? '', + ], + 'content' => [ + 'de' => $data['content_de'] ?? '', + 'en' => $data['content_en'] ?? '', + ], + 'excerpt' => [ + 'de' => $data['excerpt_de'] ?? '', + 'en' => $data['excerpt_en'] ?? '', + ], + 'meta_title' => [ + 'de' => $data['meta_title_de'] ?? '', + 'en' => $data['meta_title_en'] ?? '', + ], + 'meta_description' => [ + 'de' => $data['meta_description_de'] ?? '', + 'en' => $data['meta_description_en'] ?? '', + ], + ]; + + unset($data['title_de'], $data['title_en'], $data['content_de'], $data['content_en'], $data['excerpt_de'], $data['excerpt_en'], $data['meta_title_de'], $data['meta_title_en'], $data['meta_description_de'], $data['meta_description_en']); + + return $data; + } + + public static function mutateFormDataBeforeFill(array $data): array + { + $record = static::getModel()::find(request()?->route()?->parameter('record') ?? request()?->input('record_id') ?? null); + + if (!$record) { + return $data; + } + + $data['title_de'] = $record->getTranslation('title', 'de'); + $data['title_en'] = $record->getTranslation('title', 'en'); + $data['content_de'] = $record->getTranslation('content', 'de'); + $data['content_en'] = $record->getTranslation('content', 'en'); + $data['excerpt_de'] = $record->getTranslation('excerpt', 'de'); + $data['excerpt_en'] = $record->getTranslation('excerpt', 'en'); + $data['meta_title_de'] = $record->getTranslation('meta_title', 'de'); + $data['meta_title_en'] = $record->getTranslation('meta_title', 'en'); + $data['meta_description_de'] = $record->getTranslation('meta_description', 'de'); + $data['meta_description_en'] = $record->getTranslation('meta_description', 'en'); + + return $data; + } + + public static function mutateFormDataBeforeSave(array $data): array + { + $data['translations'] = [ + 'title' => [ + 'de' => $data['title_de'] ?? '', + 'en' => $data['title_en'] ?? '', + ], + 'content' => [ + 'de' => $data['content_de'] ?? '', + 'en' => $data['content_en'] ?? '', + ], + 'excerpt' => [ + 'de' => $data['excerpt_de'] ?? '', + 'en' => $data['excerpt_en'] ?? '', + ], + 'meta_title' => [ + 'de' => $data['meta_title_de'] ?? '', + 'en' => $data['meta_title_en'] ?? '', + ], + 'meta_description' => [ + 'de' => $data['meta_description_de'] ?? '', + 'en' => $data['meta_description_en'] ?? '', + ], + ]; + + unset($data['title_de'], $data['title_en'], $data['content_de'], $data['content_en'], $data['excerpt_de'], $data['excerpt_en'], $data['meta_title_de'], $data['meta_title_en'], $data['meta_description_de'], $data['meta_description_en']); + + return $data; + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('title') + ->label('Titel (DE)') + ->getStateUsing(fn ($record) => $record->getTranslation('title', 'de')) + ->searchable() + ->sortable(), + TextColumn::make('category.name_de') + ->label('Kategorie') + ->badge() + ->color('primary'), + IconColumn::make('is_published') + ->label('Veröffentlicht') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle'), + TextColumn::make('published_at') + ->label('Veröffentlicht am') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('created_at') + ->label('Erstellt am') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + TernaryFilter::make('is_published') + ->label('Veröffentlicht'), + ]) + ->actions([ + ViewAction::make(), + EditAction::make(), + DeleteAction::make(), + ]) + ->bulkActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListPosts::route('/'), + 'create' => Pages\CreatePost::route('/create'), + 'view' => Pages\ViewPost::route('/{record}'), + 'edit' => Pages\EditPost::route('/{record}/edit'), + ]; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} \ No newline at end of file diff --git a/app/Filament/Blog/Resources/PostResource/Pages/CreatePost.php b/app/Filament/Blog/Resources/PostResource/Pages/CreatePost.php new file mode 100644 index 0000000..273e44c --- /dev/null +++ b/app/Filament/Blog/Resources/PostResource/Pages/CreatePost.php @@ -0,0 +1,11 @@ +label('Inhalt') + ->columnSpanFull() + ->toolbarButtons(config('filament-blog.toolbar_buttons', [ + 'bold', + 'italic', + 'underline', + 'strike', + 'bulletList', + 'orderedList', + 'link', + 'table', + 'codeBlock', + 'h1', + 'h2', + 'h3', + ])); + } +} \ No newline at end of file diff --git a/app/Filament/Resources/PackageResource.php b/app/Filament/Resources/PackageResource.php index 51837bc..0ae7bad 100644 --- a/app/Filament/Resources/PackageResource.php +++ b/app/Filament/Resources/PackageResource.php @@ -93,6 +93,135 @@ class PackageResource extends Resource ]); } + + public static function featuresToRepeaterItems(mixed $features): array + { + if (is_string($features)) { + $decoded = json_decode($features, true); + + if (is_string($decoded)) { + $decoded = json_decode($decoded, true); + } + + $features = json_last_error() === JSON_ERROR_NONE ? $decoded : null; + } + + if ($features === null) { + return []; + } + + if (! is_array($features)) { + return []; + } + + if (! array_is_list($features)) { + return collect($features) + ->map(function ($value, $key) { + return [ + 'key' => (string) $key, + 'value' => is_bool($value) ? ($value ? 'true' : 'false') : (string) $value, + ]; + }) + ->values() + ->all(); + } + + return collect($features) + ->map(function ($item) { + if (is_array($item)) { + return [ + 'key' => (string) ($item['key'] ?? ''), + 'value' => (string) ($item['value'] ?? ''), + ]; + } + + return [ + 'key' => (string) $item, + 'value' => 'true', + ]; + }) + ->values() + ->all(); + } + + public static function featuresFromRepeaterItems(mixed $items): array + { + if (! is_array($items)) { + return []; + } + + $features = []; + + foreach ($items as $item) { + if (! is_array($item)) { + continue; + } + + $key = isset($item['key']) ? trim((string) $item['key']) : ''; + + if ($key === '') { + continue; + } + + $value = $item['value'] ?? true; + + if (is_string($value)) { + $normalized = strtolower(trim($value)); + + if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) { + $value = true; + } elseif (in_array($normalized, ['0', 'false', 'no', 'off'], true)) { + $value = false; + } + } + + if (is_array($value)) { + $value = $value['value'] ?? $value['enabled'] ?? true; + } + + $features[$key] = (bool) $value; + } + + return $features; + } + + public static function formatFeaturesForDisplay(mixed $features): string + { + $map = $features; + + if (! is_array($map)) { + if (is_string($map)) { + $decoded = json_decode($map, true); + + if (is_string($decoded)) { + $decoded = json_decode($decoded, true); + } + + $map = json_last_error() === JSON_ERROR_NONE ? $decoded : []; + } else { + $map = []; + } + } + + if (! array_is_list($map)) { + return collect($map) + ->filter(fn ($value) => (bool) $value) + ->keys() + ->implode(', '); + } + + return collect($map) + ->map(function ($item) { + if (is_array($item)) { + return (string) ($item['key'] ?? ''); + } + + return (string) $item; + }) + ->filter() + ->implode(', '); + } + public static function table(Table $table): Table { return $table @@ -119,6 +248,7 @@ class PackageResource extends Resource ->color('primary'), TextColumn::make('features') ->label('Features') + ->formatStateUsing(fn ($state) => static::formatFeaturesForDisplay($state)) ->limit(50), ]) ->filters([ @@ -150,4 +280,4 @@ class PackageResource extends Resource 'edit' => Pages\EditPackage::route('/{record}/edit'), ]; } -} \ No newline at end of file +} diff --git a/app/Filament/Resources/PackageResource/Pages/CreatePackage.php b/app/Filament/Resources/PackageResource/Pages/CreatePackage.php index 45f0e1b..8bd4a1a 100644 --- a/app/Filament/Resources/PackageResource/Pages/CreatePackage.php +++ b/app/Filament/Resources/PackageResource/Pages/CreatePackage.php @@ -8,4 +8,12 @@ use Filament\Resources\Pages\CreateRecord; class CreatePackage extends CreateRecord { protected static string $resource = PackageResource::class; -} \ No newline at end of file + + protected function mutateFormDataBeforeCreate(array $data): array + { + $data['features'] = PackageResource::featuresFromRepeaterItems($data['features'] ?? []); + + return $data; + } + +} diff --git a/app/Filament/Resources/PackageResource/Pages/EditPackage.php b/app/Filament/Resources/PackageResource/Pages/EditPackage.php index d55263a..c6763b9 100644 --- a/app/Filament/Resources/PackageResource/Pages/EditPackage.php +++ b/app/Filament/Resources/PackageResource/Pages/EditPackage.php @@ -17,4 +17,19 @@ class EditPackage extends EditRecord Actions\DeleteAction::make(), ]; } -} \ No newline at end of file + + protected function mutateFormDataBeforeFill(array $data): array + { + $data['features'] = PackageResource::featuresToRepeaterItems($data['features'] ?? null); + + return $data; + } + + protected function mutateFormDataBeforeSave(array $data): array + { + $data['features'] = PackageResource::featuresFromRepeaterItems($data['features'] ?? []); + + return $data; + } + +} diff --git a/app/Filament/Resources/TenantPackageResource.php b/app/Filament/Resources/TenantPackageResource.php index 9d947fc..baa0242 100644 --- a/app/Filament/Resources/TenantPackageResource.php +++ b/app/Filament/Resources/TenantPackageResource.php @@ -12,12 +12,12 @@ use Filament\Forms\Components\Toggle; use Filament\Icons\Icon; use Filament\Resources\Resource; use Filament\Tables; -use Filament\Tables\Actions\ActionGroup; -use Filament\Tables\Actions\BulkActionGroup; -use Filament\Tables\Actions\CreateAction; -use Filament\Tables\Actions\DeleteAction; -use Filament\Tables\Actions\EditAction; -use Filament\Tables\Actions\ViewAction; +use Filament\Actions\ActionGroup; +use Filament\Actions\BulkActionGroup; +use Filament\Actions\CreateAction; +use Filament\Actions\DeleteAction; +use Filament\Actions\EditAction; +use Filament\Actions\ViewAction; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 78c9b56..fa914a8 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -38,8 +38,7 @@ class TenantResource extends Resource public static function form(Schema $form): Schema { - \Illuminate\Support\Facades\Log::info('TenantResource form() method called'); - + return $form->schema([ TextInput::make('name') ->label(__('admin.tenants.fields.name')) @@ -87,8 +86,7 @@ class TenantResource extends Resource public static function table(Table $table): Table { - \Illuminate\Support\Facades\Log::info('TenantResource table() method called'); - + return $table ->columns([ Tables\Columns\TextColumn::make('id')->sortable(), @@ -183,8 +181,7 @@ class TenantResource extends Resource public static function getRelations(): array { - \Illuminate\Support\Facades\Log::info('TenantResource getRelations() method called'); - + return [ TenantPackagesRelationManager::class, PackagePurchasesRelationManager::class, diff --git a/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php b/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php index f4427bd..b4eae30 100644 --- a/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php +++ b/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php @@ -10,9 +10,9 @@ use Filament\Forms\Components\Textarea; use Filament\Schemas\Schema; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; -use Filament\Tables\Actions\BulkActionGroup; -use Filament\Tables\Actions\DeleteBulkAction; -use Filament\Tables\Actions\ViewAction; +use Filament\Actions\BulkActionGroup; +use Filament\Actions\DeleteBulkAction; +use Filament\Actions\ViewAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Filters\SelectFilter; @@ -140,4 +140,5 @@ class PackagePurchasesRelationManager extends RelationManager ]), ]); } -} \ No newline at end of file +} + diff --git a/app/Filament/Resources/TenantResource/RelationManagers/PurchasesRelationManager.php b/app/Filament/Resources/TenantResource/RelationManagers/PurchasesRelationManager.php index 8613725..ffd2a43 100644 --- a/app/Filament/Resources/TenantResource/RelationManagers/PurchasesRelationManager.php +++ b/app/Filament/Resources/TenantResource/RelationManagers/PurchasesRelationManager.php @@ -10,9 +10,9 @@ use Filament\Forms\Components\Textarea; use Filament\Schemas\Schema; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; -use Filament\Tables\Actions\BulkActionGroup; -use Filament\Tables\Actions\DeleteBulkAction; -use Filament\Tables\Actions\ViewAction; +use Filament\Actions\BulkActionGroup; +use Filament\Actions\DeleteBulkAction; +use Filament\Actions\ViewAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; @@ -128,3 +128,5 @@ class PurchasesRelationManager extends RelationManager ]); } } + + diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index b32ffda..c42a22c 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -12,11 +12,11 @@ use Filament\Forms\Components\Textarea; use Filament\Icons\Icon; use Filament\Resources\Resource; use Filament\Tables; -use Filament\Tables\Actions\ActionGroup; -use Filament\Tables\Actions\BulkActionGroup; -use Filament\Tables\Actions\DeleteBulkAction; -use Filament\Tables\Actions\EditAction; -use Filament\Tables\Actions\ViewAction; +use Filament\Actions\ActionGroup; +use Filament\Actions\BulkActionGroup; +use Filament\Actions\DeleteBulkAction; +use Filament\Actions\EditAction; +use Filament\Actions\ViewAction; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -129,4 +129,5 @@ class UserResource extends Resource 'edit' => Pages\EditUser::route('/{record}/edit'), ]; } -} \ No newline at end of file +} + diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php index fe36b24..69eb72b 100644 --- a/app/Http/Controllers/MarketingController.php +++ b/app/Http/Controllers/MarketingController.php @@ -10,15 +10,14 @@ use Stripe\Stripe; use Stripe\Checkout\Session; use Stripe\StripeClient; use Exception; -use PayPal\Api\Amount; -use PayPal\Api\Payer; -use PayPal\Api\Payment; -use PayPal\Api\RedirectUrls; -use PayPal\Api\Transaction; -use PayPal\Rest\ApiContext; -use PayPal\Auth\OAuthTokenCredential; +use PayPal\PayPalHttp\Client; +use PayPal\PayPalHttp\HttpException; +use PayPal\Checkout\Orders\OrdersCreateRequest; +use PayPal\Checkout\Orders\OrdersCaptureRequest; +use PayPal\Checkout\Orders\OrdersGetRequest; +use PayPal\Checkout\Orders\Order; use App\Models\Tenant; -use App\Models\EventPurchase; +use App\Models\BlogPost; use App\Models\Package; use App\Models\TenantPackage; use App\Models\PackagePurchase; @@ -107,6 +106,10 @@ class MarketingController extends Controller return redirect('/admin')->with('success', __('marketing.packages.free_assigned')); } + if ($package->type === 'reseller') { + return $this->stripeSubscription($request, $packageId); + } + if ($request->input('provider') === 'paypal') { return $this->paypalCheckout($request, $packageId); } @@ -151,7 +154,7 @@ class MarketingController extends Controller } /** - * PayPal checkout with auth metadata. + * PayPal checkout with v2 Orders API (one-time payment). */ public function paypalCheckout(Request $request, $packageId) { @@ -159,78 +162,228 @@ class MarketingController extends Controller $user = Auth::user(); $tenant = $user->tenant; - $apiContext = new ApiContext( - new OAuthTokenCredential( - config('services.paypal.client_id'), - config('services.paypal.secret') - ) - ); + $client = Client::create([ + 'clientId' => config('services.paypal.client_id'), + 'clientSecret' => config('services.paypal.secret'), + 'environment' => config('services.paypal.sandbox', true) ? 'sandbox' : 'live', + ]); - $payment = new Payment(); - $payer = new Payer(); - $payer->setPaymentMethod('paypal'); + $ordersController = $client->orders(); - $amountObj = new Amount(); - $amountObj->setCurrency('EUR'); - $amountObj->setTotal($package->price); - - $transaction = new Transaction(); - $transaction->setAmount($amountObj); - - $redirectUrls = new RedirectUrls(); - $redirectUrls->setReturnUrl(route('marketing.success', $packageId)); - $redirectUrls->setCancelUrl(route('packages')); - - $customData = json_encode([ + $metadata = json_encode([ 'user_id' => $user->id, 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'type' => $package->type, ]); - $payment->setIntent('sale') - ->setPayer($payer) - ->setTransactions([$transaction]) - ->setRedirectUrls($redirectUrls) - ->setNoteToPayer('Package: ' . $package->name) - ->setCustom($customData); + $createRequest = new OrdersCreateRequest(); + $createRequest->prefer('return=representation'); + $createRequest->body = [ + "intent" => "CAPTURE", + "purchase_units" => [[ + "amount" => [ + "currency_code" => "EUR", + "value" => number_format($package->price, 2, '.', ''), + ], + "description" => "Package: " . $package->name, + "custom_id" => $metadata, + ]], + "application_context" => [ + "return_url" => route('marketing.success', $packageId), + "cancel_url" => route('packages'), + ], + ]; try { - $payment->create($apiContext); + $response = $ordersController->createOrder($createRequest); + $order = $response->result; - session(['paypal_payment_id' => $payment->getId()]); + session(['paypal_order_id' => $order->id]); - return redirect($payment->getApprovalLink()); + foreach ($order->links as $link) { + if ($link->rel === 'approve') { + return redirect($link->href); + } + } + + throw new Exception('No approve link found'); + } catch (HttpException $e) { + Log::error('PayPal Orders API error: ' . $e->getMessage()); + return back()->with('error', 'Zahlung fehlgeschlagen'); } catch (\Exception $e) { Log::error('PayPal checkout error: ' . $e->getMessage()); return back()->with('error', 'Zahlung fehlgeschlagen'); } } + /** + * Stripe subscription checkout for reseller packages. + */ + public function stripeSubscription(Request $request, $packageId) + { + $package = Package::findOrFail($packageId); + $user = Auth::user(); + $tenant = $user->tenant; + + $stripe = new StripeClient(config('services.stripe.secret')); + $session = $stripe->checkout->sessions->create([ + 'payment_method_types' => ['card'], + 'line_items' => [[ + 'price_data' => [ + 'currency' => 'eur', + 'product_data' => [ + 'name' => $package->name . ' (Annual Subscription)', + ], + 'unit_amount' => $package->price * 100, + 'recurring' => [ + 'interval' => 'year', + 'interval_count' => 1, + ], + ], + 'quantity' => 1, + ]], + 'mode' => 'subscription', + 'success_url' => route('marketing.success', $packageId), + 'cancel_url' => route('packages'), + 'metadata' => [ + 'user_id' => $user->id, + 'tenant_id' => $tenant->id, + 'package_id' => $package->id, + 'type' => $package->type, + 'subscription' => 'true', + ], + ]); + + return redirect($session->url, 303); + } + public function stripeCheckout($sessionId) { // Handle Stripe success return view('marketing.success', ['provider' => 'Stripe']); } + /** + * Handle success after payment (capture PayPal, redirect if verified). + */ + public function success(Request $request, $packageId = null) + { + if (session('paypal_order_id')) { + $orderId = session('paypal_order_id'); + $client = Client::create([ + 'clientId' => config('services.paypal.client_id'), + 'clientSecret' => config('services.paypal.secret'), + 'environment' => config('services.paypal.sandbox', true) ? 'sandbox' : 'live', + ]); + + $ordersController = $client->orders(); + + $captureRequest = new OrdersCaptureRequest($orderId); + $captureRequest->prefer('return=minimal'); + + try { + $captureResponse = $ordersController->captureOrder($captureRequest); + $capture = $captureResponse->result; + + if ($capture->status === 'COMPLETED') { + $customId = $capture->purchaseUnits[0]->customId ?? null; + if ($customId) { + $metadata = json_decode($customId, true); + $package = Package::find($metadata['package_id']); + $tenant = Tenant::find($metadata['tenant_id']); + + if ($package && $tenant) { + TenantPackage::updateOrCreate( + [ + 'tenant_id' => $tenant->id, + 'package_id' => $package->id, + ], + [ + 'active' => true, + 'purchased_at' => now(), + 'expires_at' => now()->addYear(), // One-time as annual for reseller too + ] + ); + + PackagePurchase::create([ + 'tenant_id' => $tenant->id, + 'package_id' => $package->id, + 'provider_id' => 'paypal', + 'price' => $package->price, + 'type' => $package->type, + 'purchased_at' => now(), + 'refunded' => false, + ]); + + session()->forget('paypal_order_id'); + $request->session()->flash('success', __('marketing.packages.purchased_successfully', ['name' => $package->name])); + } + } + } else { + Log::error('PayPal capture failed: ' . $capture->status); + $request->session()->flash('error', 'Zahlung konnte nicht abgeschlossen werden.'); + } + } catch (HttpException $e) { + Log::error('PayPal capture error: ' . $e->getMessage()); + $request->session()->flash('error', 'Zahlung konnte nicht abgeschlossen werden.'); + } catch (\Exception $e) { + Log::error('PayPal success error: ' . $e->getMessage()); + $request->session()->flash('error', 'Fehler beim Abschließen der Zahlung.'); + } + } + + // Common logic: Redirect to admin if verified + if (Auth::check() && Auth::user()->email_verified_at) { + return redirect('/admin')->with('success', __('marketing.success.welcome')); + } + + return view('marketing.success', compact('packageId')); + } + public function blogIndex(Request $request) { $locale = $request->get('locale', app()->getLocale()); - $posts = \Stephenjude\FilamentBlog\Models\Post::query() - ->where('is_published', true) + Log::info('Blog Index Debug - Initial', [ + 'locale' => $locale, + 'full_url' => $request->fullUrl() + ]); + + $query = BlogPost::query() + ->whereHas('category', function ($query) { + $query->where('slug', 'blog'); + }); + + $totalWithCategory = $query->count(); + Log::info('Blog Index Debug - With Category', ['count' => $totalWithCategory]); + + $query->where('is_published', true) ->whereNotNull('published_at') - ->where('published_at', '<=', now()) - ->whereJsonContains("translations->locale->title->{$locale}", true) - ->orderBy('published_at', 'desc') + ->where('published_at', '<=', now()); + + $totalPublished = $query->count(); + Log::info('Blog Index Debug - Published', ['count' => $totalPublished]); + + $query->whereJsonContains("translations->locale->title->{$locale}", true); + + $totalWithTranslation = $query->count(); + Log::info('Blog Index Debug - With Translation', ['count' => $totalWithTranslation, 'locale' => $locale]); + + $posts = $query->orderBy('published_at', 'desc') ->paginate(8); + Log::info('Blog Index Debug - Final Posts', ['count' => $posts->count(), 'total' => $posts->total()]); + return view('marketing.blog', compact('posts')); } public function blogShow($slug) { $locale = app()->getLocale(); - $post = \Stephenjude\FilamentBlog\Models\Post::query() + $post = BlogPost::query() + ->whereHas('category', function ($query) { + $query->where('slug', 'blog'); + }) ->where('slug', $slug) ->where('is_published', true) ->whereNotNull('published_at') @@ -240,4 +393,24 @@ class MarketingController extends Controller return view('marketing.blog-show', compact('post')); } + + public function packagesIndex() + { + + $endcustomerPackages = Package::where('type', 'endcustomer')->orderBy('price')->get(); + $resellerPackages = Package::where('type', 'reseller')->orderBy('price')->get(); + + return view('marketing.packages', compact('endcustomerPackages', 'resellerPackages')); + } + + public function occasionsType($locale, $type) + { + + $validTypes = ['weddings', 'birthdays', 'corporate-events', 'family-celebrations']; + if (!in_array($type, $validTypes)) { + abort(404, 'Invalid occasion type'); + } + + return view('marketing.occasions', ['type' => $type]); + } } diff --git a/app/Http/Controllers/PayPalWebhookController.php b/app/Http/Controllers/PayPalWebhookController.php index d0883eb..1363a1d 100644 --- a/app/Http/Controllers/PayPalWebhookController.php +++ b/app/Http/Controllers/PayPalWebhookController.php @@ -4,8 +4,13 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; -use App\Models\EventPurchase; +use PayPal\PayPalHttp\Client; +use PayPal\Checkout\Orders\OrdersGetRequest; +use App\Models\TenantPackage; +use App\Models\PackagePurchase; +use App\Models\Package; use App\Models\Tenant; +use Exception; class PayPalWebhookController extends Controller { @@ -13,72 +18,114 @@ class PayPalWebhookController extends Controller { $input = $request->all(); $ipnMessage = $input['ipn_track_id'] ?? null; - $payerEmail = $input['payer_email'] ?? null; - $paymentStatus = $input['payment_status'] ?? null; - $mcGross = $input['mc_gross'] ?? 0; - $custom = $input['custom'] ?? null; + $verification = $this->verifyIPN($request); - if ($paymentStatus === 'Completed' && $mcGross > 0) { - // Verify IPN with PayPal (simplified; use SDK for full verification) - // $verified = $this->verifyIPN($input); + if (!$verification) { + Log::warning('PayPal IPN verification failed', ['ipn_track_id' => $ipnMessage]); + return response('Invalid IPN', 400); + } - // Parse custom for user_id or tenant_id - $data = json_decode($custom, true); - $userId = $data['user_id'] ?? null; - $tenantId = $data['tenant_id'] ?? null; - $packageId = $data['package_id'] ?? null; + $eventType = $input['payment_status'] ?? null; + $customId = $input['custom'] ?? null; - if ($userId && !$tenantId) { - $tenant = \App\Models\Tenant::where('user_id', $userId)->first(); - if ($tenant) { - $tenantId = $tenant->id; - } else { - Log::error('Tenant not found for user_id in PayPal IPN: ' . $userId); - return response('OK', 200); - } + if (!$eventType || !$customId) { + Log::warning('Missing event type or custom ID in PayPal IPN', ['input' => $input]); + return response('Invalid data', 400); + } + + if ($eventType !== 'Completed') { + Log::info('Non-completed PayPal event ignored', ['event' => $eventType, 'ipn_track_id' => $ipnMessage]); + return response('OK', 200); + } + + try { + $metadata = json_decode($customId, true); + if (!$metadata || !isset($metadata['tenant_id'], $metadata['package_id'])) { + throw new Exception('Invalid metadata'); } - if (!$tenantId || !$packageId) { - Log::error('Missing tenant or package in PayPal IPN custom data'); + $tenant = Tenant::find($metadata['tenant_id']); + $package = Package::find($metadata['package_id']); + + if (!$tenant || !$package) { + throw new Exception('Tenant or package not found'); + } + + // Idempotent: Check if already processed + $existingPurchase = PackagePurchase::where('tenant_id', $tenant->id) + ->where('package_id', $package->id) + ->where('provider_id', 'paypal') + ->where('purchased_at', '>=', now()->subDay()) // Recent to avoid duplicates + ->first(); + + if ($existingPurchase) { + Log::info('PayPal purchase already processed', ['purchase_id' => $existingPurchase->id]); return response('OK', 200); } - // Create PackagePurchase - \App\Models\PackagePurchase::create([ - 'tenant_id' => $tenantId, - 'package_id' => $packageId, - 'provider_id' => $ipnMessage, - 'price' => $mcGross, - 'type' => $data['type'] ?? 'reseller_subscription', + // Activate package + TenantPackage::updateOrCreate( + [ + 'tenant_id' => $tenant->id, + 'package_id' => $package->id, + ], + [ + 'active' => true, + 'purchased_at' => now(), + 'expires_at' => now()->addYear(), + ] + ); + + // Log purchase + PackagePurchase::create([ + 'tenant_id' => $tenant->id, + 'package_id' => $package->id, + 'provider_id' => 'paypal', + 'price' => $package->price, + 'type' => $package->type, 'purchased_at' => now(), 'refunded' => false, ]); - // Update TenantPackage if subscription - if ($data['type'] ?? '' === 'reseller_subscription') { - \App\Models\TenantPackage::updateOrCreate( - [ - 'tenant_id' => $tenantId, - 'package_id' => $packageId, - ], - [ - 'active' => true, - 'purchased_at' => now(), - 'expires_at' => now()->addYear(), - ] - ); - } + $tenant->update(['subscription_status' => 'active']); - Log::info('PayPal IPN processed for tenant ' . $tenantId . ', package ' . $packageId, $input); + Log::info('PayPal purchase processed successfully', [ + 'tenant_id' => $tenant->id, + 'package_id' => $package->id, + 'ipn_track_id' => $ipnMessage, + ]); + + return response('OK', 200); + } catch (Exception $e) { + Log::error('PayPal webhook processing error: ' . $e->getMessage(), [ + 'input' => $input, + 'ipn_track_id' => $ipnMessage, + ]); + return response('Error', 500); + } + } + + private function verifyIPN(Request $request) + { + $rawBody = $request->getContent(); + $params = $request->all(); + + // For sandbox, post to PayPal verify endpoint + $verifyParams = array_merge($params, ['cmd' => '_notify-validate']); + + $response = file_get_contents('https://ipnpb.paypal.com/cgi-bin/webscr', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => http_build_query($verifyParams), + ], + ])); + + if ($response === false) { + Log::error('PayPal IPN verification request failed'); + return false; } - return response('OK', 200); - } - - private function verifyIPN($input) - { - // Use PayPal SDK to verify - // Return true/false - return true; // Placeholder + return trim($response) === 'VERIFIED'; } } diff --git a/app/Http/Controllers/StripeWebhookController.php b/app/Http/Controllers/StripeWebhookController.php index a848988..c7fc7a8 100644 --- a/app/Http/Controllers/StripeWebhookController.php +++ b/app/Http/Controllers/StripeWebhookController.php @@ -30,6 +30,13 @@ class StripeWebhookController extends Controller } switch ($event['type']) { + case 'checkout.session.completed': + $session = $event['data']['object']; + if ($session['mode'] === 'subscription' && isset($session['metadata']['subscription']) && $session['metadata']['subscription'] === 'true') { + $this->handleSubscriptionStarted($session); + } + break; + case 'payment_intent.succeeded': $paymentIntent = $event['data']['object']; $this->handlePaymentIntentSucceeded($paymentIntent); @@ -167,4 +174,74 @@ class StripeWebhookController extends Controller // TODO: Deactivate package or notify tenant // e.g., TenantPackage::where('provider_id', $subscription)->update(['active' => false]); } + + private function handleSubscriptionStarted($session) + { + $metadata = $session['metadata']; + if (!$metadata['user_id'] && !$metadata['tenant_id'] || !$metadata['package_id']) { + Log::warning('Missing metadata in Stripe checkout session: ' . $session['id']); + return; + } + + $userId = $metadata['user_id'] ?? null; + $tenantId = $metadata['tenant_id'] ?? null; + $packageId = $metadata['package_id']; + $type = $metadata['type'] ?? 'reseller_subscription'; + + if ($userId && !$tenantId) { + $tenant = \App\Models\Tenant::where('user_id', $userId)->first(); + if ($tenant) { + $tenantId = $tenant->id; + } else { + Log::error('Tenant not found for user_id: ' . $userId); + return; + } + } + + if (!$tenantId) { + Log::error('No tenant_id found for Stripe checkout session: ' . $session['id']); + return; + } + + $subscriptionId = $session['subscription']['id'] ?? null; + if (!$subscriptionId) { + Log::error('No subscription ID in checkout session: ' . $session['id']); + return; + } + + // Activate TenantPackage for initial subscription + \App\Models\TenantPackage::updateOrCreate( + [ + 'tenant_id' => $tenantId, + 'package_id' => $packageId, + ], + [ + 'purchased_at' => now(), + 'expires_at' => now()->addYear(), + 'active' => true, + ] + ); + + // Create initial PackagePurchase + \App\Models\PackagePurchase::create([ + 'tenant_id' => $tenantId, + 'package_id' => $packageId, + 'provider_id' => $subscriptionId, + 'price' => $session['amount_total'] / 100, + 'type' => $type, + 'purchased_at' => now(), + 'refunded' => false, + ]); + + // Update tenant subscription fields if needed + $tenant = \App\Models\Tenant::find($tenantId); + if ($tenant) { + $tenant->update([ + 'subscription_id' => $subscriptionId, + 'subscription_status' => 'active', + ]); + } + + Log::info('Initial subscription activated via Stripe checkout session: ' . $session['id'] . ' for tenant ' . $tenantId); + } } \ No newline at end of file diff --git a/app/Models/BlogCategory.php b/app/Models/BlogCategory.php index 6cf89b6..f9f7c9a 100644 --- a/app/Models/BlogCategory.php +++ b/app/Models/BlogCategory.php @@ -2,24 +2,44 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Spatie\Translatable\HasTranslations; -use Stephenjude\FilamentBlog\Models\Category as BaseCategory; +use Illuminate\Support\Facades\Log; -class BlogCategory extends BaseCategory +class BlogCategory extends Model { - use HasFactory, SoftDeletes, HasTranslations; + use HasFactory, SoftDeletes; - protected $translatable = [ - 'name', - 'description', - ]; + protected $table = 'blog_categories'; protected $fillable = [ 'slug', 'is_visible', - 'translations', + 'name', + 'description', ]; + + protected $casts = [ + 'is_visible' => 'boolean', + 'name' => 'array', + 'description' => 'array', + ]; + + public function posts(): HasMany + { + return $this->hasMany(BlogPost::class, 'blog_category_id'); + } + + public function scopeIsVisible(Builder $query) + { + return $query->where('is_visible', true); + } + + public function scopeIsInvisible(Builder $query) + { + return $query->where('is_visible', false); + } } \ No newline at end of file diff --git a/app/Models/BlogPost.php b/app/Models/BlogPost.php index 46a6f7a..fcaeb28 100644 --- a/app/Models/BlogPost.php +++ b/app/Models/BlogPost.php @@ -2,16 +2,21 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\Storage; use Spatie\Translatable\HasTranslations; -use Stephenjude\FilamentBlog\Models\Post as BasePost; -class BlogPost extends BasePost +class BlogPost extends Model { use HasFactory, SoftDeletes, HasTranslations; + protected $table = 'blog_posts'; + protected $translatable = [ 'title', 'excerpt', @@ -21,7 +26,6 @@ class BlogPost extends BasePost ]; protected $fillable = [ - 'blog_author_id', 'blog_category_id', 'slug', 'banner', @@ -29,4 +33,33 @@ class BlogPost extends BasePost 'is_published', 'translations', ]; + + protected $casts = [ + 'published_at' => 'date', + 'is_published' => 'boolean', + ]; + + protected $appends = [ + 'banner_url', + ]; + + public function bannerUrl(): Attribute + { + return Attribute::get(fn () => $this->banner ? asset(Storage::url($this->banner)) : ''); + } + + public function scopePublished(Builder $query) + { + return $query->whereNotNull('published_at')->where('is_published', true); + } + + public function scopeDraft(Builder $query) + { + return $query->whereNull('published_at'); + } + + public function category(): BelongsTo + { + return $this->belongsTo(BlogCategory::class, 'blog_category_id'); + } } \ No newline at end of file diff --git a/app/Models/BlogTag.php b/app/Models/BlogTag.php index 2bb1d28..da2057e 100644 --- a/app/Models/BlogTag.php +++ b/app/Models/BlogTag.php @@ -5,10 +5,10 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use Spatie\Tags\Tag; use Spatie\Translatable\HasTranslations; -use Stephenjude\FilamentBlog\Models\Tag as BaseTag; -class BlogTag extends BaseTag +class BlogTag extends Tag { use HasFactory, SoftDeletes, HasTranslations; diff --git a/app/Models/Package.php b/app/Models/Package.php index 15f3b96..c447d23 100644 --- a/app/Models/Package.php +++ b/app/Models/Package.php @@ -40,7 +40,32 @@ class Package extends Model 'features' => 'array', ]; - // features handled by $casts = ['features' => 'array'] + + protected function features(): Attribute + { + return Attribute::make( + get: function ($value) { + if (is_array($value)) { + return $value; + } + + if (is_string($value)) { + $decoded = json_decode($value, true); + + if (is_string($decoded)) { + $decoded = json_decode($decoded, true); + } + + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + return $decoded; + } + } + + return []; + }, + ); + } + public function eventPackages(): HasMany { @@ -77,4 +102,4 @@ class Package extends Model 'max_events_per_year' => $this->max_events_per_year, ]; } -} \ No newline at end of file +} diff --git a/app/Providers/Filament/SuperAdminPanelProvider.php b/app/Providers/Filament/SuperAdminPanelProvider.php index 021a1af..869ee9b 100644 --- a/app/Providers/Filament/SuperAdminPanelProvider.php +++ b/app/Providers/Filament/SuperAdminPanelProvider.php @@ -26,17 +26,17 @@ use App\Models\BlogCategory; use App\Models\BlogTag; use App\Filament\Widgets\PlatformStatsWidget; use App\Filament\Widgets\TopTenantsByUploads; -use Stephenjude\FilamentBlog\Filament\Resources\CategoryResource; -use Stephenjude\FilamentBlog\Filament\Resources\PostResource; -use Stephenjude\FilamentBlog\BlogPlugin; +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 { - \Illuminate\Support\Facades\Log::info('SuperAdminPanelProvider panel method called'); - + return $panel ->default() ->id('superadmin') @@ -50,9 +50,9 @@ class SuperAdminPanelProvider extends PanelProvider Pages\Dashboard::class, ]) ->login(\App\Filament\SuperAdmin\Pages\Auth\Login::class) - ->plugin( + /*->plugin( BlogPlugin::make() - ) + )*/ ->profile() ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ @@ -76,8 +76,9 @@ class SuperAdminPanelProvider extends PanelProvider Authenticate::class, ]) ->resources([ - // Temporär deaktiviert: TenantResource - verdächtigt für frühen Fehler - // TenantResource::class, + + PostResource::class, + CategoryResource::class, LegalPageResource::class, ]) ->authGuard('web') diff --git a/composer.json b/composer.json index 796b2a1..939aae6 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ "paypal/paypal-server-sdk": "^1.1", "simplesoftwareio/simple-qrcode": "^4.2", "spatie/laravel-translatable": "^6.11", - "stephenjude/filament-blog": "*", "stripe/stripe-php": "*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index d8afc33..464e9b9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c7c9c8d3a298a4a78d257a1674cd117d", + "content-hash": "d33558ef249a7265942579422b1fbeec", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -780,83 +780,6 @@ ], "time": "2024-07-16T11:13:48+00:00" }, - { - "name": "composer/semver", - "version": "3.4.4", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.11", - "symfony/phpunit-bridge": "^3 || ^7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.4" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2025-08-20T19:15:30+00:00" - }, { "name": "danharrin/date-format-converter", "version": "v0.3.1", @@ -1679,80 +1602,6 @@ }, "time": "2025-09-04T14:12:50+00:00" }, - { - "name": "filament/spatie-laravel-media-library-plugin", - "version": "v4.0.7", - "source": { - "type": "git", - "url": "https://github.com/filamentphp/spatie-laravel-media-library-plugin.git", - "reference": "fc15d6a60a3ff564fbdaf6588c55dab6c931d67a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/filamentphp/spatie-laravel-media-library-plugin/zipball/fc15d6a60a3ff564fbdaf6588c55dab6c931d67a", - "reference": "fc15d6a60a3ff564fbdaf6588c55dab6c931d67a", - "shasum": "" - }, - "require": { - "filament/support": "self.version", - "php": "^8.2", - "spatie/laravel-medialibrary": "^11.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Filament\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Filament support for `spatie/laravel-medialibrary`.", - "homepage": "https://github.com/filamentphp/filament", - "support": { - "issues": "https://github.com/filamentphp/filament/issues", - "source": "https://github.com/filamentphp/filament" - }, - "time": "2025-09-01T09:39:21+00:00" - }, - { - "name": "filament/spatie-laravel-tags-plugin", - "version": "v3.3.30", - "source": { - "type": "git", - "url": "https://github.com/filamentphp/spatie-laravel-tags-plugin.git", - "reference": "7763c2bab92c619cdd9294c69004f89e136c0afc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/filamentphp/spatie-laravel-tags-plugin/zipball/7763c2bab92c619cdd9294c69004f89e136c0afc", - "reference": "7763c2bab92c619cdd9294c69004f89e136c0afc", - "shasum": "" - }, - "require": { - "illuminate/database": "^10.45|^11.0|^12.0", - "php": "^8.1", - "spatie/laravel-tags": "^4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Filament\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Filament support for `spatie/laravel-tags`.", - "homepage": "https://github.com/filamentphp/filament", - "support": { - "issues": "https://github.com/filamentphp/filament/issues", - "source": "https://github.com/filamentphp/filament" - }, - "time": "2025-05-19T07:27:08+00:00" - }, { "name": "filament/support", "version": "v4.0.7", @@ -3971,84 +3820,6 @@ ], "time": "2025-07-17T05:12:15+00:00" }, - { - "name": "maennchen/zipstream-php", - "version": "3.2.0", - "source": { - "type": "git", - "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416", - "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "ext-zlib": "*", - "php-64bit": "^8.3" - }, - "require-dev": { - "brianium/paratest": "^7.7", - "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.16", - "guzzlehttp/guzzle": "^7.5", - "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^12.0", - "vimeo/psalm": "^6.0" - }, - "suggest": { - "guzzlehttp/psr7": "^2.4", - "psr/http-message": "^2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "ZipStream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paul Duncan", - "email": "pabs@pablotron.org" - }, - { - "name": "Jonatan Männchen", - "email": "jonatan@maennchen.ch" - }, - { - "name": "Jesse Donat", - "email": "donatj@gmail.com" - }, - { - "name": "András Kolesár", - "email": "kolesar@kolesar.hu" - } - ], - "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", - "keywords": [ - "stream", - "zip" - ], - "support": { - "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0" - }, - "funding": [ - { - "url": "https://github.com/maennchen", - "type": "github" - } - ], - "time": "2025-07-17T11:15:13+00:00" - }, { "name": "masterminds/html5", "version": "2.10.0", @@ -6058,208 +5829,6 @@ }, "time": "2021-02-08T20:43:55+00:00" }, - { - "name": "spatie/eloquent-sortable", - "version": "4.5.2", - "source": { - "type": "git", - "url": "https://github.com/spatie/eloquent-sortable.git", - "reference": "c1c4f3a66cd41eb7458783c8a4c8e5d7924a9f20" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/eloquent-sortable/zipball/c1c4f3a66cd41eb7458783c8a4c8e5d7924a9f20", - "reference": "c1c4f3a66cd41eb7458783c8a4c8e5d7924a9f20", - "shasum": "" - }, - "require": { - "illuminate/database": "^9.31|^10.0|^11.0|^12.0", - "illuminate/support": "^9.31|^10.0|^11.0|^12.0", - "nesbot/carbon": "^2.63|^3.0", - "php": "^8.1", - "spatie/laravel-package-tools": "^1.9" - }, - "require-dev": { - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", - "phpunit/phpunit": "^9.5|^10.0|^11.5.3" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Spatie\\EloquentSortable\\EloquentSortableServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Spatie\\EloquentSortable\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be" - } - ], - "description": "Sortable behaviour for eloquent models", - "homepage": "https://github.com/spatie/eloquent-sortable", - "keywords": [ - "behaviour", - "eloquent", - "laravel", - "model", - "sort", - "sortable" - ], - "support": { - "issues": "https://github.com/spatie/eloquent-sortable/issues", - "source": "https://github.com/spatie/eloquent-sortable/tree/4.5.2" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-08-25T11:46:57+00:00" - }, - { - "name": "spatie/image", - "version": "3.8.6", - "source": { - "type": "git", - "url": "https://github.com/spatie/image.git", - "reference": "0872c5968a7f044fe1e960c26433e54ceaede696" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/image/zipball/0872c5968a7f044fe1e960c26433e54ceaede696", - "reference": "0872c5968a7f044fe1e960c26433e54ceaede696", - "shasum": "" - }, - "require": { - "ext-exif": "*", - "ext-json": "*", - "ext-mbstring": "*", - "php": "^8.2", - "spatie/image-optimizer": "^1.7.5", - "spatie/temporary-directory": "^2.2", - "symfony/process": "^6.4|^7.0" - }, - "require-dev": { - "ext-gd": "*", - "ext-imagick": "*", - "laravel/sail": "^1.34", - "pestphp/pest": "^2.28", - "phpstan/phpstan": "^1.10.50", - "spatie/pest-plugin-snapshots": "^2.1", - "spatie/pixelmatch-php": "^1.0", - "spatie/ray": "^1.40.1", - "symfony/var-dumper": "^6.4|7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\Image\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Manipulate images with an expressive API", - "homepage": "https://github.com/spatie/image", - "keywords": [ - "image", - "spatie" - ], - "support": { - "source": "https://github.com/spatie/image/tree/3.8.6" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-09-25T12:06:17+00:00" - }, - { - "name": "spatie/image-optimizer", - "version": "1.8.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/image-optimizer.git", - "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/4fd22035e81d98fffced65a8c20d9ec4daa9671c", - "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "php": "^7.3|^8.0", - "psr/log": "^1.0 | ^2.0 | ^3.0", - "symfony/process": "^4.2|^5.0|^6.0|^7.0" - }, - "require-dev": { - "pestphp/pest": "^1.21", - "phpunit/phpunit": "^8.5.21|^9.4.4", - "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\ImageOptimizer\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Easily optimize images using PHP", - "homepage": "https://github.com/spatie/image-optimizer", - "keywords": [ - "image-optimizer", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/image-optimizer/issues", - "source": "https://github.com/spatie/image-optimizer/tree/1.8.0" - }, - "time": "2024-11-04T08:24:54+00:00" - }, { "name": "spatie/invade", "version": "2.1.0", @@ -6319,116 +5888,6 @@ ], "time": "2024-05-17T09:06:10+00:00" }, - { - "name": "spatie/laravel-medialibrary", - "version": "11.15.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/laravel-medialibrary.git", - "reference": "9d1e9731d36817d1649bc584b2c40c0c9d4bcfac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/9d1e9731d36817d1649bc584b2c40c0c9d4bcfac", - "reference": "9d1e9731d36817d1649bc584b2c40c0c9d4bcfac", - "shasum": "" - }, - "require": { - "composer/semver": "^3.4", - "ext-exif": "*", - "ext-fileinfo": "*", - "ext-json": "*", - "illuminate/bus": "^10.2|^11.0|^12.0", - "illuminate/conditionable": "^10.2|^11.0|^12.0", - "illuminate/console": "^10.2|^11.0|^12.0", - "illuminate/database": "^10.2|^11.0|^12.0", - "illuminate/pipeline": "^10.2|^11.0|^12.0", - "illuminate/support": "^10.2|^11.0|^12.0", - "maennchen/zipstream-php": "^3.1", - "php": "^8.2", - "spatie/image": "^3.3.2", - "spatie/laravel-package-tools": "^1.16.1", - "spatie/temporary-directory": "^2.2", - "symfony/console": "^6.4.1|^7.0" - }, - "conflict": { - "php-ffmpeg/php-ffmpeg": "<0.6.1" - }, - "require-dev": { - "aws/aws-sdk-php": "^3.293.10", - "ext-imagick": "*", - "ext-pdo_sqlite": "*", - "ext-zip": "*", - "guzzlehttp/guzzle": "^7.8.1", - "larastan/larastan": "^2.7|^3.0", - "league/flysystem-aws-s3-v3": "^3.22", - "mockery/mockery": "^1.6.7", - "orchestra/testbench": "^7.0|^8.17|^9.0|^10.0", - "pestphp/pest": "^2.28|^3.5", - "phpstan/extension-installer": "^1.3.1", - "spatie/laravel-ray": "^1.33", - "spatie/pdf-to-image": "^2.2|^3.0", - "spatie/pest-expectations": "^1.13", - "spatie/pest-plugin-snapshots": "^2.1" - }, - "suggest": { - "league/flysystem-aws-s3-v3": "Required to use AWS S3 file storage", - "php-ffmpeg/php-ffmpeg": "Required for generating video thumbnails", - "spatie/pdf-to-image": "Required for generating thumbnails of PDFs and SVGs" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Spatie\\MediaLibrary\\MediaLibraryServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Spatie\\MediaLibrary\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Associate files with Eloquent models", - "homepage": "https://github.com/spatie/laravel-medialibrary", - "keywords": [ - "cms", - "conversion", - "downloads", - "images", - "laravel", - "laravel-medialibrary", - "media", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/laravel-medialibrary/issues", - "source": "https://github.com/spatie/laravel-medialibrary/tree/11.15.0" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-09-19T06:51:45+00:00" - }, { "name": "spatie/laravel-package-tools", "version": "1.92.7", @@ -6490,159 +5949,6 @@ ], "time": "2025-07-17T15:46:43+00:00" }, - { - "name": "spatie/laravel-permission", - "version": "6.21.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/laravel-permission.git", - "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6a118e8855dfffcd90403aab77bbf35a03db51b3", - "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3", - "shasum": "" - }, - "require": { - "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0", - "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0", - "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0", - "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0", - "php": "^8.0" - }, - "require-dev": { - "laravel/passport": "^11.0|^12.0", - "laravel/pint": "^1.0", - "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", - "phpunit/phpunit": "^9.4|^10.1|^11.5" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Spatie\\Permission\\PermissionServiceProvider" - ] - }, - "branch-alias": { - "dev-main": "6.x-dev", - "dev-master": "6.x-dev" - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Spatie\\Permission\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Permission handling for Laravel 8.0 and up", - "homepage": "https://github.com/spatie/laravel-permission", - "keywords": [ - "acl", - "laravel", - "permission", - "permissions", - "rbac", - "roles", - "security", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/laravel-permission/issues", - "source": "https://github.com/spatie/laravel-permission/tree/6.21.0" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-07-23T16:08:05+00:00" - }, - { - "name": "spatie/laravel-tags", - "version": "4.10.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/laravel-tags.git", - "reference": "9fc59a9328e892bbb5b01c948b0d703e22d543ec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-tags/zipball/9fc59a9328e892bbb5b01c948b0d703e22d543ec", - "reference": "9fc59a9328e892bbb5b01c948b0d703e22d543ec", - "shasum": "" - }, - "require": { - "laravel/framework": "^10.0|^11.0|^12.0", - "nesbot/carbon": "^2.63|^3.0", - "php": "^8.1", - "spatie/eloquent-sortable": "^4.0", - "spatie/laravel-package-tools": "^1.4", - "spatie/laravel-translatable": "^6.0" - }, - "require-dev": { - "orchestra/testbench": "^8.0|^9.0|^10.0", - "pestphp/pest": "^1.22|^2.0", - "phpunit/phpunit": "^9.5.2" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Spatie\\Tags\\TagsServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Spatie\\Tags\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Add tags and taggable behaviour to your Laravel app", - "homepage": "https://github.com/spatie/laravel-tags", - "keywords": [ - "laravel-tags", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/laravel-tags/issues", - "source": "https://github.com/spatie/laravel-tags/tree/4.10.0" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-03-08T07:49:06+00:00" - }, { "name": "spatie/laravel-translatable", "version": "6.11.4", @@ -6791,140 +6097,6 @@ ], "time": "2025-02-21T14:16:57+00:00" }, - { - "name": "spatie/temporary-directory", - "version": "2.3.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/temporary-directory.git", - "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b", - "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b", - "shasum": "" - }, - "require": { - "php": "^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\TemporaryDirectory\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alex Vanderbist", - "email": "alex@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Easily create, use and destroy temporary directories", - "homepage": "https://github.com/spatie/temporary-directory", - "keywords": [ - "php", - "spatie", - "temporary-directory" - ], - "support": { - "issues": "https://github.com/spatie/temporary-directory/issues", - "source": "https://github.com/spatie/temporary-directory/tree/2.3.0" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-01-13T13:04:43+00:00" - }, - { - "name": "stephenjude/filament-blog", - "version": "4.2.1", - "source": { - "type": "git", - "url": "https://github.com/stephenjude/filament-blog.git", - "reference": "040414004f876e880889e8d1646de219d85365bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/stephenjude/filament-blog/zipball/040414004f876e880889e8d1646de219d85365bc", - "reference": "040414004f876e880889e8d1646de219d85365bc", - "shasum": "" - }, - "require": { - "filament/filament": "^4.0", - "filament/spatie-laravel-tags-plugin": "^v3.0", - "php": "^8.3", - "spatie/laravel-package-tools": "^1.16.1" - }, - "require-dev": { - "laravel/pint": "^1.13.1", - "nunomaduro/collision": "^7.8.1|^8.0", - "orchestra/testbench": "^9.0|^10.0", - "pestphp/pest": "^2.18.2|^3.7", - "pestphp/pest-plugin-arch": "^2.3.3|^3.0", - "pestphp/pest-plugin-laravel": "^2.2|^3.1", - "pestphp/pest-plugin-livewire": "^2.1|^3.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Stephenjude\\FilamentBlog\\BlogServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Stephenjude\\FilamentBlog\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "stephenjude", - "email": "stephenjudesuccess@gmail.com", - "role": "Developer" - } - ], - "description": "Filament Blog Builder", - "homepage": "https://github.com/stephenjude/filament-blog", - "keywords": [ - "blog", - "filament-blog", - "laravel", - "stephenjude" - ], - "support": { - "issues": "https://github.com/stephenjude/filament-blog/issues", - "source": "https://github.com/stephenjude/filament-blog/tree/4.2.1" - }, - "funding": [ - { - "url": "https://github.com/stephenjude", - "type": "github" - } - ], - "time": "2025-09-08T07:49:43+00:00" - }, { "name": "stripe/stripe-php", "version": "v17.6.0", diff --git a/config/filament-blog.php b/config/filament-blog.php index 6f8ddd7..07622e0 100644 --- a/config/filament-blog.php +++ b/config/filament-blog.php @@ -7,9 +7,8 @@ return [ 'panels' => [ 'superadmin' => [ 'resources' => [ - \Stephenjude\FilamentBlog\Filament\Resources\PostResource::class, - \Stephenjude\FilamentBlog\Filament\Resources\CategoryResource::class, - \Stephenjude\FilamentBlog\Filament\Resources\TagResource::class, + \App\Filament\Blog\Resources\PostResource::class, + \App\Filament\Blog\Resources\CategoryResource::class, ], ], ], @@ -19,7 +18,7 @@ return [ * \Filament\Forms\Components\RichEditor::class * \Filament\Forms\Components\MarkdownEditor::class */ - 'editor' => \Filament\Forms\Components\RichEditor::class, + 'editor' => \Filament\Forms\Components\MarkdownEditor::class, /** * Configs for Posts banner file that give you option to change diff --git a/database/migrations/2025_09_27_110200_make_tenant_id_required_in_package_purchases_table.php b/database/migrations/2025_09_27_110200_make_tenant_id_required_in_package_purchases_table.php deleted file mode 100644 index f9c9942..0000000 --- a/database/migrations/2025_09_27_110200_make_tenant_id_required_in_package_purchases_table.php +++ /dev/null @@ -1,30 +0,0 @@ -dropForeign(['tenant_id']); - $table->foreignId('tenant_id')->constrained()->change(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('package_purchases', function (Blueprint $table) { - $table->dropForeign(['tenant_id']); - $table->foreignId('tenant_id')->nullable()->constrained()->change(); - }); - } -}; \ No newline at end of file diff --git a/database/migrations/2025_09_29_155553_add_multilanguage_columns_to_blog_categories.php b/database/migrations/2025_09_29_155553_add_multilanguage_columns_to_blog_categories.php new file mode 100644 index 0000000..56a60f5 --- /dev/null +++ b/database/migrations/2025_09_29_155553_add_multilanguage_columns_to_blog_categories.php @@ -0,0 +1,23 @@ +string('name')->nullable()->after('id'); + $table->text('description')->nullable()->after('name'); + }); + } + + public function down(): void + { + Schema::table('blog_categories', function (Blueprint $table) { + $table->dropColumn(['name', 'description']); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_09_27_110300_add_unique_index_to_username_in_users_table.php b/database/migrations/2025_09_29_162547_drop_old_name_description_from_blog_categories.php similarity index 54% rename from database/migrations/2025_09_27_110300_add_unique_index_to_username_in_users_table.php rename to database/migrations/2025_09_29_162547_drop_old_name_description_from_blog_categories.php index c021ed0..81dd683 100644 --- a/database/migrations/2025_09_27_110300_add_unique_index_to_username_in_users_table.php +++ b/database/migrations/2025_09_29_162547_drop_old_name_description_from_blog_categories.php @@ -11,8 +11,8 @@ return new class extends Migration */ public function up(): void { - Schema::table('users', function (Blueprint $table) { - $table->unique('username'); + Schema::table('blog_categories', function (Blueprint $table) { + $table->dropColumn(['name', 'description']); }); } @@ -21,8 +21,9 @@ return new class extends Migration */ public function down(): void { - Schema::table('users', function (Blueprint $table) { - $table->dropUnique(['username']); + Schema::table('blog_categories', function (Blueprint $table) { + $table->string('name')->after('id'); + $table->text('description')->nullable()->after('name'); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_09_29_164248_add_name_and_description_json_to_blog_categories.php b/database/migrations/2025_09_29_164248_add_name_and_description_json_to_blog_categories.php new file mode 100644 index 0000000..298bfa5 --- /dev/null +++ b/database/migrations/2025_09_29_164248_add_name_and_description_json_to_blog_categories.php @@ -0,0 +1,27 @@ +json('name')->nullable()->after('id'); + } + if (!Schema::hasColumn('blog_categories', 'description')) { + $table->json('description')->nullable()->after('name'); + } + }); + } + + public function down(): void + { + Schema::table('blog_categories', function (Blueprint $table) { + $table->dropColumn(['name', 'description']); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_09_29_204232_alter_blog_categories_columns_to_json_and_drop_translations.php b/database/migrations/2025_09_29_204232_alter_blog_categories_columns_to_json_and_drop_translations.php new file mode 100644 index 0000000..4b05525 --- /dev/null +++ b/database/migrations/2025_09_29_204232_alter_blog_categories_columns_to_json_and_drop_translations.php @@ -0,0 +1,32 @@ +json('name')->nullable()->change(); + $table->json('description')->nullable()->change(); + $table->dropColumn('translations'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('blog_categories', function (Blueprint $table) { + $table->string('name')->nullable()->change(); + $table->text('description')->nullable()->change(); + $table->json('translations')->nullable(); + }); + } +}; diff --git a/resources/lang/de/legal.php b/resources/lang/de/legal.php new file mode 100644 index 0000000..80fb010 --- /dev/null +++ b/resources/lang/de/legal.php @@ -0,0 +1,35 @@ + 'Impressum', + 'datenschutz' => 'Datenschutzerklärung', + 'impressum_title' => 'Impressum - Fotospiel', + 'datenschutz_title' => 'Datenschutzerklärung - Fotospiel', + 'impressum_section' => 'Angaben gemäß § 5 TMG', + 'company' => 'Fotospiel GmbH', + 'address' => 'Musterstraße 1, 12345 Musterstadt', + 'representative' => 'Vertreten durch: Max Mustermann', + 'contact' => 'Kontakt', + 'vat_id' => 'Umsatzsteuer-ID: DE123456789', + 'monetization' => 'Monetarisierung', + 'monetization_desc' => 'Wir monetarisieren über Packages (Einmalkäufe und Abos) via Stripe und PayPal. Preise exkl. MwSt. Support: support@fotospiel.de', + 'register_court' => 'Registergericht: Amtsgericht Musterstadt', + 'commercial_register' => 'Handelsregister: HRB 12345', + 'datenschutz_intro' => 'Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst und halten uns strikt an die Regeln der Datenschutzgesetze.', + 'responsible' => 'Verantwortlich: Fotospiel GmbH, Musterstraße 1, 12345 Musterstadt', + 'data_collection' => 'Datenerfassung: Keine PII-Speicherung, anonyme Sessions für Gäste. E-Mails werden nur für Kontaktzwecke verarbeitet.', + 'payments' => 'Zahlungen und Packages', + 'payments_desc' => 'Wir verarbeiten Zahlungen für Packages über Stripe und PayPal. Karteninformationen werden nicht gespeichert – alle Daten werden verschlüsselt übertragen.', + 'data_retention' => 'Package-Daten (Limits, Features) sind anonymisiert und werden nur für die Funktionalität benötigt. Consent für Zahlungen und E-Mails wird bei Kauf eingeholt. Daten werden nach 10 Jahren gelöscht.', + 'rights' => 'Ihre Rechte: Auskunft, Löschung, Widerspruch.', + 'cookies' => 'Cookies: Nur funktionale Cookies für die PWA.', + 'personal_data' => 'Persönliche Datenverarbeitung', + 'personal_data_desc' => 'Bei der Registrierung und Nutzung des Systems werden folgende persönliche Daten verarbeitet: Vor- und Nachname, Adresse, Telefonnummer, E-Mail-Adresse, Username. Diese Daten werden zur Erfüllung des Vertrags (Package-Kauf, Tenant-Management) und für die Authentifizierung verwendet. Die Verarbeitung erfolgt gemäß Art. 6 Abs. 1 lit. b DSGVO.', + 'account_deletion' => 'Account-Löschung', + 'account_deletion_desc' => 'Sie haben das Recht, Ihre persönlichen Daten jederzeit löschen zu lassen (Recht auf Löschung, Art. 17 DSGVO). Kontaktieren Sie uns unter [E-Mail] zur Löschung Ihres Accounts. Alle zugehörigen Daten (Events, Photos, Purchases) werden gelöscht, soweit keine gesetzlichen Aufbewahrungspflichten bestehen.', + 'data_security' => 'Datensicherheit', + 'data_security_desc' => 'Wir verwenden HTTPS, verschlüsselte Speicherung (Passwörter hashed) und regelmäßige Backups. Zugriff auf Daten ist rollebasierend beschränkt (Tenant vs SuperAdmin).', + 'and' => 'und', + 'stripe_privacy' => 'Stripe Datenschutz', + 'paypal_privacy' => 'PayPal Datenschutz', +]; \ No newline at end of file diff --git a/resources/lang/de/marketing.php b/resources/lang/de/marketing.php index 020dc1c..c54e73a 100644 --- a/resources/lang/de/marketing.php +++ b/resources/lang/de/marketing.php @@ -20,6 +20,8 @@ return [ 'max_events_year' => 'Events/Jahr', 'buy_now' => 'Jetzt kaufen', 'subscribe_now' => 'Jetzt abonnieren', + 'register_buy' => 'Registrieren und kaufen', + 'register_subscribe' => 'Registrieren und abonnieren', 'faq_title' => 'Häufige Fragen zu Packages', 'faq_q1' => 'Was ist ein Package?', 'faq_a1' => 'Ein Package definiert Limits und Features für Ihr Event, z.B. Anzahl Fotos und Galerie-Dauer.', @@ -36,5 +38,112 @@ return [ 'feature_watermark' => 'Wasserzeichen', 'feature_branding' => 'Branding', 'feature_support' => 'Support', + 'feature_basic_uploads' => 'Grundlegende Uploads', + 'feature_unlimited_sharing' => 'Unbegrenztes Teilen', + 'feature_no_watermark' => 'Kein Wasserzeichen', + 'feature_custom_tasks' => 'Benutzerdefinierte Tasks', + 'feature_advanced_analytics' => 'Erweiterte Analytics', + 'feature_priority_support' => 'Priorisierter Support', + 'feature_limited_sharing' => 'Begrenztes Teilen', + 'feature_no_branding' => 'Kein Branding', + 'feature_0' => 'Basis-Feature', + 'feature_reseller_dashboard' => 'Reseller-Dashboard', + 'feature_custom_branding' => 'Benutzerdefiniertes Branding', + 'feature_advanced_reporting' => 'Erweiterte Berichterstattung', + ], + 'nav' => [ + 'home' => 'Startseite', + 'how_it_works' => 'So funktioniert\'s', + 'features' => 'Features', + 'occasions' => 'Anlässe', + 'occasions_types' => [ + 'weddings' => 'Hochzeiten', + 'birthdays' => 'Geburtstage', + 'corporate' => 'Firmenevents', + 'family' => 'Familienfeiern', + ], + 'blog' => 'Blog', + 'packages' => 'Packages', + 'contact' => 'Kontakt', + 'discover_packages' => 'Packages entdecken', + ], + 'footer' => [ + 'company' => 'Fotospiel GmbH', + 'rights_reserved' => 'Alle Rechte vorbehalten', + ], + 'blog' => [ + 'title' => 'Fotospiel - Blog', + 'hero_title' => 'Fotospiel Blog', + 'hero_description' => 'Tipps, News und Anleitungen zu perfekten Event-Fotos mit QR-Codes, PWA und mehr. Bleib informiert!', + 'hero_cta' => 'Mehr über Fotospiel', + 'posts_title' => 'Aktuelle Blog-Posts', + 'by' => 'Von', + 'team' => 'Fotospiel Team', + 'published_at' => 'Veröffentlicht am', + 'read_more' => 'Lesen', + 'back' => 'Zurück zum Blog', + 'empty' => 'Noch keine Posts verfügbar. Bleib dran!', + ], + 'occasions' => [ + 'title' => 'Fotospiel für :type', + 'hero_title' => 'Fotospiel für :type', + 'hero_description' => 'Sammle unvergessliche Fotos von deinen Gästen mit QR-Codes. Perfekt für :type – einfach, mobil und datenschutzkonform.', + 'cta' => 'Package wählen', + 'weddings' => [ + 'title' => 'Hochzeiten mit Fotospiel', + 'description' => 'Erfange romantische Momente: Gäste teilen Fotos via QR, wähle Emotions wie \'Romantisch\' oder \'Fröhlich\'. Besser als traditionelle Fotoboxen.', + 'benefits_title' => 'Vorteile für Hochzeiten', + 'benefit1' => 'QR-Code für Gäste: Einfaches Teilen ohne App-Download.', + 'benefit2' => 'Emotion-Filter: Kategorisiere Fotos (z.B. \'Tanz\', \'Kuss\').', + 'benefit3' => 'Private Galerie: Nur freigegebene Fotos sichtbar.', + 'benefit4' => 'Download: Hochauflösend für Album.', + 'image_alt' => 'Hochzeitsfotos', + ], + 'birthdays' => [ + 'title' => 'Geburtstage feiern', + 'description' => 'Lass Freunde und Familie spontane Fotos teilen. QR auf der Torte – Spaß garantiert!', + 'benefits_title' => 'Vorteile für Geburtstage', + 'benefit1' => 'Schnelle Uploads: Kamera oder Galerie.', + 'benefit2' => 'Likes & Shares: Beliebte Momente hervorheben.', + 'benefit3' => 'Offline-fähig: PWA funktioniert ohne Internet.', + 'benefit4' => 'Anonym: Keine Registrierung nötig.', + 'image_alt' => 'Geburtstagsfotos', + ], + 'corporate' => [ + 'title' => 'Firmenevents professionell', + 'description' => 'Netzwerken und Team-Building: Sammle Fotos zentral, teile Highlights intern.', + 'benefits_title' => 'Vorteile für Firmenevents', + 'benefit1' => 'QR an Ständen: Gäste fotografieren sich selbst.', + 'benefit2' => 'Kategorien: \'Team\', \'Netzwerk\', \'Präsentation\'.', + 'benefit3' => 'Export: Für Social Media oder Intranet.', + 'benefit4' => 'GDPR-sicher: Keine PII gespeichert.', + 'image_alt' => 'Firmenevent-Fotos', + ], + 'family' => [ + 'title' => 'Familienfeiern', + 'description' => 'Von Taufen bis Jubiläen: Sammle Erinnerungen von allen Verwandten.', + 'benefits_title' => 'Vorteile für Familienfeiern', + 'benefit1' => 'Einfach für alle Altersgruppen: Große Buchstaben, Touch-freundlich.', + 'benefit2' => 'Emotionen: \'Familie\', \'Glück\', \'Zusammenhalt\'.', + 'benefit3' => 'Teilen: Per Link oder QR für Nachfeier.', + 'benefit4' => 'Unbegrenzt: Im Premium-Tarif.', + 'image_alt' => 'Familienfotos', + ], + 'not_found' => 'Anlass nicht gefunden.', + ], + 'success' => [ + 'title' => 'Erfolgreich', + 'verify_email' => 'E-Mail verifizieren', + 'check_email' => 'Überprüfen Sie Ihre E-Mail auf den Verifizierungslink.', + 'redirecting' => 'Weiterleitung zum Admin-Bereich...', + 'complete_purchase' => 'Kauf abschließen', + 'login_to_continue' => 'Melden Sie sich an, um fortzufahren.', + 'loading' => 'Laden...', + ], + 'register' => [ + 'free' => 'Kostenlos', + ], + 'currency' => [ + 'euro' => '€', ], ]; \ No newline at end of file diff --git a/resources/lang/en/legal.php b/resources/lang/en/legal.php new file mode 100644 index 0000000..d62fa0b --- /dev/null +++ b/resources/lang/en/legal.php @@ -0,0 +1,32 @@ + 'Imprint', + 'datenschutz' => 'Privacy Policy', + 'impressum_title' => 'Imprint - Fotospiel', + 'datenschutz_title' => 'Privacy Policy - Fotospiel', + 'impressum_section' => 'Information pursuant to § 5 TMG', + 'company' => 'Fotospiel GmbH', + 'address' => 'Musterstraße 1, 12345 Musterstadt', + 'representative' => 'Represented by: Max Mustermann', + 'contact' => 'Contact', + 'vat_id' => 'VAT ID: DE123456789', + 'monetization' => 'Monetization', + 'monetization_desc' => 'We monetize through Packages (one-time purchases and subscriptions) via Stripe and PayPal. Prices excl. VAT. Support: support@fotospiel.de', + 'register_court' => 'Register Court: District Court Musterstadt', + 'commercial_register' => 'Commercial Register: HRB 12345', + 'datenschutz_intro' => 'We take the protection of your personal data very seriously and strictly adhere to the rules of data protection laws.', + 'responsible' => 'Responsible: Fotospiel GmbH, Musterstraße 1, 12345 Musterstadt', + 'data_collection' => 'Data collection: No PII storage, anonymous sessions for guests. Emails are only processed for contact purposes.', + 'payments' => 'Payments and Packages', + 'payments_desc' => 'We process payments for Packages via Stripe and PayPal. Card information is not stored – all data is transmitted encrypted. See Stripe Privacy and PayPal Privacy.', + 'data_retention' => 'Package data (limits, features) is anonymized and only required for functionality. Consent for payments and emails is obtained at purchase. Data is deleted after 10 years.', + 'rights' => 'Your rights: Information, deletion, objection. Contact us under Contact.', + 'cookies' => 'Cookies: Only functional cookies for the PWA.', + 'personal_data' => 'Personal Data Processing', + 'personal_data_desc' => 'During registration and use of the system, the following personal data is processed: First and last name, address, phone number, email address, username. This data is used to fulfill the contract (Package purchase, tenant management) and for authentication. Processing is in accordance with Art. 6 Para. 1 lit. b GDPR.', + 'account_deletion' => 'Account Deletion', + 'account_deletion_desc' => 'You have the right to have your personal data deleted at any time (right to erasure, Art. 17 GDPR). Contact us at [Email] to delete your account. All associated data (events, photos, purchases) will be deleted, unless legal retention obligations exist.', + 'data_security' => 'Data Security', + 'data_security_desc' => 'We use HTTPS, encrypted storage (passwords hashed) and regular backups. Access to data is role-based restricted (Tenant vs SuperAdmin).', +]; \ No newline at end of file diff --git a/resources/lang/en/marketing.php b/resources/lang/en/marketing.php new file mode 100644 index 0000000..c7e4e48 --- /dev/null +++ b/resources/lang/en/marketing.php @@ -0,0 +1,149 @@ + [ + 'title' => 'Our Packages – Choose Your Event Package', + 'hero_title' => 'Discover our flexible Packages', + 'hero_description' => 'From free entry to premium features: Tailor your event package to your needs. Simple, secure and scalable.', + 'cta_explore' => 'Discover Packages', + 'tab_endcustomer' => 'End Customers', + 'tab_reseller' => 'Resellers & Agencies', + 'section_endcustomer' => 'Packages for End Customers (One-time purchase per Event)', + 'section_reseller' => 'Packages for Resellers (Annual Subscription)', + 'free' => 'Free', + 'one_time' => 'One-time purchase', + 'subscription' => 'Subscription', + 'year' => 'Year', + 'max_photos' => 'Photos', + 'max_guests' => 'Guests', + 'gallery_days' => 'Gallery Days', + 'max_events_year' => 'Events/Year', + 'buy_now' => 'Buy Now', + 'subscribe_now' => 'Subscribe Now', + 'register_buy' => 'Register and Buy', + 'register_subscribe' => 'Register and Subscribe', + 'faq_title' => 'Frequently Asked Questions about Packages', + 'faq_q1' => 'What is a Package?', + 'faq_a1' => 'A Package defines limits and features for your event, e.g. number of photos and gallery duration.', + 'faq_q2' => 'Can I upgrade?', + 'faq_a2' => 'Yes, choose a higher package when creating the event or upgrade later.', + 'faq_q3' => 'What happens when it expires?', + 'faq_a3' => 'The gallery remains readable, but uploads are blocked. Simply extend it.', + 'faq_q4' => 'Payment secure?', + 'faq_a4' => 'Yes, via Stripe or PayPal – secure and GDPR-compliant.', + 'final_cta' => 'Ready for your next event?', + 'contact_us' => 'Contact Us', + 'feature_live_slideshow' => 'Live Slideshow', + 'feature_analytics' => 'Analytics', + 'feature_watermark' => 'Watermark', + 'feature_branding' => 'Branding', + 'feature_support' => 'Support', + 'feature_basic_uploads' => 'Basic Uploads', + 'feature_unlimited_sharing' => 'Unlimited Sharing', + 'feature_no_watermark' => 'No Watermark', + 'feature_custom_tasks' => 'Custom Tasks', + 'feature_advanced_analytics' => 'Advanced Analytics', + 'feature_priority_support' => 'Priority Support', + 'feature_limited_sharing' => 'Limited Sharing', + 'feature_no_branding' => 'No Branding', + 'feature_0' => 'Basic Feature', + 'feature_reseller_dashboard' => 'Reseller Dashboard', + 'feature_custom_branding' => 'Custom Branding', + 'feature_advanced_reporting' => 'Advanced Reporting', + ], + 'nav' => [ + 'home' => 'Home', + 'how_it_works' => 'How it works', + 'features' => 'Features', + 'occasions' => 'Occasions', + 'occasions_types' => [ + 'weddings' => 'Weddings', + 'birthdays' => 'Birthdays', + 'corporate' => 'Corporate Events', + 'family' => 'Family Celebrations', + ], + 'blog' => 'Blog', + 'packages' => 'Packages', + 'contact' => 'Contact', + 'discover_packages' => 'Discover Packages', + ], + 'footer' => [ + 'company' => 'Fotospiel GmbH', + 'rights_reserved' => 'All rights reserved', + ], + 'blog' => [ + 'title' => 'Fotospiel - Blog', + 'hero_title' => 'Fotospiel Blog', + 'hero_description' => 'Tips, News and Guides for perfect Event Photos with QR-Codes, PWA and more. Stay informed!', + 'hero_cta' => 'More about Fotospiel', + 'posts_title' => 'Current Blog Posts', + 'by' => 'By', + 'team' => 'Fotospiel Team', + 'published_at' => 'Published on', + 'read_more' => 'Read', + 'back' => 'Back to Blog', + 'empty' => 'No posts available yet. Stay tuned!', + ], + 'occasions' => [ + 'title' => 'Fotospiel for :type', + 'hero_title' => 'Fotospiel for :type', + 'hero_description' => 'Collect unforgettable photos from your guests with QR-Codes. Perfect for :type – simple, mobile and privacy-compliant.', + 'cta' => 'Choose Package', + 'weddings' => [ + 'title' => 'Weddings with Fotospiel', + 'description' => 'Capture romantic moments: Guests share photos via QR, choose emotions like \'Romantic\' or \'Joyful\'. Better than traditional photo booths.', + 'benefits_title' => 'Benefits for Weddings', + 'benefit1' => 'QR-Code for Guests: Easy sharing without app download.', + 'benefit2' => 'Emotion Filter: Categorize photos (e.g. \'Dance\', \'Kiss\').', + 'benefit3' => 'Private Gallery: Only approved photos visible.', + 'benefit4' => 'Download: High-resolution for album.', + 'image_alt' => 'Wedding Photos', + ], + 'birthdays' => [ + 'title' => 'Celebrate Birthdays', + 'description' => 'Let friends and family share spontaneous photos. QR on the cake – fun guaranteed!', + 'benefits_title' => 'Benefits for Birthdays', + 'benefit1' => 'Quick Uploads: Camera or Gallery.', + 'benefit2' => 'Likes & Shares: Highlight popular moments.', + 'benefit3' => 'Offline-capable: PWA works without internet.', + 'benefit4' => 'Anonymous: No registration required.', + 'image_alt' => 'Birthday Photos', + ], + 'corporate' => [ + 'title' => 'Corporate Events Professionally', + 'description' => 'Networking and Team-Building: Collect photos centrally, share highlights internally.', + 'benefits_title' => 'Benefits for Corporate Events', + 'benefit1' => 'QR at Booths: Guests photograph themselves.', + 'benefit2' => 'Categories: \'Team\', \'Network\', \'Presentation\'.', + 'benefit3' => 'Export: For Social Media or Intranet.', + 'benefit4' => 'GDPR-secure: No PII stored.', + 'image_alt' => 'Corporate Event Photos', + ], + 'family' => [ + 'title' => 'Family Celebrations', + 'description' => 'From baptisms to anniversaries: Collect memories from all relatives.', + 'benefits_title' => 'Benefits for Family Celebrations', + 'benefit1' => 'Easy for all ages: Large letters, touch-friendly.', + 'benefit2' => 'Emotions: \'Family\', \'Happiness\', \'Unity\'.', + 'benefit3' => 'Share: Via link or QR for after-party.', + 'benefit4' => 'Unlimited: In premium plan.', + 'image_alt' => 'Family Photos', + ], + 'not_found' => 'Occasion not found.', + ], + 'success' => [ + 'title' => 'Success', + 'verify_email' => 'Verify Email', + 'check_email' => 'Check your email for the verification link.', + 'redirecting' => 'Redirecting to admin area...', + 'complete_purchase' => 'Complete Purchase', + 'login_to_continue' => 'Log in to continue.', + 'loading' => 'Loading...', + ], + 'register' => [ + 'free' => 'Free', + ], + 'currency' => [ + 'euro' => '€', + ], +]; \ No newline at end of file diff --git a/resources/views/layouts/marketing.blade.php b/resources/views/layouts/marketing.blade.php index b5a2363..4522152 100644 --- a/resources/views/layouts/marketing.blade.php +++ b/resources/views/layouts/marketing.blade.php @@ -6,7 +6,7 @@ @yield('title', 'Fotospiel - Event-Fotos einfach und sicher mit QR-Codes') - @vite(['resources/css/app.css']) + @vite(['resources/css/app.css', 'resources/js/app.js']) + - -
-
-
- Fotospiel - - - - -
- - - -
-
+ @include('partials.header')
@yield('content')
- - + @include('partials.footer') @stack('scripts') diff --git a/resources/views/legal/datenschutz.blade.php b/resources/views/legal/datenschutz.blade.php index 3f191a4..949baca 100644 --- a/resources/views/legal/datenschutz.blade.php +++ b/resources/views/legal/datenschutz.blade.php @@ -1,28 +1,26 @@ - - - - - - Datenschutzerklärung - Fotospiel - - -

Datenschutzerklärung

-

Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst und halten uns strikt an die Regeln der Datenschutzgesetze.

-

Verantwortlich: Fotospiel GmbH, Musterstraße 1, 12345 Musterstadt

-

Datenerfassung: Keine PII-Speicherung, anonyme Sessions für Gäste. E-Mails werden nur für Kontaktzwecke verarbeitet.

-

Zahlungen und Packages

-

Wir verarbeiten Zahlungen für Packages über Stripe und PayPal. Karteninformationen werden nicht gespeichert – alle Daten werden verschlüsselt übertragen. Siehe Stripe Datenschutz und PayPal Datenschutz.

-

Package-Daten (Limits, Features) sind anonymisiert und werden nur für die Funktionalität benötigt. Consent für Zahlungen und E-Mails wird bei Kauf eingeholt. Daten werden nach 10 Jahren gelöscht.

-

Ihre Rechte: Auskunft, Löschung, Widerspruch. Kontaktieren Sie uns unter Kontakt.

-

Cookies: Nur funktionale Cookies für die PWA.

- -

Persönliche Datenverarbeitung

-

Bei der Registrierung und Nutzung des Systems werden folgende persönliche Daten verarbeitet: Vor- und Nachname, Adresse, Telefonnummer, E-Mail-Adresse, Username. Diese Daten werden zur Erfüllung des Vertrags (Package-Kauf, Tenant-Management) und für die Authentifizierung verwendet. Die Verarbeitung erfolgt gemäß Art. 6 Abs. 1 lit. b DSGVO.

- -

Account-Löschung

-

Sie haben das Recht, Ihre persönlichen Daten jederzeit löschen zu lassen (Recht auf Löschung, Art. 17 DSGVO). Kontaktieren Sie uns unter [E-Mail] zur Löschung Ihres Accounts. Alle zugehörigen Daten (Events, Photos, Purchases) werden gelöscht, soweit keine gesetzlichen Aufbewahrungspflichten bestehen.

- -

Datensicherheit

-

Wir verwenden HTTPS, verschlüsselte Speicherung (Passwörter hashed) und regelmäßige Backups. Zugriff auf Daten ist rollebasierend beschränkt (Tenant vs SuperAdmin).

- - \ No newline at end of file +@extends('layouts.marketing') + +@section('title', __('legal.datenschutz_title')) + +@section('content') +
+

{{ __('legal.datenschutz') }}

+

{{ __('legal.datenschutz_intro') }}

+

{{ __('legal.responsible') }}

+

{{ __('legal.data_collection') }}

+

{{ __('legal.payments') }}

+

{{ __('legal.payments_desc') }} {{ __('legal.stripe_privacy') }} {{ __('legal.and') }} {{ __('legal.paypal_privacy') }}.

+

{{ __('legal.data_retention') }}

+

{{ __('legal.rights') }} {{ __('legal.contact') }}.

+

{{ __('legal.cookies') }}

+ +

{{ __('legal.personal_data') }}

+

{{ __('legal.personal_data_desc') }}

+ +

{{ __('legal.account_deletion') }}

+

{{ __('legal.account_deletion_desc') }}

+ +

{{ __('legal.data_security') }}

+

{{ __('legal.data_security_desc') }}

+
+@endsection \ No newline at end of file diff --git a/resources/views/legal/impressum.blade.php b/resources/views/legal/impressum.blade.php index 6b3098a..87d14db 100644 --- a/resources/views/legal/impressum.blade.php +++ b/resources/views/legal/impressum.blade.php @@ -1,23 +1,21 @@ - - - - - - Impressum - Fotospiel - - - -

Impressum

-

Angaben gemäß § 5 TMG

-

Fotospiel GmbH
- Musterstraße 1
- 12345 Musterstadt
- Vertreten durch: Max Mustermann
- Kontakt: Kontakt

-

Umsatzsteuer-ID: DE123456789

-

Monetarisierung

-

Wir monetarisieren über Packages (Einmalkäufe und Abos) via Stripe und PayPal. Preise exkl. MwSt. Support: support@fotospiel.de

-

Registergericht: Amtsgericht Musterstadt

-

Handelsregister: HRB 12345

- - \ No newline at end of file +@extends('layouts.marketing') + +@section('title', __('legal.impressum_title')) + +@section('content') +
+

{{ __('legal.impressum') }}

+

{{ __('legal.impressum_section') }}

+

+ {{ __('legal.company') }}
+ {{ __('legal.address') }}
+ {{ __('legal.representative') }}
+ {{ __('legal.contact') }}: {{ __('legal.contact') }} +

+

{{ __('legal.vat_id') }}

+

{{ __('legal.monetization') }}

+

{{ __('legal.monetization_desc') }}

+

{{ __('legal.register_court') }}

+

{{ __('legal.commercial_register') }}

+
+@endsection \ No newline at end of file diff --git a/resources/views/marketing/blog-show.blade.php b/resources/views/marketing/blog-show.blade.php index 318f5a8..ffff6bb 100644 --- a/resources/views/marketing/blog-show.blade.php +++ b/resources/views/marketing/blog-show.blade.php @@ -1,48 +1,17 @@ - - - - - - {{ $post->title }} - Fotospiel Blog - - - @vite(['resources/css/app.css']) - - - -
- -
+@extends('layouts.marketing') +@section('title') +{{ $post->getTranslation('title', app()->getLocale()) }} - {{ __('blog.title') }} +@endsection + +@section('content')
-

{{ $post->title }}

-

Von {{ $post->author->name ?? 'Fotospiel Team' }} | {{ $post->published_at->format('d.m.Y') }}

+

{{ $post->getTranslation('title', app()->getLocale()) }}

+

{{ __('blog.by') }} {{ $post->author->name ?? __('blog.team') }} | {{ $post->published_at->format(__('date.format')) }}

@if ($post->featured_image) - {{ $post->title }} + {{ $post->getTranslation('title', app()->getLocale()) }} @endif
@@ -50,27 +19,14 @@
- {!! $post->content !!} + {!! $post->getTranslation('content', app()->getLocale()) !!}
- - - - - \ No newline at end of file +@endsection \ No newline at end of file diff --git a/resources/views/marketing/blog.blade.php b/resources/views/marketing/blog.blade.php index c2d5cbd..068e6e6 100644 --- a/resources/views/marketing/blog.blade.php +++ b/resources/views/marketing/blog.blade.php @@ -1,32 +1,32 @@ @extends('layouts.marketing') -@section('title', 'Fotospiel - Blog') +@section('title', __('marketing.blog.title')) @section('content')
-

Fotospiel Blog

-

Tipps, News und Anleitungen zu perfekten Event-Fotos mit QR-Codes, PWA und mehr. Bleib informiert!

- Mehr über Fotospiel +

{{ __('marketing.blog.hero_title') }}

+

{{ __('marketing.blog.hero_description') }}

+ {{ __('marketing.blog.hero_cta') }}
-

Aktuelle Blog-Posts

+

{{ __('marketing.blog.posts_title') }}

@if ($posts->count() > 0)
@foreach ($posts as $post)
@if ($post->featured_image) - {{ $post->title }} + {{ $post->getTranslation('title', app()->getLocale()) }} @endif -

{{ $post->title }}

-

{{ Str::limit($post->excerpt, 150) }}

-

Veröffentlicht am {{ $post->published_at->format('d.m.Y') }}

- Lesen +

{{ $post->getTranslation('title', app()->getLocale()) }}

+

{{ Str::limit($post->getTranslation('excerpt', app()->getLocale()), 150) }}

+

{{ __('marketing.blog.published_at') }} {{ $post->published_at->format(__('date.format')) }}

+ {{ __('marketing.blog.read_more') }}
@endforeach
@@ -36,7 +36,7 @@
@endif @else -

Noch keine Posts verfügbar. Bleib dran!

+

{{ __('marketing.blog.empty') }}

@endif
diff --git a/resources/views/marketing/occasions.blade.php b/resources/views/marketing/occasions.blade.php index e83ed68..c2d6c6e 100644 --- a/resources/views/marketing/occasions.blade.php +++ b/resources/views/marketing/occasions.blade.php @@ -1,47 +1,22 @@ - - - - - - Fotospiel - {{ ucfirst($type) }} Occasion - - - @vite(['resources/css/app.css']) - - - -
- -
+@extends('layouts.marketing') +@section('title', __('marketing.occasions.title', ['type' => ucfirst($type)])) + +@section('content') +@php + Log::info('Occasions View Debug', [ + 'type' => $type ?? 'null', + 'keyType' => $type ? str_replace('-', '', $type) : 'null', + 'locale' => app()->getLocale() + ]); + $keyType = str_replace('-', '', $type ?? ''); +@endphp
-

Fotospiel für {{ ucfirst($type) }}

-

Sammle unvergessliche Fotos von deinen Gästen mit QR-Codes. Perfekt für {{ ucfirst($type) }} – einfach, mobil und datenschutzkonform.

- Package wählen +

{{ __('marketing.occasions.hero_title', ['type' => ucfirst($type)]) }}

+

{{ __('marketing.occasions.hero_description', ['type' => ucfirst($type)]) }}

+ {{ __('marketing.occasions.cta') }}
@@ -49,89 +24,76 @@
@if($type === 'weddings') -

Hochzeiten mit Fotospiel

-

Erfange romantische Momente: Gäste teilen Fotos via QR, wähle Emotions wie 'Romantisch' oder 'Fröhlich'. Besser als traditionelle Fotoboxen.

+

{{ __('marketing.occasions.weddings.title') }}

+

{{ __('marketing.occasions.weddings.description') }}

- Hochzeitsfotos + {{ __('marketing.occasions.weddings.image_alt') }}
-

Vorteile für Hochzeiten

+

{{ __('marketing.occasions.weddings.benefits_title') }}

    -
  • • QR-Code für Gäste: Einfaches Teilen ohne App-Download.
  • -
  • • Emotion-Filter: Kategorisiere Fotos (z.B. 'Tanz', 'Kuss').
  • -
  • • Private Galerie: Nur freigegebene Fotos sichtbar.
  • -
  • • Download: Hochauflösend für Album.
  • +
  • • {{ __('marketing.occasions.weddings.benefit1') }}
  • +
  • • {{ __('marketing.occasions.weddings.benefit2') }}
  • +
  • • {{ __('marketing.occasions.weddings.benefit3') }}
  • +
  • • {{ __('marketing.occasions.weddings.benefit4') }}
@elseif($type === 'birthdays') -

Geburtstage feiern

-

Lass Freunde und Familie spontane Fotos teilen. QR auf der Torte – Spaß garantiert!

+

{{ __('marketing.occasions.birthdays.title') }}

+

{{ __('marketing.occasions.birthdays.description') }}

- Geburtstagsfotos + {{ __('marketing.occasions.birthdays.image_alt') }}
-

Vorteile für Geburtstage

+

{{ __('marketing.occasions.birthdays.benefits_title') }}

    -
  • • Schnelle Uploads: Kamera oder Galerie.
  • -
  • • Likes & Shares: Beliebte Momente hervorheben.
  • -
  • • Offline-fähig: PWA funktioniert ohne Internet.
  • -
  • • Anonym: Keine Registrierung nötig.
  • +
  • • {{ __('marketing.occasions.birthdays.benefit1') }}
  • +
  • • {{ __('marketing.occasions.birthdays.benefit2') }}
  • +
  • • {{ __('marketing.occasions.birthdays.benefit3') }}
  • +
  • • {{ __('marketing.occasions.birthdays.benefit4') }}
@elseif($type === 'corporate-events') -

Firmenevents professionell

-

Netzwerken und Team-Building: Sammle Fotos zentral, teile Highlights intern.

+

{{ __('marketing.occasions.corporate.title') }}

+

{{ __('marketing.occasions.corporate.description') }}

- Firmenevent-Fotos + {{ __('marketing.occasions.corporate.image_alt') }}
-

Vorteile für Firmenevents

+

{{ __('marketing.occasions.corporate.benefits_title') }}

    -
  • • QR an Ständen: Gäste fotografieren sich selbst.
  • -
  • • Kategorien: 'Team', 'Netzwerk', 'Präsentation'.
  • -
  • • Export: Für Social Media oder Intranet.
  • -
  • • GDPR-sicher: Keine PII gespeichert.
  • +
  • • {{ __('marketing.occasions.corporate.benefit1') }}
  • +
  • • {{ __('marketing.occasions.corporate.benefit2') }}
  • +
  • • {{ __('marketing.occasions.corporate.benefit3') }}
  • +
  • • {{ __('marketing.occasions.corporate.benefit4') }}
@elseif($type === 'family-celebrations') -

Familienfeiern

-

Von Taufen bis Jubiläen: Sammle Erinnerungen von allen Verwandten.

+

{{ __('marketing.occasions.family.title') }}

+

{{ __('marketing.occasions.family.description') }}

- Familienfotos + {{ __('marketing.occasions.family.image_alt') }}
-

Vorteile für Familienfeiern

+

{{ __('marketing.occasions.family.benefits_title') }}

    -
  • • Einfach für alle Altersgruppen: Große Buchstaben, Touch-freundlich.
  • -
  • • Emotionen: 'Familie', 'Glück', 'Zusammenhalt'.
  • -
  • • Teilen: Per Link oder QR für Nachfeier.
  • -
  • • Unbegrenzt: Im Premium-Tarif.
  • +
  • • {{ __('marketing.occasions.family.benefit1') }}
  • +
  • • {{ __('marketing.occasions.family.benefit2') }}
  • +
  • • {{ __('marketing.occasions.family.benefit3') }}
  • +
  • • {{ __('marketing.occasions.family.benefit4') }}
@else -

Occasion nicht gefunden. Zurück zur Startseite.

+

{{ __('marketing.occasions.not_found') }} {{ __('nav.home') }}.

@endif
- - - - - \ No newline at end of file +@endsection \ No newline at end of file diff --git a/resources/views/marketing/packages.blade.php b/resources/views/marketing/packages.blade.php index e128729..2985a89 100644 --- a/resources/views/marketing/packages.blade.php +++ b/resources/views/marketing/packages.blade.php @@ -38,7 +38,7 @@ {{ __('marketing.packages.section_endcustomer') }}
- @foreach(\App\Models\Package::where('type', 'endcustomer')->orderBy('price')->get() as $package) + @foreach($endcustomerPackages as $package)

{{ $package->name }}

@@ -103,7 +103,7 @@ {{ __('marketing.packages.section_reseller') }}
- @foreach(\App\Models\Package::where('type', 'reseller')->orderBy('price')->get() as $package) + @foreach($resellerPackages as $package)

{{ $package->name }}

@@ -183,6 +183,8 @@
+@endsection + @push('scripts')
- Loading... + {{ __('marketing.success.loading') }}

{{ __('marketing.success.redirecting') }}

diff --git a/resources/views/partials/footer.blade.php b/resources/views/partials/footer.blade.php new file mode 100644 index 0000000..1fb4263 --- /dev/null +++ b/resources/views/partials/footer.blade.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/resources/views/partials/header.blade.php b/resources/views/partials/header.blade.php new file mode 100644 index 0000000..78ddebd --- /dev/null +++ b/resources/views/partials/header.blade.php @@ -0,0 +1,30 @@ +
+ +
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 0c6fa22..c223db8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,35 +2,56 @@ use Illuminate\Support\Facades\Route; use Inertia\Inertia; +use Illuminate\Support\Facades\Log; // Marketing-Seite mit Locale-Prefix Route::prefix('{locale?}')->where(['locale' => 'de|en'])->middleware('locale')->group(function () { Route::view('/', 'marketing')->name('marketing'); - Route::get('/occasions/{type}', function ($type) { - return view('marketing.occasions', ['type' => $type]); - })->name('occasions.type'); + Route::get('/packages', [\App\Http\Controllers\MarketingController::class, 'packagesIndex'])->name('packages'); Route::get('/register/{package_id?}', [\App\Http\Controllers\Auth\MarketingRegisterController::class, 'create'])->name('register'); Route::post('/register', [\App\Http\Controllers\Auth\MarketingRegisterController::class, 'store']); }); -// Packages Route (outside locale group for direct access) -Route::view('/packages', 'marketing.packages')->name('packages'); + // Fallback for /packages (redirect to default locale) + Route::get('/packages', function () { + return redirect('/de/packages'); + })->name('packages.fallback'); -// Blog Routes (outside locale group for direct access) -Route::get('/blog', [\App\Http\Controllers\MarketingController::class, 'blogIndex'])->name('blog'); -Route::get('/blog/{post}', [\App\Http\Controllers\MarketingController::class, 'blogShow'])->name('blog.show'); +// Fallback for /blog (redirect to default locale) +Route::get('/blog', function () { + return redirect('/de/blog'); +})->name('blog.fallback'); - // Legal Pages +// Blog Routes (inside locale group for i18n support) +Route::prefix('{locale?}')->where(['locale' => 'de|en'])->middleware('locale')->group(function () { + Route::get('/blog', [\App\Http\Controllers\MarketingController::class, 'blogIndex'])->name('blog'); + Route::get('/blog/{post}', [\App\Http\Controllers\MarketingController::class, 'blogShow'])->name('blog.show'); +}); + + // Fallbacks for Legal Pages (redirect to default locale) Route::get('/impressum', function () { - return view('legal.impressum'); - })->name('impressum'); + return redirect('/de/impressum'); + })->name('impressum.fallback'); Route::get('/datenschutz', function () { - return view('legal.datenschutz'); - })->name('datenschutz'); + return redirect('/de/datenschutz'); + })->name('datenschutz.fallback'); Route::get('/kontakt', function () { - return view('legal.kontakt'); - })->name('kontakt'); - Route::post('/kontakt', [\App\Http\Controllers\MarketingController::class, 'contact'])->name('kontakt.submit'); + return redirect('/de/kontakt'); + })->name('kontakt.fallback'); + + // Legal Pages in locale group + Route::prefix('{locale?}')->where(['locale' => 'de|en'])->middleware('locale')->group(function () { + Route::get('/impressum', function () { + return view('legal.impressum'); + })->name('impressum'); + Route::get('/datenschutz', function () { + return view('legal.datenschutz'); + })->name('datenschutz'); + Route::get('/kontakt', function () { + return view('legal.kontakt'); + })->name('kontakt'); + Route::post('/kontakt', [\App\Http\Controllers\MarketingController::class, 'contact'])->name('kontakt.submit'); + }); Route::middleware(['auth', 'verified'])->group(function () { Route::get('dashboard', function () { @@ -95,7 +116,11 @@ Route::middleware('auth')->group(function () { Route::patch('/profile', [\App\Http\Controllers\ProfileController::class, 'update'])->name('profile.update'); }); -// Success view route (no controller needed, direct view) -Route::get('/marketing/success/{package_id?}', function ($packageId = null) { - return view('marketing.success', compact('packageId')); -})->name('marketing.success'); +Route::get('/marketing/success/{package_id?}', [\App\Http\Controllers\MarketingController::class, 'success'])->name('marketing.success'); + +Route::get('{locale}/occasions/{type}', [\App\Http\Controllers\MarketingController::class, 'occasionsType']) + ->where([ + 'locale' => 'de|en', + 'type' => 'weddings|birthdays|corporate-events|family-celebrations' + ]) + ->name('occasions.type'); diff --git a/tests/Feature/PurchaseTest.php b/tests/Feature/PurchaseTest.php index 427bb6c..ae45401 100644 --- a/tests/Feature/PurchaseTest.php +++ b/tests/Feature/PurchaseTest.php @@ -10,6 +10,9 @@ use App\Models\PackagePurchase; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use Illuminate\Support\Facades\Auth; +use Mockery; +use PayPal\PayPalHttp\Client; +use PayPal\Checkout\Orders\Order; class PurchaseTest extends TestCase { @@ -71,4 +74,67 @@ class PurchaseTest extends TestCase $response->assertStatus(302); // Redirect to Stripe $this->assertStringContainsString('checkout.stripe.com', $response->headers->get('Location')); } + + public function test_paypal_checkout_creates_order() + { + $paidPackage = Package::factory()->create(['price' => 10]); + $user = User::factory()->create(['email_verified_at' => now()]); + $tenant = Tenant::factory()->create(['user_id' => $user->id]); + Auth::login($user); + + $mockClient = Mockery::mock(\PayPal\PayPalHttp\Client::class); + $mockOrders = Mockery::mock(); + $mockOrders->shouldReceive('createOrder')->andReturn(new \stdClass()); // Simplified mock + $mockClient->shouldReceive('orders')->andReturn($mockOrders); + $this->app->instance(\PayPal\PayPalHttp\Client::class, $mockClient); + + $response = $this->get(route('buy.packages', $paidPackage->id) . '?provider=paypal'); + + $response->assertStatus(302); + $this->assertNotNull(session('paypal_order_id')); + } + + public function test_paypal_success_captures_and_activates_package() + { + $paidPackage = Package::factory()->create(['price' => 10]); + $user = User::factory()->create(['email_verified_at' => now()]); + $tenant = Tenant::factory()->create(['user_id' => $user->id]); + Auth::login($user); + + $metadata = json_encode([ + 'user_id' => $user->id, + 'tenant_id' => $tenant->id, + 'package_id' => $paidPackage->id, + 'type' => $paidPackage->type, + ]); + + $mockClient = Mockery::mock(\PayPal\PayPalHttp\Client::class); + $mockOrders = Mockery::mock(); + $mockCapture = new \stdClass(); + $mockCapture->status = 'COMPLETED'; + $mockCapture->purchaseUnits = [(object)['custom_id' => $metadata]]; + $mockResponse = new \stdClass(); + $mockResponse->result = $mockCapture; + $mockOrders->shouldReceive('captureOrder')->andReturn($mockResponse); + $mockClient->shouldReceive('orders')->andReturn($mockOrders); + $this->app->instance(\PayPal\PayPalHttp\Client::class, $mockClient); + + session(['paypal_order_id' => 'test-order-id']); + + $response = $this->get(route('marketing.success', $paidPackage->id)); + + $response->assertRedirect('/admin'); + $this->assertDatabaseHas('tenant_packages', [ + 'tenant_id' => $tenant->id, + 'package_id' => $paidPackage->id, + 'active' => true, + ]); + $this->assertDatabaseHas('package_purchases', [ + 'tenant_id' => $tenant->id, + 'package_id' => $paidPackage->id, + 'provider_id' => 'paypal', + ]); + $this->assertNull(session('paypal_order_id')); + $this->assertEquals('active', $tenant->fresh()->subscription_status); + } } \ No newline at end of file