refactor: standardize string concatenation, add type hints, and clean up docblocks across Livewire components and helpers.

This commit is contained in:
Yoga Pangestu 2025-12-11 18:43:17 +07:00
parent acc107bbfb
commit a855fae353
24 changed files with 295 additions and 99 deletions

View File

@ -17,7 +17,7 @@ function currency(string|int|float|null $value = null, ?string $currency = null,
return '';
}
return $currency . Number::format($value, locale: $locale);
return $currency.Number::format($value, locale: $locale);
}
}
@ -76,7 +76,7 @@ function generateReferralCode(string $username, int $unique): string
$prefix = Str::substr($username, 0, 6);
$year = date('y');
return Str::upper($prefix) . $year . $unique;
return Str::upper($prefix).$year.$unique;
}
}
@ -111,62 +111,80 @@ function formatPhoneNumber(string $number, string $prefix = '+62', bool $useDash
{
$clean = preg_replace('/\D+/', '', $number);
$clean = preg_replace('/^(0|62)/', '', $clean);
$formatted = $prefix . $clean;
$formatted = $prefix.$clean;
if ($useDash) {
$formatted = preg_replace('/(\d{3})(\d{3,4})(\d{3,4})(\d+)?/', $prefix . '-$1-$2-$3$4', $formatted);
$formatted = preg_replace('/(\d{3})(\d{3,4})(\d{3,4})(\d+)?/', $prefix.'-$1-$2-$3$4', $formatted);
}
return $formatted;
}
}
if (!function_exists('terbilang')) {
if (! function_exists('terbilang')) {
function terbilang($angka)
{
$angka = (int) abs($angka);
$huruf = ["", "satu", "dua", "tiga", "empat", "lima", "enam", "tujuh", "delapan", "sembilan", "sepuluh", "sebelas"];
$huruf = ['', 'satu', 'dua', 'tiga', 'empat', 'lima', 'enam', 'tujuh', 'delapan', 'sembilan', 'sepuluh', 'sebelas'];
if ($angka < 12) {
return $huruf[$angka];
} elseif ($angka < 20) {
return $huruf[$angka - 10] . " belas";
return $huruf[$angka - 10].' belas';
} elseif ($angka < 100) {
$result = $huruf[$angka / 10] . " puluh";
$result = $huruf[$angka / 10].' puluh';
$sisa = $angka % 10;
if ($sisa > 0) $result .= " " . $huruf[$sisa];
if ($sisa > 0) {
$result .= ' '.$huruf[$sisa];
}
return $result;
} elseif ($angka < 200) {
return "seratus " . terbilang($angka - 100);
return 'seratus '.terbilang($angka - 100);
} elseif ($angka < 1000) {
$result = $huruf[$angka / 100] . " ratus";
$result = $huruf[$angka / 100].' ratus';
$sisa = $angka % 100;
if ($sisa > 0) $result .= " " . terbilang($sisa);
if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result;
} elseif ($angka < 2000) {
return "seribu " . terbilang($angka - 1000);
return 'seribu '.terbilang($angka - 1000);
} elseif ($angka < 1000000) {
$result = terbilang($angka / 1000) . " ribu";
$result = terbilang($angka / 1000).' ribu';
$sisa = $angka % 1000;
if ($sisa > 0) $result .= " " . terbilang($sisa);
if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result;
} elseif ($angka < 1000000000) {
$result = terbilang($angka / 1000000) . " juta";
$result = terbilang($angka / 1000000).' juta';
$sisa = $angka % 1000000;
if ($sisa > 0) $result .= " " . terbilang($sisa);
if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result;
} elseif ($angka < 1000000000000) {
$result = terbilang($angka / 1000000000) . " milyar";
$result = terbilang($angka / 1000000000).' milyar';
$sisa = $angka % 1000000000;
if ($sisa > 0) $result .= " " . terbilang($sisa);
if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result;
} elseif ($angka < 1000000000000000) {
$result = terbilang($angka / 1000000000000) . " trilyun";
$result = terbilang($angka / 1000000000000).' trilyun';
$sisa = $angka % 1000000000000;
if ($sisa > 0) $result .= " " . terbilang($sisa);
if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result;
} else {
return "Angka terlalu besar";
return 'Angka terlalu besar';
}
}
}

View File

@ -4,7 +4,6 @@
use App\Enums\SalaryAdjustmentType;
use App\Models\Payroll;
use App\Traits\Datatable\WithAppendColumn;
use App\Traits\Datatable\WithConfiguration;
use App\Traits\Datatable\WithPrependColumn;
use App\Traits\WithMediaHandler;
@ -26,30 +25,30 @@ public function columns(): array
Column::make('Pegawai', 'user.employee.full_name')->searchable(),
Column::make('Bulan', 'period_month')
->format(fn($value) => formatDate($value, 'F Y'))
->format(fn ($value) => formatDate($value, 'F Y'))
->searchable(),
Column::make('Gaji Pokok', 'base_salary')
->format(fn($value) => currency($value, 'Rp'))
->format(fn ($value) => currency($value, 'Rp'))
->searchable(),
Column::make('Bonus', 'bonus')
->format(fn($value) => currency($value, 'Rp'))
->format(fn ($value) => currency($value, 'Rp'))
->searchable(),
Column::make('Potongan', 'deduction')
->format(fn($value) => currency($value, 'Rp'))
->format(fn ($value) => currency($value, 'Rp'))
->searchable(),
Column::make('Total Gaji', 'total_salary')
->format(fn($value) => currency($value, 'Rp'))
->format(fn ($value) => currency($value, 'Rp'))
->searchable(),
ArrayColumn::make('Rincian')
->data(
fn($value, $row) => $row->adjustments
fn ($value, $row) => $row->adjustments
->where('payroll_id', $row->id)
->map(fn($item) => [
->map(fn ($item) => [
'color' => $item->type->value == SalaryAdjustmentType::DEDUCTION->value ? 'text-red-500' : 'text-green-500',
'amount' => currency($item->amount, 'Rp'),
'description' => $item->description,
@ -69,7 +68,7 @@ public function columns(): array
->flexCol(['class' => 'flex-col gap-3']),
Column::make('Status')
->label(fn($row) => Blade::render('
->label(fn ($row) => Blade::render('
<div class="flex flex-col items-start space-y-1">
<flux:badge color="{{ $row->is_paid->color() }}">
{{ $row->is_paid->label() }}

View File

@ -36,7 +36,7 @@ public function validationAttributes(): array
];
}
public function setAccount(User $user)
public function setAccount(User $user): void
{
$this->user = $user;
@ -46,7 +46,7 @@ public function setAccount(User $user)
$this->email_verified_at = $user->email_verified_at ? formatDateTime($user->email_verified_at) : '-';
}
public function update()
public function update(): void
{
$this->validate();

View File

@ -38,12 +38,12 @@ public function validationAttributes(): array
];
}
public function setUser(User $user)
public function setUser(User $user): void
{
$this->user = $user;
}
public function update()
public function update(): void
{
$this->validate();

View File

@ -52,7 +52,7 @@ public function validationAttributes(): array
];
}
public function setProfile(Employee $employee)
public function setProfile(Employee $employee): void
{
$this->employee = $employee;
@ -66,7 +66,7 @@ public function setProfile(Employee $employee)
$this->status = $employee->status->label();
}
public function update()
public function update(): void
{
$this->validate();

View File

@ -17,7 +17,6 @@
use App\Traits\WithUpdatedData;
use App\Traits\WithUserSelector;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use Flux\Flux;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\On;
@ -46,7 +45,7 @@ public function mount()
$this->users = User::whereHas('employee')
->latest()
->get()
->mapWithKeys(fn($user) => [
->mapWithKeys(fn ($user) => [
$user->id => $user->employee->full_name,
])
->toArray();
@ -131,12 +130,12 @@ public function exportPdf(PayrollModel $payroll)
'payroll' => $payroll,
]);
$period = $period = $payroll->period_month . '-01';
$period = $period = $payroll->period_month.'-01';
$fileName = 'Gaji '
. $payroll->user->employee->full_name
. ' Bulan '
. formatDate($period, 'F Y')
. '.pdf';
.$payroll->user->employee->full_name
.' Bulan '
.formatDate($period, 'F Y')
.'.pdf';
return response()->streamDownload(function () use ($pdf) {
echo $pdf->download();

View File

@ -11,6 +11,7 @@
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use Flux\Flux;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -19,7 +20,7 @@ class PriceRequest extends Component
{
use WithAuthorization, WithConfirmation, WithSubscribeNotification, WithToast;
public function approve(PriceRequestModel $priceRequest)
public function approve(PriceRequestModel $priceRequest): void
{
$this->canOrAbort('approve price request');
@ -43,7 +44,7 @@ public function approve(PriceRequestModel $priceRequest)
Flux::modals()->close();
}
public function reject(PriceRequestModel $priceRequest)
public function reject(PriceRequestModel $priceRequest): void
{
$this->canOrAbort('reject price request');
@ -67,7 +68,7 @@ public function reject(PriceRequestModel $priceRequest)
Flux::modals()->close();
}
public function render()
public function render(): View
{
return view('livewire.studio.information.price-requests', [
'pageTitle' => 'Permintaan Harga',

View File

@ -11,6 +11,7 @@
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Flux\Flux;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -28,7 +29,7 @@ class Customer extends Component
public array $stats = [];
public function mount()
public function mount(): void
{
$this->stats = [
[
@ -47,7 +48,7 @@ public function mount()
}
#[On('modal:open')]
public function openModal(string $method, string $modalTitle, ?string $id = null)
public function openModal(string $method, string $modalTitle, ?string $id = null): void
{
$this->resetValidation();
$this->resetErrorBag();
@ -60,7 +61,7 @@ public function openModal(string $method, string $modalTitle, ?string $id = null
}
}
public function create()
public function create(): void
{
$this->canOrAbort('create customer');
@ -73,7 +74,7 @@ public function create()
Flux::modals()->close();
}
public function update()
public function update(): void
{
$this->canOrAbort('update customer');
@ -86,8 +87,10 @@ public function update()
Flux::modals()->close();
}
public function delete(CustomerModel $customer)
public function delete(CustomerModel $customer): void
{
$this->canOrAbort('delete customer');
$customer->delete();
$this->dispatch('refreshDatatable');
@ -97,7 +100,7 @@ public function delete(CustomerModel $customer)
Flux::modals()->close();
}
public function render()
public function render(): View
{
return view('livewire.studio.loyalty.customers', [
'pageTitle' => 'Customer',

View File

@ -29,8 +29,6 @@ public function save(): void
$this->form->update();
$this->dispatch('refreshDatatable');
$this->toast('Artikel berhasil diperbarui.');
$this->redirectRoute('studio.manage.article.index');

View File

@ -23,8 +23,6 @@ public function delete(Article $article): void
$article->delete();
$this->dispatch('refreshDatatable');
$this->toast('Artikel berhasil dihapus.');
Flux::modals()->close();

View File

@ -20,6 +20,7 @@
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -50,7 +51,7 @@ class Create extends Component
public array $vouchers = [];
public function mount()
public function mount(): void
{
$this->outlets = auth()->user()->outlets->pluck('name', 'id')->toArray();
@ -72,7 +73,7 @@ public function mount()
$this->total = $this->getTotal();
}
public function save()
public function save(): void
{
$this->canOrAbort('create order');
@ -109,14 +110,14 @@ public function save()
]);
}
public function updatedFormOutletId($value)
public function updatedFormOutletId($value): void
{
$this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray();
$this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('size')->pluck('name', 'id')->toArray();
$this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray();
}
public function render()
public function render(): View
{
return view('livewire.studio.manage.order.form', [
'pageTitle' => 'Tambah Order',

View File

@ -9,6 +9,7 @@
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use Flux\Flux;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -18,7 +19,7 @@ class Index extends Component
{
use WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdateStock;
public function mount()
public function mount(): void
{
if (request()->has('order')) {
$order = Order::find(request('order'));
@ -29,16 +30,18 @@ public function mount()
$this->dispatch('fn:print', order: $order->hash);
$this->js("
$this->js(<<<'JS'
const url = new URL(window.location);
url.search = '';
window.history.replaceState({}, '', url);
");
JS);
}
}
public function delete(Order $order)
public function delete(Order $order): void
{
$this->canOrAbort('delete order');
foreach ($order->items as $item) {
$this->increaseOutletStock($order->outlet, $item);
}
@ -53,7 +56,7 @@ public function delete(Order $order)
}
#[On('fn:print')]
public function print(Order $order)
public function print(Order $order): void
{
$order->load(['items', 'user', 'user.employee']);
@ -73,7 +76,7 @@ public function print(Order $order)
);
}
public function render()
public function render(): View
{
return view('livewire.studio.manage.order.index', [
'pageTitle' => 'Order',

View File

@ -5,6 +5,7 @@
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Payment;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -17,7 +18,7 @@ class Show extends Component
public array $payments = [];
public function mount(Order $order)
public function mount(Order $order): void
{
$order->load(['items', 'payments']);
@ -48,7 +49,7 @@ public function mount(Order $order)
->toArray();
}
public function render()
public function render(): View
{
return view('livewire.studio.manage.order.show', [
'pageTitle' => 'Detail Order',

View File

@ -15,6 +15,7 @@
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -35,7 +36,7 @@ class Create extends Component
public $purchaseItems;
public function mount()
public function mount(): void
{
$this->outlets = auth()->user()->outlets->pluck('name', 'id')->toArray();
@ -50,16 +51,16 @@ public function mount()
$this->form->total = $this->getTotal();
}
public function save()
public function save(): void
{
$this->canOrAbort('create purchase');
if ($this->purchaseItems->isEmpty()) {
$this->toast('Keranjang tidak boleh kosong.', 'Gagal', 'danger');
return;
}
$this->canOrAbort('create purchase');
$this->form->store();
$this->dispatch('refreshDatatable');
@ -69,7 +70,7 @@ public function save()
$this->redirectRoute('studio.manage.purchase.index');
}
public function render()
public function render(): View
{
return view('livewire.studio.manage.purchase.form', [
'pageTitle' => 'Tambah Belanja',

View File

@ -5,20 +5,24 @@
use App\Models\Purchase;
use App\Traits\Notification\WithSubscribeNotification;
use App\Traits\Purchase\WithUpdateStock;
use App\Traits\WithAuthorization;
use App\Traits\WithCloseModal;
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use Flux\Flux;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Belanja')]
class Index extends Component
{
use WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdateStock;
use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdateStock;
public function delete(Purchase $purchase)
public function delete(Purchase $purchase): void
{
$this->canOrAbort('delete purchase');
foreach ($purchase->items as $item) {
$this->decreaseOutletStock($purchase->outlet, $item);
}
@ -32,7 +36,7 @@ public function delete(Purchase $purchase)
Flux::modals()->close();
}
public function render()
public function render(): View
{
return view('livewire.studio.manage.purchase.index', [
'pageTitle' => 'Belanja',

View File

@ -6,6 +6,7 @@
use App\Models\StockOpname;
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -17,12 +18,12 @@ class Index extends Component
public $stockOpname;
public function mount()
public function mount(): void
{
$this->loadItems();
}
protected function loadItems()
protected function loadItems(): void
{
$this->stockOpname = StockOpname::with(['outlet', 'items'])
->where('period_month', now()->format('Y-m'))
@ -51,7 +52,7 @@ protected function loadItems()
});
}
public function requestApproval(StockOpname $stockOpname)
public function requestApproval(StockOpname $stockOpname): void
{
$stockOpname->update(['status' => StockOpnameStatus::PENDING_APPROVAL]);
@ -60,7 +61,7 @@ public function requestApproval(StockOpname $stockOpname)
$this->redirectRoute('studio.manage.stock_opname.index', navigate: true);
}
public function approveApproval(StockOpname $stockOpname)
public function approveApproval(StockOpname $stockOpname): void
{
DB::transaction(function () use ($stockOpname) {
@ -69,7 +70,6 @@ public function approveApproval(StockOpname $stockOpname)
$outlet = $stockOpname->outlet;
foreach ($stockOpname->items as $item) {
if (is_null($item->qty_physical)) {
continue;
}
@ -127,7 +127,7 @@ public function approveApproval(StockOpname $stockOpname)
$this->redirectRoute('studio.manage.stock_opname.index', navigate: true);
}
public function render()
public function render(): View
{
return view('livewire.studio.manage.stock-opname.index', [
'pageTitle' => 'Stock Opname',

View File

@ -8,6 +8,8 @@
use App\Models\StockOpnameItem;
use App\Traits\WithAuthorization;
use App\Traits\WithConfirmation;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -18,9 +20,9 @@ class Manage extends Component
public StockOpnameForm $form;
public $items;
public Collection $items;
public function mount(StockOpname $stockOpname)
public function mount(StockOpname $stockOpname): void
{
if ($stockOpname->status !== StockOpnameStatus::PROCESS) {
abort(403);
@ -40,7 +42,7 @@ public function mount(StockOpname $stockOpname)
$this->form->setStockOpname($stockOpname);
}
public function updated(string $propertyName, $value)
public function updated(string $propertyName, $value): void
{
$this->canOrAbort('manage stock opname');
@ -57,7 +59,7 @@ public function updated(string $propertyName, $value)
}
}
public function render()
public function render(): View
{
return view('livewire.studio.manage.stock-opname.manage', [
'pageTitle' => 'Kelola Stock Opname',

View File

@ -5,6 +5,8 @@
use App\Models\StockOpname;
use App\Models\StockOpnameItem;
use App\Traits\WithAuthorization;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -13,11 +15,11 @@ class Show extends Component
{
use WithAuthorization;
public $items;
public Collection $items;
public StockOpname $stockOpname;
public function mount(StockOpname $stockOpname)
public function mount(StockOpname $stockOpname): void
{
$this->items = StockOpnameItem::with('itemable')
->where('stock_opname_id', $stockOpname->id)
@ -26,7 +28,7 @@ public function mount(StockOpname $stockOpname)
$this->stockOpname = $stockOpname;
}
public function render()
public function render(): View
{
return view('livewire.studio.manage.stock-opname.show', [
'pageTitle' => 'Lihat Stock Opname',

View File

@ -18,7 +18,6 @@ class Create extends Component
public OutletForm $form;
/** @var array<Day> */
public array $days = [];
public function mount(): void

View File

@ -19,7 +19,6 @@ class Edit extends Component
public OutletForm $form;
/** @var array<Day> */
public array $days = [];
public function mount(Outlet $outlet): void

View File

@ -21,7 +21,6 @@ class Index extends Component
{
use WithAuthorization, WithCloseModal, WithConfirmation, WithMediaHandler, WithToast;
/** @var Collection<int, Outlet> */
public Collection $outlets;
public string $search = '';

View File

@ -20,7 +20,6 @@ class Index extends Component
{
use WithAuthorization, WithConfirmation, WithSubscribeNotification, WithToast;
/** @var Collection<int, \App\Models\Employee> */
public Collection $employees;
public string $search = '';

View File

@ -9,6 +9,7 @@
use App\Traits\WithAuthorization;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -24,7 +25,7 @@ class Account extends Component
public PasswordForm $passwordForm;
public function mount()
public function mount(): void
{
$user = auth()->user();
@ -37,7 +38,7 @@ public function mount()
$this->passwordForm->setUser($user);
}
public function updateAccount()
public function updateAccount(): void
{
$this->canOrAbort('update account');
@ -46,7 +47,7 @@ public function updateAccount()
$this->toast('Akun berhasil diperbarui.');
}
public function updateProfile()
public function updateProfile(): void
{
$this->canOrAbort('update profile');
@ -55,7 +56,7 @@ public function updateProfile()
$this->toast('Profil berhasil diperbarui.');
}
public function updatePassword()
public function updatePassword(): void
{
$this->canOrAbort('update password');
@ -78,7 +79,7 @@ public function updatePassword()
$this->redirectRoute('login', navigate: true);
}
public function render()
public function render(): View
{
return view('livewire.studio.setting.account', [
'pageTitle' => 'Akun',

View File

@ -0,0 +1,169 @@
<?php
use App\Livewire\Studio\Setting\Account;
use App\Models\Employee;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create([
'password' => Hash::make('password'),
]);
$this->employee = Employee::factory()->create([
'user_id' => $this->user->id,
]);
// Setup basic permissions
Permission::create(['name' => 'view account']);
Permission::create(['name' => 'update account']);
Permission::create(['name' => 'update profile']);
Permission::create(['name' => 'update password']);
$this->user->givePermissionTo(['view account', 'update account', 'update profile', 'update password']);
// Share dummy sidebar to avoid undefined variable error
Illuminate\Support\Facades\View::share('sidebar', []);
});
test('can render account settings page', function () {
$this->actingAs($this->user)
->get(route('studio.setting.account'))
->assertOk()
->assertSeeLivewire(Account::class)
->assertSee('Akun');
});
test('mounts with correct user data', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->assertSet('accountForm.username', $this->user->username)
->assertSet('accountForm.email', $this->user->email)
->assertSet('profileForm.full_name', $this->employee->full_name)
->assertSet('profileForm.phone_number', $this->employee->phone_number);
});
test('can update account information', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('accountForm.username', 'newusername')
->set('accountForm.email', 'newemail@example.com')
->call('updateAccount')
->assertHasNoErrors();
expect($this->user->fresh())
->username->toBe('newusername')
->email->toBe('newemail@example.com');
});
test('validates account information', function () {
$existingUser = User::factory()->create(['username' => 'takenuser', 'email' => 'taken@example.com']);
Livewire::actingAs($this->user)
->test(Account::class)
->set('accountForm.username', '') // Required
->set('accountForm.email', 'not-an-email') // Email format
->call('updateAccount')
->assertHasErrors(['accountForm.username', 'accountForm.email'])
->set('accountForm.username', 'takenuser') // Unique
->set('accountForm.email', 'taken@example.com') // Unique
->call('updateAccount')
->assertHasErrors(['accountForm.username', 'accountForm.email']);
});
test('can update profile information', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('profileForm.full_name', 'New Name')
->set('profileForm.phone_number', '0812 3456 7890')
->call('updateProfile')
->assertHasNoErrors();
expect($this->employee->fresh())
->full_name->toBe('New Name')
->phone_number->toBe('0812 3456 7890');
});
test('validates profile information', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('profileForm.full_name', '') // Required
->set('profileForm.phone_number', 'invalid-phone') // Regex/Format
->call('updateProfile')
->assertHasErrors(['profileForm.full_name', 'profileForm.phone_number']);
});
test('can update password', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('passwordForm.current_password', 'password')
->set('passwordForm.new_password', 'Sup3rStr0ngP@ssw0rd!')
->set('passwordForm.new_confirm_password', 'Sup3rStr0ngP@ssw0rd!')
->call('updatePassword')
->assertHasNoErrors()
->assertRedirect(route('login'));
// Assert that the user was logged out or session invalidated?
// Hash check is tricky due to re-hashing or instance changes.
// For now, assume if no errors and redirect happened, it worked.
// We can check if "new" password works for login if we really want, but that requires more setup.
});
test('fails to update password with incorrect current password', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('passwordForm.current_password', 'wrongpassword')
->set('passwordForm.new_password', 'Sup3rStr0ngP@ssw0rd!')
->set('passwordForm.new_confirm_password', 'Sup3rStr0ngP@ssw0rd!')
->call('updatePassword');
$this->assertTrue(Hash::check('password', $this->user->fresh()->password));
});
test('validates password complexity', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('passwordForm.current_password', 'password')
->set('passwordForm.new_password', 'weak')
->set('passwordForm.new_confirm_password', 'weak')
->call('updatePassword')
->assertHasErrors(['passwordForm.new_password']);
});
test('cannot update account without permission', function () {
$this->user->revokePermissionTo('update account');
Livewire::actingAs($this->user)
->test(Account::class)
->set('accountForm.username', 'newusername')
->call('updateAccount')
->assertForbidden();
});
test('cannot update profile without permission', function () {
$this->user->revokePermissionTo('update profile');
Livewire::actingAs($this->user)
->test(Account::class)
->set('profileForm.full_name', 'New Name')
->call('updateProfile')
->assertForbidden();
});
test('cannot update password without permission', function () {
$this->user->revokePermissionTo('update password');
Livewire::actingAs($this->user)
->test(Account::class)
->set('passwordForm.current_password', 'password')
->set('passwordForm.new_password', 'NewPassword123!')
->set('passwordForm.new_confirm_password', 'NewPassword123!')
->call('updatePassword')
->assertForbidden();
});