From b213086bd769b66710877507f5e2db128695a1c1 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Sat, 26 Jul 2025 18:44:08 +0200 Subject: [PATCH 1/9] feat(regions): implement world regions --- database/factories/CountryFactory.php | 1 + .../factories/CountrySpecialRegionFactory.php | 72 ++++++ database/factories/RegionFactory.php | 60 +++++ ...2025_07_26_120000_create_regions_table.php | 29 +++ ...20001_add_region_id_to_countries_table.php | 26 +++ ...create_country_in_special_region_table.php | 31 +++ .../2025_07_26_120003_seed_eu_region.php | 41 ++++ resources/lang/en/countries.php | 16 ++ resources/lang/en/regions.php | 81 +++++++ resources/lang/en/special-memberships.php | 74 +++++++ resources/lang/hr/countries.php | 16 ++ resources/lang/hr/regions.php | 81 +++++++ resources/lang/hr/special-memberships.php | 74 +++++++ resources/lang/sl/countries.php | 16 ++ resources/lang/sl/regions.php | 81 +++++++ resources/lang/sl/special-memberships.php | 74 +++++++ resources/lang/sr/countries.php | 16 ++ resources/lang/sr/regions.php | 81 +++++++ resources/lang/sr/special-memberships.php | 74 +++++++ .../World/Resources/CountryResource.php | 26 +++ .../World/Resources/RegionResource.php | 197 +++++++++++++++++ .../RegionResource/Pages/ListRegions.php | 21 ++ .../SpecialRegionMembershipResource.php | 203 +++++++++++++++++ .../Pages/ListSpecialRegionMemberships.php | 26 +++ src/Jobs/ImportCountries.php | 75 +++++++ src/Models/Country.php | 54 +++++ src/Models/CountrySpecialRegion.php | 77 +++++++ src/Models/Region.php | 116 ++++++++++ src/Policies/RegionPolicy.php | 92 ++++++++ .../SpecialRegionMembershipPolicy.php | 59 +++++ tests/Feature/CountryResourceTest.php | 39 ++++ tests/Feature/RegionResourceTest.php | 205 ++++++++++++++++++ tests/Unit/ImportCountriesJobTest.php | 145 +++++++++++++ tests/Unit/RegionModelTest.php | 154 +++++++++++++ 34 files changed, 2433 insertions(+) create mode 100644 database/factories/CountrySpecialRegionFactory.php create mode 100644 database/factories/RegionFactory.php create mode 100644 database/migrations/2025_07_26_120000_create_regions_table.php create mode 100644 database/migrations/2025_07_26_120001_add_region_id_to_countries_table.php create mode 100644 database/migrations/2025_07_26_120002_create_country_in_special_region_table.php create mode 100644 database/migrations/2025_07_26_120003_seed_eu_region.php create mode 100644 resources/lang/en/regions.php create mode 100644 resources/lang/en/special-memberships.php create mode 100644 resources/lang/hr/regions.php create mode 100644 resources/lang/hr/special-memberships.php create mode 100644 resources/lang/sl/regions.php create mode 100644 resources/lang/sl/special-memberships.php create mode 100644 resources/lang/sr/regions.php create mode 100644 resources/lang/sr/special-memberships.php create mode 100644 src/Filament/Clusters/World/Resources/RegionResource.php create mode 100644 src/Filament/Clusters/World/Resources/RegionResource/Pages/ListRegions.php create mode 100644 src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource.php create mode 100644 src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource/Pages/ListSpecialRegionMemberships.php create mode 100644 src/Models/CountrySpecialRegion.php create mode 100644 src/Models/Region.php create mode 100644 src/Policies/RegionPolicy.php create mode 100644 src/Policies/SpecialRegionMembershipPolicy.php create mode 100644 tests/Feature/RegionResourceTest.php create mode 100644 tests/Unit/ImportCountriesJobTest.php create mode 100644 tests/Unit/RegionModelTest.php diff --git a/database/factories/CountryFactory.php b/database/factories/CountryFactory.php index 3155a1c..e03800e 100644 --- a/database/factories/CountryFactory.php +++ b/database/factories/CountryFactory.php @@ -27,6 +27,7 @@ public function definition(): array 'num_code' => str_pad(fake()->numberBetween(1, 999), 3, '0', STR_PAD_LEFT), 'name' => fake()->country(), 'flag' => fake()->emoji(), + 'region_id' => null, ]; } } diff --git a/database/factories/CountrySpecialRegionFactory.php b/database/factories/CountrySpecialRegionFactory.php new file mode 100644 index 0000000..2299470 --- /dev/null +++ b/database/factories/CountrySpecialRegionFactory.php @@ -0,0 +1,72 @@ + + */ + public function definition(): array + { + return [ + 'country_id' => Country::factory(), + 'region_id' => Region::factory()->special(), + 'start_date' => $this->faker->dateTimeBetween('-2 years', 'now')->format('Y-m-d'), + 'end_date' => $this->faker->optional(0.3)->dateTimeBetween('now', '+2 years')?->format('Y-m-d'), + ]; + } + + /** + * Set the region as active. + * + * @return $this + */ + public function active(): static + { + return $this->state([ + 'start_date' => $this->faker->dateTimeBetween('-1 year', 'now')->format('Y-m-d'), + 'end_date' => null, + ]); + } + + /** + * Set the region as ended. + * + * @return $this + */ + public function ended(): static + { + return $this->state([ + 'start_date' => $this->faker->dateTimeBetween('-2 years', '-6 months')->format('Y-m-d'), + 'end_date' => $this->faker->dateTimeBetween('-6 months', 'now')->format('Y-m-d'), + ]); + } + + /** + * Set the region as future. + * + * @return $this + */ + public function future(): static + { + return $this->state([ + 'start_date' => $this->faker->dateTimeBetween('now', '+6 months')->format('Y-m-d'), + 'end_date' => $this->faker->optional(0.5)->dateTimeBetween('+6 months', '+2 years')?->format('Y-m-d'), + ]); + } +} diff --git a/database/factories/RegionFactory.php b/database/factories/RegionFactory.php new file mode 100644 index 0000000..2dc0007 --- /dev/null +++ b/database/factories/RegionFactory.php @@ -0,0 +1,60 @@ + + */ + public function definition(): array + { + return [ + 'code' => strtoupper($this->faker->unique()->lexify('???')), + 'name' => $this->faker->unique()->words(2, true), + 'is_special' => false, + ]; + } + + /** + * Set the region as special. + * + * @return $this + */ + public function special(): static + { + return $this->state(['is_special' => true]); + } + + /** + * Set the region as geographical. + * + * @return $this + */ + public function geographical(): static + { + return $this->state(['is_special' => false]); + } + + /** + * Set the region as a child of a parent region. + * + * @return $this + */ + public function withParent(Region $parent): static + { + return $this->state(['parent_id' => $parent->id]); + } +} diff --git a/database/migrations/2025_07_26_120000_create_regions_table.php b/database/migrations/2025_07_26_120000_create_regions_table.php new file mode 100644 index 0000000..e94705a --- /dev/null +++ b/database/migrations/2025_07_26_120000_create_regions_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('code')->nullable()->unique(); + $table->unsignedBigInteger('parent_id')->nullable(); + $table->boolean('is_special')->default(false); + $table->string('name'); + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('parent_id')->references('id')->on('world_regions')->onDelete('cascade'); + $table->index(['is_special', 'parent_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('world_regions'); + } +}; diff --git a/database/migrations/2025_07_26_120001_add_region_id_to_countries_table.php b/database/migrations/2025_07_26_120001_add_region_id_to_countries_table.php new file mode 100644 index 0000000..8051d38 --- /dev/null +++ b/database/migrations/2025_07_26_120001_add_region_id_to_countries_table.php @@ -0,0 +1,26 @@ +unsignedBigInteger('region_id')->nullable()->after('flag'); + $table->foreign('region_id')->references('id')->on('world_regions')->onDelete('set null'); + $table->index('region_id'); + }); + } + + public function down(): void + { + Schema::table('world_countries', function (Blueprint $table) { + $table->dropForeign(['region_id']); + $table->dropIndex(['region_id']); + $table->dropColumn('region_id'); + }); + } +}; diff --git a/database/migrations/2025_07_26_120002_create_country_in_special_region_table.php b/database/migrations/2025_07_26_120002_create_country_in_special_region_table.php new file mode 100644 index 0000000..a411eaa --- /dev/null +++ b/database/migrations/2025_07_26_120002_create_country_in_special_region_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('country_id', 2); + $table->unsignedBigInteger('region_id'); + $table->date('start_date'); + $table->date('end_date')->nullable(); + $table->timestamps(); + + $table->foreign('country_id')->references('id')->on('world_countries')->onDelete('cascade'); + $table->foreign('region_id')->references('id')->on('world_regions')->onDelete('cascade'); + + $table->unique(['country_id', 'region_id', 'start_date'], 'unique_country_region_start'); + $table->index(['region_id', 'start_date', 'end_date'], 'idx_region_dates'); + }); + } + + public function down(): void + { + Schema::dropIfExists('world_country_in_special_region'); + } +}; diff --git a/database/migrations/2025_07_26_120003_seed_eu_region.php b/database/migrations/2025_07_26_120003_seed_eu_region.php new file mode 100644 index 0000000..6be298b --- /dev/null +++ b/database/migrations/2025_07_26_120003_seed_eu_region.php @@ -0,0 +1,41 @@ + 'EU', + 'name' => 'European Union', + 'is_special' => true, + ]); + + $euMemberCountries = [ + 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', + 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', + 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', + ]; + + $existingCountries = Country::whereIn('id', $euMemberCountries)->pluck('id'); + $membershipData = $existingCountries->mapWithKeys(fn ($countryId) => [ + $countryId => [ + 'start_date' => Carbon::now()->toDateString(), + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + + $euRegion->specialCountries()->attach($membershipData); + } + + public function down(): void + { + Region::where('code', 'EU')->delete(); + } +}; diff --git a/resources/lang/en/countries.php b/resources/lang/en/countries.php index 0260dc4..41753aa 100644 --- a/resources/lang/en/countries.php +++ b/resources/lang/en/countries.php @@ -22,6 +22,12 @@ 'num_code' => [ 'label' => 'Num. Code', ], + 'region' => [ + 'label' => 'Region', + ], + 'special_regions' => [ + 'label' => 'Special Regions', + ], ], 'actions' => [ @@ -67,6 +73,10 @@ 'label' => 'Numeric code', 'helper' => 'Numeric code (ISO-3166)', ], + 'region' => [ + 'label' => 'Geographical Region', + 'helper' => 'The geographical region this country belongs to', + ], ], 'import' => [ @@ -86,4 +96,10 @@ 'message' => 'Failed to import countries data.', ], ], + + 'filters' => [ + 'region' => [ + 'label' => 'Region', + ], + ], ]; diff --git a/resources/lang/en/regions.php b/resources/lang/en/regions.php new file mode 100644 index 0000000..bfd42e2 --- /dev/null +++ b/resources/lang/en/regions.php @@ -0,0 +1,81 @@ + 'Regions', + 'breadcrumb' => 'Regions', + 'plural' => 'Regions', + + 'form' => [ + 'name' => [ + 'label' => 'Name', + ], + 'code' => [ + 'label' => 'Code', + 'helper' => 'Optional unique code for the region (e.g., EU, ASEAN)', + ], + 'parent' => [ + 'label' => 'Parent Region', + 'helper' => 'Select a parent region to create a sub-region', + ], + 'is_special' => [ + 'label' => 'Special Region', + 'helper' => 'Special regions are custom regions like EU or EEA that countries can join/leave', + ], + ], + + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'code' => [ + 'label' => 'Code', + ], + 'parent' => [ + 'label' => 'Parent Region', + ], + 'is_special' => [ + 'label' => 'Special', + ], + 'countries_count' => [ + 'label' => 'Countries', + ], + 'children_count' => [ + 'label' => 'Sub-regions', + ], + ], + + 'filters' => [ + 'type' => [ + 'label' => 'Type', + 'geographical' => 'Geographical', + 'special' => 'Special', + ], + 'parent' => [ + 'label' => 'Parent Region', + ], + ], + + 'actions' => [ + 'create' => [ + 'label' => 'New Region', + 'heading' => 'Create Region', + ], + 'edit' => [ + 'label' => 'Edit', + 'heading' => 'Edit Region', + ], + 'delete' => [ + 'label' => 'Delete', + 'heading' => 'Delete Region', + ], + 'restore' => [ + 'label' => 'Restore', + 'heading' => 'Restore Region', + ], + 'force_delete' => [ + 'label' => 'Force Delete', + 'heading' => 'Force Delete Region', + 'description' => 'Are you sure you want to permanently delete ":name"? This action cannot be undone.', + ], + ], +]; diff --git a/resources/lang/en/special-memberships.php b/resources/lang/en/special-memberships.php new file mode 100644 index 0000000..8efc123 --- /dev/null +++ b/resources/lang/en/special-memberships.php @@ -0,0 +1,74 @@ + 'Special Region Memberships', + 'breadcrumb' => 'Special Region Memberships', + 'plural' => 'Special Region Memberships', + + 'form' => [ + 'region' => [ + 'label' => 'Special Region', + ], + 'country' => [ + 'label' => 'Country', + ], + 'start_date' => [ + 'label' => 'Start Date', + ], + 'end_date' => [ + 'label' => 'End Date', + ], + ], + + 'table' => [ + 'region' => [ + 'label' => 'Region', + ], + 'flag' => [ + 'label' => 'Flag', + ], + 'country' => [ + 'label' => 'Country', + ], + 'start_date' => [ + 'label' => 'Start Date', + ], + 'end_date' => [ + 'label' => 'End Date', + ], + 'status' => [ + 'label' => 'Status', + ], + ], + + 'filters' => [ + 'region' => [ + 'label' => 'Region', + ], + 'status' => [ + 'label' => 'Status', + ], + ], + + 'status' => [ + 'active' => 'Active', + 'future' => 'Future', + 'ended' => 'Ended', + ], + + 'actions' => [ + 'create' => [ + 'label' => 'New Membership', + 'heading' => 'Create Special Region Membership', + 'success' => 'Special region membership created successfully.', + ], + 'edit' => [ + 'label' => 'Edit', + 'heading' => 'Edit Special Region Membership', + ], + 'delete' => [ + 'label' => 'Delete', + 'heading' => 'Delete Special Region Membership', + ], + ], +]; diff --git a/resources/lang/hr/countries.php b/resources/lang/hr/countries.php index e568c0f..93a3bf7 100644 --- a/resources/lang/hr/countries.php +++ b/resources/lang/hr/countries.php @@ -22,6 +22,12 @@ 'num_code' => [ 'label' => 'Brojčana šifra', ], + 'region' => [ + 'label' => 'Regija', + ], + 'special_regions' => [ + 'label' => 'Posebne regije', + ], ], 'actions' => [ @@ -67,6 +73,10 @@ 'label' => 'Brojčana šifra', 'helper' => 'Numerička oznaka (ISO-3166)', ], + 'region' => [ + 'label' => 'Geografska regija', + 'helper' => 'Geografska regija kojoj ova zemlja pripada', + ], ], 'import' => [ @@ -86,4 +96,10 @@ 'message' => 'Neuspješan uvoz podataka država.', ], ], + + 'filters' => [ + 'region' => [ + 'label' => 'Regija', + ], + ], ]; diff --git a/resources/lang/hr/regions.php b/resources/lang/hr/regions.php new file mode 100644 index 0000000..b231faf --- /dev/null +++ b/resources/lang/hr/regions.php @@ -0,0 +1,81 @@ + 'Regije', + 'breadcrumb' => 'Regije', + 'plural' => 'Regije', + + 'form' => [ + 'name' => [ + 'label' => 'Naziv', + ], + 'code' => [ + 'label' => 'Kod', + 'helper' => 'Opcionalni jedinstveni kod za regiju (npr. EU, ASEAN)', + ], + 'parent' => [ + 'label' => 'Nadređena regija', + 'helper' => 'Odaberite nadređenu regiju za stvaranje podregije', + ], + 'is_special' => [ + 'label' => 'Posebna regija', + 'helper' => 'Posebne regije su prilagođene regije poput EU ili EEA kojima se zemlje mogu pridružiti/napustiti', + ], + ], + + 'table' => [ + 'name' => [ + 'label' => 'Naziv', + ], + 'code' => [ + 'label' => 'Kod', + ], + 'parent' => [ + 'label' => 'Nadređena regija', + ], + 'is_special' => [ + 'label' => 'Posebna', + ], + 'countries_count' => [ + 'label' => 'Zemlje', + ], + 'children_count' => [ + 'label' => 'Podregije', + ], + ], + + 'filters' => [ + 'type' => [ + 'label' => 'Tip', + 'geographical' => 'Geografska', + 'special' => 'Posebna', + ], + 'parent' => [ + 'label' => 'Nadređena regija', + ], + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Nova regija', + 'heading' => 'Stvori regiju', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Uredi regiju', + ], + 'delete' => [ + 'label' => 'Obriši', + 'heading' => 'Obriši regiju', + ], + 'restore' => [ + 'label' => 'Vrati', + 'heading' => 'Vrati regiju', + ], + 'force_delete' => [ + 'label' => 'Trajno obriši', + 'heading' => 'Trajno obriši regiju', + 'description' => 'Jeste li sigurni da želite trajno obrisati ":name"? Ova radnja se ne može poništiti.', + ], + ], +]; diff --git a/resources/lang/hr/special-memberships.php b/resources/lang/hr/special-memberships.php new file mode 100644 index 0000000..d9ed2ec --- /dev/null +++ b/resources/lang/hr/special-memberships.php @@ -0,0 +1,74 @@ + 'Članstva u posebnim regijama', + 'breadcrumb' => 'Članstva u posebnim regijama', + 'plural' => 'Članstva u posebnim regijama', + + 'form' => [ + 'region' => [ + 'label' => 'Posebna regija', + ], + 'country' => [ + 'label' => 'Zemlja', + ], + 'start_date' => [ + 'label' => 'Datum početka', + ], + 'end_date' => [ + 'label' => 'Datum završetka', + ], + ], + + 'table' => [ + 'region' => [ + 'label' => 'Regija', + ], + 'flag' => [ + 'label' => 'Zastava', + ], + 'country' => [ + 'label' => 'Zemlja', + ], + 'start_date' => [ + 'label' => 'Datum početka', + ], + 'end_date' => [ + 'label' => 'Datum završetka', + ], + 'status' => [ + 'label' => 'Status', + ], + ], + + 'filters' => [ + 'region' => [ + 'label' => 'Regija', + ], + 'status' => [ + 'label' => 'Status', + ], + ], + + 'status' => [ + 'active' => 'Aktivno', + 'future' => 'Buduće', + 'ended' => 'Završeno', + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Novo članstvo', + 'heading' => 'Stvori članstvo u posebnoj regiji', + 'success' => 'Članstvo u posebnoj regiji uspješno stvoreno.', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Uredi članstvo u posebnoj regiji', + ], + 'delete' => [ + 'label' => 'Obriši', + 'heading' => 'Obriši članstvo u posebnoj regiji', + ], + ], +]; diff --git a/resources/lang/sl/countries.php b/resources/lang/sl/countries.php index 90fb45f..b0db2c7 100644 --- a/resources/lang/sl/countries.php +++ b/resources/lang/sl/countries.php @@ -22,6 +22,12 @@ 'num_code' => [ 'label' => 'Num. šifra', ], + 'region' => [ + 'label' => 'Regija', + ], + 'special_regions' => [ + 'label' => 'Posebne regije', + ], ], 'actions' => [ @@ -67,6 +73,10 @@ 'label' => 'Num. šifra', 'helper' => 'Numerična šifra (ISO-3166)', ], + 'region' => [ + 'label' => 'Geografska regija', + 'helper' => 'Geografska regija, ki ji ta država pripada', + ], ], 'import' => [ @@ -86,4 +96,10 @@ 'message' => 'Uvoz podatkov držav ni uspel.', ], ], + + 'filters' => [ + 'region' => [ + 'label' => 'Regija', + ], + ], ]; diff --git a/resources/lang/sl/regions.php b/resources/lang/sl/regions.php new file mode 100644 index 0000000..3bbe8c5 --- /dev/null +++ b/resources/lang/sl/regions.php @@ -0,0 +1,81 @@ + 'Regije', + 'breadcrumb' => 'Regije', + 'plural' => 'Regije', + + 'form' => [ + 'name' => [ + 'label' => 'Ime', + ], + 'code' => [ + 'label' => 'Koda', + 'helper' => 'Neobvezna edinstvena koda za regijo (npr. EU, ASEAN)', + ], + 'parent' => [ + 'label' => 'Nadrejena regija', + 'helper' => 'Izberite nadrejeno regijo za ustvarjanje podregije', + ], + 'is_special' => [ + 'label' => 'Posebna regija', + 'helper' => 'Posebne regije so prilagojene regije kot EU ali EEA, katerim se lahko države pridružijo/zapustijo', + ], + ], + + 'table' => [ + 'name' => [ + 'label' => 'Ime', + ], + 'code' => [ + 'label' => 'Koda', + ], + 'parent' => [ + 'label' => 'Nadrejena regija', + ], + 'is_special' => [ + 'label' => 'Posebna', + ], + 'countries_count' => [ + 'label' => 'Države', + ], + 'children_count' => [ + 'label' => 'Podregije', + ], + ], + + 'filters' => [ + 'type' => [ + 'label' => 'Tip', + 'geographical' => 'Geografska', + 'special' => 'Posebna', + ], + 'parent' => [ + 'label' => 'Nadrejena regija', + ], + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Nova regija', + 'heading' => 'Ustvari regijo', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Uredi regijo', + ], + 'delete' => [ + 'label' => 'Izbriši', + 'heading' => 'Izbriši regijo', + ], + 'restore' => [ + 'label' => 'Obnovi', + 'heading' => 'Obnovi regijo', + ], + 'force_delete' => [ + 'label' => 'Trajno izbriši', + 'heading' => 'Trajno izbriši regijo', + 'description' => 'Ali ste prepričani, da želite trajno izbrisati ":name"? Tega dejanja ni mogoče razveljaviti.', + ], + ], +]; diff --git a/resources/lang/sl/special-memberships.php b/resources/lang/sl/special-memberships.php new file mode 100644 index 0000000..9d4b81b --- /dev/null +++ b/resources/lang/sl/special-memberships.php @@ -0,0 +1,74 @@ + 'Članstva v posebnih regijah', + 'breadcrumb' => 'Članstva v posebnih regijah', + 'plural' => 'Članstva v posebnih regijah', + + 'form' => [ + 'region' => [ + 'label' => 'Posebna regija', + ], + 'country' => [ + 'label' => 'Država', + ], + 'start_date' => [ + 'label' => 'Datum začetka', + ], + 'end_date' => [ + 'label' => 'Datum konca', + ], + ], + + 'table' => [ + 'region' => [ + 'label' => 'Regija', + ], + 'flag' => [ + 'label' => 'Zastava', + ], + 'country' => [ + 'label' => 'Država', + ], + 'start_date' => [ + 'label' => 'Datum začetka', + ], + 'end_date' => [ + 'label' => 'Datum konca', + ], + 'status' => [ + 'label' => 'Status', + ], + ], + + 'filters' => [ + 'region' => [ + 'label' => 'Regija', + ], + 'status' => [ + 'label' => 'Status', + ], + ], + + 'status' => [ + 'active' => 'Aktivno', + 'future' => 'Prihodnje', + 'ended' => 'Končano', + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Novo članstvo', + 'heading' => 'Ustvari članstvo v posebni regiji', + 'success' => 'Članstvo v posebni regiji uspešno ustvarjeno.', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Uredi članstvo v posebni regiji', + ], + 'delete' => [ + 'label' => 'Izbriši', + 'heading' => 'Izbriši članstvo v posebni regiji', + ], + ], +]; diff --git a/resources/lang/sr/countries.php b/resources/lang/sr/countries.php index 45247df..7dc1ca2 100644 --- a/resources/lang/sr/countries.php +++ b/resources/lang/sr/countries.php @@ -22,6 +22,12 @@ 'num_code' => [ 'label' => 'Brojčana šifra', ], + 'region' => [ + 'label' => 'Regija', + ], + 'special_regions' => [ + 'label' => 'Posebne regije', + ], ], 'actions' => [ @@ -67,6 +73,10 @@ 'label' => 'Brojčana šifra', 'helper' => 'Numerička oznaka (ISO-3166)', ], + 'region' => [ + 'label' => 'Geografska regija', + 'helper' => 'Geografska regija kojoj ova zemlja pripada', + ], ], 'import' => [ @@ -86,4 +96,10 @@ 'message' => 'Neuspešan uvoz podataka država.', ], ], + + 'filters' => [ + 'region' => [ + 'label' => 'Regija', + ], + ], ]; diff --git a/resources/lang/sr/regions.php b/resources/lang/sr/regions.php new file mode 100644 index 0000000..da8e082 --- /dev/null +++ b/resources/lang/sr/regions.php @@ -0,0 +1,81 @@ + 'Regije', + 'breadcrumb' => 'Regije', + 'plural' => 'Regije', + + 'form' => [ + 'name' => [ + 'label' => 'Naziv', + ], + 'code' => [ + 'label' => 'Kod', + 'helper' => 'Opcioni jedinstveni kod za regiju (npr. EU, ASEAN)', + ], + 'parent' => [ + 'label' => 'Nadređena regija', + 'helper' => 'Izaberite nadređenu regiju za stvaranje podregije', + ], + 'is_special' => [ + 'label' => 'Posebna regija', + 'helper' => 'Posebne regije su prilagođene regije poput EU ili EEA kojima se zemlje mogu pridružiti/napustiti', + ], + ], + + 'table' => [ + 'name' => [ + 'label' => 'Naziv', + ], + 'code' => [ + 'label' => 'Kod', + ], + 'parent' => [ + 'label' => 'Nadređena regija', + ], + 'is_special' => [ + 'label' => 'Posebna', + ], + 'countries_count' => [ + 'label' => 'Zemlje', + ], + 'children_count' => [ + 'label' => 'Podregije', + ], + ], + + 'filters' => [ + 'type' => [ + 'label' => 'Tip', + 'geographical' => 'Geografska', + 'special' => 'Posebna', + ], + 'parent' => [ + 'label' => 'Nadređena regija', + ], + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Nova regija', + 'heading' => 'Stvori regiju', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Uredi regiju', + ], + 'delete' => [ + 'label' => 'Obriši', + 'heading' => 'Obriši regiju', + ], + 'restore' => [ + 'label' => 'Vrati', + 'heading' => 'Vrati regiju', + ], + 'force_delete' => [ + 'label' => 'Trajno obriši', + 'heading' => 'Trajno obriši regiju', + 'description' => 'Jeste li sigurni da želite trajno obrisati ":name"? Ova radnja se ne može poništiti.', + ], + ], +]; diff --git a/resources/lang/sr/special-memberships.php b/resources/lang/sr/special-memberships.php new file mode 100644 index 0000000..b8846b2 --- /dev/null +++ b/resources/lang/sr/special-memberships.php @@ -0,0 +1,74 @@ + 'Članstva u posebnim regijama', + 'breadcrumb' => 'Članstva u posebnim regijama', + 'plural' => 'Članstva u posebnim regijama', + + 'form' => [ + 'region' => [ + 'label' => 'Posebna regija', + ], + 'country' => [ + 'label' => 'Zemlja', + ], + 'start_date' => [ + 'label' => 'Datum početka', + ], + 'end_date' => [ + 'label' => 'Datum završetka', + ], + ], + + 'table' => [ + 'region' => [ + 'label' => 'Regija', + ], + 'flag' => [ + 'label' => 'Zastava', + ], + 'country' => [ + 'label' => 'Zemlja', + ], + 'start_date' => [ + 'label' => 'Datum početka', + ], + 'end_date' => [ + 'label' => 'Datum završetka', + ], + 'status' => [ + 'label' => 'Status', + ], + ], + + 'filters' => [ + 'region' => [ + 'label' => 'Regija', + ], + 'status' => [ + 'label' => 'Status', + ], + ], + + 'status' => [ + 'active' => 'Aktivno', + 'future' => 'Buduće', + 'ended' => 'Završeno', + ], + + 'actions' => [ + 'create' => [ + 'label' => 'Novo članstvo', + 'heading' => 'Stvori članstvo u posebnoj regiji', + 'success' => 'Članstvo u posebnoj regiji uspešno stvoreno.', + ], + 'edit' => [ + 'label' => 'Uredi', + 'heading' => 'Uredi članstvo u posebnoj regiji', + ], + 'delete' => [ + 'label' => 'Obriši', + 'heading' => 'Obriši članstvo u posebnoj regiji', + ], + ], +]; diff --git a/src/Filament/Clusters/World/Resources/CountryResource.php b/src/Filament/Clusters/World/Resources/CountryResource.php index f1c01fd..f07e724 100644 --- a/src/Filament/Clusters/World/Resources/CountryResource.php +++ b/src/Filament/Clusters/World/Resources/CountryResource.php @@ -6,6 +6,7 @@ use Eclipse\World\Filament\Clusters\World; use Eclipse\World\Filament\Clusters\World\Resources\CountryResource\Pages; use Eclipse\World\Models\Country; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Resource; @@ -19,6 +20,7 @@ use Filament\Tables\Actions\RestoreAction; use Filament\Tables\Actions\RestoreBulkAction; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\TrashedFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; @@ -70,6 +72,13 @@ public static function form(Form $form): Form ->length(3) ->label(__('eclipse-world::countries.form.num_code.label')) ->helperText(__('eclipse-world::countries.form.num_code.helper')), + + Select::make('region_id') + ->label(__('eclipse-world::countries.form.region.label')) + ->relationship('region', 'name', fn ($query) => $query->where('is_special', false)) + ->searchable() + ->preload() + ->helperText(__('eclipse-world::countries.form.region.helper')), ]); } @@ -106,8 +115,25 @@ public static function table(Table $table): Table ->searchable() ->sortable() ->width(100), + + TextColumn::make('region.name') + ->label(__('eclipse-world::countries.table.region.label')) + ->searchable() + ->sortable() + ->placeholder('—'), + + TextColumn::make('special_regions') + ->label(__('eclipse-world::countries.table.special_regions.label')) + ->getStateUsing(fn ($record) => $record->getSpecialRegionsAt()->pluck('name')->join(', ')) + ->placeholder('—') + ->wrap(), ]) ->filters([ + SelectFilter::make('region_id') + ->label(__('eclipse-world::countries.filters.region.label')) + ->relationship('region', 'name', fn ($query) => $query->where('is_special', false)) + ->searchable() + ->preload(), TrashedFilter::make(), ]) ->actions([ diff --git a/src/Filament/Clusters/World/Resources/RegionResource.php b/src/Filament/Clusters/World/Resources/RegionResource.php new file mode 100644 index 0000000..386b44f --- /dev/null +++ b/src/Filament/Clusters/World/Resources/RegionResource.php @@ -0,0 +1,197 @@ +schema([ + TextInput::make('name') + ->label(__('eclipse-world::regions.form.name.label')) + ->required() + ->maxLength(255), + + TextInput::make('code') + ->label(__('eclipse-world::regions.form.code.label')) + ->maxLength(10) + ->unique(table: Region::class, ignoreRecord: true) + ->helperText(__('eclipse-world::regions.form.code.helper')), + + Select::make('parent_id') + ->label(__('eclipse-world::regions.form.parent.label')) + ->relationship('parent', 'name') + ->searchable() + ->preload() + ->helperText(__('eclipse-world::regions.form.parent.helper')), + + Checkbox::make('is_special') + ->label(__('eclipse-world::regions.form.is_special.label')) + ->helperText(__('eclipse-world::regions.form.is_special.helper')), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->defaultPaginationPageOption(50) + ->defaultSort('name') + ->striped() + ->columns([ + TextColumn::make('name') + ->label(__('eclipse-world::regions.table.name.label')) + ->searchable() + ->sortable(), + + TextColumn::make('code') + ->label(__('eclipse-world::regions.table.code.label')) + ->searchable() + ->sortable() + ->placeholder('—'), + + TextColumn::make('parent.name') + ->label(__('eclipse-world::regions.table.parent.label')) + ->searchable() + ->sortable() + ->placeholder('—'), + + IconColumn::make('is_special') + ->label(__('eclipse-world::regions.table.is_special.label')) + ->boolean() + ->sortable(), + + TextColumn::make('countries_count') + ->label(__('eclipse-world::regions.table.countries_count.label')) + ->counts('countries') + ->sortable(), + + TextColumn::make('children_count') + ->label(__('eclipse-world::regions.table.children_count.label')) + ->counts('children') + ->sortable(), + ]) + ->filters([ + SelectFilter::make('is_special') + ->label(__('eclipse-world::regions.filters.type.label')) + ->options([ + 0 => __('eclipse-world::regions.filters.type.geographical'), + 1 => __('eclipse-world::regions.filters.type.special'), + ]), + SelectFilter::make('parent_id') + ->label(__('eclipse-world::regions.filters.parent.label')) + ->relationship('parent', 'name') + ->searchable() + ->preload(), + TrashedFilter::make(), + ]) + ->actions([ + EditAction::make() + ->label(__('eclipse-world::regions.actions.edit.label')) + ->modalHeading(__('eclipse-world::regions.actions.edit.heading')), + ActionGroup::make([ + DeleteAction::make() + ->label(__('eclipse-world::regions.actions.delete.label')) + ->modalHeading(__('eclipse-world::regions.actions.delete.heading')), + RestoreAction::make() + ->label(__('eclipse-world::regions.actions.restore.label')) + ->modalHeading(__('eclipse-world::regions.actions.restore.heading')), + ForceDeleteAction::make() + ->label(__('eclipse-world::regions.actions.force_delete.label')) + ->modalHeading(__('eclipse-world::regions.actions.force_delete.heading')) + ->modalDescription(fn (Region $record): string => __('eclipse-world::regions.actions.force_delete.description', [ + 'name' => $record->name, + ])), + ]), + ]) + ->bulkActions([ + BulkActionGroup::make([ + DeleteBulkAction::make() + ->label(__('eclipse-world::regions.actions.delete.label')), + RestoreBulkAction::make() + ->label(__('eclipse-world::regions.actions.restore.label')), + ForceDeleteBulkAction::make() + ->label(__('eclipse-world::regions.actions.force_delete.label')), + ]), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListRegions::route('/'), + ]; + } + + 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', + ]; + } + + public static function getNavigationLabel(): string + { + return __('eclipse-world::regions.nav_label'); + } + + public static function getBreadcrumb(): string + { + return __('eclipse-world::regions.breadcrumb'); + } + + public static function getPluralModelLabel(): string + { + return __('eclipse-world::regions.plural'); + } +} diff --git a/src/Filament/Clusters/World/Resources/RegionResource/Pages/ListRegions.php b/src/Filament/Clusters/World/Resources/RegionResource/Pages/ListRegions.php new file mode 100644 index 0000000..5be01a4 --- /dev/null +++ b/src/Filament/Clusters/World/Resources/RegionResource/Pages/ListRegions.php @@ -0,0 +1,21 @@ +label(__('eclipse-world::regions.actions.create.label')) + ->modalHeading(__('eclipse-world::regions.actions.create.heading')), + ]; + } +} diff --git a/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource.php b/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource.php new file mode 100644 index 0000000..06d37d4 --- /dev/null +++ b/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource.php @@ -0,0 +1,203 @@ +with(['country', 'region']); + } + + public static function form(Form $form): Form + { + return $form + ->schema([ + Select::make('region_id') + ->label(__('eclipse-world::special-memberships.form.region.label')) + ->options(Region::where('is_special', true)->pluck('name', 'id')) + ->required() + ->searchable() + ->preload(), + + Select::make('country_id') + ->label(__('eclipse-world::special-memberships.form.country.label')) + ->options(Country::orderBy('name')->pluck('name', 'id')) + ->required() + ->searchable() + ->preload(), + + DatePicker::make('start_date') + ->label(__('eclipse-world::special-memberships.form.start_date.label')) + ->required() + ->default(now()), + + DatePicker::make('end_date') + ->label(__('eclipse-world::special-memberships.form.end_date.label')) + ->after('start_date'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->defaultPaginationPageOption(50) + ->defaultSort('start_date', 'desc') + ->striped() + ->columns([ + TextColumn::make('region.name') + ->label(__('eclipse-world::special-memberships.table.region.label')) + ->searchable() + ->sortable(), + + TextColumn::make('country.flag') + ->label(__('eclipse-world::special-memberships.table.flag.label')) + ->width(60), + + TextColumn::make('country.name') + ->label(__('eclipse-world::special-memberships.table.country.label')) + ->searchable() + ->sortable(), + + TextColumn::make('start_date') + ->label(__('eclipse-world::special-memberships.table.start_date.label')) + ->date() + ->sortable(), + + TextColumn::make('end_date') + ->label(__('eclipse-world::special-memberships.table.end_date.label')) + ->date() + ->sortable() + ->placeholder('—'), + + TextColumn::make('status') + ->label(__('eclipse-world::special-memberships.table.status.label')) + ->badge() + ->getStateUsing(function (CountrySpecialRegion $record) { + $today = now()->toDateString(); + + return match (true) { + $record->start_date > $today => 'future', + $record->end_date && $record->end_date < $today => 'ended', + default => 'active' + }; + }) + ->color(fn (string $state): string => match ($state) { + 'active' => 'success', + 'future' => 'warning', + 'ended' => 'danger', + }) + ->formatStateUsing(fn (string $state): string => match ($state) { + 'active' => __('eclipse-world::special-memberships.status.active'), + 'future' => __('eclipse-world::special-memberships.status.future'), + 'ended' => __('eclipse-world::special-memberships.status.ended'), + }), + ]) + ->filters([ + SelectFilter::make('region_id') + ->label(__('eclipse-world::special-memberships.filters.region.label')) + ->options(Region::where('is_special', true)->pluck('name', 'id')) + ->searchable() + ->preload(), + SelectFilter::make('status') + ->label(__('eclipse-world::special-memberships.filters.status.label')) + ->options([ + 'active' => __('eclipse-world::special-memberships.status.active'), + 'future' => __('eclipse-world::special-memberships.status.future'), + 'ended' => __('eclipse-world::special-memberships.status.ended'), + ]) + ->query(function (Builder $query, array $data): Builder { + if (! $data['value']) { + return $query; + } + + $today = now()->toDateString(); + + return match ($data['value']) { + 'active' => $query->where('start_date', '<=', $today) + ->where(fn ($q) => $q->whereNull('end_date')->orWhere('end_date', '>=', $today)), + 'future' => $query->where('start_date', '>', $today), + 'ended' => $query->where('end_date', '<', $today), + default => $query, + }; + }), + ]) + ->actions([ + EditAction::make() + ->label(__('eclipse-world::special-memberships.actions.edit.label')) + ->modalHeading(__('eclipse-world::special-memberships.actions.edit.heading')), + ActionGroup::make([ + DeleteAction::make() + ->label(__('eclipse-world::special-memberships.actions.delete.label')) + ->modalHeading(__('eclipse-world::special-memberships.actions.delete.heading')), + ]), + ]) + ->bulkActions([ + BulkActionGroup::make([ + DeleteBulkAction::make() + ->label(__('eclipse-world::special-memberships.actions.delete.label')), + ]), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListSpecialRegionMemberships::route('/'), + ]; + } + + public static function getPermissionPrefixes(): array + { + return [ + 'view_any', + 'create', + 'update', + 'delete', + 'delete_any', + ]; + } + + public static function getNavigationLabel(): string + { + return __('eclipse-world::special-memberships.nav_label'); + } + + public static function getBreadcrumb(): string + { + return __('eclipse-world::special-memberships.breadcrumb'); + } + + public static function getPluralModelLabel(): string + { + return __('eclipse-world::special-memberships.plural'); + } +} diff --git a/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource/Pages/ListSpecialRegionMemberships.php b/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource/Pages/ListSpecialRegionMemberships.php new file mode 100644 index 0000000..34c1eaa --- /dev/null +++ b/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource/Pages/ListSpecialRegionMemberships.php @@ -0,0 +1,26 @@ +label(__('eclipse-world::special-memberships.actions.create.label')) + ->modalHeading(__('eclipse-world::special-memberships.actions.create.heading')) + ->using(function (array $data) { + return CountrySpecialRegion::create($data); + }) + ->successNotificationTitle(__('eclipse-world::special-memberships.actions.create.success')), + ]; + } +} diff --git a/src/Jobs/ImportCountries.php b/src/Jobs/ImportCountries.php index e34e2f2..bbe7839 100644 --- a/src/Jobs/ImportCountries.php +++ b/src/Jobs/ImportCountries.php @@ -4,6 +4,7 @@ use Eclipse\Core\Models\User; use Eclipse\World\Models\Country; +use Eclipse\World\Models\Region; use Eclipse\World\Notifications\ImportFinishedNotification; use Exception; use Illuminate\Bus\Queueable; @@ -41,6 +42,9 @@ public function handle(): void $user = $this->userId ? User::find($this->userId) : null; try { + // First, import/update regions + $this->importRegions(); + // Load existing countries into an associative array $existingCountries = Country::withTrashed()->get()->keyBy('id'); @@ -62,6 +66,7 @@ public function handle(): void 'num_code' => $rawData['ccn3'], 'name' => $rawData['name']['common'], 'flag' => $rawData['flag'], + 'region_id' => $this->getRegionIdForCountry($rawData), ]; if (isset($existingCountries[$data['id']])) { @@ -83,4 +88,74 @@ public function handle(): void throw $e; } } + + private function importRegions(): void + { + $geographicalRegions = $this->getGeographicalRegionsStructure(); + + foreach ($geographicalRegions as $parentName => $subRegions) { + $parent = Region::updateOrCreate( + ['name' => $parentName, 'is_special' => false], + ['code' => null, 'parent_id' => null] + ); + + foreach ($subRegions as $subRegionName) { + Region::updateOrCreate( + ['name' => $subRegionName, 'is_special' => false], + ['code' => null, 'parent_id' => $parent->id] + ); + } + } + } + + private function getGeographicalRegionsStructure(): array + { + return [ + 'Africa' => [ + 'Eastern Africa', + 'Middle Africa', + 'Northern Africa', + 'Southern Africa', + 'Western Africa', + ], + 'Americas' => [ + 'Caribbean', + 'Central America', + 'North America', + 'South America', + ], + 'Asia' => [ + 'Central Asia', + 'Eastern Asia', + 'South-Eastern Asia', + 'Southern Asia', + 'Western Asia', + ], + 'Europe' => [ + 'Central Europe', + 'Eastern Europe', + 'Northern Europe', + 'Southeast Europe', + 'Southern Europe', + 'Western Europe', + ], + 'Oceania' => [ + 'Australia and New Zealand', + 'Melanesia', + 'Micronesia', + 'Polynesia', + ], + ]; + } + + private function getRegionIdForCountry(array $countryData): ?int + { + if (! isset($countryData['subregion'])) { + return null; + } + + return Region::where('name', $countryData['subregion']) + ->where('is_special', false) + ->value('id'); + } } diff --git a/src/Models/Country.php b/src/Models/Country.php index 45fb04e..93d1c83 100644 --- a/src/Models/Country.php +++ b/src/Models/Country.php @@ -3,9 +3,13 @@ namespace Eclipse\World\Models; use Eclipse\World\Factories\CountryFactory; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; class Country extends Model { @@ -23,8 +27,58 @@ class Country extends Model 'num_code', 'name', 'flag', + 'region_id', ]; + /** + * Get the region that the country belongs to. + */ + public function region(): BelongsTo + { + return $this->belongsTo(Region::class, 'region_id'); + } + + /** + * Get the special regions that the country belongs to. + */ + public function specialRegions(): BelongsToMany + { + return $this->belongsToMany(Region::class, 'world_country_in_special_region', 'country_id', 'region_id') + ->withPivot(['start_date', 'end_date']) + ->withTimestamps(); + } + + /** + * Check if the country belongs to a special region at a given date. + */ + public function belongsToSpecialRegion(Region $region, ?Carbon $date = null): bool + { + if (! $region->is_special) { + return false; + } + + return $this->getSpecialRegionsAt($date)->contains('id', $region->id); + } + + /** + * Get the special regions that the country belongs to at a given date. + */ + public function getSpecialRegionsAt(?Carbon $date = null): Collection + { + $checkDate = $date ?? Carbon::now(); + + return $this->specialRegions() + ->wherePivot('start_date', '<=', $checkDate->toDateString()) + ->where(fn ($query) => $query + ->whereNull('world_country_in_special_region.end_date') + ->orWhere('world_country_in_special_region.end_date', '>=', $checkDate->toDateString()) + ) + ->get(); + } + + /** + * Get the factory for the model. + */ protected static function newFactory(): CountryFactory { return CountryFactory::new(); diff --git a/src/Models/CountrySpecialRegion.php b/src/Models/CountrySpecialRegion.php new file mode 100644 index 0000000..98956ca --- /dev/null +++ b/src/Models/CountrySpecialRegion.php @@ -0,0 +1,77 @@ + 'date', + 'end_date' => 'date', + ]; + + /** + * Get the country that the country belongs to. + */ + public function country(): BelongsTo + { + return $this->belongsTo(Country::class, 'country_id'); + } + + /** + * Get the region that the country belongs to. + */ + public function region(): BelongsTo + { + return $this->belongsTo(Region::class, 'region_id'); + } + + /** + * Check if the country belongs to a special region at a given date. + */ + public function isActive(?string $date = null): bool + { + $checkDate = $date ?? now()->toDateString(); + + return $this->start_date <= $checkDate && + ($this->end_date === null || $this->end_date >= $checkDate); + } + + /** + * Scope a query to only include active country special regions. + * + * @param Builder $query + * @return Builder + */ + public function scopeActive($query, ?string $date = null) + { + $checkDate = $date ?? now()->toDateString(); + + return $query->where('start_date', '<=', $checkDate) + ->where(fn ($q) => $q->whereNull('end_date')->orWhere('end_date', '>=', $checkDate)); + } + + /** + * Get the factory for the model. + */ + protected static function newFactory(): CountrySpecialRegionFactory + { + return CountrySpecialRegionFactory::new(); + } +} diff --git a/src/Models/Region.php b/src/Models/Region.php new file mode 100644 index 0000000..fd415f7 --- /dev/null +++ b/src/Models/Region.php @@ -0,0 +1,116 @@ + 'boolean', + ]; + + /** + * Get the parent region. + */ + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id'); + } + + /** + * Get the child regions. + */ + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id'); + } + + /** + * Get the countries in the region. + */ + public function countries(): HasMany + { + return $this->hasMany(Country::class, 'region_id'); + } + + /** + * Get the countries in the special region. + */ + public function specialCountries(): BelongsToMany + { + return $this->belongsToMany(Country::class, 'world_country_in_special_region', 'region_id', 'country_id') + ->withPivot(['start_date', 'end_date']) + ->withTimestamps(); + } + + /** + * Get the countries in the special region at a given date. + */ + public function getCountriesInSpecialRegion(?Carbon $date = null): Collection + { + if (! $this->is_special) { + return new Collection; + } + + $checkDate = $date ?? Carbon::now(); + + return $this->specialCountries() + ->wherePivot('start_date', '<=', $checkDate->toDateString()) + ->where(fn ($query) => $query + ->whereNull('world_country_in_special_region.end_date') + ->orWhere('world_country_in_special_region.end_date', '>=', $checkDate->toDateString()) + ) + ->get(); + } + + /** + * Check if the region is geographical. + */ + public function isGeographical(): bool + { + return ! $this->is_special; + } + + /** + * Get all the descendants of the region. + */ + public function getAllDescendants(): Collection + { + $descendants = new Collection; + + foreach ($this->children as $child) { + $descendants->push($child); + $descendants = $descendants->merge($child->getAllDescendants()); + } + + return $descendants; + } + + /** + * Get the factory for the model. + */ + protected static function newFactory(): RegionFactory + { + return RegionFactory::new(); + } +} diff --git a/src/Policies/RegionPolicy.php b/src/Policies/RegionPolicy.php new file mode 100644 index 0000000..3e540e7 --- /dev/null +++ b/src/Policies/RegionPolicy.php @@ -0,0 +1,92 @@ +can('view_any_region'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(Authorizable $user, Region $region): bool + { + return $user->can('view_region'); + } + + /** + * Determine whether the user can create models. + */ + public function create(Authorizable $user): bool + { + return $user->can('create_region'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(Authorizable $user, Region $region): bool + { + return $user->can('update_region'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Authorizable $user, Region $region): bool + { + return $user->can('delete_region'); + } + + /** + * Determine whether the user can delete any models. + */ + public function deleteAny(Authorizable $user): bool + { + return $user->can('delete_any_region'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(Authorizable $user, Region $region): bool + { + return $user->can('force_delete_region'); + } + + /** + * Determine whether the user can permanently delete any models. + */ + public function forceDeleteAny(Authorizable $user): bool + { + return $user->can('force_delete_any_region'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(Authorizable $user, Region $region): bool + { + return $user->can('restore_region'); + } + + /** + * Determine whether the user can restore any models. + */ + public function restoreAny(Authorizable $user): bool + { + return $user->can('restore_any_region'); + } +} diff --git a/src/Policies/SpecialRegionMembershipPolicy.php b/src/Policies/SpecialRegionMembershipPolicy.php new file mode 100644 index 0000000..2cf6bcb --- /dev/null +++ b/src/Policies/SpecialRegionMembershipPolicy.php @@ -0,0 +1,59 @@ +can('view_any_special_region_membership'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(Authorizable $user, $record): bool + { + return $user->can('view_special_region_membership'); + } + + /** + * Determine whether the user can create models. + */ + public function create(Authorizable $user): bool + { + return $user->can('create_special_region_membership'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(Authorizable $user, $record): bool + { + return $user->can('update_special_region_membership'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Authorizable $user, $record): bool + { + return $user->can('delete_special_region_membership'); + } + + /** + * Determine whether the user can delete any models. + */ + public function deleteAny(Authorizable $user): bool + { + return $user->can('delete_any_special_region_membership'); + } +} diff --git a/tests/Feature/CountryResourceTest.php b/tests/Feature/CountryResourceTest.php index 6609101..595d8e7 100644 --- a/tests/Feature/CountryResourceTest.php +++ b/tests/Feature/CountryResourceTest.php @@ -3,6 +3,7 @@ use Eclipse\World\Filament\Clusters\World\Resources\CountryResource; use Eclipse\World\Filament\Clusters\World\Resources\CountryResource\Pages\ListCountries; use Eclipse\World\Models\Country; +use Eclipse\World\Models\Region; use function Pest\Livewire\livewire; @@ -150,3 +151,41 @@ $this->assertModelMissing($country); }); + +test('country can be assigned to a region', function () { + $region = Region::factory()->geographical()->create(); + $country = Country::factory()->create(); + + $data = ['region_id' => $region->id]; + + livewire(ListCountries::class) + ->callTableAction('edit', $country, $data) + ->assertHasNoTableActionErrors(); + + $country->refresh(); + expect($country->region_id)->toEqual($region->id); + expect($country->region->name)->toEqual($region->name); +}); + +test('countries can be filtered by region', function () { + $region1 = Region::factory()->geographical()->create(); + $region2 = Region::factory()->geographical()->create(); + + $country1 = Country::factory()->create(['region_id' => $region1->id]); + $country2 = Country::factory()->create(['region_id' => $region2->id]); + $country3 = Country::factory()->create(['region_id' => null]); + + livewire(ListCountries::class) + ->filterTable('region_id', $region1->id) + ->assertCanSeeTableRecords([$country1]) + ->assertCanNotSeeTableRecords([$country2, $country3]); +}); + +test('region column is displayed in countries table', function () { + $region = Region::factory()->geographical()->create(['name' => 'Test Region']); + $country = Country::factory()->create(['region_id' => $region->id]); + + livewire(ListCountries::class) + ->assertCanSeeTableRecords([$country]) + ->assertTableColumnExists('region.name'); +}); diff --git a/tests/Feature/RegionResourceTest.php b/tests/Feature/RegionResourceTest.php new file mode 100644 index 0000000..27c6989 --- /dev/null +++ b/tests/Feature/RegionResourceTest.php @@ -0,0 +1,205 @@ +setUpSuperAdmin(); +}); + +test('unauthorized access can be prevented', function () { + // Create regular user with no permissions + $this->setUpCommonUser(); + + // Create test region + $region = Region::factory()->create(); + + // View table + $this->get(RegionResource::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_region'); + + // Create region + livewire(ListRegions::class) + ->assertActionDisabled('create'); + + // Edit region + livewire(ListRegions::class) + ->assertCanSeeTableRecords([$region]) + ->assertTableActionDisabled('edit', $region); + + // Delete region + livewire(ListRegions::class) + ->assertTableActionDisabled('delete', $region) + ->assertTableBulkActionDisabled('delete'); + + // Restore and force delete + $region->delete(); + $this->assertSoftDeleted($region); + + livewire(ListRegions::class) + ->assertTableActionDisabled('restore', $region) + ->assertTableBulkActionDisabled('restore') + ->assertTableActionDisabled('forceDelete', $region) + ->assertTableBulkActionDisabled('forceDelete'); +}); + +test('regions table can be displayed', function () { + $this->get(RegionResource::getUrl()) + ->assertSuccessful(); +}); + +test('form validation works', function () { + $component = livewire(ListRegions::class); + + // Test required fields + $component->callAction('create') + ->assertHasActionErrors([ + 'name' => 'required', + ]); + + // Test with valid data + $component->callAction('create', Region::factory()->definition()) + ->assertHasNoActionErrors(); +}); + +test('new region can be created', function () { + $data = Region::factory()->definition(); + + livewire(ListRegions::class) + ->callAction('create', $data) + ->assertHasNoActionErrors(); + + $region = Region::where('name', $data['name'])->first(); + expect($region)->toBeObject(); + + foreach ($data as $key => $val) { + expect($region->$key)->toEqual($val); + } +}); + +test('existing region can be updated', function () { + $region = Region::factory()->create([ + 'name' => 'Test Region', + 'code' => 'TR', + 'is_special' => false, + ]); + + $data = [ + 'name' => 'Updated Region', + 'code' => 'UR', + 'is_special' => true, + ]; + + livewire(ListRegions::class) + ->callTableAction('edit', $region, $data) + ->assertHasNoTableActionErrors(); + + $region->refresh(); + + foreach ($data as $key => $val) { + expect($region->$key)->toEqual($val); + } +}); + +test('region can be deleted', function () { + $region = Region::factory()->create(); + + livewire(ListRegions::class) + ->callTableAction('delete', $region) + ->assertHasNoTableActionErrors(); + + $this->assertSoftDeleted($region); +}); + +test('region can be restored', function () { + $region = Region::factory()->create(); + $region->delete(); + + $this->assertSoftDeleted($region); + + livewire(ListRegions::class) + ->filterTable('trashed') + ->assertTableActionExists('restore') + ->assertTableActionEnabled('restore', $region) + ->assertTableActionVisible('restore', $region) + ->callTableAction('restore', $region) + ->assertHasNoTableActionErrors(); + + $this->assertNotSoftDeleted($region); +}); + +test('region can be force deleted', function () { + $region = Region::factory()->create(); + + $region->delete(); + $this->assertSoftDeleted($region); + + livewire(ListRegions::class) + ->filterTable('trashed') + ->assertTableActionExists('forceDelete') + ->assertTableActionEnabled('forceDelete', $region) + ->assertTableActionVisible('forceDelete', $region) + ->callTableAction('forceDelete', $region) + ->assertHasNoTableActionErrors(); + + $this->assertModelMissing($region); +}); + +test('region with parent can be created', function () { + $parent = Region::factory()->create(); + $data = Region::factory()->withParent($parent)->make()->toArray(); + + livewire(ListRegions::class) + ->callAction('create', $data) + ->assertHasNoActionErrors(); + + $region = Region::where('name', $data['name'])->first(); + expect($region)->toBeObject(); + expect($region->parent_id)->toEqual($parent->id); +}); + +test('special region can be created', function () { + $data = Region::factory()->special()->make()->toArray(); + + livewire(ListRegions::class) + ->callAction('create', $data) + ->assertHasNoActionErrors(); + + $region = Region::where('name', $data['name'])->first(); + expect($region)->toBeObject(); + expect($region->is_special)->toBeTrue(); +}); + +test('regions can be filtered by type', function () { + $geographical = Region::factory()->geographical()->create(); + $special = Region::factory()->special()->create(); + + // Filter by geographical + livewire(ListRegions::class) + ->filterTable('is_special', '0') + ->assertCanSeeTableRecords([$geographical]) + ->assertCanNotSeeTableRecords([$special]); + + // Filter by special + livewire(ListRegions::class) + ->filterTable('is_special', '1') + ->assertCanSeeTableRecords([$special]) + ->assertCanNotSeeTableRecords([$geographical]); +}); + +test('regions can be filtered by parent', function () { + $parent = Region::factory()->create(); + $child = Region::factory()->withParent($parent)->create(); + $orphan = Region::factory()->create(); + + livewire(ListRegions::class) + ->filterTable('parent_id', $parent->id) + ->assertCanSeeTableRecords([$child]) + ->assertCanNotSeeTableRecords([$parent, $orphan]); +}); diff --git a/tests/Unit/ImportCountriesJobTest.php b/tests/Unit/ImportCountriesJobTest.php new file mode 100644 index 0000000..94bc17a --- /dev/null +++ b/tests/Unit/ImportCountriesJobTest.php @@ -0,0 +1,145 @@ + Http::response([ + [ + 'cca2' => 'US', + 'cca3' => 'USA', + 'ccn3' => '840', + 'name' => ['common' => 'United States'], + 'flag' => '🇺🇸', + 'independent' => true, + 'region' => 'Americas', + 'subregion' => 'North America', + ], + [ + 'cca2' => 'DE', + 'cca3' => 'DEU', + 'ccn3' => '276', + 'name' => ['common' => 'Germany'], + 'flag' => '🇩🇪', + 'independent' => true, + 'region' => 'Europe', + 'subregion' => 'Western Europe', + ], + ]), + ]); + + $job = new ImportCountries(null); + $job->handle(); + + // Check that regions were created + expect(Region::where('name', 'Americas')->exists())->toBeTrue(); + expect(Region::where('name', 'Europe')->exists())->toBeTrue(); + expect(Region::where('name', 'North America')->exists())->toBeTrue(); + expect(Region::where('name', 'Western Europe')->exists())->toBeTrue(); + + // Check parent-child relationships + $americas = Region::where('name', 'Americas')->first(); + $northAmerica = Region::where('name', 'North America')->first(); + expect($northAmerica->parent_id)->toEqual($americas->id); + + $europe = Region::where('name', 'Europe')->first(); + $westernEurope = Region::where('name', 'Western Europe')->first(); + expect($westernEurope->parent_id)->toEqual($europe->id); +}); + +test('import countries job assigns regions to countries', function () { + // Mock the HTTP response + Http::fake([ + 'https://raw.githubusercontent.com/mledoze/countries/master/dist/countries.json' => Http::response([ + [ + 'cca2' => 'US', + 'cca3' => 'USA', + 'ccn3' => '840', + 'name' => ['common' => 'United States'], + 'flag' => '🇺🇸', + 'independent' => true, + 'region' => 'Americas', + 'subregion' => 'North America', + ], + ]), + ]); + + $job = new ImportCountries(null); + $job->handle(); + + $country = Country::where('id', 'US')->first(); + expect($country)->toBeObject(); + expect($country->region)->toBeInstanceOf(Region::class); + expect($country->region->name)->toEqual('North America'); +}); + +test('import countries job handles countries without subregion', function () { + // Create a country directly to test the region assignment logic + $countryData = [ + 'cca2' => 'XX', + 'cca3' => 'XXX', + 'ccn3' => '999', + 'name' => ['common' => 'Test Country'], + 'flag' => '🏳️', + 'independent' => true, + 'region' => 'Test Region', + // No subregion field + ]; + + // Test the region assignment logic directly + $job = new ImportCountries(null); + $reflection = new ReflectionClass($job); + $method = $reflection->getMethod('getRegionIdForCountry'); + $method->setAccessible(true); + + $regionId = $method->invoke($job, $countryData); + expect($regionId)->toBeNull(); + + // Also test that a country can be created with null region_id + $country = Country::create([ + 'id' => 'XX', + 'a3_id' => 'XXX', + 'num_code' => '999', + 'name' => 'Test Country', + 'flag' => '🏳️', + 'region_id' => null, + ]); + + expect($country->region_id)->toBeNull(); +}); + +test('import countries job updates existing countries', function () { + // Create existing country + $existingCountry = Country::factory()->create([ + 'id' => 'US', + 'name' => 'Old Name', + ]); + + // Mock the HTTP response + Http::fake([ + 'https://raw.githubusercontent.com/mledoze/countries/master/dist/countries.json' => Http::response([ + [ + 'cca2' => 'US', + 'cca3' => 'USA', + 'ccn3' => '840', + 'name' => ['common' => 'United States'], + 'flag' => '🇺🇸', + 'independent' => true, + 'region' => 'Americas', + 'subregion' => 'North America', + ], + ]), + ]); + + $job = new ImportCountries(null); + $job->handle(); + + $existingCountry->refresh(); + expect($existingCountry->name)->toEqual('United States'); + expect($existingCountry->region)->toBeInstanceOf(Region::class); + expect($existingCountry->region->name)->toEqual('North America'); +}); diff --git a/tests/Unit/RegionModelTest.php b/tests/Unit/RegionModelTest.php new file mode 100644 index 0000000..eace81e --- /dev/null +++ b/tests/Unit/RegionModelTest.php @@ -0,0 +1,154 @@ +create(); + $child = Region::factory()->withParent($parent)->create(); + + expect($child->parent)->toBeInstanceOf(Region::class); + expect($child->parent->id)->toEqual($parent->id); + expect($parent->children)->toHaveCount(1); + expect($parent->children->first()->id)->toEqual($child->id); +}); + +test('region can have countries', function () { + $region = Region::factory()->create(); + $country = Country::factory()->create(['region_id' => $region->id]); + + expect($region->countries)->toHaveCount(1); + expect($region->countries->first()->id)->toEqual($country->id); +}); + +test('special region can have countries with dates', function () { + $region = Region::factory()->special()->create(); + $country = Country::factory()->create(); + + $startDate = Carbon::now()->subDays(30); + $endDate = Carbon::now()->addDays(30); + + $region->specialCountries()->attach($country->id, [ + 'start_date' => $startDate->toDateString(), + 'end_date' => $endDate->toDateString(), + ]); + + expect($region->specialCountries)->toHaveCount(1); + expect($region->specialCountries->first()->id)->toEqual($country->id); +}); + +test('getCountriesInSpecialRegion returns countries within date range', function () { + $region = Region::factory()->special()->create(); + $country1 = Country::factory()->create(); + $country2 = Country::factory()->create(); + $country3 = Country::factory()->create(); + + $now = Carbon::now(); + + // Country 1: Active (started in past, no end date) + $region->specialCountries()->attach($country1->id, [ + 'start_date' => $now->copy()->subDays(30)->toDateString(), + 'end_date' => null, + ]); + + // Country 2: Active (started in past, ends in future) + $region->specialCountries()->attach($country2->id, [ + 'start_date' => $now->copy()->subDays(30)->toDateString(), + 'end_date' => $now->copy()->addDays(30)->toDateString(), + ]); + + // Country 3: Inactive (ended in past) + $region->specialCountries()->attach($country3->id, [ + 'start_date' => $now->copy()->subDays(60)->toDateString(), + 'end_date' => $now->copy()->subDays(10)->toDateString(), + ]); + + $activeCountries = $region->getCountriesInSpecialRegion($now); + + expect($activeCountries)->toHaveCount(2); + expect($activeCountries->pluck('id')->toArray())->toContain($country1->id, $country2->id); + expect($activeCountries->pluck('id')->toArray())->not->toContain($country3->id); +}); + +test('getCountriesInSpecialRegion returns empty collection for geographical region', function () { + $region = Region::factory()->geographical()->create(); + $country = Country::factory()->create(); + + $region->specialCountries()->attach($country->id, [ + 'start_date' => Carbon::now()->toDateString(), + ]); + + $countries = $region->getCountriesInSpecialRegion(); + + expect($countries)->toHaveCount(0); +}); + +test('isGeographical returns correct value', function () { + $geographical = Region::factory()->geographical()->create(); + $special = Region::factory()->special()->create(); + + expect($geographical->isGeographical())->toBeTrue(); + expect($special->isGeographical())->toBeFalse(); +}); + +test('getAllDescendants returns all nested children', function () { + $grandparent = Region::factory()->create(); + $parent = Region::factory()->withParent($grandparent)->create(); + $child = Region::factory()->withParent($parent)->create(); + $sibling = Region::factory()->withParent($grandparent)->create(); + + $descendants = $grandparent->getAllDescendants(); + + expect($descendants)->toHaveCount(3); + expect($descendants->pluck('id')->toArray())->toContain($parent->id, $child->id, $sibling->id); +}); + +test('country belongsToSpecialRegion works correctly', function () { + $region = Region::factory()->special()->create(); + $country = Country::factory()->create(); + + $now = Carbon::now(); + + // Add country to region with current membership + $region->specialCountries()->attach($country->id, [ + 'start_date' => $now->copy()->subDays(30)->toDateString(), + 'end_date' => $now->copy()->addDays(30)->toDateString(), + ]); + + expect($country->belongsToSpecialRegion($region, $now))->toBeTrue(); + expect($country->belongsToSpecialRegion($region, $now->copy()->addDays(60)))->toBeFalse(); +}); + +test('country getSpecialRegionsAt returns correct regions', function () { + $region1 = Region::factory()->special()->create(); + $region2 = Region::factory()->special()->create(); + $region3 = Region::factory()->special()->create(); + $country = Country::factory()->create(); + + $now = Carbon::now(); + + // Active in region1 + $region1->specialCountries()->attach($country->id, [ + 'start_date' => $now->copy()->subDays(30)->toDateString(), + 'end_date' => null, + ]); + + // Active in region2 + $region2->specialCountries()->attach($country->id, [ + 'start_date' => $now->copy()->subDays(30)->toDateString(), + 'end_date' => $now->copy()->addDays(30)->toDateString(), + ]); + + // Not active in region3 (ended) + $region3->specialCountries()->attach($country->id, [ + 'start_date' => $now->copy()->subDays(60)->toDateString(), + 'end_date' => $now->copy()->subDays(10)->toDateString(), + ]); + + $activeRegions = $country->getSpecialRegionsAt($now); + + expect($activeRegions)->toHaveCount(2); + expect($activeRegions->pluck('id')->toArray())->toContain($region1->id, $region2->id); + expect($activeRegions->pluck('id')->toArray())->not->toContain($region3->id); +}); From 330caa6567b2062d6f0725b4b10967f64f9e54e4 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Sat, 26 Jul 2025 18:47:29 +0200 Subject: [PATCH 2/9] chore(workflows): fix workflows for contributors --- .github/workflows/linter.yml | 2 +- .github/workflows/test-runner.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index a548779..737b3a5 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Run Laravel Pint uses: aglipanci/laravel-pint-action@latest diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 4a1f073..b81008c 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -41,7 +41,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Validate composer.json and composer.lock run: composer validate --strict From 179d3748b04f5169bc22575901bc2ca1f6f9c8e1 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Tue, 29 Jul 2025 16:43:12 +0200 Subject: [PATCH 3/9] chore(regions): move EU region seeding into ImportCountries job and use Relation Manager --- .../factories/CountrySpecialRegionFactory.php | 72 ------- .../2025_07_26_120003_seed_eu_region.php | 41 ---- ...07_29_161719_add_default_to_start_date.php | 23 ++ resources/lang/en/countries.php | 4 + resources/lang/en/special-memberships.php | 74 ------- resources/lang/hr/countries.php | 4 + resources/lang/hr/special-memberships.php | 74 ------- resources/lang/sl/countries.php | 4 + resources/lang/sl/special-memberships.php | 74 ------- resources/lang/sr/countries.php | 4 + resources/lang/sr/special-memberships.php | 74 ------- .../World/Resources/CountryResource.php | 8 + .../SpecialRegionMembershipResource.php | 203 ------------------ .../Pages/ListSpecialRegionMemberships.php | 26 --- src/Jobs/ImportCountries.php | 35 +++ src/Models/Country.php | 7 +- src/Models/CountrySpecialRegion.php | 77 ------- .../SpecialRegionMembershipPolicy.php | 59 ----- 18 files changed, 86 insertions(+), 777 deletions(-) delete mode 100644 database/factories/CountrySpecialRegionFactory.php delete mode 100644 database/migrations/2025_07_26_120003_seed_eu_region.php create mode 100644 database/migrations/2025_07_29_161719_add_default_to_start_date.php delete mode 100644 resources/lang/en/special-memberships.php delete mode 100644 resources/lang/hr/special-memberships.php delete mode 100644 resources/lang/sl/special-memberships.php delete mode 100644 resources/lang/sr/special-memberships.php delete mode 100644 src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource.php delete mode 100644 src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource/Pages/ListSpecialRegionMemberships.php delete mode 100644 src/Models/CountrySpecialRegion.php delete mode 100644 src/Policies/SpecialRegionMembershipPolicy.php diff --git a/database/factories/CountrySpecialRegionFactory.php b/database/factories/CountrySpecialRegionFactory.php deleted file mode 100644 index 2299470..0000000 --- a/database/factories/CountrySpecialRegionFactory.php +++ /dev/null @@ -1,72 +0,0 @@ - - */ - public function definition(): array - { - return [ - 'country_id' => Country::factory(), - 'region_id' => Region::factory()->special(), - 'start_date' => $this->faker->dateTimeBetween('-2 years', 'now')->format('Y-m-d'), - 'end_date' => $this->faker->optional(0.3)->dateTimeBetween('now', '+2 years')?->format('Y-m-d'), - ]; - } - - /** - * Set the region as active. - * - * @return $this - */ - public function active(): static - { - return $this->state([ - 'start_date' => $this->faker->dateTimeBetween('-1 year', 'now')->format('Y-m-d'), - 'end_date' => null, - ]); - } - - /** - * Set the region as ended. - * - * @return $this - */ - public function ended(): static - { - return $this->state([ - 'start_date' => $this->faker->dateTimeBetween('-2 years', '-6 months')->format('Y-m-d'), - 'end_date' => $this->faker->dateTimeBetween('-6 months', 'now')->format('Y-m-d'), - ]); - } - - /** - * Set the region as future. - * - * @return $this - */ - public function future(): static - { - return $this->state([ - 'start_date' => $this->faker->dateTimeBetween('now', '+6 months')->format('Y-m-d'), - 'end_date' => $this->faker->optional(0.5)->dateTimeBetween('+6 months', '+2 years')?->format('Y-m-d'), - ]); - } -} diff --git a/database/migrations/2025_07_26_120003_seed_eu_region.php b/database/migrations/2025_07_26_120003_seed_eu_region.php deleted file mode 100644 index 6be298b..0000000 --- a/database/migrations/2025_07_26_120003_seed_eu_region.php +++ /dev/null @@ -1,41 +0,0 @@ - 'EU', - 'name' => 'European Union', - 'is_special' => true, - ]); - - $euMemberCountries = [ - 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', - 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', - 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', - ]; - - $existingCountries = Country::whereIn('id', $euMemberCountries)->pluck('id'); - $membershipData = $existingCountries->mapWithKeys(fn ($countryId) => [ - $countryId => [ - 'start_date' => Carbon::now()->toDateString(), - 'created_at' => now(), - 'updated_at' => now(), - ], - ]); - - $euRegion->specialCountries()->attach($membershipData); - } - - public function down(): void - { - Region::where('code', 'EU')->delete(); - } -}; diff --git a/database/migrations/2025_07_29_161719_add_default_to_start_date.php b/database/migrations/2025_07_29_161719_add_default_to_start_date.php new file mode 100644 index 0000000..f465cfa --- /dev/null +++ b/database/migrations/2025_07_29_161719_add_default_to_start_date.php @@ -0,0 +1,23 @@ +date('start_date')->default(DB::raw('CURRENT_DATE'))->change(); + }); + } + + public function down(): void + { + Schema::table('world_country_in_special_region', function (Blueprint $table) { + $table->date('start_date')->change(); + }); + } +}; diff --git a/resources/lang/en/countries.php b/resources/lang/en/countries.php index 41753aa..fc58386 100644 --- a/resources/lang/en/countries.php +++ b/resources/lang/en/countries.php @@ -77,6 +77,10 @@ 'label' => 'Geographical Region', 'helper' => 'The geographical region this country belongs to', ], + 'special_regions' => [ + 'label' => 'Special Regions', + 'helper' => 'Special regions this country belongs to (e.g., European Union)', + ], ], 'import' => [ diff --git a/resources/lang/en/special-memberships.php b/resources/lang/en/special-memberships.php deleted file mode 100644 index 8efc123..0000000 --- a/resources/lang/en/special-memberships.php +++ /dev/null @@ -1,74 +0,0 @@ - 'Special Region Memberships', - 'breadcrumb' => 'Special Region Memberships', - 'plural' => 'Special Region Memberships', - - 'form' => [ - 'region' => [ - 'label' => 'Special Region', - ], - 'country' => [ - 'label' => 'Country', - ], - 'start_date' => [ - 'label' => 'Start Date', - ], - 'end_date' => [ - 'label' => 'End Date', - ], - ], - - 'table' => [ - 'region' => [ - 'label' => 'Region', - ], - 'flag' => [ - 'label' => 'Flag', - ], - 'country' => [ - 'label' => 'Country', - ], - 'start_date' => [ - 'label' => 'Start Date', - ], - 'end_date' => [ - 'label' => 'End Date', - ], - 'status' => [ - 'label' => 'Status', - ], - ], - - 'filters' => [ - 'region' => [ - 'label' => 'Region', - ], - 'status' => [ - 'label' => 'Status', - ], - ], - - 'status' => [ - 'active' => 'Active', - 'future' => 'Future', - 'ended' => 'Ended', - ], - - 'actions' => [ - 'create' => [ - 'label' => 'New Membership', - 'heading' => 'Create Special Region Membership', - 'success' => 'Special region membership created successfully.', - ], - 'edit' => [ - 'label' => 'Edit', - 'heading' => 'Edit Special Region Membership', - ], - 'delete' => [ - 'label' => 'Delete', - 'heading' => 'Delete Special Region Membership', - ], - ], -]; diff --git a/resources/lang/hr/countries.php b/resources/lang/hr/countries.php index 93a3bf7..e9f8be5 100644 --- a/resources/lang/hr/countries.php +++ b/resources/lang/hr/countries.php @@ -77,6 +77,10 @@ 'label' => 'Geografska regija', 'helper' => 'Geografska regija kojoj ova zemlja pripada', ], + 'special_regions' => [ + 'label' => 'Posebne regije', + 'helper' => 'Posebne regije kojima ova zemlja pripada (npr. Europska unija)', + ], ], 'import' => [ diff --git a/resources/lang/hr/special-memberships.php b/resources/lang/hr/special-memberships.php deleted file mode 100644 index d9ed2ec..0000000 --- a/resources/lang/hr/special-memberships.php +++ /dev/null @@ -1,74 +0,0 @@ - 'Članstva u posebnim regijama', - 'breadcrumb' => 'Članstva u posebnim regijama', - 'plural' => 'Članstva u posebnim regijama', - - 'form' => [ - 'region' => [ - 'label' => 'Posebna regija', - ], - 'country' => [ - 'label' => 'Zemlja', - ], - 'start_date' => [ - 'label' => 'Datum početka', - ], - 'end_date' => [ - 'label' => 'Datum završetka', - ], - ], - - 'table' => [ - 'region' => [ - 'label' => 'Regija', - ], - 'flag' => [ - 'label' => 'Zastava', - ], - 'country' => [ - 'label' => 'Zemlja', - ], - 'start_date' => [ - 'label' => 'Datum početka', - ], - 'end_date' => [ - 'label' => 'Datum završetka', - ], - 'status' => [ - 'label' => 'Status', - ], - ], - - 'filters' => [ - 'region' => [ - 'label' => 'Regija', - ], - 'status' => [ - 'label' => 'Status', - ], - ], - - 'status' => [ - 'active' => 'Aktivno', - 'future' => 'Buduće', - 'ended' => 'Završeno', - ], - - 'actions' => [ - 'create' => [ - 'label' => 'Novo članstvo', - 'heading' => 'Stvori članstvo u posebnoj regiji', - 'success' => 'Članstvo u posebnoj regiji uspješno stvoreno.', - ], - 'edit' => [ - 'label' => 'Uredi', - 'heading' => 'Uredi članstvo u posebnoj regiji', - ], - 'delete' => [ - 'label' => 'Obriši', - 'heading' => 'Obriši članstvo u posebnoj regiji', - ], - ], -]; diff --git a/resources/lang/sl/countries.php b/resources/lang/sl/countries.php index b0db2c7..c4dd7cf 100644 --- a/resources/lang/sl/countries.php +++ b/resources/lang/sl/countries.php @@ -77,6 +77,10 @@ 'label' => 'Geografska regija', 'helper' => 'Geografska regija, ki ji ta država pripada', ], + 'special_regions' => [ + 'label' => 'Posebne regije', + 'helper' => 'Posebne regije, ki jim ta država pripada (npr. Evropska unija)', + ], ], 'import' => [ diff --git a/resources/lang/sl/special-memberships.php b/resources/lang/sl/special-memberships.php deleted file mode 100644 index 9d4b81b..0000000 --- a/resources/lang/sl/special-memberships.php +++ /dev/null @@ -1,74 +0,0 @@ - 'Članstva v posebnih regijah', - 'breadcrumb' => 'Članstva v posebnih regijah', - 'plural' => 'Članstva v posebnih regijah', - - 'form' => [ - 'region' => [ - 'label' => 'Posebna regija', - ], - 'country' => [ - 'label' => 'Država', - ], - 'start_date' => [ - 'label' => 'Datum začetka', - ], - 'end_date' => [ - 'label' => 'Datum konca', - ], - ], - - 'table' => [ - 'region' => [ - 'label' => 'Regija', - ], - 'flag' => [ - 'label' => 'Zastava', - ], - 'country' => [ - 'label' => 'Država', - ], - 'start_date' => [ - 'label' => 'Datum začetka', - ], - 'end_date' => [ - 'label' => 'Datum konca', - ], - 'status' => [ - 'label' => 'Status', - ], - ], - - 'filters' => [ - 'region' => [ - 'label' => 'Regija', - ], - 'status' => [ - 'label' => 'Status', - ], - ], - - 'status' => [ - 'active' => 'Aktivno', - 'future' => 'Prihodnje', - 'ended' => 'Končano', - ], - - 'actions' => [ - 'create' => [ - 'label' => 'Novo članstvo', - 'heading' => 'Ustvari članstvo v posebni regiji', - 'success' => 'Članstvo v posebni regiji uspešno ustvarjeno.', - ], - 'edit' => [ - 'label' => 'Uredi', - 'heading' => 'Uredi članstvo v posebni regiji', - ], - 'delete' => [ - 'label' => 'Izbriši', - 'heading' => 'Izbriši članstvo v posebni regiji', - ], - ], -]; diff --git a/resources/lang/sr/countries.php b/resources/lang/sr/countries.php index 7dc1ca2..93a642e 100644 --- a/resources/lang/sr/countries.php +++ b/resources/lang/sr/countries.php @@ -77,6 +77,10 @@ 'label' => 'Geografska regija', 'helper' => 'Geografska regija kojoj ova zemlja pripada', ], + 'special_regions' => [ + 'label' => 'Posebne regije', + 'helper' => 'Posebne regije kojima ova zemlja pripada (npr. Evropska unija)', + ], ], 'import' => [ diff --git a/resources/lang/sr/special-memberships.php b/resources/lang/sr/special-memberships.php deleted file mode 100644 index b8846b2..0000000 --- a/resources/lang/sr/special-memberships.php +++ /dev/null @@ -1,74 +0,0 @@ - 'Članstva u posebnim regijama', - 'breadcrumb' => 'Članstva u posebnim regijama', - 'plural' => 'Članstva u posebnim regijama', - - 'form' => [ - 'region' => [ - 'label' => 'Posebna regija', - ], - 'country' => [ - 'label' => 'Zemlja', - ], - 'start_date' => [ - 'label' => 'Datum početka', - ], - 'end_date' => [ - 'label' => 'Datum završetka', - ], - ], - - 'table' => [ - 'region' => [ - 'label' => 'Regija', - ], - 'flag' => [ - 'label' => 'Zastava', - ], - 'country' => [ - 'label' => 'Zemlja', - ], - 'start_date' => [ - 'label' => 'Datum početka', - ], - 'end_date' => [ - 'label' => 'Datum završetka', - ], - 'status' => [ - 'label' => 'Status', - ], - ], - - 'filters' => [ - 'region' => [ - 'label' => 'Regija', - ], - 'status' => [ - 'label' => 'Status', - ], - ], - - 'status' => [ - 'active' => 'Aktivno', - 'future' => 'Buduće', - 'ended' => 'Završeno', - ], - - 'actions' => [ - 'create' => [ - 'label' => 'Novo članstvo', - 'heading' => 'Stvori članstvo u posebnoj regiji', - 'success' => 'Članstvo u posebnoj regiji uspešno stvoreno.', - ], - 'edit' => [ - 'label' => 'Uredi', - 'heading' => 'Uredi članstvo u posebnoj regiji', - ], - 'delete' => [ - 'label' => 'Obriši', - 'heading' => 'Obriši članstvo u posebnoj regiji', - ], - ], -]; diff --git a/src/Filament/Clusters/World/Resources/CountryResource.php b/src/Filament/Clusters/World/Resources/CountryResource.php index f07e724..86ad166 100644 --- a/src/Filament/Clusters/World/Resources/CountryResource.php +++ b/src/Filament/Clusters/World/Resources/CountryResource.php @@ -79,6 +79,14 @@ public static function form(Form $form): Form ->searchable() ->preload() ->helperText(__('eclipse-world::countries.form.region.helper')), + + Select::make('special_regions') + ->label(__('eclipse-world::countries.form.special_regions.label')) + ->multiple() + ->relationship('specialRegions', 'name', fn ($query) => $query->where('is_special', true)) + ->searchable() + ->preload() + ->helperText(__('eclipse-world::countries.form.special_regions.helper')), ]); } diff --git a/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource.php b/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource.php deleted file mode 100644 index 06d37d4..0000000 --- a/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource.php +++ /dev/null @@ -1,203 +0,0 @@ -with(['country', 'region']); - } - - public static function form(Form $form): Form - { - return $form - ->schema([ - Select::make('region_id') - ->label(__('eclipse-world::special-memberships.form.region.label')) - ->options(Region::where('is_special', true)->pluck('name', 'id')) - ->required() - ->searchable() - ->preload(), - - Select::make('country_id') - ->label(__('eclipse-world::special-memberships.form.country.label')) - ->options(Country::orderBy('name')->pluck('name', 'id')) - ->required() - ->searchable() - ->preload(), - - DatePicker::make('start_date') - ->label(__('eclipse-world::special-memberships.form.start_date.label')) - ->required() - ->default(now()), - - DatePicker::make('end_date') - ->label(__('eclipse-world::special-memberships.form.end_date.label')) - ->after('start_date'), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->defaultPaginationPageOption(50) - ->defaultSort('start_date', 'desc') - ->striped() - ->columns([ - TextColumn::make('region.name') - ->label(__('eclipse-world::special-memberships.table.region.label')) - ->searchable() - ->sortable(), - - TextColumn::make('country.flag') - ->label(__('eclipse-world::special-memberships.table.flag.label')) - ->width(60), - - TextColumn::make('country.name') - ->label(__('eclipse-world::special-memberships.table.country.label')) - ->searchable() - ->sortable(), - - TextColumn::make('start_date') - ->label(__('eclipse-world::special-memberships.table.start_date.label')) - ->date() - ->sortable(), - - TextColumn::make('end_date') - ->label(__('eclipse-world::special-memberships.table.end_date.label')) - ->date() - ->sortable() - ->placeholder('—'), - - TextColumn::make('status') - ->label(__('eclipse-world::special-memberships.table.status.label')) - ->badge() - ->getStateUsing(function (CountrySpecialRegion $record) { - $today = now()->toDateString(); - - return match (true) { - $record->start_date > $today => 'future', - $record->end_date && $record->end_date < $today => 'ended', - default => 'active' - }; - }) - ->color(fn (string $state): string => match ($state) { - 'active' => 'success', - 'future' => 'warning', - 'ended' => 'danger', - }) - ->formatStateUsing(fn (string $state): string => match ($state) { - 'active' => __('eclipse-world::special-memberships.status.active'), - 'future' => __('eclipse-world::special-memberships.status.future'), - 'ended' => __('eclipse-world::special-memberships.status.ended'), - }), - ]) - ->filters([ - SelectFilter::make('region_id') - ->label(__('eclipse-world::special-memberships.filters.region.label')) - ->options(Region::where('is_special', true)->pluck('name', 'id')) - ->searchable() - ->preload(), - SelectFilter::make('status') - ->label(__('eclipse-world::special-memberships.filters.status.label')) - ->options([ - 'active' => __('eclipse-world::special-memberships.status.active'), - 'future' => __('eclipse-world::special-memberships.status.future'), - 'ended' => __('eclipse-world::special-memberships.status.ended'), - ]) - ->query(function (Builder $query, array $data): Builder { - if (! $data['value']) { - return $query; - } - - $today = now()->toDateString(); - - return match ($data['value']) { - 'active' => $query->where('start_date', '<=', $today) - ->where(fn ($q) => $q->whereNull('end_date')->orWhere('end_date', '>=', $today)), - 'future' => $query->where('start_date', '>', $today), - 'ended' => $query->where('end_date', '<', $today), - default => $query, - }; - }), - ]) - ->actions([ - EditAction::make() - ->label(__('eclipse-world::special-memberships.actions.edit.label')) - ->modalHeading(__('eclipse-world::special-memberships.actions.edit.heading')), - ActionGroup::make([ - DeleteAction::make() - ->label(__('eclipse-world::special-memberships.actions.delete.label')) - ->modalHeading(__('eclipse-world::special-memberships.actions.delete.heading')), - ]), - ]) - ->bulkActions([ - BulkActionGroup::make([ - DeleteBulkAction::make() - ->label(__('eclipse-world::special-memberships.actions.delete.label')), - ]), - ]); - } - - public static function getPages(): array - { - return [ - 'index' => Pages\ListSpecialRegionMemberships::route('/'), - ]; - } - - public static function getPermissionPrefixes(): array - { - return [ - 'view_any', - 'create', - 'update', - 'delete', - 'delete_any', - ]; - } - - public static function getNavigationLabel(): string - { - return __('eclipse-world::special-memberships.nav_label'); - } - - public static function getBreadcrumb(): string - { - return __('eclipse-world::special-memberships.breadcrumb'); - } - - public static function getPluralModelLabel(): string - { - return __('eclipse-world::special-memberships.plural'); - } -} diff --git a/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource/Pages/ListSpecialRegionMemberships.php b/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource/Pages/ListSpecialRegionMemberships.php deleted file mode 100644 index 34c1eaa..0000000 --- a/src/Filament/Clusters/World/Resources/SpecialRegionMembershipResource/Pages/ListSpecialRegionMemberships.php +++ /dev/null @@ -1,26 +0,0 @@ -label(__('eclipse-world::special-memberships.actions.create.label')) - ->modalHeading(__('eclipse-world::special-memberships.actions.create.heading')) - ->using(function (array $data) { - return CountrySpecialRegion::create($data); - }) - ->successNotificationTitle(__('eclipse-world::special-memberships.actions.create.success')), - ]; - } -} diff --git a/src/Jobs/ImportCountries.php b/src/Jobs/ImportCountries.php index bbe7839..76afd6d 100644 --- a/src/Jobs/ImportCountries.php +++ b/src/Jobs/ImportCountries.php @@ -12,6 +12,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Log; class ImportCountries implements ShouldQueue @@ -76,6 +77,9 @@ public function handle(): void } } + // Seed special regions after countries are imported + $this->seedSpecialRegions(); + Log::info('Countries import completed'); if ($user) { $user->notify(new ImportFinishedNotification('success', 'countries', null, $this->locale)); @@ -158,4 +162,35 @@ private function getRegionIdForCountry(array $countryData): ?int ->where('is_special', false) ->value('id'); } + + private function seedSpecialRegions(): void + { + // Create EU special region + $euRegion = Region::updateOrCreate( + ['code' => 'EU'], + [ + 'name' => 'European Union', + 'is_special' => true, + ] + ); + + $euMemberCountries = [ + 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', + 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', + 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', + ]; + + $existingCountries = Country::whereIn('id', $euMemberCountries)->pluck('id'); + $membershipData = $existingCountries->mapWithKeys(fn ($countryId) => [ + $countryId => [ + 'start_date' => Carbon::now()->toDateString(), + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + + // Clear existing memberships and add new ones + $euRegion->specialCountries()->detach(); + $euRegion->specialCountries()->attach($membershipData); + } } diff --git a/src/Models/Country.php b/src/Models/Country.php index 93d1c83..854f526 100644 --- a/src/Models/Country.php +++ b/src/Models/Country.php @@ -69,9 +69,10 @@ public function getSpecialRegionsAt(?Carbon $date = null): Collection return $this->specialRegions() ->wherePivot('start_date', '<=', $checkDate->toDateString()) - ->where(fn ($query) => $query - ->whereNull('world_country_in_special_region.end_date') - ->orWhere('world_country_in_special_region.end_date', '>=', $checkDate->toDateString()) + ->where( + fn ($query) => $query + ->whereNull('world_country_in_special_region.end_date') + ->orWhere('world_country_in_special_region.end_date', '>=', $checkDate->toDateString()) ) ->get(); } diff --git a/src/Models/CountrySpecialRegion.php b/src/Models/CountrySpecialRegion.php deleted file mode 100644 index 98956ca..0000000 --- a/src/Models/CountrySpecialRegion.php +++ /dev/null @@ -1,77 +0,0 @@ - 'date', - 'end_date' => 'date', - ]; - - /** - * Get the country that the country belongs to. - */ - public function country(): BelongsTo - { - return $this->belongsTo(Country::class, 'country_id'); - } - - /** - * Get the region that the country belongs to. - */ - public function region(): BelongsTo - { - return $this->belongsTo(Region::class, 'region_id'); - } - - /** - * Check if the country belongs to a special region at a given date. - */ - public function isActive(?string $date = null): bool - { - $checkDate = $date ?? now()->toDateString(); - - return $this->start_date <= $checkDate && - ($this->end_date === null || $this->end_date >= $checkDate); - } - - /** - * Scope a query to only include active country special regions. - * - * @param Builder $query - * @return Builder - */ - public function scopeActive($query, ?string $date = null) - { - $checkDate = $date ?? now()->toDateString(); - - return $query->where('start_date', '<=', $checkDate) - ->where(fn ($q) => $q->whereNull('end_date')->orWhere('end_date', '>=', $checkDate)); - } - - /** - * Get the factory for the model. - */ - protected static function newFactory(): CountrySpecialRegionFactory - { - return CountrySpecialRegionFactory::new(); - } -} diff --git a/src/Policies/SpecialRegionMembershipPolicy.php b/src/Policies/SpecialRegionMembershipPolicy.php deleted file mode 100644 index 2cf6bcb..0000000 --- a/src/Policies/SpecialRegionMembershipPolicy.php +++ /dev/null @@ -1,59 +0,0 @@ -can('view_any_special_region_membership'); - } - - /** - * Determine whether the user can view the model. - */ - public function view(Authorizable $user, $record): bool - { - return $user->can('view_special_region_membership'); - } - - /** - * Determine whether the user can create models. - */ - public function create(Authorizable $user): bool - { - return $user->can('create_special_region_membership'); - } - - /** - * Determine whether the user can update the model. - */ - public function update(Authorizable $user, $record): bool - { - return $user->can('update_special_region_membership'); - } - - /** - * Determine whether the user can delete the model. - */ - public function delete(Authorizable $user, $record): bool - { - return $user->can('delete_special_region_membership'); - } - - /** - * Determine whether the user can delete any models. - */ - public function deleteAny(Authorizable $user): bool - { - return $user->can('delete_any_special_region_membership'); - } -} From ae9d6fca6d22190d81f0cdb9c82c266278b09c5d Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Wed, 30 Jul 2025 10:04:32 +0200 Subject: [PATCH 4/9] chore(regions): allow setting special regions with start and optional end dates --- ...07_29_161719_add_default_to_start_date.php | 23 -------------- resources/lang/en/countries.php | 4 +++ resources/lang/hr/countries.php | 4 +++ resources/lang/sl/countries.php | 4 +++ resources/lang/sr/countries.php | 4 +++ .../World/Resources/CountryResource.php | 30 +++++++++++++++---- src/Models/Country.php | 6 ++++ src/Models/CountryInSpecialRegion.php | 27 +++++++++++++++++ 8 files changed, 73 insertions(+), 29 deletions(-) delete mode 100644 database/migrations/2025_07_29_161719_add_default_to_start_date.php create mode 100644 src/Models/CountryInSpecialRegion.php diff --git a/database/migrations/2025_07_29_161719_add_default_to_start_date.php b/database/migrations/2025_07_29_161719_add_default_to_start_date.php deleted file mode 100644 index f465cfa..0000000 --- a/database/migrations/2025_07_29_161719_add_default_to_start_date.php +++ /dev/null @@ -1,23 +0,0 @@ -date('start_date')->default(DB::raw('CURRENT_DATE'))->change(); - }); - } - - public function down(): void - { - Schema::table('world_country_in_special_region', function (Blueprint $table) { - $table->date('start_date')->change(); - }); - } -}; diff --git a/resources/lang/en/countries.php b/resources/lang/en/countries.php index fc58386..4b6c4d2 100644 --- a/resources/lang/en/countries.php +++ b/resources/lang/en/countries.php @@ -80,6 +80,10 @@ 'special_regions' => [ 'label' => 'Special Regions', 'helper' => 'Special regions this country belongs to (e.g., European Union)', + 'add_button' => 'Add Special Region', + 'region_label' => 'Region', + 'start_date_label' => 'Start Date', + 'end_date_label' => 'End Date', ], ], diff --git a/resources/lang/hr/countries.php b/resources/lang/hr/countries.php index e9f8be5..e651092 100644 --- a/resources/lang/hr/countries.php +++ b/resources/lang/hr/countries.php @@ -80,6 +80,10 @@ 'special_regions' => [ 'label' => 'Posebne regije', 'helper' => 'Posebne regije kojima ova zemlja pripada (npr. Europska unija)', + 'add_button' => 'Dodaj posebnu regiju', + 'region_label' => 'Regija', + 'start_date_label' => 'Datum početka', + 'end_date_label' => 'Datum završetka', ], ], diff --git a/resources/lang/sl/countries.php b/resources/lang/sl/countries.php index c4dd7cf..dfe01f9 100644 --- a/resources/lang/sl/countries.php +++ b/resources/lang/sl/countries.php @@ -80,6 +80,10 @@ 'special_regions' => [ 'label' => 'Posebne regije', 'helper' => 'Posebne regije, ki jim ta država pripada (npr. Evropska unija)', + 'add_button' => 'Dodaj posebno regijo', + 'region_label' => 'Regija', + 'start_date_label' => 'Datum začetka', + 'end_date_label' => 'Datum konca', ], ], diff --git a/resources/lang/sr/countries.php b/resources/lang/sr/countries.php index 93a642e..98dff38 100644 --- a/resources/lang/sr/countries.php +++ b/resources/lang/sr/countries.php @@ -80,6 +80,10 @@ 'special_regions' => [ 'label' => 'Posebne regije', 'helper' => 'Posebne regije kojima ova zemlja pripada (npr. Evropska unija)', + 'add_button' => 'Dodaj posebnu regiju', + 'region_label' => 'Regija', + 'start_date_label' => 'Datum početka', + 'end_date_label' => 'Datum završetka', ], ], diff --git a/src/Filament/Clusters/World/Resources/CountryResource.php b/src/Filament/Clusters/World/Resources/CountryResource.php index 86ad166..049b30a 100644 --- a/src/Filament/Clusters/World/Resources/CountryResource.php +++ b/src/Filament/Clusters/World/Resources/CountryResource.php @@ -6,6 +6,8 @@ use Eclipse\World\Filament\Clusters\World; use Eclipse\World\Filament\Clusters\World\Resources\CountryResource\Pages; use Eclipse\World\Models\Country; +use Filament\Forms\Components\DatePicker; +use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; @@ -80,13 +82,29 @@ public static function form(Form $form): Form ->preload() ->helperText(__('eclipse-world::countries.form.region.helper')), - Select::make('special_regions') + Repeater::make('countryInSpecialRegions') + ->relationship() ->label(__('eclipse-world::countries.form.special_regions.label')) - ->multiple() - ->relationship('specialRegions', 'name', fn ($query) => $query->where('is_special', true)) - ->searchable() - ->preload() - ->helperText(__('eclipse-world::countries.form.special_regions.helper')), + ->columns(3) + ->columnSpan(2) + ->createItemButtonLabel(__('eclipse-world::countries.form.special_regions.add_button')) + ->defaultItems(0) + ->minItems(0) + ->schema([ + Select::make('region_id') + ->relationship('region', 'name', fn ($query) => $query->where('is_special', true)) + ->searchable() + ->preload() + ->label(__('eclipse-world::countries.form.special_regions.region_label')), + + DatePicker::make('start_date') + ->required() + ->label(__('eclipse-world::countries.form.special_regions.start_date_label')), + + DatePicker::make('end_date') + ->nullable() + ->label(__('eclipse-world::countries.form.special_regions.end_date_label')), + ]), ]); } diff --git a/src/Models/Country.php b/src/Models/Country.php index 854f526..4803e42 100644 --- a/src/Models/Country.php +++ b/src/Models/Country.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; @@ -48,6 +49,11 @@ public function specialRegions(): BelongsToMany ->withTimestamps(); } + public function countryInSpecialRegions(): HasMany + { + return $this->hasMany(CountryInSpecialRegion::class, 'country_id'); + } + /** * Check if the country belongs to a special region at a given date. */ diff --git a/src/Models/CountryInSpecialRegion.php b/src/Models/CountryInSpecialRegion.php new file mode 100644 index 0000000..51ebd52 --- /dev/null +++ b/src/Models/CountryInSpecialRegion.php @@ -0,0 +1,27 @@ + 'date', + 'end_date' => 'date', + ]; + + protected $fillable = [ + 'country_id', + 'region_id', + 'start_date', + 'end_date', + ]; + + public function region() + { + return $this->belongsTo(Region::class); + } +} From ab76a693de8f574fda79027c3ea9e11bae80b087 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 31 Jul 2025 10:15:53 +0200 Subject: [PATCH 5/9] chore(regions): enhance special regions with validation, filters, and optimized imports --- resources/lang/en/countries.php | 12 +++- resources/lang/hr/countries.php | 12 +++- resources/lang/sl/countries.php | 12 +++- resources/lang/sr/countries.php | 12 +++- .../World/Resources/CountryResource.php | 59 ++++++++++++++++++- .../World/Resources/RegionResource.php | 21 ++++++- src/Jobs/ImportCountries.php | 48 +++++++++++---- 7 files changed, 154 insertions(+), 22 deletions(-) diff --git a/resources/lang/en/countries.php b/resources/lang/en/countries.php index 4b6c4d2..9b05f45 100644 --- a/resources/lang/en/countries.php +++ b/resources/lang/en/countries.php @@ -106,8 +106,16 @@ ], 'filters' => [ - 'region' => [ - 'label' => 'Region', + 'geographical_region' => [ + 'label' => 'Geographical Region', ], + 'special_region' => [ + 'label' => 'Special Region', + ], + ], + + 'validation' => [ + 'duplicate_special_region_membership' => 'This country is already a member of :region.', + 'unknown_region' => 'this region', ], ]; diff --git a/resources/lang/hr/countries.php b/resources/lang/hr/countries.php index e651092..4bf8e87 100644 --- a/resources/lang/hr/countries.php +++ b/resources/lang/hr/countries.php @@ -106,8 +106,16 @@ ], 'filters' => [ - 'region' => [ - 'label' => 'Regija', + 'geographical_region' => [ + 'label' => 'Geografska regija', ], + 'special_region' => [ + 'label' => 'Posebna regija', + ], + ], + + 'validation' => [ + 'duplicate_special_region_membership' => 'Ova država je već član regije :region.', + 'unknown_region' => 'ove regije', ], ]; diff --git a/resources/lang/sl/countries.php b/resources/lang/sl/countries.php index dfe01f9..fae931d 100644 --- a/resources/lang/sl/countries.php +++ b/resources/lang/sl/countries.php @@ -106,8 +106,16 @@ ], 'filters' => [ - 'region' => [ - 'label' => 'Regija', + 'geographical_region' => [ + 'label' => 'Geografska regija', ], + 'special_region' => [ + 'label' => 'Posebna regija', + ], + ], + + 'validation' => [ + 'duplicate_special_region_membership' => 'Ta država je že članica regije :region.', + 'unknown_region' => 'te regije', ], ]; diff --git a/resources/lang/sr/countries.php b/resources/lang/sr/countries.php index 98dff38..74f48fb 100644 --- a/resources/lang/sr/countries.php +++ b/resources/lang/sr/countries.php @@ -106,8 +106,16 @@ ], 'filters' => [ - 'region' => [ - 'label' => 'Regija', + 'geographical_region' => [ + 'label' => 'Geografska regija', ], + 'special_region' => [ + 'label' => 'Posebna regija', + ], + ], + + 'validation' => [ + 'duplicate_special_region_membership' => 'Ova država je već član regije :region.', + 'unknown_region' => 'ove regije', ], ]; diff --git a/src/Filament/Clusters/World/Resources/CountryResource.php b/src/Filament/Clusters/World/Resources/CountryResource.php index 049b30a..15c64f2 100644 --- a/src/Filament/Clusters/World/Resources/CountryResource.php +++ b/src/Filament/Clusters/World/Resources/CountryResource.php @@ -95,7 +95,39 @@ public static function form(Form $form): Form ->relationship('region', 'name', fn ($query) => $query->where('is_special', true)) ->searchable() ->preload() - ->label(__('eclipse-world::countries.form.special_regions.region_label')), + ->label(__('eclipse-world::countries.form.special_regions.region_label')) + ->rules([ + function ($get) { + return function (string $attribute, $value, \Closure $fail) use ($get) { + if (! $value) { + return; + } + + $countryId = $get('../../id'); + $currentRecordId = $get('id'); + + if (! $countryId) { + return; + } + + // Check for any existing membership with same country and region + $query = \Eclipse\World\Models\CountryInSpecialRegion::where('country_id', $countryId) + ->where('region_id', $value); + + // Exclude current record when editing + if ($currentRecordId) { + $query->where('id', '!=', $currentRecordId); + } + + if ($query->exists()) { + $regionName = \Eclipse\World\Models\Region::find($value)?->name ?? __('eclipse-world::countries.validation.unknown_region'); + $fail(__('eclipse-world::countries.validation.duplicate_special_region_membership', [ + 'region' => $regionName, + ])); + } + }; + }, + ]), DatePicker::make('start_date') ->required() @@ -156,10 +188,33 @@ public static function table(Table $table): Table ]) ->filters([ SelectFilter::make('region_id') - ->label(__('eclipse-world::countries.filters.region.label')) + ->label(__('eclipse-world::countries.filters.geographical_region.label')) ->relationship('region', 'name', fn ($query) => $query->where('is_special', false)) ->searchable() ->preload(), + SelectFilter::make('special_regions') + ->label(__('eclipse-world::countries.filters.special_region.label')) + ->options(function () { + return \Eclipse\World\Models\Region::where('is_special', true) + ->pluck('name', 'id') + ->toArray(); + }) + ->query(function (Builder $query, array $data): Builder { + if (! empty($data['value'])) { + return $query->whereHas('specialRegions', function (Builder $query) use ($data) { + $query->where('world_regions.id', $data['value']) + ->where('world_country_in_special_region.start_date', '<=', now()) + ->where(function ($query) { + $query->whereNull('world_country_in_special_region.end_date') + ->orWhere('world_country_in_special_region.end_date', '>=', now()); + }); + }); + } + + return $query; + }) + ->searchable() + ->preload(), TrashedFilter::make(), ]) ->actions([ diff --git a/src/Filament/Clusters/World/Resources/RegionResource.php b/src/Filament/Clusters/World/Resources/RegionResource.php index 386b44f..901972b 100644 --- a/src/Filament/Clusters/World/Resources/RegionResource.php +++ b/src/Filament/Clusters/World/Resources/RegionResource.php @@ -97,8 +97,25 @@ public static function table(Table $table): Table TextColumn::make('countries_count') ->label(__('eclipse-world::regions.table.countries_count.label')) - ->counts('countries') - ->sortable(), + ->getStateUsing(function (Region $record): int { + if ($record->is_special) { + return $record->getCountriesInSpecialRegion()->count(); + } + + return $record->countries()->count(); + }) + ->sortable(query: function (Builder $query, string $direction): Builder { + return $query->withCount([ + 'countries', + 'specialCountries' => function ($query) { + $query->where('world_country_in_special_region.start_date', '<=', now()) + ->where(function ($query) { + $query->whereNull('world_country_in_special_region.end_date') + ->orWhere('world_country_in_special_region.end_date', '>=', now()); + }); + }, + ])->orderBy('countries_count', $direction); + }), TextColumn::make('children_count') ->label(__('eclipse-world::regions.table.children_count.label')) diff --git a/src/Jobs/ImportCountries.php b/src/Jobs/ImportCountries.php index 76afd6d..653ec00 100644 --- a/src/Jobs/ImportCountries.php +++ b/src/Jobs/ImportCountries.php @@ -181,16 +181,44 @@ private function seedSpecialRegions(): void ]; $existingCountries = Country::whereIn('id', $euMemberCountries)->pluck('id'); - $membershipData = $existingCountries->mapWithKeys(fn ($countryId) => [ - $countryId => [ - 'start_date' => Carbon::now()->toDateString(), - 'created_at' => now(), - 'updated_at' => now(), - ], - ]); - // Clear existing memberships and add new ones - $euRegion->specialCountries()->detach(); - $euRegion->specialCountries()->attach($membershipData); + // Get current memberships + $currentMemberships = $euRegion->specialCountries() + ->wherePivot('start_date', '<=', Carbon::now()->toDateString()) + ->where(function ($query) { + $query->whereNull('world_country_in_special_region.end_date') + ->orWhere('world_country_in_special_region.end_date', '>=', Carbon::now()->toDateString()); + }) + ->pluck('world_countries.id') + ->toArray(); + + // Only update if there are changes needed + $countriesToAdd = $existingCountries->diff($currentMemberships); + $countriesToRemove = collect($currentMemberships)->diff($existingCountries); + + // Add new countries + if ($countriesToAdd->isNotEmpty()) { + $membershipData = $countriesToAdd->mapWithKeys(fn ($countryId) => [ + $countryId => [ + 'start_date' => Carbon::now()->toDateString(), + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + $euRegion->specialCountries()->attach($membershipData); + } + + // Remove countries that are no longer members + if ($countriesToRemove->isNotEmpty()) { + foreach ($countriesToRemove as $countryId) { + $euRegion->specialCountries() + ->wherePivot('country_id', $countryId) + ->whereNull('world_country_in_special_region.end_date') + ->updateExistingPivot($countryId, [ + 'end_date' => Carbon::now()->subDay()->toDateString(), + 'updated_at' => now(), + ]); + } + } } } From 2256591112077b6ef3ba4f399a851a80423bfc97 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Mon, 4 Aug 2025 10:23:22 +0200 Subject: [PATCH 6/9] chore(regions): format import countries --- src/Jobs/ImportCountries.php | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Jobs/ImportCountries.php b/src/Jobs/ImportCountries.php index 2b8704d..1868b61 100644 --- a/src/Jobs/ImportCountries.php +++ b/src/Jobs/ImportCountries.php @@ -6,13 +6,7 @@ use Eclipse\World\Models\Country; use Eclipse\World\Models\Region; 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 Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Log; class ImportCountries extends QueueableJob { @@ -22,8 +16,8 @@ class ImportCountries extends QueueableJob protected function execute(): void { - // First, import/update regions - $this->importRegions(); + // First, import/update regions + $this->importRegions(); // Load existing countries into an associative array $existingCountries = Country::withTrashed()->get()->keyBy('id'); @@ -46,7 +40,7 @@ protected function execute(): void 'num_code' => $rawData['ccn3'], 'name' => $rawData['name']['common'], 'flag' => $rawData['flag'], - 'region_id' => $this->getRegionIdForCountry($rawData), + 'region_id' => $this->getRegionIdForCountry($rawData), ]; if (isset($existingCountries[$data['id']])) { @@ -56,8 +50,8 @@ protected function execute(): void } } - // Seed special regions after countries are imported - $this->seedSpecialRegions(); + // Seed special regions after countries are imported + $this->seedSpecialRegions(); } protected function getJobName(): string From 9f90ca53edc1ae3f1273fd1d64e361c7865e520e Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 14 Aug 2025 11:31:45 +0200 Subject: [PATCH 7/9] chore(regions): add common pkg to deps --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 8fa8245..0147aaa 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "php": "^8.2", "bezhansalleh/filament-shield": "^3.3", "datalinx/php-utils": "^2.5", + "eclipsephp/common": "^1.0", "filament/filament": "^3.3", "laravel/framework": "^11.0", "spatie/laravel-package-tools": "^1.19" From 8bd88945ed2135160cdb323f1e55a4995ffc4cc2 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 14 Aug 2025 11:33:52 +0200 Subject: [PATCH 8/9] fix(composer): fix pkg version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0147aaa..763917f 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "php": "^8.2", "bezhansalleh/filament-shield": "^3.3", "datalinx/php-utils": "^2.5", - "eclipsephp/common": "^1.0", + "eclipsephp/common": "dev-main", "filament/filament": "^3.3", "laravel/framework": "^11.0", "spatie/laravel-package-tools": "^1.19" From 0512d889d2f7997359317b732be85c132fef9a0e Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 14 Aug 2025 13:32:27 +0200 Subject: [PATCH 9/9] chore(ci): fix fikament support version --- .github/workflows/test-runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 50dfb2c..2dbc09f 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -67,7 +67,7 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/support" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/support:^3.3" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress --no-interaction - name: Run test suite