diff --git a/app/Http/Controllers/Admin/System/GeneralSettingController.php b/app/Http/Controllers/Admin/System/GeneralSettingController.php new file mode 100644 index 0000000..1a374b0 --- /dev/null +++ b/app/Http/Controllers/Admin/System/GeneralSettingController.php @@ -0,0 +1,42 @@ + GeneralSetting::first(), + ]); + } + + public function update(GeneralSettingRequest $request): RedirectResponse + { + $validated = $request->validated(); + + $setting = GeneralSetting::first(); + if ($setting) { + $setting->update($validated); + } else { + $setting = GeneralSetting::create($validated); + } + + if ($request->hasFile('logo')) { + $setting->addMediaFromRequest('logo')->toMediaCollection('logo'); + } + + if ($request->hasFile('icon')) { + $setting->addMediaFromRequest('icon')->toMediaCollection('icon'); + } + + return redirect()->back()->with('success', 'Pengaturan berhasil diperbarui'); + } +} diff --git a/app/Http/Requests/Admin/System/GeneralSettingRequest.php b/app/Http/Requests/Admin/System/GeneralSettingRequest.php new file mode 100644 index 0000000..0f8989b --- /dev/null +++ b/app/Http/Requests/Admin/System/GeneralSettingRequest.php @@ -0,0 +1,37 @@ +|string> + */ + public function rules(): array + { + $settingExists = GeneralSetting::exists(); + + return [ + 'name' => ['required', 'string', 'max:255'], + 'description' => ['required', 'string'], + 'address' => ['required', 'string'], + 'phone' => ['required', 'string', 'max:20'], + 'logo' => [$settingExists ? 'nullable' : 'required', 'image', 'max:2048'], + 'icon' => [$settingExists ? 'nullable' : 'required', 'image', 'max:1024'], + ]; + } +} diff --git a/app/Models/GeneralSetting.php b/app/Models/GeneralSetting.php new file mode 100644 index 0000000..aff5e07 --- /dev/null +++ b/app/Models/GeneralSetting.php @@ -0,0 +1,91 @@ +addMediaCollection('logo') + ->singleFile(); + + $this->addMediaCollection('icon') + ->singleFile(); + } + + protected function logoUrl(): Attribute + { + return Attribute::make( + get: fn () => $this->getFirstMediaUrl('logo') ?: null, + ); + } + + protected function iconUrl(): Attribute + { + return Attribute::make( + get: fn () => $this->getFirstMediaUrl('icon') ?: 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); + } + } +} diff --git a/database/migrations/2026_04_21_000456_create_general_settings_table.php b/database/migrations/2026_04_21_000456_create_general_settings_table.php new file mode 100644 index 0000000..129ee47 --- /dev/null +++ b/database/migrations/2026_04_21_000456_create_general_settings_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->text('address')->nullable(); + $table->string('phone')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('general_settings'); + } +}; diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 99bff9f..4fe55b6 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -1,5 +1,5 @@ import { Link } from '@inertiajs/react'; -import { Boxes, DollarSign, History, LayoutGrid, List, ScrollText, ShoppingCart, User, Wallet } from 'lucide-react'; +import { Boxes, DollarSign, History, LayoutGrid, List, ScrollText, Settings, ShoppingCart, User, Wallet } from 'lucide-react'; import AppLogo from '@/components/app-logo'; import { NavMain } from '@/components/nav-main'; import { @@ -69,6 +69,11 @@ const financeNavItems: NavItem[] = [ ]; const systemNavItems: NavItem[] = [ + { + title: 'Pengaturan', + href: system.settings.index().url, + icon: Settings, + }, { title: 'Logs', href: system.logs.index().url, diff --git a/resources/js/pages/admin/system/settings/index.tsx b/resources/js/pages/admin/system/settings/index.tsx new file mode 100644 index 0000000..3b636b0 --- /dev/null +++ b/resources/js/pages/admin/system/settings/index.tsx @@ -0,0 +1,250 @@ +import { Head, useForm } from '@inertiajs/react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Field } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from '@/components/ui/textarea'; +import type { GeneralSetting } from '@/types'; +import React, { useRef, useState } from 'react'; +import { toast } from 'sonner'; +import systemRoutes from '@/routes/system'; +import { ImagePlus, X } from 'lucide-react'; + +export default function SettingIndex({ setting }: { setting: GeneralSetting | null }) { + const { data, setData, post, processing, errors } = useForm({ + name: setting?.name || '', + description: setting?.description || '', + address: setting?.address || '', + phone: setting?.phone || '', + logo: null as File | null, + icon: null as File | null, + }); + + const logoInputRef = useRef(null); + const iconInputRef = useRef(null); + + const [logoPreview, setLogoPreview] = useState(setting?.logo_url || null); + const [iconPreview, setIconPreview] = useState(setting?.icon_url || null); + + const handleLogoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setData('logo', file); + const reader = new FileReader(); + reader.onloadend = () => setLogoPreview(reader.result as string); + reader.readAsDataURL(file); + }; + + const removeLogo = () => { + setData('logo', null); + setLogoPreview(null); + if (logoInputRef.current) logoInputRef.current.value = ''; + }; + + const handleIconChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setData('icon', file); + const reader = new FileReader(); + reader.onloadend = () => setIconPreview(reader.result as string); + reader.readAsDataURL(file); + }; + + const removeIcon = () => { + setData('icon', null); + setIconPreview(null); + if (iconInputRef.current) iconInputRef.current.value = ''; + }; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(systemRoutes.settings.update().url, { + forceFormData: true, + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + }, + }); + }; + + return ( +
+ + +
+
+

Pengaturan Umum

+
+
+ +
+
+
+ + + Informasi Aplikasi + + + + + setData('name', e.target.value)} + autoComplete='off' + placeholder='Contoh: VN Grup Dress' + maxLength={100} + /> + {errors.name &&

{errors.name}

} +
+ + + +