Skip to content

Commit df82b13

Browse files
committed
feat: implement team management page and update team retrieval logic
1 parent de7b69b commit df82b13

6 files changed

Lines changed: 232 additions & 23 deletions

File tree

backend/src/repositories/team.repository.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,31 @@ import userRespository from "./user.repository";
44
import { RoleType } from "~/constants/enums";
55

66
class TeamRepository {
7-
findWithPagination = async ({ page, limit, mentorId }: { page?: number; limit?: number; mentorId?: string }) => {
7+
findWithPagination = async () => {
88
const includeUser = {
99
omit: {
1010
password: true,
1111
candidateId: true,
12+
email: true,
13+
role: true,
14+
createdAt: true,
15+
updatedAt: true,
1216
},
1317
};
1418
const include = {
1519
candidates: {
1620
include: {
1721
user: includeUser,
1822
},
23+
omit: {
24+
phone: true,
25+
// major: true,
26+
semester: true,
27+
mentorNote: true,
28+
createdAt: true,
29+
updatedAt: true,
30+
teamId: true,
31+
},
1932
},
2033
mentorship: {
2134
select: {
@@ -32,14 +45,24 @@ class TeamRepository {
3245
topic: true,
3346
};
3447

35-
const { data, meta } = await paginate<any>(prisma.team, {
36-
page,
37-
limit,
38-
orderBy: { id: "desc" },
48+
// const { data, meta } = await paginate<any>(prisma.team, {
49+
// page,
50+
// limit,
51+
// orderBy: { id: "desc" },
52+
// include,
53+
// omit: {
54+
// mentorNote: true,
55+
// },
56+
// });
57+
const data = prisma.team.findMany({
58+
orderBy: { group: "asc" },
3959
include,
60+
omit: {
61+
mentorNote: true,
62+
},
4063
});
4164

42-
return { teams: data, meta };
65+
return data;
4366
};
4467

4568
findByIdWithMembers = async (id: string, displayScore: boolean = false, role: RoleType) => {

backend/src/services/team.service.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,9 @@ class TeamService {
2121
const safeLimit = Number.isFinite(limitNum) && limitNum > 0 ? limitNum : 10;
2222

2323
const mentorId = role === RoleType.MENTOR ? userId : undefined;
24-
const { teams, meta } = await teamRepository.findWithPagination({
25-
page: safePage,
26-
limit: safeLimit,
27-
mentorId,
28-
});
24+
const data = await teamRepository.findWithPagination();
2925

30-
return {
31-
data: teams,
32-
pagination: {
33-
total: meta.total,
34-
page: meta.page,
35-
limit: meta.limit,
36-
},
37-
};
26+
return data;
3827
}
3928

4029
async getDetail(id: string, role: RoleType) {

frontend/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ import MentorBaremPage from "./pages/Mentor/Barem";
1515
import AdminPage from "./pages/Admin";
1616
import ReportsPage from "./pages/Admin/Reports";
1717
import CandidatePages from "./pages/Admin/Candidates";
18+
import TeamPage from "./pages/Teams";
1819
const App = () => {
1920
return (
2021
<BrowserRouter>
2122
<Routes>
2223
<Route path="/" element={<MainLayout />}>
2324
<Route index element={<IndexPage />} />
2425
<Route path="login" element={<LoginPage />} />
26+
<Route path="teams" element={<TeamPage />} />
2527
<Route path="active/token/:token" element={<ActivePage />} />
2628
<Route path="submissions" element={<SubmissionsPage />} />
2729

frontend/src/api-requests/team.requests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { ResponseDetailData, ResponsePaginate, TeamType } from "~/types/team.types";
1+
import type { ResponseDetailData, TeamType } from "~/types/team.types";
22
import { privateApi } from "~/utils/axiosInstance";
33

44
class TeamApi {
55
static async getAllTeams() {
6-
const res = await privateApi.get<ResponsePaginate<TeamType[]>>("/teams");
6+
const res = await privateApi.get<ResponseDetailData<TeamType[]>>("/teams");
77
return res.data;
88
}
99

frontend/src/pages/Candidate/ChangeNameTeam.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ export function ChangeNameTeam({ team }: { team: TeamType }) {
6060
onChange={(e) => setNewName(e.target.value)}
6161
/>
6262
<span className="text-sm text-red-500 italic">
63-
Lưu ý cần cẩn thận khi bấm xác nhận, bạn không thể đặt lại tên nhóm nữa! Hãy kiểm tra thật kỹ để
64-
tránh bị trùng tên với nhóm khác!
63+
Lưu ý cần cẩn thận khi bấm xác nhận, bạn không thể đặt lại tên nhóm nữa!
6564
</span>
6665
<div className="mt-4 space-y-2">
6766
<p className="text-sm font-medium text-gray-700">Gợi ý tên nhóm:</p>

frontend/src/pages/Teams/index.tsx

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { BookOpen, User, Calendar, Clock } from "lucide-react";
2+
import BadgeLeader from "~/components/BadgeLeader";
3+
import { useQuery } from "@tanstack/react-query";
4+
import TeamApi from "~/api-requests/team.requests";
5+
import Loading from "~/components/Loading";
6+
7+
const TeamPage = () => {
8+
const { data: teams, isLoading } = useQuery({
9+
queryKey: ["teams"],
10+
queryFn: async () => {
11+
const res = await TeamApi.getAllTeams();
12+
return res.result;
13+
},
14+
});
15+
if (isLoading) <Loading />;
16+
17+
return (
18+
<>
19+
<section className="mb-6 sm:mb-8">
20+
<h1 className="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">Danh sách các nhóm</h1>
21+
<p className="mt-2 text-sm text-gray-600">
22+
Xem thông tin các nhóm tham gia Challenge vòng 3. Tổng cộng:{" "}
23+
<span className="text-primary font-semibold">{teams?.length || 0}</span> nhóm
24+
</p>
25+
</section>
26+
27+
<section className="space-y-8">
28+
{teams?.map((team, index) => (
29+
<>
30+
{index > 0 && <div className="border-t border-gray-200/50" />}
31+
<div key={team.id} className="grid grid-cols-1 gap-4 lg:grid-cols-12 lg:gap-2">
32+
<div className="col-span-1 lg:col-span-8">
33+
<div className="overflow-hidden rounded-lg border border-gray-200/70 bg-white shadow-xs transition-all">
34+
<div className="border-b border-gray-200/70 bg-gradient-to-r from-gray-50/80 to-white px-5 py-4 sm:px-6 sm:py-5">
35+
<h2 className="text-base font-semibold tracking-tight text-gray-900 sm:text-lg">
36+
[NHÓM <span className="text-primary font-bold">{team?.group}</span>] -{" "}
37+
{team?.name ? (
38+
<span className="text-primary font-bold">{team.name}</span>
39+
) : (
40+
<span className="text-red-500">Chưa đặt tên nhóm</span>
41+
)}
42+
</h2>
43+
</div>
44+
<div className="overflow-auto">
45+
<table className="w-full">
46+
<thead className="bg-gray-50/50">
47+
<tr>
48+
<th className="px-4 py-3 text-left text-xs font-semibold tracking-wide text-gray-600 uppercase sm:px-6 sm:py-3.5">
49+
STT
50+
</th>
51+
<th className="px-4 py-3 text-left text-xs font-semibold tracking-wide text-gray-600 uppercase sm:px-6 sm:py-3.5">
52+
Họ và tên
53+
</th>
54+
<th className="px-4 py-3 text-left text-xs font-semibold tracking-wide text-gray-600 uppercase sm:px-6 sm:py-3.5">
55+
MSSV
56+
</th>
57+
<th className="px-4 py-3 text-left text-xs font-semibold tracking-wide text-gray-600 uppercase sm:px-6 sm:py-3.5">
58+
Ngành
59+
</th>
60+
</tr>
61+
</thead>
62+
<tbody className="divide-y divide-gray-200/60 bg-white">
63+
{team.candidates?.map((candidate, index) => {
64+
const isLeader = candidate.id === team.leaderId;
65+
return (
66+
<tr
67+
key={candidate.id}
68+
className="transition-colors hover:bg-gray-50/50"
69+
>
70+
<td className="px-4 py-3.5 text-sm font-medium whitespace-nowrap text-gray-900 sm:px-6 sm:py-4">
71+
{index + 1}
72+
</td>
73+
<td
74+
className={`${isLeader ? "font-semibold text-gray-900" : "text-gray-700"} px-4 py-3.5 text-sm whitespace-nowrap sm:px-6 sm:py-4`}
75+
>
76+
<div className="flex items-center gap-2">
77+
{candidate.user.fullName}
78+
{isLeader && <BadgeLeader />}
79+
</div>
80+
</td>
81+
<td className="px-4 py-3.5 text-sm whitespace-nowrap text-gray-600 sm:px-6 sm:py-4">
82+
<p className="text-sm font-semibold text-gray-900">
83+
{candidate.studentCode}
84+
</p>
85+
</td>
86+
<td className="px-4 py-3.5 text-sm text-gray-600 sm:px-6 sm:py-4">
87+
<p className="text-sm text-gray-900">
88+
{candidate.major}
89+
</p>
90+
</td>
91+
</tr>
92+
);
93+
})}
94+
</tbody>
95+
</table>
96+
</div>
97+
</div>
98+
</div>
99+
100+
<div className="col-span-1 space-y-2 lg:col-span-4">
101+
<div className="overflow-hidden rounded-lg border border-gray-200/70 bg-white shadow-xs transition-all">
102+
<div className="border-b border-gray-200/70 bg-gradient-to-br from-gray-50/80 to-white px-5 py-4">
103+
<h3 className="flex items-center gap-2 text-sm font-semibold text-gray-900">
104+
<BookOpen className="text-primary h-4 w-4" />
105+
Thông tin nhóm
106+
</h3>
107+
</div>
108+
<div className="space-y-3 px-5 py-4">
109+
<div className="flex items-start gap-3">
110+
<div className="bg-primary/10 text-primary flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg">
111+
<BookOpen className="h-4 w-4" />
112+
</div>
113+
<div className="flex-1">
114+
<p className="text-xs font-medium text-gray-500">Đề tài</p>
115+
<p className="text-primary mt-0.5 text-sm leading-relaxed font-semibold">
116+
{team.topic?.title || "Chưa có đề tài"}
117+
</p>
118+
{team.topic?.filePath && (
119+
<a
120+
href={team.topic.filePath}
121+
target="_blank"
122+
rel="noopener noreferrer"
123+
className="text-primary mt-1 inline-block text-xs font-medium hover:underline"
124+
>
125+
Xem chi tiết đề tài →
126+
</a>
127+
)}
128+
</div>
129+
</div>
130+
<div className="flex items-start gap-3">
131+
<div className="bg-primary/10 text-primary flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg">
132+
<User className="h-4 w-4" />
133+
</div>
134+
<div className="flex-1">
135+
<p className="text-xs font-medium text-gray-500">Mentor</p>
136+
<p className="text-primary mt-0.5 text-sm font-semibold">
137+
{team.mentorship?.mentor?.fullName || "Chưa có mentor"}
138+
</p>
139+
</div>
140+
</div>
141+
</div>
142+
</div>
143+
144+
<div className="overflow-hidden rounded-lg border border-amber-200/60 bg-gradient-to-br from-amber-50/50 to-white shadow-xs transition-all">
145+
<div className="border-b border-amber-200/60 bg-gradient-to-br from-amber-50/80 to-white px-5 py-4">
146+
<h3 className="flex items-center gap-2 text-sm font-semibold text-gray-900">
147+
<Calendar className="h-4 w-4 text-amber-600" />
148+
Lịch thuyết trình
149+
</h3>
150+
</div>
151+
<div className="space-y-3 px-5 py-4">
152+
<div className="flex items-start gap-3">
153+
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-amber-100/50 text-amber-600">
154+
<Calendar className="h-4 w-4" />
155+
</div>
156+
<div className="flex-1">
157+
<p className="text-xs font-medium text-gray-500">Ngày</p>
158+
<p className="mt-0.5 text-xs font-semibold text-gray-900 italic">
159+
Chưa mở
160+
</p>
161+
</div>
162+
</div>
163+
<div className="flex items-start gap-3">
164+
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-amber-100/50 text-amber-600">
165+
<Clock className="h-4 w-4" />
166+
</div>
167+
<div className="flex-1">
168+
<p className="text-xs font-medium text-gray-500">Giờ</p>
169+
<p className="mt-0.5 text-xs font-semibold text-gray-900 italic">
170+
Chưa mở
171+
</p>
172+
</div>
173+
</div>
174+
<div className="flex items-start gap-3">
175+
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-amber-100/50 text-amber-600">
176+
<BookOpen className="h-4 w-4" />
177+
</div>
178+
<div className="flex-1">
179+
<p className="text-xs font-medium text-gray-500">Địa điểm</p>
180+
<p className="mt-0.5 text-xs font-semibold text-gray-900 italic">
181+
Chưa mở
182+
</p>
183+
</div>
184+
</div>
185+
</div>
186+
</div>
187+
</div>
188+
</div>
189+
</>
190+
))}
191+
</section>
192+
</>
193+
);
194+
};
195+
196+
export default TeamPage;

0 commit comments

Comments
 (0)