feat: implement member account settings page for profile and password updates
This commit is contained in:
parent
0795f8b0d7
commit
d720934533
58
app/Livewire/Forms/Member/Setting/ProfileForm.php
Normal file
58
app/Livewire/Forms/Member/Setting/ProfileForm.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
90
app/Livewire/Member/Setting/Account.php
Normal file
90
app/Livewire/Member/Setting/Account.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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'),
|
||||
])
|
||||
|
||||
144
resources/views/livewire/member/setting/account.blade.php
Normal file
144
resources/views/livewire/member/setting/account.blade.php
Normal 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>
|
||||
@ -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" />
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
71
tests/Feature/Member/Setting/AccountTest.php
Normal file
71
tests/Feature/Member/Setting/AccountTest.php
Normal 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'));
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user