feat(account-settings): consolidate account, profile, and password management into a single component and update routing

This commit is contained in:
Yoga Pangestu 2025-11-22 22:34:41 +07:00
parent d0c41e9d89
commit 86325dabde
9 changed files with 251 additions and 296 deletions

View File

@ -3,35 +3,82 @@
namespace App\Livewire\Studio\Setting;
use App\Livewire\Forms\Studio\Setting\AccountForm;
use App\Livewire\Forms\Studio\Setting\PasswordForm;
use App\Livewire\Forms\Studio\Setting\ProfileForm;
use App\Models\User;
use App\Traits\Notification\WithSubscribeNotification;
use App\Traits\WithAuthorization;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Akun')]
#[Title('Pengaturan')]
class Account extends Component
{
use WithAuthorization, WithSubscribeNotification, WithToast, WithUpdatedData;
public AccountForm $form;
public AccountForm $accountForm;
public function mount(User $user)
public ProfileForm $profileForm;
public PasswordForm $passwordForm;
public function mount()
{
$this->form->setAccount($user);
$user = auth()->user();
$user->load('employee');
$this->accountForm->setAccount($user);
$this->profileForm->setProfile($user->employee);
$this->passwordForm->setUser($user);
}
public function update()
public function updateAccount()
{
$this->canOrAbort('update account');
$this->form->update();
$this->accountForm->update();
$this->toast('Akun berhasil diperbarui.');
}
public function updateProfile()
{
$this->canOrAbort('update profile');
$this->profileForm->update();
$this->toast('Profil berhasil diperbarui.');
}
public function updatePassword()
{
$this->canOrAbort('update password');
if (! Hash::check($this->passwordForm->current_password, $this->passwordForm->user->password)) {
$this->toast('Kata sandi yang Anda masukkan tidak sesuai dengan kata sandi saat ini.', 'Gagal', 'danger');
return;
}
$this->passwordForm->update();
auth()->logoutOtherDevices($this->passwordForm->current_password);
auth()->logout();
session()->invalidate();
session()->regenerateToken();
$this->toast('Kata sandi berhasil diperbarui, Silakan masuk kembali.');
$this->redirectRoute('login', navigate: true);
}
public function render()
{
return view('livewire.studio.setting.account', [

View File

@ -1,56 +0,0 @@
<?php
namespace App\Livewire\Studio\Setting;
use App\Livewire\Forms\Studio\Setting\PasswordForm;
use App\Models\User;
use App\Traits\Notification\WithSubscribeNotification;
use App\Traits\WithAuthorization;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Kata Sandi')]
class Password extends Component
{
use WithAuthorization, WithSubscribeNotification, WithToast, WithUpdatedData;
public PasswordForm $form;
public function mount(User $user)
{
$this->form->setUser($user);
}
public function update()
{
$this->canOrAbort('update password');
if (! Hash::check($this->form->current_password, $this->form->user->password)) {
$this->toast('Kata sandi yang Anda masukkan tidak sesuai dengan kata sandi saat ini.', 'Gagal', 'danger');
return;
}
auth()->logoutOtherDevices($this->form->current_password);
$this->form->update();
auth()->logout();
session()->invalidate();
session()->regenerateToken();
$this->toast('Kata sandi berhasil diperbarui.');
$this->redirectRoute('login', navigate: true);
}
public function render()
{
return view('livewire.studio.setting.password', [
'pageTitle' => 'Kata Sandi',
]);
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Livewire\Studio\Setting;
use App\Livewire\Forms\Studio\Setting\ProfileForm;
use App\Models\Employee;
use App\Traits\Notification\WithSubscribeNotification;
use App\Traits\WithAuthorization;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Profil')]
class Profile extends Component
{
use WithAuthorization, WithSubscribeNotification, WithToast, WithUpdatedData;
public ProfileForm $form;
public function mount(Employee $employee)
{
$this->form->setProfile($employee);
}
public function update()
{
$this->canOrAbort('update profile');
$this->form->update();
$this->toast('Profil berhasil diperbarui.');
}
public function render()
{
return view('livewire.studio.setting.profile', [
'pageTitle' => 'Profil',
]);
}
}

View File

@ -23,8 +23,7 @@ class="max-lg:hidden! hidden dark:flex" />
<flux:dropdown position="top" align="end">
<flux:profile circle name="{{ auth()->user()?->employee?->full_name }}" />
<flux:navmenu>
<flux:navmenu.item href="{{ route('studio.setting.account', auth()->id()) }}" icon="cog"
wire:navigate.hover>
<flux:navmenu.item href="{{ route('studio.setting.account') }}" icon="cog" wire:navigate.hover>
Pengaturan
</flux:navmenu.item>
<flux:menu.separator />

View File

@ -1,24 +0,0 @@
<div class="overflow-x-auto pb-2">
<flux:navbar>
<flux:navbar.item href="{{ route('studio.setting.account', auth()->id()) }}" icon="key"
:current="request()->routeIs('studio.setting.account', auth()->id())" wire:navigate.hover wire:ignore>
Akun
</flux:navbar.item>
<flux:navbar.item href="{{ route('studio.setting.profile', auth()->user()?->employee?->id) }}" icon="user"
:current="request()->routeIs('studio.setting.profile', auth()->user()?->employee?->id)" wire:navigate.hover
wire:ignore>
Profil
</flux:navbar.item>
<flux:navbar.item href="{{ route('studio.setting.password', auth()->id()) }}" icon="lock-closed"
:current="request()->routeIs('studio.setting.password', auth()->id())" wire:navigate.hover wire:ignore>
Kata
Sandi
</flux:navbar.item>
</flux:navbar>
</div>
<flux:text class="text-xs italic block md:hidden" color="yellow">
* Geser ke kanan untuk melihat selengkapnya.
</flux:text>

View File

@ -1,40 +1,208 @@
<flux:main>
@include('components.setting-tabs')
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
</div>
<div class="flex flex-col gap-4 mt-4">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 items-start">
<div class="col-span-1 lg:col-span-2">
<flux:card class="space-y-6 p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<flux:field>
<flux:label>Nama Pengguna <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="johndoe" wire:model.live.debounce.500ms="form.username"
autocomplete="off" />
<flux:error name="form.username" />
</flux:field>
<div class="mt-6">
<flux:separator variant="subtle" class="my-8" />
<flux:field>
<flux:label>Email <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="info.pangestuyoga@gmail.com"
wire:model.live.debounce.500ms="form.email" autocomplete="off" />
<flux:error name="form.email" />
</flux:field>
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
<div class="w-80">
<flux:heading size="lg">Akun</flux:heading>
<flux:subheading>Pengaturan untuk kredensial masuk Anda.</flux:subheading>
</div>
<flux:input label="Status Akun" wire:model="form.status" readonly />
<div class="flex-1 space-y-6">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 items-start">
<div class="col-span-1 lg:col-span-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<flux:field>
<flux:label>Nama Pengguna <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="johndoe" wire:model.live.debounce.500ms="accountForm.username"
autocomplete="off" />
<flux:error name="accountForm.username" />
</flux:field>
<flux:input label="Email Terverifikasi Pada" wire:model="form.email_verified_at" readonly />
<flux:field>
<flux:label>Email <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="info.pangestuyoga@gmail.com"
wire:model.live.debounce.500ms="accountForm.email" autocomplete="off" />
<flux:error name="accountForm.email" />
</flux:field>
<flux:input label="Status Akun" wire:model="accountForm.status" readonly />
<flux:input label="Email Terverifikasi Pada" wire:model="accountForm.email_verified_at"
readonly />
</div>
</div>
</flux:card>
</div>
@can('update account')
<div class="flex justify-end">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="updateAccount">
Simpan
</flux:button>
</div>
@endcan
</div>
</div>
</div>
@can('update account')
<div class="flex justify-start mt-4">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="update">
Simpan
</flux:button>
<flux:separator variant="subtle" class="my-8" />
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
<div class="w-80">
<flux:heading size="lg">Profil</flux:heading>
<flux:subheading>Informasi mengenai data diri Anda.</flux:subheading>
</div>
@endcan
<div class="flex-1 space-y-6">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 items-start">
<div class="col-span-1 lg:col-span-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<flux:field>
<flux:label>Nama Lengkap <span class="text-red-500 ms-1">*</span>
</flux:label>
<flux:input placeholder="John Doe"
wire:model.live.debounce.500ms="profileForm.full_name" autofocus
autocomplete="off" />
<flux:error name="profileForm.full_name" />
</flux:field>
</div>
<flux:field>
<flux:label>Nomor Telepon <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="0821 xxxx xxxx" mask="9999 9999 99999"
wire:model.live.debounce.500ms="profileForm.phone_number" autocomplete="off" />
<flux:error name="profileForm.phone_number" />
</flux:field>
<flux:field>
<flux:label>Gaji Pokok</flux:label>
<flux:input.group>
<flux:input.group.prefix>Rp</flux:input.group.prefix>
<flux:input wire:model="profileForm.base_salary" readonly placeholder="1.500.000" />
</flux:input.group>
<flux:error name="profileForm.base_salary" />
</flux:field>
<flux:field>
<flux:label>Tanggal Lahir <span class="text-red-500 ms-1">*</span></flux:label>
<flux:date-picker with-today wire:model.live.debounce.500ms="profileForm.birthdate"
placeholder="Pilih Tanggal" autocomplete="off" locale="id-ID" selectable-header />
<flux:error name="profileForm.birthdate" />
</flux:field>
<flux:input label="Tanggal Bergabung" wire:model="profileForm.hire_date" readonly />
<div class="md:col-span-2">
<flux:textarea label="Alamat"
placeholder="Jl. Taman Pahlawan No.41, Nagri Kaler, Kec. Purwakarta, Kabupaten Purwakarta, Jawa Barat 41119"
wire:model.live.debounce.500ms="profileForm.address" autocomplete="off"
rows="2" />
</div>
<flux:field>
<flux:label>Jenis Kelamin <span class="text-red-500 ms-1">*</span></flux:label>
<flux:radio.group wire:model.live="profileForm.gender" variant="buttons"
class="w-full *:flex-1">
@foreach (\App\Enums\Gender::cases() as $gender)
<flux:radio value="{{ $gender->value }}">
{{ $gender->label() }}
</flux:radio>
@endforeach
</flux:radio.group>
<flux:error name="profileForm.gender" />
</flux:field>
<flux:input label="Status Kerja" wire:model="profileForm.status" readonly />
</div>
</div>
</div>
@can('update profile')
<div class="flex justify-end">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="updateProfile">
Simpan
</flux:button>
</div>
@endcan
</div>
</div>
<flux:separator variant="subtle" class="my-8" />
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
<div class="w-80">
<flux:heading size="lg">Kata Sandi</flux:heading>
<flux:subheading>Atur ulang kata sandi Anda.</flux:subheading>
</div>
<div class="flex-1 space-y-6">
<flux:field>
<flux:label>Kata Sandi Saat Ini <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password"
wire:model.live.debounce.500ms="passwordForm.current_password" viewable />
<flux:error name="passwordForm.current_password" />
</flux:field>
<flux:field>
<flux:label>Kata Sandi Baru <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password"
wire:model.live.debounce.500ms="passwordForm.new_password" viewable />
<flux:error name="passwordForm.new_password" />
</flux:field>
<flux:field>
<flux:label>Konfirmasi Kata Sandi <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password"
wire:model.live.debounce.500ms="passwordForm.new_confirm_password" viewable />
<flux:error name="passwordForm.new_confirm_password" />
</flux:field>
@can('update password')
<div class="flex justify-end">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="updatePassword">
Simpan
</flux:button>
</div>
@endcan
</div>
</div>
<flux:separator variant="subtle" class="my-8" />
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6 pb-10">
<div class="w-80">
<flux:heading size="lg">Notifikasi</flux:heading>
<flux:subheading>Izinkan notifikasi untuk menerima pemberitahuan.</flux:subheading>
</div>
<div class="flex-1 space-y-6">
<flux:fieldset class="space-y-4">
<flux:switch checked label="Communication emails"
description="Receive emails about your account activity." />
<flux:separator variant="subtle" />
<flux:switch checked label="Marketing emails"
description="Receive emails about new products, features, and more." />
<flux:separator variant="subtle" />
<flux:switch label="Social emails"
description="Receive emails for friend requests, follows, and more." />
<flux:separator variant="subtle" />
<flux:switch label="Security emails"
description="Receive emails about your account activity and security." />
</flux:fieldset>
</div>
</div>
</flux:main>

View File

@ -1,42 +0,0 @@
<flux:main>
@include('components.setting-tabs')
<div class="flex flex-col gap-4 mt-4">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 items-start">
<div class="col-span-1 lg:col-span-2">
<flux:card class="space-y-6 p-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<flux:field>
<flux:label>Kata Sandi Saat Ini <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password"
wire:model.live.debounce.500ms="form.current_password" viewable />
<flux:error name="form.current_password" />
</flux:field>
<flux:field>
<flux:label>Kata Sandi Baru <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password"
wire:model.live.debounce.500ms="form.new_password" viewable />
<flux:error name="form.new_password" />
</flux:field>
<flux:field>
<flux:label>Konfirmasi Kata Sandi <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password"
wire:model.live.debounce.500ms="form.new_confirm_password" viewable />
<flux:error name="form.new_confirm_password" />
</flux:field>
</div>
</flux:card>
</div>
</div>
</div>
@can('update password')
<div class="flex justify-start mt-4">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="update">
Simpan
</flux:button>
</div>
@endcan
</flux:main>

View File

@ -1,89 +0,0 @@
<flux:main>
@include('components.setting-tabs')
<div class="flex flex-col gap-4 mt-4">
<div class="flex flex-col lg:flex-row gap-4">
<div class="w-full lg:w-3/4 space-y-4">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 items-start">
<div class="col-span-1 lg:col-span-2">
<flux:card class="space-y-6 p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<flux:field>
<flux:label>Nama Lengkap <span class="text-red-500 ms-1">*</span>
</flux:label>
<flux:input placeholder="John Doe"
wire:model.live.debounce.500ms="form.full_name" autofocus
autocomplete="off" />
<flux:error name="form.full_name" />
</flux:field>
</div>
<flux:field>
<flux:label>Nomor Telepon <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="0821 xxxx xxxx" mask="9999 9999 99999"
wire:model.live.debounce.500ms="form.phone_number" autocomplete="off" />
<flux:error name="form.phone_number" />
</flux:field>
<flux:field>
<flux:label>Gaji Pokok</flux:label>
<flux:input.group>
<flux:input.group.prefix>Rp</flux:input.group.prefix>
<flux:input wire:model="form.base_salary" readonly placeholder="1.500.000" />
</flux:input.group>
<flux:error name="form.base_salary" />
</flux:field>
<flux:field>
<flux:label>Tanggal Lahir <span class="text-red-500 ms-1">*</span></flux:label>
<flux:date-picker with-today wire:model.live.debounce.500ms="form.birthdate"
placeholder="Pilih Tanggal" autocomplete="off" locale="id-ID"
selectable-header />
<flux:error name="form.birthdate" />
</flux:field>
<flux:input label="Tanggal Bergabung" wire:model="form.hire_date" readonly />
<div class="md:col-span-2">
<flux:textarea label="Alamat"
placeholder="Jl. Taman Pahlawan No.41, Nagri Kaler, Kec. Purwakarta, Kabupaten Purwakarta, Jawa Barat 41119"
wire:model.live.debounce.500ms="form.address" autocomplete="off"
rows="2" />
</div>
</div>
</flux:card>
</div>
</div>
</div>
<div class="w-full lg:w-1/3 space-y-4">
<flux:card class="space-y-6 p-6">
<flux:field>
<flux:label>Jenis Kelamin <span class="text-red-500 ms-1">*</span></flux:label>
<flux:radio.group wire:model.live="form.gender" variant="buttons" class="w-full *:flex-1">
@foreach (\App\Enums\Gender::cases() as $gender)
<flux:radio value="{{ $gender->value }}">
{{ $gender->label() }}
</flux:radio>
@endforeach
</flux:radio.group>
<flux:error name="form.gender" />
</flux:field>
</flux:card>
<flux:card class="space-y-6 p-6">
<flux:input label="Status Kerja" wire:model="form.status" readonly />
</flux:card>
</div>
</div>
</div>
@can('update profile')
<div class="flex justify-start mt-4">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="update">
Simpan
</flux:button>
</div>
@endcan
</flux:main>

View File

@ -45,16 +45,12 @@
use App\Livewire\Studio\Master\User\Index as UserIndex;
use App\Livewire\Studio\Setting\Account as AccountComponent;
use App\Livewire\Studio\Setting\Application as ApplicationComponent;
use App\Livewire\Studio\Setting\Password as PasswordComponent;
use App\Livewire\Studio\Setting\Profile as ProfileComponent;
use App\Livewire\Studio\Setting\Whatsapp as WhatsappComponent;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->prefix('studio')->group(function () {
Route::prefix('setting')->name('studio.setting.')->group(function () {
Route::get('/account/{user}', AccountComponent::class)->name('account')->middleware('can:view account');
Route::get('/profile/{employee}', ProfileComponent::class)->name('profile')->middleware('can:view profile');
Route::get('/password/{user}', PasswordComponent::class)->name('password')->middleware('can:view password');
Route::get('/account', AccountComponent::class)->name('account')->middleware('can:view account');
Route::get('/applications', ApplicationComponent::class)->name('application')->middleware('can:view application settings');
});
Route::prefix('dashboard')->name('studio.dashboard.')->group(function () {
@ -200,7 +196,4 @@
Route::delete('/{choose_us}/delete', ChooseUsComponent::class)->name('delete')->middleware('can:delete choose us');
});
});
Route::get('/settings/applications', ApplicationComponent::class)->name('studio.setting.application')->middleware('can:view application settings');
// Route::get('/settings/whatsapp', WhatsappComponent::class)->name('studio.setting.whatsapp')->middleware('can:view whatsapp settings');
});