feat: implement member account settings page for profile and password updates

This commit is contained in:
Yoga Pangestu 2026-01-04 22:24:55 +07:00
parent 0795f8b0d7
commit d720934533
8 changed files with 370 additions and 5 deletions

View File

@ -0,0 +1,58 @@
<?php
namespace App\Livewire\Forms\Member\Setting;
use App\Enums\Gender;
use App\Models\Customer;
use App\Rules\PhoneNumber;
use Illuminate\Validation\Rule;
use Livewire\Form;
class ProfileForm extends Form
{
public ?Customer $customer = null;
public string $name = '';
public string $phone_number = '';
public string $gender = '';
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'phone_number' => ['required', 'string', new PhoneNumber],
'gender' => ['required', Rule::in(Gender::cases())],
];
}
public function validationAttributes(): array
{
return [
'name' => 'nama lengkap',
'phone_number' => 'nomor telepon',
'gender' => 'jenis kelamin',
];
}
public function setProfile(Customer $customer): void
{
$this->customer = $customer;
$this->name = $customer->name;
$this->phone_number = $customer->phone_number;
$this->gender = $customer->gender->value;
}
public function update(): void
{
$this->validate();
$this->customer->update([
'name' => $this->name,
'phone_number' => $this->phone_number,
'gender' => $this->gender,
]);
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Livewire\Member\Setting;
use App\Livewire\Forms\Member\Setting\ProfileForm;
use App\Livewire\Forms\Studio\Setting\AccountForm;
use App\Livewire\Forms\Studio\Setting\PasswordForm;
use App\Traits\Components\WithToast;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Pengaturan')]
#[Layout('components.layouts.member')]
class Account extends Component
{
use WithToast;
public AccountForm $accountForm;
public ProfileForm $profileForm;
public PasswordForm $passwordForm;
public bool $hasMemberData = false;
public function mount(): void
{
$user = auth()->user();
$this->accountForm->setAccount($user);
$customer = $user->customer;
$this->hasMemberData = (bool) $customer;
if ($this->hasMemberData) {
$this->profileForm->setProfile($customer);
}
$this->passwordForm->setUser($user);
}
public function updateAccount(): void
{
$this->accountForm->update();
$this->toast('Akun berhasil diperbarui.');
}
public function updateProfile(): void
{
if (! $this->hasMemberData) {
return;
}
$this->profileForm->update();
$this->toast('Profil member berhasil diperbarui.');
}
public function updatePassword(): void
{
if (! Hash::check($this->passwordForm->current_password, auth()->user()->password)) {
$this->toast('Kata sandi yang Anda masukkan tidak sesuai dengan kata sandi saat ini.', 'Gagal', 'danger');
return;
}
auth()->logoutOtherDevices($this->passwordForm->current_password);
$this->passwordForm->update();
auth()->logout();
session()->invalidate();
session()->regenerateToken();
$this->toast('Kata sandi berhasil diperbarui. Silakan masuk kembali.');
$this->redirectRoute('login', navigate: true);
}
public function render(): View
{
return view('livewire.member.setting.account', [
'pageTitle' => 'Pengaturan Akun',
]);
}
}

View File

@ -66,10 +66,10 @@ public function updatePassword(): void
return;
}
$this->passwordForm->update();
auth()->logoutOtherDevices($this->passwordForm->current_password);
$this->passwordForm->update();
auth()->logout();
session()->invalidate();
session()->regenerateToken();

View File

@ -1,4 +1,4 @@
@include('components.sections.ui.shared._header', [
'profileName' => auth()->user()?->customer?->name,
'settingsRoute' => route('studio.setting.account', auth()->id())
'profileName' => auth()->user()?->customer?->name ?? auth()->user()->username,
'settingsRoute' => route('member.setting'),
])

View File

