refactor: migrate general settings to spatie/laravel-settings and rename properties for consistency

This commit is contained in:
Yoga Pangestu 2026-05-02 01:05:09 +07:00
parent 044dffefa8
commit 3fcdaa7475
23 changed files with 518 additions and 431 deletions

View File

@ -8,7 +8,6 @@
use App\Enums\PriceType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\Order\OrderRequest;
use App\Models\GeneralSetting;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
@ -122,7 +121,6 @@ public function show(Order $order): Response
return Inertia::render('admin/manage/order/show', [
'order' => $order,
'setting' => GeneralSetting::first(),
]);
}

View File

@ -4,8 +4,9 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\System\GeneralSettingRequest;
use App\Models\GeneralSetting;
use App\Settings\GeneralSettings;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Inertia\Inertia;
use Inertia\Response;
@ -13,37 +14,47 @@ class GeneralSettingController extends Controller
{
public function index(): Response
{
return Inertia::render('admin/system/settings/index', [
'setting' => GeneralSetting::first(),
]);
return Inertia::render('admin/system/settings/index');
}
public function update(GeneralSettingRequest $request): RedirectResponse
public function update(GeneralSettingRequest $request, GeneralSettings $settings): RedirectResponse
{
$validated = $request->validated();
$setting = GeneralSetting::first();
if ($setting) {
$setting->update($validated);
} else {
$setting = GeneralSetting::create($validated);
}
$settings->site_name = $validated['site_name'];
$settings->site_description = $validated['site_description'];
$settings->site_address = $validated['site_address'];
$settings->site_phone = $validated['site_phone'];
if ($request->hasFile('logo_light')) {
$setting->addMediaFromRequest('logo_light')->toMediaCollection('logo_light');
if ($settings->logo_light) {
Storage::disk('public')->delete($settings->logo_light);
}
$settings->logo_light = $request->file('logo_light')->store('settings', 'public');
}
if ($request->hasFile('logo_dark')) {
$setting->addMediaFromRequest('logo_dark')->toMediaCollection('logo_dark');
if ($settings->logo_dark) {
Storage::disk('public')->delete($settings->logo_dark);
}
$settings->logo_dark = $request->file('logo_dark')->store('settings', 'public');
}
if ($request->hasFile('icon_light')) {
$setting->addMediaFromRequest('icon_light')->toMediaCollection('icon_light');
if ($settings->icon_light) {
Storage::disk('public')->delete($settings->icon_light);
}
$settings->icon_light = $request->file('icon_light')->store('settings', 'public');
}
if ($request->hasFile('icon_dark')) {
$setting->addMediaFromRequest('icon_dark')->toMediaCollection('icon_dark');
if ($settings->icon_dark) {
Storage::disk('public')->delete($settings->icon_dark);
}
$settings->icon_dark = $request->file('icon_dark')->store('settings', 'public');
}
$settings->save();
return redirect()->back()->with('success', 'Pengaturan berhasil diperbarui');
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\GeneralSetting;
use App\Models\Product;
class HomepageController extends Controller
@ -14,7 +13,6 @@ public function __invoke()
$categorySlug = request('category');
return inertia('homepage', [
'setting' => GeneralSetting::first(),
'categories' => Category::active()->get(),
'bestSellers' => Product::with(['categories' => fn ($q) => $q->active(), 'prices'])
->when($search, function ($query, $search) {

View File

@ -2,7 +2,6 @@
namespace App\Http\Middleware;
use App\Models\GeneralSetting;
use Illuminate\Http\Request;
use Inertia\Middleware;
@ -39,7 +38,6 @@ public function share(Request $request): array
return [
...parent::share($request),
'name' => config('app.name'),
'setting' => GeneralSetting::first(),
'auth' => [
'user' => $request->user() ? $request->user()->load('profile') : null,
'roles' => $request->user() ? $request->user()->getRoleNames() : [],

View File

@ -2,7 +2,6 @@
namespace App\Http\Requests\Admin\System;
use App\Models\GeneralSetting;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
@ -23,16 +22,14 @@ public function authorize(): bool
*/
public function rules(): array
{
$settingExists = GeneralSetting::exists();
return [
'name' => ['required', 'string', 'max:50'],
'description' => ['required', 'string'],
'address' => ['required', 'string'],
'phone' => ['required', 'string', 'max:20'],
'logo_light' => [$settingExists ? 'nullable' : 'required', 'image', 'max:2048'],
'site_name' => ['required', 'string', 'max:50'],
'site_description' => ['required', 'string'],
'site_address' => ['required', 'string'],
'site_phone' => ['required', 'string', 'max:20'],
'logo_light' => ['nullable', 'image', 'max:2048'],
'logo_dark' => ['nullable', 'image', 'max:2048'],
'icon_light' => [$settingExists ? 'nullable' : 'required', 'image', 'max:1024'],
'icon_light' => ['nullable', 'image', 'max:1024'],
'icon_dark' => ['nullable', 'image', 'max:1024'],
];
}

View File

@ -1,111 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Models\Activity;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
#[Guarded(['id'])]
#[Appends(['logo_light_url', 'logo_dark_url', 'icon_light_url', 'icon_dark_url'])]
class GeneralSetting extends Model implements HasMedia
{
use HasFactory, InteractsWithMedia, LogsActivity;
public function registerMediaCollections(): void
{
$this->addMediaCollection('logo_light')
->singleFile();
$this->addMediaCollection('logo_dark')
->singleFile();
$this->addMediaCollection('icon_light')
->singleFile();
$this->addMediaCollection('icon_dark')
->singleFile();
}
protected function logoLightUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->getFirstMediaUrl('logo_light') ?: null,
);
}
protected function logoDarkUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->getFirstMediaUrl('logo_dark') ?: null,
);
}
protected function iconLightUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->getFirstMediaUrl('icon_light') ?: null,
);
}
protected function iconDarkUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->getFirstMediaUrl('icon_dark') ?: null,
);
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['name', 'description', 'address', 'phone'])
->logOnlyDirty()
->useLogName('Pengaturan Umum');
}
public function tapActivity(Activity $activity, string $eventName)
{
$activity->description = match ($eventName) {
'created' => 'TAMBAH',
'updated' => 'UBAH',
'deleted' => 'HAPUS',
default => $activity->description,
};
if (isset($activity->properties['attributes'])) {
$attributeMap = [
'name' => 'Nama Aplikasi',
'description' => 'Deskripsi',
'address' => 'Alamat',
'phone' => 'No. Telepon',
];
$properties = $activity->properties->toArray();
$localizeValues = function ($attrs) use ($attributeMap) {
$newAttrs = [];
foreach ($attrs as $key => $value) {
$label = $attributeMap[$key] ?? $key;
$newAttrs[$label] = $value;
}
return $newAttrs;
};
$properties['attributes'] = $localizeValues($properties['attributes']);
if (isset($properties['old'])) {
$properties['old'] = $localizeValues($properties['old']);
}
$activity->properties = collect($properties);
}
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* View Service Provider
*
* This provider handles global data sharing for both Inertia and Blade views.
*/
namespace App\Providers;
use App\Settings\GeneralSettings;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Inertia\Inertia;
class ViewServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
if ($this->app->runningInConsole() && ! $this->app->environment('testing')) {
return;
}
// Share settings with Inertia
Inertia::share('setting', function () {
try {
$settings = app(GeneralSettings::class);
return [
'site_name' => $settings->site_name,
'site_description' => $settings->site_description,
'site_address' => $settings->site_address,
'site_phone' => $settings->site_phone,
'logo_light_url' => $settings->logo_light ? Storage::url($settings->logo_light) : null,
'logo_dark_url' => $settings->logo_dark ? Storage::url($settings->logo_dark) : null,
'icon_light_url' => $settings->icon_light ? Storage::url($settings->icon_light) : null,
'icon_dark_url' => $settings->icon_dark ? Storage::url($settings->icon_dark) : null,
];
} catch (\Exception $e) {
return [];
}
});
// Share settings with Blade views
View::composer('*', function ($view) {
try {
$settings = app(GeneralSettings::class);
$view->with('setting', [
'site_name' => $settings->site_name,
'site_description' => $settings->site_description,
'site_address' => $settings->site_address,
'site_phone' => $settings->site_phone,
'logo_light_url' => $settings->logo_light ? Storage::url($settings->logo_light) : null,
'logo_dark_url' => $settings->logo_dark ? Storage::url($settings->logo_dark) : null,
'icon_light_url' => $settings->icon_light ? Storage::url($settings->icon_light) : null,
'icon_dark_url' => $settings->icon_dark ? Storage::url($settings->icon_dark) : null,
]);
} catch (\Exception $e) {
$view->with('setting', []);
}
});
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class GeneralSettings extends Settings
{
public string $site_name;
public string $site_description;
public string $site_address;
public string $site_phone;
public ?string $logo_light;
public ?string $logo_dark;
public ?string $icon_light;
public ?string $icon_dark;
public static function group(): string
{
return 'general';
}
}

View File

@ -2,8 +2,10 @@
use App\Providers\AppServiceProvider;
use App\Providers\FortifyServiceProvider;
use App\Providers\ViewServiceProvider;
return [
AppServiceProvider::class,
FortifyServiceProvider::class,
ViewServiceProvider::class,
];

View File

@ -19,6 +19,7 @@
"spatie/laravel-activitylog": "^4.12",
"spatie/laravel-medialibrary": "^11.21",
"spatie/laravel-permission": "^7.3",
"spatie/laravel-settings": "^3.8",
"spatie/laravel-sluggable": "^3.8"
},
"require-dev": {

405
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1e97290615923580b9a565671ca7bfae",
"content-hash": "a42046c1187875d3d873b061a01fa885",
"packages": [
{
"name": "archtechx/enums",
@ -445,6 +445,54 @@
},
"time": "2024-07-08T12:26:09+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.6",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=14"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^14",
"phpstan/phpstan": "1.4.10 || 2.1.30",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
},
"time": "2026-02-07T07:09:04+00:00"
},
{
"name": "doctrine/inflector",
"version": "2.1.0",
@ -3976,6 +4024,117 @@
},
"time": "2025-09-24T15:06:41+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
"homepage": "http://www.phpdoc.org",
"keywords": [
"FQSEN",
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
},
"time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9",
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^1.0",
"php": "^7.4 || ^8.0",
"phpdocumentor/reflection-common": "^2.0",
"phpstan/phpdoc-parser": "^2.0"
},
"require-dev": {
"ext-tokenizer": "*",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psalm/phar": "^4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0"
},
"time": "2026-01-06T21:53:42+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",
@ -5316,6 +5475,91 @@
],
"time": "2026-04-07T15:19:42+00:00"
},
{
"name": "spatie/laravel-settings",
"version": "3.8.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-settings.git",
"reference": "09b788ee96d205699420dedb8a6aa8c4c8af84fe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-settings/zipball/09b788ee96d205699420dedb8a6aa8c4c8af84fe",
"reference": "09b788ee96d205699420dedb8a6aa8c4c8af84fe",
"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.8.0"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-04-28T07:05:06+00:00"
},
{
"name": "spatie/laravel-sluggable",
"version": "3.8.1",
@ -8245,54 +8489,6 @@
],
"time": "2026-03-29T15:46:14+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.6",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=14"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^14",
"phpstan/phpstan": "1.4.10 || 2.1.30",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
},
"time": "2026-02-07T07:09:04+00:00"
},
{
"name": "fakerphp/faker",
"version": "v1.24.1",
@ -9630,59 +9826,6 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
"homepage": "http://www.phpdoc.org",
"keywords": [
"FQSEN",
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
},
"time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "6.0.3",
@ -9748,64 +9891,6 @@
},
"time": "2026-03-18T20:49:53+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9",
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^1.0",
"php": "^7.4 || ^8.0",
"phpdocumentor/reflection-common": "^2.0",
"phpstan/phpdoc-parser": "^2.0"
},
"require-dev": {
"ext-tokenizer": "*",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psalm/phar": "^4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0"
},
"time": "2026-01-06T21:53:42+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "12.5.6",

