diff --git a/app/Http/Controllers/User/UserController.php b/app/Http/Controllers/User/UserController.php index 63f07cc..7b838c7 100644 --- a/app/Http/Controllers/User/UserController.php +++ b/app/Http/Controllers/User/UserController.php @@ -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, ]); } diff --git a/app/Services/User/Contracts/UserServiceInterface.php b/app/Services/User/Contracts/UserServiceInterface.php index 18c20ce..6982e10 100644 --- a/app/Services/User/Contracts/UserServiceInterface.php +++ b/app/Services/User/Contracts/UserServiceInterface.php @@ -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; } \ No newline at end of file diff --git a/app/Services/User/UserService.php b/app/Services/User/UserService.php index a2ecf26..edcce26 100644 --- a/app/Services/User/UserService.php +++ b/app/Services/User/UserService.php @@ -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'); + } } \ No newline at end of file diff --git a/database/seeders/MockDataSeeder.php b/database/seeders/MockDataSeeder.php index b212d43..069f188 100644 --- a/database/seeders/MockDataSeeder.php +++ b/database/seeders/MockDataSeeder.php @@ -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, - ]); - } } } diff --git a/lang/vi/messages.php b/lang/vi/messages.php index 38c274d..e9efc59 100644 --- a/lang/vi/messages.php +++ b/lang/vi/messages.php @@ -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ư.", ]; diff --git a/resources/views/components/avatar.blade.php b/resources/views/components/avatar.blade.php new file mode 100644 index 0000000..6352af1 --- /dev/null +++ b/resources/views/components/avatar.blade.php @@ -0,0 +1,14 @@ +@props(['user']) + +@php + $name = $user->name ?? ''; + $initials = strtoupper(substr($name, 0, 2)); +@endphp + +@if(isset($user->avatar) && $user->avatar) + merge(['class' => 'w-12 h-12 rounded-full object-cover']) }} /> +@else +
merge(['class' => 'w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 font-bold']) }}> + {{ $initials }} +
+@endif diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 111f141..fa29f47 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -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') { diff --git a/resources/views/user/ranking.blade.php b/resources/views/user/ranking.blade.php index caf6ddf..2270edf 100644 --- a/resources/views/user/ranking.blade.php +++ b/resources/views/user/ranking.blade.php @@ -4,7 +4,7 @@ @section('content')
- {{-- 1. HERO SECTION --}} +

🏆 Bảng xếp hạng

@@ -13,27 +13,27 @@ trophy
- {{-- 4. TAB SWITCHER --}} +
- -
- {{-- CONTENT CONTAINER --}} +
- {{-- ==================== TAB: NHẬN ==================== --}} +
- {{-- 2. TOP 3 PODIUM --}} + @if(!empty($receivedRankers) && count($receivedRankers) >= 3)
- {{-- 2nd Place --}} +
@@ -44,10 +44,10 @@
{{ $receivedRankers[1]->name ?? '---' }} {{ $receivedRankers[1]->department ?? '' }} - {{ $receivedRankers[1]->score ?? 0 }} điểm + {{ $receivedRankers[1]->score ?? 0 }} thẻ
- {{-- 1st Place --}} +
👑
@@ -59,10 +59,10 @@
{{ $receivedRankers[0]->name ?? '---' }} {{ $receivedRankers[0]->department ?? '' }} - {{ $receivedRankers[0]->score ?? 0 }} điểm + {{ $receivedRankers[0]->score ?? 0 }} thẻ
- {{-- 3rd Place --}} +
@@ -73,17 +73,17 @@
{{ $receivedRankers[2]->name ?? '---' }} {{ $receivedRankers[2]->department ?? '' }} - {{ $receivedRankers[2]->score ?? 0 }} điểm + {{ $receivedRankers[2]->score ?? 0 }} thẻ
@endif - {{-- 3. THÀNH TÍCH CỦA BẠN --}} + @if($receivedCurrentUserRankItem)
- You +
#{{ $receivedCurrentUserRankItem->rank }}
@@ -95,7 +95,7 @@

{{ $receivedCurrentUserRankItem->score }}

-

Điểm

+

Thẻ

@endif @@ -127,7 +127,7 @@
{{ $group->first()->score }} - điểm + thẻ
@@ -136,7 +136,7 @@ @foreach($group as $user)
- +

{{ $user->name }}

{{ $user->department }}

@@ -152,12 +152,12 @@
- {{-- ==================== TAB: GỬI ==================== --}} + @@ -260,7 +260,7 @@ @foreach($group as $user)
- +

{{ $user->name }}

{{ $user->department }}

diff --git a/routes/web.php b/routes/web.php index b150299..dea4026 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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'); }); }); diff --git a/tests/Feature/SendThankCardTest.php b/tests/Feature/SendThankCardTest.php index 27ecda1..48ace77 100644 --- a/tests/Feature/SendThankCardTest.php +++ b/tests/Feature/SendThankCardTest.php @@ -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, + ]); + } }