feat: implement member referral system including code generation, usage tracking, and UI integration.
This commit is contained in:
parent
716fb3448b
commit
33d11cedea
55
app/Livewire/Member/Referral.php
Normal file
55
app/Livewire/Member/Referral.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Member;
|
||||
|
||||
use App\Models\ReferralUsage;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Referral')]
|
||||
#[Layout('components.layouts.member')]
|
||||
class Referral extends Component
|
||||
{
|
||||
/**
|
||||
* Target spending for referral reward.
|
||||
*/
|
||||
public const TARGET_SPENDING = 50000;
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$user = auth()->user();
|
||||
$referralCode = $user->referralCode;
|
||||
|
||||
$usages = ReferralUsage::with(['user', 'user.customer', 'user.membership'])
|
||||
->where('referral_code_id', $referralCode->id)
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($usage) {
|
||||
$spending = $usage->user?->membership?->total_spending ?? 0;
|
||||
$progress = min(100, ($spending / self::TARGET_SPENDING) * 100);
|
||||
$isRewardEarned = $spending >= self::TARGET_SPENDING;
|
||||
|
||||
return [
|
||||
'name' => $usage->user?->customer?->name ?? 'Perfumers',
|
||||
'joined_at' => $usage->created_at,
|
||||
'spending' => $spending,
|
||||
'progress' => $progress,
|
||||
'is_reward_earned' => $isRewardEarned,
|
||||
];
|
||||
});
|
||||
|
||||
$message = 'Halo! Ayo bergabung di Yadi Parfum dan dapatkan keuntungan eksklusif. Gunakan kode referral saya: ' . ($referralCode?->code ?? '') . ' saat mendaftar! ' . url('/auth/register');
|
||||
$whatsappUrl = 'https://wa.me/?text=' . urlencode($message);
|
||||
|
||||
return view('livewire.member.referral', [
|
||||
'referralCode' => $referralCode,
|
||||
'usages' => $usages,
|
||||
'whatsappUrl' => $whatsappUrl,
|
||||
'targetSpending' => self::TARGET_SPENDING,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,6 @@
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Veelasky\LaravelHashId\Eloquent\HashableId;
|
||||
|
||||
@ -27,8 +26,8 @@ public function user(): BelongsTo
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function referralUsage(): HasOne
|
||||
public function usages(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasOne(ReferralUsage::class);
|
||||
return $this->hasMany(ReferralUsage::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,9 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z
|
||||
<flux:navlist.item icon="check-badge" href="{{ route('member.redeem_point') }}"
|
||||
:current="request()->routeIs('member.redeem_point')" wire:navigate>
|
||||
Penukaran Poin</flux:navlist.item>
|
||||
<flux:navlist.item icon="user-group" href="{{ route('member.referral') }}"
|
||||
:current="request()->routeIs('member.referral')" wire:navigate>
|
||||
Referral</flux:navlist.item>
|
||||
|
||||
@can('view studio overview')
|
||||
<div class="mt-3 mb-1">
|
||||
|
||||
@ -2,44 +2,32 @@
|
||||
<flux:heading size="xl" level="1">{{ $pageTitle }}</flux:heading>
|
||||
|
||||
<div class="mt-6">
|
||||
@if ($membership)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
@foreach ($stats as $stat)
|
||||
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="lg">{{ $stat['title'] }}</flux:heading>
|
||||
</div>
|
||||
<flux:heading class="text-4xl! font-bold py-3 ">{{ $stat['value'] }}</flux:heading>
|
||||
<flux:text size="lg">{{ $stat['description'] }}</flux:text>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
@foreach ($stats as $stat)
|
||||
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="lg">{{ $stat['title'] }}</flux:heading>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-200 dark:bg-zinc-700 rounded-xl p-6 shadow-md mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Proses ke Tier {{ $nextTierName }}</h2>
|
||||
<div class="relative">
|
||||
<div class="flex justify-between mb-2 text-sm text-gray-600">
|
||||
<flux:text>Sekarang: {{ $currentSpending }}</flux:text>
|
||||
<flux:text>Target: {{ $nextTierSpending }}</flux:text>
|
||||
</div>
|
||||
<div class="h-4 bg-white rounded-full overflow-hidden">
|
||||
<div class="h-full bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-full transition-all duration-500"
|
||||
style="width: {{ $progress }}%"></div>
|
||||
</div>
|
||||
<flux:heading class="mt-2">{{ $progress }} %</flux:heading>
|
||||
<flux:heading class="text-4xl! font-bold py-3 ">{{ $stat['value'] }}</flux:heading>
|
||||
<flux:text size="lg">{{ $stat['description'] }}</flux:text>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-200 dark:bg-zinc-700 rounded-xl p-6 shadow-md mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Proses ke Tier {{ $nextTierName }}</h2>
|
||||
<div class="relative">
|
||||
<div class="flex justify-between mb-2 text-sm text-gray-600">
|
||||
<flux:text>Sekarang: {{ $currentSpending }}</flux:text>
|
||||
<flux:text>Target: {{ $nextTierSpending }}</flux:text>
|
||||
</div>
|
||||
<div class="h-4 bg-white rounded-full overflow-hidden">
|
||||
<div class="h-full bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-full transition-all duration-500"
|
||||
style="width: {{ $progress }}%"></div>
|
||||
</div>
|
||||
<flux:heading class="mt-2">{{ $progress }} %</flux:heading>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex justify-center flex-col">
|
||||
<dotlottie-wc src="https://lottie.host/4282c234-6d0f-4a8d-ad04-3d82f2783814/FuhTCVgPGi.lottie" autoplay
|
||||
loop style="width: 200px; height: 200px; margin: 0 auto;">
|
||||
</dotlottie-wc>
|
||||
<flux:text class="block text-center italic mt-5">
|
||||
Apa? Anda belum menjadi member, ayo sini <flux:link href="{{ route('member.overview') }}"
|
||||
wire:navigate>daftar.</flux:link>
|
||||
</flux:text>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</flux:main>
|
||||
@assets
|
||||
|
||||
@ -18,7 +18,8 @@
|
||||
|
||||
<x-slot name="actions">
|
||||
<flux:button href="{{ $whatsappUrl }}" target="_blank">Undang teman</flux:button>
|
||||
<flux:button href="{{ route('member.redeem_point') }}">Ambil Hadiah</flux:button>
|
||||
<flux:button href="{{ route('member.referral') }}" wire:navigate>Riwayat</flux:button>
|
||||
<flux:button href="{{ route('member.redeem_point') }}" wire:navigate>Ambil Hadiah</flux:button>
|
||||
</x-slot>
|
||||
</flux:callout>
|
||||
|
||||
|
||||
80
resources/views/livewire/member/referral.blade.php
Normal file
80
resources/views/livewire/member/referral.blade.php
Normal file
@ -0,0 +1,80 @@
|
||||
<flux:main>
|
||||
<flux:heading size="xl" level="1">Referral</flux:heading>
|
||||
<flux:text class="mb-6 mt-2 text-base">Ajak temanmu bergabung dan dapatkan hadiah menarik dengan kode referralmu.
|
||||
</flux:text>
|
||||
|
||||
<div class="mt-6">
|
||||
<flux:card class="mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<flux:heading size="lg">Kode Referral Anda</flux:heading>
|
||||
<div class="flex items-center gap-2 mt-2" x-data="{ copied: false }">
|
||||
<div
|
||||
class="px-4 py-2 bg-zinc-100 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 font-mono text-xl font-bold tracking-wider">
|
||||
{{ $referralCode->code }}
|
||||
</div>
|
||||
<flux:button x-bind:icon="copied ? 'check' : 'clipboard'"
|
||||
@click="navigator.clipboard.writeText('{{ $referralCode->code }}'); copied = true; setTimeout(() => copied = false, 2000)">
|
||||
<span x-text="copied ? 'Tersalin' : 'Salin'"></span>
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<flux:button href="{{ $whatsappUrl }}" target="_blank" color="emerald" icon="share">
|
||||
Undang Teman via WhatsApp
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:heading size="lg" class="mb-4">Riwayat Penggunaan</flux:heading>
|
||||
|
||||
@if ($usages->isEmpty())
|
||||
<flux:card class="flex flex-col items-center justify-center py-12">
|
||||
@include('components.animations.lottie.not-found', [
|
||||
'message' => 'Belum ada yang menggunakan kode referral Anda. Ayo ajak teman-temanmu!',
|
||||
])
|
||||
</flux:card>
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
@foreach ($usages as $usage)
|
||||
<flux:card>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="md">{{ $usage['name'] }}</flux:heading>
|
||||
<flux:text size="sm">Bergabung pada
|
||||
{{ formatDateLocalized($usage['joined_at'], 'd M Y') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
<div>
|
||||
@if ($usage['is_reward_earned'])
|
||||
<flux:badge color="emerald" inset="top bottom">Target Tercapai</flux:badge>
|
||||
@else
|
||||
<flux:badge color="amber" inset="top bottom">Dalam Proses</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<flux:text>Akumulasi Belanja: {{ formatCurrencyNumber($usage['spending'], 'Rp') }}
|
||||
</flux:text>
|
||||
<flux:text>Target: {{ formatCurrencyNumber($targetSpending, 'Rp') }}</flux:text>
|
||||
</div>
|
||||
<div class="h-2 bg-zinc-100 dark:bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-emerald-500 transition-all duration-500"
|
||||
style="width: {{ $usage['progress'] }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:main>
|
||||
|
||||
@assets
|
||||
<script src="https://unpkg.com/@lottiefiles/dotlottie-wc@0.8.5/dist/dotlottie-wc.js" type="module"></script>
|
||||
@endassets
|
||||
@ -3,8 +3,8 @@
|
||||
use App\Livewire\Member\Membership;
|
||||
use App\Livewire\Member\Overview;
|
||||
use App\Livewire\Member\RedeemPoint;
|
||||
use App\Livewire\Member\Referral;
|
||||
use App\Livewire\Member\Setting\Account;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth', 'verified'])
|
||||
@ -15,5 +15,6 @@
|
||||
|
||||
Route::get('memberships', Membership::class)->name('membership');
|
||||
Route::get('redeem-points', RedeemPoint::class)->name('redeem_point');
|
||||
Route::get('referrals', Referral::class)->name('referral');
|
||||
Route::get('settings', Account::class)->name('setting');
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user