Add system settings management features including controllers for managing system, social media, and marketplace settings. Implement request validation for each settings type and create corresponding UI components for user interaction. Update permissions in the Permission and Role enums to manage access to settings functionalities, and enhance the sidebar for navigation to the new settings sections.

This commit is contained in:
Yoga Pangestu 2026-06-11 18:30:21 +07:00
parent 9798150994
commit 67c27c2954
26 changed files with 1609 additions and 161 deletions

View File

@ -77,6 +77,9 @@ enum Permission: string
case PAYROLL_ADJUST = 'payroll.adjust';
case PAYROLL_CLOSE = 'payroll.close';
case SETTINGS_VIEW = 'settings.view';
case SETTINGS_UPDATE = 'settings.update';
public function label(): string
{
return match ($this) {
@ -148,6 +151,9 @@ public function label(): string
self::PAYROLL_PAY => 'Bayar Gaji',
self::PAYROLL_ADJUST => 'Sesuaikan Gaji',
self::PAYROLL_CLOSE => 'Tutup Periode Gaji',
self::SETTINGS_VIEW => 'Lihat Pengaturan Aplikasi',
self::SETTINGS_UPDATE => 'Ubah Pengaturan Aplikasi',
};
}
@ -179,6 +185,7 @@ public function group(): string
self::EMPLOYEE_ADVANCES_PAY => 'Kasbon',
self::PAYROLL_VIEW, self::PAYROLL_PAY, self::PAYROLL_ADJUST,
self::PAYROLL_CLOSE => 'Gaji',
self::SETTINGS_VIEW, self::SETTINGS_UPDATE => 'Pengaturan Aplikasi',
};
}

View File

