diff --git a/admin/app/Filament/Resources/FaqResource.php b/admin/app/Filament/Resources/FaqResource.php new file mode 100644 index 00000000..b717b4b6 --- /dev/null +++ b/admin/app/Filament/Resources/FaqResource.php @@ -0,0 +1,107 @@ +components([ + TextInput::make('heading') + ->required() + ->maxLength(255) + ->columnSpanFull() + ->hintIcon('heroicon-o-information-circle') + ->hintIconTooltip('The question text shown to the visitor.'), + Textarea::make('content') + ->required() + ->rows(5) + ->columnSpanFull() + ->hintIcon('heroicon-o-information-circle') + ->hintIconTooltip('The answer to the question.'), + TextInput::make('order') + ->numeric() + ->default(0) + ->hintIcon('heroicon-o-information-circle') + ->hintIconTooltip('Display order — lower numbers appear first.'), + TextInput::make('position') + ->nullable() + ->maxLength(100) + ->hintIcon('heroicon-o-information-circle') + ->hintIconTooltip('Leave empty to show on the main FAQ page. Set a value (e.g. "homepage", "products") to display in that specific position.'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('heading') + ->limit(50) + ->wrap() + ->searchable(), + TextColumn::make('order') + ->sortable(), + TextColumn::make('position') + ->placeholder('FAQ page') + ->sortable(), + TextColumn::make('created_at') + ->dateTime() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('updated_at') + ->dateTime() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('order') + ->filters([ + // + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => ListFaqs::route('/'), + 'create' => CreateFaq::route('/create'), + 'edit' => EditFaq::route('/{record}/edit'), + ]; + } +} diff --git a/admin/app/Filament/Resources/FaqResource/Pages/CreateFaq.php b/admin/app/Filament/Resources/FaqResource/Pages/CreateFaq.php new file mode 100644 index 00000000..69d6c517 --- /dev/null +++ b/admin/app/Filament/Resources/FaqResource/Pages/CreateFaq.php @@ -0,0 +1,18 @@ +getResource()::getUrl('index'); + } +} diff --git a/admin/app/Filament/Resources/FaqResource/Pages/EditFaq.php b/admin/app/Filament/Resources/FaqResource/Pages/EditFaq.php new file mode 100644 index 00000000..0cebef42 --- /dev/null +++ b/admin/app/Filament/Resources/FaqResource/Pages/EditFaq.php @@ -0,0 +1,26 @@ +getResource()::getUrl('index'); + } + + protected function getHeaderActions(): array + { + return [ + DeleteAction::make(), + ]; + } +} diff --git a/admin/app/Filament/Resources/FaqResource/Pages/ListFaqs.php b/admin/app/Filament/Resources/FaqResource/Pages/ListFaqs.php new file mode 100644 index 00000000..52f2a299 --- /dev/null +++ b/admin/app/Filament/Resources/FaqResource/Pages/ListFaqs.php @@ -0,0 +1,23 @@ + + */ +class FaqFactory extends Factory +{ + /** + * @return array + */ + public function definition(): array + { + return [ + 'heading' => fake()->sentence(6), + 'content' => fake()->paragraph(), + 'order' => fake()->numberBetween(0, 100), + 'position' => fake()->optional()->randomElement(['homepage', 'products']), + ]; + } +} diff --git a/admin/database/migrations/2026_06_20_000007_create_faqs_table.php b/admin/database/migrations/2026_06_20_000007_create_faqs_table.php new file mode 100644 index 00000000..c1f0541d --- /dev/null +++ b/admin/database/migrations/2026_06_20_000007_create_faqs_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('heading'); + $table->text('content'); + $table->unsignedSmallInteger('order')->default(0); + $table->string('position')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('faqs'); + } +}; diff --git a/admin/database/seeders/FaqSeeder.php b/admin/database/seeders/FaqSeeder.php new file mode 100644 index 00000000..a50b70d1 --- /dev/null +++ b/admin/database/seeders/FaqSeeder.php @@ -0,0 +1,18 @@ +each->delete(); + + Faq::factory()->count(20)->create(); + } +} diff --git a/admin/database/seeders/TestSeeder.php b/admin/database/seeders/TestSeeder.php index 92f7dcce..d5afbcd1 100644 --- a/admin/database/seeders/TestSeeder.php +++ b/admin/database/seeders/TestSeeder.php @@ -21,6 +21,7 @@ public function run(): void SliderSeeder::class, MenuSeeder::class, PageSeeder::class, + FaqSeeder::class, ]); } } diff --git a/admin/docs/CACHE.md b/admin/docs/CACHE.md index dacaf4eb..e6d18529 100644 --- a/admin/docs/CACHE.md +++ b/admin/docs/CACHE.md @@ -22,6 +22,7 @@ Legend: `[ ]` not started, `[x]` implemented. | 6 | `products.category.{category_id}` | Published products list for a category page | 15 min | Product saved / deleted | | 7 | `varieties.product.{product_id}` | All varieties for a product (price, inventory, status) | 15 min | Variety saved / deleted | | 9 | `pages.{slug}` | Single published page record | 1 hour | Page saved / deleted | +| 10 | `faqs.{position}` | FAQs for a given position (null = main FAQ page) | 1 hour | FAQ saved / deleted | --- diff --git a/admin/docs/IMPLEMENTATION.md b/admin/docs/IMPLEMENTATION.md index a6c47d91..7b02a825 100644 --- a/admin/docs/IMPLEMENTATION.md +++ b/admin/docs/IMPLEMENTATION.md @@ -28,6 +28,7 @@ Catalog layer and platform basics. - [x] Sliders + Slides (each slide has one polymorphic image via `images` table) - [x] Menus + Menu Items (nested via `parent_id`; optional polymorphic image per item) - [x] Pages (CMS static pages; polymorphic image; SCHEDULED status with `published_at`) +- [x] FAQs (question + answer; `order` and nullable `position` for placement context) - [~] Addresses (model only, no resource yet) Sample data for manual admin testing lives in `TestSeeder` (`php artisan db:seed --class=TestSeeder`); `DatabaseSeeder` holds only necessary data. @@ -35,6 +36,8 @@ Sample data for manual admin testing lives in `TestSeeder` (`php artisan db:seed Cross-cutting improvements landed: - Navigation groups reorganized: Catalog / Promotions / Attribute / Content / Address. - `CACHE.md` added to track identified-but-not-implemented cache keys. +- `ColorPicker` added to Variety form; `ColorColumn` in the table. Auto-fill from attribute only triggers when `attribute_id` changes, preserving manual overrides. +- Documentation reorganized into `docs/` directory (`ShoFlow db doc.md`, `VARIETY_GUIDE.md`, `IMPLEMENTATION.md`, `CACHE.md`). ## Phase 0 - Finish current branch (`implement_variety`) @@ -66,7 +69,7 @@ Depend mostly on Images only. - [x] Sliders + Slides - [x] Menus + Menu Items - [x] Pages -- [ ] FAQs +- [x] FAQs - [ ] Reviews - [ ] Wishlists - [ ] Tags diff --git a/admin/tests/Feature/Filament/Resource/FaqResourceTest.php b/admin/tests/Feature/Filament/Resource/FaqResourceTest.php new file mode 100644 index 00000000..7094538e --- /dev/null +++ b/admin/tests/Feature/Filament/Resource/FaqResourceTest.php @@ -0,0 +1,85 @@ +assertOk(); +}); + +it('can list faqs in the table.', function () { + $faqs = Faq::factory()->count(5)->create(); + + livewire(FaqResource\Pages\ListFaqs::class) + ->assertCanSeeTableRecords($faqs); +}); + +it('can render edit page.', function () { + $faq = Faq::factory()->create(); + + get(FaqResource::getUrl('edit', [ + 'record' => $faq, + ]))->assertOk(); +}); + +it('can update faq model.', function () { + $faq = Faq::factory()->create(); + $newFaq = Faq::factory()->make(); + + livewire(FaqResource\Pages\EditFaq::class, [ + 'record' => $faq->getRouteKey(), + ]) + ->fillForm([ + 'heading' => $newFaq->heading, + 'content' => $newFaq->content, + 'order' => $newFaq->order, + 'position' => $newFaq->position, + ]) + ->call('save') + ->assertHasNoFormErrors(); + + expect($faq->refresh()) + ->heading->toBe($newFaq->heading) + ->content->toBe($newFaq->content) + ->order->toBe($newFaq->order); +}); + +it('can create faq model.', function () { + $newFaq = Faq::factory()->make(); + + livewire(FaqResource\Pages\CreateFaq::class) + ->fillForm([ + 'heading' => $newFaq->heading, + 'content' => $newFaq->content, + 'order' => $newFaq->order, + 'position' => $newFaq->position, + ]) + ->call('create') + ->assertHasNoFormErrors(); + + $this->assertDatabaseHas(Faq::class, [ + 'heading' => $newFaq->heading, + 'order' => $newFaq->order, + ]); +}); + +it('can delete faq model.', function () { + $faq = Faq::factory()->create(); + + livewire(FaqResource\Pages\EditFaq::class, [ + 'record' => $faq->getRouteKey(), + ]) + ->callAction(DeleteAction::class); + + $this->assertModelMissing($faq); +});