From f6118990cdb969b59b1780387bd2c2091b6b463f Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Wed, 24 Jun 2026 20:58:10 +0330 Subject: [PATCH] feat(admin): add global site settings Add Setting entity (model, migration, factory, Filament resource + pages, fa/en lang, tests) for global key/value site settings, unique on key. Seed footer/contact reference data via SettingSeeder. Co-authored-by: Cursor --- .../Filament/Resources/SettingResource.php | 119 ++++++++++++++++++ .../SettingResource/Pages/CreateSetting.php | 18 +++ .../SettingResource/Pages/EditSetting.php | 26 ++++ .../SettingResource/Pages/ListSettings.php | 21 ++++ admin/app/Models/Setting.php | 36 ++++++ admin/database/factories/SettingFactory.php | 27 ++++ ...026_06_20_000022_create_settings_table.php | 27 ++++ admin/database/seeders/DatabaseSeeder.php | 1 + admin/database/seeders/SettingSeeder.php | 112 +++++++++++++++++ admin/docs/IMPLEMENTATION.md | 2 +- admin/lang/en/setting.php | 17 +++ admin/lang/fa/setting.php | 17 +++ .../Filament/Resource/SettingResourceTest.php | 61 +++++++++ admin/tests/Feature/SettingTest.php | 19 +++ 14 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 admin/app/Filament/Resources/SettingResource.php create mode 100644 admin/app/Filament/Resources/SettingResource/Pages/CreateSetting.php create mode 100644 admin/app/Filament/Resources/SettingResource/Pages/EditSetting.php create mode 100644 admin/app/Filament/Resources/SettingResource/Pages/ListSettings.php create mode 100644 admin/app/Models/Setting.php create mode 100644 admin/database/factories/SettingFactory.php create mode 100644 admin/database/migrations/2026_06_20_000022_create_settings_table.php create mode 100644 admin/database/seeders/SettingSeeder.php create mode 100644 admin/lang/en/setting.php create mode 100644 admin/lang/fa/setting.php create mode 100644 admin/tests/Feature/Filament/Resource/SettingResourceTest.php create mode 100644 admin/tests/Feature/SettingTest.php diff --git a/admin/app/Filament/Resources/SettingResource.php b/admin/app/Filament/Resources/SettingResource.php new file mode 100644 index 00000000..102ccc4c --- /dev/null +++ b/admin/app/Filament/Resources/SettingResource.php @@ -0,0 +1,119 @@ +components([ + TextInput::make('key') + ->label(trans('setting.key')) + ->required() + ->maxLength(255) + ->unique(Setting::class, 'key', ignoreRecord: true), + TextInput::make('label') + ->label(trans('setting.label_field')) + ->maxLength(255), + Toggle::make('autoload') + ->label(trans('setting.autoload')) + ->default(true), + Textarea::make('content') + ->label(trans('setting.content')) + ->columnSpanFull(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->defaultSort('created_at', 'desc') + ->columns([ + TextColumn::make('id') + ->label('#') + ->sortable(), + TextColumn::make('key') + ->label(trans('setting.key')) + ->searchable(), + TextColumn::make('label') + ->label(trans('setting.label_field')) + ->searchable() + ->limit(30) + ->wrap(), + IconColumn::make('autoload') + ->label(trans('setting.autoload')) + ->boolean() + ->sortable(), + TextColumn::make('created_at') + ->label(trans('setting.created_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => ListSettings::route('/'), + 'create' => CreateSetting::route('/create'), + 'edit' => EditSetting::route('/{record}/edit'), + ]; + } +} diff --git a/admin/app/Filament/Resources/SettingResource/Pages/CreateSetting.php b/admin/app/Filament/Resources/SettingResource/Pages/CreateSetting.php new file mode 100644 index 00000000..46d08736 --- /dev/null +++ b/admin/app/Filament/Resources/SettingResource/Pages/CreateSetting.php @@ -0,0 +1,18 @@ +getResource()::getUrl('index'); + } +} diff --git a/admin/app/Filament/Resources/SettingResource/Pages/EditSetting.php b/admin/app/Filament/Resources/SettingResource/Pages/EditSetting.php new file mode 100644 index 00000000..ff8335d7 --- /dev/null +++ b/admin/app/Filament/Resources/SettingResource/Pages/EditSetting.php @@ -0,0 +1,26 @@ +getResource()::getUrl('index'); + } + + protected function getHeaderActions(): array + { + return [ + DeleteAction::make(), + ]; + } +} diff --git a/admin/app/Filament/Resources/SettingResource/Pages/ListSettings.php b/admin/app/Filament/Resources/SettingResource/Pages/ListSettings.php new file mode 100644 index 00000000..041db3b7 --- /dev/null +++ b/admin/app/Filament/Resources/SettingResource/Pages/ListSettings.php @@ -0,0 +1,21 @@ + */ + use HasFactory; + + protected $fillable = [ + 'key', + 'label', + 'content', + 'autoload', + ]; + + protected $casts = [ + 'autoload' => 'boolean', + ]; +} diff --git a/admin/database/factories/SettingFactory.php b/admin/database/factories/SettingFactory.php new file mode 100644 index 00000000..2ca7dae6 --- /dev/null +++ b/admin/database/factories/SettingFactory.php @@ -0,0 +1,27 @@ + + */ +class SettingFactory extends Factory +{ + /** + * @return array + */ + public function definition(): array + { + return [ + 'key' => fake()->unique()->slug(2), + 'label' => fake()->optional()->words(2, true), + 'content' => fake()->optional()->sentence(), + 'autoload' => fake()->boolean(), + ]; + } +} diff --git a/admin/database/migrations/2026_06_20_000022_create_settings_table.php b/admin/database/migrations/2026_06_20_000022_create_settings_table.php new file mode 100644 index 00000000..a20de24d --- /dev/null +++ b/admin/database/migrations/2026_06_20_000022_create_settings_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('key')->unique(); + $table->string('label')->nullable(); + $table->text('content')->nullable(); + $table->boolean('autoload')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('settings'); + } +}; diff --git a/admin/database/seeders/DatabaseSeeder.php b/admin/database/seeders/DatabaseSeeder.php index cc0c55bc..e43f6d22 100644 --- a/admin/database/seeders/DatabaseSeeder.php +++ b/admin/database/seeders/DatabaseSeeder.php @@ -21,6 +21,7 @@ public function run(): void AncestorSeeder::class, AttributeSeeder::class, AttributeGroupCategorySeeder::class, + SettingSeeder::class, ]); } } diff --git a/admin/database/seeders/SettingSeeder.php b/admin/database/seeders/SettingSeeder.php new file mode 100644 index 00000000..5e021106 --- /dev/null +++ b/admin/database/seeders/SettingSeeder.php @@ -0,0 +1,112 @@ +settings() as $setting) { + Setting::updateOrCreate( + ['key' => $setting['key']], + [ + 'label' => $setting['label'], + 'content' => $setting['content'], + 'autoload' => true, + ], + ); + } + } + + /** + * @return array + */ + private function settings(): array + { + return [ + [ + 'key' => 'site_name', + 'label' => 'نام فروشگاه', + 'content' => 'فروشگاه اینترنتی شاپ‌فلو', + ], + [ + 'key' => 'site_phone', + 'label' => 'شماره تماس', + 'content' => '021-91000000', + ], + [ + 'key' => 'site_email', + 'label' => 'ایمیل', + 'content' => 'support@shopflow.ir', + ], + [ + 'key' => 'site_address', + 'label' => 'آدرس', + 'content' => 'تهران، خیابان ولیعصر، بالاتر از میدان ونک، کوچه نگار، پلاک ۲۴، طبقه سوم، کد پستی: 1990783412', + ], + [ + 'key' => 'site_working_hours', + 'label' => 'ساعات کاری', + 'content' => $this->json([ + 'شنبه تا چهارشنبه از ساعت ۰۹:۰۰ الی ۱۸', + 'پنجشنبه و ایام تعطیل رسمی به جز جمعه‌ها از ساعت ۰۹:۰۰ الی ۱۴', + ]), + ], + [ + 'key' => 'footer_about', + 'label' => 'درباره فروشگاه (فوتر)', + 'content' => 'فروشگاه اینترنتی شاپ‌فلو که فعالیت خود را از سال ۱۳۹۸ آغاز کرده است، امروز به یک فروشگاه چندمنظوره و جامع تبدیل شده است. ما در شاپ‌فلو طیف گسترده‌ای از محصولات شامل پوشاک و لوازم متنوع برای مردان، زنان و کودکان، عطر و لوازم آرایشی و بهداشتی، کالای خواب و لوازم خانه و آشپزخانه، لوازم الکترونیک، تجهیزات ورزشی و ملزومات سفر را با بهترین قیمت در اختیار شما قرار می‌دهیم. فلسفه ما ایجاد یک تجربه خرید آنلاین سریع، آسان و مطمئن است که بر سه اصل استوار است: روش‌های پرداخت متنوع، هفت روز ضمانت بازگشت کالا و تضمین اصالت محصولات. هدف ما از روز نخست چیزی فراتر از فروش بوده است؛ ما به دنبال جلب رضایت مشتریان و بهبود مداوم کیفیت خدمات هستیم.', + ], + [ + 'key' => 'footer_copyright', + 'label' => 'متن کپی‌رایت', + 'content' => 'تمامی حقوق این وب‌سایت متعلق به فروشگاه اینترنتی شاپ‌فلو است.', + ], + [ + 'key' => 'footer_links_shop', + 'label' => 'لینک‌های فروشگاه (فوتر)', + 'content' => $this->json([ + ['label' => 'وبلاگ', 'url' => '/blog'], + ['label' => 'مردانه', 'url' => '/men'], + ['label' => 'زنانه', 'url' => '/women'], + ['label' => 'بچگانه', 'url' => '/kids'], + ['label' => 'آرایشی و بهداشتی', 'url' => '/beauty'], + ['label' => 'خرید هدیه', 'url' => '/gifts'], + ]), + ], + [ + 'key' => 'footer_links_support', + 'label' => 'لینک‌های پشتیبانی (فوتر)', + 'content' => $this->json([ + ['label' => 'درباره ما', 'url' => '/about-us'], + ['label' => 'پرسش‌های متداول', 'url' => '/faq'], + ['label' => 'شرایط بازگشت کالا', 'url' => '/return-conditions'], + ['label' => 'حریم خصوصی', 'url' => '/privacy'], + ['label' => 'همکاری با ما', 'url' => '/cooperation'], + ]), + ], + [ + 'key' => 'footer_socials', + 'label' => 'شبکه‌های اجتماعی (فوتر)', + 'content' => $this->json([ + ['name' => 'اینستاگرام', 'url' => 'https://instagram.com/shopflow'], + ['name' => 'تلگرام', 'url' => 'https://t.me/shopflow'], + ['name' => 'لینکدین', 'url' => 'https://linkedin.com/company/shopflow'], + ]), + ], + ]; + } + + /** + * @param array $value + */ + private function json(array $value): string + { + return (string) json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } +} diff --git a/admin/docs/IMPLEMENTATION.md b/admin/docs/IMPLEMENTATION.md index 107e17d0..70610cff 100644 --- a/admin/docs/IMPLEMENTATION.md +++ b/admin/docs/IMPLEMENTATION.md @@ -115,7 +115,7 @@ The main goal; depends on most of phases 1-3. - [ ] Guarantees + `guarantee_items` + `guarantee_item_images` + `guarantee_logs` + `guarantee_item_logs` - [ ] Accounting Sources - [ ] Persons -- [ ] Settings +- [x] Settings (global site key/value settings: model, migration, factory, idempotent reference seeder (footer/contact keys) in `DatabaseSeeder`, Filament resource (+ pages), fa/en lang, tests. Unique on `key`; `autoload` defaults true) - [ ] Cards / Saved Cards ## Phase 6 - Users extended & misc diff --git a/admin/lang/en/setting.php b/admin/lang/en/setting.php new file mode 100644 index 00000000..0e0da38a --- /dev/null +++ b/admin/lang/en/setting.php @@ -0,0 +1,17 @@ + 'Setting', + 'plural_label' => 'Settings', + 'navigation_group' => 'Settings', + 'subheading' => 'Site settings stored as key/value pairs.', + + 'key' => 'Key', + 'label_field' => 'Label', + 'content' => 'Value', + 'autoload' => 'Autoload', + 'created_at' => 'Created At', + 'updated_at' => 'Updated At', +]; diff --git a/admin/lang/fa/setting.php b/admin/lang/fa/setting.php new file mode 100644 index 00000000..8035e021 --- /dev/null +++ b/admin/lang/fa/setting.php @@ -0,0 +1,17 @@ + 'تنظیم', + 'plural_label' => 'تنظیمات', + 'navigation_group' => 'تنظیمات', + 'subheading' => 'تنظیمات سایت به صورت جفت کلید/مقدار.', + + 'key' => 'کلید', + 'label_field' => 'عنوان', + 'content' => 'مقدار', + 'autoload' => 'بارگذاری خودکار', + 'created_at' => 'تاریخ ایجاد', + 'updated_at' => 'تاریخ بروزرسانی', +]; diff --git a/admin/tests/Feature/Filament/Resource/SettingResourceTest.php b/admin/tests/Feature/Filament/Resource/SettingResourceTest.php new file mode 100644 index 00000000..2e6fd984 --- /dev/null +++ b/admin/tests/Feature/Filament/Resource/SettingResourceTest.php @@ -0,0 +1,61 @@ +assertOk(); +}); + +it('can list settings in the table.', function () { + $settings = Setting::factory()->count(5)->create(); + + livewire(SettingResource\Pages\ListSettings::class) + ->assertCanSeeTableRecords($settings); +}); + +it('can render edit setting page.', function () { + $setting = Setting::factory()->create(); + + get(SettingResource::getUrl('edit', [ + 'record' => $setting, + ]))->assertOk(); +}); + +it('can create setting model.', function () { + livewire(SettingResource\Pages\CreateSetting::class) + ->fillForm([ + 'key' => 'site_phone', + 'label' => 'شماره تماس', + 'content' => '021-00000000', + 'autoload' => true, + ]) + ->call('create') + ->assertHasNoFormErrors(); + + $this->assertDatabaseHas(Setting::class, [ + 'key' => 'site_phone', + 'content' => '021-00000000', + ]); +}); + +it('can delete setting model.', function () { + $setting = Setting::factory()->create(); + + livewire(SettingResource\Pages\EditSetting::class, [ + 'record' => $setting->getRouteKey(), + ]) + ->callAction(DeleteAction::class); + + $this->assertModelMissing($setting); +}); diff --git a/admin/tests/Feature/SettingTest.php b/admin/tests/Feature/SettingTest.php new file mode 100644 index 00000000..c9de68ef --- /dev/null +++ b/admin/tests/Feature/SettingTest.php @@ -0,0 +1,19 @@ +create(['autoload' => true]); + + expect($setting->refresh()->autoload)->toBeTrue(); +}); + +it('enforces a unique key.', function (): void { + Setting::factory()->create(['key' => 'site_phone']); + + expect(fn () => Setting::factory()->create(['key' => 'site_phone'])) + ->toThrow(QueryException::class); +});