diff --git a/messages/en/agent/en_agent.json b/messages/en/agent/en_agent.json index 32ec52931..0c683f730 100644 --- a/messages/en/agent/en_agent.json +++ b/messages/en/agent/en_agent.json @@ -30,7 +30,22 @@ "result": "RESULT", "uploading": "Đang tải model…", "reset": "Reset the AI recognition", - "video": "Video is zoomed in for better viewing." + "video": "Video is zoomed in for better viewing.", + "status": { + "minImagesPerClass": "At least {minImagesPerClass} images per class are required to train a good model!\nCurrent: {totalImages} images\nNeeded: {missingImage} images", + "notBalance": "Data is unbalanced! Each class needs at least {minImage} images.", + "dataPreparation": "Preparing data...", + "createModel": "Creating model...", + "trainModel": "Training model...", + "trainSuccess": "Model has been trained successfully!", + "trainError": "Error during model training: {error}", + "readyDownload": "Preparing model download...", + "downloadSuccess": "Model has been downloaded successfully! Includes: model.json, weights.bin, model-info.json, labels.json (ZIP).", + "downloadError": "Error while downloading model: {error}", + "imageAnalysing": "Analyzing image...", + "imageAnalyseFail": "Error analyzing image: {error}", + "predict": "Prediction: " + } } } } diff --git a/messages/en/classroom/en_classroom.json b/messages/en/classroom/en_classroom.json index a9eb40e7e..27ccd46c4 100644 --- a/messages/en/classroom/en_classroom.json +++ b/messages/en/classroom/en_classroom.json @@ -42,12 +42,13 @@ "backToClassrooms": "Back to Classrooms", "header": "Classroom Details", "selected": "Selected", + "grade": "Grade", "students": { "label": "Students", "noStudent": "No students enrolled yet.", "noStudentSubtext": "Start building your class by adding students" }, - "courses": "Courses", + "course": "Course", "curriculum": { "label": "Curriculum" }, @@ -99,6 +100,12 @@ "asmTotal": "Total Assignments", "submitted": "submitted" } + }, + "myLearning": { + "title": "My Classrooms", + "noClassroom": "You are not enrolled in any classrooms yet.", + "noClassroomSubtext": "Explore available classrooms and start learning today!", + "grade": "Grade" } } } diff --git a/messages/en/common/en_common.json b/messages/en/common/en_common.json index b299db6ce..10e06a34d 100644 --- a/messages/en/common/en_common.json +++ b/messages/en/common/en_common.json @@ -109,7 +109,13 @@ "addQuestion": "Add Question", "addCriterion": "Add Criterion", "createGroup": "Create Group", - "cancelSubscription": "Cancel Subscription" + "cancelSubscription": "Cancel Subscription", + "removeFromGroup": "Remove from Group", + "backToClassroomList": "Back to Classroom List", + "continueLearning": "Continue Learning", + "markAsComplete": "Mark as Complete", + "startQuiz": "Start Quiz", + "contact": "Contact Us" }, "message": { "courseCreateSuccess": "Course created successfully!", @@ -189,7 +195,10 @@ "numberOfLessons": "No. Lessons", "accountType": "Account Type", "assignedDate": "Assigned Date", - "course": "Course" + "course": "Course", + "joinedAt": "Joined At", + "subscription": "Subscription", + "student": "Student(s)" }, "paging": { "previous": "Previous", @@ -233,6 +242,10 @@ "inprogress": "In Progress", "locked": "Locked" }, + "orgUserStatus": { + "active": "Active", + "inactive": "Inactive" + }, "level": { "all": "All Levels", "beginner": "Beginner", diff --git a/messages/en/organization/en_organization.json b/messages/en/organization/en_organization.json index d54dd03b9..74801d551 100644 --- a/messages/en/organization/en_organization.json +++ b/messages/en/organization/en_organization.json @@ -209,6 +209,7 @@ "numberOfStudents": "Number of Students: {quantity}", "groupList": "Group List", "createdDate": "Created Date", + "updatedAt": "Updated Date", "totalStudents": "Total Students", "attendance": "Attendance", "step1": { diff --git a/messages/en/product/en_plan.json b/messages/en/product/en_plan.json index c5056fcfe..66f103daf 100644 --- a/messages/en/product/en_plan.json +++ b/messages/en/product/en_plan.json @@ -64,6 +64,16 @@ "recommended": "RECOMMENDED", "choosePlanBtn": "Choose Plan", "month": "Month" + }, + "user": { + "title": "Plans & Pricing", + "subTitle": "Flexible Plans", + "des1": "Whether your time-saving automation needs are large or small, we're here to help you scale.", + "des2": "Choose the perfect plan for your team and unlock unlimited potential.", + "annual": "Annual", + "semiAnnual": "SemiAnnual", + "popular": "Most Popular", + "support": "Support & Access" } } } diff --git a/messages/en/user/en_myLearning.json b/messages/en/user/en_myLearning.json index b26dfc224..6004648dc 100644 --- a/messages/en/user/en_myLearning.json +++ b/messages/en/user/en_myLearning.json @@ -1,5 +1,5 @@ { - "MyLearning": { + "myLearning": { "title": "Your Learning Journey", "subtitle": "Continue your learning journey with these courses", "description": "Continue learning and developing your skills with your enrolled courses.", diff --git a/messages/vi/agent/vi_agent.json b/messages/vi/agent/vi_agent.json index e1b771e37..b7fea3c65 100644 --- a/messages/vi/agent/vi_agent.json +++ b/messages/vi/agent/vi_agent.json @@ -30,7 +30,22 @@ "result": "KẾT QUẢ", "uploading": "Đang tải mô hình…", "reset": "Đặt lại quá trình nhận diện AI", - "video": "Video được phóng to để dễ quan sát hơn." + "video": "Video được phóng to để dễ quan sát hơn.", + "status": { + "minImagesPerClass": "Cần ít nhất {minImagesPerClass} ảnh cho mỗi class để train model tốt!\nHiện tại: {totalImages} ảnh\nCần: {missingImage} ảnh", + "notBalance": "Dữ liệu không cân bằng! Cần ít nhất {minImage} ảnh cho mỗi class.", + "dataPreparation": "Đang chuẩn bị dữ liệu...", + "createModel": "Đang tạo model...", + "trainModel": "Đang train model...", + "trainSuccess": "Model đã được train thành công!", + "trainError": "Lỗi khi train model: {error}", + "readyDownload": "Đang chuẩn bị tải xuống model...", + "downloadSuccess": "Model đã được tải xuống thành công! Bao gồm: model.json, weights.bin, model-info.json, labels.json (ZIP).", + "downloadError": "Lỗi khi tải xuống model: {error}", + "imageAnalysing": "Đang phân tích ảnh...", + "imageAnalyseFail": "Lỗi khi phân tích ảnh: {error}", + "predict": "Dự đoán: " + } } } } diff --git a/messages/vi/classroom/vi_classroom.json b/messages/vi/classroom/vi_classroom.json index 2d5e14f33..3a6ba6b4d 100644 --- a/messages/vi/classroom/vi_classroom.json +++ b/messages/vi/classroom/vi_classroom.json @@ -41,12 +41,13 @@ "backToClassrooms": "Quay lại Lớp học", "header": "Chi tiết lớp học", "selected": "Đã chọn", + "grade": "Khối", "students": { "label": "Học sinh", "noStudent": "Chưa có học sinh nào.", "noStudentSubtext": "Bắt đầu xây dựng lớp học của bạn bằng cách thêm học sinh" }, - "courses": "Khóa học", + "course": "Khóa học", "curriculum": { "label": "Chương trình học" }, @@ -57,7 +58,7 @@ }, "teacher": "Giáo viên", "meet": { - "label": "Họp", + "label": "Cuộc Họp", "joinButton": "Tham gia cuộc họp" }, "quickStats": { @@ -98,6 +99,12 @@ "asmTotal": "Tổng số Bài Tập", "submitted": "đã nộp" } + }, + "myLearning": { + "title": "Lớp học của tôi", + "noClassroom": "Bạn chưa tham gia lớp học nào.", + "noClassroomSubtext": "Khám phá các lớp học có sẵn và bắt đầu học ngay hôm nay!", + "grade": "Khối" } } } diff --git a/messages/vi/common/vi_common.json b/messages/vi/common/vi_common.json index 5f8c00763..748c43460 100644 --- a/messages/vi/common/vi_common.json +++ b/messages/vi/common/vi_common.json @@ -15,6 +15,7 @@ "camera": "Mở Camera", "ready": "Sẵn sàng", "connect": "Kết nối", + "disconnect": "Ngắt kết nối", "update": "Cập Nhật", "browse": "Tải Lên", "delete": "Xóa", @@ -66,7 +67,7 @@ "exploreRobotAi": "Khám Phá Robot AI", "readBlogs": "Xem Blog", "addKit": "Thêm Bộ Kit", - "publish": "Xuất Bản", + "publish": "Công khai", "print": "In", "share": "Chia sẻ", "start": "Bắt Đầu", @@ -108,7 +109,13 @@ "addQuestion": "Thêm Câu Hỏi", "addCriterion": "Thêm Tiêu Chí", "createGroup": "Tạo Nhóm", - "cancelSubscription": "Hủy Đăng Ký" + "cancelSubscription": "Hủy Đăng Ký", + "removeFromGroup": "Xóa khỏi Nhóm", + "backToClassroomList": "Quay lại Danh sách Lớp học", + "continueLearning": "Tiếp tục Học", + "markAsComplete": "Đánh dấu hoàn thành", + "startQuiz": "Bắt Đầu Bài Kiểm Tra", + "contact": "Liên Hệ Ngay" }, "message": { "courseCreateSuccess": "Khóa học được tạo thành công!", @@ -187,7 +194,10 @@ "numberOfLessons": "Số Bài Học", "accountType": "Loại Tài Khoản", "assignedDate": "Ngày Gán", - "course": "Khóa Học" + "course": "Khóa Học", + "joinedAt": "Ngày tham gia", + "subscription": "Gói đăng ký", + "student": "Học sinh" }, "paging": { "previous": "Trước", @@ -237,6 +247,10 @@ "inprogress": "Đang Diễn Ra", "locked": "Khóa" }, + "orgUserStatus": { + "active": "Đã xác thực", + "inactive": "Chưa xác thực" + }, "accountType": { "accountTypeLabel": "Loại Tài Khoản", "admin": "Quản Trị Viên", diff --git a/messages/vi/common/vi_toast.json b/messages/vi/common/vi_toast.json index e36ebc6c7..e31d816cd 100644 --- a/messages/vi/common/vi_toast.json +++ b/messages/vi/common/vi_toast.json @@ -15,7 +15,7 @@ "removeCourseFromCurriculum": "Đã xóa khóa học khỏi chương trình học thành công!", "lessonStart": "Bài học đã bắt đầu!", "lessonStatus": "Trạng thái bài học đã được cập nhật thành {status}!", - "sectionComplete": "Phần đã hoàn thành!", + "sectionComplete": "Đã hoàn thành!", "addComponentToKit": "Đã thêm thành phần vào bộ dụng cụ thành công!", "removeComponent": "Đã xóa thành phần khỏi bộ dụng cụ thành công!", "updateComponentInKit": "Đã cập nhật thành phần bộ dụng cụ thành công!", diff --git a/messages/vi/organization/vi_organization.json b/messages/vi/organization/vi_organization.json index 97665832f..d105108e5 100644 --- a/messages/vi/organization/vi_organization.json +++ b/messages/vi/organization/vi_organization.json @@ -208,6 +208,7 @@ "numberOfStudents": "Số lượng: {quantity} học sinh", "groupList": "Danh sách học sinh", "createdDate": "Ngày tạo", + "updatedAt": "Ngày cập nhật", "totalStudents": "Tổng số học sinh", "attendance": "Tham gia", "step1": { diff --git a/messages/vi/product/vi_plan.json b/messages/vi/product/vi_plan.json index 719b3bf0f..6d9bc1933 100644 --- a/messages/vi/product/vi_plan.json +++ b/messages/vi/product/vi_plan.json @@ -63,6 +63,16 @@ "recommended": "ĐỀ XUẤT", "choosePlanBtn": "Chọn gói", "month": "Tháng" + }, + "user": { + "title": "Gói & Giá", + "subTitle": "Gói linh hoạt", + "des1": "Dù nhu cầu tự động hóa của bạn lớn hay nhỏ, chúng tôi luôn sẵn sàng hỗ trợ bạn phát triển.", + "des2": "Chọn gói phù hợp nhất cho đội của bạn và mở khóa tiềm năng không giới hạn.", + "annual": "Hàng năm", + "semiAnnual": "Nửa năm", + "popular": "Phổ biến nhất", + "support": "Hỗ trợ & Quyền truy cập" } } } diff --git a/messages/vi/user/vi_myLearning.json b/messages/vi/user/vi_myLearning.json index 732a478e0..f50a8d2db 100644 --- a/messages/vi/user/vi_myLearning.json +++ b/messages/vi/user/vi_myLearning.json @@ -1,5 +1,5 @@ { - "MyLearning": { + "myLearning": { "title": "Hành Trình Học Tập Của Bạn", "subtitle": "Tiếp tục hành trình học tập của bạn với các khóa học này", "description": "Tiếp tục học hỏi và phát triển kỹ năng với các khóa học bạn đã đăng ký.", diff --git a/next.config.ts b/next.config.ts index 0965534b3..8209fdb08 100644 --- a/next.config.ts +++ b/next.config.ts @@ -9,6 +9,7 @@ const nextConfig: NextConfig = { { protocol: 'https', hostname: 'github.com' }, { protocol: 'https', hostname: 'encrypted-tbn0.gstatic.com' }, { protocol: 'https', hostname: 'classroom.strawbees.com' }, + { protocol: 'https', hostname: 'strawbees.com' }, { protocol: 'http', hostname: 'res.cloudinary.com' }, { protocol: 'https', hostname: 'res.cloudinary.com' } ], diff --git a/package-lock.json b/package-lock.json index 7b173aee0..adf27485e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,7 @@ "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", "lucide-react": "^0.511.0", - "next": "^15.5.5", + "next": "^15.5.7", "next-auth": "^4.24.11", "next-intl": "^4.3.4", "next-themes": "^0.4.6", @@ -1391,9 +1391,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.5.tgz", - "integrity": "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1407,9 +1407,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.5.tgz", - "integrity": "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1423,9 +1423,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.5.tgz", - "integrity": "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -1439,9 +1439,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.5.tgz", - "integrity": "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -1455,9 +1455,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.5.tgz", - "integrity": "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -1471,9 +1471,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.5.tgz", - "integrity": "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -1487,9 +1487,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.5.tgz", - "integrity": "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -1503,9 +1503,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.5.tgz", - "integrity": "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1519,9 +1519,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.5.tgz", - "integrity": "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -11210,12 +11210,12 @@ } }, "node_modules/next": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.5.tgz", - "integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.5", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -11228,14 +11228,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.5", - "@next/swc-darwin-x64": "15.5.5", - "@next/swc-linux-arm64-gnu": "15.5.5", - "@next/swc-linux-arm64-musl": "15.5.5", - "@next/swc-linux-x64-gnu": "15.5.5", - "@next/swc-linux-x64-musl": "15.5.5", - "@next/swc-win32-arm64-msvc": "15.5.5", - "@next/swc-win32-x64-msvc": "15.5.5", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { diff --git a/package.json b/package.json index a702c22c6..4c02cec30 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", "lucide-react": "^0.511.0", - "next": "^15.5.5", + "next": "^15.5.7", "next-auth": "^4.24.11", "next-intl": "^4.3.4", "next-themes": "^0.4.6", diff --git a/src/app/[locale]/classroom/[classroomId]/page.tsx b/src/app/[locale]/classroom/[classroomId]/page.tsx index 6f7dd763f..d1f224fe0 100644 --- a/src/app/[locale]/classroom/[classroomId]/page.tsx +++ b/src/app/[locale]/classroom/[classroomId]/page.tsx @@ -19,7 +19,7 @@ export type ClassroomNavItems = 'overview' | 'course' | 'quiz' | 'assignment' | export default function ClassroomDetailPage() { const { classroomId } = useParams() - const auth = useAppSelector((state) => state.auth) + const { selectedOrgUserId } = useAppSelector((state) => state.selectedOrganization) const currentRole = useAppSelector((state) => state.selectedOrganization.currentRole) const [currentTab, setCurrentTab] = React.useState('overview') @@ -27,12 +27,12 @@ export default function ClassroomDetailPage() { const { data: courseEnrollment } = useSearchCourseEnrollmentQuery( { courseId: classroomData?.data.course.id, - studentId: auth?.user?.userId || '', + studentId: selectedOrgUserId!, classroomId: Number(classroomId), pageNumber: 1, pageSize: 20 }, - { skip: !auth.user?.userId || !classroomData?.data.course.id || currentRole !== LicenseType.STUDENT } + { skip: !selectedOrgUserId || !classroomData?.data.course.id || currentRole !== LicenseType.STUDENT } ) if (isLoading) { return ( @@ -50,7 +50,7 @@ export default function ClassroomDetailPage() { {currentTab === 'overview' && currentRole === LicenseType.TEACHER ? : null} {currentTab === 'overview' && currentRole === LicenseType.STUDENT ? ( - + ) : null} {currentTab === 'course' ? (
@@ -69,7 +69,7 @@ export default function ClassroomDetailPage() { ) : null} {currentTab === 'student' ? (
- +
) : null}
diff --git a/src/app/[locale]/lab/layout.tsx b/src/app/[locale]/lab/layout.tsx index ef2956339..42fc40ae5 100644 --- a/src/app/[locale]/lab/layout.tsx +++ b/src/app/[locale]/lab/layout.tsx @@ -15,7 +15,7 @@ export default async function CodeLab({ return (
-
{children}
+
{children}
) } diff --git a/src/app/[locale]/lab/workspace-3d/page.tsx b/src/app/[locale]/lab/workspace-3d/page.tsx index 0586edad5..9de69addc 100644 --- a/src/app/[locale]/lab/workspace-3d/page.tsx +++ b/src/app/[locale]/lab/workspace-3d/page.tsx @@ -3,7 +3,7 @@ import React from 'react' export default function Workspace3DLibraryPage() { return ( -
+
) diff --git a/src/app/[locale]/organization/group/[groupId]/page.tsx b/src/app/[locale]/organization/group/[groupId]/page.tsx index c5bfc1bbb..c635feb52 100644 --- a/src/app/[locale]/organization/group/[groupId]/page.tsx +++ b/src/app/[locale]/organization/group/[groupId]/page.tsx @@ -1,10 +1,10 @@ -import OrganizationGroupDetail from '@/features/group/components/detail/OrganizationGroupDetail' +import OrganizationGroupTable from '@/features/group/components/detail/OrganizationGroupTableDetail' import React from 'react' export default function GroupDetailPage() { return (
- +
) } diff --git a/src/components/shared/card/CardHorizontal.tsx b/src/components/shared/card/CardHorizontal.tsx index b975a2c28..e73fc905b 100644 --- a/src/components/shared/card/CardHorizontal.tsx +++ b/src/components/shared/card/CardHorizontal.tsx @@ -46,7 +46,7 @@ export default function CardHorizontal({ {/* Description */} - {description &&

{description}

} + {description &&

{description}

} {/* CTA button */} {/* )} + {/* TODO */} - + */}
diff --git a/src/features/classroom/components/detail/StudentClassroomDetails.tsx b/src/features/classroom/components/detail/StudentClassroomDetails.tsx index f4ce58ef1..cadffa88e 100644 --- a/src/features/classroom/components/detail/StudentClassroomDetails.tsx +++ b/src/features/classroom/components/detail/StudentClassroomDetails.tsx @@ -6,36 +6,15 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/shadcn/ca import { Badge } from '@/components/shadcn/badge' import { Button } from '@/components/shadcn/button' import { Avatar, AvatarFallback, AvatarImage } from '@/components/shadcn/avatar' -import { Separator } from '@/components/shadcn/separator' -import { - Calendar, - Users, - BookOpen, - Copy, - Settings, - UserPlus, - MoreVertical, - ArrowLeft, - Clock, - GraduationCap, - Mail, - Edit -} from 'lucide-react' -import { format } from 'date-fns' -import { ClassroomStatus } from '@/features/classroom/types/classroom.type' +import { Users, BookOpen, Copy, MoreVertical, Mail, Camera, Video } from 'lucide-react' import Link from 'next/link' import Image from 'next/image' -import { getStatusBadgeClass } from '@/utils/badgeColor' import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks' -import { LicenseType, UserRole } from '@/types/userRole' -import { - useCreateCurriculumEnrollmentMutation, - useSearchCurriculumEnrollmentQuery -} from '@/features/enrollment/api/curriculumEnrollmentApi' + import { useRouter } from 'next/navigation' import { useLocale, useTranslations } from 'next-intl' import { signIn } from 'next-auth/react' -import { CourseEnrollment, CurriculumEnrollment, EnrollmentStatus } from '@/features/enrollment/types/enrollment.type' +import { CourseEnrollment, EnrollmentStatus } from '@/features/enrollment/types/enrollment.type' import { toast } from 'sonner' import { ClassroomNavItems } from 'app/[locale]/classroom/[classroomId]/page' import { setCourseEnrollmentId } from '@/features/enrollment/slice/enrollmentSlice' @@ -43,13 +22,13 @@ import { useCreateCourseEnrollmentMutation } from '@/features/enrollment/api/cou export type StudentClassroomDetailProps = { courseEnrollment?: CourseEnrollment - setCurrentTab: (tab: ClassroomNavItems) => void } -export default function StudentClassroomDetail({ courseEnrollment, setCurrentTab }: StudentClassroomDetailProps) { +export default function StudentClassroomDetail({ courseEnrollment }: StudentClassroomDetailProps) { const tc = useTranslations('common') const tt = useTranslations('toast') + const tClassroom = useTranslations('classroom') const { classroomId } = useParams() - const auth = useAppSelector((state) => state.auth) + const { selectedOrgUserId } = useAppSelector((state) => state.selectedOrganization) const router = useRouter() const locale = useLocale() const dispatch = useAppDispatch() @@ -62,18 +41,18 @@ export default function StudentClassroomDetail({ courseEnrollment, setCurrentTab const copyClassCode = () => { if (classroom?.classCode) { navigator.clipboard.writeText(classroom.classCode) - // You can add a toast notification here + toast.success(tt('successMessage.copiedToClipboard')) } } const handleEnroll = () => { - if (!auth.user?.userId) { + if (!selectedOrgUserId) { signIn('oidc', { callbackUrl: `/`, prompt: 'login' }) return } if (classroom?.course.id) { createEnrollment({ courseId: classroom?.course.id, - studentId: auth?.user?.userId, + studentId: selectedOrgUserId, status: EnrollmentStatus.IN_PROGRESS, classroomId: Number(classroomId) }) @@ -105,10 +84,10 @@ export default function StudentClassroomDetail({ courseEnrollment, setCurrentTab return (
-

Classroom not found

-

The classroom you're looking for doesn't exist.

+

{tClassroom('detail.notFound')}

+

{tClassroom('detail.notFoundSubtext')}

- +
@@ -128,7 +107,7 @@ export default function StudentClassroomDetail({ courseEnrollment, setCurrentTab - Course + {tClassroom('detail.course')} @@ -159,7 +138,7 @@ export default function StudentClassroomDetail({ courseEnrollment, setCurrentTab router.push(`/resource/course/${classroom.course.id}/learn`) }} > - Continue Learning + {tc('button.continueLearning')} ) : ( -

Share this code with students to join the class

+

{tClassroom('detail.classCode.description')}

@@ -242,7 +221,7 @@ export default function StudentClassroomDetail({ courseEnrollment, setCurrentTab {classroom.teacher && ( - Teacher + {tClassroom('detail.teacher')}
@@ -275,30 +254,15 @@ export default function StudentClassroomDetail({ courseEnrollment, setCurrentTab
- - - +
- Meet + {tClassroom('detail.meet.label')}
-
- -
- - - - Visible to students -
diff --git a/src/features/classroom/components/list/ClassroomList.tsx b/src/features/classroom/components/list/ClassroomList.tsx index 25a659ee4..1d7d93716 100644 --- a/src/features/classroom/components/list/ClassroomList.tsx +++ b/src/features/classroom/components/list/ClassroomList.tsx @@ -5,7 +5,6 @@ import { ClassroomStatus } from '@/features/classroom/types/classroom.type' import { Badge } from '@/components/shadcn/badge' import { Card, CardContent } from '@/components/shadcn/card' import { Users, BookOpen, Clock, GraduationCap } from 'lucide-react' -import { format } from 'date-fns' import React, { useState } from 'react' import { getStatusBadgeClass } from '@/utils/badgeColor' import Link from 'next/link' @@ -16,25 +15,25 @@ import SearchBar from '@/components/shared/search/SearchBar' import SSelect from '@/components/shared/SSelect' import { useLocale, useTranslations } from 'next-intl' -import { formatDate } from '@/utils/index' +import { formatDate, useStatusTranslation } from '@/utils/index' export default function ClassroomList() { - const tClassroom = useTranslations('classroom') const locale = useLocale() + const statusTranslations = useStatusTranslation() + const tClassroom = useTranslations('classroom.myLearning') + + const { selectedOrgUserId } = useAppSelector((state) => state.selectedOrganization) - const user = useAppSelector((state) => state.auth?.user) const queryParams = useAppSelector((state) => state.classroom) const [selectedStatus, setSelectedStatus] = useState('') - const classroomQueryParams = { - ...queryParams, - status: selectedStatus || undefined - } - - const { data, isLoading, error } = useSearchClassroomsQuery({ - ...queryParams, - studentId: user?.userId - }) + const { data, isLoading, error } = useSearchClassroomsQuery( + { + ...queryParams, + studentId: selectedOrgUserId + }, + { skip: !selectedOrgUserId } + ) const classrooms = data?.data.items || [] if (isLoading) { @@ -50,7 +49,7 @@ export default function ClassroomList() { if (error || !classrooms || classrooms.length === 0) { return (
- +
) } @@ -97,7 +96,7 @@ export default function ClassroomList() { {/* Status Badge */}
- {classroom.status} + {statusTranslations(classroom.status)}
@@ -108,7 +107,7 @@ export default function ClassroomList() {

{classroom.name}

- {classroom.grade} + {tClassroom('grade')} {classroom.grade}
diff --git a/src/features/classroom/components/ui/ClassroomSubHeader.tsx b/src/features/classroom/components/ui/ClassroomSubHeader.tsx index 38e77e50d..d92ebbf4b 100644 --- a/src/features/classroom/components/ui/ClassroomSubHeader.tsx +++ b/src/features/classroom/components/ui/ClassroomSubHeader.tsx @@ -22,7 +22,6 @@ interface Props { export default function ClassroomSubHeader({ classroom, curriculumId, currentTab, setCurrentTab }: Props) { const t = useTranslations('Header') - const pathname = usePathname() const subNavItems: { name: string; currentTab: ClassroomNavItems }[] = [ { name: 'overview', currentTab: 'overview' }, diff --git a/src/features/classroom/components/upsert/CreateClassroom.tsx b/src/features/classroom/components/upsert/CreateClassroom.tsx index c4e9b6a7a..a3c03f6af 100644 --- a/src/features/classroom/components/upsert/CreateClassroom.tsx +++ b/src/features/classroom/components/upsert/CreateClassroom.tsx @@ -19,7 +19,7 @@ import { CalendarIcon } from 'lucide-react' import { format } from 'date-fns' import { cn } from '@/utils/shadcn/utils' import BackButton from '@/components/shared/button/BackButton' -import GroupTable from '@/features/group/components/list/GroupTable' +import GroupTableWithTeacher from '@/features/group/components/list/GroupTableWithTeacher' import { Grade } from '@/features/classroom/types/classroom.type' type ClassroomFormData = { @@ -192,7 +192,7 @@ export default function CreateClassroom() { - setSelectedGroups(groups)} /> + setSelectedGroups(groups)} /> {/* Basic Information Section */} @@ -253,15 +253,17 @@ export default function CreateClassroom() { { const today = new Date() today.setHours(0, 0, 0, 0) - const effectiveMinDate = minDate && minDate > today ? minDate : today - return date < effectiveMinDate || (maxDate ? date > maxDate : false) + return date < today }} - initialFocus + autoFocus /> @@ -286,15 +288,7 @@ export default function CreateClassroom() { - { - return (minDate ? date < minDate : false) || (maxDate ? date > maxDate : false) - }} - initialFocus - /> + diff --git a/src/features/creator-3d/components/creator3d/Creator3D.tsx b/src/features/creator-3d/components/creator3d/Creator3D.tsx index 771c19a2f..fd7712519 100644 --- a/src/features/creator-3d/components/creator3d/Creator3D.tsx +++ b/src/features/creator-3d/components/creator3d/Creator3D.tsx @@ -111,8 +111,8 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { const response = await updateEmulator({ emulationId: existing.emulationId, body: { - name: `Emulator for ${existing.name}`, - description: `Emulator created for assembly ${existing.name}`, + name: `${existing.name}`, + description: `${existing.description}`, visibility: 'private', definition_json: JSON.stringify(exportData), status: existing.status @@ -554,8 +554,7 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { // Import data await importAssemblyFromJSON(jsonData) - - toast.success('✅ Import JSON thành công!') + // toast.success('✅ Import JSON thành công!') // Reset input để có thể import lại cùng file if (fileInputRef.current) { @@ -563,7 +562,7 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { } } catch (err: any) { console.error('Import error:', err) - toast.error(`❌ Lỗi khi import: ${err.message}`) + toast.error(`Lỗi khi import: ${err.message}`) } }, [dispatch] diff --git a/src/features/creator-3d/components/right-sidebar/WorkspaceTree.tsx b/src/features/creator-3d/components/right-sidebar/WorkspaceTree.tsx index 31e14ed1a..b5e20542b 100644 --- a/src/features/creator-3d/components/right-sidebar/WorkspaceTree.tsx +++ b/src/features/creator-3d/components/right-sidebar/WorkspaceTree.tsx @@ -177,13 +177,13 @@ export default function WorkspaceTree() { const handleAddAction = (type: WorkspaceAction['type']) => { const newId = `action_${nextActionNumber}` if (type === 'highlight') { - dispatch(addAction({ id: newId, name: `Highlight Action ${nextActionNumber}`, type })) + dispatch(addAction({ id: newId, name: `Bước ${nextActionNumber}`, type })) dispatch( // TODO: hard code activityId tạm thời addStepToActivity({ activityId: activities[0]?.id || newId, step: { - title: `Highlight Step ${nextActionNumber}`, + title: `Bước ${nextActionNumber}`, actionId: newId, description: '', expectedResult: '', @@ -194,12 +194,12 @@ export default function WorkspaceTree() { dispatch(setSelectedAction(newId)) } if (type === 'transform_arm') { - dispatch(addAction({ id: newId, name: `Transform Action ${nextActionNumber}`, type })) + dispatch(addAction({ id: newId, name: `Bước ${nextActionNumber}`, type })) dispatch( addStepToActivity({ activityId: activities[0]?.id || newId, step: { - title: `Transform Step ${nextActionNumber}`, + title: `Bước ${nextActionNumber}`, actionId: newId, description: '', expectedResult: '', diff --git a/src/features/emulator/api/emulatorApi.ts b/src/features/emulator/api/emulatorApi.ts index 714350c31..1969cc1d1 100644 --- a/src/features/emulator/api/emulatorApi.ts +++ b/src/features/emulator/api/emulatorApi.ts @@ -60,10 +60,11 @@ export const emulatorApi = createApi({ }), invalidatesTags: ['Emulator'] }), - deleteEmulator: builder.mutation, { emulationId: string }>({ - query: ({ emulationId }) => ({ + deleteEmulator: builder.mutation, { emulationId: string; permanent?: boolean }>({ + query: ({ emulationId, permanent }) => ({ url: `/v1/emulations/${emulationId}`, - method: 'DELETE' + method: 'DELETE', + params: permanent ? { permanent } : undefined }), invalidatesTags: ['Emulator'] }) @@ -74,5 +75,6 @@ export const { useGetEmulatorByIdQuery, useSearchEmulationsQuery, useCreateEmulatorMutation, - useUpdateEmulatorMutation + useUpdateEmulatorMutation, + useDeleteEmulatorMutation } = emulatorApi diff --git a/src/features/emulator/components/straw-lab/StrawLabList.tsx b/src/features/emulator/components/straw-lab/StrawLabList.tsx index 04b7948f7..211bd9e21 100644 --- a/src/features/emulator/components/straw-lab/StrawLabList.tsx +++ b/src/features/emulator/components/straw-lab/StrawLabList.tsx @@ -60,10 +60,9 @@ export default function StrawLabList() { } return ( -
+
-

Danh sách mô hình

diff --git a/src/features/emulator/components/workspace-3d/Workspace3dLibrary.tsx b/src/features/emulator/components/workspace-3d/Workspace3dLibrary.tsx index de714ed7f..43cd39ab2 100644 --- a/src/features/emulator/components/workspace-3d/Workspace3dLibrary.tsx +++ b/src/features/emulator/components/workspace-3d/Workspace3dLibrary.tsx @@ -11,7 +11,11 @@ import { Button } from '@/components/shadcn/button' import { Card, CardContent } from '@/components/shadcn/card' import SEmpty from '@/components/shared/empty/SEmpty' -import { useSearchEmulationsQuery, useUpdateEmulatorMutation } from '@/features/emulator/api/emulatorApi' +import { + useDeleteEmulatorMutation, + useSearchEmulationsQuery, + useUpdateEmulatorMutation +} from '@/features/emulator/api/emulatorApi' import BackButton from '@/components/shared/button/BackButton' import { Popover, PopoverContent, PopoverTrigger } from '@/components/shadcn/popover' import { EmulatorStatus, EmulatorWithThumbnail } from '@/features/emulator/types/emulator.type' @@ -32,6 +36,7 @@ export default function Workspace3dLibrary() { const { data, isLoading } = useSearchEmulationsQuery({ page: 1 }) const [updateEmulation] = useUpdateEmulatorMutation() + const [deleteEmulation] = useDeleteEmulatorMutation() const emulations = data?.data.items || [] @@ -47,22 +52,20 @@ export default function Workspace3dLibrary() { } }).unwrap() - toast.success('Đã publish mô hình!') + toast.success('Mô hình đã được công khai!') } catch (error) { - toast.error('❌ Publish thất bại') + toast.error('Thất bại khi công khai mô hình.') console.error(error) } } - const handleArchiveEmulation = async (emulator: EmulatorWithThumbnail) => { + const handleDeleteEmulation = async (emulator: EmulatorWithThumbnail) => { openModal('confirm', { - message: tt('confirmMessage.archive', { title: emulator.name }), + message: tt('confirmMessage.delete', { title: emulator.name }), onConfirm: async () => { - await updateEmulation({ + await deleteEmulation({ emulationId: emulator.emulationId, - body: { - status: EmulatorStatus.ARCHIVED - } + permanent: true }).unwrap() toast.success('Đã xóa mô hình!') @@ -154,10 +157,10 @@ export default function Workspace3dLibrary() { )}
diff --git a/src/features/group/components/detail/GroupColumn.tsx b/src/features/group/components/detail/GroupColumn.tsx new file mode 100644 index 000000000..73257f060 --- /dev/null +++ b/src/features/group/components/detail/GroupColumn.tsx @@ -0,0 +1,109 @@ +import { createActionsColumnFromItems, createSelectColumn } from '@/components/shared/data-table/columns-helpers' +import { useLocale, useTranslations } from 'next-intl' +import { useModal } from '@/providers/ModalProvider' +import { ColumnDef } from '@tanstack/react-table' +import { toast } from 'sonner' +import { GroupDetailStudent } from '@/features/group/types/group.type' +import { useDeleteGroupMutation } from '@/features/group/api/groupApi' +import { Badge } from '@/components/shadcn/badge' +import { Avatar, AvatarFallback } from '@/components/shadcn/avatar' +import { formatDate, useOrgUserStatusTranslation } from '@/utils/index' + +export function useGetGroupColumn(): ColumnDef[] { + const { openModal } = useModal() + const locale = useLocale() + const [deleteGroup] = useDeleteGroupMutation() + const tc = useTranslations('common') + const tt = useTranslations('toast') + const orgUserStatusTranslation = useOrgUserStatusTranslation() + + const handleDelete = async (id: string) => { + try { + await deleteGroup(id).unwrap() + toast.success(tt('successMessage.delete')) + } catch (error) { + toast.error(tt('errorMessage')) + } + } + + const getInitials = (fullName: string) => { + return fullName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) + } + + return [ + createSelectColumn(), + { + accessorKey: 'fullName', + header: tc('tableHeader.student'), + cell: ({ row }) => { + const student = row.original + return ( +
+ + + {getInitials(student.fullName)} + + +
+
{student.fullName}
+
{student.userName}
+
+
+ ) + } + }, + { + accessorKey: 'email', + header: tc('tableHeader.email'), + cell: ({ row }) =>
{row.original.email}
+ }, + { + accessorKey: 'isActive', + header: tc('tableHeader.status'), + cell: ({ row }) => ( + + {row.original.isActive ? orgUserStatusTranslation('active') : orgUserStatusTranslation('inactive')} + + ) + }, + { + accessorKey: 'subscriptionOrderId', + header: tc('tableHeader.subscription'), + cell: ({ row }) =>
#{row.original.subscriptionOrderId}
+ }, + { + accessorKey: 'joinedAt', + header: tc('tableHeader.joinedAt'), + cell: ({ row }) =>
{formatDate(row.original.joinedAt, { locale })}
+ }, + createActionsColumnFromItems([ + // { + // label: tc('button.viewDetails'), + // onClick: ({ original }) => { + // openModal('studentDetails', { studentId: original.organizationUserId }) + // } + // }, + // { + // label: tc('button.update'), + // onClick: ({ original }) => { + // openModal('upsertStudent', { id: original.organizationUserId }) + // } + // }, + { + label: tc('button.removeFromGroup'), + danger: true, + onClick: async ({ original }) => { + openModal('confirm', { + message: `${tt('confirmMessage.remove', { title: original.fullName })}`, + onConfirm: () => handleDelete(original.organizationUserId) + }) + } + } + ]) + ] +} diff --git a/src/features/group/components/detail/OrganizationGroupDetail.tsx b/src/features/group/components/detail/OrganizationGroupDetail.tsx deleted file mode 100644 index 39dbabf60..000000000 --- a/src/features/group/components/detail/OrganizationGroupDetail.tsx +++ /dev/null @@ -1,165 +0,0 @@ -'use client' -import { useGetGroupByIdQuery } from '@/features/group/api/groupApi' -import { Button } from '@/components/shadcn/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/shadcn/card' -import { Badge } from '@/components/shadcn/badge' -import { Avatar, AvatarFallback } from '@/components/shadcn/avatar' -import { Skeleton } from '@/components/shadcn/skeleton' -import { ArrowLeft, Users, Calendar, CheckCircle2, XCircle } from 'lucide-react' -import { useParams, useRouter } from 'next/navigation' -import { Group, GroupStatus } from '@/features/group/types/group.type' -import { formatDate } from '@/utils/index' -import { useLocale, useTranslations } from 'next-intl' - -export default function OrganizationGroupDetail() { - const router = useRouter() - const locale = useLocale() - const { groupId } = useParams() - - const tc = useTranslations('common') - const to = useTranslations('organization.group') - - const { data, isLoading, isError } = useGetGroupByIdQuery(Number(groupId), { skip: !groupId }) - - const groupData: Group | undefined = data?.data - - const getInitials = (name: string) => { - return name - .split(' ') - .map((n) => n[0]) - .join('') - .toUpperCase() - .slice(0, 2) - } - - if (isLoading) { - return ( -
- - - -
- ) - } - - if (isError || !groupData) { - return ( -
- - - Lỗi - Không thể tải thông tin nhóm - - - - - -
- ) - } - - const activeStudents = groupData.students.filter((s) => s.isActive).length - const totalStudents = groupData.students.length - - return ( -
- {/* Header */} -
- -
- - {/* Group Info Card */} - - -
-
- {groupData.name} - - {to('groupCode')} {groupData.code} - -
- - {groupData.status === GroupStatus.ACTIVE ? tc('status.active') : tc('status.inactive')} - -
-
- -
-
- -
-

{to('totalStudents')}

-

{totalStudents}

-
-
-
- -
-

{tc('status.active')}

-

{activeStudents}

-
-
-
- -
-

{to('createdDate')}

-

{formatDate(groupData.createdAt, { locale })}

-
-
-
-
- -
-

{to('groupList')}

-
- -
- {groupData.students.map((student) => ( -
-
- - - {getInitials(student.fullName)} - - -
-

{student.fullName}

-

{student.email}

-

- Tham gia: {formatDate(student.joinedAt, { locale })} -

-
-
-
- {student.isActive ? ( - - - Hoạt động - - ) : ( - - - Không hoạt động - - )} -
-
- ))} -
-
-
-
- ) -} diff --git a/src/features/group/components/detail/OrganizationGroupTableDetail.tsx b/src/features/group/components/detail/OrganizationGroupTableDetail.tsx new file mode 100644 index 000000000..1cd8e1299 --- /dev/null +++ b/src/features/group/components/detail/OrganizationGroupTableDetail.tsx @@ -0,0 +1,158 @@ +'use client' +import { useLocale, useTranslations } from 'next-intl' +import { useGetGroupByIdQuery } from '@/features/group/api/groupApi' +import { DataTable } from '@/components/shared/data-table/data-table' +import { useGetGroupColumn } from '@/features/group/components/detail/GroupColumn' +import { useMemo } from 'react' +import BackButton from '@/components/shared/button/BackButton' +import { useParams } from 'next/navigation' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/shadcn/card' +import { Badge } from '@/components/shadcn/badge' +import { Users, Calendar, Hash, Copy } from 'lucide-react' +import { formatDate, useOrgUserStatusTranslation } from '@/utils/index' +import { toast } from 'sonner' + +export default function OrganizationGroupTable() { + const { groupId } = useParams() + const locale = useLocale() + const to = useTranslations('organization.group') + const tc = useTranslations('common') + const tt = useTranslations('toast') + const columns = useGetGroupColumn() + const orgUserStatusTranslation = useOrgUserStatusTranslation() + const { data, isLoading } = useGetGroupByIdQuery(Number(groupId), { skip: !groupId }) + + const groupData = data?.data + + const rows = useMemo( + () => groupData?.students?.map((student) => ({ ...student, id: student.organizationUserId })) ?? [], + [groupData] + ) + + const stats = useMemo(() => { + if (!groupData?.students) return { total: 0, active: 0, inactive: 0 } + + const total = groupData.students.length + const active = groupData.students.filter((s) => s.isActive).length + const inactive = total - active + + return { total, active, inactive } + }, [groupData]) + + if (isLoading) { + return ( +
+
+
{tc('loading')}
+
+
+ ) + } + + const handleCopyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + toast.success(tt('successMessage.copiedToClipboard')) + } + + if (!groupData) { + return ( +
+
+
{tc('noData')}
+
+
+ ) + } + + return ( +
+ {/* Header Section */} +
+ +
+
+

{groupData.name}

+ + {orgUserStatusTranslation(groupData.status)} + +
+

{to('subTitle')}

+
+
+ {/* Group Information Cards */} +
+ {/* Large Card: Statistics - Spans 1 column */} + + + {to('totalStudents')} + + + +
{stats.total}
+
+
+
+
+ {orgUserStatusTranslation('active')} +
+ {stats.active} +
+
+
+
+ {orgUserStatusTranslation('inactive')} +
+ {stats.inactive} +
+
+ + + + {/* Right Side: Stacked Cards - Spans 2 columns */} +
+ + + {to('groupCode')} + + + +
+

{groupData.code}

+ +
+
+
+ +
+ + + {to('createdDate')} + + + +
{formatDate(groupData.createdAt, { locale })}
+
+
+ + + + {to('updatedAt')} + + + +
{formatDate(groupData.updatedAt)}
+
+
+
+
+
+ +
+ ) +} diff --git a/src/features/group/components/list/GroupTable.tsx b/src/features/group/components/list/GroupTableWithTeacher.tsx similarity index 84% rename from src/features/group/components/list/GroupTable.tsx rename to src/features/group/components/list/GroupTableWithTeacher.tsx index a34f8aa42..796973335 100644 --- a/src/features/group/components/list/GroupTable.tsx +++ b/src/features/group/components/list/GroupTableWithTeacher.tsx @@ -2,15 +2,16 @@ import React, { useEffect, useState } from 'react' import { SingleSelectWithSearch } from '@/components/shared/SingleSelectWithSearch' import { useSearchGroupByOrganizationIdQuery } from '@/features/group/api/groupApi' import { LicenseAssignmentType } from '@/features/license-assignment/types/licenseAssignment' -import { useSearchUserV2Query } from '@/features/user/api/userApi' +import { useGetOrganizationUserQuery, useSearchUserV2Query } from '@/features/user/api/userApi' import { useAppSelector } from '@/hooks/redux-hooks' -import { getOptions } from '@/utils/index' +import { getOptions, getOptionsV2 } from '@/utils/index' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/shadcn/table' import { Checkbox } from '@/components/shadcn/checkbox' import { Group } from '@/features/group/types/group.type' import { useTranslations } from 'next-intl' +import { LicenseType } from '@/types/userRole' -type GroupTableProps = { +type GroupTableWithTeacherProps = { onGroupsChange: ( groups: { groupCode: string @@ -21,26 +22,29 @@ type GroupTableProps = { ) => void } -export default function GroupTable({ onGroupsChange }: GroupTableProps) { - const tClassroom = useTranslations('classroom.create') +export default function GroupTableWithTeacher({ onGroupsChange }: GroupTableWithTeacherProps) { const [selectedRows, setSelectedRows] = useState([]) const [teacherAssignments, setTeacherAssignments] = useState>({}) - const searchUserQuery = useAppSelector((state) => state.user) - const { selectedSubscriptionOrderId, selectedOrganizationId } = useAppSelector((state) => state.selectedOrganization) + const { selectedOrganizationId } = useAppSelector((state) => state.selectedOrganization) const { data } = useSearchGroupByOrganizationIdQuery( { organizationId: selectedOrganizationId!, params: {} }, { skip: !selectedOrganizationId } ) - const { data: teacherData } = useSearchUserV2Query({ - ...searchUserQuery, - license_type: LicenseAssignmentType.TEACHER, - subscription_order_id: selectedSubscriptionOrderId - }) + const { data: organizationUserData } = useGetOrganizationUserQuery( + { organizationId: selectedOrganizationId!, role: LicenseType.TEACHER }, + { skip: !selectedOrganizationId } + ) - const teacherOptions = getOptions(teacherData?.data.items, 'userName', 'imageUrl', 'email') + const teacherOptions = getOptionsV2( + organizationUserData?.data.items, + 'userName', + 'organizationUserId', + 'imageUrl', + 'email' + ) const groups = data?.data.items || [] const emitSelectedGroups = () => { @@ -52,7 +56,7 @@ export default function GroupTable({ onGroupsChange }: GroupTableProps) { groupCode: group.code, groupName: group.name, teacherId: teacherAssignments[groupId], - studentIds: group.students.map((s) => s.userId) + studentIds: group.students.map((s) => s.organizationUserId) } }) diff --git a/src/features/group/slice/groupSlice.ts b/src/features/group/slice/groupSlice.ts new file mode 100644 index 000000000..f30629c7b --- /dev/null +++ b/src/features/group/slice/groupSlice.ts @@ -0,0 +1,14 @@ +import { GroupSliceParams } from '@/features/group/types/group.type' +import { createQuerySlice } from '@/libs/redux/createQuerySlice' + +const initialState: GroupSliceParams = { + pageNumber: 1, + pageSize: 20, + search: '', + orderBy: '', + status: '' +} + +export const groupSlice = createQuerySlice('groupSlice', initialState) + +export const { setPageIndex, setPageSize, setSearchTerm, setParam, setMultipleParams, resetParams } = groupSlice.actions diff --git a/src/features/group/types/group.type.ts b/src/features/group/types/group.type.ts index 9a14c03b3..2a39dd482 100644 --- a/src/features/group/types/group.type.ts +++ b/src/features/group/types/group.type.ts @@ -1,3 +1,4 @@ +import { SliceQueryParams } from '@/libs/redux/createQuerySlice' import { SearchPaginatedRequestParams } from '@/types/baseModel' export enum GroupStatus { @@ -33,3 +34,8 @@ export type GroupQueryParams = { includeArchived?: boolean activeOnly?: boolean } & SearchPaginatedRequestParams + +export type GroupSliceParams = { + includeArchived?: boolean + activeOnly?: boolean +} & SliceQueryParams diff --git a/src/features/kit-components/components/list/ComponentList.tsx b/src/features/kit-components/components/list/ComponentList.tsx index ae7a5bd3c..894893490 100644 --- a/src/features/kit-components/components/list/ComponentList.tsx +++ b/src/features/kit-components/components/list/ComponentList.tsx @@ -1,11 +1,9 @@ 'use client' import { Button } from '@/components/shadcn/button' -import { Card, CardContent } from '@/components/shadcn/card' import { Input } from '@/components/shadcn/input' import { DataTable } from '@/components/shared/data-table/data-table' import SEmpty from '@/components/shared/empty/SEmpty' import LoadingComponent from '@/components/shared/loading/LoadingComponent' -import { SPagination } from '@/components/shared/SPagination' import { useDeleteComponentMutation, useSearchComponentQuery } from '@/features/kit-components/api/kitComponentApi' import { useGetComponentColumn } from '@/features/kit-components/components/list/ComponentColumn' import { setPageIndex, setSearchTerm } from '@/features/kit-components/slice/componentSlice' @@ -14,7 +12,6 @@ import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks' import { useModal } from '@/providers/ModalProvider' import { Plus, Search } from 'lucide-react' import { useTranslations } from 'next-intl' -import Image from 'next/image' import React from 'react' export default function ComponentList() { @@ -72,7 +69,7 @@ export default function ComponentList() { data={rows} columns={columns} enableRowSelection - pagingData={componentData?.data} + pagingData={componentData} pagingParams={queryParams} handlePageChange={handlePageChange} /> diff --git a/src/features/kit-components/components/list/SelectComponentList.tsx b/src/features/kit-components/components/list/SelectComponentList.tsx index dced0b08a..61a7adf7e 100644 --- a/src/features/kit-components/components/list/SelectComponentList.tsx +++ b/src/features/kit-components/components/list/SelectComponentList.tsx @@ -10,11 +10,10 @@ import { useUpdateKitComponentsMutation } from '@/features/kit-components/api/kitComponentApi' import { useGetComponentColumn } from '@/features/kit-components/components/list/ComponentColumn' -import { setPageIndex, setPageSize, setSearchTerm } from '@/features/kit-components/slice/componentSlice' +import { resetParams, setPageIndex, setPageSize, setSearchTerm } from '@/features/kit-components/slice/componentSlice' import { ComponentSliceParams, KitComponent } from '@/features/kit-components/types/kit-component.type' import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks' import { useModal } from '@/providers/ModalProvider' -import Loading from 'app/[locale]/loading' import { useLocale, useTranslations } from 'next-intl' import Link from 'next/link' import React, { useEffect, useState } from 'react' @@ -60,7 +59,7 @@ export default function SelectComponentList({ } useEffect(() => { - dispatch(setPageSize(6)) + dispatch(resetParams()) }, [dispatch]) const { data, isLoading } = useSearchComponentQuery(queryParams) @@ -212,6 +211,9 @@ export default function SelectComponentList({ id: c.componentId // map lại để dùng chung column logic }))} columns={extendedColumns} + pagingData={data} + pagingParams={queryParams} + handlePageChange={handlePageChange} enableRowSelection={false} /> diff --git a/src/features/plan/components/header/SubscriptionHeader.tsx b/src/features/plan/components/header/SubscriptionHeader.tsx index 0cc1cc980..f80fce428 100644 --- a/src/features/plan/components/header/SubscriptionHeader.tsx +++ b/src/features/plan/components/header/SubscriptionHeader.tsx @@ -6,8 +6,10 @@ import { setParam } from '@/features/plan/slice/planProductSlice' import { BillingCycle } from '@/features/plan/types/plan.type' import { containerVariants, itemVariants } from '@/utils/motion' import { useEffect } from 'react' +import { useTranslations } from 'next-intl' export function SubscriptionHeader() { + const t = useTranslations('plan.user') const dispatch = useAppDispatch() const billingCycle = useAppSelector((state) => state.plan.billingCycle) @@ -27,19 +29,19 @@ export function SubscriptionHeader() {
- Flexible Plans + {t('subTitle')} - Plans & Pricing + {t('title')} - Whether your time-saving automation needs are large or small, we're here to help you scale. + {t('des1')} - Choose the perfect plan for your team and unlock unlimited potential. + {t('des2')} @@ -53,8 +55,8 @@ export function SubscriptionHeader() { >
{[ - { label: 'Semiannual', value: BillingCycle.SEMIANNUAL }, - { label: 'Annual', value: BillingCycle.ANNUAL } + { label: t('semiAnnual'), value: BillingCycle.SEMIANNUAL }, + { label: t('annual'), value: BillingCycle.ANNUAL } ].map((option) => ( {/* Features */} diff --git a/src/features/resource/course/components/detail/CourseDetail.tsx b/src/features/resource/course/components/detail/CourseDetail.tsx deleted file mode 100644 index 85a88ea7a..000000000 --- a/src/features/resource/course/components/detail/CourseDetail.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client' - -import LoadingComponent from '@/components/shared/loading/LoadingComponent' -import { useSearchCourseEnrollmentQuery } from '@/features/enrollment/api/courseEnrollmentApi' -import CourseDetailEnrolled from '@/features/resource/course/components/detail/enrolled/CourseDetailEnrolled' -import CourseDetailNotEnrolled from '@/features/resource/course/components/detail/not-enrolled/CourseDetailNotEnrolled' -import { useAppSelector } from '@/hooks/redux-hooks' -import { LicenseType, UserRole } from '@/types/userRole' -import { useParams } from 'next/navigation' - -export default function CourseDetail() { - const param = useParams() - const courseIdParam = param?.courseId - const courseId = courseIdParam ? Number(courseIdParam) : undefined - - const userRole = useAppSelector((state) => state.selectedOrganization.currentRole) - const studentId = useAppSelector((state) => state.auth.user?.userId) - - const { data, isLoading, error } = useSearchCourseEnrollmentQuery( - { pageNumber: 1, pageSize: 10, courseId, studentId }, - { skip: !studentId } - ) - - if (isLoading) { - return ( -
- -
- ) - } - if (error) { - return

Error: {(error as any)?.message ?? 'Unknown error'}

- } - - const enrollmentItems = data?.data?.items ?? [] - const firstEnrollment = enrollmentItems[0] - - if (firstEnrollment) { - return - } - - if (userRole === LicenseType.TEACHER) { - return - } - - return -} diff --git a/src/features/resource/course/components/my-learning/MyLearningHero.tsx b/src/features/resource/course/components/my-learning/MyLearningHero.tsx index fa000c6e0..1f40c1447 100644 --- a/src/features/resource/course/components/my-learning/MyLearningHero.tsx +++ b/src/features/resource/course/components/my-learning/MyLearningHero.tsx @@ -14,7 +14,7 @@ type MyLearningHeroProps = { } export function MyLearningHero({ course, studentId }: MyLearningHeroProps) { - const t = useTranslations('MyLearning') + const t = useTranslations('myLearning') const auth = useAppSelector((state) => state.auth) const { data } = useSearchCourseEnrollmentQuery( diff --git a/src/features/resource/course/components/my-learning/MyLearningList.tsx b/src/features/resource/course/components/my-learning/MyLearningList.tsx index 93b6bf77a..242a6467b 100644 --- a/src/features/resource/course/components/my-learning/MyLearningList.tsx +++ b/src/features/resource/course/components/my-learning/MyLearningList.tsx @@ -1,4 +1,3 @@ -// app/my-learning/MyLearningList.tsx 'use client' import React, { useMemo } from 'react' @@ -13,19 +12,15 @@ import { SpecializationCard } from '@/features/certificate/components/list/Speci import { useSearchCurriculumEnrollmentQuery } from '@/features/enrollment/api/curriculumEnrollmentApi' import { CourseCard } from '@/features/certificate/components/list/CourseCard' import ClassroomList from '@/features/classroom/components/list/ClassroomList' -import { Separator } from '@/components/shadcn/separator' import { MyLearningSidebar } from './MyLearningSidebar' -import { Badge } from '@/components/shadcn/badge' type MyLearningListProps = { studentId?: string } export function MyLearningList({ studentId }: MyLearningListProps) { - const t = useTranslations('MyLearning') - - const courseEnrollParams = useAppSelector((state) => state.courseEnrollment) - const curriculumEnrollParams = useAppSelector((state) => state.curriculumEnrollment) + const t = useTranslations('myLearning') + const tClassroom = useTranslations('classroom.myLearning') const { data: courseEnrollment, isLoading: isLoadingCourseEnrollment } = useSearchCourseEnrollmentQuery( { studentId }, @@ -75,7 +70,7 @@ export function MyLearningList({ studentId }: MyLearningListProps) {
{/* Classroom Section */}
-

My Classrooms

+

{tClassroom('title')}

diff --git a/src/features/resource/lesson/components/detail/LessonContent.tsx b/src/features/resource/lesson/components/detail/LessonContent.tsx index 87f01464d..acf0542e0 100644 --- a/src/features/resource/lesson/components/detail/LessonContent.tsx +++ b/src/features/resource/lesson/components/detail/LessonContent.tsx @@ -35,6 +35,7 @@ export default function LessonContent({ token, lessonId, sectionStatus, enrollme const dispatch = useAppDispatch() const t = useTranslations('LessonDetails') + const tc = useTranslations('common') const tt = useTranslations('toast') // const { data: userData, status } = useSession() @@ -82,7 +83,13 @@ export default function LessonContent({ token, lessonId, sectionStatus, enrollme const lastItem = content.data.items[content.data.items.length - 1] if (lastItem.contentType === ContentType.QUIZ) { - return + return ( + + ) } else if (lastItem.contentType === ContentType.ASSIGNMENT) { return ( {/* Content */}
- +
@@ -104,8 +111,8 @@ export default function LessonContent({ token, lessonId, sectionStatus, enrollme {/* Complete section button */} {isLoggedIn && currentSectionProgress?.status === ProgressStatus.IN_PROGRESS && (
-
)} diff --git a/src/features/resource/lesson/components/detail/LessonDetail.tsx b/src/features/resource/lesson/components/detail/LessonDetail.tsx index 19cb9817e..b028071f0 100644 --- a/src/features/resource/lesson/components/detail/LessonDetail.tsx +++ b/src/features/resource/lesson/components/detail/LessonDetail.tsx @@ -27,7 +27,7 @@ export default function LessonDetail() { // redux const dispatch = useAppDispatch() - const userId = useAppSelector((state) => state.auth.user?.userId) + const { selectedOrgUserId } = useAppSelector((state) => state.selectedOrganization) const { selectedSectionId, mode } = useAppSelector((state) => state.lessonDetail) const token = useAppSelector((state) => state.auth.token) const shouldRefetch = useAppSelector((state) => state.studentProgress.shouldRefetchSectionProgress) @@ -43,9 +43,9 @@ export default function LessonDetail() { const sectionData = sections?.data?.items ?? [] const { data: enrollment } = useSearchCourseEnrollmentQuery( - { studentId: userId, courseId, pageNumber: 1, pageSize: 10 }, + { studentId: selectedOrgUserId!, courseId, pageNumber: 1, pageSize: 10 }, { - skip: !userId || !courseId + skip: !selectedOrgUserId || !courseId } ) diff --git a/src/features/resource/question/components/QuizEditor.tsx b/src/features/resource/question/components/QuizEditor.tsx index 98065cee5..7a5658665 100644 --- a/src/features/resource/question/components/QuizEditor.tsx +++ b/src/features/resource/question/components/QuizEditor.tsx @@ -124,14 +124,14 @@ const QuizEditor = () => { questions: formatQuestions(quiz.questions, true) }).unwrap() - toast.success(`${quiz.questions.length} questions updated successfully`) + toast.success(`Đã tạo bài kiểm tra thành công`) } else { await createQuestion({ quizId: Number(quizId), questions: formatQuestions(quiz.questions, false) }).unwrap() - toast.success(`${quiz.questions.length} questions created successfully`) + toast.success(`Đã tạo bài kiểm tra thành công`) } dispatch(markAsSaved()) diff --git a/src/features/resource/question/components/QuizEditorSidebar.tsx b/src/features/resource/question/components/QuizEditorSidebar.tsx index c62ebd540..b12e74866 100644 --- a/src/features/resource/question/components/QuizEditorSidebar.tsx +++ b/src/features/resource/question/components/QuizEditorSidebar.tsx @@ -30,6 +30,7 @@ type QuizEditorSidebarProps = { export const QuizEditorSidebar = ({ onAddQuestion }: QuizEditorSidebarProps) => { const tq = useTranslations('quiz') const tt = useTranslations('toast') + const tc = useTranslations('common') const { quizId, sectionId, lessonId } = useParams() const dispatch = useAppDispatch() @@ -184,7 +185,7 @@ export const QuizEditorSidebar = ({ onAddQuestion }: QuizEditorSidebarProps) => diff --git a/src/features/resource/quiz/components/player/QuizPlayerContainer.tsx b/src/features/resource/quiz/components/player/QuizPlayerContainer.tsx index f093410ad..2bed73db9 100644 --- a/src/features/resource/quiz/components/player/QuizPlayerContainer.tsx +++ b/src/features/resource/quiz/components/player/QuizPlayerContainer.tsx @@ -1,7 +1,6 @@ 'use client' import { useEffect } from 'react' -import { useIsMobile } from '@/hooks/use-mobile' import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks' import QuizSidebar from '@/features/resource/quiz/components/player/QuizSidebar' import QuizMainContent from '@/features/resource/quiz/components/player/QuizMainContent' diff --git a/src/features/resource/quiz/components/player/question/types/MultipleChoiceQuestion.tsx b/src/features/resource/quiz/components/player/question/types/MultipleChoiceQuestion.tsx index 022447226..ea8ca7fa6 100644 --- a/src/features/resource/quiz/components/player/question/types/MultipleChoiceQuestion.tsx +++ b/src/features/resource/quiz/components/player/question/types/MultipleChoiceQuestion.tsx @@ -69,7 +69,7 @@ export default function MultipleChoiceQuestion({ question }: MultipleChoiceQuest > {/* Checkbox */} {/* Answer Label */} {String.fromCharCode(65 + index)} @@ -97,13 +92,6 @@ export default function SingleChoiceQuestion({ question }: SingleChoiceQuestionP )} - - {/* Checkmark indicator for selected */} - {!isSubmitted && isChosen && ( -
- -
- )} ) })} diff --git a/src/features/resource/quiz/components/viewer/QuizViewer.tsx b/src/features/resource/quiz/components/viewer/QuizViewer.tsx index 5a6c71d37..2c634e943 100644 --- a/src/features/resource/quiz/components/viewer/QuizViewer.tsx +++ b/src/features/resource/quiz/components/viewer/QuizViewer.tsx @@ -16,16 +16,25 @@ import QuizAttempt from '@/features/resource/quiz/components/viewer/QuizAttempt' import { Attempt } from '@/features/resource/quiz/types/quiz.type' import { LicenseType, UserRole } from '@/types/userRole' import { useTranslations } from 'next-intl' +import { PaginatedResult } from '@/types/baseModel' +import { StudentProgress } from '@/features/student-progress/types/studentProgress.type' type QuizViewerProps = { quiz: QuizContent isShowQuestionAnswer?: boolean studentQuizId?: number + sectionStatus?: PaginatedResult } -export default function QuizViewer({ quiz, isShowQuestionAnswer, studentQuizId }: QuizViewerProps) { +export default function QuizViewer({ quiz, isShowQuestionAnswer, studentQuizId, sectionStatus }: QuizViewerProps) { const tq = useTranslations('quiz.detail') + const tc = useTranslations('common') const dispatch = useAppDispatch() + const selectedQuiz = useAppSelector((state) => state.quizPlayer.selectedQuiz) + + const quizStatus = sectionStatus?.items.find((item) => item.sectionId === selectedQuiz?.id)?.status + console.log('Quiz Status:', quizStatus) + const { data: quizData, isLoading } = useGetQuizByIdQuery(quiz.quizId, { skip: !quiz.quizId }) const [selectedAttempt, setSelectedAttempt] = useState(null) const role = useAppSelector((state) => state.selectedOrganization.currentRole) @@ -77,6 +86,8 @@ export default function QuizViewer({ quiz, isShowQuestionAnswer, studentQuizId } } } + const canStartQuiz = !isShowQuestionAnswer && quizStatus !== 'Completed' && quizStatus !== 'Locked' + return (
{/* Quiz Header */} @@ -112,7 +123,7 @@ export default function QuizViewer({ quiz, isShowQuestionAnswer, studentQuizId }

{tq('timeLimit')}

- {quiz.timeLimitInMinutes} {tq('mins')} + {quiz.timeLimitInMinutes ? `${quiz.timeLimitInMinutes} ${tq('mins')}` : '-'}

@@ -255,18 +266,13 @@ export default function QuizViewer({ quiz, isShowQuestionAnswer, studentQuizId } })}
- ) : ( + ) : !canStartQuiz ? (
- {/* loading button when creating quiz attempt */} -
- )} + ) : null} {studentQuizId && ( ) => { + state.isSectionDone = action.payload } } }) @@ -55,6 +60,7 @@ export const { setSelectedSectionId, setSelectedSectionStatus, triggerRefetchSectionProgress, - clearRefetchSectionProgress + clearRefetchSectionProgress, + setSectionDone } = studentProgressSlice.actions export const studentProgressReducer = studentProgressSlice.reducer diff --git a/src/features/subscription/slice/selectedOrganizationSlice.ts b/src/features/subscription/slice/selectedOrganizationSlice.ts index dc424512c..84afcd47f 100644 --- a/src/features/subscription/slice/selectedOrganizationSlice.ts +++ b/src/features/subscription/slice/selectedOrganizationSlice.ts @@ -3,12 +3,14 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' interface SelectedOrganizationState { selectedOrganizationId: number | null + selectedOrgUserId?: string | null selectedSubscriptionOrderId?: number | null currentRole?: LicenseType | UserRole.ADMIN | UserRole.STAFF | UserRole.GUEST } const initialState: SelectedOrganizationState = { selectedOrganizationId: null, + selectedOrgUserId: null, selectedSubscriptionOrderId: null, currentRole: UserRole.GUEST } @@ -20,6 +22,9 @@ const selectedOrganizationSlice = createSlice({ setSelectedOrganizationId: (state, action: PayloadAction) => { state.selectedOrganizationId = action.payload }, + setSelectedOrgUserId: (state, action: PayloadAction) => { + state.selectedOrgUserId = action.payload + }, setSelectedSubscriptionOrderId: (state, action: PayloadAction) => { state.selectedSubscriptionOrderId = action.payload }, @@ -34,7 +39,12 @@ const selectedOrganizationSlice = createSlice({ } }) -export const { setSelectedOrganizationId, setSelectedSubscriptionOrderId, setCurrentRole, clearSelectedOrganization } = - selectedOrganizationSlice.actions +export const { + setSelectedOrganizationId, + setSelectedOrgUserId, + setSelectedSubscriptionOrderId, + setCurrentRole, + clearSelectedOrganization +} = selectedOrganizationSlice.actions export default selectedOrganizationSlice.reducer diff --git a/src/features/user/api/userApi.ts b/src/features/user/api/userApi.ts index 1c8825f20..3f2d1704e 100644 --- a/src/features/user/api/userApi.ts +++ b/src/features/user/api/userApi.ts @@ -28,10 +28,10 @@ export const userApi = createCrudApi({ ApiSuccessResponse>, OrganizationUserQueryParams >({ - query: ({ organizationId, pageNumber, pageSize }) => ({ + query: ({ organizationId, pageNumber, pageSize, role }) => ({ url: `/organizations/${organizationId}/users`, method: 'GET', - params: { pageNumber, pageSize } + params: { pageNumber, pageSize, role } }), providesTags: ['User'] }) diff --git a/src/features/user/components/modal/AddPeopleModal.tsx b/src/features/user/components/modal/AddPeopleModal.tsx index 028f515f0..f39ff3928 100644 --- a/src/features/user/components/modal/AddPeopleModal.tsx +++ b/src/features/user/components/modal/AddPeopleModal.tsx @@ -279,9 +279,9 @@ export default function AddPeopleModal() { ) : debouncedKeyword.trim() ? (
- {tc('update.students.noStudentFound')} "{debouncedKeyword}" + {tClassroom('update.students.noStudentFound')} "{debouncedKeyword}"
-
{tc('update.students.noStudentFoundSubtext')}
+
{tClassroom('update.students.noStudentFoundSubtext')}
) : null}
diff --git a/src/features/user/types/user.type.ts b/src/features/user/types/user.type.ts index 454cac05e..426974d93 100644 --- a/src/features/user/types/user.type.ts +++ b/src/features/user/types/user.type.ts @@ -87,4 +87,5 @@ export type OrganizationUserQueryParams = { organizationId: number pageNumber?: number pageSize?: number + role?: LicenseType } diff --git a/src/libs/auth/authOptions.ts b/src/libs/auth/authOptions.ts index 60ede2904..45dd84980 100644 --- a/src/libs/auth/authOptions.ts +++ b/src/libs/auth/authOptions.ts @@ -69,7 +69,7 @@ export const authOptions: NextAuthOptions = { }, async jwt({ token, account, profile }) { if (account?.access_token) { - console.log('JWT callback', { profile }) + // console.log('JWT callback', { profile }) token.accessToken = account.access_token token.idToken = account.id_token token.role = profile?.role || UserRole.GUEST @@ -80,7 +80,6 @@ export const authOptions: NextAuthOptions = { console.error('Failed to parse organizations JSON:', err) token.organizations = undefined } - console.log('Token debug:', token) // try { // const decoded: any = jwtDecode(account.access_token) diff --git a/src/libs/redux/rootReducer.ts b/src/libs/redux/rootReducer.ts index eacfb9c73..2b9d1d0d3 100644 --- a/src/libs/redux/rootReducer.ts +++ b/src/libs/redux/rootReducer.ts @@ -74,6 +74,7 @@ import { studentAssignmentSelectedSlice } from '@/features/assignment/slice/stud import { enrollmentSlice } from '@/features/enrollment/slice/enrollmentSlice' import { groupApi } from '@/features/group/api/groupApi' import { organizationSpecialSlice } from '@/features/organization/slice/organizationSpecialSlice' +import { groupSlice } from '@/features/group/slice/groupSlice' export const rootReducer = combineReducers({ // Add your reducers here @@ -118,6 +119,7 @@ export const rootReducer = combineReducers({ enrollment: enrollmentSlice.reducer, organizationSpecial: organizationSpecialSlice.reducer, selectedCurriculum: selectedCurriculumSlice.reducer, + group: groupSlice.reducer, // api reducers [courseApi.reducerPath]: courseApi.reducer, diff --git a/src/providers/AuthSessionSync.tsx b/src/providers/AuthSessionSync.tsx index 84ad81be8..d9d61fa42 100644 --- a/src/providers/AuthSessionSync.tsx +++ b/src/providers/AuthSessionSync.tsx @@ -5,6 +5,7 @@ import { setToken, setUser } from '@/features/auth/authSlice' import { setCurrentRole, setSelectedOrganizationId, + setSelectedOrgUserId, setSelectedSubscriptionOrderId } from '@/features/subscription/slice/selectedOrganizationSlice' import { UserRole } from '@/types/userRole' @@ -67,6 +68,7 @@ export default function AuthSessionSync() { if (activeSub) { dispatch(setSelectedOrganizationId(firstOrg.id)) dispatch(setSelectedSubscriptionOrderId(activeSub.subscriptionId)) + dispatch(setSelectedOrgUserId('e821a170-2f5a-4a37-8d15-432a03af4a43')) //hardcode dispatch(setCurrentRole(activeSub.type)) // Đây là LicenseType } } diff --git a/src/utils/index.ts b/src/utils/index.ts index 64e0c8882..23755eb56 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -50,7 +50,7 @@ export interface FormatDateOptions { day?: 'numeric' | '2-digit' } export const formatDate = (dateString: string, options: FormatDateOptions = {}) => { - const { locale = 'en', showTime = false, pattern, year = 'numeric', month = 'short', day = 'numeric' } = options + const { locale, showTime = false, pattern, year = 'numeric', month = 'short', day = 'numeric' } = options const date = new Date(dateString) @@ -141,6 +141,40 @@ export const getOptions = ( : undefined })) || [] +type OptionResult = { + value: string + label: string + imageUrl?: string + subLabel?: string + status?: string + date?: string +} + +export const getOptionsV2 = ( + data: any[] | undefined, + labelKey: string, + valueKey: string, // ✅ THÊM + imageKey?: string, + subLabelKey?: string, + statusKey?: string, + startDateKey?: string, + endDateKey?: string +): OptionResult[] => + data?.map((item) => ({ + value: item[valueKey]?.toString() ?? '', + label: item[labelKey], + imageUrl: imageKey ? item[imageKey] : undefined, + subLabel: subLabelKey ? item[subLabelKey] : undefined, + status: statusKey ? item[statusKey] : undefined, + date: + startDateKey && endDateKey + ? 'Start Date: ' + + (item[startDateKey] ? new Date(item[startDateKey]).toLocaleDateString() : 'N/A') + + ' - End Date: ' + + (item[endDateKey] ? new Date(item[endDateKey]).toLocaleDateString() : 'N/A') + : undefined + })) || [] + export function fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() @@ -176,6 +210,13 @@ export const useStatusTranslation = () => { } } +export const useOrgUserStatusTranslation = () => { + const tc = useTranslations('common.orgUserStatus') + return (status: string) => { + return tc(status.toLowerCase()) + } +} + export const useLevelTranslation = () => { const tc = useTranslations('common.level') return (level: string) => {