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 '';
} }
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); $prefix = Str::substr($username, 0, 6);
$year = date('y'); $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('/\D+/', '', $number);
$clean = preg_replace('/^(0|62)/', '', $clean); $clean = preg_replace('/^(0|62)/', '', $clean);
$formatted = $prefix . $clean; $formatted = $prefix.$clean;
if ($useDash) { 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; return $formatted;
} }
} }
if (!function_exists('terbilang')) { if (! function_exists('terbilang')) {
function terbilang($angka) function terbilang($angka)
{ {
$angka = (int) abs($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) { if ($angka < 12) {
return $huruf[$angka]; return $huruf[$angka];
} elseif ($angka < 20) { } elseif ($angka < 20) {
return $huruf[$angka - 10] . " belas"; return $huruf[$angka - 10].' belas';
} elseif ($angka < 100) { } elseif ($angka < 100) {
$result = $huruf[$angka / 10] . " puluh"; $result = $huruf[$angka / 10].' puluh';
$sisa = $angka % 10; $sisa = $angka % 10;
if ($sisa > 0) $result .= " " . $huruf[$sisa]; if ($sisa > 0) {
$result .= ' '.$huruf[$sisa];
}
return $result; return $result;
} elseif ($angka < 200) { } elseif ($angka < 200) {
return "seratus " . terbilang($angka - 100); return 'seratus '.terbilang($angka - 100);
} elseif ($angka < 1000) { } elseif ($angka < 1000) {
$result = $huruf[$angka / 100] . " ratus"; $result = $huruf[$angka / 100].' ratus';
$sisa = $angka % 100; $sisa = $angka % 100;
if ($sisa > 0) $result .= " " . terbilang($sisa); if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result; return $result;
} elseif ($angka < 2000) { } elseif ($angka < 2000) {
return "seribu " . terbilang($angka - 1000); return 'seribu '.terbilang($angka - 1000);
} elseif ($angka < 1000000) { } elseif ($angka < 1000000) {
$result = terbilang($angka / 1000) . " ribu"; $result = terbilang($angka / 1000).' ribu';
$sisa = $angka % 1000; $sisa = $angka % 1000;
if ($sisa > 0) $result .= " " . terbilang($sisa); if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result; return $result;
} elseif ($angka < 1000000000) { } elseif ($angka < 1000000000) {
$result = terbilang($angka / 1000000) . " juta"; $result = terbilang($angka / 1000000).' juta';
$sisa = $angka % 1000000; $sisa = $angka % 1000000;
if ($sisa > 0) $result .= " " . terbilang($sisa); if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result; return $result;
} elseif ($angka < 1000000000000) { } elseif ($angka < 1000000000000) {
$result = terbilang($angka / 1000000000) . " milyar"; $result = terbilang($angka / 1000000000).' milyar';
$sisa = $angka % 1000000000; $sisa = $angka % 1000000000;
if ($sisa > 0) $result .= " " . terbilang($sisa); if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result; return $result;
} elseif ($angka < 1000000000000000) { } elseif ($angka < 1000000000000000) {
$result = terbilang($angka / 1000000000000) . " trilyun"; $result = terbilang($angka / 1000000000000).' trilyun';
$sisa = $angka % 1000000000000; $sisa = $angka % 1000000000000;
if ($sisa > 0) $result .= " " . terbilang($sisa); if ($sisa > 0) {
$result .= ' '.terbilang($sisa);
}
return $result; return $result;
} else { } else {
return "Angka terlalu besar"; return 'Angka terlalu besar';
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@
use App\Traits\WithConfirmation; use App\Traits\WithConfirmation;
use App\Traits\WithToast; use App\Traits\WithToast;
use App\Traits\WithUpdatedData; use App\Traits\WithUpdatedData;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
@ -50,7 +51,7 @@ class Create extends Component
public array $vouchers = []; public array $vouchers = [];
public function mount() public function mount(): void
{ {
$this->outlets = auth()->user()->outlets->pluck('name', 'id')->toArray(); $this->outlets = auth()->user()->outlets->pluck('name', 'id')->toArray();
@ -72,7 +73,7 @@ public function mount()
$this->total = $this->getTotal(); $this->total = $this->getTotal();
} }
public function save() public function save(): void
{ {
$this->canOrAbort('create order'); $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->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->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(); $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', [ return view('livewire.studio.manage.order.form', [
'pageTitle' => 'Tambah Order', 'pageTitle' => 'Tambah Order',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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