Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions messages/en/common/en_common.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@
"endsoon": "End Soon",
"inprogress": "In Progress"
},
"level": {
"all": "All Levels",
"beginner": "Beginner",
"intermidiate": "Intermidiate",
"advanced": "Advanced"
},
"accountType": {
"accountTypeLabel": "Account Type",
"admin": "Admin",
Expand Down
6 changes: 6 additions & 0 deletions messages/vi/common/vi_common.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@
"badge": {
"selected": "Chọn"
},
"level": {
"all": "Tất Cả Trình Độ",
"beginner": "Mới Bắt Đầu",
"intermidiate": "Trung Bình",
"advanced": "Nâng Cao"
},
"status": {
"all": "Tất Cả",
"filterBy": "Lọc Theo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export default function GradeAssignmentModal({ studentAssignmentId, onClose, onS
attemptData.questionAttempts.forEach((qAttempt) => {
initialScores[qAttempt.id] = {}
qAttempt.rubricScore.forEach((criterion) => {
initialScores[qAttempt.id][criterion.rubricCriterionId] = (criterion as any).currentPoints ?? null
// accept either `currentPoints` or `points` depending on API response
initialScores[qAttempt.id][criterion.rubricCriterionId] = (criterion as any).currentPoints ?? (criterion as any).points ?? null
})
})

Expand Down Expand Up @@ -203,7 +204,14 @@ export default function GradeAssignmentModal({ studentAssignmentId, onClose, onS
: `Question #${index + 1}`
const questionContent = originalQuestion?.content || ''

const currentTotalScore = calculateQuestionTotal(qAttempt.id)
const currentTotalScore = (() => {
const questionScores = scores[qAttempt.id]
if (questionScores) {
return Object.values(questionScores).reduce((acc, curr) => (acc || 0) + (curr || 0), 0) || 0
}
// fallback to API values
return qAttempt.rubricScore.reduce((acc: number, c: any) => acc + ((c.currentPoints ?? c.points) || 0), 0)
})()
const maxQuestionScore = calculateMaxPoints(qAttempt.rubricScore)

return (
Expand Down Expand Up @@ -247,9 +255,10 @@ export default function GradeAssignmentModal({ studentAssignmentId, onClose, onS
type="number"
className='h-10 w-full rounded-md border-slate-200 bg-slate-50 pr-4 text-slate-900 focus:bg-white'
value={
// prefer edited state, otherwise fallback to API fields
scores[qAttempt.id] && scores[qAttempt.id][criterion.rubricCriterionId] != null
? String(scores[qAttempt.id][criterion.rubricCriterionId])
: ''
: String((criterion as any).currentPoints ?? (criterion as any).points ?? '')
}
onChange={(e) => handleScoreChange(qAttempt.id, criterion.rubricCriterionId, e.target.value)}
placeholder="Nhập điểm"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ export function SubmissionReviewDialog({ submission, studentAssignmentId, onSucc
attemptData.questionAttempts.forEach((qAttempt) => {
initialScores[qAttempt.id] = {}
qAttempt.rubricScore.forEach((criterion) => {
// accept either `currentPoints` (server-side naming) or `points` (alternate payload)
initialScores[qAttempt.id][criterion.rubricCriterionId] =
(criterion as any).currentPoints ?? null
(criterion as any).currentPoints ?? (criterion as any).points ?? null
})
})

Expand Down Expand Up @@ -273,10 +274,13 @@ export function SubmissionReviewDialog({ submission, studentAssignmentId, onSucc
max={criterion.maxPoints}
min={0}
disabled={isReviewed || isGrading}
// 'null ?? ""' -> ""
// '0 ?? ""' -> 0
// '5 ?? ""' -> 5
value={scores[question.id]?.[criterion.rubricCriterionId] ?? ''}
value={
// prefer edited state, otherwise fallback to API fields
scores[question.id]?.[criterion.rubricCriterionId] ??
(criterion as any).currentPoints ??
(criterion as any).points ??
''
}
onChange={(e) =>
handleScoreChange(question.id, criterion.rubricCriterionId, e.target.value)
}
Expand All @@ -295,10 +299,10 @@ export function SubmissionReviewDialog({ submission, studentAssignmentId, onSucc
<p className='text-sm font-medium text-gray-700'>
{t('modal.total')}:{' '}
{question.rubricScore.reduce((sum, c) => {
const score =
scores[question.id]?.[c.rubricCriterionId] !== undefined
? scores[question.id][c.rubricCriterionId] || 0
: 0
const key = c.rubricCriterionId
const scoreFromState = scores[question.id]?.[key]
const scoreFromApi = (c as any).currentPoints ?? (c as any).points
const score = scoreFromState !== undefined ? scoreFromState || 0 : scoreFromApi ?? 0
return sum + score
}, 0)}{' '}
/ {question.rubricScore.reduce((sum, c) => sum + c.maxPoints, 0)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ function mapApiToSubmissions(students: StudentStatistic[], assignmentTitle: stri
const latestAttempt = student.attempts.length > 0 ? student.attempts[student.attempts.length - 1] : null

let grade: string | null = null
if (latestAttempt && (latestAttempt.status === 'Graded' || latestAttempt.status === 'Passed')) {
// Show grade for attempts that have been reviewed (Graded, Passed, or Failed)
if (latestAttempt && (latestAttempt.status === 'Graded' || latestAttempt.status === 'Passed' || latestAttempt.status === 'Failed')) {
grade = `${latestAttempt.totalScore}`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,19 @@ import {
import LessonTable from '@/features/resource/lesson/components/list/LessonTable'
import { useModal } from '@/providers/ModalProvider'
import { getLevelBadgeClass, getStatusBadgeClass } from '@/utils/badgeColor'
import { capitalizeFirst } from '@/utils/index'
import { capitalizeFirst, useLevelTranslation, useStatusTranslation } from '@/utils/index'
import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks'
import { resetParams, setPageIndex, setPageSize } from '@/features/resource/lesson/slice/lessonSlice'
import { UserRole } from '@/types/userRole'
import { useLazyExportToRSAQuery } from '@/features/resource/export/api/exportApi'
import ExportRSAButton from '@/components/shared/button/ExportRSAButton'
import CurriculumKitList from '@/features/resource/kit/components/list/KitListSection'
import KitListSection from '@/features/resource/kit/components/list/KitListSection'
import { Card } from '@/components/shadcn/card'

export default function AdminCourseDetail() {
const t = useTranslations('Admin.course_details')
const tt = useTranslations('toast')
const tc = useTranslations('common')
const translateStatus = useStatusTranslation()
const translateLevel = useLevelTranslation()

// Get current user
const user = useAppSelector((state) => state.auth.user)
Expand Down Expand Up @@ -148,14 +147,14 @@ export default function AdminCourseDetail() {
<div className='mb-4 flex flex-wrap gap-4 text-sm'>
<span>
{t('status')}:{' '}
<Badge className={getStatusBadgeClass(course.data.status)}>{capitalizeFirst(course.data.status)}</Badge>
<Badge className={getStatusBadgeClass(course.data.status)}>{translateStatus(course.data.status)}</Badge>
</span>
<span>
{t('age')}: <Badge className='border-rose-300 bg-rose-100 text-rose-800'>{course.data.ageRangeLabel}</Badge>
</span>
<span>
{t('level')}:{' '}
<Badge className={getLevelBadgeClass(course.data.level)}>{capitalizeFirst(course.data.level)}</Badge>
<Badge className={getLevelBadgeClass(course.data.level)}>{translateLevel(course.data.level)}</Badge>
</span>
</div>
<hr className='mb-6 border-gray-300' />
Expand Down
10 changes: 3 additions & 7 deletions src/features/resource/course/components/upsert/UpsertCourse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ const defaultCourseData: CourseFormData = {
prerequisites: '',
studentTasks: '',
level: CourseLevel.BEGINNER,
imageUrl: null as any,
price: 1000
imageUrl: null as any
}

async function CreateCourseJsonPayload(data: CourseFormData, userId: string) {
Expand All @@ -50,8 +49,7 @@ async function CreateCourseJsonPayload(data: CourseFormData, userId: string) {
studentTasks: data.studentTasks,
prerequisites: data.prerequisites,
level: data.level,
image: imageBase64,
price: data.price
image: imageBase64
}
}

Expand All @@ -68,7 +66,6 @@ async function PatchCourseJsonPayload(oldData: CourseFormData, newData: CourseFo
if (oldData.code !== newData.code) patchData.code = newData.code
if (oldData.prerequisites !== newData.prerequisites) patchData.prerequisites = newData.prerequisites
if (oldData.level !== newData.level) patchData.level = newData.level
if (oldData.price !== newData.price) patchData.price = newData.price

if (newData.imageUrl && typeof newData.imageUrl !== 'string') {
const base64 = await fileToBase64(newData.imageUrl)
Expand Down Expand Up @@ -101,8 +98,7 @@ function mapCourseToFormData(course: ApiSuccessResponse<Course>): CourseFormData
prerequisites: course.data.prerequisites ?? '',
ageRangeId: course.data.ageRangeId?.toString() ?? '',
imageUrl: null as any,
imagePreviewUrl: course.data.imageUrl ?? undefined,
price: course.data.price ?? 0
imagePreviewUrl: course.data.imageUrl ?? undefined
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/features/resource/course/forms/courseForm.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ export function useCourseSchemas() {
slug: z.string().optional(),
description: z.string().min(50, tv('course.description', { length: 50 })),
ageRangeId: z.string().min(1, tv('course.ageRangeId')),
imagePreviewUrl: z.string().optional(),
price: z.number().min(1000, tv('course.priceMin', { min: 1000 }))
imagePreviewUrl: z.string().optional()
})

const createCourseSchema = baseCourseSchema.extend({
Expand Down
7 changes: 7 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,10 @@ export const useStatusTranslation = () => {
return tc(status.toLowerCase())
}
}

export const useLevelTranslation = () => {
const tc = useTranslations('common.level')
return (level: string) => {
return tc(level.toLowerCase())
}
}