feat(setting): membuat pengaturan untuk mengatur akun, profil dan mengubah kata sandi

This commit is contained in:
Yoga Pangestu 2025-10-02 18:41:50 +07:00
parent 92a54dbdbf
commit 739f7e761e
13 changed files with 548 additions and 7 deletions

View File

@ -0,0 +1,58 @@
<?php
namespace App\Livewire\Forms;
use App\Models\User;
use App\Traits\WithMediaHandler;
use Illuminate\Validation\Rule;
use Livewire\Form;
class AccountForm extends Form
{
use WithMediaHandler;
public ?User $user = null;
public string $username = '';
public string $email = '';
public string $status = '';
public ?string $email_verified_at = null;
public function rules(): array
{
return [
'username' => ['required', 'alpha_num', 'min:5', 'max:20', Rule::unique('users', 'username')->ignore($this->user)],
'email' => ['required', 'email', 'max:100', Rule::unique('users', 'email')->ignore($this->user)],
];
}
public function validationAttributes(): array
{
return [
'username' => 'nama pengguna',
];
}
public function setAccount(User $user)
{
$this->user = $user;
$this->username = $user->username;
$this->email = $user->email;
$this->status = $user->status->label();
$this->email_verified_at = $user->email_verified_at ? formatDateTime($user->email_verified_at) : '-';
}
public function update()
{
$this->validate();
$this->user->update([
'username' => $this->username,
'email' => $this->email,
]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Livewire\Forms;
use App\Models\User;
use App\Traits\WithMediaHandler;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Livewire\Form;
class PasswordForm extends Form
{
use WithMediaHandler;
public ?User $user = null;
public string $current_password = '';
public string $new_password = '';
public string $new_confirm_password = '';
public function rules(): array
{
return [
'current_password' => ['required', 'string'],
'new_password' => ['required', 'string', Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised()],
'new_confirm_password' => ['required', 'string', 'same:new_password'],
];
}
public function validationAttributes(): array
{
return [
'current_password' => 'kata sandi saat ini',
'new_password' => 'kata sandi baru',
'new_confirm_password' => 'konfirmasi kata sandi baru',
];
}
public function setUser(User $user)
{
$this->user = $user;
}
public function update()
{
$this->validate();
$this->user->update([
'password' => Hash::make($this->new_password),
]);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Livewire\Forms;
use App\Enums\Gender;
use App\Models\Employee;
use App\Rules\PhoneNumber;
use Carbon\Carbon;
use Illuminate\Validation\Rule;
use Livewire\Form;
class ProfileForm extends Form
{
public ?Employee $employee = null;
public string $full_name = '';
public string $phone_number = '';
public string $base_salary = '';
public string $birthdate = '';
public string $hire_date = '';
public string $address = '';
public string $gender = '';
public string $status = '';
public function rules(): array
{
return [
'full_name' => ['required', 'string', 'max:100'],
'phone_number' => ['required', 'string', new PhoneNumber],
'birthdate' => ['required', 'date', 'before:'.Carbon::now()->subYears(18)->format('Y-m-d')],
'address' => ['required', 'string'],
'gender' => ['required', Rule::in(Gender::cases())],
];
}
public function validationAttributes(): array
{
return [
'full_name' => 'nama lengkap',
'phone_number' => 'nomor telepon',
'base_salary' => 'gaji pokok',
'birthdate' => 'tanggal lahir',
'address' => 'alamat',
'gender' => 'jenis kelamin',
];
}
public function setProfile(Employee $employee)
{
$this->employee = $employee;
$this->full_name = $employee->full_name;
$this->phone_number = $employee->phone_number;
$this->base_salary = $employee->base_salary;
$this->birthdate = $employee->birthdate;
$this->hire_date = formatDate($employee->hire_date);
$this->address = $employee->address;
$this->gender = $employee->gender->value;
$this->status = $employee->status->label();
}
public function update()
{
$this->validate();
$this->employee->update([
'full_name' => $this->full_name,
'phone_number' => $this->phone_number,
'birthdate' => $this->birthdate,
'address' => $this->address,
'gender' => $this->gender,
]);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Livewire\Studio\Setting;
use App\Livewire\Forms\AccountForm;
use App\Models\User;
use App\Traits\WithUpdatedData;
use Flux\Flux;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Akun')]
class Account extends Component
{
use WithUpdatedData;
public AccountForm $form;
public function mount(User $user)
{
$this->form->setAccount($user);
}
public function update()
{
$this->form->update();
Flux::toast(
heading: 'Berhasil',
text: 'Akun berhasil diperbarui.',
variant: 'success',
duration: 3000
);
}
public function render()
{
return view('livewire.studio.setting.account', [
'pageTitle' => 'Akun',
]);
}
}

View File

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

View File

@ -0,0 +1,42 @@
<?php
namespace App\Livewire\Studio\Setting;
use App\Livewire\Forms\ProfileForm;
use App\Models\Employee;
use App\Traits\WithUpdatedData;
use Flux\Flux;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Profil')]
class Profile extends Component
{
use WithUpdatedData;
public ProfileForm $form;
public function mount(Employee $employee)
{
$this->form->setProfile($employee);
}
public function update()
{
$this->form->update();
Flux::toast(
heading: 'Berhasil',
text: 'Profil berhasil diperbarui.',
variant: 'success',
duration: 3000
);
}
public function render()
{
return view('livewire.studio.setting.profile', [
'pageTitle' => 'Profil',
]);
}
}

View File

@ -19,9 +19,6 @@ protected function casts(): array
{
return [
'gender' => Gender::class,
'birthdate' => 'date',
'hire_date' => 'date',
'resign_date' => 'date',
'status' => EmploymentStatus::class,
];
}

View File

@ -21,10 +21,13 @@ class="max-lg:hidden! hidden dark:flex" />
</flux:navbar.item>
</flux:navbar>
<flux:dropdown position="top" align="end">
<flux:avatar circle size="sm" src="https://unavatar.io/x/calebporzio" as="button" />
<flux:profile circle name="{{ auth()->user()?->employee?->full_name }}"
avatar="https://unavatar.io/x/calebporzio" />
<flux:navmenu>
<flux:navmenu.item href="#" icon="cog">Pengaturan</flux:navmenu.item>
<flux:navmenu.item href="#" icon="paint-brush">Tema</flux:navmenu.item>
<flux:navmenu.item href="{{ route('studio.setting.account', auth()->id()) }}" icon="cog"
wire:navigate.hover>
Pengaturan
</flux:navmenu.item>
<flux:navmenu.item href="#" icon="chart-bar">Statistik</flux:navmenu.item>
<flux:menu.separator />
@livewire('auth.logout')

View File

@ -0,0 +1,24 @@
<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

@ -0,0 +1,38 @@
<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-2 gap-6">
<flux:field>
<flux:label>Nama Pengguna <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Masukkan nama pengguna"
wire:model.live.debounce.500ms="form.username" autocomplete="off" />
<flux:error name="form.username" />
</flux:field>
<flux:field>
<flux:label>Email <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Masukkan email" wire:model.live.debounce.500ms="form.email"
autocomplete="off" />
<flux:error name="form.email" />
</flux:field>
<flux:input label="Status Akun" wire:model="form.status" readonly />
<flux:input label="Email Terverifikasi Pada" wire:model="form.email_verified_at" readonly />
</div>
</flux:card>
</div>
</div>
</div>
<div class="flex justify-start mt-4">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="update">
Simpan
</flux:button>
</div>
</flux:main>

View File

@ -0,0 +1,40 @@
<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="Masukkan kata sandi saat ini" 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="Masukkan kata sandi baru" 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="Masukkan konfirmasi kata sandi" 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>
<div class="flex justify-start mt-4">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="update">
Simpan
</flux:button>
</div>
</flux:main>

View File

@ -0,0 +1,89 @@
<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="Masukkan nama lengkap"
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="Masukkan nomor telepon" 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 />
</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"
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:field>
<flux:label>Alamat <span class="text-red-500 ms-1">*</span></flux:label>
<flux:textarea placeholder="Masukkan alamat lengkap"
wire:model.live.debounce.500ms="form.address" autocomplete="off"
rows="2" />
<flux:error name="form.address" />
</flux:field>
</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>
<div class="flex justify-start mt-4">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="update">
Simpan
</flux:button>
</div>
</flux:main>

View File

@ -23,15 +23,26 @@
use App\Livewire\Studio\Master\User\Create as UserCreate;
use App\Livewire\Studio\Master\User\Edit as UserEdit;
use App\Livewire\Studio\Master\User\Index as UserIndex;
use App\Livewire\Studio\Setting\Account as AccountComponent;
use App\Livewire\Studio\Setting\Password as PasswordComponent;
use App\Livewire\Studio\Setting\Profile as ProfileComponent;
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');
Route::get('/profile/{employee}', ProfileComponent::class)->name('profile');
Route::get('/password/{user}', PasswordComponent::class)->name('password');
});
Route::prefix('dashboard')
->name('studio.dashboard.')
->group(function () {
Route::get('overview', OverviewComponent::class)->name('overview');
Route::get('/overview', OverviewComponent::class)->name('overview');
});
Route::prefix('master')