diff --git a/app-modules/catalog/database/factories/EvCompanyFactory.php b/app-modules/catalog/database/factories/EvCompanyFactory.php new file mode 100644 index 0000000..d9fe011 --- /dev/null +++ b/app-modules/catalog/database/factories/EvCompanyFactory.php @@ -0,0 +1,32 @@ + + */ +class EvCompanyFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->company(), + 'mm_name' => fake()->company(), + 'slug' => fake()->unique()->slug(), + 'description' => fake()->sentence(), + 'mm_description' => null, + 'contact' => fake()->phoneNumber(), + 'address' => fake()->address(), + 'logo' => null, + 'is_active' => true, + ]; + } +} diff --git a/app-modules/catalog/database/migrations/2026_08_05_135107_create_ev_companies_table.php b/app-modules/catalog/database/migrations/2026_08_05_135107_create_ev_companies_table.php new file mode 100644 index 0000000..88e39c4 --- /dev/null +++ b/app-modules/catalog/database/migrations/2026_08_05_135107_create_ev_companies_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('name'); + $table->string('mm_name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->text('mm_description')->nullable(); + $table->string('contact'); + $table->string('address'); + $table->string('logo')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ev_companies'); + } +}; diff --git a/app-modules/catalog/src/CatalogPlugin.php b/app-modules/catalog/src/CatalogPlugin.php new file mode 100644 index 0000000..2082eac --- /dev/null +++ b/app-modules/catalog/src/CatalogPlugin.php @@ -0,0 +1,38 @@ +discoverResources( + in: __DIR__.'/Filament/Resources', + for: 'Modules\Catalog\Filament\Resources', + ) + ->discoverPages( + in: __DIR__.'/Filament/Pages', + for: 'Modules\Catalog\Filament\Pages', + ) + ->discoverWidgets( + in: __DIR__.'/Filament/Widgets', + for: 'Modules\Catalog\Filament\Widgets', + ); + } + + public function boot(Panel $panel): void {} + + public static function make(): static + { + return app(static::class); + } +} diff --git a/app-modules/catalog/src/Filament/Pages/.gitkeep b/app-modules/catalog/src/Filament/Pages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app-modules/catalog/src/Filament/Resources/.gitkeep b/app-modules/catalog/src/Filament/Resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app-modules/catalog/src/Filament/Resources/EvCompanies/EvCompanyResource.php b/app-modules/catalog/src/Filament/Resources/EvCompanies/EvCompanyResource.php new file mode 100644 index 0000000..c687054 --- /dev/null +++ b/app-modules/catalog/src/Filament/Resources/EvCompanies/EvCompanyResource.php @@ -0,0 +1,51 @@ + ListEvCompanies::route('/'), + 'create' => CreateEvCompany::route('/create'), + 'edit' => EditEvCompany::route('/{record}/edit'), + ]; + } +} diff --git a/app-modules/catalog/src/Filament/Resources/EvCompanies/Pages/CreateEvCompany.php b/app-modules/catalog/src/Filament/Resources/EvCompanies/Pages/CreateEvCompany.php new file mode 100644 index 0000000..a56b0c4 --- /dev/null +++ b/app-modules/catalog/src/Filament/Resources/EvCompanies/Pages/CreateEvCompany.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('name') + ->required() + ->maxLength(255), + TextInput::make('mm_name') + ->required() + ->maxLength(255), + Textarea::make('description') + ->columnSpanFull(), + Textarea::make('mm_description') + ->columnSpanFull(), + TextInput::make('contact') + ->required() + ->tel() + ->maxLength(255), + TextInput::make('address') + ->required() + ->maxLength(255), + FileUpload::make('logo') + ->image(), + Toggle::make('is_active') + ->required() + ->default(true), + ]); + } +} diff --git a/app-modules/catalog/src/Filament/Resources/EvCompanies/Tables/EvCompaniesTable.php b/app-modules/catalog/src/Filament/Resources/EvCompanies/Tables/EvCompaniesTable.php new file mode 100644 index 0000000..d46686a --- /dev/null +++ b/app-modules/catalog/src/Filament/Resources/EvCompanies/Tables/EvCompaniesTable.php @@ -0,0 +1,49 @@ +columns([ + ImageColumn::make('logo') + ->circular(), + TextColumn::make('name') + ->searchable() + ->sortable(), + TextColumn::make('slug') + ->searchable(), + TextColumn::make('contact') + ->searchable(), + IconColumn::make('is_active') + ->boolean(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + TernaryFilter::make('is_active'), + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app-modules/catalog/src/Filament/Widgets/.gitkeep b/app-modules/catalog/src/Filament/Widgets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app-modules/catalog/src/Models/EvCompany.php b/app-modules/catalog/src/Models/EvCompany.php new file mode 100644 index 0000000..d589be6 --- /dev/null +++ b/app-modules/catalog/src/Models/EvCompany.php @@ -0,0 +1,59 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'name', + 'mm_name', + 'slug', + 'description', + 'mm_description', + 'contact', + 'address', + 'logo', + 'is_active', + ]; + + protected static function booted(): void + { + static::creating(function (self $evCompany): void { + if (filled($evCompany->slug)) { + return; + } + + $slug = Str::slug($evCompany->name); + $uniqueSlug = $slug; + $suffix = 1; + + while (static::where('slug', $uniqueSlug)->exists()) { + $uniqueSlug = "{$slug}-{$suffix}"; + $suffix++; + } + + $evCompany->slug = $uniqueSlug; + }); + } + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'is_active' => 'boolean', + ]; + } +} diff --git a/app-modules/catalog/tests/Feature/EvCompanyResourceTest.php b/app-modules/catalog/tests/Feature/EvCompanyResourceTest.php new file mode 100644 index 0000000..ebe480a --- /dev/null +++ b/app-modules/catalog/tests/Feature/EvCompanyResourceTest.php @@ -0,0 +1,63 @@ +admin = User::factory()->create()->assignRole('admin'); + $this->actingAs($this->admin); +}); + +test('can list ev companies', function () { + $companies = EvCompany::factory()->count(3)->create(); + + Livewire::test(ListEvCompanies::class) + ->assertOk() + ->assertCanSeeTableRecords($companies); +}); + +test('can create an ev company', function () { + $company = EvCompany::factory()->make(); + + Livewire::test(CreateEvCompany::class) + ->fillForm([ + 'name' => $company->name, + 'mm_name' => $company->mm_name, + 'contact' => $company->contact, + 'address' => $company->address, + 'is_active' => true, + ]) + ->call('create') + ->assertNotified() + ->assertRedirect(); + + assertDatabaseHas(EvCompany::class, [ + 'name' => $company->name, + 'slug' => Str::slug($company->name), + ]); +}); + +test('can edit an ev company', function () { + $company = EvCompany::factory()->create(); + + Livewire::test(EditEvCompany::class, ['record' => $company->getRouteKey()]) + ->assertOk() + ->fillForm(['name' => 'Updated Name']) + ->call('save') + ->assertNotified(); + + assertDatabaseHas(EvCompany::class, [ + 'id' => $company->id, + 'name' => 'Updated Name', + ]); +}); diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 24491f4..b848310 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -19,6 +19,7 @@ use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; +use Modules\Catalog\CatalogPlugin; class AdminPanelProvider extends PanelProvider { @@ -38,7 +39,9 @@ class AdminPanelProvider extends PanelProvider NavigationGroup::make()->label('Operations'), NavigationGroup::make()->label('Access'), ]) - ->plugins([]) + ->plugins([ + CatalogPlugin::make(), + ]) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages') ->pages([