feature: main page - code be
This commit is contained in:
@@ -21,8 +21,20 @@ class UserController extends Controller
|
|||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
$administrations = $this->userService->getDashboard($user, $selectedMonth);
|
$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()
|
public function sendThankcards()
|
||||||
|
|||||||
@@ -23,4 +23,14 @@ class Administration extends Model
|
|||||||
'receiver',
|
'receiver',
|
||||||
'date',
|
'date',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function senderUser()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'sender', 'msnv');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function receiverUser()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'receiver', 'msnv');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,4 +69,14 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return '';
|
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 sendThankcards(User $sender, string $receiverMsnv, int $amount): void;
|
||||||
|
|
||||||
public function updatePassword(User $user, string $newPassword): 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;
|
||||||
}
|
}
|
||||||
@@ -94,4 +94,102 @@ class UserService implements UserServiceInterface
|
|||||||
$user->first_login = User::FIRST_LOGIN_FALSE;
|
$user->first_login = User::FIRST_LOGIN_FALSE;
|
||||||
$user->save();
|
$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
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
<x-stat-card
|
<x-stat-card
|
||||||
title="Thanks đã nhận"
|
title="Thanks đã nhận"
|
||||||
value="128"
|
value="{{ $receivedCount }}"
|
||||||
iconBgClass="bg-purple-50"
|
iconBgClass="bg-purple-50"
|
||||||
iconColorClass="text-purple-500">
|
iconColorClass="text-purple-500">
|
||||||
<svg class="w-[26px] h-[26px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
|
<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
|
<x-stat-card
|
||||||
title="Thanks đã gửi"
|
title="Thanks đã gửi"
|
||||||
value="82"
|
value="{{ $sentCount }}"
|
||||||
iconBgClass="bg-blue-50"
|
iconBgClass="bg-blue-50"
|
||||||
iconColorClass="text-blue-500">
|
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">
|
<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
|
<x-stat-card
|
||||||
title="Thứ hạng"
|
title="Thứ hạng"
|
||||||
value="#5"
|
value="#{{ $currentRank > 0 ? $currentRank : '-' }}"
|
||||||
iconBgClass="bg-green-50"
|
iconBgClass="bg-green-50"
|
||||||
iconColorClass="text-green-500">
|
iconColorClass="text-green-500">
|
||||||
<svg class="w-[26px] h-[26px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
|
<svg class="w-[26px] h-[26px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
|
||||||
@@ -119,26 +119,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="ranking-received" class="flex-1 flex flex-col mb-2">
|
<div id="ranking-list-container" 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ẻ" />
|
<!-- Pre-rendered ranking lists -->
|
||||||
<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ẻ" />
|
<div id="ranking-received-content" class="flex-1 flex flex-col">
|
||||||
<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ẻ" />
|
@include('user.partials.ranking_list', ['rankers' => $receivedRankers, 'currentUserRankItem' => $receivedCurrentUserRankItem])
|
||||||
<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>
|
||||||
|
<div id="ranking-sent-content" class="hidden flex-1 flex flex-col">
|
||||||
<div class="my-3 border-t border-dashed border-gray-100 mx-2"></div>
|
@include('user.partials.ranking_list', ['rankers' => $sentRankers, 'currentUserRankItem' => $sentCurrentUserRankItem])
|
||||||
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,8 +139,8 @@
|
|||||||
function switchRankingTab(tab) {
|
function switchRankingTab(tab) {
|
||||||
const receivedTab = document.getElementById('tab-received');
|
const receivedTab = document.getElementById('tab-received');
|
||||||
const sentTab = document.getElementById('tab-sent');
|
const sentTab = document.getElementById('tab-sent');
|
||||||
const receivedContent = document.getElementById('ranking-received');
|
const receivedContent = document.getElementById('ranking-received-content');
|
||||||
const sentContent = document.getElementById('ranking-sent');
|
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 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]';
|
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 có xếp hạng cho tháng này</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
+1
-4
@@ -38,10 +38,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
||||||
|
|
||||||
Route::middleware('force_change_password')->group(function () {
|
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');
|
Route::get('/my-page', [UserController::class, 'index'])->name('user.dashboard');
|
||||||
|
|
||||||
// Coming soon routes for new tabs
|
// Coming soon routes for new tabs
|
||||||
|
|||||||
@@ -37,4 +37,59 @@ class UserDashboardLayoutTest extends TestCase
|
|||||||
$response->assertSee('lg:min-w-[340px]', false);
|
$response->assertSee('lg:min-w-[340px]', false);
|
||||||
$response->assertSee('lg:max-w-[400px]', 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user