View File

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

View File

@ -1,31 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('general_settings', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->text('description');
$table->text('address');
$table->string('phone', 20);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('general_settings');
}
};

View File

@ -2,7 +2,7 @@
namespace Database\Seeders;
use App\Models\GeneralSetting;
use App\Settings\GeneralSettings;
use Illuminate\Database\Seeder;
class GeneralSettingSeeder extends Seeder
@ -12,11 +12,11 @@ class GeneralSettingSeeder extends Seeder
*/
public function run(): void
{
GeneralSetting::updateOrCreate([
'name' => 'VN Grup',
'description' => 'VN Grup adalah Toko Baju Daster Online yang menghadirkan koleksi baju daster berkualitas tinggi untuk wanita yang mengutamakan kenyamanan tanpa mengorbankan gaya. Kami menyediakan berbagai macam daster dengan desain yang beragam, mulai dari yang sederhana dan klasik hingga yang modern dan trendi, cocok untuk berbagai kebutuhan, baik di rumah maupun saat bersantai di luar.',
'address' => 'Jl. Raya Pabuaran-Cipeunduy, Kec. Pabuaran, Kab. Subang, Jawa Barat',
'phone' => '089679965828',
]);
$settings = app(GeneralSettings::class);
$settings->site_name = 'VN Grup';
$settings->site_description = 'VN Grup adalah Toko Baju Daster Online yang menghadirkan koleksi baju daster berkualitas tinggi untuk wanita yang mengutamakan kenyamanan tanpa mengorbankan gaya. Kami menyediakan berbagai macam daster dengan desain yang beragam, mulai dari yang sederhana dan klasik hingga yang modern dan trendi, cocok untuk berbagai kebutuhan, baik di rumah maupun saat bersantai di luar.';
$settings->site_address = 'Jl. Raya Pabuaran-Cipeunduy, Kec. Pabuaran, Kab. Subang, Jawa Barat';
$settings->site_phone = '089679965828';
$settings->save();
}
}

