Add user settings management features including Appearance, Password, and Profile controllers for handling user preferences. Implement corresponding requests for validation and create views for each setting. Enhance middleware to manage appearance settings across the application. Update routes for new settings functionality and integrate UI components for user navigation in the settings layout.
This commit is contained in:
parent
ce460ed4fd
commit
01997dccc4
36
app/Http/Controllers/Admin/Setting/AppearanceController.php
Normal file
36
app/Http/Controllers/Admin/Setting/AppearanceController.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setting\UpdateAppearanceRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
|
||||
class AppearanceController extends Controller
|
||||
{
|
||||
public function edit(): Response
|
||||
{
|
||||
return Inertia::render('admin/setting/Appearance', [
|
||||
'appearance' => request()->cookie('appearance', 'system'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateAppearanceRequest $request): RedirectResponse
|
||||
{
|
||||
$appearance = $request->string('appearance')->toString();
|
||||
|
||||
Inertia::flash('success', 'Tampilan berhasil diperbarui.');
|
||||
|
||||
return redirect()
|
||||
->route('admin.setting.appearance')
|
||||
->withCookie(Cookie::create('appearance')
|
||||
->withValue($appearance)
|
||||
->withExpires(now()->addYear())
|
||||
->withPath('/')
|
||||
->withHttpOnly(false)
|
||||
->withSameSite('lax'));
|
||||
}
|
||||
}
|
||||
29
app/Http/Controllers/Admin/Setting/PasswordController.php
Normal file
29
app/Http/Controllers/Admin/Setting/PasswordController.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setting\UpdatePasswordRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
public function edit(): Response
|
||||
{
|
||||
return Inertia::render('admin/setting/Password');
|
||||
}
|
||||
|
||||
public function update(UpdatePasswordRequest $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$user->password = Hash::make($request->string('password')->toString());
|
||||
$user->save();
|
||||
|
||||
Inertia::flash('success', 'Kata sandi berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('admin.setting.password');
|
||||
}
|
||||
}
|
||||
37
app/Http/Controllers/Admin/Setting/ProfileController.php
Normal file
37
app/Http/Controllers/Admin/Setting/ProfileController.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setting;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setting\UpdateProfileRequest;
|
||||
use App\Services\Setting\ProfileService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProfileService $profileService,
|
||||
) {}
|
||||
|
||||
public function edit(): Response
|
||||
{
|
||||
$user = auth()->user()->load('profile');
|
||||
|
||||
return Inertia::render('admin/setting/Profile', [
|
||||
'genders' => Gender::selectOptions(),
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateProfileRequest $request): RedirectResponse
|
||||
{
|
||||
$this->profileService->update($request->validated());
|
||||
|
||||
Inertia::flash('success', 'Profil berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('admin.setting.profile');
|
||||
}
|
||||
}
|
||||
21
app/Http/Middleware/HandleAppearance.php
Normal file
21
app/Http/Middleware/HandleAppearance.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HandleAppearance
|
||||
{
|
||||
/**
|
||||
* @param Closure(Request): Response $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
View::share('appearance', $request->cookie('appearance', 'system'));
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,7 @@ public function share(Request $request): array
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state')
|
||||
|| $request->cookie('sidebar_state') === 'true',
|
||||
'appearance' => $request->cookie('appearance', 'system'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
21
app/Http/Requests/Admin/Setting/UpdateAppearanceRequest.php
Normal file
21
app/Http/Requests/Admin/Setting/UpdateAppearanceRequest.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setting;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateAppearanceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'appearance' => ['required', Rule::in(['light', 'dark', 'system'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/Admin/Setting/UpdatePasswordRequest.php
Normal file
22
app/Http/Requests/Admin/Setting/UpdatePasswordRequest.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setting;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class UpdatePasswordRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/Admin/Setting/UpdateProfileRequest.php
Normal file
28
app/Http/Requests/Admin/Setting/UpdateProfileRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setting;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateProfileRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'email', 'max:100', Rule::unique('users', 'email')->ignore($this->user()?->id)],
|
||||
'username' => ['required', 'string', 'max:20', 'alpha_dash', Rule::unique('users', 'username')->ignore($this->user()?->id)],
|
||||
'full_name' => ['required', 'string', 'max:200'],
|
||||
'phone_number' => ['nullable', 'string', 'regex:/^08\d{8,11}$/'],
|
||||
'gender' => ['nullable', Rule::enum(Gender::class)],
|
||||
'birth_date' => ['nullable', 'date', 'before:today'],
|
||||
'address' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Services/Setting/ProfileService.php
Normal file
30
app/Services/Setting/ProfileService.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Setting;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ProfileService
|
||||
{
|
||||
public function update(array $validated): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
DB::transaction(function () use ($user, $validated): void {
|
||||
$user->email = $validated['email'];
|
||||
$user->username = $validated['username'];
|
||||
$user->save();
|
||||
|
||||
$user->profile()->updateOrCreate(
|
||||
['user_id' => $user->id],
|
||||
[
|
||||
'full_name' => $validated['full_name'],
|
||||
'phone_number' => $validated['phone_number'] ?? null,
|
||||
'gender' => $validated['gender'] ?? null,
|
||||
'birth_date' => $validated['birth_date'] ?? null,
|
||||
'address' => $validated['address'] ?? null,
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\HandleAppearance;
|
||||
use App\Http\Middleware\HandleInertiaRequests;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
@ -24,6 +25,7 @@
|
||||
]);
|
||||
|
||||
$middleware->web(append: [
|
||||
HandleAppearance::class,
|
||||
HandleInertiaRequests::class,
|
||||
AddLinkHeadersForPreloadedAssets::class,
|
||||
]);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { router, usePage } from '@inertiajs/vue3';
|
||||
import { LogOut } from '@lucide/vue';
|
||||
import { LogOut, User } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -9,9 +9,9 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator, DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import Separator from './ui/separator/Separator.vue';
|
||||
|
||||
const page = usePage();
|
||||
|
||||
@ -49,6 +49,11 @@ function logout() {
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="router.visit('/admin/setting/profile')">
|
||||
<User />
|
||||
Profil
|
||||
</DropdownMenuItem>
|
||||
<Separator />
|
||||
<DropdownMenuItem variant="destructive" @click="logout">
|
||||
<LogOut />
|
||||
Keluar
|
||||
|
||||
17
resources/js/composables/useAppearance.ts
Normal file
17
resources/js/composables/useAppearance.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { AppearanceMode } from '@/types/settings';
|
||||
|
||||
export function applyAppearance(mode: AppearanceMode): void {
|
||||
const root = document.documentElement;
|
||||
|
||||
root.classList.remove('dark');
|
||||
|
||||
if (mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
root.classList.add('dark');
|
||||
}
|
||||
}
|
||||
|
||||
export function useAppearance() {
|
||||
return {
|
||||
applyAppearance,
|
||||
};
|
||||
}
|
||||
59
resources/js/layouts/SettingsLayout.vue
Normal file
59
resources/js/layouts/SettingsLayout.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { KeyRound, Palette, User } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const page = usePage();
|
||||
|
||||
const currentPath = computed(() => page.url.split('?')[0]);
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
href: '/admin/setting/profile',
|
||||
label: 'Profil',
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
href: '/admin/setting/password',
|
||||
label: 'Kata Sandi',
|
||||
icon: KeyRound,
|
||||
},
|
||||
{
|
||||
href: '/admin/setting/appearance',
|
||||
label: 'Tampilan',
|
||||
icon: Palette,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="Pengaturan">
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Pengaturan
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6 lg:flex-row">
|
||||
<nav class="flex shrink-0 flex-row gap-1 overflow-x-auto lg:w-56 lg:flex-col lg:overflow-visible">
|
||||
<Link v-for="item in navItems" :key="item.href" :href="item.href" :class="cn(
|
||||
'inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium whitespace-nowrap transition-colors',
|
||||
currentPath === item.href
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground',
|
||||
)">
|
||||
<component :is="item.icon" class="size-4 shrink-0" />
|
||||
{{ item.label }}
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
121
resources/js/pages/admin/setting/Appearance.vue
Normal file
121
resources/js/pages/admin/setting/Appearance.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { Monitor, Moon, Save, Sun } from '@lucide/vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { applyAppearance } from '@/composables/useAppearance';
|
||||
import SettingsLayout from '@/layouts/SettingsLayout.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AppearanceFormData, AppearanceMode } from '@/types/settings';
|
||||
|
||||
const props = defineProps<{
|
||||
appearance: AppearanceMode;
|
||||
}>();
|
||||
|
||||
const form = useForm<AppearanceFormData>({
|
||||
appearance: props.appearance,
|
||||
});
|
||||
|
||||
const options: Array<{
|
||||
value: AppearanceMode;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: typeof Sun;
|
||||
}> = [
|
||||
{
|
||||
value: 'light',
|
||||
label: 'Terang',
|
||||
description: 'Tampilan terang untuk lingkungan yang cukup pencahayaan.',
|
||||
icon: Sun,
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: 'Gelap',
|
||||
description: 'Tampilan gelap yang nyaman di malam hari.',
|
||||
icon: Moon,
|
||||
},
|
||||
{
|
||||
value: 'system',
|
||||
label: 'Sistem',
|
||||
description: 'Mengikuti pengaturan tampilan perangkat Anda.',
|
||||
icon: Monitor,
|
||||
},
|
||||
];
|
||||
|
||||
function onAppearanceChange(value: string) {
|
||||
form.appearance = value as AppearanceMode;
|
||||
applyAppearance(form.appearance);
|
||||
}
|
||||
|
||||
function submit() {
|
||||
form.put('/admin/setting/appearance', {
|
||||
onError: () => {
|
||||
toast.error('Gagal menyimpan tampilan.');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Tampilan" />
|
||||
|
||||
<SettingsLayout>
|
||||
<form @submit.prevent="submit">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel>Mode Tampilan</FieldLabel>
|
||||
<RadioGroup :model-value="form.appearance" class="grid gap-3"
|
||||
@update:model-value="onAppearanceChange">
|
||||
<Label v-for="option in options" :key="option.value"
|
||||
:for="`appearance-${option.value}`" :class="cn(
|
||||
'flex cursor-pointer items-start gap-3 rounded-lg border p-4 transition-colors',
|
||||
form.appearance === option.value
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'hover:bg-accent/50',
|
||||
)">
|
||||
<RadioGroupItem :id="`appearance-${option.value}`" :value="option.value"
|
||||
class="mt-0.5" />
|
||||
<div class="flex flex-1 items-start gap-3">
|
||||
<component :is="option.icon"
|
||||
class="text-muted-foreground mt-0.5 size-5 shrink-0" />
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm leading-none font-medium">
|
||||
{{ option.label }}
|
||||
</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{{ option.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
</RadioGroup>
|
||||
<FieldError :errors="form.errors.appearance ? [form.errors.appearance] : []" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<Button type="submit" :disabled="form.processing">
|
||||
<Save class="size-4" />
|
||||
Simpan Tampilan
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
89
resources/js/pages/admin/setting/Password.vue
Normal file
89
resources/js/pages/admin/setting/Password.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import SettingsLayout from '@/layouts/SettingsLayout.vue';
|
||||
import type { PasswordFormData } from '@/types/settings';
|
||||
|
||||
const form = useForm<PasswordFormData>({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.put('/admin/setting/password', {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal memperbarui kata sandi. Periksa kembali formulir.');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Kata Sandi" />
|
||||
|
||||
<SettingsLayout>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="grid gap-6">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldSet>
|
||||
<FieldGroup class="grid gap-4 sm:grid-cols-3">
|
||||
<Field>
|
||||
<FieldLabel for="current_password" required>
|
||||
Kata Sandi Saat Ini
|
||||
</FieldLabel>
|
||||
<Input id="current_password" v-model="form.current_password" type="password"
|
||||
placeholder="********" autocomplete="current-password" />
|
||||
<FieldError
|
||||
:errors="form.errors.current_password ? [form.errors.current_password] : []" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="password" required>
|
||||
Kata Sandi Baru
|
||||
</FieldLabel>
|
||||
<Input id="password" v-model="form.password" type="password" placeholder="********"
|
||||
autocomplete="new-password" />
|
||||
<FieldError :errors="form.errors.password ? [form.errors.password] : []" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="password_confirmation" required>
|
||||
Konfirmasi Kata Sandi Baru
|
||||
</FieldLabel>
|
||||
<Input id="password_confirmation" v-model="form.password_confirmation"
|
||||
type="password" placeholder="********" autocomplete="new-password" />
|
||||
<FieldError
|
||||
:errors="form.errors.password_confirmation ? [form.errors.password_confirmation] : []" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button type="submit" :disabled="form.processing">
|
||||
<Save class="size-4" />
|
||||
{{ form.processing ? 'Menyimpan...' : 'Simpan' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
137
resources/js/pages/admin/setting/Profile.vue
Normal file
137
resources/js/pages/admin/setting/Profile.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { PhoneNumberInput } from '@/components/ui/phone-number-input';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import SettingsLayout from '@/layouts/SettingsLayout.vue';
|
||||
import type { EnumOption, ProfileFormData, ProfileUser } from '@/types/settings';
|
||||
|
||||
const props = defineProps<{
|
||||
user: ProfileUser;
|
||||
genders: EnumOption[];
|
||||
}>();
|
||||
|
||||
const form = useForm<ProfileFormData>({
|
||||
email: props.user.email ?? '',
|
||||
username: props.user.username ?? '',
|
||||
full_name: props.user.profile?.full_name ?? '',
|
||||
phone_number: props.user.profile?.phone_number ?? '',
|
||||
gender: props.user.profile?.gender ?? '',
|
||||
birth_date: props.user.profile?.birth_date_input ?? '',
|
||||
address: props.user.profile?.address ?? '',
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.put('/admin/setting/profile', {
|
||||
onError: () => {
|
||||
toast.error('Gagal menyimpan profil. Periksa kembali formulir.');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Profil" />
|
||||
|
||||
<SettingsLayout>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="grid gap-6">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldSet>
|
||||
<FieldGroup class="grid gap-4 sm:grid-cols-3">
|
||||
<Field>
|
||||
<FieldLabel for="email" required>
|
||||
Email
|
||||
</FieldLabel>
|
||||
<Input id="email" v-model="form.email" type="email"
|
||||
placeholder="nama@perusahaan.com" />
|
||||
<FieldError :errors="form.errors.email ? [form.errors.email] : []" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="username" required>
|
||||
Username
|
||||
</FieldLabel>
|
||||
<Input id="username" v-model="form.username" type="text" placeholder="username" />
|
||||
<FieldError :errors="form.errors.username ? [form.errors.username] : []" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="full_name" required>
|
||||
Nama Lengkap
|
||||
</FieldLabel>
|
||||
<Input id="full_name" v-model="form.full_name" type="text"
|
||||
placeholder="Nama lengkap" />
|
||||
<FieldError :errors="form.errors.full_name ? [form.errors.full_name] : []" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="phone_number">
|
||||
Nomor Telepon
|
||||
</FieldLabel>
|
||||
<PhoneNumberInput id="phone_number" v-model="form.phone_number" />
|
||||
<FieldError :errors="form.errors.phone_number ? [form.errors.phone_number] : []" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="birth_date">
|
||||
Tanggal Lahir
|
||||
</FieldLabel>
|
||||
<DatePicker id="birth_date" v-model="form.birth_date" />
|
||||
<FieldError :errors="form.errors.birth_date ? [form.errors.birth_date] : []" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel>
|
||||
Jenis Kelamin
|
||||
</FieldLabel>
|
||||
<RadioGroup v-model="form.gender" class="flex flex-wrap gap-4 pt-1">
|
||||
<div v-for="gender in genders" :key="gender.value"
|
||||
class="flex items-center gap-2">
|
||||
<RadioGroupItem :id="`gender-${gender.value}`" :value="gender.value" />
|
||||
<label :for="`gender-${gender.value}`" class="text-sm">
|
||||
{{ gender.label }}
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<FieldError :errors="form.errors.gender ? [form.errors.gender] : []" />
|
||||
</Field>
|
||||
|
||||
<Field class="sm:col-span-3">
|
||||
<FieldLabel for="address">
|
||||
Alamat
|
||||
</FieldLabel>
|
||||
<Textarea id="address" v-model="form.address" placeholder="Alamat lengkap"
|
||||
rows="3" />
|
||||
<FieldError :errors="form.errors.address ? [form.errors.address] : []" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button type="submit" :disabled="form.processing">
|
||||
<Save class="size-4" />
|
||||
{{ form.processing ? 'Menyimpan...' : 'Simpan' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
1
resources/js/types/global.d.ts
vendored
1
resources/js/types/global.d.ts
vendored
@ -23,6 +23,7 @@ declare module '@inertiajs/core' {
|
||||
error: string | null;
|
||||
};
|
||||
sidebarOpen: boolean;
|
||||
appearance: 'light' | 'dark' | 'system';
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
41
resources/js/types/settings.ts
Normal file
41
resources/js/types/settings.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { EnumOption } from '@/types/employee';
|
||||
|
||||
export type AppearanceMode = 'light' | 'dark' | 'system';
|
||||
|
||||
export type UserProfile = {
|
||||
full_name: string | null;
|
||||
phone_number: string | null;
|
||||
gender: string | null;
|
||||
gender_label: string | null;
|
||||
birth_date_input: string | null;
|
||||
address: string | null;
|
||||
};
|
||||
|
||||
export type ProfileUser = {
|
||||
id: number;
|
||||
email: string;
|
||||
username: string;
|
||||
profile: UserProfile | null;
|
||||
};
|
||||
|
||||
export type ProfileFormData = {
|
||||
email: string;
|
||||
username: string;
|
||||
full_name: string;
|
||||
phone_number: string;
|
||||
gender: string;
|
||||
birth_date: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export type PasswordFormData = {
|
||||
current_password: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
};
|
||||
|
||||
export type AppearanceFormData = {
|
||||
appearance: AppearanceMode;
|
||||
};
|
||||
|
||||
export type { EnumOption };
|
||||
@ -1,6 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])>
|
||||
|
||||
<head>
|
||||
<script>
|
||||
(function() {
|
||||
const appearance = @json($appearance ?? 'system');
|
||||
|
||||
if (appearance === 'dark' || (appearance === 'system' && window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{{ config('app.name', 'Laravel') }} - Progressive Web App">
|
||||
@ -18,7 +31,9 @@
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
</x-inertia::head>
|
||||
</head>
|
||||
|
||||
<body class="font-sans antialiased">
|
||||
<x-inertia::app />
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
use App\Http\Controllers\Admin\Master\ProductController;
|
||||
use App\Http\Controllers\Admin\Master\RawMaterialController;
|
||||
use App\Http\Controllers\Admin\Master\SupplierController;
|
||||
use App\Http\Controllers\Admin\Setting\AppearanceController;
|
||||
use App\Http\Controllers\Admin\Setting\PasswordController;
|
||||
use App\Http\Controllers\Admin\Setting\ProfileController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\Auth\LogoutController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@ -143,6 +146,17 @@
|
||||
|
||||
});
|
||||
|
||||
Route::prefix('setting')->name('setting.')->group(function () {
|
||||
Route::get('profile', [ProfileController::class, 'edit'])->name('profile');
|
||||
Route::put('profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
|
||||
Route::get('password', [PasswordController::class, 'edit'])->name('password');
|
||||
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
|
||||
|
||||
Route::get('appearance', [AppearanceController::class, 'edit'])->name('appearance');
|
||||
Route::put('appearance', [AppearanceController::class, 'update'])->name('appearance.update');
|
||||
});
|
||||
|
||||
Route::prefix('hr')->name('hr.')->middleware('permission:'.Permission::EMPLOYEES_VIEW->value)->group(function () {
|
||||
Route::prefix('employees')->name('employees.')
|
||||
->middleware('permission:'.Permission::EMPLOYEES_VIEW->value)
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
*
|
||||
* @see https://vite-pwa-org.netlify.app/frameworks/laravel.html
|
||||
*/
|
||||
|
||||
$publicPath = getcwd();
|
||||
|
||||
$uri = urldecode(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user