From 43a0b1a79e7c670bb2ca4ed4ecf3ce25ae591b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omer=20=C5=A0abi=C4=87?= Date: Wed, 2 Jul 2025 15:48:13 +0200 Subject: [PATCH 01/11] feat: add posts prototype --- database/factories/PostFactory.php | 25 ++++ .../2025_07_02_122310_create_posts_table.php | 30 +++++ .../Clusters/World/Resources/PostResource.php | 116 ++++++++++++++++++ .../PostResource/Pages/CreatePost.php | 18 +++ .../Resources/PostResource/Pages/EditPost.php | 23 ++++ .../PostResource/Pages/ListPosts.php | 19 +++ src/Models/Post.php | 26 ++++ 7 files changed, 257 insertions(+) create mode 100644 database/factories/PostFactory.php create mode 100644 database/migrations/2025_07_02_122310_create_posts_table.php create mode 100644 src/Filament/Clusters/World/Resources/PostResource.php create mode 100644 src/Filament/Clusters/World/Resources/PostResource/Pages/CreatePost.php create mode 100644 src/Filament/Clusters/World/Resources/PostResource/Pages/EditPost.php create mode 100644 src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php create mode 100644 src/Models/Post.php diff --git a/database/factories/PostFactory.php b/database/factories/PostFactory.php new file mode 100644 index 0000000..c314ed1 --- /dev/null +++ b/database/factories/PostFactory.php @@ -0,0 +1,25 @@ + $this->faker->word(), + 'name' => $this->faker->name(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + + 'country_id' => Country::factory(), + ]; + } +} diff --git a/database/migrations/2025_07_02_122310_create_posts_table.php b/database/migrations/2025_07_02_122310_create_posts_table.php new file mode 100644 index 0000000..ba83e5d --- /dev/null +++ b/database/migrations/2025_07_02_122310_create_posts_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('country_id', 2); + $table->string('code'); + $table->string('name'); + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('country_id') + ->references('id') + ->on('world_countries') + ->cascadeOnDelete() + ->cascadeOnUpdate(); + }); + } + + public function down(): void + { + Schema::dropIfExists('world_posts'); + } +}; diff --git a/src/Filament/Clusters/World/Resources/PostResource.php b/src/Filament/Clusters/World/Resources/PostResource.php new file mode 100644 index 0000000..8ccb376 --- /dev/null +++ b/src/Filament/Clusters/World/Resources/PostResource.php @@ -0,0 +1,116 @@ +schema([ + Select::make('country_id') + ->relationship('country', 'name') + ->searchable() + ->required(), + + TextInput::make('code') + ->required(), + + TextInput::make('name') + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('country.name') + ->searchable() + ->sortable(), + + TextColumn::make('code'), + + TextColumn::make('name') + ->searchable() + ->sortable(), + ]) + ->filters([ + TrashedFilter::make(), + ]) + ->actions([ + EditAction::make(), + DeleteAction::make(), + RestoreAction::make(), + ForceDeleteAction::make(), + ]) + ->bulkActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + RestoreBulkAction::make(), + ForceDeleteBulkAction::make(), + ]), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => PostResource\Pages\ListPosts::route('/'), + 'create' => PostResource\Pages\CreatePost::route('/create'), + 'edit' => PostResource\Pages\EditPost::route('/{record}/edit'), + ]; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } + + public static function getPermissionPrefixes(): array + { + return [ + 'view_any', + 'create', + 'update', + 'restore', + 'restore_any', + 'delete', + 'delete_any', + 'force_delete', + 'force_delete_any', + ]; + } +} diff --git a/src/Filament/Clusters/World/Resources/PostResource/Pages/CreatePost.php b/src/Filament/Clusters/World/Resources/PostResource/Pages/CreatePost.php new file mode 100644 index 0000000..43f81b4 --- /dev/null +++ b/src/Filament/Clusters/World/Resources/PostResource/Pages/CreatePost.php @@ -0,0 +1,18 @@ +belongsTo(Country::class); + } +} From 333eb81fe2f2e4f9056be30b2da56608e8cae579 Mon Sep 17 00:00:00 2001 From: SlimDeluxe <131700+SlimDeluxe@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:48:35 +0000 Subject: [PATCH 02/11] style: fix code style --- database/migrations/2025_07_02_122310_create_posts_table.php | 3 ++- src/Filament/Clusters/World/Resources/PostResource.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/database/migrations/2025_07_02_122310_create_posts_table.php b/database/migrations/2025_07_02_122310_create_posts_table.php index ba83e5d..ab951d6 100644 --- a/database/migrations/2025_07_02_122310_create_posts_table.php +++ b/database/migrations/2025_07_02_122310_create_posts_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ public function up(): void { Schema::create('world_posts', function (Blueprint $table) { diff --git a/src/Filament/Clusters/World/Resources/PostResource.php b/src/Filament/Clusters/World/Resources/PostResource.php index 8ccb376..461add2 100644 --- a/src/Filament/Clusters/World/Resources/PostResource.php +++ b/src/Filament/Clusters/World/Resources/PostResource.php @@ -23,7 +23,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; -class PostResource extends Resource implements HasShieldPermissions +class PostResource extends Resource implements HasShieldPermissions { protected static ?string $model = Post::class; From eae64f637b2c8e8a9a877583c70d33a2d405b00b Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 3 Jul 2025 23:19:43 +0200 Subject: [PATCH 03/11] feat(posts): implement postal code resource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch create/edit forms to use dialogs (same pattern as country) - Show country name with flag emoji in posts table - Enforce unique country+post-code key (DB migration + user-friendly UI error) - Add import script for SI and HR (prompt for country, run in background) - Extract all UI strings to lang files and add translations - Add tests for: • Posts CRUD • Access/permissions (mirror country resource tests) • Filtering by country • Preventing duplicate country+post-code combos --- resources/lang/en/posts.php | 81 +++++ resources/lang/hr/posts.php | 81 +++++ resources/lang/sl/posts.php | 81 +++++ resources/lang/sr/posts.php | 81 +++++ src/Console/Commands/ImportPostsCommand.php | 45 +++ src/EclipseWorldServiceProvider.php | 2 + .../Clusters/World/Resources/PostResource.php | 87 ++++- .../PostResource/Pages/CreatePost.php | 18 - .../Resources/PostResource/Pages/EditPost.php | 23 -- .../PostResource/Pages/ListPosts.php | 37 +- src/Jobs/ImportPosts.php | 132 +++++++ src/Models/Post.php | 19 + src/Policies/PostPolicy.php | 84 +++++ tests/Feature/PostResourceTest.php | 330 ++++++++++++++++++ 14 files changed, 1046 insertions(+), 55 deletions(-) create mode 100644 resources/lang/en/posts.php create mode 100644 resources/lang/hr/posts.php create mode 100644 resources/lang/sl/posts.php create mode 100644 resources/lang/sr/posts.php create mode 100644 src/Console/Commands/ImportPostsCommand.php delete mode 100644 src/Filament/Clusters/World/Resources/PostResource/Pages/CreatePost.php delete mode 100644 src/Filament/Clusters/World/Resources/PostResource/Pages/EditPost.php create mode 100644 src/Jobs/ImportPosts.php create mode 100644 src/Policies/PostPolicy.php create mode 100644 tests/Feature/PostResourceTest.php diff --git a/resources/lang/en/posts.php b/resources/lang/en/posts.php new file mode 100644 index 0000000..5cb8501 --- /dev/null +++ b/resources/lang/en/posts.php @@ -0,0 +1,81 @@ + 'Posts', + 'breadcrumb' => 'Posts', + 'plural' => 'Posts', + + 'table' => [ + 'country' => [ + 'label' => 'Country', + ], + 'flag' => [ + 'label' => 'Flag', + ], + 'code' => [ + 'label' => 'Code', + ], + 'name' => [ + 'label' => 'Name', + ], + ], + + 'actions' => [ + 'create' => [ + 'label' => 'New post', + 'heading' => 'Create Post', + ], + 'edit' => [ + 'label' => 'Edit', + 'heading' => 'Edit Post', + ], + 'delete' => [ + 'label' => 'Delete', + 'heading' => 'Delete Post', + ], + 'restore' => [ + 'label' => 'Restore', + 'heading' => 'Restore Post', + ], + 'force_delete' => [ + 'label' => 'Permanent delete', + 'heading' => 'Permanent Deletion of Post', + 'description' => 'Are you sure you want to delete the post :name? This action cannot be undone.', + ], + ], + + 'form' => [ + 'country_id' => [ + 'label' => 'Country', + ], + 'code' => [ + 'label' => 'Code', + ], + 'name' => [ + 'label' => 'Name', + ], + ], + + 'filter' => [ + 'country' => [ + 'label' => 'Country', + ], + ], + + 'validation' => [ + 'unique_country_code' => 'A post with this code already exists for the selected country.', + ], + + 'import' => [ + 'action_label' => 'Import Posts', + 'modal_heading' => 'Import Posts', + 'country_label' => 'Select Country', + 'country_helper' => 'Choose the country for which you want to import postal data', + 'success_title' => 'Import Posts', + 'success_message' => 'The import posts job has been queued for :country.', + 'countries' => [ + 'SI' => 'Slovenia', + 'HR' => 'Croatia', + ], + ], +]; \ No newline at end of file diff --git a/resources/lang/hr/posts.php b/resources/lang/hr/posts.php new file mode 100644 index 0000000..87fed47 --- /dev/null +++ b/resources/lang/hr/posts.php @@ -0,0 +1,81 @@ + 'Pošte', + 'breadcrumb' => 'Pošte', + 'plural' => 'Pošte', + + 'table' => [ + 'country' => [ + 'label' => 'Država', + ], + 'flag' => [ + 'label' => 'Zastava', + ], + 'code' => [ + 'label' => 'Šifra', + ], + 'name' => [ + 'label' => 'Ime', + ], + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Nova pošta', + 'heading' => 'Stvaranje pošte', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Uređivanje pošte', + ], + 'delete' => [ + 'label' => 'Izbriši', + 'heading' => 'Brisanje pošte', + ], + 'restore' => [ + 'label' => 'Obnovi', + 'heading' => 'Obnova pošte', + ], + 'force_delete' => [ + 'label' => 'Trajno izbriši', + 'heading' => 'Trajno brisanje pošte', + 'description' => 'Jeste li sigurni da želite izbrisati poštu :name? Zapis više neće biti moguće obnoviti.', + ], + ], + + 'form' => [ + 'country_id' => [ + 'label' => 'Država', + ], + 'code' => [ + 'label' => 'Šifra', + ], + 'name' => [ + 'label' => 'Ime', + ], + ], + + 'filter' => [ + 'country' => [ + 'label' => 'Država', + ], + ], + + 'validation' => [ + 'unique_country_code' => 'Pošta s ovom šifrom već postoji za odabranu državu.', + ], + + 'import' => [ + 'action_label' => 'Uvezi pošte', + 'modal_heading' => 'Uvoz pošta', + 'country_label' => 'Odaberi državu', + 'country_helper' => 'Odaberi državu za koju želiš uvoziti poštanske podatke', + 'success_title' => 'Uvoz pošta', + 'success_message' => 'Zadatak za uvoz pošta je dodan u red čekanja za :country.', + 'countries' => [ + 'SI' => 'Slovenija', + 'HR' => 'Hrvatska', + ], + ], +]; \ No newline at end of file diff --git a/resources/lang/sl/posts.php b/resources/lang/sl/posts.php new file mode 100644 index 0000000..b6ca151 --- /dev/null +++ b/resources/lang/sl/posts.php @@ -0,0 +1,81 @@ + 'Pošte', + 'breadcrumb' => 'Pošte', + 'plural' => 'Pošte', + + 'table' => [ + 'country' => [ + 'label' => 'Država', + ], + 'flag' => [ + 'label' => 'Zastava', + ], + 'code' => [ + 'label' => 'Šifra', + ], + 'name' => [ + 'label' => 'Ime', + ], + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Nova pošta', + 'heading' => 'Ustvarjanje pošte', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Urejanje pošte', + ], + 'delete' => [ + 'label' => 'Izbriši', + 'heading' => 'Brisanje pošte', + ], + 'restore' => [ + 'label' => 'Obnovi', + 'heading' => 'Obnovitev pošte', + ], + 'force_delete' => [ + 'label' => 'Trajno izbriši', + 'heading' => 'Trajen izbris pošte', + 'description' => 'Ste prepričani, da želite izbrisati pošto :name? Zapisa ne bo možno več obnoviti.', + ], + ], + + 'form' => [ + 'country_id' => [ + 'label' => 'Država', + ], + 'code' => [ + 'label' => 'Šifra', + ], + 'name' => [ + 'label' => 'Ime', + ], + ], + + 'filter' => [ + 'country' => [ + 'label' => 'Država', + ], + ], + + 'validation' => [ + 'unique_country_code' => 'Pošta s to šifro že obstaja za izbrano državo.', + ], + + 'import' => [ + 'action_label' => 'Uvozi pošte', + 'modal_heading' => 'Uvoz pošt', + 'country_label' => 'Izberi državo', + 'country_helper' => 'Izberi državo, za katero želiš uvoziti poštne podatke', + 'success_title' => 'Uvoz pošt', + 'success_message' => 'Naloga za uvoz pošt je bila dodana v čakalno vrsto za :country.', + 'countries' => [ + 'SI' => 'Slovenija', + 'HR' => 'Hrvaška', + ], + ], +]; \ No newline at end of file diff --git a/resources/lang/sr/posts.php b/resources/lang/sr/posts.php new file mode 100644 index 0000000..2af15d3 --- /dev/null +++ b/resources/lang/sr/posts.php @@ -0,0 +1,81 @@ + 'Pošte', + 'breadcrumb' => 'Pošte', + 'plural' => 'Pošte', + + 'table' => [ + 'country' => [ + 'label' => 'Država', + ], + 'flag' => [ + 'label' => 'Zastava', + ], + 'code' => [ + 'label' => 'Šifra', + ], + 'name' => [ + 'label' => 'Ime', + ], + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Nova pošta', + 'heading' => 'Kreiranje pošte', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Uređivanje pošte', + ], + 'delete' => [ + 'label' => 'Izbriši', + 'heading' => 'Brisanje pošte', + ], + 'restore' => [ + 'label' => 'Obnovi', + 'heading' => 'Obnavljanje pošte', + ], + 'force_delete' => [ + 'label' => 'Trajno izbriši', + 'heading' => 'Trajno brisanje pošte', + 'description' => 'Da li ste sigurni da želite izbrisati poštu :name? Zapis više neće biti moguće obnoviti.', + ], + ], + + 'form' => [ + 'country_id' => [ + 'label' => 'Država', + ], + 'code' => [ + 'label' => 'Šifra', + ], + 'name' => [ + 'label' => 'Ime', + ], + ], + + 'filter' => [ + 'country' => [ + 'label' => 'Država', + ], + ], + + 'validation' => [ + 'unique_country_code' => 'Pošta sa ovom šifrom već postoji za odabranu državu.', + ], + + 'import' => [ + 'action_label' => 'Uvezi pošte', + 'modal_heading' => 'Uvoz pošta', + 'country_label' => 'Odaberi državu', + 'country_helper' => 'Odaberi državu za koju želiš da uvoziš poštanske podatke', + 'success_title' => 'Uvoz pošta', + 'success_message' => 'Zadatak za uvoz pošta je dodat u red čekanja za :country.', + 'countries' => [ + 'SI' => 'Slovenija', + 'HR' => 'Hrvatska', + ], + ], +]; \ No newline at end of file diff --git a/src/Console/Commands/ImportPostsCommand.php b/src/Console/Commands/ImportPostsCommand.php new file mode 100644 index 0000000..a361490 --- /dev/null +++ b/src/Console/Commands/ImportPostsCommand.php @@ -0,0 +1,45 @@ +argument('country')); + + if (!in_array($country, ['SI', 'HR'])) { + $this->error('Invalid country code. Only SI and HR are supported.'); + return self::FAILURE; + } + + $this->info("Dispatching import job for country: {$country}"); + + ImportPosts::dispatch($country); + + $this->info('Import job has been queued successfully!'); + $this->comment('The import will run in the background. Check the logs for progress.'); + + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/src/EclipseWorldServiceProvider.php b/src/EclipseWorldServiceProvider.php index 22da1a2..9c8a16f 100644 --- a/src/EclipseWorldServiceProvider.php +++ b/src/EclipseWorldServiceProvider.php @@ -3,6 +3,7 @@ namespace Eclipse\World; use Eclipse\World\Console\Commands\ImportCommand; +use Eclipse\World\Console\Commands\ImportPostsCommand; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -17,6 +18,7 @@ public function configurePackage(Package $package): void ->hasTranslations() ->hasCommands([ ImportCommand::class, + ImportPostsCommand::class, ]) ->discoversMigrations() ->runsMigrations(); diff --git a/src/Filament/Clusters/World/Resources/PostResource.php b/src/Filament/Clusters/World/Resources/PostResource.php index 461add2..0c4ddee 100644 --- a/src/Filament/Clusters/World/Resources/PostResource.php +++ b/src/Filament/Clusters/World/Resources/PostResource.php @@ -8,7 +8,9 @@ use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; +use Filament\Forms\Get; use Filament\Resources\Resource; +use Filament\Tables\Actions\ActionGroup; use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Actions\DeleteBulkAction; @@ -19,9 +21,11 @@ use Filament\Tables\Actions\RestoreBulkAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\TrashedFilter; +use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; +use Illuminate\Validation\Rule; class PostResource extends Resource implements HasShieldPermissions { @@ -40,13 +44,29 @@ public static function form(Form $form): Form Select::make('country_id') ->relationship('country', 'name') ->searchable() - ->required(), + ->required() + ->label(__('eclipse-world::posts.form.country_id.label')) + ->live(), TextInput::make('code') - ->required(), + ->required() + ->label(__('eclipse-world::posts.form.code.label')) + ->rules(function (Get $get, ?Post $record) { + return [ + 'required', + 'string', + Rule::unique('world_posts', 'code') + ->where('country_id', $get('country_id')) + ->ignore($record?->id), + ]; + }) + ->validationMessages([ + 'unique' => __('eclipse-world::posts.validation.unique_country_code'), + ]), TextInput::make('name') - ->required(), + ->required() + ->label(__('eclipse-world::posts.form.name.label')), ]); } @@ -55,29 +75,57 @@ public static function table(Table $table): Table return $table ->columns([ TextColumn::make('country.name') + ->label(__('eclipse-world::posts.table.country.label')) ->searchable() ->sortable(), - TextColumn::make('code'), + TextColumn::make('country.flag') + ->label(__('eclipse-world::posts.table.flag.label')) + ->width(100), + + TextColumn::make('code') + ->label(__('eclipse-world::posts.table.code.label')), TextColumn::make('name') + ->label(__('eclipse-world::posts.table.name.label')) ->searchable() ->sortable(), ]) ->filters([ TrashedFilter::make(), + SelectFilter::make('country_id') + ->label(__('eclipse-world::posts.filter.country.label')) + ->relationship('country', 'name') + ->searchable() + ->preload(), ]) ->actions([ - EditAction::make(), - DeleteAction::make(), - RestoreAction::make(), - ForceDeleteAction::make(), + EditAction::make() + ->label(__('eclipse-world::posts.actions.edit.label')) + ->modalHeading(__('eclipse-world::posts.actions.edit.heading')), + ActionGroup::make([ + DeleteAction::make() + ->label(__('eclipse-world::posts.actions.delete.label')) + ->modalHeading(__('eclipse-world::posts.actions.delete.heading')), + RestoreAction::make() + ->label(__('eclipse-world::posts.actions.restore.label')) + ->modalHeading(__('eclipse-world::posts.actions.restore.heading')), + ForceDeleteAction::make() + ->label(__('eclipse-world::posts.actions.force_delete.label')) + ->modalHeading(__('eclipse-world::posts.actions.force_delete.heading')) + ->modalDescription(fn(Post $record): string => __('eclipse-world::posts.actions.force_delete.description', [ + 'name' => $record->name, + ])), + ]), ]) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), - RestoreBulkAction::make(), - ForceDeleteBulkAction::make(), + DeleteBulkAction::make() + ->label(__('eclipse-world::posts.actions.delete.label')), + RestoreBulkAction::make() + ->label(__('eclipse-world::posts.actions.restore.label')), + ForceDeleteBulkAction::make() + ->label(__('eclipse-world::posts.actions.force_delete.label')), ]), ]); } @@ -86,8 +134,6 @@ public static function getPages(): array { return [ 'index' => PostResource\Pages\ListPosts::route('/'), - 'create' => PostResource\Pages\CreatePost::route('/create'), - 'edit' => PostResource\Pages\EditPost::route('/{record}/edit'), ]; } @@ -113,4 +159,19 @@ public static function getPermissionPrefixes(): array 'force_delete_any', ]; } + + public static function getNavigationLabel(): string + { + return __('eclipse-world::posts.nav_label'); + } + + public static function getBreadcrumb(): string + { + return __('eclipse-world::posts.breadcrumb'); + } + + public static function getPluralModelLabel(): string + { + return __('eclipse-world::posts.plural'); + } } diff --git a/src/Filament/Clusters/World/Resources/PostResource/Pages/CreatePost.php b/src/Filament/Clusters/World/Resources/PostResource/Pages/CreatePost.php deleted file mode 100644 index 43f81b4..0000000 --- a/src/Filament/Clusters/World/Resources/PostResource/Pages/CreatePost.php +++ /dev/null @@ -1,18 +0,0 @@ -label(__('eclipse-world::posts.actions.create.label')) + ->modalHeading(__('eclipse-world::posts.actions.create.heading')), + Action::make('import_posts') + ->label(__('eclipse-world::posts.import.action_label')) + ->icon('heroicon-o-arrow-down-tray') + ->form([ + Select::make('country_id') + ->label(__('eclipse-world::posts.import.country_label')) + ->helperText(__('eclipse-world::posts.import.country_helper')) + ->options([ + 'SI' => __('eclipse-world::posts.import.countries.SI'), + 'HR' => __('eclipse-world::posts.import.countries.HR'), + ]) + ->required() + ->native(false), + ]) + ->modalHeading(__('eclipse-world::posts.import.modal_heading')) + ->action(function (array $data) { + // Dispatch the job with selected country + ImportPosts::dispatch($data['country_id']); + + // Show notification + Notification::make() + ->title(__('eclipse-world::posts.import.success_title')) + ->body(__('eclipse-world::posts.import.success_message', [ + 'country' => __('eclipse-world::posts.import.countries.' . $data['country_id']) + ])) + ->success() + ->send(); + }) + ->requiresConfirmation(), ]; } } diff --git a/src/Jobs/ImportPosts.php b/src/Jobs/ImportPosts.php new file mode 100644 index 0000000..1571bfe --- /dev/null +++ b/src/Jobs/ImportPosts.php @@ -0,0 +1,132 @@ +countryId = $countryId; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (!in_array($this->countryId, ['SI', 'HR'])) { + throw new Exception("Country {$this->countryId} not supported for import"); + } + + $batchSize = 1000; + $offset = 0; + $processedCodes = []; + + Log::info("Starting postal data import for country: {$this->countryId}"); + + do { + [$totalRecords, $records] = $this->getData($batchSize, $offset); + + foreach ($records as $record) { + [$postalCode, $placeName] = $this->getRecordData($record); + + if (array_key_exists($postalCode, $processedCodes)) { + continue; + } + + $processedCodes[$postalCode] = true; + + $existingPost = Post::where('country_id', $this->countryId) + ->where('code', $postalCode) + ->first(); + + if (empty($existingPost)) { + Post::create([ + 'country_id' => $this->countryId, + 'code' => $postalCode, + 'name' => $placeName, + ]); + } elseif ($existingPost->name !== $placeName) { + $existingPost->update(['name' => $placeName]); + } + } + + $offset += $batchSize; + } while ($offset < $totalRecords); + + Log::info("Postal data import completed for {$this->countryId}"); + } + + /** + * Get data from the external API + */ + private function getData(int $batchSize, int $offset): array + { + $url = self::OPENDATASOFT_RECORDS_API_URL + . "search/?dataset=geonames-postal-code@public" + . "&q=" + . "&rows={$batchSize}" + . "&start={$offset}" + . "&sort=postal_code" + . "&refine.country_code={$this->countryId}"; + + $response = Http::get($url); + + if (!$response->successful()) { + throw new Exception("Failed to fetch data from Opendatasoft API: " . $response->status()); + } + + $data = $response->json(); + + if (empty($data)) { + throw new Exception('Empty data set received from Opendatasoft API'); + } + + return [ + $data['nhits'], + $data['records'], + ]; + } + + /** + * Extract code and name from record based on country + */ + private function getRecordData(array $record): array + { + switch ($this->countryId) { + case 'HR': + return [ + $record['fields']['postal_code'], + $record['fields']['admin_name3'], + ]; + + case 'SI': + return [ + $record['fields']['postal_code'], + $record['fields']['place_name'], + ]; + + default: + throw new Exception("Country {$this->countryId} not supported"); + } + } +} \ No newline at end of file diff --git a/src/Models/Post.php b/src/Models/Post.php index 6c97954..70794e4 100644 --- a/src/Models/Post.php +++ b/src/Models/Post.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Validation\Rule; class Post extends Model { @@ -23,4 +24,22 @@ public function country(): BelongsTo { return $this->belongsTo(Country::class); } + + /** + * Get validation rules for the model + */ + public static function getValidationRules(?self $record = null): array + { + return [ + 'country_id' => ['required', 'string', 'max:2', 'exists:world_countries,id'], + 'code' => [ + 'required', + 'string', + Rule::unique('world_posts', 'code') + ->where('country_id', request('country_id')) + ->ignore($record?->id), + ], + 'name' => ['required', 'string', 'max:255'], + ]; + } } diff --git a/src/Policies/PostPolicy.php b/src/Policies/PostPolicy.php new file mode 100644 index 0000000..cec8bd5 --- /dev/null +++ b/src/Policies/PostPolicy.php @@ -0,0 +1,84 @@ +can('view_any_post'); + } + + /** + * Determine whether the user can create models. + */ + public function create(Authorizable $user): bool + { + return $user->can('create_post'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(Authorizable $user, Post $post): bool + { + return $user->can('update_post'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Authorizable $user, Post $post): bool + { + return $user->can('delete_post'); + } + + /** + * Determine whether the user can bulk delete. + */ + public function deleteAny(Authorizable $user): bool + { + return $user->can('delete_any_post'); + } + + /** + * Determine whether the user can permanently delete. + */ + public function forceDelete(Authorizable $user, Post $post): bool + { + return $user->can('force_delete_post'); + } + + /** + * Determine whether the user can permanently bulk delete. + */ + public function forceDeleteAny(Authorizable $user): bool + { + return $user->can('force_delete_any_post'); + } + + /** + * Determine whether the user can restore. + */ + public function restore(Authorizable $user, Post $post): bool + { + return $user->can('restore_post'); + } + + /** + * Determine whether the user can bulk restore. + */ + public function restoreAny(Authorizable $user): bool + { + return $user->can('restore_any_post'); + } +} \ No newline at end of file diff --git a/tests/Feature/PostResourceTest.php b/tests/Feature/PostResourceTest.php new file mode 100644 index 0000000..cadce18 --- /dev/null +++ b/tests/Feature/PostResourceTest.php @@ -0,0 +1,330 @@ +setUpSuperAdmin(); +}); + +test('unauthorized access can be prevented', function () { + // Create regular user with no permissions + $this->setUpCommonUser(); + + // Create test data + $country = Country::factory()->create(); + $post = Post::factory()->create(['country_id' => $country->id]); + + // View table + $this->get(PostResource::getUrl()) + ->assertForbidden(); + + // Add direct permission to view the table, since otherwise any other action below is not available even for testing + $this->user->givePermissionTo('view_any_post'); + + // Create post + livewire(ListPosts::class) + ->assertActionDisabled('create'); + + // Edit post + livewire(ListPosts::class) + ->assertCanSeeTableRecords([$post]) + ->assertTableActionDisabled('edit', $post); + + // Delete post + livewire(ListPosts::class) + ->assertTableActionDisabled('delete', $post) + ->assertTableBulkActionDisabled('delete'); + + // Restore and force delete + $post->delete(); + $this->assertSoftDeleted($post); + + livewire(ListPosts::class) + ->assertTableActionDisabled('restore', $post) + ->assertTableBulkActionDisabled('restore') + ->assertTableActionDisabled('forceDelete', $post) + ->assertTableBulkActionDisabled('forceDelete'); +}); + +test('posts table can be displayed', function () { + $this->get(PostResource::getUrl()) + ->assertSuccessful(); +}); + +test('form validation works', function () { + $country = Country::factory()->create(); + $component = livewire(ListPosts::class); + + // Test required fields + $component->callAction('create') + ->assertHasActionErrors([ + 'country_id' => 'required', + 'code' => 'required', + 'name' => 'required', + ]); + + // Test with valid data + $validData = [ + 'country_id' => $country->id, + 'code' => '1000', + 'name' => 'Ljubljana', + ]; + + $component->callAction('create', $validData) + ->assertHasNoActionErrors(); +}); + +test('new post can be created', function () { + $country = Country::factory()->create(); + $data = [ + 'country_id' => $country->id, + 'code' => '1000', + 'name' => 'Ljubljana', + ]; + + livewire(ListPosts::class) + ->callAction('create', $data) + ->assertHasNoActionErrors(); + + $post = Post::where('code', $data['code']) + ->where('country_id', $data['country_id']) + ->first(); + + expect($post)->toBeObject(); + + foreach ($data as $key => $val) { + expect($post->$key)->toEqual($val); + } +}); + +test('existing post can be updated', function () { + $country = Country::factory()->create(); + $post = Post::factory()->create([ + 'country_id' => $country->id, + 'code' => '1000', + 'name' => 'Ljubljana', + ]); + + $data = [ + 'code' => '2000', + 'name' => 'Maribor', + ]; + + livewire(ListPosts::class) + ->callTableAction('edit', $post, $data) + ->assertHasNoTableActionErrors(); + + $post->refresh(); + + foreach ($data as $key => $val) { + expect($post->$key)->toEqual($val); + } +}); + +test('post can be deleted', function () { + $country = Country::factory()->create(); + $post = Post::factory()->create(['country_id' => $country->id]); + + livewire(ListPosts::class) + ->callTableAction('delete', $post) + ->assertHasNoTableActionErrors(); + + $this->assertSoftDeleted($post); +}); + +test('post can be restored', function () { + $country = Country::factory()->create(); + $post = Post::factory()->create(['country_id' => $country->id]); + $post->delete(); + + $this->assertSoftDeleted($post); + + livewire(ListPosts::class) + ->filterTable('trashed') + ->assertTableActionExists('restore') + ->assertTableActionEnabled('restore', $post) + ->assertTableActionVisible('restore', $post) + ->callTableAction('restore', $post) + ->assertHasNoTableActionErrors(); + + $this->assertNotSoftDeleted($post); +}); + +test('post can be force deleted', function () { + $country = Country::factory()->create(); + $post = Post::factory()->create(['country_id' => $country->id]); + + $post->delete(); + $this->assertSoftDeleted($post); + + livewire(ListPosts::class) + ->filterTable('trashed') + ->assertTableActionExists('forceDelete') + ->assertTableActionEnabled('forceDelete', $post) + ->assertTableActionVisible('forceDelete', $post) + ->callTableAction('forceDelete', $post) + ->assertHasNoTableActionErrors(); + + $this->assertModelMissing($post); +}); + +test('filtering by country works', function () { + // Create two countries + $country1 = Country::factory()->create(['id' => 'SI', 'name' => 'Slovenia']); + $country2 = Country::factory()->create(['id' => 'HR', 'name' => 'Croatia']); + + // Create posts for each country + $post1 = Post::factory()->create(['country_id' => $country1->id, 'name' => 'Ljubljana']); + $post2 = Post::factory()->create(['country_id' => $country2->id, 'name' => 'Zagreb']); + + // Test filtering by first country + livewire(ListPosts::class) + ->filterTable('country_id', $country1->id) + ->assertCanSeeTableRecords([$post1]) + ->assertCanNotSeeTableRecords([$post2]); + + // Test filtering by second country + livewire(ListPosts::class) + ->filterTable('country_id', $country2->id) + ->assertCanSeeTableRecords([$post2]) + ->assertCanNotSeeTableRecords([$post1]); + + // Test removing filter shows all posts + livewire(ListPosts::class) + ->removeTableFilter('country_id') + ->assertCanSeeTableRecords([$post1, $post2]); +}); + +test('cannot create duplicate country-post code combo', function () { + $country = Country::factory()->create(['id' => 'SI']); + + // Create first post + $firstPost = Post::factory()->create([ + 'country_id' => $country->id, + 'code' => '1000', + 'name' => 'Ljubljana', + ]); + + // Try to create duplicate country-code combination + $duplicateData = [ + 'country_id' => $country->id, + 'code' => '1000', + 'name' => 'Different Name', + ]; + + livewire(ListPosts::class) + ->callAction('create', $duplicateData) + ->assertHasActionErrors(['code']); + + // Verify only one post exists + expect(Post::where('country_id', $country->id)->where('code', '1000')->count()) + ->toBe(1); +}); + +test('can create same post code for different countries', function () { + // Create two countries + $country1 = Country::factory()->create(['id' => 'SI']); + $country2 = Country::factory()->create(['id' => 'HR']); + + // Create post with same code for first country + $post1Data = [ + 'country_id' => $country1->id, + 'code' => '1000', + 'name' => 'Ljubljana', + ]; + + livewire(ListPosts::class) + ->callAction('create', $post1Data) + ->assertHasNoActionErrors(); + + // Create post with same code for second country (should work) + $post2Data = [ + 'country_id' => $country2->id, + 'code' => '1000', + 'name' => 'Zagreb', + ]; + + livewire(ListPosts::class) + ->callAction('create', $post2Data) + ->assertHasNoActionErrors(); + + // Verify both posts exist + expect(Post::where('code', '1000')->count())->toBe(2); + expect(Post::where('country_id', $country1->id)->where('code', '1000')->count())->toBe(1); + expect(Post::where('country_id', $country2->id)->where('code', '1000')->count())->toBe(1); +}); + +test('country flag is displayed in table', function () { + $country = Country::factory()->create([ + 'id' => 'SI', + 'name' => 'Slovenia', + 'flag' => '🇸🇮', + ]); + + $post = Post::factory()->create([ + 'country_id' => $country->id, + 'code' => '1000', + 'name' => 'Ljubljana', + ]); + + livewire(ListPosts::class) + ->assertCanSeeTableRecords([$post]) + ->assertSeeHtml('🇸🇮'); +}); + +test('updating post respects unique constraint', function () { + $country = Country::factory()->create(['id' => 'SI']); + + // Create two posts + $post1 = Post::factory()->create([ + 'country_id' => $country->id, + 'code' => '1000', + 'name' => 'Ljubljana', + ]); + + $post2 = Post::factory()->create([ + 'country_id' => $country->id, + 'code' => '2000', + 'name' => 'Maribor', + ]); + + // Try to update post2 to have same code as post1 + livewire(ListPosts::class) + ->callTableAction('edit', $post2, [ + 'code' => '1000', // This should fail + 'name' => 'Updated Name', + ]) + ->assertHasTableActionErrors(['code']); + + // Verify post2 wasn't updated + $post2->refresh(); + expect($post2->code)->toBe('2000'); + expect($post2->name)->toBe('Maribor'); +}); + +test('can update post with same code (no change)', function () { + $country = Country::factory()->create(['id' => 'SI']); + + $post = Post::factory()->create([ + 'country_id' => $country->id, + 'code' => '1000', + 'name' => 'Ljubljana', + ]); + + // Update post name but keep same code (should work) + livewire(ListPosts::class) + ->callTableAction('edit', $post, [ + 'code' => '1000', // Same code + 'name' => 'Updated Ljubljana', + ]) + ->assertHasNoTableActionErrors(); + + $post->refresh(); + expect($post->code)->toBe('1000'); + expect($post->name)->toBe('Updated Ljubljana'); +}); \ No newline at end of file From 8bbeef4fd467d2d943de7c3f6517a3bcb06d670b Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Fri, 4 Jul 2025 08:46:18 +0200 Subject: [PATCH 04/11] chore(posts): add missing factory to post model --- src/Models/Post.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Models/Post.php b/src/Models/Post.php index 70794e4..1f7fed6 100644 --- a/src/Models/Post.php +++ b/src/Models/Post.php @@ -2,6 +2,7 @@ namespace Eclipse\World\Models; +use Eclipse\World\Factories\PostFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -25,6 +26,11 @@ public function country(): BelongsTo return $this->belongsTo(Country::class); } + protected static function newFactory(): PostFactory + { + return PostFactory::new(); + } + /** * Get validation rules for the model */ From f42fddb051e4b7585df5be4dddb9af028e2db8ec Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Mon, 7 Jul 2025 11:58:49 +0200 Subject: [PATCH 05/11] chore(posts): add missing migration --- ...add_unique_country_code_to_posts_table.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 database/migrations/2025_07_03_191122_add_unique_country_code_to_posts_table.php diff --git a/database/migrations/2025_07_03_191122_add_unique_country_code_to_posts_table.php b/database/migrations/2025_07_03_191122_add_unique_country_code_to_posts_table.php new file mode 100644 index 0000000..230c252 --- /dev/null +++ b/database/migrations/2025_07_03_191122_add_unique_country_code_to_posts_table.php @@ -0,0 +1,28 @@ +unique(['country_id', 'code'], 'unique_country_post_code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('world_posts', function (Blueprint $table) { + $table->dropUnique('unique_country_post_code'); + }); + } +}; From 08c948ab32ddb2f6e1c7a88868fea4a687df0c4f Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Mon, 7 Jul 2025 11:59:06 +0200 Subject: [PATCH 06/11] chore(posts): rename signature --- src/Console/Commands/ImportPostsCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/Commands/ImportPostsCommand.php b/src/Console/Commands/ImportPostsCommand.php index a361490..555099c 100644 --- a/src/Console/Commands/ImportPostsCommand.php +++ b/src/Console/Commands/ImportPostsCommand.php @@ -12,7 +12,7 @@ class ImportPostsCommand extends Command * * @var string */ - protected $signature = 'eclipse-world:import-posts {country : The country code (SI or HR)}'; + protected $signature = 'world:import-post {country : The country code (SI or HR)}'; /** * The console command description. From eb309e922e9a59240a1e5d8148cef04e7d739155 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Mon, 7 Jul 2025 11:59:32 +0200 Subject: [PATCH 07/11] chore(posts): prepend country flag in the country column --- src/Filament/Clusters/World/Resources/PostResource.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Filament/Clusters/World/Resources/PostResource.php b/src/Filament/Clusters/World/Resources/PostResource.php index 0c4ddee..0cf5eef 100644 --- a/src/Filament/Clusters/World/Resources/PostResource.php +++ b/src/Filament/Clusters/World/Resources/PostResource.php @@ -76,13 +76,10 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('country.name') ->label(__('eclipse-world::posts.table.country.label')) + ->formatStateUsing(fn(string $state, Post $record) => trim("{$record->country->flag} {$state}")) ->searchable() ->sortable(), - TextColumn::make('country.flag') - ->label(__('eclipse-world::posts.table.flag.label')) - ->width(100), - TextColumn::make('code') ->label(__('eclipse-world::posts.table.code.label')), @@ -92,12 +89,12 @@ public static function table(Table $table): Table ->sortable(), ]) ->filters([ - TrashedFilter::make(), SelectFilter::make('country_id') ->label(__('eclipse-world::posts.filter.country.label')) ->relationship('country', 'name') ->searchable() ->preload(), + TrashedFilter::make(), ]) ->actions([ EditAction::make() From c8335ae777de4af820dbca87b06240f009038436 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Mon, 7 Jul 2025 12:03:51 +0200 Subject: [PATCH 08/11] chore(posts): remove unused translations --- resources/lang/en/posts.php | 3 --- resources/lang/hr/posts.php | 3 --- resources/lang/sl/posts.php | 3 --- resources/lang/sr/posts.php | 3 --- 4 files changed, 12 deletions(-) diff --git a/resources/lang/en/posts.php b/resources/lang/en/posts.php index 5cb8501..e64e477 100644 --- a/resources/lang/en/posts.php +++ b/resources/lang/en/posts.php @@ -9,9 +9,6 @@ 'country' => [ 'label' => 'Country', ], - 'flag' => [ - 'label' => 'Flag', - ], 'code' => [ 'label' => 'Code', ], diff --git a/resources/lang/hr/posts.php b/resources/lang/hr/posts.php index 87fed47..285fe02 100644 --- a/resources/lang/hr/posts.php +++ b/resources/lang/hr/posts.php @@ -9,9 +9,6 @@ 'country' => [ 'label' => 'Država', ], - 'flag' => [ - 'label' => 'Zastava', - ], 'code' => [ 'label' => 'Šifra', ], diff --git a/resources/lang/sl/posts.php b/resources/lang/sl/posts.php index b6ca151..bfeacfd 100644 --- a/resources/lang/sl/posts.php +++ b/resources/lang/sl/posts.php @@ -9,9 +9,6 @@ 'country' => [ 'label' => 'Država', ], - 'flag' => [ - 'label' => 'Zastava', - ], 'code' => [ 'label' => 'Šifra', ], diff --git a/resources/lang/sr/posts.php b/resources/lang/sr/posts.php index 2af15d3..1353b77 100644 --- a/resources/lang/sr/posts.php +++ b/resources/lang/sr/posts.php @@ -9,9 +9,6 @@ 'country' => [ 'label' => 'Država', ], - 'flag' => [ - 'label' => 'Zastava', - ], 'code' => [ 'label' => 'Šifra', ], From 71cffb765015b8f641b85fab7d67f1d949945f04 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Tue, 8 Jul 2025 15:44:05 +0200 Subject: [PATCH 09/11] feat(notifications): add real-time notifications to imports - Add ImportFinishedNotification with broadcast and database channels - Connect ImportFinishedNotification with existing import modules --- resources/lang/en/countries.php | 18 +++++ resources/lang/en/posts.php | 11 +++ resources/lang/hr/countries.php | 18 +++++ resources/lang/hr/posts.php | 11 +++ resources/lang/sl/countries.php | 18 +++++ resources/lang/sl/posts.php | 11 +++ resources/lang/sr/countries.php | 18 +++++ resources/lang/sr/posts.php | 11 +++ src/Console/Commands/ImportCommand.php | 3 +- src/Console/Commands/ImportPostsCommand.php | 3 +- .../CountryResource/Pages/ListCountries.php | 3 +- .../PostResource/Pages/ListPosts.php | 5 +- src/Jobs/ImportCountries.php | 74 ++++++++++++++----- src/Jobs/ImportPosts.php | 68 +++++++++++------ 14 files changed, 224 insertions(+), 48 deletions(-) diff --git a/resources/lang/en/countries.php b/resources/lang/en/countries.php index 5f905a5..0260dc4 100644 --- a/resources/lang/en/countries.php +++ b/resources/lang/en/countries.php @@ -68,4 +68,22 @@ 'helper' => 'Numeric code (ISO-3166)', ], ], + + 'import' => [ + 'action_label' => 'Import Countries', + 'modal_heading' => 'Import Countries', + 'success_title' => 'Import Countries', + 'success_message' => 'The import countries job has been queued.', + ], + + 'notifications' => [ + 'success' => [ + 'title' => 'Countries Import Completed', + 'message' => 'All countries have been successfully imported and updated.', + ], + 'failed' => [ + 'title' => 'Countries Import Failed', + 'message' => 'Failed to import countries data.', + ], + ], ]; diff --git a/resources/lang/en/posts.php b/resources/lang/en/posts.php index e64e477..62ab903 100644 --- a/resources/lang/en/posts.php +++ b/resources/lang/en/posts.php @@ -75,4 +75,15 @@ 'HR' => 'Croatia', ], ], + + 'notifications' => [ + 'success' => [ + 'title' => 'Posts Import Completed', + 'message' => 'Postal data for :country has been successfully imported.', + ], + 'failed' => [ + 'title' => 'Posts Import Failed', + 'message' => 'Failed to import postal data for :country.', + ], + ], ]; \ No newline at end of file diff --git a/resources/lang/hr/countries.php b/resources/lang/hr/countries.php index 6c64cec..e568c0f 100644 --- a/resources/lang/hr/countries.php +++ b/resources/lang/hr/countries.php @@ -68,4 +68,22 @@ 'helper' => 'Numerička oznaka (ISO-3166)', ], ], + + 'import' => [ + 'action_label' => 'Uvezi države', + 'modal_heading' => 'Uvoz država', + 'success_title' => 'Uvoz država', + 'success_message' => 'Zadatak za uvoz država je dodan u red čekanja.', + ], + + 'notifications' => [ + 'success' => [ + 'title' => 'Uvoz država završen', + 'message' => 'Sve države su uspješno uvežene i ažurirane.', + ], + 'failed' => [ + 'title' => 'Uvoz država neuspješan', + 'message' => 'Neuspješan uvoz podataka država.', + ], + ], ]; diff --git a/resources/lang/hr/posts.php b/resources/lang/hr/posts.php index 285fe02..1dda81e 100644 --- a/resources/lang/hr/posts.php +++ b/resources/lang/hr/posts.php @@ -75,4 +75,15 @@ 'HR' => 'Hrvatska', ], ], + + 'notifications' => [ + 'success' => [ + 'title' => 'Uvoz pošta završen', + 'message' => 'Poštanski podaci za :country su uspješno uveženi.', + ], + 'failed' => [ + 'title' => 'Uvoz pošta neuspješan', + 'message' => 'Neuspješan uvoz poštanskih podataka za :country.', + ], + ], ]; \ No newline at end of file diff --git a/resources/lang/sl/countries.php b/resources/lang/sl/countries.php index 11349cc..90fb45f 100644 --- a/resources/lang/sl/countries.php +++ b/resources/lang/sl/countries.php @@ -68,4 +68,22 @@ 'helper' => 'Numerična šifra (ISO-3166)', ], ], + + 'import' => [ + 'action_label' => 'Uvozi države', + 'modal_heading' => 'Uvoz držav', + 'success_title' => 'Uvoz držav', + 'success_message' => 'Naloga za uvoz držav je bila dodana v čakalno vrsto.', + ], + + 'notifications' => [ + 'success' => [ + 'title' => 'Uvoz držav uspešno končan', + 'message' => 'Vse države so bile uspešno uvožene in posodobljene.', + ], + 'failed' => [ + 'title' => 'Uvoz držav neuspešen', + 'message' => 'Uvoz podatkov držav ni uspel.', + ], + ], ]; diff --git a/resources/lang/sl/posts.php b/resources/lang/sl/posts.php index bfeacfd..5c311ac 100644 --- a/resources/lang/sl/posts.php +++ b/resources/lang/sl/posts.php @@ -75,4 +75,15 @@ 'HR' => 'Hrvaška', ], ], + + 'notifications' => [ + 'success' => [ + 'title' => 'Uvoz pošt uspešno končan', + 'message' => 'Poštni podatki za :country so bili uspešno uvoženi.', + ], + 'failed' => [ + 'title' => 'Uvoz pošt neuspešen', + 'message' => 'Uvoz poštnih podatkov za :country ni uspel.', + ], + ], ]; \ No newline at end of file diff --git a/resources/lang/sr/countries.php b/resources/lang/sr/countries.php index 5e3cdb0..45247df 100644 --- a/resources/lang/sr/countries.php +++ b/resources/lang/sr/countries.php @@ -68,4 +68,22 @@ 'helper' => 'Numerička oznaka (ISO-3166)', ], ], + + 'import' => [ + 'action_label' => 'Uvezi države', + 'modal_heading' => 'Uvoz država', + 'success_title' => 'Uvoz država', + 'success_message' => 'Zadatak za uvoz država je dodat u red čekanja.', + ], + + 'notifications' => [ + 'success' => [ + 'title' => 'Uvoz država završen', + 'message' => 'Sve države su uspešno uvežene i ažurirane.', + ], + 'failed' => [ + 'title' => 'Uvoz država neuspešan', + 'message' => 'Neuspešan uvoz podataka država.', + ], + ], ]; diff --git a/resources/lang/sr/posts.php b/resources/lang/sr/posts.php index 1353b77..4b0f018 100644 --- a/resources/lang/sr/posts.php +++ b/resources/lang/sr/posts.php @@ -75,4 +75,15 @@ 'HR' => 'Hrvatska', ], ], + + 'notifications' => [ + 'success' => [ + 'title' => 'Uvoz pošta završen', + 'message' => 'Poštanski podaci za :country su uspešno uveženi.', + ], + 'failed' => [ + 'title' => 'Uvoz pošta neuspešan', + 'message' => 'Neuspešan uvoz poštanskih podataka za :country.', + ], + ], ]; \ No newline at end of file diff --git a/src/Console/Commands/ImportCommand.php b/src/Console/Commands/ImportCommand.php index 88d1000..8ef90b0 100644 --- a/src/Console/Commands/ImportCommand.php +++ b/src/Console/Commands/ImportCommand.php @@ -4,6 +4,7 @@ use Eclipse\World\Jobs\ImportCountries; use Illuminate\Console\Command; +use Illuminate\Support\Facades\App; class ImportCommand extends Command { @@ -16,7 +17,7 @@ public function handle(): void $this->info('Import command started'); // Run ImportCountries job - ImportCountries::dispatchSync(); + ImportCountries::dispatchSync(auth()->id(), App::getLocale()); $this->info('Import command ran successfully'); } diff --git a/src/Console/Commands/ImportPostsCommand.php b/src/Console/Commands/ImportPostsCommand.php index 555099c..db2857c 100644 --- a/src/Console/Commands/ImportPostsCommand.php +++ b/src/Console/Commands/ImportPostsCommand.php @@ -4,6 +4,7 @@ use Eclipse\World\Jobs\ImportPosts; use Illuminate\Console\Command; +use Illuminate\Support\Facades\App; class ImportPostsCommand extends Command { @@ -35,7 +36,7 @@ public function handle(): int $this->info("Dispatching import job for country: {$country}"); - ImportPosts::dispatch($country); + ImportPosts::dispatch($country, auth()->id(), App::getLocale()); $this->info('Import job has been queued successfully!'); $this->comment('The import will run in the background. Check the logs for progress.'); diff --git a/src/Filament/Clusters/World/Resources/CountryResource/Pages/ListCountries.php b/src/Filament/Clusters/World/Resources/CountryResource/Pages/ListCountries.php index e8e4e38..ae096ea 100644 --- a/src/Filament/Clusters/World/Resources/CountryResource/Pages/ListCountries.php +++ b/src/Filament/Clusters/World/Resources/CountryResource/Pages/ListCountries.php @@ -8,6 +8,7 @@ use Filament\Actions\CreateAction; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; +use Illuminate\Support\Facades\App; class ListCountries extends ListRecords { @@ -24,7 +25,7 @@ protected function getHeaderActions(): array ->icon('heroicon-o-arrow-down-tray') ->action(function () { // Dispatch the job - ImportCountries::dispatch(); + ImportCountries::dispatch(auth()->id(), App::getLocale()); // Show notification Notification::make() diff --git a/src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php b/src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php index eaa0eaa..9b79467 100644 --- a/src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php +++ b/src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php @@ -9,6 +9,7 @@ use Filament\Forms\Components\Select; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; +use Illuminate\Support\Facades\App; class ListPosts extends ListRecords { @@ -36,8 +37,8 @@ protected function getHeaderActions(): array ]) ->modalHeading(__('eclipse-world::posts.import.modal_heading')) ->action(function (array $data) { - // Dispatch the job with selected country - ImportPosts::dispatch($data['country_id']); + // Dispatch the job + ImportPosts::dispatch($data['country_id'], auth()->id(), App::getLocale()); // Show notification Notification::make() diff --git a/src/Jobs/ImportCountries.php b/src/Jobs/ImportCountries.php index 791c604..340ecaa 100644 --- a/src/Jobs/ImportCountries.php +++ b/src/Jobs/ImportCountries.php @@ -3,11 +3,15 @@ namespace Eclipse\World\Jobs; use Eclipse\World\Models\Country; +use Eclipse\Core\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use App\Notifications\ImportFinishedNotification; +use Exception; +use Illuminate\Support\Facades\Log; class ImportCountries implements ShouldQueue { @@ -17,32 +21,64 @@ class ImportCountries implements ShouldQueue public bool $failOnTimeout = true; + public ?int $userId; + public string $locale; + /** + * Create a new job instance. + */ + public function __construct(?int $userId, string $locale = 'en') + { + $this->userId = $userId; + $this->locale = $locale; + } + public function handle(): void { - // Load existing countries into an associative array - $existingCountries = Country::withTrashed()->get()->keyBy('id'); + Log::info("Starting countries import"); + + $user = $this->userId ? User::find($this->userId) : null; + + try { + // Load existing countries into an associative array + $existingCountries = Country::withTrashed()->get()->keyBy('id'); - // Load new country data - $countries = json_decode(file_get_contents('https://raw.githubusercontent.com/mledoze/countries/master/dist/countries.json'), true); + // Load new country data + $countries = json_decode(file_get_contents('https://raw.githubusercontent.com/mledoze/countries/master/dist/countries.json'), true); - foreach ($countries as $rawData) { - if (! $rawData['independent']) { - continue; + if (!$countries) { + throw new Exception('Failed to fetch or parse countries data'); } - $data = [ - 'id' => $rawData['cca2'], - 'a3_id' => $rawData['cca3'], - 'num_code' => $rawData['ccn3'], - 'name' => $rawData['name']['common'], - 'flag' => $rawData['flag'], - ]; - - if (isset($existingCountries[$data['id']])) { - $existingCountries[$data['id']]->update($data); - } else { - Country::create($data); + foreach ($countries as $rawData) { + if (! $rawData['independent']) { + continue; + } + + $data = [ + 'id' => $rawData['cca2'], + 'a3_id' => $rawData['cca3'], + 'num_code' => $rawData['ccn3'], + 'name' => $rawData['name']['common'], + 'flag' => $rawData['flag'], + ]; + + if (isset($existingCountries[$data['id']])) { + $existingCountries[$data['id']]->update($data); + } else { + Country::create($data); + } + } + + Log::info("Countries import completed"); + if ($user) { + $user->notify(new ImportFinishedNotification('success', 'countries', null, $this->locale)); + } + } catch (Exception $e) { + Log::error("Countries import failed: " . $e->getMessage()); + if ($user) { + $user->notify(new ImportFinishedNotification('failed', 'countries', null, $this->locale)); } + throw $e; } } } diff --git a/src/Jobs/ImportPosts.php b/src/Jobs/ImportPosts.php index 1571bfe..2b976dd 100644 --- a/src/Jobs/ImportPosts.php +++ b/src/Jobs/ImportPosts.php @@ -3,6 +3,7 @@ namespace Eclipse\World\Jobs; use Eclipse\World\Models\Post; +use Eclipse\Core\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -10,6 +11,8 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use App\Notifications\ImportFinishedNotification; + use Exception; class ImportPosts implements ShouldQueue @@ -19,13 +22,17 @@ class ImportPosts implements ShouldQueue private const string OPENDATASOFT_RECORDS_API_URL = 'https://data.opendatasoft.com/api/records/1.0/'; public string $countryId; + public int $userId; + public string $locale; /** * Create a new job instance. */ - public function __construct(string $countryId) + public function __construct(string $countryId, int $userId, string $locale = 'en') { $this->countryId = $countryId; + $this->userId = $userId; + $this->locale = $locale; } /** @@ -43,37 +50,50 @@ public function handle(): void Log::info("Starting postal data import for country: {$this->countryId}"); - do { - [$totalRecords, $records] = $this->getData($batchSize, $offset); + $user = $this->userId ? User::find($this->userId) : null; - foreach ($records as $record) { - [$postalCode, $placeName] = $this->getRecordData($record); + try { + do { + [$totalRecords, $records] = $this->getData($batchSize, $offset); - if (array_key_exists($postalCode, $processedCodes)) { - continue; - } + foreach ($records as $record) { + [$postalCode, $placeName] = $this->getRecordData($record); - $processedCodes[$postalCode] = true; + if (array_key_exists($postalCode, $processedCodes)) { + continue; + } - $existingPost = Post::where('country_id', $this->countryId) - ->where('code', $postalCode) - ->first(); + $processedCodes[$postalCode] = true; - if (empty($existingPost)) { - Post::create([ - 'country_id' => $this->countryId, - 'code' => $postalCode, - 'name' => $placeName, - ]); - } elseif ($existingPost->name !== $placeName) { - $existingPost->update(['name' => $placeName]); + $existingPost = Post::where('country_id', $this->countryId) + ->where('code', $postalCode) + ->first(); + + if (empty($existingPost)) { + Post::create([ + 'country_id' => $this->countryId, + 'code' => $postalCode, + 'name' => $placeName, + ]); + } elseif ($existingPost->name !== $placeName) { + $existingPost->update(['name' => $placeName]); + } } - } - $offset += $batchSize; - } while ($offset < $totalRecords); + $offset += $batchSize; + } while ($offset < $totalRecords); - Log::info("Postal data import completed for {$this->countryId}"); + Log::info("Postal data import completed for {$this->countryId}"); + if ($user) { + $user->notify(new ImportFinishedNotification('success', 'posts', $this->countryId, $this->locale)); + } + } catch (Exception $e) { + Log::error("Postal data import failed for {$this->countryId}: {$e->getMessage()}"); + if ($user) { + $user->notify(new ImportFinishedNotification('failed', 'posts', $this->countryId, $this->locale)); + } + throw $e; + } } /** From df1774de9de588f972a6d8405409f3f808c8252d Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 10 Jul 2025 12:34:04 +0200 Subject: [PATCH 10/11] chore(notifcations): show real-time toast notification --- resources/lang/en/posts.php | 2 +- resources/lang/hr/posts.php | 2 +- resources/lang/sl/posts.php | 2 +- resources/lang/sr/posts.php | 2 +- src/Console/Commands/ImportCommand.php | 3 +- src/Console/Commands/ImportPostsCommand.php | 8 +- .../Clusters/World/Resources/PostResource.php | 6 +- .../PostResource/Pages/ListPosts.php | 2 +- src/Jobs/ImportCountries.php | 16 ++- src/Jobs/ImportPosts.php | 29 ++-- .../ImportFinishedNotification.php | 129 ++++++++++++++++++ src/Policies/PostPolicy.php | 2 +- tests/Feature/PostResourceTest.php | 14 +- 13 files changed, 174 insertions(+), 43 deletions(-) create mode 100644 src/Notifications/ImportFinishedNotification.php diff --git a/resources/lang/en/posts.php b/resources/lang/en/posts.php index 62ab903..d1c96c6 100644 --- a/resources/lang/en/posts.php +++ b/resources/lang/en/posts.php @@ -86,4 +86,4 @@ 'message' => 'Failed to import postal data for :country.', ], ], -]; \ No newline at end of file +]; diff --git a/resources/lang/hr/posts.php b/resources/lang/hr/posts.php index 1dda81e..a28d748 100644 --- a/resources/lang/hr/posts.php +++ b/resources/lang/hr/posts.php @@ -86,4 +86,4 @@ 'message' => 'Neuspješan uvoz poštanskih podataka za :country.', ], ], -]; \ No newline at end of file +]; diff --git a/resources/lang/sl/posts.php b/resources/lang/sl/posts.php index 5c311ac..0003c25 100644 --- a/resources/lang/sl/posts.php +++ b/resources/lang/sl/posts.php @@ -86,4 +86,4 @@ 'message' => 'Uvoz poštnih podatkov za :country ni uspel.', ], ], -]; \ No newline at end of file +]; diff --git a/resources/lang/sr/posts.php b/resources/lang/sr/posts.php index 4b0f018..0c0fbf1 100644 --- a/resources/lang/sr/posts.php +++ b/resources/lang/sr/posts.php @@ -86,4 +86,4 @@ 'message' => 'Neuspešan uvoz poštanskih podataka za :country.', ], ], -]; \ No newline at end of file +]; diff --git a/src/Console/Commands/ImportCommand.php b/src/Console/Commands/ImportCommand.php index 8ef90b0..88d1000 100644 --- a/src/Console/Commands/ImportCommand.php +++ b/src/Console/Commands/ImportCommand.php @@ -4,7 +4,6 @@ use Eclipse\World\Jobs\ImportCountries; use Illuminate\Console\Command; -use Illuminate\Support\Facades\App; class ImportCommand extends Command { @@ -17,7 +16,7 @@ public function handle(): void $this->info('Import command started'); // Run ImportCountries job - ImportCountries::dispatchSync(auth()->id(), App::getLocale()); + ImportCountries::dispatchSync(); $this->info('Import command ran successfully'); } diff --git a/src/Console/Commands/ImportPostsCommand.php b/src/Console/Commands/ImportPostsCommand.php index db2857c..d68d3f1 100644 --- a/src/Console/Commands/ImportPostsCommand.php +++ b/src/Console/Commands/ImportPostsCommand.php @@ -4,7 +4,6 @@ use Eclipse\World\Jobs\ImportPosts; use Illuminate\Console\Command; -use Illuminate\Support\Facades\App; class ImportPostsCommand extends Command { @@ -29,18 +28,19 @@ public function handle(): int { $country = strtoupper($this->argument('country')); - if (!in_array($country, ['SI', 'HR'])) { + if (! in_array($country, ['SI', 'HR'])) { $this->error('Invalid country code. Only SI and HR are supported.'); + return self::FAILURE; } $this->info("Dispatching import job for country: {$country}"); - ImportPosts::dispatch($country, auth()->id(), App::getLocale()); + ImportPosts::dispatch($country); $this->info('Import job has been queued successfully!'); $this->comment('The import will run in the background. Check the logs for progress.'); return self::SUCCESS; } -} \ No newline at end of file +} diff --git a/src/Filament/Clusters/World/Resources/PostResource.php b/src/Filament/Clusters/World/Resources/PostResource.php index 0cf5eef..3a0127f 100644 --- a/src/Filament/Clusters/World/Resources/PostResource.php +++ b/src/Filament/Clusters/World/Resources/PostResource.php @@ -20,8 +20,8 @@ use Filament\Tables\Actions\RestoreAction; use Filament\Tables\Actions\RestoreBulkAction; use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Filters\TrashedFilter; use Filament\Tables\Filters\SelectFilter; +use Filament\Tables\Filters\TrashedFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; @@ -76,7 +76,7 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('country.name') ->label(__('eclipse-world::posts.table.country.label')) - ->formatStateUsing(fn(string $state, Post $record) => trim("{$record->country->flag} {$state}")) + ->formatStateUsing(fn (string $state, Post $record) => trim("{$record->country->flag} {$state}")) ->searchable() ->sortable(), @@ -110,7 +110,7 @@ public static function table(Table $table): Table ForceDeleteAction::make() ->label(__('eclipse-world::posts.actions.force_delete.label')) ->modalHeading(__('eclipse-world::posts.actions.force_delete.heading')) - ->modalDescription(fn(Post $record): string => __('eclipse-world::posts.actions.force_delete.description', [ + ->modalDescription(fn (Post $record): string => __('eclipse-world::posts.actions.force_delete.description', [ 'name' => $record->name, ])), ]), diff --git a/src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php b/src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php index 9b79467..92e82e3 100644 --- a/src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php +++ b/src/Filament/Clusters/World/Resources/PostResource/Pages/ListPosts.php @@ -44,7 +44,7 @@ protected function getHeaderActions(): array Notification::make() ->title(__('eclipse-world::posts.import.success_title')) ->body(__('eclipse-world::posts.import.success_message', [ - 'country' => __('eclipse-world::posts.import.countries.' . $data['country_id']) + 'country' => __('eclipse-world::posts.import.countries.'.$data['country_id']), ])) ->success() ->send(); diff --git a/src/Jobs/ImportCountries.php b/src/Jobs/ImportCountries.php index 340ecaa..e34e2f2 100644 --- a/src/Jobs/ImportCountries.php +++ b/src/Jobs/ImportCountries.php @@ -2,15 +2,15 @@ namespace Eclipse\World\Jobs; -use Eclipse\World\Models\Country; use Eclipse\Core\Models\User; +use Eclipse\World\Models\Country; +use Eclipse\World\Notifications\ImportFinishedNotification; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use App\Notifications\ImportFinishedNotification; -use Exception; use Illuminate\Support\Facades\Log; class ImportCountries implements ShouldQueue @@ -22,7 +22,9 @@ class ImportCountries implements ShouldQueue public bool $failOnTimeout = true; public ?int $userId; + public string $locale; + /** * Create a new job instance. */ @@ -34,7 +36,7 @@ public function __construct(?int $userId, string $locale = 'en') public function handle(): void { - Log::info("Starting countries import"); + Log::info('Starting countries import'); $user = $this->userId ? User::find($this->userId) : null; @@ -45,7 +47,7 @@ public function handle(): void // Load new country data $countries = json_decode(file_get_contents('https://raw.githubusercontent.com/mledoze/countries/master/dist/countries.json'), true); - if (!$countries) { + if (! $countries) { throw new Exception('Failed to fetch or parse countries data'); } @@ -69,12 +71,12 @@ public function handle(): void } } - Log::info("Countries import completed"); + Log::info('Countries import completed'); if ($user) { $user->notify(new ImportFinishedNotification('success', 'countries', null, $this->locale)); } } catch (Exception $e) { - Log::error("Countries import failed: " . $e->getMessage()); + Log::error('Countries import failed: '.$e->getMessage()); if ($user) { $user->notify(new ImportFinishedNotification('failed', 'countries', null, $this->locale)); } diff --git a/src/Jobs/ImportPosts.php b/src/Jobs/ImportPosts.php index 2b976dd..7f91303 100644 --- a/src/Jobs/ImportPosts.php +++ b/src/Jobs/ImportPosts.php @@ -2,8 +2,10 @@ namespace Eclipse\World\Jobs; -use Eclipse\World\Models\Post; use Eclipse\Core\Models\User; +use Eclipse\World\Models\Post; +use Eclipse\World\Notifications\ImportFinishedNotification; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -11,9 +13,6 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use App\Notifications\ImportFinishedNotification; - -use Exception; class ImportPosts implements ShouldQueue { @@ -22,7 +21,9 @@ class ImportPosts implements ShouldQueue private const string OPENDATASOFT_RECORDS_API_URL = 'https://data.opendatasoft.com/api/records/1.0/'; public string $countryId; + public int $userId; + public string $locale; /** @@ -40,7 +41,7 @@ public function __construct(string $countryId, int $userId, string $locale = 'en */ public function handle(): void { - if (!in_array($this->countryId, ['SI', 'HR'])) { + if (! in_array($this->countryId, ['SI', 'HR'])) { throw new Exception("Country {$this->countryId} not supported for import"); } @@ -102,17 +103,17 @@ public function handle(): void private function getData(int $batchSize, int $offset): array { $url = self::OPENDATASOFT_RECORDS_API_URL - . "search/?dataset=geonames-postal-code@public" - . "&q=" - . "&rows={$batchSize}" - . "&start={$offset}" - . "&sort=postal_code" - . "&refine.country_code={$this->countryId}"; + .'search/?dataset=geonames-postal-code@public' + .'&q=' + ."&rows={$batchSize}" + ."&start={$offset}" + .'&sort=postal_code' + ."&refine.country_code={$this->countryId}"; $response = Http::get($url); - if (!$response->successful()) { - throw new Exception("Failed to fetch data from Opendatasoft API: " . $response->status()); + if (! $response->successful()) { + throw new Exception('Failed to fetch data from Opendatasoft API: '.$response->status()); } $data = $response->json(); @@ -149,4 +150,4 @@ private function getRecordData(array $record): array throw new Exception("Country {$this->countryId} not supported"); } } -} \ No newline at end of file +} diff --git a/src/Notifications/ImportFinishedNotification.php b/src/Notifications/ImportFinishedNotification.php new file mode 100644 index 0000000..093693c --- /dev/null +++ b/src/Notifications/ImportFinishedNotification.php @@ -0,0 +1,129 @@ +status = $status; + $this->importType = $importType; + $this->identifier = $identifier; + $this->locale = $locale; + } + + /** + * Channels: database only + */ + public function via(): array + { + return ['database']; + } + + /** + * For storing in DB + */ + public function toDatabase($notifiable): array + { + $title = $this->getTitle(); + $body = $this->getBody(); + $icon = $this->status === 'success' ? 'heroicon-o-check-circle' : 'heroicon-o-x-circle'; + $iconColor = $this->status === 'success' ? 'success' : 'danger'; + + FilamentNotification::make() + ->title($title) + ->body($body) + ->icon($icon) + ->iconColor($iconColor) + ->broadcast($notifiable); + + return FilamentNotification::make() + ->title($title) + ->body($body) + ->icon($icon) + ->iconColor($iconColor) + ->getDatabaseMessage(); + } + + public function toArray(): array + { + return [ + 'title' => $this->getTitle(), + 'body' => $this->getBody(), + 'status' => $this->status, + 'importType' => $this->importType, + 'identifier' => $this->identifier, + ]; + } + + /** + * Generate title based on import type and status using the locale + */ + private function getTitle(): string + { + $translationKey = "eclipse-world::{$this->importType}.notifications.{$this->status}.title"; + $title = __($translationKey, [], $this->locale); + + // Fallback to English if locale translation is missing + if ($title === $translationKey) { + $title = __($translationKey, [], 'en'); + } + + return $title; + } + + /** + * Generate body message based on import details using the locale + */ + private function getBody(): string + { + $translationKey = "eclipse-world::{$this->importType}.notifications.{$this->status}.message"; + + $parameters = []; + if ($this->identifier) { + if ($this->importType === 'posts') { + $countryKey = "eclipse-world::posts.import.countries.{$this->identifier}"; + $countryName = __($countryKey, [], $this->locale); + + // Fallback to English if locale translation is missing + if ($countryName === $countryKey) { + $countryName = __($countryKey, [], 'en'); + } + + // Final fallback to identifier itself (country code, e.g. SI) + if ($countryName === $countryKey) { + $countryName = $this->identifier; + } + + $parameters['country'] = $countryName; + } else { + $parameters['country'] = $this->identifier; + } + } + + $body = __($translationKey, $parameters, $this->locale); + + // Fallback to English if locale translation is missing + if ($body === $translationKey) { + $body = __($translationKey, $parameters, 'en'); + } + + return $body; + } +} diff --git a/src/Policies/PostPolicy.php b/src/Policies/PostPolicy.php index cec8bd5..2251a4c 100644 --- a/src/Policies/PostPolicy.php +++ b/src/Policies/PostPolicy.php @@ -81,4 +81,4 @@ public function restoreAny(Authorizable $user): bool { return $user->can('restore_any_post'); } -} \ No newline at end of file +} diff --git a/tests/Feature/PostResourceTest.php b/tests/Feature/PostResourceTest.php index cadce18..73124f6 100644 --- a/tests/Feature/PostResourceTest.php +++ b/tests/Feature/PostResourceTest.php @@ -94,7 +94,7 @@ $post = Post::where('code', $data['code']) ->where('country_id', $data['country_id']) ->first(); - + expect($post)->toBeObject(); foreach ($data as $key => $val) { @@ -202,7 +202,7 @@ test('cannot create duplicate country-post code combo', function () { $country = Country::factory()->create(['id' => 'SI']); - + // Create first post $firstPost = Post::factory()->create([ 'country_id' => $country->id, @@ -265,7 +265,7 @@ 'name' => 'Slovenia', 'flag' => '🇸🇮', ]); - + $post = Post::factory()->create([ 'country_id' => $country->id, 'code' => '1000', @@ -279,14 +279,14 @@ test('updating post respects unique constraint', function () { $country = Country::factory()->create(['id' => 'SI']); - + // Create two posts $post1 = Post::factory()->create([ 'country_id' => $country->id, 'code' => '1000', 'name' => 'Ljubljana', ]); - + $post2 = Post::factory()->create([ 'country_id' => $country->id, 'code' => '2000', @@ -309,7 +309,7 @@ test('can update post with same code (no change)', function () { $country = Country::factory()->create(['id' => 'SI']); - + $post = Post::factory()->create([ 'country_id' => $country->id, 'code' => '1000', @@ -327,4 +327,4 @@ $post->refresh(); expect($post->code)->toBe('1000'); expect($post->name)->toBe('Updated Ljubljana'); -}); \ No newline at end of file +}); From ed15e45d6f0f367de65a77e63cf1ddd397232ae8 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 10 Jul 2025 22:19:40 +0200 Subject: [PATCH 11/11] chore(notifications): refactor toDatabase method --- .../ImportFinishedNotification.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Notifications/ImportFinishedNotification.php b/src/Notifications/ImportFinishedNotification.php index 093693c..0c7a12b 100644 --- a/src/Notifications/ImportFinishedNotification.php +++ b/src/Notifications/ImportFinishedNotification.php @@ -45,20 +45,16 @@ public function toDatabase($notifiable): array $body = $this->getBody(); $icon = $this->status === 'success' ? 'heroicon-o-check-circle' : 'heroicon-o-x-circle'; $iconColor = $this->status === 'success' ? 'success' : 'danger'; - - FilamentNotification::make() - ->title($title) - ->body($body) - ->icon($icon) - ->iconColor($iconColor) - ->broadcast($notifiable); - - return FilamentNotification::make() + + $notification = FilamentNotification::make() ->title($title) ->body($body) ->icon($icon) - ->iconColor($iconColor) - ->getDatabaseMessage(); + ->iconColor($iconColor); + + $notification->broadcast($notifiable); + + return $notification->getDatabaseMessage(); } public function toArray(): array