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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->passwordForm->update();
|
|
||||||
|
|
||||||
auth()->logoutOtherDevices($this->passwordForm->current_password);
|
auth()->logoutOtherDevices($this->passwordForm->current_password);
|
||||||
|
|
||||||
|
$this->passwordForm->update();
|
||||||
|
|
||||||
auth()->logout();
|
auth()->logout();
|
||||||
session()->invalidate();
|
session()->invalidate();
|
||||||
session()->regenerateToken();
|
session()->regenerateToken();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
@include('components.sections.ui.shared._header', [
|
@include('components.sections.ui.shared._header', [
|
||||||
'profileName' => auth()->user()?->customer?->name,
|
'profileName' => auth()->user()?->customer?->name ?? auth()->user()->username,
|
||||||
'settingsRoute' => route('studio.setting.account', auth()->id())
|
'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: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"
|
<flux:input placeholder="********" type="password" wire:model="passwordForm.new_confirm_password"
|
||||||
viewable />
|
viewable />
|
||||||
<flux:error name="passwordForm.new_confirm_password" />
|
<flux:error name="passwordForm.new_confirm_password" />
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
use App\Livewire\Member\Membership;
|
use App\Livewire\Member\Membership;
|
||||||
use App\Livewire\Member\Overview;
|
use App\Livewire\Member\Overview;
|
||||||
use App\Livewire\Member\RedeemPoint;
|
use App\Livewire\Member\RedeemPoint;
|
||||||
|
use App\Livewire\Member\Setting\Account;
|
||||||
use App\Livewire\Member\Voucher;
|
use App\Livewire\Member\Voucher;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -14,4 +15,5 @@
|
|||||||
Route::get('vouchers', Voucher::class)->name('voucher');
|
Route::get('vouchers', Voucher::class)->name('voucher');
|
||||||
Route::get('memberships', Membership::class)->name('membership');
|
Route::get('memberships', Membership::class)->name('membership');
|
||||||
Route::get('redeem-points', RedeemPoint::class)->name('redeem_point');
|
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