m
This commit is contained in:
@@ -61,6 +61,25 @@ class UserController extends Controller
|
||||
return view('user.send', compact('users'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics for a specific recipient.
|
||||
*/
|
||||
public function getRecipientStats(Request $request): JsonResponse
|
||||
{
|
||||
$receiverMsnv = (string) $request->query('receiver');
|
||||
if (empty($receiverMsnv)) {
|
||||
return response()->json(['success' => false, 'message' => 'Receiver is required'], 400);
|
||||
}
|
||||
|
||||
$sender = Auth::user();
|
||||
$sentCount = $this->userService->getSentCardsToUserThisMonth($sender, $receiverMsnv);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'sent_count' => $sentCount,
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeThankcards(SendThankCardRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
@@ -91,10 +110,15 @@ class UserController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$sender = Auth::user();
|
||||
$newRecipientSentCount = $this->userService->getSentCardsToUserThisMonth($sender, (string) $request->input('receiver'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('messages.thank_card_send_success'),
|
||||
'receiver_email' => $receiverUser ? $receiverUser->mail : '',
|
||||
'new_balance' => (int) $sender->card,
|
||||
'new_recipient_sent_count' => $newRecipientSentCount,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,4 +20,6 @@ interface UserServiceInterface
|
||||
public function getRankingList(string $type, string $selectedMonth, User $currentUser): array;
|
||||
|
||||
public function updateProfile(User $user, array $data, ?\Illuminate\Http\UploadedFile $avatarFile = null): void;
|
||||
|
||||
public function getSentCardsToUserThisMonth(User $sender, string $receiverMsnv): int;
|
||||
}
|
||||
@@ -275,4 +275,15 @@ class UserService implements UserServiceInterface
|
||||
|
||||
$user->save();
|
||||
}
|
||||
|
||||
public function getSentCardsToUserThisMonth(User $sender, string $receiverMsnv): int
|
||||
{
|
||||
$startOfMonth = Carbon::now()->startOfMonth();
|
||||
$endOfMonth = Carbon::now()->endOfMonth();
|
||||
|
||||
return (int) Administration::where('msnv', $sender->msnv)
|
||||
->where('receiver', $receiverMsnv)
|
||||
->whereBetween('date', [$startOfMonth, $endOfMonth])
|
||||
->sum('sent');
|
||||
}
|
||||
}
|
||||
@@ -65,23 +65,6 @@ class MockDataSeeder extends Seeder
|
||||
['msnv' => 1005, 'name' => 'Bản Giang', 'mail' => 'bangiang@runsystem.net', 'card' => 36, 'flag' => config('constants.FLAG_SEND_ENABLED'), 'first_login' => config('constants.FIRST_LOGIN_FALSE')],
|
||||
];
|
||||
|
||||
// Generate additional users to reach exactly 100 member users
|
||||
$firstNames = ['Nguyễn', 'Trần', 'Lê', 'Phạm', 'Hoàng', 'Huỳnh', 'Phan', 'Vũ', 'Võ', 'Đặng'];
|
||||
$middleNames = ['Văn', 'Thị', 'Minh', 'Đức', 'Quốc', 'Thanh', 'Hữu', 'Ngọc', 'Quỳnh', 'Thu'];
|
||||
$lastNames = ['Anh', 'Bình', 'Chi', 'Dương', 'Em', 'Giang', 'Huy', 'Khánh', 'Lâm', 'Nam', 'Oanh', 'Phúc', 'Quang', 'Sơn', 'Trang', 'Vy'];
|
||||
|
||||
for ($i = 6; $i <= 100; $i++) {
|
||||
$randomName = $firstNames[array_rand($firstNames)] . ' ' . $middleNames[array_rand($middleNames)] . ' ' . $lastNames[array_rand($lastNames)];
|
||||
$users[] = [
|
||||
'msnv' => 1000 + $i,
|
||||
'name' => $randomName,
|
||||
'mail' => 'dev' . str_pad($i, 3, '0', STR_PAD_LEFT) . '@runsystem.net',
|
||||
'card' => rand(0, 30),
|
||||
'flag' => rand(0, 5) > 0 ? config('constants.FLAG_SEND_ENABLED') : config('constants.FLAG_SEND_DISABLED'),
|
||||
'first_login' => rand(0, 4) == 0 ? config('constants.FIRST_LOGIN_TRUE') : config('constants.FIRST_LOGIN_FALSE')
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($users as $idx => $u) {
|
||||
User::create([
|
||||
'msnv' => $u['msnv'],
|
||||
@@ -95,18 +78,6 @@ class MockDataSeeder extends Seeder
|
||||
'flag_send' => $u['flag'],
|
||||
'first_login' => $u['first_login'],
|
||||
]);
|
||||
|
||||
// For other users, if they have cards, create a matching purchase log in June 2026
|
||||
if (!in_array($u['msnv'], [1001, 1002, 1003, 1005])) {
|
||||
if ($u['card'] > 0) {
|
||||
AddCard::create([
|
||||
'buyer' => $u['msnv'],
|
||||
'num_card' => $u['card'],
|
||||
'seller' => 9999,
|
||||
'date' => '2026-06-01'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inactive User
|
||||
@@ -122,123 +93,5 @@ class MockDataSeeder extends Seeder
|
||||
'flag_send' => config('constants.FLAG_SEND_DISABLED'),
|
||||
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
|
||||
]);
|
||||
|
||||
// 2. CREATE CARD ADDITION LOGS (add_card table)
|
||||
// Manual purchase history for specific users:
|
||||
$addCardLogs = [
|
||||
// 1001: total 30 cards bought
|
||||
['buyer' => 1001, 'num_card' => 15, 'date' => '2026-06-01'],
|
||||
['buyer' => 1001, 'num_card' => 10, 'date' => '2026-07-01'],
|
||||
['buyer' => 1001, 'num_card' => 5, 'date' => '2026-08-01'],
|
||||
|
||||
// 1002: total 15 cards bought
|
||||
['buyer' => 1002, 'num_card' => 10, 'date' => '2026-06-02'],
|
||||
['buyer' => 1002, 'num_card' => 5, 'date' => '2026-07-01'],
|
||||
|
||||
// 1003: total 15 cards bought
|
||||
['buyer' => 1003, 'num_card' => 10, 'date' => '2026-06-03'],
|
||||
['buyer' => 1003, 'num_card' => 5, 'date' => '2026-08-02'],
|
||||
|
||||
// 1005: total 50 cards bought
|
||||
['buyer' => 1005, 'num_card' => 50, 'date' => '2026-06-04'],
|
||||
];
|
||||
|
||||
foreach ($addCardLogs as $log) {
|
||||
AddCard::create([
|
||||
'buyer' => $log['buyer'],
|
||||
'num_card' => $log['num_card'],
|
||||
'seller' => 9999,
|
||||
'date' => $log['date']
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. CREATE CARD SENDING HISTORY (administration table)
|
||||
$transactions = [
|
||||
// June 2026 transactions
|
||||
['sender' => 1001, 'receiver' => 1005, 'amount' => 5, 'date' => '2026-06-05'],
|
||||
['sender' => 1001, 'receiver' => 1002, 'amount' => 3, 'date' => '2026-06-12'],
|
||||
['sender' => 1002, 'receiver' => 1004, 'amount' => 4, 'date' => '2026-06-10'],
|
||||
['sender' => 1003, 'receiver' => 1001, 'amount' => 2, 'date' => '2026-06-15'],
|
||||
['sender' => 1005, 'receiver' => 1002, 'amount' => 5, 'date' => '2026-06-20'],
|
||||
|
||||
// July 2026 transactions
|
||||
['sender' => 1001, 'receiver' => 1002, 'amount' => 5, 'date' => '2026-07-02'],
|
||||
['sender' => 1001, 'receiver' => 1003, 'amount' => 3, 'date' => '2026-07-10'],
|
||||
['sender' => 1002, 'receiver' => 1001, 'amount' => 5, 'date' => '2026-07-15'],
|
||||
['sender' => 1003, 'receiver' => 1004, 'amount' => 4, 'date' => '2026-07-08'],
|
||||
['sender' => 1005, 'receiver' => 1002, 'amount' => 4, 'date' => '2026-07-12'],
|
||||
|
||||
// August 2026 transactions
|
||||
['sender' => 1001, 'receiver' => 1005, 'amount' => 4, 'date' => '2026-08-05'],
|
||||
['sender' => 1002, 'receiver' => 1003, 'amount' => 3, 'date' => '2026-08-12'],
|
||||
['sender' => 1003, 'receiver' => 1001, 'amount' => 3, 'date' => '2026-08-10'],
|
||||
['sender' => 1005, 'receiver' => 1004, 'amount' => 5, 'date' => '2026-08-15'],
|
||||
];
|
||||
|
||||
foreach ($transactions as $t) {
|
||||
// Sender record
|
||||
Administration::create([
|
||||
'msnv' => $t['sender'],
|
||||
'received' => 0,
|
||||
'sender' => null,
|
||||
'sent' => $t['amount'],
|
||||
'receiver' => $t['receiver'],
|
||||
'date' => $t['date']
|
||||
]);
|
||||
|
||||
// Receiver record
|
||||
Administration::create([
|
||||
'msnv' => $t['receiver'],
|
||||
'received' => $t['amount'],
|
||||
'sender' => $t['sender'],
|
||||
'sent' => 0,
|
||||
'receiver' => null,
|
||||
'date' => $t['date']
|
||||
]);
|
||||
}
|
||||
|
||||
// 4. CREATE 10 USERS FOR TIE-RANKING IN CURRENT MONTH
|
||||
$currentMonth = Carbon::now()->format('Y-m');
|
||||
$currentDate = Carbon::now()->format('Y-m-d');
|
||||
$depts = config('constants.DEPARTMENTS');
|
||||
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
$tieMsnv = 8000 + $i;
|
||||
User::create([
|
||||
'msnv' => $tieMsnv,
|
||||
'name' => "Top 1 User {$i}",
|
||||
'mail' => "top1_user_{$i}@runsystem.net",
|
||||
'pass' => $password,
|
||||
'departments' => $depts[($i - 1) % count($depts)],
|
||||
'role' => config('constants.ROLE_MEMBER'),
|
||||
'status' => config('constants.STATUS_ACTIVE'),
|
||||
'card' => 100,
|
||||
'flag_send' => config('constants.FLAG_SEND_ENABLED'),
|
||||
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
|
||||
]);
|
||||
|
||||
$senderMsnv = 8000 + $i;
|
||||
$receiverMsnv = 8000 + ($i % 10 + 1);
|
||||
|
||||
// Sender record
|
||||
Administration::create([
|
||||
'msnv' => $senderMsnv,
|
||||
'received' => 0,
|
||||
'sender' => null,
|
||||
'sent' => 100,
|
||||
'receiver' => $receiverMsnv,
|
||||
'date' => $currentDate,
|
||||
]);
|
||||
|
||||
// Receiver record
|
||||
Administration::create([
|
||||
'msnv' => $receiverMsnv,
|
||||
'received' => 100,
|
||||
'sender' => $senderMsnv,
|
||||
'sent' => 0,
|
||||
'receiver' => null,
|
||||
'date' => $currentDate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ return [
|
||||
'thank_card_send_success' => 'Đã gửi Thank card thành công!',
|
||||
|
||||
'error' => [
|
||||
'cannot_send_card' => "Bạn không thể gửi Thank Card.\nCó thể bạn đã sử dụng hết số lượng Thank Card hoặc chưa được cấp quyền gửi. Vui lòng liên hệ Admin để được hỗ trợ.",
|
||||
'cannot_send_card' => "Bạn không thể gửi Thank Card.\nCó thể bạn đã sử dụng hết số lượng Thank Card hoặc chưa được cấp quyền gửi.\nVui lòng liên hệ Admin để được hỗ trợ.",
|
||||
'max_send_limit_template' => 'Một tháng bạn chỉ được gửi tối đa :max card cho một người. Bạn đã gửi :sent card cho nhân viên này.',
|
||||
'receiver_invalid' => 'Người nhận không tồn tại hoặc đã nghỉ việc.',
|
||||
]
|
||||
],
|
||||
'confirm_send_card' => "Đừng quên hoàn thành Thank Card của bạn nhé!\nChúng mình sẽ gửi thông báo đến người nhận\nđể họ đến khu vực nhận thư.",
|
||||
];
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
@props(['user'])
|
||||
|
||||
@php
|
||||
$name = $user->name ?? '';
|
||||
$initials = strtoupper(substr($name, 0, 2));
|
||||
@endphp
|
||||
|
||||
@if(isset($user->avatar) && $user->avatar)
|
||||
<img src="{{ $user->avatar }}" {{ $attributes->merge(['class' => 'w-12 h-12 rounded-full object-cover']) }} />
|
||||
@else
|
||||
<div {{ $attributes->merge(['class' => 'w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 font-bold']) }}>
|
||||
{{ $initials }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -153,6 +153,19 @@
|
||||
window.showToast(flashError, 'error');
|
||||
localStorage.removeItem('flash_error');
|
||||
}
|
||||
|
||||
// Auto prevent double submit for all standard form submissions
|
||||
document.querySelectorAll('form').forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitButtons = form.querySelectorAll('button[type="submit"], input[type="submit"]');
|
||||
submitButtons.forEach(btn => {
|
||||
setTimeout(() => {
|
||||
btn.disabled = true;
|
||||
btn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
window.showToast = function(message, type = 'success') {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-[1280px] mx-auto w-full pb-16 px-4 lg:px-8">
|
||||
{{-- 1. HERO SECTION --}}
|
||||
|
||||
<div class="relative overflow-hidden rounded-[32px] mb-10 shadow-[0_8px_32px_rgba(0,0,0,0.04)]" style="background: linear-gradient(135deg, #f5f7ff 0%, #e0e7ff 100%);">
|
||||
<x-hero-banner>
|
||||
<h2 class="text-[32px] md:text-[42px] font-extrabold text-[#1a2b49] mb-3 drop-shadow-sm">🏆 Bảng xếp hạng</h2>
|
||||
@@ -13,27 +13,27 @@
|
||||
<img src="{{ asset('images/illustrations/trophy.svg') }}" alt="trophy" class="absolute right-4 bottom-0 w-40 h-40 md:w-56 md:h-56 opacity-20 pointer-events-none">
|
||||
</div>
|
||||
|
||||
{{-- 4. TAB SWITCHER --}}
|
||||
|
||||
<div class="flex justify-center mb-10">
|
||||
<div class="inline-flex bg-[#F5F7FB] rounded-[20px] p-1.5 shadow-inner border border-gray-100">
|
||||
<button id="tab-btn-received" onclick="switchRankingTab('received')" class="tab-btn bg-white shadow-[0_4px_12px_rgba(0,0,0,0.05)] text-primary px-8 py-3 rounded-[16px] font-bold text-[15px] transition-all duration-300 flex items-center gap-2">
|
||||
<button id="tab-btn-received" onclick="switchRankingTab('received')" class="tab-btn bg-white shadow-[0_4px_12px_rgba(0,0,0,0.05)] text-primary px-10 py-4 rounded-[20px] font-bold text-[18px] transition-all duration-300 flex items-center gap-2">
|
||||
<span class="text-xl">📥</span> TOP Nhận
|
||||
</button>
|
||||
<button id="tab-btn-sent" onclick="switchRankingTab('sent')" class="tab-btn text-gray-500 hover:text-gray-800 hover:bg-gray-50 px-8 py-3 rounded-[16px] font-bold text-[15px] transition-all duration-300 flex items-center gap-2">
|
||||
<button id="tab-btn-sent" onclick="switchRankingTab('sent')" class="tab-btn text-gray-500 hover:text-gray-800 hover:bg-gray-50 px-10 py-4 rounded-[20px] font-bold text-[18px] transition-all duration-300 flex items-center gap-2">
|
||||
<span class="text-xl">📤</span> TOP Gửi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- CONTENT CONTAINER --}}
|
||||
|
||||
<div id="ranking-content-container">
|
||||
|
||||
{{-- ==================== TAB: NHẬN ==================== --}}
|
||||
|
||||
<div id="content-received" class="tab-content block animate-fade-in">
|
||||
{{-- 2. TOP 3 PODIUM --}}
|
||||
|
||||
@if(!empty($receivedRankers) && count($receivedRankers) >= 3)
|
||||
<div class="flex justify-center items-end gap-4 md:gap-10 mb-12 pt-8">
|
||||
{{-- 2nd Place --}}
|
||||
|
||||
<div class="flex flex-col items-center group relative z-10">
|
||||
<div class="relative w-24 h-24 md:w-28 md:h-28 mb-4 transition-transform duration-300 group-hover:-translate-y-2">
|
||||
<div class="absolute inset-0 bg-gray-200 rounded-full animate-pulse opacity-20"></div>
|
||||
@@ -44,10 +44,10 @@
|
||||
</div>
|
||||
<span class="font-bold text-[#1a2b49] text-center text-sm md:text-base">{{ $receivedRankers[1]->name ?? '---' }}</span>
|
||||
<span class="text-xs md:text-sm text-gray-500 text-center">{{ $receivedRankers[1]->department ?? '' }}</span>
|
||||
<span class="text-sm font-extrabold text-primary mt-1">{{ $receivedRankers[1]->score ?? 0 }} điểm</span>
|
||||
<span class="text-sm font-extrabold text-primary mt-1">{{ $receivedRankers[1]->score ?? 0 }} thẻ</span>
|
||||
</div>
|
||||
|
||||
{{-- 1st Place --}}
|
||||
|
||||
<div class="flex flex-col items-center group relative z-20">
|
||||
<div class="absolute -top-10 text-4xl animate-bounce">👑</div>
|
||||
<div class="relative w-32 h-32 md:w-40 md:h-40 mb-4 transition-transform duration-300 group-hover:-translate-y-3">
|
||||
@@ -59,10 +59,10 @@
|
||||
</div>
|
||||
<span class="font-extrabold text-[#1a2b49] text-center text-lg md:text-xl">{{ $receivedRankers[0]->name ?? '---' }}</span>
|
||||
<span class="text-sm text-gray-500 text-center">{{ $receivedRankers[0]->department ?? '' }}</span>
|
||||
<span class="text-lg font-extrabold text-yellow-500 mt-1">{{ $receivedRankers[0]->score ?? 0 }} điểm</span>
|
||||
<span class="text-lg font-extrabold text-yellow-500 mt-1">{{ $receivedRankers[0]->score ?? 0 }} thẻ</span>
|
||||
</div>
|
||||
|
||||
{{-- 3rd Place --}}
|
||||
|
||||
<div class="flex flex-col items-center group relative z-10">
|
||||
<div class="relative w-24 h-24 md:w-28 md:h-28 mb-4 transition-transform duration-300 group-hover:-translate-y-2">
|
||||
<div class="absolute inset-0 bg-orange-200 rounded-full animate-pulse opacity-20"></div>
|
||||
@@ -73,17 +73,17 @@
|
||||
</div>
|
||||
<span class="font-bold text-[#1a2b49] text-center text-sm md:text-base">{{ $receivedRankers[2]->name ?? '---' }}</span>
|
||||
<span class="text-xs md:text-sm text-gray-500 text-center">{{ $receivedRankers[2]->department ?? '' }}</span>
|
||||
<span class="text-sm font-extrabold text-primary mt-1">{{ $receivedRankers[2]->score ?? 0 }} điểm</span>
|
||||
<span class="text-sm font-extrabold text-primary mt-1">{{ $receivedRankers[2]->score ?? 0 }} thẻ</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 3. THÀNH TÍCH CỦA BẠN --}}
|
||||
|
||||
@if($receivedCurrentUserRankItem)
|
||||
<div class="max-w-xl mx-auto mb-12 bg-white rounded-[24px] p-5 shadow-[0_8px_24px_rgba(52,98,247,0.08)] border border-blue-100 flex items-center justify-between transform transition-all hover:scale-[1.02]">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<img src="{{ $receivedCurrentUserRankItem->avatar ?? asset('images/avatar.png') }}" alt="You" class="w-16 h-16 rounded-full object-cover border-2 border-blue-100">
|
||||
<x-avatar :user="$receivedCurrentUserRankItem" class="w-16 h-16" />
|
||||
<div class="absolute -bottom-2 -right-2 bg-primary w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold border-2 border-white shadow-sm">
|
||||
#{{ $receivedCurrentUserRankItem->rank }}
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-3xl font-black text-primary">{{ $receivedCurrentUserRankItem->score }}</p>
|
||||
<p class="text-xs font-medium text-gray-500 uppercase">Điểm</p>
|
||||
<p class="text-xs font-medium text-gray-500 uppercase">Thẻ</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -127,7 +127,7 @@
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-primary font-black text-2xl">{{ $group->first()->score }}</span>
|
||||
<span class="text-sm font-bold text-gray-400">điểm</span>
|
||||
<span class="text-sm font-bold text-gray-400">thẻ</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
@foreach($group as $user)
|
||||
<div class="group bg-white rounded-[16px] p-4 flex items-center gap-4 transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_12px_24px_rgba(0,0,0,0.06)] border border-gray-100 cursor-default relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-blue-50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<img src="{{ $user->avatar ?? asset('images/avatar.png') }}" class="w-12 h-12 rounded-full object-cover shadow-sm relative z-10">
|
||||
<x-avatar :user="$user" class="w-12 h-12" />
|
||||
<div class="flex-1 min-w-0 relative z-10">
|
||||
<p class="font-bold text-[#1a2b49] truncate group-hover:text-primary transition-colors">{{ $user->name }}</p>
|
||||
<p class="text-xs text-gray-500 truncate">{{ $user->department }}</p>
|
||||
@@ -152,12 +152,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ==================== TAB: GỬI ==================== --}}
|
||||
|
||||
<div id="content-sent" class="tab-content hidden animate-fade-in">
|
||||
{{-- 2. TOP 3 PODIUM --}}
|
||||
|
||||
@if(!empty($sentRankers) && count($sentRankers) >= 3)
|
||||
<div class="flex justify-center items-end gap-4 md:gap-10 mb-12 pt-8">
|
||||
{{-- 2nd Place --}}
|
||||
|
||||
<div class="flex flex-col items-center group relative z-10">
|
||||
<div class="relative w-24 h-24 md:w-28 md:h-28 mb-4 transition-transform duration-300 group-hover:-translate-y-2">
|
||||
<div class="absolute inset-0 bg-gray-200 rounded-full animate-pulse opacity-20"></div>
|
||||
@@ -168,10 +168,10 @@
|
||||
</div>
|
||||
<span class="font-bold text-[#1a2b49] text-center text-sm md:text-base">{{ $sentRankers[1]->name ?? '---' }}</span>
|
||||
<span class="text-xs md:text-sm text-gray-500 text-center">{{ $sentRankers[1]->department ?? '' }}</span>
|
||||
<span class="text-sm font-extrabold text-primary mt-1">{{ $sentRankers[1]->score ?? 0 }} điểm</span>
|
||||
<span class="text-sm font-extrabold text-primary mt-1">{{ $sentRankers[1]->score ?? 0 }} thẻ</span>
|
||||
</div>
|
||||
|
||||
{{-- 1st Place --}}
|
||||
|
||||
<div class="flex flex-col items-center group relative z-20">
|
||||
<div class="absolute -top-10 text-4xl animate-bounce">👑</div>
|
||||
<div class="relative w-32 h-32 md:w-40 md:h-40 mb-4 transition-transform duration-300 group-hover:-translate-y-3">
|
||||
@@ -183,10 +183,10 @@
|
||||
</div>
|
||||
<span class="font-extrabold text-[#1a2b49] text-center text-lg md:text-xl">{{ $sentRankers[0]->name ?? '---' }}</span>
|
||||
<span class="text-sm text-gray-500 text-center">{{ $sentRankers[0]->department ?? '' }}</span>
|
||||
<span class="text-lg font-extrabold text-yellow-500 mt-1">{{ $sentRankers[0]->score ?? 0 }} điểm</span>
|
||||
<span class="text-lg font-extrabold text-yellow-500 mt-1">{{ $sentRankers[0]->score ?? 0 }} thẻ</span>
|
||||
</div>
|
||||
|
||||
{{-- 3rd Place --}}
|
||||
|
||||
<div class="flex flex-col items-center group relative z-10">
|
||||
<div class="relative w-24 h-24 md:w-28 md:h-28 mb-4 transition-transform duration-300 group-hover:-translate-y-2">
|
||||
<div class="absolute inset-0 bg-orange-200 rounded-full animate-pulse opacity-20"></div>
|
||||
@@ -197,17 +197,17 @@
|
||||
</div>
|
||||
<span class="font-bold text-[#1a2b49] text-center text-sm md:text-base">{{ $sentRankers[2]->name ?? '---' }}</span>
|
||||
<span class="text-xs md:text-sm text-gray-500 text-center">{{ $sentRankers[2]->department ?? '' }}</span>
|
||||
<span class="text-sm font-extrabold text-primary mt-1">{{ $sentRankers[2]->score ?? 0 }} điểm</span>
|
||||
<span class="text-sm font-extrabold text-primary mt-1">{{ $sentRankers[2]->score ?? 0 }} thẻ</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 3. THÀNH TÍCH CỦA BẠN --}}
|
||||
|
||||
@if($sentCurrentUserRankItem)
|
||||
<div class="max-w-xl mx-auto mb-12 bg-white rounded-[24px] p-5 shadow-[0_8px_24px_rgba(52,98,247,0.08)] border border-blue-100 flex items-center justify-between transform transition-all hover:scale-[1.02]">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<img src="{{ $sentCurrentUserRankItem->avatar ?? asset('images/avatar.png') }}" alt="You" class="w-16 h-16 rounded-full object-cover border-2 border-blue-100">
|
||||
<x-avatar :user="$sentCurrentUserRankItem" class="w-16 h-16" />
|
||||
<div class="absolute -bottom-2 -right-2 bg-primary w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold border-2 border-white shadow-sm">
|
||||
#{{ $sentCurrentUserRankItem->rank }}
|
||||
</div>
|
||||
@@ -219,7 +219,7 @@
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-3xl font-black text-primary">{{ $sentCurrentUserRankItem->score }}</p>
|
||||
<p class="text-xs font-medium text-gray-500 uppercase">Điểm</p>
|
||||
<p class="text-xs font-medium text-gray-500 uppercase">Thẻ</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -251,7 +251,7 @@
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-primary font-black text-2xl">{{ $group->first()->score }}</span>
|
||||
<span class="text-sm font-bold text-gray-400">điểm</span>
|
||||
<span class="text-sm font-bold text-gray-400">thẻ</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
@foreach($group as $user)
|
||||
<div class="group bg-white rounded-[16px] p-4 flex items-center gap-4 transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_12px_24px_rgba(0,0,0,0.06)] border border-gray-100 cursor-default relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-purple-50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<img src="{{ $user->avatar ?? asset('images/avatar.png') }}" class="w-12 h-12 rounded-full object-cover shadow-sm relative z-10">
|
||||
<x-avatar :user="$user" class="w-12 h-12" />
|
||||
<div class="flex-1 min-w-0 relative z-10">
|
||||
<p class="font-bold text-[#1a2b49] truncate group-hover:text-primary transition-colors">{{ $user->name }}</p>
|
||||
<p class="text-xs text-gray-500 truncate">{{ $user->department }}</p>
|
||||
|
||||
@@ -49,6 +49,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::view('/stats', 'coming-soon')->name('user.stats');
|
||||
|
||||
Route::get('/send-thankcards', [UserController::class, 'sendThankcards'])->name('user.send');
|
||||
Route::get('/send-thankcards/recipient-stats', [UserController::class, 'getRecipientStats'])->name('user.recipient_stats');
|
||||
Route::post('/send-thankcards', [UserController::class, 'storeThankcards'])->name('user.store_send');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,5 +115,29 @@ class SendThankCardTest extends TestCase
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_get_recipient_stats_returns_correct_sent_count(): void
|
||||
{
|
||||
$this->actingAs($this->sender);
|
||||
|
||||
// Before sending, stats should return 0 sent cards
|
||||
$response = $this->getJson(route('user.recipient_stats', ['receiver' => '5002']));
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson([
|
||||
'success' => true,
|
||||
'sent_count' => 0,
|
||||
]);
|
||||
|
||||
// Send a card to the receiver
|
||||
$this->userService->sendThankcards($this->sender, '5002', 3);
|
||||
|
||||
// Now it should return 3
|
||||
$response = $this->getJson(route('user.recipient_stats', ['receiver' => '5002']));
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson([
|
||||
'success' => true,
|
||||
'sent_count' => 3,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user