Skip to content

Commit 12819c0

Browse files
committed
feat: refactor presentation scheduling to use SchedulePresent model and update related components
1 parent 2f46d6c commit 12819c0

5 files changed

Lines changed: 175 additions & 28 deletions

File tree

backend/prisma/schema.prisma

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ model Team {
5757
topicId String @map("topic_id") @db.VarChar(36)
5858
mentorNote String? @map("mentor_note") @db.Text
5959
60-
mentorship Mentorship @relation("MentorshipToTeam", fields: [mentorshipId], references: [id])
61-
leader Candidate? @relation("TeamLeader", fields: [leaderId], references: [id])
62-
topic Topic @relation(fields: [topicId], references: [id])
63-
candidates Candidate[] @relation("CandidateToTeam")
64-
submissions Submission[]
65-
reports Report[]
66-
presents Present[]
60+
mentorship Mentorship @relation("MentorshipToTeam", fields: [mentorshipId], references: [id])
61+
leader Candidate? @relation("TeamLeader", fields: [leaderId], references: [id])
62+
topic Topic @relation(fields: [topicId], references: [id])
63+
candidates Candidate[] @relation("CandidateToTeam")
64+
submissions Submission[]
65+
reports Report[]
66+
schedulePresents SchedulePresent[]
6767
6868
@@map("teams")
6969
}
@@ -173,20 +173,20 @@ model BaremScore {
173173
@@map("scores")
174174
}
175175