@ -141,6 +141,8 @@ public function permissions(): array
Permission::PAYROLL_PAY,
Permission::PAYROLL_ADJUST,
Permission::PAYROLL_CLOSE,
Permission::SETTINGS_VIEW,
Permission::SETTINGS_UPDATE,
],
self::ADMIN_BAHAN_BAKU => [
Permission::DASHBOARD_VIEW,

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Admin\System;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\System\Setting\MarketplaceRequest;
use App\Http\Requests\Admin\System\Setting\SocialMediaRequest;
use App\Http\Requests\Admin\System\Setting\SystemRequest;
use App\Services\System\Setting\MarketplaceService;
use App\Services\System\Setting\SocialMediaService;
use App\Services\System\Setting\SystemService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class SettingController extends Controller
{
public function __construct(
private readonly SystemService $systemService,
private readonly SocialMediaService $socialMediaService,
private readonly MarketplaceService $marketplaceService,
) {}
public function index(): Response
{
return Inertia::render('admin/system/setting/Index', [
'system' => $this->systemService->systemData(),
'socialMedia' => $this->socialMediaService->socialMediaData(),
'marketplace' => $this->marketplaceService->marketplaceData(),
]);
}
public function updateSystem(SystemRequest $request): RedirectResponse
{
$this->systemService->updateSystem($request->validated());
Inertia::flash('success', 'Pengaturan sistem berhasil disimpan.');
return redirect()->route('admin.system.setting.index');
}
public function updateSocialMedia(SocialMediaRequest $request): RedirectResponse
{
$this->socialMediaService->updateSocialMedia($request->validated());
Inertia::flash('success', 'Pengaturan media sosial berhasil disimpan.');
return redirect()->route('admin.system.setting.index');
}
public function updateMarketplace(MarketplaceRequest $request): RedirectResponse
{
$this->marketplaceService->updateMarketplace($request->validated());
Inertia::flash('success', 'Pengaturan marketplace berhasil disimpan.');
return redirect()->route('admin.system.setting.index');
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests\Admin\System\Setting;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class MarketplaceRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::SETTINGS_UPDATE->value) ?? false;
}
public function rules(): array
{
return [
'tiktok_shop_enabled' => ['required', 'boolean'],
'tiktok_shop_admin_fee' => ['required', 'numeric', 'min:0', 'max:100'],
'tiktok_shop_transaction_fee' => ['required', 'numeric', 'min:0', 'max:100'],
'tiktok_shop_payment_fee' => ['required', 'numeric', 'min:0', 'max:100'],
'tiktok_shop_affiliate_commission' => ['required', 'numeric', 'min:0', 'max:100'],
'tiktok_shop_shipping_subsidy' => ['required', 'numeric', 'min:0', 'max:100'],
'tiktok_shop_vat_rate' => ['required', 'numeric', 'min:0', 'max:100'],
'tiktok_shop_url' => ['nullable', 'url', 'max:500'],
'tiktok_shop_id' => ['nullable', 'string', 'max:100'],
'shopee_enabled' => ['required', 'boolean'],
'shopee_commission_fee' => ['required', 'numeric', 'min:0', 'max:100'],
'shopee_transaction_fee' => ['required', 'numeric', 'min:0', 'max:100'],
'shopee_service_fee' => ['required', 'numeric', 'min:0', 'max:100'],
'shopee_payment_fee' => ['required', 'numeric', 'min:0', 'max:100'],
'shopee_affiliate_commission' => ['required', 'numeric', 'min:0', 'max:100'],
'shopee_shipping_subsidy' => ['required', 'numeric', 'min:0', 'max:100'],
'shopee_voucher_fee' => ['required', 'numeric', 'min:0', 'max:100'],
'shopee_shop_url' => ['nullable', 'url', 'max:500'],
'shopee_shop_id' => ['nullable', 'string', 'max:100'],
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Admin\System\Setting;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class SocialMediaRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::SETTINGS_UPDATE->value) ?? false;
}
public function rules(): array
{
return [
'instagram_url' => ['nullable', 'url', 'max:100'],
'facebook_url' => ['nullable', 'url', 'max:100'],
'tiktok_url' => ['nullable', 'url', 'max:100'],
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Admin\System\Setting;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class SystemRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::SETTINGS_UPDATE->value) ?? false;
}
public function rules(): array
{
return [
'app_name' => ['required', 'string', 'max:100'],
'about_app' => ['nullable', 'string'],
'email' => ['nullable', 'email', 'max:100'],
'phone' => ['nullable', 'string', 'max:20'],
'logo' => ['nullable', 'image', 'max:2048'],
'login_cover' => ['nullable', 'image', 'max:5120'],
];
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Services\System\Setting;
use App\Settings\MarketplaceSettings;
class MarketplaceService
{
public function marketplaceData(): array
{
$settings = app(MarketplaceSettings::class);
return [
'tiktok_shop_enabled' => $settings->tiktok_shop_enabled,
'tiktok_shop_admin_fee' => $settings->tiktok_shop_admin_fee,
'tiktok_shop_transaction_fee' => $settings->tiktok_shop_transaction_fee,
'tiktok_shop_payment_fee' => $settings->tiktok_shop_payment_fee,
'tiktok_shop_affiliate_commission' => $settings->tiktok_shop_affiliate_commission,
'tiktok_shop_shipping_subsidy' => $settings->tiktok_shop_shipping_subsidy,
'tiktok_shop_vat_rate' => $settings->tiktok_shop_vat_rate,
'tiktok_shop_url' => $settings->tiktok_shop_url,
'tiktok_shop_id' => $settings->tiktok_shop_id,
'shopee_enabled' => $settings->shopee_enabled,
'shopee_commission_fee' => $settings->shopee_commission_fee,
'shopee_transaction_fee' => $settings->shopee_transaction_fee,
'shopee_service_fee' => $settings->shopee_service_fee,
'shopee_payment_fee' => $settings->shopee_payment_fee,
'shopee_affiliate_commission' => $settings->shopee_affiliate_commission,
'shopee_shipping_subsidy' => $settings->shopee_shipping_subsidy,
'shopee_voucher_fee' => $settings->shopee_voucher_fee,
'shopee_shop_url' => $settings->shopee_shop_url,
'shopee_shop_id' => $settings->shopee_shop_id,
];
}
public function updateMarketplace(array $validated): void
{
$settings = app(MarketplaceSettings::class);
$settings->tiktok_shop_enabled = $validated['tiktok_shop_enabled'];
$settings->tiktok_shop_admin_fee = $validated['tiktok_shop_admin_fee'];
$settings->tiktok_shop_transaction_fee = $validated['tiktok_shop_transaction_fee'];
$settings->tiktok_shop_payment_fee = $validated['tiktok_shop_payment_fee'];
$settings->tiktok_shop_affiliate_commission = $validated['tiktok_shop_affiliate_commission'];
$settings->tiktok_shop_shipping_subsidy = $validated['tiktok_shop_shipping_subsidy'];
$settings->tiktok_shop_vat_rate = $validated['tiktok_shop_vat_rate'];
$settings->tiktok_shop_url = $validated['tiktok_shop_url'] ?? null;
$settings->tiktok_shop_id = $validated['tiktok_shop_id'] ?? null;
$settings->shopee_enabled = $validated['shopee_enabled'];
$settings->shopee_commission_fee = $validated['shopee_commission_fee'];
$settings->shopee_transaction_fee = $validated['shopee_transaction_fee'];
$settings->shopee_service_fee = $validated['shopee_service_fee'];
$settings->shopee_payment_fee = $validated['shopee_payment_fee'];
$settings->shopee_affiliate_commission = $validated['shopee_affiliate_commission'];
$settings->shopee_shipping_subsidy = $validated['shopee_shipping_subsidy'];
$settings->shopee_voucher_fee = $validated['shopee_voucher_fee'];
$settings->shopee_shop_url = $validated['shopee_shop_url'] ?? null;
$settings->shopee_shop_id = $validated['shopee_shop_id'] ?? null;
$settings->save();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Services\System\Setting;
use App\Settings\SocialMediaSettings;
class SocialMediaService
{
public function socialMediaData(): array
{
$settings = app(SocialMediaSettings::class);
return [
'instagram_url' => $settings->instagram_url,
'facebook_url' => $settings->facebook_url,
'tiktok_url' => $settings->tiktok_url,
];
}
public function updateSocialMedia(array $validated): void
{
$settings = app(SocialMediaSettings::class);
$settings->instagram_url = $validated['instagram_url'] ?? null;
$settings->facebook_url = $validated['facebook_url'] ?? null;
$settings->tiktok_url = $validated['tiktok_url'] ?? null;
$settings->save();
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Services\System\Setting;
use App\Settings\SystemSettings;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class SystemService
{
public function systemData(): array
{
$settings = app(SystemSettings::class);
return [
'app_name' => $settings->app_name,
'about_app' => $settings->about_app,
'email' => $settings->email,
'phone' => $settings->phone,
'logo' => $settings->logo,
'logo_url' => $settings->logo ? Storage::disk('public')->url($settings->logo) : null,
'login_cover' => $settings->login_cover,
'login_cover_url' => $settings->login_cover ? Storage::disk('public')->url($settings->login_cover) : null,
];
}
public function updateSystem(array $validated): void
{
$settings = app(SystemSettings::class);
$settings->app_name = $validated['app_name'];
$settings->about_app = $validated['about_app'] ?? null;
$settings->email = $validated['email'] ?? null;
$settings->phone = $validated['phone'] ?? null;
if (isset($validated['logo']) && $validated['logo'] instanceof UploadedFile) {
Storage::disk('public')->delete($settings->logo);
$settings->logo = $validated['logo']->store('settings/system', 'public');
}
if (isset($validated['login_cover']) && $validated['login_cover'] instanceof UploadedFile) {
Storage::disk('public')->delete($settings->login_cover);
$settings->login_cover = $validated['login_cover']->store('settings/system', 'public');
}
$settings->save();
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class MarketplaceSettings extends Settings
{
public bool $tiktok_shop_enabled;
public float $tiktok_shop_admin_fee;
public float $tiktok_shop_transaction_fee;
public float $tiktok_shop_payment_fee;
public float $tiktok_shop_affiliate_commission;
public float $tiktok_shop_shipping_subsidy;
public float $tiktok_shop_vat_rate;
public ?string $tiktok_shop_url;
public ?string $tiktok_shop_id;
public bool $shopee_enabled;
public float $shopee_commission_fee;
public float $shopee_transaction_fee;
public float $shopee_service_fee;
public float $shopee_payment_fee;
public float $shopee_affiliate_commission;
public float $shopee_shipping_subsidy;
public float $shopee_voucher_fee;
public ?string $shopee_shop_url;
public ?string $shopee_shop_id;
public static function group(): string
{
return 'marketplace';
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class SocialMediaSettings extends Settings
{
public ?string $instagram_url;
public ?string $facebook_url;
public ?string $tiktok_url;
public static function group(): string
{
return 'social_media';
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class SystemSettings extends Settings
{
public string $app_name;
public ?string $about_app;
public ?string $email;
public ?string $phone;
public ?string $logo;
public ?string $login_cover;
public static function group(): string
{
return 'system';
}
}

View File

@ -15,6 +15,7 @@
"laravel/tinker": "^3.0",
"laravel/wayfinder": "^0.1.14",
"spatie/laravel-permission": "^8.0",
"spatie/laravel-settings": "^3.9",
"spatie/laravel-sluggable": "^4.0"
},
"require-dev": {

466
composer.lock generated
View File

@ -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": "dd4192a3bb321ccbee15386b55196fc5",
"content-hash": "25e9e3d1a32e479b704c046ede69c405",
"packages": [
{
"name": "brick/math",
@ -210,6 +210,54 @@
},
"time": "2024-07-08T12:26:09+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.6",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=14"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^14",
"phpstan/phpstan": "1.4.10 || 2.1.30",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
},
"time": "2026-02-07T07:09:04+00:00"
},
{
"name": "doctrine/inflector",
"version": "2.1.0",
@ -2677,6 +2725,117 @@
],
"time": "2026-02-16T23:10:27+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
"homepage": "http://www.phpdoc.org",
"keywords": [
"FQSEN",
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
},
"time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9",
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^1.0",
"php": "^7.4 || ^8.0",
"phpdocumentor/reflection-common": "^2.0",
"phpstan/phpdoc-parser": "^2.0"
},
"require-dev": {
"ext-tokenizer": "*",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psalm/phar": "^4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0"
},
"time": "2026-01-06T21:53:42+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",
@ -3636,6 +3795,91 @@
],
"time": "2026-05-30T19:30:22+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.2",
@ -3714,6 +3958,67 @@
],
"time": "2026-04-30T17:28:09+00:00"
},
{
"name": "spatie/temporary-directory",
"version": "2.3.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/temporary-directory.git",
"reference": "662e481d6ec07ef29fd05010433428851a42cd07"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/temporary-directory/zipball/662e481d6ec07ef29fd05010433428851a42cd07",
"reference": "662e481d6ec07ef29fd05010433428851a42cd07",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\TemporaryDirectory\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alex Vanderbist",
"email": "alex@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Easily create, use and destroy temporary directories",
"homepage": "https://github.com/spatie/temporary-directory",
"keywords": [
"php",
"spatie",
"temporary-directory"
],
"support": {
"issues": "https://github.com/spatie/temporary-directory/issues",
"source": "https://github.com/spatie/temporary-directory/tree/2.3.1"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-01-12T07:42:22+00:00"
},
{
"name": "symfony/clock",
"version": "v8.1.0",
@ -6646,54 +6951,6 @@
],
"time": "2024-05-06T16:37:16+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.6",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=14"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^14",
"phpstan/phpstan": "1.4.10 || 2.1.30",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
},
"time": "2026-02-07T07:09:04+00:00"
},
{
"name": "fakerphp/faker",
"version": "v1.24.1",
@ -8180,59 +8437,6 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
"homepage": "http://www.phpdoc.org",
"keywords": [
"FQSEN",
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
},
"time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "6.0.3",
@ -8298,64 +8502,6 @@
},
"time": "2026-03-18T20:49:53+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9",
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^1.0",
"php": "^7.4 || ^8.0",
"phpdocumentor/reflection-common": "^2.0",
"phpstan/phpdoc-parser": "^2.0"
},
"require-dev": {
"ext-tokenizer": "*",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psalm/phar": "^4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0"
},
"time": "2026-01-06T21:53:42+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "12.5.7",

107
config/settings.php Normal file
View File

@ -0,0 +1,107 @@
<?php
use Spatie\LaravelData\Data;
use Spatie\LaravelSettings\SettingsCasts\DataCast;
use Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast;
use Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast;
use Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository;
use Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository;
return [
/*
* Each settings class used in your application must be registered, you can
* put them (manually) here.
*/
'settings' => [
],
/*
* 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' => DatabaseSettingsRepository::class,
'model' => null,
'table' => null,
'connection' => null,
],
'redis' => [
'type' => 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 => DateTimeInterfaceCast::class,
DateTimeZone::class => DateTimeZoneCast::class,
// Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class,
Data::class => 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'),
];

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create(config('settings.repositories.database.table') ?? 'settings', function (Blueprint $table): void {
$table->id();
$table->string('group');
$table->string('name');
$table->boolean('locked')->default(false);
$table->json('payload');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
$table->unique(['group', 'name']);
});
}
};

View File

@ -0,0 +1,48 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->inGroup('system', function (SettingsBlueprint $blueprint): void {
$blueprint->add('app_name', config('app.name', 'DST Collection'));
$blueprint->add('about_app', null);
$blueprint->add('email', null);
$blueprint->add('phone', null);
$blueprint->add('logo', null);
$blueprint->add('login_cover', null);
});
$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 {
$blueprint->add('tiktok_shop_enabled', true);
$blueprint->add('tiktok_shop_admin_fee', 5.0);
$blueprint->add('tiktok_shop_transaction_fee', 1.0);
$blueprint->add('tiktok_shop_payment_fee', 0.7);
$blueprint->add('tiktok_shop_affiliate_commission', 0.0);
$blueprint->add('tiktok_shop_shipping_subsidy', 0.0);
$blueprint->add('tiktok_shop_vat_rate', 11.0);
$blueprint->add('tiktok_shop_url', null);
$blueprint->add('tiktok_shop_id', null);
$blueprint->add('shopee_enabled', true);
$blueprint->add('shopee_commission_fee', 6.0);
$blueprint->add('shopee_transaction_fee', 2.0);
$blueprint->add('shopee_service_fee', 0.0);
$blueprint->add('shopee_payment_fee', 0.0);
$blueprint->add('shopee_affiliate_commission', 0.0);
$blueprint->add('shopee_shipping_subsidy', 0.0);
$blueprint->add('shopee_voucher_fee', 0.0);
$blueprint->add('shopee_shop_url', null);
$blueprint->add('shopee_shop_id', null);
});
}
};

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import { Banknote, CalendarDays, Clock, FolderTree, Layers, LayoutDashboard, Package, Receipt, User, UserCheck, Users, Wallet, WalletCards } from '@lucide/vue';
import { Banknote, CalendarDays, Clock, FolderTree, Layers, LayoutDashboard, Package, Receipt, Settings2, User, UserCheck, Users, Wallet, WalletCards } from '@lucide/vue';
import { computed } from 'vue';
import {
Sidebar,
@ -40,6 +40,8 @@ const showMasterMenu = computed(() => (
can('categories.view') || can('products.view') || can('raw-materials.view')
|| can('suppliers.view') || can('customers.view')
));
const isSettingActive = computed(() => page.url.startsWith('/admin/system/setting'));
const showSystemMenu = computed(() => can('settings.view'));
</script>
<template>
@ -190,6 +192,21 @@ const showMasterMenu = computed(() => (
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup v-if="showSystemMenu">
<SidebarGroupLabel>Sistem</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-if="can('settings.view')">
<SidebarMenuButton as-child tooltip="Pengaturan" :is-active="isSettingActive">
<Link href="/admin/system/setting">
<Settings2 />
<span>Pengaturan</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import { ImagePlus, X } from '@lucide/vue';
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
const props = defineProps<{
id: string;
label: string;
description?: string;
currentUrl?: string | null;
errors?: string[];
accept?: string;
}>();
const model = defineModel<File | null>();
const previewUrl = ref<string | null>(null);
const displayUrl = computed(() => previewUrl.value ?? props.currentUrl ?? null);
function onFileChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0] ?? null;
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
previewUrl.value = null;
}
model.value = file;
previewUrl.value = file ? URL.createObjectURL(file) : null;
}
function clearFile() {
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
previewUrl.value = null;
}
model.value = null;
}
</script>
<template>
<Field>
<FieldLabel :for="id">
{{ label }}
</FieldLabel>
<FieldDescription v-if="description">
{{ description }}
</FieldDescription>
<div class="space-y-3">
<div
v-if="displayUrl"
:class="cn(
'relative overflow-hidden rounded-lg border bg-muted/30',
id === 'favicon' ? 'size-20' : 'aspect-video max-w-md',
)"
>
<img :src="displayUrl" :alt="label" class="size-full object-cover">
<Button
v-if="model"
type="button"
variant="secondary"
size="icon"
class="absolute top-2 right-2 size-7"
@click="clearFile"
>
<X class="size-4" />
</Button>
</div>
<div class="flex items-center gap-2">
<Input
:id="id"
type="file"
:accept="accept ?? 'image/*'"
class="max-w-md cursor-pointer"
@change="onFileChange"
/>
<ImagePlus v-if="!displayUrl" class="text-muted-foreground size-5 shrink-0" />
</div>
</div>
<FieldError :errors="errors ?? []" />
</Field>
</template>

View File

@ -0,0 +1,279 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Save, ShoppingBag, Store } from '@lucide/vue';
import { ref } from 'vue';
import { toast } from 'vue-sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { cn } from '@/lib/utils';
import type { MarketplacePlatform, MarketplaceSettingsData } from '@/types/setting';
const props = defineProps<{
data: MarketplaceSettingsData;
}>();
const activePlatform = ref<MarketplacePlatform>('tiktok_shop');
const platformItems: Array<{
key: MarketplacePlatform;
label: string;
icon: typeof Store;
}> = [
{ key: 'tiktok_shop', label: 'TikTok Shop', icon: Store },
{ key: 'shopee', label: 'Shopee', icon: ShoppingBag },
];
const form = useForm({
tiktok_shop_enabled: props.data.tiktok_shop_enabled,
tiktok_shop_admin_fee: props.data.tiktok_shop_admin_fee,
tiktok_shop_transaction_fee: props.data.tiktok_shop_transaction_fee,
tiktok_shop_payment_fee: props.data.tiktok_shop_payment_fee,
tiktok_shop_affiliate_commission: props.data.tiktok_shop_affiliate_commission,
tiktok_shop_shipping_subsidy: props.data.tiktok_shop_shipping_subsidy,
tiktok_shop_vat_rate: props.data.tiktok_shop_vat_rate,
tiktok_shop_url: props.data.tiktok_shop_url ?? '',
tiktok_shop_id: props.data.tiktok_shop_id ?? '',
shopee_enabled: props.data.shopee_enabled,
shopee_commission_fee: props.data.shopee_commission_fee,
shopee_transaction_fee: props.data.shopee_transaction_fee,
shopee_service_fee: props.data.shopee_service_fee,
shopee_payment_fee: props.data.shopee_payment_fee,
shopee_affiliate_commission: props.data.shopee_affiliate_commission,
shopee_shipping_subsidy: props.data.shopee_shipping_subsidy,
shopee_voucher_fee: props.data.shopee_voucher_fee,
shopee_shop_url: props.data.shopee_shop_url ?? '',
shopee_shop_id: props.data.shopee_shop_id ?? '',
});
function submit() {
form.put('/admin/system/setting/marketplace', {
preserveScroll: true,
onError: () => {
toast.error('Gagal menyimpan pengaturan marketplace.');
},
});
}
</script>
<template>
<form @submit.prevent="submit">
<div class="flex flex-col gap-6 lg:flex-row">
<nav class="flex shrink-0 flex-row gap-1 overflow-x-auto lg:w-44 lg:flex-col lg:overflow-visible">
<button v-for="item in platformItems" :key="item.key" type="button" :class="cn(
'inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium whitespace-nowrap transition-colors',
activePlatform === item.key
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground',
)" @click="activePlatform = item.key">
<component :is="item.icon" class="size-4 shrink-0" />
{{ item.label }}
</button>
</nav>
<div class="min-w-0 flex-1 space-y-6">
<Card v-show="activePlatform === 'tiktok_shop'">
<CardHeader>
<div class="flex items-center justify-between gap-4">
<div>
<CardTitle>TikTok Shop</CardTitle>
<CardDescription>
Biaya dan konfigurasi penjualan di TikTok Shop.
</CardDescription>
</div>
<div class="flex items-center gap-2">
<Switch id="tiktok_shop_enabled" v-model:checked="form.tiktok_shop_enabled" />
<FieldLabel for="tiktok_shop_enabled" class="mb-0 text-sm">
Aktif
</FieldLabel>
</div>
</div>
</CardHeader>
<CardContent>
<FieldSet>
<FieldGroup class="grid gap-4 sm:grid-cols-2">
<Field>
<FieldLabel for="tiktok_shop_admin_fee">Biaya Admin (%)</FieldLabel>
<Input id="tiktok_shop_admin_fee" v-model="form.tiktok_shop_admin_fee" type="number"
step="0.01" min="0" max="100" />
<FieldDescription>Komisi platform TikTok Shop.</FieldDescription>
<FieldError
:errors="form.errors.tiktok_shop_admin_fee ? [form.errors.tiktok_shop_admin_fee] : []" />
</Field>
<Field>
<FieldLabel for="tiktok_shop_transaction_fee">Biaya Transaksi (%)</FieldLabel>
<Input id="tiktok_shop_transaction_fee" v-model="form.tiktok_shop_transaction_fee"
type="number" step="0.01" min="0" max="100" />
<FieldError
:errors="form.errors.tiktok_shop_transaction_fee ? [form.errors.tiktok_shop_transaction_fee] : []" />
</Field>
<Field>
<FieldLabel for="tiktok_shop_payment_fee">Biaya Pembayaran (%)</FieldLabel>
<Input id="tiktok_shop_payment_fee" v-model="form.tiktok_shop_payment_fee"
type="number" step="0.01" min="0" max="100" />
<FieldError
:errors="form.errors.tiktok_shop_payment_fee ? [form.errors.tiktok_shop_payment_fee] : []" />
</Field>
<Field>
<FieldLabel for="tiktok_shop_affiliate_commission">Komisi Afiliasi (%)</FieldLabel>
<Input id="tiktok_shop_affiliate_commission"
v-model="form.tiktok_shop_affiliate_commission" type="number" step="0.01"
min="0" max="100" />
<FieldDescription>Biaya jika penjualan via afiliasi/kreator.</FieldDescription>
<FieldError
:errors="form.errors.tiktok_shop_affiliate_commission ? [form.errors.tiktok_shop_affiliate_commission] : []" />
</Field>
<Field>
<FieldLabel for="tiktok_shop_shipping_subsidy">Subsidi Ongkir (%)</FieldLabel>
<Input id="tiktok_shop_shipping_subsidy" v-model="form.tiktok_shop_shipping_subsidy"
type="number" step="0.01" min="0" max="100" />
<FieldError
:errors="form.errors.tiktok_shop_shipping_subsidy ? [form.errors.tiktok_shop_shipping_subsidy] : []" />
</Field>
<Field>
<FieldLabel for="tiktok_shop_vat_rate">PPN / Pajak (%)</FieldLabel>
<Input id="tiktok_shop_vat_rate" v-model="form.tiktok_shop_vat_rate" type="number"
step="0.01" min="0" max="100" />
<FieldError
:errors="form.errors.tiktok_shop_vat_rate ? [form.errors.tiktok_shop_vat_rate] : []" />
</Field>
<Field>
<FieldLabel for="tiktok_shop_url">URL Toko</FieldLabel>
<Input id="tiktok_shop_url" v-model="form.tiktok_shop_url" type="url"
placeholder="https://www.tiktok.com/@toko" />
<FieldError
:errors="form.errors.tiktok_shop_url ? [form.errors.tiktok_shop_url] : []" />
</Field>
<Field>
<FieldLabel for="tiktok_shop_id">Shop ID</FieldLabel>
<Input id="tiktok_shop_id" v-model="form.tiktok_shop_id"
placeholder="ID toko TikTok Shop" />
<FieldError
:errors="form.errors.tiktok_shop_id ? [form.errors.tiktok_shop_id] : []" />
</Field>
</FieldGroup>
</FieldSet>
</CardContent>
</Card>
<Card v-show="activePlatform === 'shopee'">
<CardHeader>
<div class="flex items-center justify-between gap-4">
<div>
<CardTitle>Shopee</CardTitle>
<CardDescription>
Biaya dan konfigurasi penjualan di Shopee.
</CardDescription>
</div>
<div class="flex items-center gap-2">
<Switch id="shopee_enabled" v-model:checked="form.shopee_enabled" />
<FieldLabel for="shopee_enabled" class="mb-0 text-sm">
Aktif
</FieldLabel>
</div>
</div>
</CardHeader>
<CardContent>
<FieldSet>
<FieldGroup class="grid gap-4 sm:grid-cols-2">
<Field>
<FieldLabel for="shopee_commission_fee">Komisi Shopee (%)</FieldLabel>
<Input id="shopee_commission_fee" v-model="form.shopee_commission_fee" type="number"
step="0.01" min="0" max="100" />
<FieldDescription>Komisi penjualan kategori umum.</FieldDescription>
<FieldError
:errors="form.errors.shopee_commission_fee ? [form.errors.shopee_commission_fee] : []" />
</Field>
<Field>
<FieldLabel for="shopee_transaction_fee">Biaya Transaksi (%)</FieldLabel>
<Input id="shopee_transaction_fee" v-model="form.shopee_transaction_fee"
type="number" step="0.01" min="0" max="100" />
<FieldError
:errors="form.errors.shopee_transaction_fee ? [form.errors.shopee_transaction_fee] : []" />
</Field>
<Field>
<FieldLabel for="shopee_service_fee">Biaya Layanan (%)</FieldLabel>
<Input id="shopee_service_fee" v-model="form.shopee_service_fee" type="number"
step="0.01" min="0" max="100" />
<FieldDescription>Biaya program Shopee (jika berlaku).</FieldDescription>
<FieldError
:errors="form.errors.shopee_service_fee ? [form.errors.shopee_service_fee] : []" />
</Field>
<Field>
<FieldLabel for="shopee_payment_fee">Biaya Pembayaran (%)</FieldLabel>
<Input id="shopee_payment_fee" v-model="form.shopee_payment_fee" type="number"
step="0.01" min="0" max="100" />
<FieldError
:errors="form.errors.shopee_payment_fee ? [form.errors.shopee_payment_fee] : []" />
</Field>
<Field>
<FieldLabel for="shopee_affiliate_commission">Komisi Afiliasi Shopee (%)
</FieldLabel>
<Input id="shopee_affiliate_commission" v-model="form.shopee_affiliate_commission"
type="number" step="0.01" min="0" max="100" />
<FieldDescription>Shopee Affiliate / kolaborasi kreator.</FieldDescription>
<FieldError
:errors="form.errors.shopee_affiliate_commission ? [form.errors.shopee_affiliate_commission] : []" />
</Field>
<Field>
<FieldLabel for="shopee_shipping_subsidy">Subsidi Ongkir (%)</FieldLabel>
<Input id="shopee_shipping_subsidy" v-model="form.shopee_shipping_subsidy"
type="number" step="0.01" min="0" max="100" />
<FieldError
:errors="form.errors.shopee_shipping_subsidy ? [form.errors.shopee_shipping_subsidy] : []" />
</Field>
<Field>
<FieldLabel for="shopee_voucher_fee">Biaya Voucher / Diskon (%)</FieldLabel>
<Input id="shopee_voucher_fee" v-model="form.shopee_voucher_fee" type="number"
step="0.01" min="0" max="100" />
<FieldDescription>Estimasi biaya voucher toko atau campaign.</FieldDescription>
<FieldError
:errors="form.errors.shopee_voucher_fee ? [form.errors.shopee_voucher_fee] : []" />
</Field>
<Field>
<FieldLabel for="shopee_shop_url">URL Toko</FieldLabel>
<Input id="shopee_shop_url" v-model="form.shopee_shop_url" type="url"
placeholder="https://shopee.co.id/namatoko" />
<FieldError
:errors="form.errors.shopee_shop_url ? [form.errors.shopee_shop_url] : []" />
</Field>
<Field>
<FieldLabel for="shopee_shop_id">Shop ID</FieldLabel>
<Input id="shopee_shop_id" v-model="form.shopee_shop_id"
placeholder="ID toko Shopee" />
<FieldError
:errors="form.errors.shopee_shop_id ? [form.errors.shopee_shop_id] : []" />
</Field>
</FieldGroup>
</FieldSet>
</CardContent>
</Card>
<div class="flex justify-end">
<Button type="submit" :disabled="form.processing">
<Save class="size-4" />
{{ form.processing ? 'Menyimpan...' : 'Simpan' }}
</Button>
</div>
</div>
</div>
</form>
</template>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Save } from '@lucide/vue';
import { toast } from 'vue-sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import type { SocialMediaSettingsData } from '@/types/setting';
const props = defineProps<{
data: SocialMediaSettingsData;
}>();
const form = useForm({
instagram_url: props.data.instagram_url ?? '',
facebook_url: props.data.facebook_url ?? '',
tiktok_url: props.data.tiktok_url ?? '',
});
function submit() {
form.put('/admin/system/setting/social-media', {
preserveScroll: true,
onError: () => {
toast.error('Gagal menyimpan pengaturan media sosial.');
},
});
}
</script>
<template>
<form @submit.prevent="submit">
<Card>
<CardHeader>
<CardTitle>Media Sosial</CardTitle>
<CardDescription>
Tautan profil media sosial yang ditampilkan di website atau aplikasi.
</CardDescription>
</CardHeader>
<CardContent>
<FieldSet>
<FieldGroup class="grid gap-4">
<Field>
<FieldLabel for="instagram_url">
Instagram
</FieldLabel>
<Input id="instagram_url" v-model="form.instagram_url" type="url"
placeholder="https://instagram.com/username" />
<FieldError :errors="form.errors.instagram_url ? [form.errors.instagram_url] : []" />
</Field>
<Field>
<FieldLabel for="facebook_url">
Facebook
</FieldLabel>
<Input id="facebook_url" v-model="form.facebook_url" type="url"
placeholder="https://facebook.com/pagename" />
<FieldError :errors="form.errors.facebook_url ? [form.errors.facebook_url] : []" />
</Field>
<Field>
<FieldLabel for="tiktok_url">
TikTok
</FieldLabel>
<Input id="tiktok_url" v-model="form.tiktok_url" type="url"
placeholder="https://tiktok.com/@username" />
<FieldError :errors="form.errors.tiktok_url ? [form.errors.tiktok_url] : []" />
</Field>
</FieldGroup>
</FieldSet>
</CardContent>
</Card>
<div class="mt-6 flex justify-end">
<Button type="submit" :disabled="form.processing">
<Save class="size-4" />
{{ form.processing ? 'Menyimpan...' : 'Simpan' }}
</Button>
</div>
</form>
</template>

View File

@ -0,0 +1,116 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Save } from '@lucide/vue';
import { toast } from 'vue-sonner';
import ImageUploadField from '@/components/admin/setting/ImageUploadField.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { PhoneNumberInput } from '@/components/ui/phone-number-input';
import { Textarea } from '@/components/ui/textarea';
import type { SystemSettingsData } from '@/types/setting';
const props = defineProps<{
data: SystemSettingsData;
}>();
const form = useForm({
app_name: props.data.app_name,
about_app: props.data.about_app ?? '',
email: props.data.email ?? '',
phone: props.data.phone ?? '',
logo: null as File | null,
login_cover: null as File | null,
});
function submit() {
form.post('/admin/system/setting/system', {
forceFormData: true,
preserveScroll: true,
onError: () => {
toast.error('Gagal menyimpan pengaturan sistem.');
},
});
}
</script>
<template>
<form @submit.prevent="submit">
<div class="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Informasi Aplikasi</CardTitle>
<CardDescription>
Nama, deskripsi, dan kontak yang ditampilkan di aplikasi.
</CardDescription>
</CardHeader>
<CardContent>
<FieldSet>
<FieldGroup class="grid gap-4 sm:grid-cols-2">
<Field class="sm:col-span-2">
<FieldLabel for="app_name" required>
Nama Aplikasi
</FieldLabel>
<Input id="app_name" v-model="form.app_name" placeholder="DST Collection" />
<FieldError :errors="form.errors.app_name ? [form.errors.app_name] : []" />
</Field>
<Field class="sm:col-span-2">
<FieldLabel for="about_app">
Tentang Aplikasi
</FieldLabel>
<Textarea id="about_app" v-model="form.about_app" rows="4"
placeholder="Deskripsi singkat tentang bisnis atau aplikasi Anda." />
<FieldError :errors="form.errors.about_app ? [form.errors.about_app] : []" />
</Field>
<Field>
<FieldLabel for="email">
Email
</FieldLabel>
<Input id="email" v-model="form.email" type="email" placeholder="info@perusahaan.com" />
<FieldError :errors="form.errors.email ? [form.errors.email] : []" />
</Field>
<Field>
<FieldLabel for="phone">
Nomor Telepon
</FieldLabel>
<PhoneNumberInput id="phone" v-model="form.phone" />
<FieldError :errors="form.errors.phone ? [form.errors.phone] : []" />
</Field>
</FieldGroup>
</FieldSet>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Branding</CardTitle>
<CardDescription>
Logo aplikasi dan gambar latar halaman login.
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup class="grid gap-6 sm:grid-cols-2">
<ImageUploadField id="logo" v-model="form.logo" label="Logo"
description="Disarankan PNG transparan, maks. 2 MB." :current-url="data.logo_url"
:errors="form.errors.logo ? [form.errors.logo] : []" />
<ImageUploadField id="login_cover" v-model="form.login_cover" label="Cover Login"
description="Gambar latar halaman login, maks. 5 MB." :current-url="data.login_cover_url"
:errors="form.errors.login_cover ? [form.errors.login_cover] : []" />
</FieldGroup>
</CardContent>
</Card>
<div class="flex justify-end">
<Button type="submit" :disabled="form.processing">
<Save class="size-4" />
{{ form.processing ? 'Menyimpan...' : 'Simpan' }}
</Button>
</div>
</div>
</form>
</template>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { Settings2, Share2, ShoppingBag } from '@lucide/vue';
import AdminLayout from '@/layouts/AdminLayout.vue';
import { cn } from '@/lib/utils';
import type { SettingSection } from '@/types/setting';
const activeSection = defineModel<SettingSection>('section', { required: true });
const navItems: Array<{
key: SettingSection;
label: string;
icon: typeof Settings2;
}> = [
{ key: 'system', label: 'Sistem', icon: Settings2 },
{ key: 'social', label: 'Media Sosial', icon: Share2 },
{ key: 'marketplace', label: 'Marketplace', icon: ShoppingBag },
];
</script>
<template>
<AdminLayout>
<div class="space-y-6">
<div class="space-y-1">
<h2 class="text-2xl font-bold tracking-tight">
Pengaturan
</h2>
</div>
<div class="flex flex-col gap-6 lg:flex-row">
<nav class="flex shrink-0 flex-row gap-1 overflow-x-auto lg:w-56 lg:flex-col lg:overflow-visible">
<button v-for="item in navItems" :key="item.key" type="button" :class="cn(
'inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium whitespace-nowrap transition-colors',
activeSection === item.key
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground',
)" @click="activeSection = item.key">
<component :is="item.icon" class="size-4 shrink-0" />
{{ item.label }}
</button>
</nav>
<div class="min-w-0 flex-1">
<slot />
</div>
</div>
</div>
</AdminLayout>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { ref } from 'vue';
import MarketplaceSection from '@/components/admin/setting/MarketplaceSection.vue';
import SocialMediaSection from '@/components/admin/setting/SocialMediaSection.vue';
import SystemSection from '@/components/admin/setting/SystemSection.vue';
import SettingLayout from '@/layouts/SettingLayout.vue';
import type {
MarketplaceSettingsData,
SettingSection,
SocialMediaSettingsData,
SystemSettingsData,
} from '@/types/setting';
defineProps<{
system: SystemSettingsData;
socialMedia: SocialMediaSettingsData;
marketplace: MarketplaceSettingsData;
}>();
const activeSection = ref<SettingSection>('system');
</script>
<template>
<Head title="Pengaturan" />
<SettingLayout v-model:section="activeSection">
<SystemSection v-if="activeSection === 'system'" :data="system" />
<SocialMediaSection v-else-if="activeSection === 'social'" :data="socialMedia" />
<MarketplaceSection v-else-if="activeSection === 'marketplace'" :data="marketplace" />
</SettingLayout>
</template>

View File

@ -0,0 +1,42 @@
export type SettingSection = 'system' | 'social' | 'marketplace';
export type MarketplacePlatform = 'tiktok_shop' | 'shopee';
export type SystemSettingsData = {
app_name: string;
about_app: string | null;
email: string | null;
phone: string | null;
logo: string | null;
logo_url: string | null;
login_cover: string | null;
login_cover_url: string | null;
};
export type SocialMediaSettingsData = {
instagram_url: string | null;
facebook_url: string | null;
tiktok_url: string | null;
};
export type MarketplaceSettingsData = {
tiktok_shop_enabled: boolean;
tiktok_shop_admin_fee: number;
tiktok_shop_transaction_fee: number;
tiktok_shop_payment_fee: number;
tiktok_shop_affiliate_commission: number;
tiktok_shop_shipping_subsidy: number;
tiktok_shop_vat_rate: number;
tiktok_shop_url: string | null;
tiktok_shop_id: string | null;
shopee_enabled: boolean;
shopee_commission_fee: number;
shopee_transaction_fee: number;
shopee_service_fee: number;
shopee_payment_fee: number;
shopee_affiliate_commission: number;
shopee_shipping_subsidy: number;
shopee_voucher_fee: number;
shopee_shop_url: string | null;
shopee_shop_id: string | null;
};

View File

@ -18,6 +18,7 @@
use App\Http\Controllers\Admin\Master\ProductController;
use App\Http\Controllers\Admin\Master\RawMaterialController;
use App\Http\Controllers\Admin\Master\SupplierController;
use App\Http\Controllers\Admin\System\SettingController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\LogoutController;
use Illuminate\Support\Facades\Route;
@ -153,6 +154,26 @@
});
Route::prefix('system')->name('system.')->group(function () {
Route::prefix('setting')->name('setting.')
->middleware('permission:'.Permission::SETTINGS_VIEW->value)
->group(function () {
Route::get('/', [SettingController::class, 'index'])->name('index');
Route::post('system', [SettingController::class, 'updateSystem'])
->middleware('permission:'.Permission::SETTINGS_UPDATE->value)
->name('system.update');
Route::put('social-media', [SettingController::class, 'updateSocialMedia'])
->middleware('permission:'.Permission::SETTINGS_UPDATE->value)
->name('social-media.update');
Route::put('marketplace', [SettingController::class, 'updateMarketplace'])
->middleware('permission:'.Permission::SETTINGS_UPDATE->value)
->name('marketplace.update');
});
});
Route::prefix('account')->name('account.')->group(function () {
Route::get('profile', [ProfileController::class, 'edit'])->name('profile');
Route::put('profile', [ProfileController::class, 'update'])->name('profile.update');