diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php new file mode 100644 index 0000000..be19eb0 --- /dev/null +++ b/app/Http/Controllers/Admin/AdminSettingsController.php @@ -0,0 +1,77 @@ + $this->service->getSystemData(), + 'homepage' => $this->service->getHomepageData(), + 'socialMedia' => $this->service->getSocialMediaData(), + 'marketplace' => $this->service->getMarketplaceData(), + 'hr' => $this->service->getHRData(), + ]); + } + + public function updateSystem(UpdateSystemRequest $request): RedirectResponse + { + $this->service->updateSystem($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan sistem berhasil diperbarui.']); + + return to_route('admin.settings.index'); + } + + public function updateHomepage(UpdateHomepageRequest $request): RedirectResponse + { + $this->service->updateHomepage($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan homepage berhasil diperbarui.']); + + return to_route('admin.settings.index'); + } + + public function updateSocialMedia(UpdateSocialMediaRequest $request): RedirectResponse + { + $this->service->updateSocialMedia($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan media sosial berhasil diperbarui.']); + + return to_route('admin.settings.index'); + } + + public function updateMarketplace(UpdateMarketplaceRequest $request): RedirectResponse + { + $this->service->updateMarketplace($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan marketplace berhasil diperbarui.']); + + return to_route('admin.settings.index'); + } + + public function updateHR(UpdateHRRequest $request): RedirectResponse + { + $this->service->updateHR($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan hr berhasil diperbarui.']); + + return to_route('admin.settings.index'); + } +} diff --git a/app/Http/Requests/Admin/Settings/UpdateHRRequest.php b/app/Http/Requests/Admin/Settings/UpdateHRRequest.php new file mode 100644 index 0000000..d675547 --- /dev/null +++ b/app/Http/Requests/Admin/Settings/UpdateHRRequest.php @@ -0,0 +1,33 @@ + ['required', 'string', 'max:5'], + 'scheduled_check_out_time' => ['required', 'string', 'max:5'], + 'late_penalty_amount' => ['required', 'integer', 'min:0'], + 'absent_penalty_amount' => ['required', 'integer', 'min:0'], + ]; + } + + public function attributes(): array + { + return [ + 'scheduled_check_in_time' => 'jam masuk kerja', + 'scheduled_check_out_time' => 'jam pulang kerja', + 'late_penalty_amount' => 'denda keterlambatan', + 'absent_penalty_amount' => 'denda bolos', + ]; + } +} diff --git a/app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php b/app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php new file mode 100644 index 0000000..38a2543 --- /dev/null +++ b/app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php @@ -0,0 +1,32 @@ + ['nullable', 'string', 'max:2000'], + 'about_image_url' => ['nullable', 'string', 'max:2000'], + 'gallery_images' => ['nullable', 'array'], + 'gallery_images.*' => ['string', 'max:2000'], + ]; + } + + public function attributes(): array + { + return [ + 'hero_image_url' => 'url foto hero', + 'about_image_url' => 'url foto tentang kami', + 'gallery_images' => 'gambar galeri', + ]; + } +} diff --git a/app/Http/Requests/Admin/Settings/UpdateMarketplaceRequest.php b/app/Http/Requests/Admin/Settings/UpdateMarketplaceRequest.php new file mode 100644 index 0000000..1590c83 --- /dev/null +++ b/app/Http/Requests/Admin/Settings/UpdateMarketplaceRequest.php @@ -0,0 +1,108 @@ + ['required', 'string', 'in:per_transaksi,per_produk'], + "{$prefix}.type" => ['required', 'string', 'in:flat,persentase'], + "{$prefix}.value" => ['required', 'numeric', 'min:0'], + ]; + } + + public function rules(): array + { + $keys = [ + 'tiktok_shop_platform_commission', + 'tiktok_shop_logistics_service_fee', + 'tiktok_shop_dynamic_commission', + 'tiktok_shop_order_processing_fee', + 'tiktok_shop_affiliate', + 'tiktok_shop_pre_order_service_fee', + 'shopee_admin_fee', + 'shopee_program_fee', + 'shopee_shipping_savings', + 'shopee_premium', + 'shopee_service_fee', + 'shopee_order_processing_fee', + 'shopee_ams_commission_fee', + 'shopee_pre_order', + 'shopee_live_extra', + ]; + + $rules = []; + + foreach ($keys as $key) { + $rules[$key] = $this->feeRuleRules(); + $rules = array_merge($rules, $this->feeFieldRules($key)); + } + + return $rules; + } + + public function attributes(): array + { + return [ + 'tiktok_shop_platform_commission.base' => 'dasar komisi platform tiktok shop', + 'tiktok_shop_platform_commission.type' => 'tipe komisi platform tiktok shop', + 'tiktok_shop_platform_commission.value' => 'nilai komisi platform tiktok shop', + 'tiktok_shop_logistics_service_fee.base' => 'dasar layanan logistik tiktok shop', + 'tiktok_shop_logistics_service_fee.type' => 'tipe layanan logistik tiktok shop', + 'tiktok_shop_logistics_service_fee.value' => 'nilai layanan logistik tiktok shop', + 'tiktok_shop_dynamic_commission.base' => 'dasar komisi dinamis tiktok shop', + 'tiktok_shop_dynamic_commission.type' => 'tipe komisi dinamis tiktok shop', + 'tiktok_shop_dynamic_commission.value' => 'nilai komisi dinamis tiktok shop', + 'tiktok_shop_order_processing_fee.base' => 'dasar pemrosesan pesanan tiktok shop', + 'tiktok_shop_order_processing_fee.type' => 'tipe pemrosesan pesanan tiktok shop', + 'tiktok_shop_order_processing_fee.value' => 'nilai pemrosesan pesanan tiktok shop', + 'tiktok_shop_affiliate.base' => 'dasar affiliate tiktok shop', + 'tiktok_shop_affiliate.type' => 'tipe affiliate tiktok shop', + 'tiktok_shop_affiliate.value' => 'nilai affiliate tiktok shop', + 'tiktok_shop_pre_order_service_fee.base' => 'dasar layanan PO tiktok shop', + 'tiktok_shop_pre_order_service_fee.type' => 'tipe layanan PO tiktok shop', + 'tiktok_shop_pre_order_service_fee.value' => 'nilai layanan PO tiktok shop', + 'shopee_admin_fee.base' => 'dasar biaya administrasi shopee', + 'shopee_admin_fee.type' => 'tipe biaya administrasi shopee', + 'shopee_admin_fee.value' => 'nilai biaya administrasi shopee', + 'shopee_program_fee.base' => 'dasar biaya program shopee', + 'shopee_program_fee.type' => 'tipe biaya program shopee', + 'shopee_program_fee.value' => 'nilai biaya program shopee', + 'shopee_shipping_savings.base' => 'dasar hemat biaya kirim shopee', + 'shopee_shipping_savings.type' => 'tipe hemat biaya kirim shopee', + 'shopee_shipping_savings.value' => 'nilai hemat biaya kirim shopee', + 'shopee_premium.base' => 'dasar premi shopee', + 'shopee_premium.type' => 'tipe premi shopee', + 'shopee_premium.value' => 'nilai premi shopee', + 'shopee_service_fee.base' => 'dasar biaya layanan shopee', + 'shopee_service_fee.type' => 'tipe biaya layanan shopee', + 'shopee_service_fee.value' => 'nilai biaya layanan shopee', + 'shopee_order_processing_fee.base' => 'dasar biaya proses pesanan shopee', + 'shopee_order_processing_fee.type' => 'tipe biaya proses pesanan shopee', + 'shopee_order_processing_fee.value' => 'nilai biaya proses pesanan shopee', + 'shopee_ams_commission_fee.base' => 'dasar biaya komisi AMS shopee', + 'shopee_ams_commission_fee.type' => 'tipe biaya komisi AMS shopee', + 'shopee_ams_commission_fee.value' => 'nilai biaya komisi AMS shopee', + 'shopee_pre_order.base' => 'dasar PO shopee', + 'shopee_pre_order.type' => 'tipe PO shopee', + 'shopee_pre_order.value' => 'nilai PO shopee', + 'shopee_live_extra.base' => 'dasar live extra shopee', + 'shopee_live_extra.type' => 'tipe live extra shopee', + 'shopee_live_extra.value' => 'nilai live extra shopee', + ]; + } +} diff --git a/app/Http/Requests/Admin/Settings/UpdateSocialMediaRequest.php b/app/Http/Requests/Admin/Settings/UpdateSocialMediaRequest.php new file mode 100644 index 0000000..3898d58 --- /dev/null +++ b/app/Http/Requests/Admin/Settings/UpdateSocialMediaRequest.php @@ -0,0 +1,31 @@ + ['nullable', 'string', 'max:500'], + 'facebook_url' => ['nullable', 'string', 'max:500'], + 'tiktok_url' => ['nullable', 'string', 'max:500'], + ]; + } + + public function attributes(): array + { + return [ + 'instagram_url' => 'instagram', + 'facebook_url' => 'facebook', + 'tiktok_url' => 'tiktok', + ]; + } +} diff --git a/app/Http/Requests/Admin/Settings/UpdateSystemRequest.php b/app/Http/Requests/Admin/Settings/UpdateSystemRequest.php new file mode 100644 index 0000000..f6a27da --- /dev/null +++ b/app/Http/Requests/Admin/Settings/UpdateSystemRequest.php @@ -0,0 +1,35 @@ + ['required', 'string', 'max:255'], + 'about_app' => ['nullable', 'string', 'max:2000'], + 'address' => ['nullable', 'string', 'max:500'], + 'email' => ['nullable', 'email', 'max:255'], + 'phone' => ['nullable', 'string', 'max:20'], + ]; + } + + public function attributes(): array + { + return [ + 'app_name' => 'nama aplikasi', + 'about_app' => 'tentang aplikasi', + 'address' => 'alamat', + 'email' => 'email', + 'phone' => 'nomor telepon', + ]; + } +} diff --git a/app/Services/Admin/AdminSettingsService.php b/app/Services/Admin/AdminSettingsService.php new file mode 100644 index 0000000..51b80a5 --- /dev/null +++ b/app/Services/Admin/AdminSettingsService.php @@ -0,0 +1,146 @@ + $settings->app_name, + 'about_app' => $settings->about_app, + 'address' => $settings->address, + 'email' => $settings->email, + 'phone' => $settings->phone, + ]; + } + + public function getHomepageData(): array + { + $settings = app(HomepageSettings::class); + + return [ + 'hero_image_url' => $settings->hero_image_url, + 'about_image_url' => $settings->about_image_url, + 'gallery_images' => $settings->gallery_images, + ]; + } + + public function getSocialMediaData(): array + { + $settings = app(SocialMediaSettings::class); + + return [ + 'instagram_url' => $settings->instagram_url, + 'facebook_url' => $settings->facebook_url, + 'tiktok_url' => $settings->tiktok_url, + ]; + } + + public function getMarketplaceData(): array + { + $settings = app(MarketplaceSettings::class); + + return [ + 'tiktok_shop' => [ + 'platform_commission' => $settings->tiktok_shop_platform_commission->toArray(), + 'logistics_service_fee' => $settings->tiktok_shop_logistics_service_fee->toArray(), + 'dynamic_commission' => $settings->tiktok_shop_dynamic_commission->toArray(), + 'order_processing_fee' => $settings->tiktok_shop_order_processing_fee->toArray(), + 'affiliate' => $settings->tiktok_shop_affiliate->toArray(), + 'pre_order_service_fee' => $settings->tiktok_shop_pre_order_service_fee->toArray(), + ], + 'shopee' => [ + 'admin_fee' => $settings->shopee_admin_fee->toArray(), + 'program_fee' => $settings->shopee_program_fee->toArray(), + 'shipping_savings' => $settings->shopee_shipping_savings->toArray(), + 'premium' => $settings->shopee_premium->toArray(), + 'service_fee' => $settings->shopee_service_fee->toArray(), + 'order_processing_fee' => $settings->shopee_order_processing_fee->toArray(), + 'ams_commission_fee' => $settings->shopee_ams_commission_fee->toArray(), + 'pre_order' => $settings->shopee_pre_order->toArray(), + 'live_extra' => $settings->shopee_live_extra->toArray(), + ], + ]; + } + + public function getHRData(): array + { + $settings = app(HRSettings::class); + + return [ + 'scheduled_check_in_time' => $settings->scheduled_check_in_time, + 'scheduled_check_out_time' => $settings->scheduled_check_out_time, + 'late_penalty_amount' => $settings->late_penalty_amount, + 'absent_penalty_amount' => $settings->absent_penalty_amount, + ]; + } + + public function updateSystem(array $data): void + { + $settings = app(SystemSettings::class); + $settings->fill($data); + $settings->save(); + } + + public function updateHomepage(array $data): void + { + $settings = app(HomepageSettings::class); + $settings->fill($data); + $settings->save(); + } + + public function updateSocialMedia(array $data): void + { + $settings = app(SocialMediaSettings::class); + $settings->fill($data); + $settings->save(); + } + + public function updateMarketplace(array $data): void + { + $settings = app(MarketplaceSettings::class); + + $feeKeys = [ + 'tiktok_shop_platform_commission', + 'tiktok_shop_logistics_service_fee', + 'tiktok_shop_dynamic_commission', + 'tiktok_shop_order_processing_fee', + 'tiktok_shop_affiliate', + 'tiktok_shop_pre_order_service_fee', + 'shopee_admin_fee', + 'shopee_program_fee', + 'shopee_shipping_savings', + 'shopee_premium', + 'shopee_service_fee', + 'shopee_order_processing_fee', + 'shopee_ams_commission_fee', + 'shopee_pre_order', + 'shopee_live_extra', + ]; + + foreach ($feeKeys as $key) { + if (isset($data[$key])) { + $settings->{$key} = MarketplaceFeeRule::from($data[$key]); + } + } + + $settings->save(); + } + + public function updateHR(array $data): void + { + $settings = app(HRSettings::class); + $settings->fill($data); + $settings->save(); + } +} diff --git a/app/Settings/HRSettings.php b/app/Settings/HRSettings.php new file mode 100644 index 0000000..bb17062 --- /dev/null +++ b/app/Settings/HRSettings.php @@ -0,0 +1,18 @@ + $this->base, + 'type' => $this->type, + 'value' => (float) $this->value, + ]; + } +} diff --git a/composer.json b/composer.json index 2c94b3c..5ad144c 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,10 @@ "laravel/tinker": "^3.0", "laravel/wayfinder": "^0.1.14", "league/flysystem-aws-s3-v3": "^3.0", + "spatie/laravel-data": "^4.23", "spatie/laravel-medialibrary": "^11.23", "spatie/laravel-permission": "^8.3", + "spatie/laravel-settings": "^3.9", "spatie/laravel-sluggable": "^4.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 592ed49..b23bc25 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e3f219d587b4456806b7c536eab8b82f", + "content-hash": "5891ce301b1670b38277f3a3c2ac2426", "packages": [ { "name": "aws/aws-crt-php", @@ -4679,6 +4679,90 @@ }, "time": "2026-06-29T08:28:30+00:00" }, + { + "name": "spatie/laravel-data", + "version": "4.23.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-data.git", + "reference": "230543769c996e407fec2873930626aed7dd0d3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/230543769c996e407fec2873930626aed7dd0d3b", + "reference": "230543769c996e407fec2873930626aed7dd0d3b", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/reflection-docblock": "^5.3 || ^6.0", + "phpdocumentor/type-resolver": "^1.7 || ^2.0", + "spatie/laravel-package-tools": "^1.9.0", + "spatie/php-structure-discoverer": "^2.0" + }, + "require-dev": { + "fakerphp/faker": "^1.14", + "friendsofphp/php-cs-fixer": "^3.0", + "inertiajs/inertia-laravel": "^2.0|^3.0", + "livewire/livewire": "^3.0|^4.0", + "mockery/mockery": "^1.6", + "nesbot/carbon": "^2.63|^3.0", + "orchestra/testbench": "^8.37.0|^9.16|^10.9|^11.0", + "pestphp/pest": "^2.36|^3.8|^4.3", + "pestphp/pest-plugin-laravel": "^2.4|^3.0|^4.0", + "pestphp/pest-plugin-livewire": "^2.1|^3.0|^4.0", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "spatie/invade": "^1.0", + "spatie/laravel-typescript-transformer": "^2.5", + "spatie/pest-plugin-snapshots": "^2.1", + "spatie/test-time": "^1.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelData\\LaravelDataServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\LaravelData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "Create unified resources and data transfer objects", + "homepage": "https://github.com/spatie/laravel-data", + "keywords": [ + "laravel", + "laravel-data", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-data/issues", + "source": "https://github.com/spatie/laravel-data/tree/4.23.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-08T14:41:13+00:00" + }, { "name": "spatie/laravel-medialibrary", "version": "11.23.3", @@ -4938,6 +5022,91 @@ ], "time": "2026-07-03T15:36:01+00:00" }, + { + "name": "spatie/laravel-settings", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-settings.git", + "reference": "6c1351cf57c4ae96cd2313ae395c6d367dfeee01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-settings/zipball/6c1351cf57c4ae96cd2313ae395c6d367dfeee01", + "reference": "6c1351cf57c4ae96cd2313ae395c6d367dfeee01", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/database": "^11.0|^12.0|^13.0", + "php": "^8.2", + "phpdocumentor/type-resolver": "^1.5|^2.0", + "spatie/temporary-directory": "^1.3|^2.0" + }, + "require-dev": { + "ext-redis": "*", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^2.0|^3.0|^4.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/laravel-data": "^2.0.0|^4.0.0", + "spatie/pest-plugin-snapshots": "^2.0", + "spatie/phpunit-snapshot-assertions": "^4.2|^5.0", + "spatie/ray": "^1.36" + }, + "suggest": { + "spatie/data-transfer-object": "Allows for DTO casting to settings. (deprecated)" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelSettings\\LaravelSettingsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\LaravelSettings\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Store your application settings", + "homepage": "https://github.com/spatie/laravel-settings", + "keywords": [ + "laravel-settings", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-settings/issues", + "source": "https://github.com/spatie/laravel-settings/tree/3.9.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-26T13:18:06+00:00" + }, { "name": "spatie/laravel-sluggable", "version": "4.0.3", @@ -5016,6 +5185,85 @@ ], "time": "2026-07-28T13:16:49+00:00" }, + { + "name": "spatie/php-structure-discoverer", + "version": "2.4.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/php-structure-discoverer.git", + "reference": "fa2b7dae8e8a22c0306154c4b052420e054f7e2b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/fa2b7dae8e8a22c0306154c4b052420e054f7e2b", + "reference": "fa2b7dae8e8a22c0306154c4b052420e054f7e2b", + "shasum": "" + }, + "require": { + "illuminate/collections": "^11.0|^12.0|^13.0", + "php": "^8.3", + "spatie/laravel-package-tools": "^1.92.7", + "symfony/finder": "^6.0|^7.3.5|^8.0" + }, + "require-dev": { + "amphp/parallel": "^2.3.2", + "illuminate/console": "^11.0|^12.0|^13.0", + "nunomaduro/collision": "^7.0|^8.8.3", + "orchestra/testbench": "^9.5|^10.8|^11.0", + "pestphp/pest": "^3.8|^4.0", + "pestphp/pest-plugin-laravel": "^3.2|^4.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.2", + "spatie/laravel-ray": "^1.43.1" + }, + "suggest": { + "amphp/parallel": "When you want to use the Parallel discover worker" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\StructureDiscoverer\\StructureDiscovererServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\StructureDiscoverer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "Automatically discover structures within your PHP application", + "homepage": "https://github.com/spatie/php-structure-discoverer", + "keywords": [ + "discover", + "laravel", + "php", + "php-structure-discoverer" + ], + "support": { + "issues": "https://github.com/spatie/php-structure-discoverer/issues", + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.4.4" + }, + "funding": [ + { + "url": "https://github.com/LaravelAutoDiscoverer", + "type": "github" + } + ], + "time": "2026-06-15T07:14:32+00:00" + }, { "name": "spatie/temporary-directory", "version": "2.4.0", diff --git a/config/settings.php b/config/settings.php new file mode 100644 index 0000000..b6e0266 --- /dev/null +++ b/config/settings.php @@ -0,0 +1,100 @@ + [ + + ], + + /* + * The path where the settings classes will be created. + */ + 'setting_class_path' => app_path('Settings'), + + /* + * In these directories settings migrations will be stored and ran when migrating. A settings + * migration created via the make:settings-migration command will be stored in the first path or + * a custom defined path when running the command. + */ + 'migrations_paths' => [ + database_path('settings'), + ], + + /* + * When no repository was set for a settings class the following repository + * will be used for loading and saving settings. + */ + 'default_repository' => 'database', + + /* + * Settings will be stored and loaded from these repositories. + */ + 'repositories' => [ + 'database' => [ + 'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class, + 'model' => null, + 'table' => null, + 'connection' => null, + ], + 'redis' => [ + 'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class, + 'connection' => null, + 'prefix' => null, + ], + ], + + /* + * The encoder and decoder will determine how settings are stored and + * retrieved in the database. By default, `json_encode` and `json_decode` + * are used. + */ + 'encoder' => null, + 'decoder' => null, + + /* + * The contents of settings classes can be cached through your application, + * settings will be stored within a provided Laravel store and can have an + * additional prefix. + */ + 'cache' => [ + 'enabled' => (bool)env('SETTINGS_CACHE_ENABLED', false), + 'store' => null, + 'prefix' => null, + 'ttl' => null, + + /* + * When enabled, uses Laravel's memoized cache driver (requires Laravel 12.9+) + * to keep resolved values in memory during a single request. + */ + 'memo' => env('SETTINGS_CACHE_MEMO', false), + ], + + /* + * These global casts will be automatically used whenever a property within + * your settings class isn't a default PHP type. + */ + 'global_casts' => [ + DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class, + DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class, +// Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class, + Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class, + ], + + /* + * The package will look for settings in these paths and automatically + * register them. + */ + 'auto_discover_settings' => [ + app_path('Settings'), + ], + + /* + * Automatically discovered settings classes can be cached, so they don't + * need to be searched each time the application boots up. + */ + 'discovered_settings_cache_path' => base_path('bootstrap/cache'), +]; diff --git a/database/settings/2026_06_11_085959_create_app_settings.php b/database/settings/2026_06_11_085959_create_app_settings.php new file mode 100644 index 0000000..6eeb8f3 --- /dev/null +++ b/database/settings/2026_06_11_085959_create_app_settings.php @@ -0,0 +1,48 @@ +migrator->inGroup('system', function (SettingsBlueprint $blueprint): void { + $blueprint->add('app_name', config('app.name', 'DST Collection')); + $blueprint->add('address', 'Jl. Raya Cipeundeuy - Pabuaran No.909, Pabuaran, Kec. Pabuaran, Kabupaten Subang, Jawa Barat 41262'); + $blueprint->add('about_app', ''); + $blueprint->add('email', ''); + $blueprint->add('phone', ''); + $blueprint->add('logo', ''); + $blueprint->add('login_cover', ''); + }); + + $this->migrator->inGroup('social_media', function (SettingsBlueprint $blueprint): void { + $blueprint->add('instagram_url', null); + $blueprint->add('facebook_url', null); + $blueprint->add('tiktok_url', null); + }); + + $this->migrator->inGroup('marketplace', function (SettingsBlueprint $blueprint): void { + // TikTok Shop + $blueprint->add('tiktok_shop_platform_commission', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('tiktok_shop_logistics_service_fee', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('tiktok_shop_dynamic_commission', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('tiktok_shop_order_processing_fee', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('tiktok_shop_affiliate', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('tiktok_shop_pre_order_service_fee', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + + // Shopee + $blueprint->add('shopee_admin_fee', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('shopee_program_fee', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('shopee_shipping_savings', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('shopee_premium', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('shopee_service_fee', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('shopee_order_processing_fee', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('shopee_ams_commission_fee', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('shopee_pre_order', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + $blueprint->add('shopee_live_extra', MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + }); + } +}; diff --git a/database/settings/2026_06_11_120502_remove_system_media_from_settings.php b/database/settings/2026_06_11_120502_remove_system_media_from_settings.php new file mode 100644 index 0000000..d061e4f --- /dev/null +++ b/database/settings/2026_06_11_120502_remove_system_media_from_settings.php @@ -0,0 +1,15 @@ +migrator->inGroup('system', function (SettingsBlueprint $blueprint): void { + $blueprint->delete('logo'); + $blueprint->delete('login_cover'); + }); + } +}; diff --git a/database/settings/2026_06_17_000001_update_marketplace_fee_fields.php b/database/settings/2026_06_17_000001_update_marketplace_fee_fields.php new file mode 100644 index 0000000..c22dc98 --- /dev/null +++ b/database/settings/2026_06_17_000001_update_marketplace_fee_fields.php @@ -0,0 +1,70 @@ +migrator->inGroup('marketplace', function (SettingsBlueprint $blueprint): void { + $oldKeys = [ + 'tiktok_shop_admin_fee', + 'tiktok_shop_transaction_fee', + 'tiktok_shop_payment_fee', + 'tiktok_shop_affiliate_commission', + 'tiktok_shop_shipping_subsidy', + 'tiktok_shop_vat_rate', + 'shopee_commission_fee', + 'shopee_transaction_fee', + 'shopee_service_fee', + 'shopee_payment_fee', + 'shopee_affiliate_commission', + 'shopee_shipping_subsidy', + 'shopee_voucher_fee', + ]; + + $existing = DB::table('settings') + ->where('group', 'marketplace') + ->pluck('name') + ->toArray(); + + foreach ($oldKeys as $key) { + if (in_array($key, $existing)) { + $blueprint->delete($key); + } + } + + $newFields = [ + 'tiktok_shop_platform_commission', + 'tiktok_shop_logistics_service_fee', + 'tiktok_shop_dynamic_commission', + 'tiktok_shop_order_processing_fee', + 'tiktok_shop_affiliate', + 'tiktok_shop_pre_order_service_fee', + 'shopee_admin_fee', + 'shopee_program_fee', + 'shopee_shipping_savings', + 'shopee_premium', + 'shopee_service_fee', + 'shopee_order_processing_fee', + 'shopee_ams_commission_fee', + 'shopee_pre_order', + 'shopee_live_extra', + ]; + + $current = DB::table('settings') + ->where('group', 'marketplace') + ->pluck('name') + ->toArray(); + + foreach ($newFields as $key) { + if (! in_array($key, $current)) { + $blueprint->add($key, MarketplaceFeeRule::defaultPercent(0.0)->toArray()); + } + } + }); + } +}; diff --git a/database/settings/2026_06_18_100000_create_hr_settings.php b/database/settings/2026_06_18_100000_create_hr_settings.php new file mode 100644 index 0000000..ad4e83e --- /dev/null +++ b/database/settings/2026_06_18_100000_create_hr_settings.php @@ -0,0 +1,17 @@ +migrator->inGroup('hr', function (SettingsBlueprint $blueprint): void { + $blueprint->add('scheduled_check_in_time', '08:00'); + $blueprint->add('scheduled_check_out_time', '17:00'); + $blueprint->add('late_penalty_amount', 0); + $blueprint->add('absent_penalty_amount', 0); + }); + } +}; diff --git a/database/settings/2026_06_20_110000_create_homepage_settings.php b/database/settings/2026_06_20_110000_create_homepage_settings.php new file mode 100644 index 0000000..13cc1a1 --- /dev/null +++ b/database/settings/2026_06_20_110000_create_homepage_settings.php @@ -0,0 +1,89 @@ +migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): void { + // Hero Section + $blueprint->add('hero_badge', 'Koleksi Segar 2026'); + $blueprint->add('hero_title_line1', 'Ekspresikan'); + $blueprint->add('hero_title_line2', 'Gaya'); + $blueprint->add('hero_title_highlight', 'Segar Anda'); + $blueprint->add('hero_description', 'Selamat datang di {app_name}. Temukan keanggunan motif batik modern dan setelan pakaian santai berkualitas premium. Dirancang khusus dengan bahan adem yang menyejukkan aktivitas harian Anda.'); + $blueprint->add('hero_cta_primary_text', 'Beli Sekarang'); + $blueprint->add('hero_cta_secondary_text', 'Tentang Kami'); + $blueprint->add('hero_image_url', 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80'); + $blueprint->add('hero_bg_text_left', 'DST'); + $blueprint->add('hero_bg_text_right', 'Collection'); + + // Scroll Indicator + $blueprint->add('scroll_hashtag', '#DSTCollection'); + $blueprint->add('scroll_tagline', 'Bahan Adem & Lembut'); + + // Catalog Section + $blueprint->add('catalog_badge', 'Katalog Eksklusif'); + $blueprint->add('catalog_title', 'Koleksi Busana Pilihan'); + $blueprint->add('catalog_description', 'Gunakan kategori dan filter di bawah untuk menyesuaikan pencarian busana idaman Anda dengan mudah.'); + $blueprint->add('catalog_search_placeholder', 'Cari nama pakaian...'); + + // Deal Section + $blueprint->add('deal_badge', 'Promo Terbatas'); + $blueprint->add('deal_title', 'Penawaran Bulan Ini'); + $blueprint->add('deal_description', 'Jangan lewatkan promosi batik dan daster eksklusif kami! Dapatkan potongan harga spesial up to 25% untuk varian daster pilihan dan setelan pakaian modern. Dapatkan kenyamanan ekstra dengan bahan menyejukkan sebelum masa promo habis!'); + $blueprint->add('deal_cta_text', 'Jelajahi Produk Diskon'); + $blueprint->add('deal_images', [ + 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80', + 'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=800&auto=format&fit=crop&q=80', + 'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=80', + 'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80', + ]); + + // Order Guide Section + $blueprint->add('order_guide_badge', 'Langkah Pemesanan'); + $blueprint->add('order_guide_title', 'Cara Melakukan Pemesanan'); + $blueprint->add('order_guide_description', 'Sistem pemesanan kami sangat mudah dan terhubung langsung via WhatsApp untuk pelayanan cepat dan personal.'); + $blueprint->add('order_steps', [ + [ + 'title' => 'Pilih Produk', + 'description' => 'Jelajahi pakaian batik dan daster favorit Anda, lalu ketuk "Lihat Detail" untuk memeriksa ukuran.', + ], + [ + 'title' => 'Pilih Varian & Harga', + 'description' => 'Tentukan varian ukuran yang diinginkan dan pilih jenis harga (Eceran, Grosir, Agen, dll.).', + ], + [ + 'title' => 'Masukkan Keranjang', + 'description' => 'Masukkan ke Keranjang Belanja untuk menampung seluruh daftar pakaian yang ingin Anda beli.', + ], + [ + 'title' => 'Kirim ke WhatsApp', + 'description' => 'Klik tombol kirim pesanan, admin kami akan merespons rincian transfer bank dan pengiriman kurir.', + ], + ]); + + // About Section + $blueprint->add('about_badge', 'Tentang Kami'); + $blueprint->add('about_title', 'DST Collection'); + $blueprint->add('about_image_url', 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80'); + $blueprint->add('about_features', [ + 'Kain Rayon Super Tebal & Menyerap Keringat', + 'Motif Eksklusif & Tidak Pasaran', + 'Dukungan Penuh Layanan Admin Via WhatsApp', + ]); + + // Contact Section + $blueprint->add('contact_badge', 'Kontak Kami'); + $blueprint->add('contact_title', 'Ada Pertanyaan? Hubungi Kami'); + $blueprint->add('contact_description', 'Kami sangat senang mendengarkan pertanyaan Anda terkait spesifikasi produk, ketersediaan grosir, atau kemitraan. Hubungi tim admin kami melalui media di bawah.'); + $blueprint->add('contact_form_title', 'Kirim Pesan Cepat'); + + // Footer + $blueprint->add('footer_description', 'Galeri resmi {app_name}. Pilihan busana lokal premium berpotongan modern dengan kenyamanan menyejukkan.'); + $blueprint->add('footer_copyright', '© 2026 {app_name} DST Collection. Hak Cipta Dilindungi.'); + }); + } +}; diff --git a/database/settings/2026_06_20_120000_rename_deal_to_gallery_in_homepage.php b/database/settings/2026_06_20_120000_rename_deal_to_gallery_in_homepage.php new file mode 100644 index 0000000..29eeac2 --- /dev/null +++ b/database/settings/2026_06_20_120000_rename_deal_to_gallery_in_homepage.php @@ -0,0 +1,32 @@ +migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): void { + // Delete old deal fields + $blueprint->delete('deal_badge'); + $blueprint->delete('deal_title'); + $blueprint->delete('deal_description'); + $blueprint->delete('deal_cta_text'); + $blueprint->delete('deal_images'); + + // Add new gallery fields + $blueprint->add('gallery_badge', 'Galeri Kami'); + $blueprint->add('gallery_title', 'Koleksi Lookbook'); + $blueprint->add('gallery_description', 'Intip koleksi lookbook kami untuk inspirasi gaya sehari-hari. Padu padan batik modern dan daster yang nyaman untuk berbagai suasana.'); + $blueprint->add('gallery_images', [ + 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80', + 'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=800&auto=format&fit=crop&q=80', + 'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=80', + 'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80', + 'https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=800&auto=format&fit=crop&q=80', + 'https://images.unsplash.com/photo-1509631179647-0177331693ae?w=800&auto=format&fit=crop&q=80', + ]); + }); + } +}; diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index e217ae3..f28ac31 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -88,7 +88,7 @@ const hrItems: NavMenuItem[] = [ ]; const sistemItems: NavMenuItem[] = [ - { title: 'Pengaturan', href: '#', icon: Settings }, + { title: 'Pengaturan', href: '/admin/settings', icon: Settings }, { title: 'Log Aktivitas', href: '#', icon: Activity }, ]; diff --git a/resources/js/components/phone-number-input.tsx b/resources/js/components/phone-number-input.tsx index 029a26c..9cb3604 100644 --- a/resources/js/components/phone-number-input.tsx +++ b/resources/js/components/phone-number-input.tsx @@ -9,8 +9,8 @@ type PhoneNumberInputProps = { className?: string; }; -function formatPhone(value: string): string { - const digits = value.replace(/[^0-9]/g, ''); +function formatPhone(value: string | null | undefined): string { + const digits = (value ?? '').replace(/[^0-9]/g, ''); const groups: string[] = []; for (let i = 0; i < digits.length; i += 4) { groups.push(digits.slice(i, i + 4)); diff --git a/resources/js/components/ui/tabs.tsx b/resources/js/components/ui/tabs.tsx new file mode 100644 index 0000000..7f2c4f2 --- /dev/null +++ b/resources/js/components/ui/tabs.tsx @@ -0,0 +1,89 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Tabs as TabsPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const tabsListVariants = cva( + "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/resources/js/pages/admin/settings/index.tsx b/resources/js/pages/admin/settings/index.tsx new file mode 100644 index 0000000..1755d7f --- /dev/null +++ b/resources/js/pages/admin/settings/index.tsx @@ -0,0 +1,398 @@ +import InputError from '@/components/input-error'; +import { PhoneNumberInput } from '@/components/phone-number-input'; +import { RupiahInput } from '@/components/rupiah-input'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Separator } from '@/components/ui/separator'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; +import { updateHomepage, updateMarketplace, updateSocialMedia, updateSystem } from '@/routes/admin/settings'; +import { Form, Head } from '@inertiajs/react'; +import { useState } from 'react'; + +type MarketplaceFeeRule = { + base: string; + type: string; + value: number; +}; + +type Props = { + system: { + app_name: string; + about_app: string; + address: string; + email: string; + phone: string; + }; + homepage: { + hero_image_url: string; + about_image_url: string; + gallery_images: string[]; + }; + socialMedia: { + instagram_url: string; + facebook_url: string; + tiktok_url: string; + }; + marketplace: { + tiktok_shop: Record; + shopee: Record; + }; + hr: { + scheduled_check_in_time: string; + scheduled_check_out_time: string; + late_penalty_amount: number; + absent_penalty_amount: number; + }; +}; + +const sidebarTabs = [ + { key: 'sistem', label: 'Sistem' }, + { key: 'homepage', label: 'Homepage' }, + { key: 'media-sosial', label: 'Media Sosial' }, + { key: 'marketplace', label: 'Marketplace' }, + { key: 'hr', label: 'HR' }, +] as const; + +type TabKey = (typeof sidebarTabs)[number]['key']; + +function MarketplaceVariableInput({ label, prefix, data }: { label: string; prefix: string; data: MarketplaceFeeRule }) { + const [type, setType] = useState(data.type); + + return ( +
+ {label} +
+
+ Dasar + +
+ + +
+
+ + +
+
+
+
+ Tipe + +
+ + +
+
+ + +
+
+
+
+ Nilai + {type === 'flat' ? ( + + ) : ( + + )} +
+
+
+ ); +} + +function MarketplaceColumnHeader() { + return ( +
+ Biaya +
+ Dasar + Tipe + Nilai +
+
+ ); +} + +function MarketplaceCard({ title, variables }: { title: string; variables: { label: string; prefix: string; data: MarketplaceFeeRule }[] }) { + return ( + + + {title} + + + + {variables.map((v) => ( + + ))} + + + ); +} + +export default function AdminSettings({ system, homepage, socialMedia, marketplace, hr }: Props) { + const [activeTab, setActiveTab] = useState('sistem'); + const [marketplaceTab, setMarketplaceTab] = useState<'tiktok-shop' | 'shopee'>('tiktok-shop'); + + const tiktokShopVariables = [ + { label: 'Komisi Platform', prefix: 'tiktok_shop_platform_commission', data: marketplace.tiktok_shop.platform_commission }, + { label: 'Layanan Logistik', prefix: 'tiktok_shop_logistics_service_fee', data: marketplace.tiktok_shop.logistics_service_fee }, + { label: 'Komisi Dinamis', prefix: 'tiktok_shop_dynamic_commission', data: marketplace.tiktok_shop.dynamic_commission }, + { label: 'Pemrosesan Pesanan', prefix: 'tiktok_shop_order_processing_fee', data: marketplace.tiktok_shop.order_processing_fee }, + { label: 'Affiliate', prefix: 'tiktok_shop_affiliate', data: marketplace.tiktok_shop.affiliate }, + { label: 'Layanan PO', prefix: 'tiktok_shop_pre_order_service_fee', data: marketplace.tiktok_shop.pre_order_service_fee }, + ]; + + const shopeeVariables = [ + { label: 'Biaya Administrasi', prefix: 'shopee_admin_fee', data: marketplace.shopee.admin_fee }, + { label: 'Biaya Program', prefix: 'shopee_program_fee', data: marketplace.shopee.program_fee }, + { label: 'Hemat Biaya Kirim', prefix: 'shopee_shipping_savings', data: marketplace.shopee.shipping_savings }, + { label: 'Premi', prefix: 'shopee_premium', data: marketplace.shopee.premium }, + { label: 'Biaya Layanan', prefix: 'shopee_service_fee', data: marketplace.shopee.service_fee }, + { label: 'Biaya Proses Pesanan', prefix: 'shopee_order_processing_fee', data: marketplace.shopee.order_processing_fee }, + { label: 'Biaya Komisi AMS', prefix: 'shopee_ams_commission_fee', data: marketplace.shopee.ams_commission_fee }, + { label: 'PO', prefix: 'shopee_pre_order', data: marketplace.shopee.pre_order }, + { label: 'Live Extra', prefix: 'shopee_live_extra', data: marketplace.shopee.live_extra }, + ]; + + return ( + <> + + +
+
+
+

+ Pengaturan +

+
+
+ +
+ + + + +
+ {activeTab === 'sistem' && ( +
+ {({ processing, errors }) => ( +
+ + + Sistem + + +
+ + + +
+
+ + + +
+
+ + + +
+
+ +