176-
model Present {
176+
model SchedulePresent {
177177
id String @id @default(uuid()) @db.VarChar(36)
178178
teamId String @map("team_id") @db.VarChar(36)
179179
180180
trialDate String @unique @map("trial_date") @db.VarChar(255)
181181
officialDate Json @map("offical_date") @db.Json
182+
finalDate String @unique @map("final_date") @db.VarChar(255)
182183
183184
createdAt DateTime @default(now()) @map("created_at")
184185
updatedAt DateTime @updatedAt @map("updated_at")
185186
186-
// Quan hệ với model Team
187187
team Team @relation(fields: [teamId], references: [id])
188188
189-
@@map("presents")
189+
@@map("schedule_presents")
190190
}
191191

192192
enum Role {

backend/src/repositories/team.repository.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import prisma from "~/configs/prisma";
22
import { paginate } from "~/utils/pagination";
33
import userRespository from "./user.repository";
44
import { RoleType } from "~/constants/enums";
5-
import Present from "~/schemas/present.schema";
5+
import Present from "~/schemas/schedule-present.schema";
6+
import SchedulePresent from "~/schemas/schedule-present.schema";
67

78
class TeamRepository {
89
findWithPagination = async () => {
@@ -239,8 +240,8 @@ class TeamRepository {
239240
};
240241

241242
createPresentationSchedule = async (data: { teamId: string; trialDate: string; officialDate: string[] }) => {
242-
return prisma.present.create({
243-
data: new Present({
243+
return prisma.schedulePresent.create({
244+
data: new SchedulePresent({
244245
teamId: data.teamId,
245246
trialDate: data.trialDate,
246247
officialDate: data.officialDate,
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { v4 as uuidv4 } from "uuid";
22

3-
interface PresentType {
3+
interface SchedulePresentType {
44
id?: string;
55
teamId: string;
66
trialDate: string;
@@ -9,14 +9,14 @@ interface PresentType {
99
updatedAt?: Date;
1010
}
1111

12-
class Present {
12+
class SchedulePresent {
1313
id: string;
1414
teamId: string;
1515
trialDate: string;
1616
officialDate: string[];
1717
createdAt: Date;
1818
updatedAt: Date;
19-
constructor(present: PresentType) {
19+
constructor(present: SchedulePresentType) {
2020
this.id = present.id || uuidv4();
2121
this.teamId = present.teamId;
2222
this.trialDate = present.trialDate;
@@ -26,4 +26,4 @@ class Present {
2626
}
2727
}
2828

29-
export default Present;
29+
export default SchedulePresent;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Calendar, Clock } from "lucide-react";
2+
import {
3+
AlertDialog,
4+
AlertDialogAction,
5+
AlertDialogCancel,
6+
AlertDialogContent,
7+
AlertDialogDescription,
8+
AlertDialogFooter,
9+
AlertDialogHeader,
10+
AlertDialogTitle,
11+
} from "~/components/ui/alert-dialog";
12+
13+
const ConfirmRegister = ({
14+
handleConfirmSubmit,
15+
showConfirmDialog,
16+
setShowConfirmDialog,
17+
trialSlot,
18+
officialSlots,
19+
}: {
20+
handleConfirmSubmit: () => void;
21+
showConfirmDialog: boolean;
22+
setShowConfirmDialog: React.Dispatch<React.SetStateAction<boolean>>;
23+
trialSlot: string;
24+
officialSlots: string[];
25+
}) => {
26+
return (
27+
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
28+
<AlertDialogContent className="max-w-2xl">
29+
<AlertDialogHeader>
30+
<AlertDialogTitle>Xác nhận đăng ký thời gian thuyết trình</AlertDialogTitle>
31+
<AlertDialogDescription>
32+
Kiểm tra lại thông tin đăng ký của bạn trước khi xác nhận.
33+
</AlertDialogDescription>
34+
</AlertDialogHeader>
35+
36+
<div className="space-y-4 pb-4">
37+
<div className="space-y-2">
38+
<div className="flex items-center gap-2">
39+
<Clock className="h-4 w-4 text-blue-500" />
40+
<h4 className="font-semibold text-gray-900">Thuyết trình thử</h4>
41+
</div>
42+
{trialSlot ? (
43+
<div className="rounded-md border-1 border-blue-400 bg-blue-50 p-3">
44+
<div className="flex items-center gap-2 text-sm">
45+
<Calendar className="h-3.5 w-3.5 text-blue-600" />
46+
<span className="font-medium text-blue-900">{trialSlot.split("|")[0]}</span>
47+
<span className="text-blue-600"> - </span>
48+
<span className="text-blue-800">{trialSlot.split("|")[1]}</span>
49+
</div>
50+
</div>
51+
) : (
52+
<div className="">
53+
<p className="text-sm text-gray-500 italic">Không đăng ký</p>
54+
</div>
55+
)}
56+
</div>
57+
58+
<div className="space-y-2">
59+
<div className="flex items-center gap-2">
60+
<Clock className="h-4 w-4 text-green-600" />
61+
<h4 className="font-semibold text-green-700">Thuyết trình chính thức</h4>
62+
</div>
63+
<div className="space-y-2">
64+
{officialSlots.map((slot) => (
65+
<div key={slot} className="bg-primary/10 border-primary rounded-md border-1 p-3">
66+
<div className="flex items-center gap-2 text-sm">
67+
<Calendar className="h-3.5 w-3.5 text-green-600" />
68+
<span className="font-medium text-green-900">{slot.split("|")[0]}</span>
69+
<span className="text-green-600"> - </span>
70+
<span className="text-green-800">{slot.split("|")[1]}</span>
71+
</div>
72+
</div>
73+
))}
74+
</div>
75+
</div>
76+
77+
<div className="rounded-md border border-amber-200 bg-amber-50 p-3">
78+
<p className="text-sm text-amber-800">
79+
<span className="font-semibold">Lưu ý:</span> Sau khi xác nhận, thông tin sẽ được gửi đến
80+
ban tổ chức. Đảm bảo tất cả thành viên có thể tham gia các khung giờ đã chọn.
81+
</p>
82+
</div>
83+
</div>
84+
85+
<AlertDialogFooter>
86+
<AlertDialogCancel>Hủy</AlertDialogCancel>
87+
<AlertDialogAction onClick={handleConfirmSubmit}>Xác nhận</AlertDialogAction>
88+
</AlertDialogFooter>
89+
</AlertDialogContent>
90+
</AlertDialog>
91+
);
92+
};
93+
94+
export default ConfirmRegister;

frontend/src/pages/Present/FormRegisterPresent.tsx

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
import React, { useState } from "react";
2+
import { useMutation } from "@tanstack/react-query";
23
import { Calendar, Clock, Send } from "lucide-react";
34
import { Button } from "~/components/ui/button";
45
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
56
import { Checkbox } from "~/components/ui/checkbox";
67
import { Label } from "~/components/ui/label";
78

9+
import TeamApi from "~/api-requests/team.requests";
10+
import Notification from "~/utils/notification";
11+
import { useAppSelector } from "~/hooks/useRedux";
12+
import type { AxiosError } from "axios";
13+
import ConfirmRegister from "./ConfirmRegister";
14+
815
interface TimeSlots {
916
[date: string]: string[];
1017
}
1118

12-
// Thời gian thuyết trình thử - mỗi ngày 4 slot
1319
const trialTimeSlots: TimeSlots = {
1420
"17/01/2026": ["7:00 - 7:45", "8:00 - 8:45", "9:00 - 9:45", "10:00 - 10:45"],
1521
"18/01/2026": ["7:00 - 7:45", "8:00 - 8:45", "9:00 - 9:45", "10:00 - 10:45"],
1622
"19/01/2026": ["7:00 - 7:45", "8:00 - 8:45", "9:00 - 9:45", "10:00 - 10:45"],
1723
"20/01/2026": ["7:00 - 7:45", "8:00 - 8:45", "9:00 - 9:45", "10:00 - 10:45"],
1824
};
1925

20-
// Thời gian thuyết trình chính thức - mỗi ngày 2 slot
2126
const officialTimeSlots: TimeSlots = {
2227
"17/01/2026": ["13:00 - 13:45", "14:00 - 14:45"],
2328
"18/01/2026": ["13:00 - 13:45", "14:00 - 14:45"],
@@ -26,8 +31,30 @@ const officialTimeSlots: TimeSlots = {
2631
};
2732

2833
const FormRegisterPresent = () => {
34+
const userInfo = useAppSelector((state) => state.user.userInfo);
35+
const teamId = userInfo.candidate?.teamId || "";
36+
2937
const [trialSlot, setTrialSlot] = useState<string>("");
3038
const [officialSlots, setOfficialSlots] = useState<string[]>([]);
39+
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
40+
41+
const registerMutation = useMutation({
42+
mutationFn: (data: { teamId: string; trialDate: string; officialDate: string[] }) =>
43+
TeamApi.createSchedulePresentation(data),
44+
onError: (error: AxiosError<{ message?: string }>) => {
45+
console.log(error);
46+
Notification.error({
47+
text: error.response?.data?.message || "Đăng ký thời gian thuyết trình thất bại!",
48+
});
49+
},
50+
onSuccess: () => {
51+
Notification.success({
52+
text: "Đăng ký thời gian thuyết trình thành công!",
53+
});
54+
setTrialSlot("");
55+
setOfficialSlots([]);
56+
},
57+
});
3158

3259
const handleOfficialSlotChange = (slot: string) => {
3360
setOfficialSlots((prev) => {
@@ -40,11 +67,16 @@ const FormRegisterPresent = () => {
4067

4168
const handleSubmit = async (e: React.FormEvent) => {
4269
e.preventDefault();
43-
console.log({
44-
trialSlot,
45-
officialSlots,
70+
setShowConfirmDialog(true);
71+
};
72+
73+
const handleConfirmSubmit = () => {
74+
registerMutation.mutate({
75+
teamId,
76+
trialDate: trialSlot || "",
77+
officialDate: officialSlots,
4678
});
47-
// TODO: Implement API call
79+
setShowConfirmDialog(false);
4880
};
4981

5082
return (
@@ -70,8 +102,15 @@ const FormRegisterPresent = () => {
70102
</div>
71103
<p className="text-sm text-gray-600">
72104
Chọn <span className="font-semibold">MỘT</span> khung giờ để thuyết trình thử nghiệm và nhận
73-
phản hồi
105+
phản hồi từ mentor.
74106
</p>
107+
<div className="rounded-md border-l-4 border-blue-400 bg-blue-50 p-3">
108+
<p className="text-xs text-blue-800">
109+
<span className="font-semibold">Lưu ý:</span> Chỉ có{" "}
110+
<span className="font-bold">10 slot</span> thuyết trình thử cho toàn bộ các nhóm. Mỗi nhóm
111+
chỉ được chọn 1 khung giờ tham gia.
112+
</p>
113+
</div>
75114

76115
<RadioGroup value={trialSlot} onValueChange={setTrialSlot}>
77116
{Object.entries(trialTimeSlots).map(([date, slots]) => (
@@ -104,7 +143,6 @@ const FormRegisterPresent = () => {
104143

105144
<div className="border-t border-gray-200"></div>
106145

107-
{/* Official Presentation */}
108146
<div className="space-y-4">
109147
<div className="flex items-center gap-2">
110148
<Clock className="h-5 w-5 text-green-600" />
@@ -117,6 +155,13 @@ const FormRegisterPresent = () => {
117155
Chọn <span className="font-semibold text-green-700">NHIỀU</span> khung giờ bạn có thể tham gia
118156
thuyết trình chính thức
119157
</p>
158+
<div className="rounded-md border-l-4 border-green-400 bg-green-50 p-3">
159+
<p className="text-xs text-green-800">
160+
<span className="font-semibold">Lưu ý:</span> Tất cả các nhóm đều được quyền tham gia thuyết
161+
trình chính thức. Vui lòng chọn <span className="font-bold">TẤT CẢ</span> các khung giờ mà{" "}
162+
<span className="font-bold">TẤT CẢ</span> thành viên trong nhóm có thể tham gia được.
163+
</p>
164+
</div>
120165

121166
<div className="space-y-4">
122167
{Object.entries(officialTimeSlots).map(([date, slots]) => (
@@ -147,22 +192,29 @@ const FormRegisterPresent = () => {
147192
</div>
148193
</div>
149194

150-
{/* Submit Button */}
151195
<div className="flex items-center gap-3 border-t border-gray-200/70 pt-5">
152196
<Button
153197
type="submit"
154198
className="group flex items-center gap-2 transition-all hover:shadow-md"
155-
disabled={officialSlots.length === 0}
199+
disabled={officialSlots.length === 0 || registerMutation.isPending}
156200
>
157201
<Send className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
158-
Đăng ký
202+
{registerMutation.isPending ? "Đang đăng ký..." : "Đăng ký"}
159203
</Button>
160204
<p className="text-xs text-gray-500">
161205
<span className="font-semibold text-red-600">Lưu ý:</span> Bạn phải chọn ít nhất 1 khung giờ cho
162206
thuyết trình chính thức
163207
</p>
164208
</div>
165209
</form>
210+
211+
<ConfirmRegister
212+
handleConfirmSubmit={handleConfirmSubmit}
213+
showConfirmDialog={showConfirmDialog}
214+
setShowConfirmDialog={setShowConfirmDialog}
215+
trialSlot={trialSlot}
216+
officialSlots={officialSlots}
217+
/>
166218
</section>
167219
);
168220
};

0 commit comments

Comments
 (0)