@ -0,0 +1,144 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
</div>
<div class="mt-6">
<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">Akun</flux:heading>
<flux:subheading>Pengaturan untuk kredensial masuk Anda.</flux:subheading>
</div>
<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="accountForm.username"
autocomplete="off" />
<flux:error name="accountForm.username" />
</flux:field>
<flux:field>
<flux:label>Email <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="info.pangestuyoga@gmail.com" wire:model="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>
</div>
<div class="flex justify-end">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="updateAccount">
Simpan
</flux:button>
</div>
</div>
</div>
</div>
@if ($hasMemberData)
<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 Member</flux:heading>
<flux:subheading>Informasi mengenai data keanggotaan Anda.</flux:subheading>
</div>
<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="profileForm.name" autofocus
autocomplete="off" />
<flux:error name="profileForm.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="profileForm.phone_number" autocomplete="off" />
<flux:error name="profileForm.phone_number" />
</flux:field>
<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>
</div>
</div>
</div>
<div class="flex justify-end">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="updateProfile">
Simpan
</flux:button>
</div>
</div>
</div>
@endif
<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="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="passwordForm.new_password" viewable />
<flux:error name="passwordForm.new_password" />
</flux:field>
<flux:field>
<flux:label>Konfirmasi Kata Sandi Baru<span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password" wire:model="passwordForm.new_confirm_password"
viewable />
<flux:error name="passwordForm.new_confirm_password" />
</flux:field>
<div class="flex justify-end">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="updatePassword">
Simpan
</flux:button>
</div>
</div>
</div>
</flux:main>

View File

@ -156,7 +156,7 @@ class="w-full *:flex-1">
</flux:field>
<flux:field>
<flux:label>Konfirmasi Kata Sandi <span class="text-red-500 ms-1">*</span></flux:label>
<flux:label>Konfirmasi Kata Sandi Baru<span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password" wire:model="passwordForm.new_confirm_password"
viewable />
<flux:error name="passwordForm.new_confirm_password" />

View File

@ -3,6 +3,7 @@
use App\Livewire\Member\Membership;
use App\Livewire\Member\Overview;
use App\Livewire\Member\RedeemPoint;
use App\Livewire\Member\Setting\Account;
use App\Livewire\Member\Voucher;
use Illuminate\Support\Facades\Route;
@ -14,4 +15,5 @@
Route::get('vouchers', Voucher::class)->name('voucher');
Route::get('memberships', Membership::class)->name('membership');
Route::get('redeem-points', RedeemPoint::class)->name('redeem_point');
Route::get('settings', Account::class)->name('setting');
});

View File

@ -0,0 +1,71 @@
<?php
use App\Enums\Gender;
use App\Livewire\Member\Setting\Account;
use App\Models\Customer;
use Illuminate\Support\Facades\Hash;
use Livewire\Livewire;
beforeEach(function () {
$this->setupUser();
});
it('renders member setting account page', function () {
$this->actingAs($this->user);
Livewire::test(Account::class)
->assertStatus(200)
->assertSee('Pengaturan Akun');
});
it('can update account information', function () {
$this->actingAs($this->user);
Livewire::test(Account::class)
->set('accountForm.username', 'newusername')
->set('accountForm.email', 'new@example.com')
->call('updateAccount')
->assertHasNoErrors();
$this->assertDatabaseHas('users', [
'id' => $this->user->id,
'username' => 'newusername',
'email' => 'new@example.com',
]);
});
it('can update member profile when customer exists', function () {
$this->actingAs($this->user);
$customer = Customer::factory()->for($this->user)->create([
'name' => 'Old Name',
'phone_number' => '0812 3456 7890',
'gender' => Gender::MALE,
]);
Livewire::test(Account::class)
->set('profileForm.name', 'New Name')
->set('profileForm.phone_number', '0812 3456 7891')
->set('profileForm.gender', Gender::FEMALE->value)
->call('updateProfile')
->assertHasNoErrors();
$this->assertDatabaseHas('customers', [
'id' => $customer->id,
'name' => 'New Name',
'phone_number' => '0812 3456 7891',
'gender' => Gender::FEMALE->value,
]);
});
it('can change password', function () {
$this->actingAs($this->user);
$this->user->update(['password' => Hash::make('OldPassword123!')]);
Livewire::test(Account::class)
->set('passwordForm.current_password', 'OldPassword123!')
->set('passwordForm.new_password', '@Member#2026_Settings!')
->set('passwordForm.new_confirm_password', '@Member#2026_Settings!')
->call('updatePassword')
->assertHasNoErrors()
->assertRedirect(route('login'));
});