feature: main page - code be

This commit is contained in:
antv
2026-07-06 16:07:35 +07:00
parent e1fd70071b
commit 618dcef37f
9 changed files with 249 additions and 30 deletions
+13 -1
View File
@@ -21,8 +21,20 @@ class UserController extends Controller
$user = Auth::user();
$administrations = $this->userService->getDashboard($user, $selectedMonth);
$stats = $this->userService->getPersonalStats($user, $selectedMonth);
return view('user.dashboard', compact('administrations', 'selectedMonth', 'user'));
$receivedRanking = $this->userService->getRankingList('received', $selectedMonth, $user);
$sentRanking = $this->userService->getRankingList('sent', $selectedMonth, $user);
return view('user.dashboard', array_merge([
'administrations' => $administrations,
'selectedMonth' => $selectedMonth,
'user' => $user,
'receivedRankers' => $receivedRanking['rankers'],
'receivedCurrentUserRankItem' => $receivedRanking['currentUserRankItem'],
'sentRankers' => $sentRanking['rankers'],
'sentCurrentUserRankItem' => $sentRanking['currentUserRankItem']
], $stats));
}
public function sendThankcards()
+10
View File
@@ -23,4 +23,14 @@ class Administration extends Model
'receiver',
'date',
];
public function senderUser()
{
return $this->belongsTo(User::class, 'sender', 'msnv');
}
public function receiverUser()
{
return $this->belongsTo(User::class, 'receiver', 'msnv');
}
}
+10
View File
@@ -69,4 +69,14 @@ class User extends Authenticatable
{
return '';
}
public function transactions()
{
return $this->hasMany(Administration::class, 'msnv', 'msnv');
}
public function cardPurchases()
{
return $this->hasMany(AddCard::class, 'buyer', 'msnv');
}
}
@@ -14,4 +14,8 @@ interface UserServiceInterface
public function sendThankcards(User $sender, string $receiverMsnv, int $amount): void;
public function updatePassword(User $user, string $newPassword): void;
public function getPersonalStats(User $user, string $selectedMonth): array;
public function getRankingList(string $type, string $selectedMonth, User $currentUser): array;
}
+98
View File
@@ -94,4 +94,102 @@ class UserService implements UserServiceInterface
$user->first_login = User::FIRST_LOGIN_FALSE;
$user->save();
}
public function getPersonalStats(User $user, string $selectedMonth): array
{
$startOfMonth = Carbon::parse($selectedMonth)->startOfMonth();
$endOfMonth = Carbon::parse($selectedMonth)->endOfMonth();
$receivedCount = (int) Administration::where('msnv', $user->msnv)
->whereBetween('date', [$startOfMonth, $endOfMonth])
->sum('received');
$sentCount = (int) Administration::where('msnv', $user->msnv)
->whereBetween('date', [$startOfMonth, $endOfMonth])
->sum('sent');
// Calculate current rank (based on received)
$rankings = User::where('status', User::STATUS_ACTIVE)
->addSelect([
'score' => Administration::selectRaw('COALESCE(SUM(received), 0)')
->whereColumn('msnv', 'user.msnv')
->whereBetween('date', [$startOfMonth, $endOfMonth])
])
->orderByDesc('score')
->get();
$currentRank = 0;
$prevScore = null;
$rank = 1;
$denseRank = 0;
foreach ($rankings as $item) {
$score = (int) $item->score;
if ($score !== $prevScore) {
$denseRank = $rank;
$prevScore = $score;
}
if ($item->msnv == $user->msnv) {
$currentRank = $denseRank;
break;
}
$rank++;
}
return [
'receivedCount' => $receivedCount,
'sentCount' => $sentCount,
'currentRank' => $currentRank
];
}
public function getRankingList(string $type, string $selectedMonth, User $currentUser): array
{
$startOfMonth = Carbon::parse($selectedMonth)->startOfMonth();
$endOfMonth = Carbon::parse($selectedMonth)->endOfMonth();
$column = $type === 'sent' ? 'sent' : 'received';
$users = User::where('status', User::STATUS_ACTIVE)
->addSelect([
'score' => Administration::selectRaw('COALESCE(SUM(' . $column . '), 0)')
->whereColumn('msnv', 'user.msnv')
->whereBetween('date', [$startOfMonth, $endOfMonth])
])
->orderByDesc('score')
->get();
$prevScore = null;
$rank = 1;
$denseRank = 0;
$rankers = [];
$currentUserRankItem = null;
foreach ($users as $index => $userItem) {
$score = (int) $userItem->score;
if ($score !== $prevScore) {
$denseRank = $rank;
$prevScore = $score;
}
$userItem->rank = $denseRank;
$userItem->score = $score;
if ($userItem->msnv == $currentUser->msnv) {
$currentUserRankItem = $userItem;
}
if ($index < 5) {
$rankers[] = $userItem;
}
$rank++;
}
return [
'rankers' => $rankers,
'currentUserRankItem' => $currentUserRankItem
];
}
}
+13 -25
View File
@@ -19,7 +19,7 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<x-stat-card
title="Thanks đã nhận"
value="128"
value="{{ $receivedCount }}"
iconBgClass="bg-purple-50"
iconColorClass="text-purple-500">
<svg class="w-[26px] h-[26px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
@@ -29,7 +29,7 @@
<x-stat-card
title="Thanks đã gửi"
value="82"
value="{{ $sentCount }}"
iconBgClass="bg-blue-50"
iconColorClass="text-blue-500">
<svg class="w-[26px] h-[26px] transform -rotate-45" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
@@ -40,7 +40,7 @@
<x-stat-card
title="Thứ hạng"
value="#5"
value="#{{ $currentRank > 0 ? $currentRank : '-' }}"
iconBgClass="bg-green-50"
iconColorClass="text-green-500">
<svg class="w-[26px] h-[26px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
@@ -119,26 +119,14 @@
</button>
</div>
<div id="ranking-received" class="flex-1 flex flex-col mb-2">
<x-ranking-item rank="1" name="Lê Quỳnh Chi" team="Marketing Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed=Chi&backgroundColor=fef3c7" score="95" unit="thẻ" />
<x-ranking-item rank="2" name="Trần Hoàng Nam" team="Dev Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed=Nam&backgroundColor=e2e8f0" score="82" unit="thẻ" />
<x-ranking-item rank="3" name="Phạm Đức Huy" team="Product Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed=Huy&backgroundColor=e2e8f0" score="67" unit="thẻ" />
<x-ranking-item rank="4" name="Hoàng Anh Tuấn" team="Dev Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed=Tuan&backgroundColor=e2e8f0" score="58" unit="thẻ" />
<div class="my-3 border-t border-dashed border-gray-100 mx-2"></div>
<x-ranking-item rank="7" name="{{ Auth::check() ? (Auth::user()->name ?? 'Nguyễn Minh Anh') : 'Nguyễn Minh Anh' }}" team="Product Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed={{ Auth::id() ?? 'MinhAnh' }}&backgroundColor=bfdbfe" score="45" unit="thẻ" :isCurrent="true" />
</div>
<div id="ranking-sent" class="flex-1 flex flex-col mb-2 hidden">
<x-ranking-item rank="1" name="Nguyễn Văn Lâm" team="HR Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed=Lam&backgroundColor=fef3c7" score="88" unit="thẻ" />
<x-ranking-item rank="2" name="Phạm Thu Thủy" team="Admin Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed=Thuy&backgroundColor=e2e8f0" score="75" unit="thẻ" />
<x-ranking-item rank="3" name="Lê Quỳnh Chi" team="Marketing Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed=Chi&backgroundColor=e2e8f0" score="64" unit="thẻ" />
<x-ranking-item rank="4" name="Đỗ Thùy Linh" team="Marketing Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed=Linh&backgroundColor=e2e8f0" score="55" unit="thẻ" />
<div class="my-3 border-t border-dashed border-gray-100 mx-2"></div>
<x-ranking-item rank="12" name="{{ Auth::check() ? (Auth::user()->name ?? 'Nguyễn Minh Anh') : 'Nguyễn Minh Anh' }}" team="Product Team" avatar="https://api.dicebear.com/7.x/notionists/svg?seed={{ Auth::id() ?? 'MinhAnh' }}&backgroundColor=bfdbfe" score="18" unit="thẻ" :isCurrent="true" />
<div id="ranking-list-container" class="flex-1 flex flex-col mb-2">
<!-- Pre-rendered ranking lists -->
<div id="ranking-received-content" class="flex-1 flex flex-col">
@include('user.partials.ranking_list', ['rankers' => $receivedRankers, 'currentUserRankItem' => $receivedCurrentUserRankItem])
</div>
<div id="ranking-sent-content" class="hidden flex-1 flex flex-col">
@include('user.partials.ranking_list', ['rankers' => $sentRankers, 'currentUserRankItem' => $sentCurrentUserRankItem])
</div>
</div>
</div>
</div>
@@ -151,8 +139,8 @@
function switchRankingTab(tab) {
const receivedTab = document.getElementById('tab-received');
const sentTab = document.getElementById('tab-sent');
const receivedContent = document.getElementById('ranking-received');
const sentContent = document.getElementById('ranking-sent');
const receivedContent = document.getElementById('ranking-received-content');
const sentContent = document.getElementById('ranking-sent-content');
const activeClass = 'flex items-center justify-center gap-2 w-[120px] py-2 rounded-[10px] transition-all bg-white shadow-[0_2px_4px_rgba(0,0,0,0.04)] text-[#3462f7] font-bold text-[14px]';
const inactiveClass = 'flex items-center justify-center gap-2 w-[120px] py-2 rounded-[10px] transition-all text-[#64748b] hover:text-gray-800 font-bold text-[14px]';
@@ -0,0 +1,45 @@
@if(count($rankers) > 0)
@foreach($rankers as $item)
<x-ranking-item
rank="{{ $item->rank }}"
name="{{ $item->name }}"
team="{{ match((int) $item->departments) {
1 => 'Dev Team',
2 => 'HR Team',
3 => 'Sale Team',
4 => 'Board Team',
5 => 'Marketing Team',
default => 'Team'
} }}"
avatar="{{ $item->avatar ?? 'https://api.dicebear.com/7.x/notionists/svg?seed=' . urlencode($item->name) . '&backgroundColor=bfdbfe' }}"
score="{{ $item->score }}"
unit="thẻ"
:isCurrent="$item->msnv == Auth::user()->msnv"
/>
@endforeach
@if($currentUserRankItem && $currentUserRankItem->rank > 5)
<div class="my-3 border-t border-dashed border-gray-100 mx-2"></div>
<x-ranking-item
rank="{{ $currentUserRankItem->rank }}"
name="{{ $currentUserRankItem->name }}"
team="{{ match((int) $currentUserRankItem->departments) {
1 => 'Dev Team',
2 => 'HR Team',
3 => 'Sale Team',
4 => 'Board Team',
5 => 'Marketing Team',
default => 'Team'
} }}"
avatar="{{ $currentUserRankItem->avatar ?? 'https://api.dicebear.com/7.x/notionists/svg?seed=' . urlencode($currentUserRankItem->name) . '&backgroundColor=bfdbfe' }}"
score="{{ $currentUserRankItem->score }}"
unit="thẻ"
:isCurrent="true"
/>
@endif
@else
<div class="py-12 text-center">
<span class="text-[36px]">🏆</span>
<p class="text-gray-400 font-medium text-[13px] mt-2">Chưa xếp hạng cho tháng này</p>
</div>
@endif
+1 -4
View File
@@ -38,10 +38,7 @@ Route::middleware('auth')->group(function () {
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
Route::middleware('force_change_password')->group(function () {
Route::middleware('member')->group(function () {
Route::middleware('member')->group(function () {
Route::get('/my-page', [UserController::class, 'index'])->name('user.dashboard');
// Coming soon routes for new tabs
+55
View File
@@ -37,4 +37,59 @@ class UserDashboardLayoutTest extends TestCase
$response->assertSee('lg:min-w-[340px]', false);
$response->assertSee('lg:max-w-[400px]', false);
}
public function test_dashboard_renders_ranking_list_from_database(): void
{
$user1 = \App\Models\User::create([
'msnv' => 5001,
'name' => 'User A',
'mail' => 'usera@runsystem.net',
'pass' => md5('password'),
'departments' => 1,
'role' => \App\Models\User::ROLE_MEMBER,
'status' => \App\Models\User::STATUS_ACTIVE,
'card' => 10,
'flag_send' => \App\Models\User::FLAG_SEND_ENABLED,
'first_login' => \App\Models\User::FIRST_LOGIN_FALSE,
]);
$user2 = \App\Models\User::create([
'msnv' => 5002,
'name' => 'User B',
'mail' => 'userb@runsystem.net',
'pass' => md5('password'),
'departments' => 2,
'role' => \App\Models\User::ROLE_MEMBER,
'status' => \App\Models\User::STATUS_ACTIVE,
'card' => 10,
'flag_send' => \App\Models\User::FLAG_SEND_ENABLED,
'first_login' => \App\Models\User::FIRST_LOGIN_FALSE,
]);
\App\Models\Administration::create([
'msnv' => 5002,
'received' => 0,
'sender' => null,
'sent' => 5,
'receiver' => 5001,
'date' => \Carbon\Carbon::now()->format('Y-m-d')
]);
\App\Models\Administration::create([
'msnv' => 5001,
'received' => 5,
'sender' => 5002,
'sent' => 0,
'receiver' => null,
'date' => \Carbon\Carbon::now()->format('Y-m-d')
]);
$this->actingAs($user1);
$response = $this->get('/my-page');
$response->assertOk();
$response->assertSee('User A', false);
$response->assertSee('User B', false);
}
}