Add admin settings routes and corresponding feature tests
- Introduced routes for managing admin settings including system, homepage, social media, marketplace, and HR settings. - Created a comprehensive test suite for the AdminSettingsController to ensure proper functionality and validation of settings updates. - Implemented tests for authentication, data integrity, and realistic user scenarios to validate the settings management process.
This commit is contained in:
parent
c2b93a0489
commit
a1a73cb222
77
app/Http/Controllers/Admin/AdminSettingsController.php
Normal file
77
app/Http/Controllers/Admin/AdminSettingsController.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Settings\UpdateHRRequest;
|
||||
use App\Http\Requests\Admin\Settings\UpdateHomepageRequest;
|
||||
use App\Http\Requests\Admin\Settings\UpdateMarketplaceRequest;
|
||||
use App\Http\Requests\Admin\Settings\UpdateSocialMediaRequest;
|
||||
use App\Http\Requests\Admin\Settings\UpdateSystemRequest;
|
||||
use App\Services\Admin\AdminSettingsService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AdminSettingsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private AdminSettingsService $service
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
return Inertia::render('admin/settings/index', [
|
||||
'system' => $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');
|
||||
}
|
||||
}
|
||||
33
app/Http/Requests/Admin/Settings/UpdateHRRequest.php
Normal file
33
app/Http/Requests/Admin/Settings/UpdateHRRequest.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateHRRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'scheduled_check_in_time' => ['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',
|
||||
];
|
||||
}
|
||||
}
|
||||
32
app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php
Normal file
32
app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateHomepageRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'hero_image_url' => ['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',
|
||||
];
|
||||
}
|
||||
}
|
||||
108
app/Http/Requests/Admin/Settings/UpdateMarketplaceRequest.php
Normal file
108
app/Http/Requests/Admin/Settings/UpdateMarketplaceRequest.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateMarketplaceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function feeRuleRules(): array
|
||||
{
|
||||
return ['required', 'array'];
|
||||
}
|
||||
|
||||
private function feeFieldRules(string $prefix): array
|
||||
{
|
||||
return [
|
||||
"{$prefix}.base" => ['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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateSocialMediaRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'instagram_url' => ['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',
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Http/Requests/Admin/Settings/UpdateSystemRequest.php
Normal file
35
app/Http/Requests/Admin/Settings/UpdateSystemRequest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateSystemRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'app_name' => ['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',
|
||||
];
|
||||
}
|
||||
}
|
||||
146
app/Services/Admin/AdminSettingsService.php
Normal file
146
app/Services/Admin/AdminSettingsService.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Settings\HRSettings;
|
||||
use App\Settings\HomepageSettings;
|
||||
use App\Settings\MarketplaceSettings;
|
||||
use App\Settings\SocialMediaSettings;
|
||||
use App\Settings\SystemSettings;
|
||||
use App\Support\Marketplace\MarketplaceFeeRule;
|
||||
|
||||
class AdminSettingsService
|
||||
{
|
||||
public function getSystemData(): array
|
||||
{
|
||||
$settings = app(SystemSettings::class);
|
||||
|
||||
return [
|
||||
'app_name' => $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();
|
||||
}
|
||||
}
|
||||
18
app/Settings/HRSettings.php
Normal file
18
app/Settings/HRSettings.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Settings;
|
||||
|
||||
use Spatie\LaravelSettings\Settings;
|
||||
|
||||
class HRSettings extends Settings
|
||||
{
|
||||
public string $scheduled_check_in_time;
|
||||
public string $scheduled_check_out_time;
|
||||
public int $late_penalty_amount;
|
||||
public int $absent_penalty_amount;
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
return 'hr';
|
||||
}
|
||||
}
|
||||
55
app/Settings/HomepageSettings.php
Normal file
55
app/Settings/HomepageSettings.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Settings;
|
||||
|
||||
use Spatie\LaravelSettings\Settings;
|
||||
|
||||
class HomepageSettings extends Settings
|
||||
{
|
||||
public string $hero_badge;
|
||||
public string $hero_title_line1;
|
||||
public string $hero_title_line2;
|
||||
public string $hero_title_highlight;
|
||||
public string $hero_description;
|
||||
public string $hero_cta_primary_text;
|
||||
public string $hero_cta_secondary_text;
|
||||
public ?string $hero_image_url;
|
||||
public string $hero_bg_text_left;
|
||||
public string $hero_bg_text_right;
|
||||
|
||||
public string $scroll_hashtag;
|
||||
public string $scroll_tagline;
|
||||
|
||||
public string $catalog_badge;
|
||||
public string $catalog_title;
|
||||
public string $catalog_description;
|
||||
public string $catalog_search_placeholder;
|
||||
|
||||
public string $gallery_badge;
|
||||
public string $gallery_title;
|
||||
public string $gallery_description;
|
||||
public array $gallery_images;
|
||||
|
||||
public string $order_guide_badge;
|
||||
public string $order_guide_title;
|
||||
public string $order_guide_description;
|
||||
public array $order_steps;
|
||||
|
||||
public string $about_badge;
|
||||
public string $about_title;
|
||||
public ?string $about_image_url;
|
||||
public array $about_features;
|
||||
|
||||
public string $contact_badge;
|
||||
public string $contact_title;
|
||||
public string $contact_description;
|
||||
public string $contact_form_title;
|
||||
|
||||
public string $footer_description;
|
||||
public string $footer_copyright;
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
return 'homepage';
|
||||
}
|
||||
}
|
||||
31
app/Settings/MarketplaceSettings.php
Normal file
31
app/Settings/MarketplaceSettings.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Settings;
|
||||
|
||||
use App\Support\Marketplace\MarketplaceFeeRule;
|
||||
use Spatie\LaravelSettings\Settings;
|
||||
|
||||
class MarketplaceSettings extends Settings
|
||||
{
|
||||
public MarketplaceFeeRule $tiktok_shop_platform_commission;
|
||||
public MarketplaceFeeRule $tiktok_shop_logistics_service_fee;
|
||||
public MarketplaceFeeRule $tiktok_shop_dynamic_commission;
|
||||
public MarketplaceFeeRule $tiktok_shop_order_processing_fee;
|
||||
public MarketplaceFeeRule $tiktok_shop_affiliate;
|
||||
public MarketplaceFeeRule $tiktok_shop_pre_order_service_fee;
|
||||
|
||||
public MarketplaceFeeRule $shopee_admin_fee;
|
||||
public MarketplaceFeeRule $shopee_program_fee;
|
||||
public MarketplaceFeeRule $shopee_shipping_savings;
|
||||
public MarketplaceFeeRule $shopee_premium;
|
||||
public MarketplaceFeeRule $shopee_service_fee;
|
||||
public MarketplaceFeeRule $shopee_order_processing_fee;
|
||||
public MarketplaceFeeRule $shopee_ams_commission_fee;
|
||||
public MarketplaceFeeRule $shopee_pre_order;
|
||||
public MarketplaceFeeRule $shopee_live_extra;
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
return 'marketplace';
|
||||
}
|
||||
}
|
||||
17
app/Settings/SocialMediaSettings.php
Normal file
17
app/Settings/SocialMediaSettings.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
19
app/Settings/SystemSettings.php
Normal file
19
app/Settings/SystemSettings.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Settings;
|
||||
|
||||
use Spatie\LaravelSettings\Settings;
|
||||
|
||||
class SystemSettings extends Settings
|
||||
{
|
||||
public string $app_name;
|
||||
public ?string $address;
|
||||
public ?string $about_app;
|
||||
public ?string $email;
|
||||
public ?string $phone;
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
return 'system';
|
||||
}
|
||||
}
|
||||
32
app/Support/Marketplace/MarketplaceFeeRule.php
Normal file
32
app/Support/Marketplace/MarketplaceFeeRule.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Marketplace;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class MarketplaceFeeRule extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $base = 'per_transaksi',
|
||||
public string $type = 'persentase',
|
||||
public float $value = 0.0,
|
||||
) {}
|
||||
|
||||
public static function defaultPercent(float $value): static
|
||||
{
|
||||
return new static(
|
||||
base: 'per_transaksi',
|
||||
type: 'persentase',
|
||||
value: $value,
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'base' => $this->base,
|
||||
'type' => $this->type,
|
||||
'value' => (float) $this->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
250
composer.lock
generated
250
composer.lock
generated
@ -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",
|
||||
|
||||
100
config/settings.php
Normal file
100
config/settings.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
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' => 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'),
|
||||
];
|
||||
48
database/settings/2026_06_11_085959_create_app_settings.php
Normal file
48
database/settings/2026_06_11_085959_create_app_settings.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use App\Support\Marketplace\MarketplaceFeeRule;
|
||||
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('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());
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
<?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->delete('logo');
|
||||
$blueprint->delete('login_cover');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use App\Support\Marketplace\MarketplaceFeeRule;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
17
database/settings/2026_06_18_100000_create_hr_settings.php
Normal file
17
database/settings/2026_06_18_100000_create_hr_settings.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->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);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->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.');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
89
resources/js/components/ui/tabs.tsx
Normal file
89
resources/js/components/ui/tabs.tsx
Normal file
@ -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<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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<typeof TabsPrimitive.List> &
|
||||
VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||
398
resources/js/pages/admin/settings/index.tsx
Normal file
398
resources/js/pages/admin/settings/index.tsx
Normal file
@ -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<string, MarketplaceFeeRule>;
|
||||
shopee: Record<string, MarketplaceFeeRule>;
|
||||
};
|
||||
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 (
|
||||
<div className="flex flex-col gap-3 rounded-md border p-3 md:flex-row md:items-start md:gap-4">
|
||||
<span className="min-w-[160px] text-sm font-medium md:pt-2">{label}</span>
|
||||
<div className="grid flex-1 grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground md:hidden">Dasar</span>
|
||||
<RadioGroup name={`${prefix}[base]`} defaultValue={data.base} className="flex gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="per_transaksi" id={`${prefix}-base-per_transaksi`} />
|
||||
<Label htmlFor={`${prefix}-base-per_transaksi`} className="font-normal text-xs">Per Transaksi</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="per_produk" id={`${prefix}-base-per_produk`} />
|
||||
<Label htmlFor={`${prefix}-base-per_produk`} className="font-normal text-xs">Per Produk</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground md:hidden">Tipe</span>
|
||||
<RadioGroup name={`${prefix}[type]`} defaultValue={data.type} onValueChange={setType} className="flex gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="flat" id={`${prefix}-type-flat`} />
|
||||
<Label htmlFor={`${prefix}-type-flat`} className="font-normal text-xs">Flat</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="persentase" id={`${prefix}-type-persentase`} />
|
||||
<Label htmlFor={`${prefix}-type-persentase`} className="font-normal text-xs">Persentase</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground md:hidden">Nilai</span>
|
||||
{type === 'flat' ? (
|
||||
<RupiahInput name={`${prefix}[value]`} defaultValue={data.value} />
|
||||
) : (
|
||||
<Input
|
||||
name={`${prefix}[value]`}
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
defaultValue={data.value}
|
||||
placeholder="0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MarketplaceColumnHeader() {
|
||||
return (
|
||||
<div className="hidden gap-3 px-3 pb-1 text-xs font-medium text-muted-foreground md:grid md:grid-cols-[160px_1fr] md:gap-4">
|
||||
<span>Biaya</span>
|
||||
<div className="grid flex-1 grid-cols-3 gap-3">
|
||||
<span>Dasar</span>
|
||||
<span>Tipe</span>
|
||||
<span>Nilai</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MarketplaceCard({ title, variables }: { title: string; variables: { label: string; prefix: string; data: MarketplaceFeeRule }[] }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-2">
|
||||
<MarketplaceColumnHeader />
|
||||
{variables.map((v) => (
|
||||
<MarketplaceVariableInput key={v.prefix} {...v} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminSettings({ system, homepage, socialMedia, marketplace, hr }: Props) {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('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 (
|
||||
<>
|
||||
<Head title="Pengaturan" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Pengaturan
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row lg:space-x-12">
|
||||
<aside className="w-full max-w-xl lg:w-48">
|
||||
<nav className="flex flex-col space-y-1 space-x-0" aria-label="Admin Settings">
|
||||
{sidebarTabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.key}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={cn('w-full justify-start', {
|
||||
'bg-muted': activeTab === tab.key,
|
||||
})}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<Separator className="my-6 lg:hidden" />
|
||||
|
||||
<section className="w-full min-w-0 flex-1 space-y-6">
|
||||
{activeTab === 'sistem' && (
|
||||
<Form action={updateSystem()} options={{ preserveScroll: true }}>
|
||||
{({ processing, errors }) => (
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Sistem</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label htmlFor="app_name">
|
||||
Nama Aplikasi <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input id="app_name" name="app_name" placeholder="Masukkan nama aplikasi" defaultValue={system.app_name} />
|
||||
<InputError message={errors.app_name} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" name="email" type="email" placeholder="Masukkan email" defaultValue={system.email} />
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone">No. Telepon</Label>
|
||||
<PhoneNumberInput name="phone" defaultValue={system.phone} />
|
||||
<InputError message={errors.phone} />
|
||||
</div>
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label htmlFor="address">Alamat</Label>
|
||||
<Textarea id="address" name="address" placeholder="Masukkan alamat" rows={3} defaultValue={system.address} />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label htmlFor="about_app">Tentang Aplikasi</Label>
|
||||
<Textarea id="about_app" name="about_app" placeholder="Masukkan deskripsi aplikasi" rows={4} defaultValue={system.about_app} />
|
||||
<InputError message={errors.about_app} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="submit" disabled={processing}>{processing ? 'Menyimpan...' : 'Simpan'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{activeTab === 'homepage' && (
|
||||
<Form action={updateHomepage()} options={{ preserveScroll: true }}>
|
||||
{({ processing, errors }) => (
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Homepage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="hero_image_url">URL Foto Hero</Label>
|
||||
<Input id="hero_image_url" name="hero_image_url" placeholder="https://..." defaultValue={homepage.hero_image_url} />
|
||||
<InputError message={errors.hero_image_url} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="about_image_url">URL Foto Tentang Kami</Label>
|
||||
<Input id="about_image_url" name="about_image_url" placeholder="https://..." defaultValue={homepage.about_image_url} />
|
||||
<InputError message={errors.about_image_url} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="submit" disabled={processing}>{processing ? 'Menyimpan...' : 'Simpan'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{activeTab === 'media-sosial' && (
|
||||
<Form action={updateSocialMedia()} options={{ preserveScroll: true }}>
|
||||
{({ processing, errors }) => (
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Media Sosial</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="instagram_url">Instagram</Label>
|
||||
<Input id="instagram_url" name="instagram_url" placeholder="https://instagram.com/..." defaultValue={socialMedia.instagram_url ?? ''} />
|
||||
<InputError message={errors.instagram_url} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="facebook_url">Facebook</Label>
|
||||
<Input id="facebook_url" name="facebook_url" placeholder="https://facebook.com/..." defaultValue={socialMedia.facebook_url ?? ''} />
|
||||
<InputError message={errors.facebook_url} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="tiktok_url">TikTok</Label>
|
||||
<Input id="tiktok_url" name="tiktok_url" placeholder="https://tiktok.com/..." defaultValue={socialMedia.tiktok_url ?? ''} />
|
||||
<InputError message={errors.tiktok_url} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="submit" disabled={processing}>{processing ? 'Menyimpan...' : 'Simpan'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{activeTab === 'marketplace' && (
|
||||
<Form action={updateMarketplace()} options={{ preserveScroll: true }}>
|
||||
{({ processing }) => (
|
||||
<div className="grid gap-6">
|
||||
<Tabs value={marketplaceTab} onValueChange={(v) => setMarketplaceTab(v as 'tiktok-shop' | 'shopee')}>
|
||||
<TabsList className="mb-3">
|
||||
<TabsTrigger value="tiktok-shop">TikTok Shop</TabsTrigger>
|
||||
<TabsTrigger value="shopee">Shopee</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div hidden={marketplaceTab !== 'tiktok-shop'}>
|
||||
<MarketplaceCard title="TikTok Shop" variables={tiktokShopVariables} />
|
||||
</div>
|
||||
<div hidden={marketplaceTab !== 'shopee'}>
|
||||
<MarketplaceCard title="Shopee" variables={shopeeVariables} />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="submit" disabled={processing}>{processing ? 'Menyimpan...' : 'Simpan'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{activeTab === 'hr' && (
|
||||
<Form action="/admin/settings/hr" options={{ preserveScroll: true }}>
|
||||
{({ processing, errors }) => (
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>HR</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="scheduled_check_in_time">
|
||||
Jam Masuk Kerja <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input id="scheduled_check_in_time" name="scheduled_check_in_time" type="time" defaultValue={hr.scheduled_check_in_time} />
|
||||
<InputError message={errors.scheduled_check_in_time} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="scheduled_check_out_time">
|
||||
Jam Pulang Kerja <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input id="scheduled_check_out_time" name="scheduled_check_out_time" type="time" defaultValue={hr.scheduled_check_out_time} />
|
||||
<InputError message={errors.scheduled_check_out_time} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Denda Keterlambatan <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<RupiahInput name="late_penalty_amount" defaultValue={hr.late_penalty_amount} />
|
||||
<InputError message={errors.late_penalty_amount} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Denda Bolos <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<RupiahInput name="absent_penalty_amount" defaultValue={hr.absent_penalty_amount} />
|
||||
<InputError message={errors.absent_penalty_amount} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="submit" disabled={processing}>{processing ? 'Menyimpan...' : 'Simpan'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AdminSettings.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Pengaturan',
|
||||
href: '/admin/settings',
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\AdminSettingsController;
|
||||
use App\Http\Controllers\Admin\Finance\CashAccountController;
|
||||
use App\Http\Controllers\Admin\Finance\EmployeeAdvanceController;
|
||||
use App\Http\Controllers\Admin\Finance\ExpenseController;
|
||||
@ -57,6 +58,15 @@
|
||||
Route::delete('payroll-adjustments/{payrollAdjustment}', [PayrollAdjustmentController::class, 'destroy'])->name('payroll-adjustments.destroy');
|
||||
});
|
||||
|
||||
Route::prefix('admin/settings')->name('admin.settings.')->group(function () {
|
||||
Route::get('/', [AdminSettingsController::class, 'index'])->name('index');
|
||||
Route::put('system', [AdminSettingsController::class, 'updateSystem'])->name('update-system');
|
||||
Route::put('homepage', [AdminSettingsController::class, 'updateHomepage'])->name('update-homepage');
|
||||
Route::put('social-media', [AdminSettingsController::class, 'updateSocialMedia'])->name('update-social-media');
|
||||
Route::put('marketplace', [AdminSettingsController::class, 'updateMarketplace'])->name('update-marketplace');
|
||||
Route::put('hr', [AdminSettingsController::class, 'updateHR'])->name('update-hr');
|
||||
});
|
||||
|
||||
Route::prefix('admin/hr')->name('admin.hr.')->group(function () {
|
||||
Route::resource('employees', EmployeeController::class)->except(['show'])->parameters(['employees' => 'user']);
|
||||
Route::post('employees/{user}/toggle-active', [EmployeeController::class, 'toggleActive'])->name('employees.toggle-active');
|
||||
|
||||
906
tests/Feature/Admin/Settings/AdminSettingsTest.php
Normal file
906
tests/Feature/Admin/Settings/AdminSettingsTest.php
Normal file
@ -0,0 +1,906 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Settings\HRSettings;
|
||||
use App\Settings\HomepageSettings;
|
||||
use App\Settings\MarketplaceSettings;
|
||||
use App\Settings\SocialMediaSettings;
|
||||
use App\Settings\SystemSettings;
|
||||
use App\Support\Marketplace\MarketplaceFeeRule;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AUTHENTICATION
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('guests are redirected to the login page', function () {
|
||||
$response = $this->get(route('admin.settings.index'));
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('guest cannot update system settings', function () {
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'Unauthorized',
|
||||
]);
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('guest cannot update homepage settings', function () {
|
||||
$response = $this->put(route('admin.settings.update-homepage'), [
|
||||
'hero_image_url' => 'https://unauthorized.com/image.jpg',
|
||||
]);
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('guest cannot update social media settings', function () {
|
||||
$response = $this->put(route('admin.settings.update-social-media'), [
|
||||
'instagram_url' => 'https://unauthorized.com',
|
||||
]);
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('guest cannot update marketplace settings', function () {
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 100],
|
||||
]);
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('guest cannot update hr settings', function () {
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
]);
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| INDEX PAGE
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('authenticated users can visit the settings page', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.settings.index'));
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('settings page renders with correct inertia component', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.settings.index'));
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/settings/index')
|
||||
->has('system')
|
||||
->has('homepage')
|
||||
->has('socialMedia')
|
||||
->has('marketplace')
|
||||
->has('hr')
|
||||
);
|
||||
});
|
||||
|
||||
test('settings page contains system data', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$system = app(SystemSettings::class);
|
||||
|
||||
$response = $this->get(route('admin.settings.index'));
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('system.app_name', $system->app_name)
|
||||
->where('system.email', $system->email)
|
||||
->where('system.phone', $system->phone)
|
||||
);
|
||||
});
|
||||
|
||||
test('settings page contains marketplace data', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$marketplace = app(MarketplaceSettings::class);
|
||||
|
||||
$response = $this->get(route('admin.settings.index'));
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('marketplace.tiktok_shop.platform_commission.base', $marketplace->tiktok_shop_platform_commission->base)
|
||||
->where('marketplace.tiktok_shop.platform_commission.type', $marketplace->tiktok_shop_platform_commission->type)
|
||||
->where('marketplace.tiktok_shop.platform_commission.value', json_decode(json_encode($marketplace->tiktok_shop_platform_commission->value)))
|
||||
->where('marketplace.shopee.admin_fee.base', $marketplace->shopee_admin_fee->base)
|
||||
);
|
||||
});
|
||||
|
||||
test('settings page contains hr data', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$hr = app(HRSettings::class);
|
||||
|
||||
$response = $this->get(route('admin.settings.index'));
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('hr.scheduled_check_in_time', $hr->scheduled_check_in_time)
|
||||
->where('hr.scheduled_check_out_time', $hr->scheduled_check_out_time)
|
||||
->where('hr.late_penalty_amount', $hr->late_penalty_amount)
|
||||
->where('hr.absent_penalty_amount', $hr->absent_penalty_amount)
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| UPDATE SYSTEM
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('user can update system settings with valid data', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'DST Collection Updated',
|
||||
'about_app' => 'Tentang DST Collection',
|
||||
'address' => 'Jl. Test No. 123',
|
||||
'email' => 'test@dst.com',
|
||||
'phone' => '081234567890',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.settings.index'));
|
||||
|
||||
$settings = app(SystemSettings::class);
|
||||
expect($settings->app_name)->toBe('DST Collection Updated');
|
||||
expect($settings->about_app)->toBe('Tentang DST Collection');
|
||||
expect($settings->address)->toBe('Jl. Test No. 123');
|
||||
expect($settings->email)->toBe('test@dst.com');
|
||||
expect($settings->phone)->toBe('081234567890');
|
||||
});
|
||||
|
||||
test('system app_name is required', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('app_name');
|
||||
});
|
||||
|
||||
test('system email must be valid email format', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'DST',
|
||||
'email' => 'not-an-email',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('system app_name must not exceed 255 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => str_repeat('a', 256),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('app_name');
|
||||
});
|
||||
|
||||
test('system about_app must not exceed 2000 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'DST',
|
||||
'about_app' => str_repeat('a', 2001),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('about_app');
|
||||
});
|
||||
|
||||
test('system address must not exceed 500 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'DST',
|
||||
'address' => str_repeat('a', 501),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('address');
|
||||
});
|
||||
|
||||
test('system phone must not exceed 20 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'DST',
|
||||
'phone' => str_repeat('1', 21),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('phone');
|
||||
});
|
||||
|
||||
test('system nullable fields can be empty', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'DST',
|
||||
'about_app' => '',
|
||||
'address' => '',
|
||||
'email' => '',
|
||||
'phone' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| UPDATE HOMEPAGE
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('user can update homepage settings with valid data', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-homepage'), [
|
||||
'hero_image_url' => 'https://example.com/hero.jpg',
|
||||
'about_image_url' => 'https://example.com/about.jpg',
|
||||
'gallery_images' => ['https://example.com/g1.jpg', 'https://example.com/g2.jpg'],
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.settings.index'));
|
||||
|
||||
$settings = app(HomepageSettings::class);
|
||||
expect($settings->hero_image_url)->toBe('https://example.com/hero.jpg');
|
||||
expect($settings->about_image_url)->toBe('https://example.com/about.jpg');
|
||||
expect($settings->gallery_images)->toBe(['https://example.com/g1.jpg', 'https://example.com/g2.jpg']);
|
||||
});
|
||||
|
||||
test('homepage fields are nullable', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-homepage'), [
|
||||
'hero_image_url' => null,
|
||||
'about_image_url' => null,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
test('homepage hero_image_url must not exceed 2000 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-homepage'), [
|
||||
'hero_image_url' => str_repeat('a', 2001),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('hero_image_url');
|
||||
});
|
||||
|
||||
test('homepage gallery_images must be array', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-homepage'), [
|
||||
'gallery_images' => 'not-an-array',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('gallery_images');
|
||||
});
|
||||
|
||||
test('homepage gallery_images items must not exceed 2000 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-homepage'), [
|
||||
'gallery_images' => [str_repeat('a', 2001)],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('gallery_images.0');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| UPDATE SOCIAL MEDIA
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('user can update social media settings with valid data', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-social-media'), [
|
||||
'instagram_url' => 'https://instagram.com/dst',
|
||||
'facebook_url' => 'https://facebook.com/dst',
|
||||
'tiktok_url' => 'https://tiktok.com/@dst',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.settings.index'));
|
||||
|
||||
$settings = app(SocialMediaSettings::class);
|
||||
expect($settings->instagram_url)->toBe('https://instagram.com/dst');
|
||||
expect($settings->facebook_url)->toBe('https://facebook.com/dst');
|
||||
expect($settings->tiktok_url)->toBe('https://tiktok.com/@dst');
|
||||
});
|
||||
|
||||
test('social media fields are nullable', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-social-media'), [
|
||||
'instagram_url' => null,
|
||||
'facebook_url' => null,
|
||||
'tiktok_url' => null,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
test('social media instagram_url must not exceed 500 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-social-media'), [
|
||||
'instagram_url' => str_repeat('a', 501),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('instagram_url');
|
||||
});
|
||||
|
||||
test('social media facebook_url must not exceed 500 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-social-media'), [
|
||||
'facebook_url' => str_repeat('a', 501),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('facebook_url');
|
||||
});
|
||||
|
||||
test('social media tiktok_url must not exceed 500 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-social-media'), [
|
||||
'tiktok_url' => str_repeat('a', 501),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('tiktok_url');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| UPDATE MARKETPLACE
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('user can update marketplace settings with valid data', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$feeRule = ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 5000];
|
||||
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => $feeRule,
|
||||
'tiktok_shop_logistics_service_fee' => $feeRule,
|
||||
'tiktok_shop_dynamic_commission' => $feeRule,
|
||||
'tiktok_shop_order_processing_fee' => $feeRule,
|
||||
'tiktok_shop_affiliate' => $feeRule,
|
||||
'tiktok_shop_pre_order_service_fee' => $feeRule,
|
||||
'shopee_admin_fee' => $feeRule,
|
||||
'shopee_program_fee' => $feeRule,
|
||||
'shopee_shipping_savings' => $feeRule,
|
||||
'shopee_premium' => $feeRule,
|
||||
'shopee_service_fee' => $feeRule,
|
||||
'shopee_order_processing_fee' => $feeRule,
|
||||
'shopee_ams_commission_fee' => $feeRule,
|
||||
'shopee_pre_order' => $feeRule,
|
||||
'shopee_live_extra' => $feeRule,
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.settings.index'));
|
||||
|
||||
$settings = app(MarketplaceSettings::class);
|
||||
expect($settings->tiktok_shop_platform_commission->base)->toBe('per_transaksi');
|
||||
expect($settings->tiktok_shop_platform_commission->type)->toBe('flat');
|
||||
expect($settings->tiktok_shop_platform_commission->value)->toBe(5000.0);
|
||||
expect($settings->shopee_admin_fee->base)->toBe('per_transaksi');
|
||||
expect($settings->shopee_admin_fee->type)->toBe('flat');
|
||||
expect($settings->shopee_admin_fee->value)->toBe(5000.0);
|
||||
});
|
||||
|
||||
test('marketplace accepts percentage type', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$feeRule = ['base' => 'per_produk', 'type' => 'persentase', 'value' => 2.5];
|
||||
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => $feeRule,
|
||||
'tiktok_shop_logistics_service_fee' => $feeRule,
|
||||
'tiktok_shop_dynamic_commission' => $feeRule,
|
||||
'tiktok_shop_order_processing_fee' => $feeRule,
|
||||
'tiktok_shop_affiliate' => $feeRule,
|
||||
'tiktok_shop_pre_order_service_fee' => $feeRule,
|
||||
'shopee_admin_fee' => $feeRule,
|
||||
'shopee_program_fee' => $feeRule,
|
||||
'shopee_shipping_savings' => $feeRule,
|
||||
'shopee_premium' => $feeRule,
|
||||
'shopee_service_fee' => $feeRule,
|
||||
'shopee_order_processing_fee' => $feeRule,
|
||||
'shopee_ams_commission_fee' => $feeRule,
|
||||
'shopee_pre_order' => $feeRule,
|
||||
'shopee_live_extra' => $feeRule,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
|
||||
$settings = app(MarketplaceSettings::class);
|
||||
expect($settings->tiktok_shop_platform_commission->base)->toBe('per_produk');
|
||||
expect($settings->tiktok_shop_platform_commission->type)->toBe('persentase');
|
||||
expect($settings->tiktok_shop_platform_commission->value)->toBe(2.5);
|
||||
});
|
||||
|
||||
test('marketplace base field must be valid', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$feeRule = ['base' => 'invalid', 'type' => 'flat', 'value' => 0];
|
||||
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => $feeRule,
|
||||
'tiktok_shop_logistics_service_fee' => $feeRule,
|
||||
'tiktok_shop_dynamic_commission' => $feeRule,
|
||||
'tiktok_shop_order_processing_fee' => $feeRule,
|
||||
'tiktok_shop_affiliate' => $feeRule,
|
||||
'tiktok_shop_pre_order_service_fee' => $feeRule,
|
||||
'shopee_admin_fee' => $feeRule,
|
||||
'shopee_program_fee' => $feeRule,
|
||||
'shopee_shipping_savings' => $feeRule,
|
||||
'shopee_premium' => $feeRule,
|
||||
'shopee_service_fee' => $feeRule,
|
||||
'shopee_order_processing_fee' => $feeRule,
|
||||
'shopee_ams_commission_fee' => $feeRule,
|
||||
'shopee_pre_order' => $feeRule,
|
||||
'shopee_live_extra' => $feeRule,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('tiktok_shop_platform_commission.base');
|
||||
});
|
||||
|
||||
test('marketplace type field must be valid', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$feeRule = ['base' => 'per_transaksi', 'type' => 'invalid', 'value' => 0];
|
||||
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => $feeRule,
|
||||
'tiktok_shop_logistics_service_fee' => $feeRule,
|
||||
'tiktok_shop_dynamic_commission' => $feeRule,
|
||||
'tiktok_shop_order_processing_fee' => $feeRule,
|
||||
'tiktok_shop_affiliate' => $feeRule,
|
||||
'tiktok_shop_pre_order_service_fee' => $feeRule,
|
||||
'shopee_admin_fee' => $feeRule,
|
||||
'shopee_program_fee' => $feeRule,
|
||||
'shopee_shipping_savings' => $feeRule,
|
||||
'shopee_premium' => $feeRule,
|
||||
'shopee_service_fee' => $feeRule,
|
||||
'shopee_order_processing_fee' => $feeRule,
|
||||
'shopee_ams_commission_fee' => $feeRule,
|
||||
'shopee_pre_order' => $feeRule,
|
||||
'shopee_live_extra' => $feeRule,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('tiktok_shop_platform_commission.type');
|
||||
});
|
||||
|
||||
test('marketplace value must be numeric', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$feeRule = ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 'not-a-number'];
|
||||
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => $feeRule,
|
||||
'tiktok_shop_logistics_service_fee' => $feeRule,
|
||||
'tiktok_shop_dynamic_commission' => $feeRule,
|
||||
'tiktok_shop_order_processing_fee' => $feeRule,
|
||||
'tiktok_shop_affiliate' => $feeRule,
|
||||
'tiktok_shop_pre_order_service_fee' => $feeRule,
|
||||
'shopee_admin_fee' => $feeRule,
|
||||
'shopee_program_fee' => $feeRule,
|
||||
'shopee_shipping_savings' => $feeRule,
|
||||
'shopee_premium' => $feeRule,
|
||||
'shopee_service_fee' => $feeRule,
|
||||
'shopee_order_processing_fee' => $feeRule,
|
||||
'shopee_ams_commission_fee' => $feeRule,
|
||||
'shopee_pre_order' => $feeRule,
|
||||
'shopee_live_extra' => $feeRule,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('tiktok_shop_platform_commission.value');
|
||||
});
|
||||
|
||||
test('marketplace value must be at least 0', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$feeRule = ['base' => 'per_transaksi', 'type' => 'flat', 'value' => -1];
|
||||
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => $feeRule,
|
||||
'tiktok_shop_logistics_service_fee' => $feeRule,
|
||||
'tiktok_shop_dynamic_commission' => $feeRule,
|
||||
'tiktok_shop_order_processing_fee' => $feeRule,
|
||||
'tiktok_shop_affiliate' => $feeRule,
|
||||
'tiktok_shop_pre_order_service_fee' => $feeRule,
|
||||
'shopee_admin_fee' => $feeRule,
|
||||
'shopee_program_fee' => $feeRule,
|
||||
'shopee_shipping_savings' => $feeRule,
|
||||
'shopee_premium' => $feeRule,
|
||||
'shopee_service_fee' => $feeRule,
|
||||
'shopee_order_processing_fee' => $feeRule,
|
||||
'shopee_ams_commission_fee' => $feeRule,
|
||||
'shopee_pre_order' => $feeRule,
|
||||
'shopee_live_extra' => $feeRule,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('tiktok_shop_platform_commission.value');
|
||||
});
|
||||
|
||||
test('marketplace fee rule array is required', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => 'not-an-array',
|
||||
'tiktok_shop_logistics_service_fee' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'tiktok_shop_dynamic_commission' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'tiktok_shop_order_processing_fee' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'tiktok_shop_affiliate' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'tiktok_shop_pre_order_service_fee' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_admin_fee' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_program_fee' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_shipping_savings' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_premium' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_service_fee' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_order_processing_fee' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_ams_commission_fee' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_pre_order' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
'shopee_live_extra' => ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('tiktok_shop_platform_commission');
|
||||
});
|
||||
|
||||
test('marketplace each fee key must be provided', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$feeRule = ['base' => 'per_transaksi', 'type' => 'flat', 'value' => 0];
|
||||
|
||||
$response = $this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => $feeRule,
|
||||
// Missing other required keys
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors([
|
||||
'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',
|
||||
]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| UPDATE HR
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('user can update hr settings with valid data', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 10000,
|
||||
'absent_penalty_amount' => 50000,
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.settings.index'));
|
||||
|
||||
$settings = app(HRSettings::class);
|
||||
expect($settings->scheduled_check_in_time)->toBe('09:00');
|
||||
expect($settings->scheduled_check_out_time)->toBe('18:00');
|
||||
expect($settings->late_penalty_amount)->toBe(10000);
|
||||
expect($settings->absent_penalty_amount)->toBe(50000);
|
||||
});
|
||||
|
||||
test('hr scheduled_check_in_time is required', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 0,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('scheduled_check_in_time');
|
||||
});
|
||||
|
||||
test('hr scheduled_check_out_time is required', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 0,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('scheduled_check_out_time');
|
||||
});
|
||||
|
||||
test('hr late_penalty_amount is required', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => '',
|
||||
'absent_penalty_amount' => 0,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('late_penalty_amount');
|
||||
});
|
||||
|
||||
test('hr absent_penalty_amount is required', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('absent_penalty_amount');
|
||||
});
|
||||
|
||||
test('hr penalty amounts must be at least 0', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => -1,
|
||||
'absent_penalty_amount' => -1,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors(['late_penalty_amount', 'absent_penalty_amount']);
|
||||
});
|
||||
|
||||
test('hr penalty amounts must be integers', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 1000.50,
|
||||
'absent_penalty_amount' => 2000.75,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors(['late_penalty_amount', 'absent_penalty_amount']);
|
||||
});
|
||||
|
||||
test('hr time fields must not exceed 5 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '09:000',
|
||||
'scheduled_check_out_time' => '18:000',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 0,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors(['scheduled_check_in_time', 'scheduled_check_out_time']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| EDGE CASES & DATA INTEGRITY
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('system settings default values exist', function () {
|
||||
$settings = app(SystemSettings::class);
|
||||
|
||||
expect($settings->app_name)->not->toBeEmpty();
|
||||
expect($settings->about_app)->not->toBeNull();
|
||||
expect($settings->address)->not->toBeNull();
|
||||
expect($settings->email)->not->toBeNull();
|
||||
expect($settings->phone)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('hr settings default values exist', function () {
|
||||
$settings = app(HRSettings::class);
|
||||
|
||||
expect($settings->scheduled_check_in_time)->not->toBeEmpty();
|
||||
expect($settings->scheduled_check_out_time)->not->toBeEmpty();
|
||||
expect($settings->late_penalty_amount)->toBeInt();
|
||||
expect($settings->absent_penalty_amount)->toBeInt();
|
||||
});
|
||||
|
||||
test('marketplace settings default values are MarketplaceFeeRule', function () {
|
||||
$settings = app(MarketplaceSettings::class);
|
||||
|
||||
expect($settings->tiktok_shop_platform_commission)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->tiktok_shop_logistics_service_fee)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->tiktok_shop_dynamic_commission)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->tiktok_shop_order_processing_fee)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->tiktok_shop_affiliate)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->tiktok_shop_pre_order_service_fee)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_admin_fee)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_program_fee)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_shipping_savings)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_premium)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_service_fee)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_order_processing_fee)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_ams_commission_fee)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_pre_order)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
expect($settings->shopee_live_extra)->toBeInstanceOf(MarketplaceFeeRule::class);
|
||||
});
|
||||
|
||||
test('marketplace fee rule to array contains correct keys', function () {
|
||||
$rule = MarketplaceFeeRule::defaultPercent(0.0);
|
||||
$array = $rule->toArray();
|
||||
|
||||
expect($array)->toHaveKeys(['base', 'type', 'value']);
|
||||
expect($array['base'])->toBe('per_transaksi');
|
||||
expect($array['type'])->toBe('persentase');
|
||||
expect($array['value'])->toBe(0.0);
|
||||
});
|
||||
|
||||
test('updating one setting group does not affect others', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$systemBefore = app(SystemSettings::class);
|
||||
$hrBefore = app(HRSettings::class);
|
||||
|
||||
$this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'Changed Name',
|
||||
'about_app' => $systemBefore->about_app,
|
||||
'address' => $systemBefore->address,
|
||||
'email' => $systemBefore->email,
|
||||
'phone' => $systemBefore->phone,
|
||||
]);
|
||||
|
||||
$systemAfter = app(SystemSettings::class);
|
||||
$hrAfter = app(HRSettings::class);
|
||||
|
||||
expect($systemAfter->app_name)->toBe('Changed Name');
|
||||
expect($hrAfter->scheduled_check_in_time)->toBe($hrBefore->scheduled_check_in_time);
|
||||
expect($hrAfter->late_penalty_amount)->toBe($hrBefore->late_penalty_amount);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| REALISTIC USER SCENARIOS
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('user updates all settings groups in sequence', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
// Update system
|
||||
$this->put(route('admin.settings.update-system'), [
|
||||
'app_name' => 'DST Updated',
|
||||
'about_app' => 'About Updated',
|
||||
'address' => 'Address Updated',
|
||||
'email' => 'updated@dst.com',
|
||||
'phone' => '089999999999',
|
||||
])->assertSessionHasNoErrors();
|
||||
|
||||
// Update homepage
|
||||
$this->put(route('admin.settings.update-homepage'), [
|
||||
'hero_image_url' => 'https://example.com/new-hero.jpg',
|
||||
'about_image_url' => 'https://example.com/new-about.jpg',
|
||||
])->assertSessionHasNoErrors();
|
||||
|
||||
// Update social media
|
||||
$this->put(route('admin.settings.update-social-media'), [
|
||||
'instagram_url' => 'https://instagram.com/newdst',
|
||||
'facebook_url' => 'https://facebook.com/newdst',
|
||||
'tiktok_url' => 'https://tiktok.com/@newdst',
|
||||
])->assertSessionHasNoErrors();
|
||||
|
||||
// Update marketplace
|
||||
$feeRule = ['base' => 'per_transaksi', 'type' => 'persentase', 'value' => 3.0];
|
||||
$this->put(route('admin.settings.update-marketplace'), [
|
||||
'tiktok_shop_platform_commission' => $feeRule,
|
||||
'tiktok_shop_logistics_service_fee' => $feeRule,
|
||||
'tiktok_shop_dynamic_commission' => $feeRule,
|
||||
'tiktok_shop_order_processing_fee' => $feeRule,
|
||||
'tiktok_shop_affiliate' => $feeRule,
|
||||
'tiktok_shop_pre_order_service_fee' => $feeRule,
|
||||
'shopee_admin_fee' => $feeRule,
|
||||
'shopee_program_fee' => $feeRule,
|
||||
'shopee_shipping_savings' => $feeRule,
|
||||
'shopee_premium' => $feeRule,
|
||||
'shopee_service_fee' => $feeRule,
|
||||
'shopee_order_processing_fee' => $feeRule,
|
||||
'shopee_ams_commission_fee' => $feeRule,
|
||||
'shopee_pre_order' => $feeRule,
|
||||
'shopee_live_extra' => $feeRule,
|
||||
])->assertSessionHasNoErrors();
|
||||
|
||||
// Update HR
|
||||
$this->put(route('admin.settings.update-hr'), [
|
||||
'scheduled_check_in_time' => '08:30',
|
||||
'scheduled_check_out_time' => '17:30',
|
||||
'late_penalty_amount' => 15000,
|
||||
'absent_penalty_amount' => 75000,
|
||||
])->assertSessionHasNoErrors();
|
||||
|
||||
// Verify all settings persisted
|
||||
$system = app(SystemSettings::class);
|
||||
$homepage = app(HomepageSettings::class);
|
||||
$socialMedia = app(SocialMediaSettings::class);
|
||||
$marketplace = app(MarketplaceSettings::class);
|
||||
$hr = app(HRSettings::class);
|
||||
|
||||
expect($system->app_name)->toBe('DST Updated');
|
||||
expect($homepage->hero_image_url)->toBe('https://example.com/new-hero.jpg');
|
||||
expect($socialMedia->instagram_url)->toBe('https://instagram.com/newdst');
|
||||
expect($marketplace->tiktok_shop_platform_commission->value)->toBe(3.0);
|
||||
expect($hr->scheduled_check_in_time)->toBe('08:30');
|
||||
expect($hr->late_penalty_amount)->toBe(15000);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user