diff --git a/app/Livewire/Forms/Studio/Loyalty/TierForm.php b/app/Livewire/Forms/Studio/Loyalty/TierForm.php
index 2b032f1..e9fed04 100644
--- a/app/Livewire/Forms/Studio/Loyalty/TierForm.php
+++ b/app/Livewire/Forms/Studio/Loyalty/TierForm.php
@@ -4,6 +4,7 @@
use App\Models\Tier;
use App\Rules\UnsignedInteger;
+use Illuminate\Support\Facades\DB;
use Livewire\Form;
class TierForm extends Form
@@ -21,7 +22,7 @@ public function rules(): array
return [
'name' => ['required', 'string', 'max:50'],
'min_points' => ['required', new UnsignedInteger],
- 'max_points' => ['nullable', new UnsignedInteger],
+ 'max_points' => ['nullable', new UnsignedInteger, 'gte:min_points'],
];
}
@@ -34,7 +35,7 @@ public function validationAttributes(): array
];
}
- public function setTier(Tier $tier)
+ public function setTier(Tier $tier): void
{
$this->tier = $tier;
@@ -43,23 +44,27 @@ public function setTier(Tier $tier)
$this->max_points = $tier->max_points;
}
- public function store()
+ public function store(): Tier
{
$this->validate();
- return Tier::create($this->prepareSavedData());
+ return DB::transaction(function () {
+ return Tier::create($this->prepareSavedData());
+ });
}
- public function update()
+ public function update(): Tier
{
$this->validate();
- $this->tier->update($this->prepareSavedData());
+ return DB::transaction(function () {
+ $this->tier->update($this->prepareSavedData());
- return $this->tier;
+ return $this->tier;
+ });
}
- private function prepareSavedData()
+ private function prepareSavedData(): array
{
return [
'name' => $this->name,
diff --git a/app/Livewire/Studio/Loyalty/Tier.php b/app/Livewire/Studio/Loyalty/Tier.php
index 4034f72..6ef5aa3 100644
--- a/app/Livewire/Studio/Loyalty/Tier.php
+++ b/app/Livewire/Studio/Loyalty/Tier.php
@@ -14,6 +14,8 @@
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Flux\Flux;
+use Illuminate\Contracts\View\View;
+use Illuminate\Support\Collection;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Component;
@@ -23,7 +25,7 @@ class Tier extends Component
{
use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdatedData;
- public array $tiers = [];
+ public Collection $tiers;
public TierForm $form;
@@ -31,13 +33,13 @@ class Tier extends Component
public string $modalTitle = '';
- public function mount()
+ public function mount(): void
{
$this->loadTiers();
}
#[On('data:tiersUpdated')]
- public function loadTiers()
+ public function loadTiers(): void
{
$this->tiers = TierModel::with(['rewards', 'memberships.user.customer.orders.items'])
->withCount(['memberships', 'rewards'])
@@ -50,6 +52,7 @@ public function loadTiers()
->first();
$topMemberStats = null;
+
if ($topMembership && $topMembership->user && $topMembership->user->customer) {
$orders = $topMembership->user->customer->orders;
@@ -82,22 +85,16 @@ public function loadTiers()
];
}
- return [
- 'id' => $tier->id,
- 'hash' => $tier->hash,
- 'name' => $tier->name,
- 'min_points' => currency($tier->min_points),
- 'max_points' => currency($tier->max_points),
- 'total_members' => currency($tier->memberships_count),
- 'total_rewards' => currency($tier->rewards_count),
- 'top_member' => $topMemberStats,
- ];
- })
- ->toArray();
+ $tier->top_member = $topMemberStats;
+ $tier->total_members = $tier->memberships_count;
+ $tier->total_rewards = $tier->rewards_count;
+
+ return $tier;
+ });
}
#[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();
@@ -110,70 +107,79 @@ public function openModal(string $method, string $modalTitle, ?string $id = null
}
}
- public function create()
+ public function create(): void
{
$this->canOrAbort('create tier');
+ $minPoints = (int) $this->form->min_points;
+ $maxPoints = (int) $this->form->max_points;
+
+ if (TierModel::isOverlap($minPoints, $maxPoints)) {
+ $this->toast('Range poin yang kamu masukkan bertabrakan dengan tier yang sudah ada.', 'Info', 'warning');
+
+ return;
+ }
+
$tier = $this->form->store();
- $tiers = $this->tiers;
- $tiers[] = [
- 'id' => $tier->id,
- 'hash' => $tier->hash,
- 'name' => $tier->name,
- 'min_points' => currency($tier->min_points, ''),
- 'max_points' => currency($tier->max_points, ''),
- 'total_members' => 0,
- 'top_member' => null,
- 'total_rewards' => 0,
- ];
+ // Add computed properties to the model
+ $tier->total_members = 0;
+ $tier->total_rewards = 0;
+ $tier->top_member = null;
- usort($tiers, fn ($a, $b) => replaceCurrency($a['min_points']) <=> replaceCurrency($b['min_points']));
-
- $this->tiers = $tiers;
+ // Add new tier to collection and sort
+ $this->tiers = $this->tiers->push($tier)->sortBy('min_points')->values();
$this->toast('Tier berhasil ditambahkan.');
Flux::modals()->close();
}
- public function update()
+ public function update(): void
{
$this->canOrAbort('update tier');
+ $tierId = $this->form->tier->id;
+ $minPoints = (int) $this->form->min_points;
+ $maxPoints = (int) $this->form->max_points;
+
+ if (TierModel::isOverlap($minPoints, $maxPoints, $tierId)) {
+ $this->toast('Range poin yang kamu masukkan bertabrakan dengan tier yang sudah ada.', 'Info', 'warning');
+
+ return;
+ }
+
$tier = $this->form->update();
- // update array tiers
- $tiers = $this->tiers;
- foreach ($tiers as $index => $item) {
- if ($item['hash'] === $tier->hash) {
- $tiers[$index]['name'] = $tier->name;
- $tiers[$index]['min_points'] = currency($tier->min_points, '');
- $tiers[$index]['max_points'] = currency($tier->max_points, '');
- break;
+ // Update the tier in collection
+ $this->tiers = $this->tiers->map(function ($t) use ($tier) {
+ if ($t->hash === $tier->hash) {
+ $t->name = $tier->name;
+ $t->min_points = $tier->min_points;
+ $t->max_points = $tier->max_points;
}
- }
- $this->tiers = $tiers;
+
+ return $t;
+ });
$this->toast('Tier berhasil diperbarui.');
Flux::modals()->close();
}
- public function delete(TierModel $tier)
+ public function delete(TierModel $tier): void
{
$tier->delete();
- // update array tiers
- $tiers = array_values(array_filter($this->tiers, fn ($item) => $item['hash'] !== $tier->hash));
- $this->tiers = $tiers;
+ // Remove tier from collection
+ $this->tiers = $this->tiers->filter(fn ($t) => $t->hash !== $tier->hash)->values();
$this->toast('Tier berhasil dihapus.');
Flux::modals()->close();
}
- public function render()
+ public function render(): View
{
return view('livewire.studio.loyalty.tier.index', [
'pageTitle' => 'Tier',
diff --git a/app/Models/Tier.php b/app/Models/Tier.php
index 8e1782b..48fec59 100644
--- a/app/Models/Tier.php
+++ b/app/Models/Tier.php
@@ -25,6 +25,19 @@ protected function casts(): array
];
}
+ public static function isOverlap(int $minPoints, ?int $maxPoints, ?int $ignoreId = null): bool
+ {
+ $maxPointsCheck = $maxPoints ?? PHP_INT_MAX;
+
+ return self::when($ignoreId, fn ($query) => $query->where('id', '!=', $ignoreId))
+ ->get()
+ ->contains(function ($tier) use ($minPoints, $maxPointsCheck) {
+ $tierMax = $tier->max_points ?? PHP_INT_MAX;
+
+ return ($minPoints <= $tierMax) && ($maxPointsCheck >= $tier->min_points);
+ });
+ }
+
public function memberships(): HasMany
{
return $this->hasMany(Membership::class);
diff --git a/resources/views/livewire/studio/loyalty/tier/index.blade.php b/resources/views/livewire/studio/loyalty/tier/index.blade.php
index e9de912..6aab52e 100644
--- a/resources/views/livewire/studio/loyalty/tier/index.blade.php
+++ b/resources/views/livewire/studio/loyalty/tier/index.blade.php
@@ -22,7 +22,7 @@ class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900
@can('create tier reward')
- @if ($tier['total_rewards'] == 0)
+ @if ($tier->total_rewards == 0)
Belum ada hadiah untuk tier ini, silakan tambah terlebih dahulu.
@@ -30,7 +30,7 @@ class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900
+ wire:click="$dispatch('modal:open:reward', {method: 'create', 'modalTitle': 'Tambah Hadiah', 'tier_id': '{{ $tier->id }}'})">
Tambah
@@ -39,41 +39,41 @@ class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900
@endcan
- {{ $tier['name'] }}
+ {{ $tier->name }}
Min. Poin:
- {{ $tier['min_points'] }}
+ {{ currency($tier->min_points) }}
Maks. Poin:
- {{ $tier['max_points'] }}
+ {{ currency($tier->max_points) }}
Total Member:
- {{ $tier['total_members'] }}
+ {{ $tier->total_members }}
Total Hadiah:
- {{ $tier['total_rewards'] }}
+ {{ $tier->total_rewards }}
- @if ($tier['top_member'])
+ @if ($tier->top_member)
Member Dengan Poin
Tertinggi:
- {{ $tier['top_member']['name'] }}
- {{ $tier['top_member']['points'] }} Poin
+ {{ $tier->top_member['name'] }}
+ {{ $tier->top_member['points'] }} Poin
Statistik Order
Order:
- {{ $tier['top_member']['total_orders'] }}x
+ {{ $tier->top_member['total_orders'] }}x
Parfum Yang Dibeli:
- {{ $tier['top_member']['items']['total_perfume'] }} ml
+ {{ $tier->top_member['items']['total_perfume'] }} ml
Produk Yang Dibeli:
- {{ $tier['top_member']['items']['total_product'] }} pcs
+ {{ $tier->top_member['items']['total_product'] }} pcs
Botol Yang Dibeli:
- {{ $tier['top_member']['items']['total_bottle'] }} pcs
+ {{ $tier->top_member['items']['total_bottle'] }} pcs
Subtotal Belanja:
- {{ $tier['top_member']['total']['subtotal'] }}
+ {{ $tier->top_member['total']['subtotal'] }}
Diskon:
- {{ $tier['top_member']['total']['discount'] }}
+ {{ $tier->top_member['total']['discount'] }}
Total Belanja:
- {{ $tier['top_member']['total']['grand_total'] }}
+ {{ $tier->top_member['total']['grand_total'] }}
@@ -117,7 +117,7 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
+ wire:click="$dispatch('modal:open:reward', {method: 'create', 'modalTitle': '{{ $tier->name }}', 'tier_id': '{{ $tier->id }}'})">
@@ -127,7 +127,7 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
+ wire:click="$dispatch('modal:open:reward', {method: 'create', 'modalTitle': 'Tambah Hadiah', 'tier_id': '{{ $tier->id }}'})">
@@ -137,7 +137,7 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
+ wire:click="$dispatch('modal:open', {method: 'update', 'modalTitle': 'Ubah Tier', 'id': '{{ $tier->hash }}'})">
@@ -147,7 +147,7 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
+ wire:click="$dispatch('fn:confirmAction', {id: '{{ $tier->hash }}'})">
@@ -160,13 +160,14 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
@include('components.modals.confirmation', [
- 'modalName' => 'confirmation-modal',
+ 'modalName' => 'delete-confirmation',
'modalTitle' => 'Apakah Anda yakin?',
'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.',
'buttonVariant' => 'primary',
'buttonColor' => 'danger',
'buttonText' => 'Ya, Hapus',
])
+
{{ $modalTitle }}
diff --git a/tests/Feature/Livewire/Studio/Loyalty/TierTest.php b/tests/Feature/Livewire/Studio/Loyalty/TierTest.php
index e7b8444..1dc2963 100644
--- a/tests/Feature/Livewire/Studio/Loyalty/TierTest.php
+++ b/tests/Feature/Livewire/Studio/Loyalty/TierTest.php
@@ -15,6 +15,7 @@
use App\Models\TierReward;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
@@ -58,9 +59,10 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
+ expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
expect($component->tiers)->toHaveCount(2);
- expect($component->tiers[0]['name'])->toBe('Bronze');
- expect($component->tiers[1]['name'])->toBe('Silver');
+ expect($component->tiers[0]->name)->toBe('Bronze');
+ expect($component->tiers[1]->name)->toBe('Silver');
});
it('displays all tiers in the view', function () {
@@ -79,9 +81,10 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
- expect($component->tiers[0]['name'])->toBe('Bronze');
- expect($component->tiers[1]['name'])->toBe('Silver');
- expect($component->tiers[2]['name'])->toBe('Platinum');
+ expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
+ expect($component->tiers[0]->name)->toBe('Bronze');
+ expect($component->tiers[1]->name)->toBe('Silver');
+ expect($component->tiers[2]->name)->toBe('Platinum');
});
/*
@@ -112,7 +115,9 @@ function mountTierComponent(User $user)
->call('create')
->assertHasNoErrors()
->assertSet('tiers', function ($tiers) {
- return count($tiers) === 1 && $tiers[0]['name'] === 'Gold';
+ return $tiers instanceof \Illuminate\Support\Collection &&
+ $tiers->count() === 1 &&
+ $tiers[0]->name === 'Gold';
});
$this->assertDatabaseHas('tiers', [
@@ -149,9 +154,10 @@ function mountTierComponent(User $user)
->set('form.max_points', '1000')
->call('create');
- expect($component->tiers[0]['name'])->toBe('Bronze');
- expect($component->tiers[1]['name'])->toBe('Silver');
- expect($component->tiers[2]['name'])->toBe('Gold');
+ expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
+ expect($component->tiers[0]->name)->toBe('Bronze');
+ expect($component->tiers[1]->name)->toBe('Silver');
+ expect($component->tiers[2]->name)->toBe('Gold');
});
it('validates required fields when creating tier', function () {
@@ -225,9 +231,9 @@ function mountTierComponent(User $user)
->call('update')
->assertHasNoErrors()
->assertSet('tiers', function ($tiers) use ($tier) {
- $updatedTier = collect($tiers)->firstWhere('hash', $tier->hash);
+ $updatedTier = $tiers->firstWhere('hash', $tier->hash);
- return $updatedTier['name'] === 'Gold';
+ return $updatedTier->name === 'Gold';
});
$tier->refresh();
@@ -299,7 +305,9 @@ function mountTierComponent(User $user)
->call('delete', $tier)
->assertHasNoErrors()
->assertSet('tiers', function ($tiers) use ($tier) {
- return ! collect($tiers)->contains('hash', $tier->hash);
+ return ! $tiers->contains(function ($t) use ($tier) {
+ return $t->hash === $tier->hash;
+ });
});
expect(TierModel::count())->toBe(0);
@@ -342,8 +350,8 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
- $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id);
- expect($loadedTier['total_members'])->toBe('2');
+ $loadedTier = $component->tiers->firstWhere('id', $tier->id);
+ expect($loadedTier->total_members)->toBe(2);
});
it('loads tiers with rewards count', function () {
@@ -354,9 +362,9 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
- $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id);
+ $loadedTier = $component->tiers->firstWhere('id', $tier->id);
- expect((int) $loadedTier['total_rewards'])->toBe(3);
+ expect((int) $loadedTier->total_rewards)->toBe(3);
});
it('loads top member stats when tier has memberships', function () {
@@ -478,14 +486,14 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
- $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id);
- expect($loadedTier['top_member'])->not->toBeNull();
- expect($loadedTier['top_member']['name'])->toBe('John Doe');
- expect($loadedTier['top_member']['points'])->toContain('500');
- expect($loadedTier['top_member']['total_orders'])->toContain('2');
- expect($loadedTier['top_member']['items']['total_perfume'])->toContain('3');
- expect($loadedTier['top_member']['items']['total_product'])->toContain('3');
- expect($loadedTier['top_member']['items']['total_bottle'])->toContain('1');
+ $loadedTier = $component->tiers->firstWhere('id', $tier->id);
+ expect($loadedTier->top_member)->not->toBeNull();
+ expect($loadedTier->top_member['name'])->toBe('John Doe');
+ expect($loadedTier->top_member['points'])->toContain('500');
+ expect($loadedTier->top_member['total_orders'])->toContain('2');
+ expect($loadedTier->top_member['items']['total_perfume'])->toContain('3');
+ expect($loadedTier->top_member['items']['total_product'])->toContain('3');
+ expect($loadedTier->top_member['items']['total_bottle'])->toContain('1');
});
it('shows null top member when tier has no memberships', function () {
@@ -493,8 +501,8 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
- $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id);
- expect($loadedTier['top_member'])->toBeNull();
+ $loadedTier = $component->tiers->firstWhere('id', $tier->id);
+ expect($loadedTier->top_member)->toBeNull();
});
it('reloads tiers when data:tiersUpdated event is dispatched', function () {
@@ -507,7 +515,7 @@ function mountTierComponent(User $user)
$component->dispatch('data:tiersUpdated')
->assertSet('tiers', function ($tiers) {
- return count($tiers) === 2;
+ return $tiers->count() === 2;
});
});
@@ -534,6 +542,8 @@ function mountTierComponent(User $user)
expect($component->method)->toBe('update');
expect($component->modalTitle)->toBe('Ubah Tier');
expect($component->form->name)->toBe($tier->name);
+ expect($component->form->min_points)->toBe((string) $tier->min_points);
+ expect($component->form->max_points)->toBe($tier->max_points ? (string) $tier->max_points : null);
});
it('sets form data when opening update modal', function () {
@@ -565,9 +575,9 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
- $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id);
- expect($loadedTier['min_points'])->toContain('1.000');
- expect($loadedTier['max_points'])->toContain('5.000');
+ $loadedTier = $component->tiers->firstWhere('id', $tier->id);
+ expect(currency($loadedTier->min_points))->toContain('1.000');
+ expect(currency($loadedTier->max_points))->toContain('5.000');
});
it('handles currency formatting in form input', function () {
@@ -599,8 +609,8 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
- $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id);
- expect($loadedTier['max_points'])->not->toBeNull();
+ $loadedTier = $component->tiers->firstWhere('id', $tier->id);
+ expect(currency($loadedTier->max_points))->not->toBeNull();
});
it('handles tier with zero min_points', function () {
@@ -612,14 +622,14 @@ function mountTierComponent(User $user)
$component = mountTierComponent($this->user);
- $loadedTier = collect($component->tiers)->firstWhere('id', $tier->id);
- expect($loadedTier['min_points'])->toContain('0');
+ $loadedTier = $component->tiers->firstWhere('id', $tier->id);
+ expect(currency($loadedTier->min_points))->toContain('0');
});
it('handles empty tiers list', function () {
$component = mountTierComponent($this->user);
- expect($component->tiers)->toBe([]);
+ expect($component->tiers)->toBeInstanceOf(Collection::class);
});
it('updates tier array correctly after update', function () {
@@ -636,8 +646,8 @@ function mountTierComponent(User $user)
->set('form.max_points', '5.000')
->call('update');
- $updatedTier = collect($component->tiers)->firstWhere('hash', $tier->hash);
- expect($updatedTier['name'])->toBe('Gold');
- expect($updatedTier['min_points'])->toContain('1.000');
- expect($updatedTier['max_points'])->toContain('5.000');
+ $updatedTier = $component->tiers->firstWhere('hash', $tier->hash);
+ expect($updatedTier->name)->toBe('Gold');
+ expect(currency($updatedTier->min_points))->toContain('1.000');
+ expect(currency($updatedTier->max_points))->toContain('5.000');
});