View File

@ -0,0 +1,18 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('general.site_name', 'VN Grup');
$this->migrator->add('general.site_description', 'VN Grup adalah Toko Baju Daster Online yang menghadirkan koleksi baju daster berkualitas tinggi untuk wanita yang mengutamakan kenyamanan tanpa mengorbankan gaya.');
$this->migrator->add('general.site_address', 'Jl. Raya Pabuaran-Cipeunduy, Kec. Pabuaran, Kab. Subang, Jawa Barat');
$this->migrator->add('general.site_phone', '089679965828');
$this->migrator->add('general.logo_light', null);
$this->migrator->add('general.logo_dark', null);
$this->migrator->add('general.icon_light', null);
$this->migrator->add('general.icon_dark', null);
}
};

View File

@ -9,20 +9,20 @@ export default function AppLogo() {
{setting?.logo_light_url ? (
<img
src={setting.logo_light_url}
alt={setting.name}
alt={setting.site_name}
className="max-h-8 w-auto dark:hidden"
/>
) : null}
{setting?.logo_dark_url ? (
<img
src={setting.logo_dark_url}
alt={setting.name}
alt={setting.site_name}
className="hidden max-h-8 w-auto dark:block"
/>
) : null}
{!setting?.logo_light_url && !setting?.logo_dark_url && (
<span className="truncate font-semibold tracking-tight">
{setting?.name || 'VN Grup'}
{setting?.site_name || 'VN Grup'}
</span>
)}
</div>

View File

@ -62,17 +62,17 @@ export default function OrderShow({ order, setting }: ShowProps) {
<img src={setting.logo_light_url} alt="Logo" className="h-10 w-auto" />
) : (
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-2xl font-bold text-primary-foreground">
{setting?.name?.charAt(0) || 'V'}
{setting?.site_name?.charAt(0) || 'V'}
</div>
)}
<div>
<h2 className="text-2xl font-bold text-foreground">{setting?.name || 'VN Grup'}</h2>
<h2 className="text-2xl font-bold text-foreground">{setting?.site_name || 'VN Grup'}</h2>
<p className="text-sm text-muted-foreground italic">Your Style, Our Passion</p>
</div>
</div>
<div className="max-w-xs space-y-1 text-sm text-muted-foreground">
<p>{setting?.address || 'Alamat Toko Belum Diatur'}</p>
<p>{setting?.phone || '-'}</p>
<p>{setting?.site_address || 'Alamat Toko Belum Diatur'}</p>
<p>{setting?.site_phone || '-'}</p>
</div>
</div>
@ -147,7 +147,7 @@ export default function OrderShow({ order, setting }: ShowProps) {
<div className="rounded-xl bg-muted/30 p-4 space-y-2 border border-dashed">
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Catatan:</h4>
<p className="text-xs text-muted-foreground leading-relaxed">
Terima kasih telah berbelanja di {setting?.name || 'VN Grup'}.
Terima kasih telah berbelanja di {setting?.site_name || 'VN Grup'}.
Barang yang sudah dibeli tidak dapat ditukar atau dikembalikan kecuali ada perjanjian sebelumnya.
Simpan invoice ini sebagai bukti pembelian yang sah.
</p>
@ -181,7 +181,7 @@ export default function OrderShow({ order, setting }: ShowProps) {
<div className="pt-10 text-center text-sm text-muted-foreground print:pt-20">
<p className="font-medium">Semoga hari Anda menyenangkan!</p>
<p className="text-[10px] mt-1 opacity-50">Generated by {setting?.name || 'VN Grup'} System</p>
<p className="text-[10px] mt-1 opacity-50">Generated by {setting?.site_name || 'VN Grup'} System</p>
</div>
</CardContent>
</Card>

View File

@ -17,10 +17,10 @@ export default function SettingIndex({
setting: GeneralSetting | null;
}) {
const { data, setData, post, processing, errors } = useForm({
name: setting?.name || '',
description: setting?.description || '',
address: setting?.address || '',
phone: setting?.phone || '',
site_name: setting?.site_name || '',
site_description: setting?.site_description || '',
site_address: setting?.site_address || '',
site_phone: setting?.site_phone || '',
logo_light: null as File | null,
logo_dark: null as File | null,
icon_light: null as File | null,
@ -107,41 +107,41 @@ export default function SettingIndex({
<CardContent className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Field>
<Label htmlFor="phone" required>
<Label htmlFor="site_phone" required>
No. Telepon
</Label>
<Input
id="phone"
value={data.phone || ''}
id="site_phone"
value={data.site_phone || ''}
onChange={(e) =>
setData('phone', e.target.value)
setData('site_phone', e.target.value)
}
placeholder="0812xxxx"
/>
<FieldError
error={errors.phone}
error={errors.site_phone}
label="No. Telepon"
className="text-xs"
/>
</Field>
<Field>
<Label htmlFor="name" required>
<Label htmlFor="site_name" required>
Nama Aplikasi
</Label>
<Input
id="name"
name="name"
value={data.name}
id="site_name"
name="site_name"
value={data.site_name}
onChange={(e) =>
setData('name', e.target.value)
setData('site_name', e.target.value)
}
autoComplete="off"
placeholder="Contoh: VN Grup Dress"
maxLength={100}
/>
<FieldError
error={errors.name}
error={errors.site_name}
label="Nama Aplikasi"
className="text-xs"
/>
@ -149,15 +149,15 @@ export default function SettingIndex({
</div>
<Field>
<Label htmlFor="description" required>
<Label htmlFor="site_description" required>
Deskripsi
</Label>
<Textarea
id="description"
value={data.description || ''}
id="site_description"
value={data.site_description || ''}
onChange={(e) =>
setData(
'description',
'site_description',
e.target.value,
)
}
@ -165,27 +165,27 @@ export default function SettingIndex({
rows={4}
/>
<FieldError
error={errors.description}
error={errors.site_description}
label="Deskripsi"
className="mt-1 text-xs"
/>
</Field>
<Field>
<Label htmlFor="address" required>
<Label htmlFor="site_address" required>
Alamat
</Label>
<Textarea
id="address"
value={data.address || ''}
id="site_address"
value={data.site_address || ''}
onChange={(e) =>
setData('address', e.target.value)
setData('site_address', e.target.value)
}
placeholder="Alamat lengkap toko"
rows={3}
/>
<FieldError
error={errors.address}
error={errors.site_address}
label="Alamat"
className="mt-1 text-xs"
/>

View File

@ -141,11 +141,11 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
{setting?.logo_light_url ? (
<img
src={setting.logo_light_url}
alt={setting.name}
alt={setting.site_name}
className="max-h-8 w-auto"
/>
) : (
setting?.name || 'VN Grup'
setting?.site_name || 'VN Grup'
)}
</div>
@ -334,7 +334,7 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
<p className="mb-4 text-sm font-medium opacity-60">{formatCurrency(item.retail_price)}</p>
<a
href={`https://wa.me/628123456789?text=${encodeURIComponent(`Halo ${setting?.name || 'VN Grup'}, saya ingin memesan ${item.name} seharga ${formatCurrency(item.retail_price)}. Mohon info detail pembayarannya.`)}`}
href={`https://wa.me/628123456789?text=${encodeURIComponent(`Halo ${setting?.site_name || 'VN Grup'}, saya ingin memesan ${item.name} seharga ${formatCurrency(item.retail_price)}. Mohon info detail pembayarannya.`)}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 border border-[#1e4a6d] px-6 py-2 text-[8px] font-bold uppercase tracking-widest transition-all hover:bg-[#1e4a6d] hover:text-white"
@ -412,10 +412,10 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
{/* Brand Info */}
<div className="lg:col-span-6">
<div className="mb-6 flex items-center gap-2 text-xl font-bold uppercase tracking-tighter">
{setting?.logo_light_url ? <img src={setting.logo_light_url} alt={setting.name} className="max-h-8" /> : (setting?.name || 'VN Grup')}
{setting?.logo_light_url ? <img src={setting.logo_light_url} alt={setting.site_name} className="max-h-8" /> : (setting?.site_name || 'VN Grup')}
</div>
<p className="mb-8 text-sm leading-relaxed opacity-60">
{setting?.description || 'Menghadirkan produk esensial yang tak lekang oleh waktu dengan fokus pada kualitas, keberlanjutan, dan gaya yang abadi.'}
{setting?.site_description || 'Menghadirkan produk esensial yang tak lekang oleh waktu dengan fokus pada kualitas, keberlanjutan, dan gaya yang abadi.'}
</p>
<div className="flex gap-4">
<Facebook size={18} className="cursor-pointer hover:opacity-50" />
@ -443,13 +443,13 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
<Search size={14} className="rotate-45" />
</div>
<p className="leading-relaxed whitespace-pre-line">
{setting?.address || 'Jl. Raya Utama No. 123, \n Kec. Lengkong, Kota Bandung, \n Jawa Barat 40262'}
{setting?.site_address || 'Jl. Raya Utama No. 123, \n Kec. Lengkong, Kota Bandung, \n Jawa Barat 40262'}
</p>
</div>
<div className="flex items-center gap-3">
<Phone size={14} />
<a href={`https://wa.me/${(setting?.phone || '+628123456789').replace(/\D/g, '')}`} target="_blank" className="hover:underline">
{setting?.phone || '+62 812 3456 789'}
<a href={`https://wa.me/${(setting?.site_phone || '+628123456789').replace(/\D/g, '')}`} target="_blank" className="hover:underline">
{setting?.site_phone || '+62 812 3456 789'}
</a>
</div>
</div>
@ -458,7 +458,7 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
{/* Bottom Bar */}
<div className="px-6 py-6 text-center text-[10px] font-bold uppercase tracking-widest lg:px-12 lg:text-left">
<p className="opacity-50">© {new Date().getFullYear()} {setting?.name || 'VN Grup'}.</p>
<p className="opacity-50">© {new Date().getFullYear()} {setting?.site_name || 'VN Grup'}.</p>
</div>
</div>
</footer>
@ -564,7 +564,7 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
<div className="mt-auto pt-8 border-t border-[#1e4a6d]/10">
<a
href={`https://wa.me/628123456789?text=${encodeURIComponent(`Halo ${setting?.name || 'VN Grup'}, saya tertarik dengan produk ${selectedProduct?.name} seharga ${formatCurrency(selectedProduct?.retail_price || 0)}. Bisa tanya-tanya dulu?`)}`}
href={`https://wa.me/628123456789?text=${encodeURIComponent(`Halo ${setting?.site_name || 'VN Grup'}, saya tertarik dengan produk ${selectedProduct?.name} seharga ${formatCurrency(selectedProduct?.retail_price || 0)}. Bisa tanya-tanya dulu?`)}`}
target="_blank"
rel="noopener noreferrer"
className="w-full bg-[#1e4a6d] py-4 text-[10px] font-bold uppercase tracking-[0.2em] text-white transition-all hover:bg-[#15344e] flex items-center justify-center gap-2"

View File

@ -1,13 +1,10 @@
export interface GeneralSetting {
id: number;
name: string;
description: string | null;
address: string | null;
phone: string | null;
site_name: string;
site_description: string | null;
site_address: string | null;
site_phone: string | null;
logo_light_url: string | null;
logo_dark_url: string | null;
icon_light_url: string | null;
icon_dark_url: string | null;
created_at: string;
updated_at: string;
}

View File

@ -31,12 +31,8 @@
}
</style>
@php
$setting = \App\Models\GeneralSetting::first();
@endphp
@if ($setting && $setting->icon_light_url)
<link rel="icon" href="{{ $setting->icon_light_url }}" sizes="any">
@if ($setting['icon_light_url'] ?? null)
<link rel="icon" href="{{ $setting['icon_light_url'] }}" sizes="any">
@else
<link rel="icon" href="/favicon.ico" sizes="any">
@endif
@ -47,7 +43,7 @@
@viteReactRefresh
@vite(['resources/css/app.css', 'resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"])
<x-inertia::head>
<title>{{ config('app.name', 'VN Grup') }}</title>
<title>{{ $setting['site_name'] ?? config('app.name', 'VN Grup') }}</title>
</x-inertia::head>
</head>

View File

@ -4,10 +4,10 @@
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PriceType;
use App\Models\GeneralSetting;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Settings\GeneralSettings;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseHas;
@ -47,12 +47,12 @@
]);
actingAs($user);
GeneralSetting::create([
'name' => 'VN Grup',
'phone' => '08123456789',
'address' => 'Jl. Test No. 1',
'description' => 'Test Description',
]);
$settings = app(GeneralSettings::class);
$settings->site_name = 'VN Grup';
$settings->site_phone = '08123456789';
$settings->site_address = 'Jl. Test No. 1';
$settings->site_description = 'Test Description';
$settings->save();
});
it('can access order index page', function () {

View File

@ -1,6 +1,6 @@
<?php
use App\Models\GeneralSetting;
use App\Settings\GeneralSettings;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
@ -49,19 +49,11 @@
it('can update general settings with logo and icon variants', function () {
Storage::fake('public');
// Ensure a setting exists first for testing update logic in Controller
$setting = GeneralSetting::create([
'name' => 'Old Name',
'description' => 'Old Description',
'address' => 'Old Address',
'phone' => '000',
]);
$data = [
'name' => 'VNGrup Dress',
'description' => 'Toko baju premium',
'address' => 'Jakarta, Indonesia',
'phone' => '081234567890',
'site_name' => 'VNGrup Dress',
'site_description' => 'Toko baju premium',
'site_address' => 'Jakarta, Indonesia',
'site_phone' => '081234567890',
'logo_light' => UploadedFile::fake()->image('logo_light.png'),
'logo_dark' => UploadedFile::fake()->image('logo_dark.png'),
'icon_light' => UploadedFile::fake()->image('icon_light.png'),
@ -72,26 +64,34 @@
->assertRedirect()
->assertSessionHas('success');
assertDatabaseHas('general_settings', [
'id' => $setting->id,
'name' => 'VNGrup Dress',
'phone' => '081234567890',
assertDatabaseHas('settings', [
'group' => 'general',
'name' => 'site_name',
'payload' => json_encode('VNGrup Dress'),
]);
$setting->refresh();
expect($setting->getFirstMediaUrl('logo_light'))->not->toBeEmpty();
expect($setting->getFirstMediaUrl('logo_dark'))->not->toBeEmpty();
expect($setting->getFirstMediaUrl('icon_light'))->not->toBeEmpty();
expect($setting->getFirstMediaUrl('icon_dark'))->not->toBeEmpty();
assertDatabaseHas('settings', [
'group' => 'general',
'name' => 'site_phone',
'payload' => json_encode('081234567890'),
]);
$settings = app(GeneralSettings::class);
expect($settings->logo_light)->not->toBeNull();
expect($settings->logo_dark)->not->toBeNull();
expect($settings->icon_light)->not->toBeNull();
expect($settings->icon_dark)->not->toBeNull();
Storage::disk('public')->assertExists($settings->logo_light);
Storage::disk('public')->assertExists($settings->logo_dark);
Storage::disk('public')->assertExists($settings->icon_light);
Storage::disk('public')->assertExists($settings->icon_dark);
});
it('validates settings update with new variants', function () {
// If setting doesn't exist, logo_light and icon_light are required
GeneralSetting::truncate();
postJson(route('system.settings.update'), [])
->assertStatus(422)
->assertJsonValidationErrors(['name', 'description', 'address', 'phone', 'logo_light', 'icon_light']);
->assertJsonValidationErrors(['site_name', 'site_description', 'site_address', 'site_phone']);
});
});
@ -101,7 +101,7 @@
});
it('cannot update settings without permission', function () {
postJson(route('system.settings.update'), ['name' => 'Unauthorized'])
postJson(route('system.settings.update'), ['site_name' => 'Unauthorized'])
->assertStatus(403);
});
});