hierarchy cleanup
This commit is contained in:
parent
dd6fe3a9c8
commit
e46e0314ed
|
|
@ -145,8 +145,8 @@ export const updateCourseStatus = (courseId: number, isActive: boolean) =>
|
|||
export const updateCourse = (courseId: number, data: UpdateCourseRequest) =>
|
||||
http.put(`/course-management/courses/${courseId}`, data)
|
||||
|
||||
// SubCourse APIs (New Hierarchy)
|
||||
export const getSubCoursesByCourse = (courseId: number) =>
|
||||
// Sub-Module APIs (Unified Hierarchy)
|
||||
export const getSubModulesByCourse = (courseId: number) =>
|
||||
http.get(`/course-management/courses/${courseId}/hierarchy`).then((res) => {
|
||||
const rows: CourseHierarchyRow[] = res.data?.data ?? []
|
||||
const subModuleMap = new Map<number, { id: number; course_id: number; module_id?: number; title: string; description: string; level: string; cefr_level?: string; thumbnail: string; display_order: number; sub_level?: string; is_active: boolean }>()
|
||||
|
|
@ -178,7 +178,7 @@ export const getSubCoursesByCourse = (courseId: number) =>
|
|||
} as unknown as { data: GetSubCoursesResponse }
|
||||
})
|
||||
|
||||
export const createSubCourse = (data: CreateSubCourseRequest) =>
|
||||
export const createSubModule = (data: CreateSubCourseRequest) =>
|
||||
http
|
||||
.post("/course-management/levels", {
|
||||
course_id: data.course_id,
|
||||
|
|
@ -205,23 +205,23 @@ export const createSubCourse = (data: CreateSubCourseRequest) =>
|
|||
}),
|
||||
)
|
||||
|
||||
export const updateSubCourseThumbnail = (subCourseId: number, thumbnailUrl: string) =>
|
||||
http.post(`/course-management/sub-courses/${subCourseId}/thumbnail`, {
|
||||
export const updateSubModuleThumbnail = (subModuleId: number, thumbnailUrl: string) =>
|
||||
http.post(`/course-management/sub-courses/${subModuleId}/thumbnail`, {
|
||||
thumbnail_url: thumbnailUrl,
|
||||
})
|
||||
|
||||
export const updateSubCourse = (subCourseId: number, data: UpdateSubCourseRequest) =>
|
||||
http.put(`/course-management/sub-modules/${subCourseId}`, data)
|
||||
export const updateSubModule = (subModuleId: number, data: UpdateSubCourseRequest) =>
|
||||
http.put(`/course-management/sub-modules/${subModuleId}`, data)
|
||||
|
||||
export const updateSubCourseStatus = (subCourseId: number, data: UpdateSubCourseStatusRequest) =>
|
||||
http.put(`/course-management/sub-modules/${subCourseId}`, data)
|
||||
export const updateSubModuleStatus = (subModuleId: number, data: UpdateSubCourseStatusRequest) =>
|
||||
http.put(`/course-management/sub-modules/${subModuleId}`, data)
|
||||
|
||||
export const deleteSubCourse = (subCourseId: number) =>
|
||||
http.delete(`/course-management/sub-modules/${subCourseId}`)
|
||||
export const deleteSubModule = (subModuleId: number) =>
|
||||
http.delete(`/course-management/sub-modules/${subModuleId}`)
|
||||
|
||||
// SubCourse Video APIs
|
||||
export const getVideosBySubCourse = (subCourseId: number) =>
|
||||
http.get<GetSubCourseVideosResponse>(`/course-management/sub-modules/${subCourseId}/videos`)
|
||||
// Sub-Module Video APIs
|
||||
export const getVideosBySubModule = (subModuleId: number) =>
|
||||
http.get<GetSubCourseVideosResponse>(`/course-management/sub-modules/${subModuleId}/videos`)
|
||||
|
||||
export const createSubCourseVideo = (data: CreateSubCourseVideoRequest) =>
|
||||
http.post("/course-management/sub-module-videos", {
|
||||
|
|
@ -250,11 +250,10 @@ export const updateSubCourseVideo = (videoId: number, data: UpdateSubCourseVideo
|
|||
export const deleteSubCourseVideo = (videoId: number) =>
|
||||
http.delete(`/course-management/sub-module-videos/${videoId}`)
|
||||
|
||||
// Practice APIs - for SubCourse practices (New Hierarchy)
|
||||
// Practices are question sets: POST /question-sets with set_type: "PRACTICE", owner_type: "SUB_COURSE".
|
||||
export const getPracticesBySubCourse = (subCourseId: number) =>
|
||||
// Practice APIs - for Sub-Module practices (Unified Hierarchy)
|
||||
export const getPracticesBySubModule = (subModuleId: number) =>
|
||||
http.get<GetQuestionSetsResponse>("/question-sets/by-owner", {
|
||||
params: { owner_type: "SUB_MODULE", owner_id: subCourseId },
|
||||
params: { owner_type: "SUB_MODULE", owner_id: subModuleId },
|
||||
})
|
||||
|
||||
export const createPractice = (data: CreatePracticeRequest) =>
|
||||
|
|
@ -427,15 +426,15 @@ export const deleteQuestionSet = (questionSetId: number) =>
|
|||
export const createVimeoVideo = (data: CreateVimeoVideoRequest) =>
|
||||
http.post("/course-management/videos/vimeo", data)
|
||||
|
||||
// Sub-course Prerequisite APIs
|
||||
export const getSubCoursePrerequisites = (subCourseId: number) =>
|
||||
http.get<GetSubCoursePrerequisitesResponse>(`/course-management/sub-courses/${subCourseId}/prerequisites`)
|
||||
// Sub-module Prerequisite APIs
|
||||
export const getSubModulePrerequisites = (subModuleId: number) =>
|
||||
http.get<GetSubCoursePrerequisitesResponse>(`/course-management/sub-courses/${subModuleId}/prerequisites`)
|
||||
|
||||
export const addSubCoursePrerequisite = (subCourseId: number, data: AddSubCoursePrerequisiteRequest) =>
|
||||
http.post(`/course-management/sub-courses/${subCourseId}/prerequisites`, data)
|
||||
export const addSubModulePrerequisite = (subModuleId: number, data: AddSubCoursePrerequisiteRequest) =>
|
||||
http.post(`/course-management/sub-courses/${subModuleId}/prerequisites`, data)
|
||||
|
||||
export const removeSubCoursePrerequisite = (subCourseId: number, prerequisiteId: number) =>
|
||||
http.delete(`/course-management/sub-courses/${subCourseId}/prerequisites/${prerequisiteId}`)
|
||||
export const removeSubModulePrerequisite = (subModuleId: number, prerequisiteId: number) =>
|
||||
http.delete(`/course-management/sub-courses/${subModuleId}/prerequisites/${prerequisiteId}`)
|
||||
|
||||
// Learning Path APIs
|
||||
export const getLearningPath = (courseId: number) =>
|
||||
|
|
@ -476,9 +475,9 @@ export const createHumanLanguageLesson = (data: CreateHumanLanguageLessonRequest
|
|||
}),
|
||||
)
|
||||
|
||||
export const getSubCourseEntryAssessment = (subCourseId: number) =>
|
||||
export const getSubModuleEntryAssessment = (subModuleId: number) =>
|
||||
http.get<GetSubCourseEntryAssessmentResponse>(
|
||||
`/question-sets/sub-courses/${subCourseId}/entry-assessment`,
|
||||
`/question-sets/sub-courses/${subModuleId}/entry-assessment`,
|
||||
)
|
||||
|
||||
const buildReorderPayload = (items: ReorderItem[]) => {
|
||||
|
|
@ -508,9 +507,24 @@ export const reorderCategories = (items: ReorderItem[]) =>
|
|||
export const reorderCourses = (items: ReorderItem[]) =>
|
||||
http.put("/course-management/courses/reorder", buildReorderPayload(items))
|
||||
|
||||
export const reorderSubCourses = (items: ReorderItem[]) =>
|
||||
export const reorderSubModules = (items: ReorderItem[]) =>
|
||||
http.put("/course-management/sub-courses/reorder", buildReorderPayload(items))
|
||||
|
||||
// Backward-compatible aliases
|
||||
export const getSubCoursesByCourse = getSubModulesByCourse
|
||||
export const createSubCourse = createSubModule
|
||||
export const updateSubCourseThumbnail = updateSubModuleThumbnail
|
||||
export const updateSubCourse = updateSubModule
|
||||
export const updateSubCourseStatus = updateSubModuleStatus
|
||||
export const deleteSubCourse = deleteSubModule
|
||||
export const getVideosBySubCourse = getVideosBySubModule
|
||||
export const getPracticesBySubCourse = getPracticesBySubModule
|
||||
export const getSubCoursePrerequisites = getSubModulePrerequisites
|
||||
export const addSubCoursePrerequisite = addSubModulePrerequisite
|
||||
export const removeSubCoursePrerequisite = removeSubModulePrerequisite
|
||||
export const getSubCourseEntryAssessment = getSubModuleEntryAssessment
|
||||
export const reorderSubCourses = reorderSubModules
|
||||
|
||||
export const reorderVideos = (items: ReorderItem[]) =>
|
||||
http.put("/course-management/videos/reorder", buildReorderPayload(items))
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { ContentOverviewPage } from "../pages/content-management/ContentOverview
|
|||
import { CoursesPage } from "../pages/content-management/CoursesPage"
|
||||
import { PracticeQuestionsPage } from "../pages/content-management/PracticeQuestionsPage"
|
||||
import { AddNewPracticePage } from "../pages/content-management/AddNewPracticePage"
|
||||
import { SubCoursesPage } from "../pages/content-management/SubCoursesPage"
|
||||
import { SubCourseContentPage } from "../pages/content-management/SubCourseContentPage"
|
||||
import { SubModulesPage } from "../pages/content-management/SubCoursesPage"
|
||||
import { SubModuleContentPage } from "../pages/content-management/SubCourseContentPage"
|
||||
import { SpeakingPage } from "../pages/content-management/SpeakingPage"
|
||||
import { AddVideoPage } from "../pages/content-management/AddVideoPage"
|
||||
import { AddPracticePage } from "../pages/content-management/AddPracticePage"
|
||||
|
|
@ -80,24 +80,29 @@ export function AppRoutes() {
|
|||
<Route path="flows" element={<CourseFlowBuilderPage />} />
|
||||
<Route path="human-language" element={<HumanLanguagePage />} />
|
||||
<Route
|
||||
path="human-language/:categoryId/:courseId/sub-module/:subCourseId/add-practice"
|
||||
path="human-language/:categoryId/:courseId/sub-module/:subModuleId/add-practice"
|
||||
element={<AddNewPracticePage />}
|
||||
/>
|
||||
<Route
|
||||
path="human-language/:categoryId/:courseId/sub-module/:subCourseId/practices/:practiceId/questions"
|
||||
path="human-language/:categoryId/:courseId/sub-module/:subModuleId/practices/:practiceId/questions"
|
||||
element={<PracticeQuestionsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="human-language/:categoryId/:courseId/sub-module/:subCourseId"
|
||||
path="human-language/:categoryId/:courseId/sub-module/:subModuleId"
|
||||
element={<HumanLanguageSubModulePage />}
|
||||
/>
|
||||
<Route path="category/:categoryId" element={<ContentOverviewPage />} />
|
||||
<Route path="category/:categoryId/courses" element={<CoursesPage />} />
|
||||
{/* Course → Sub-course → Video/Practice */}
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-courses" element={<SubCoursesPage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subCourseId" element={<SubCourseContentPage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subCourseId/add-practice" element={<AddNewPracticePage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subCourseId/practices/:practiceId/questions" element={<PracticeQuestionsPage />} />
|
||||
{/* Course → Sub-module → Lesson/Practice */}
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-modules" element={<SubModulesPage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId" element={<SubModuleContentPage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId/add-practice" element={<AddNewPracticePage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId/practices/:practiceId/questions" element={<PracticeQuestionsPage />} />
|
||||
{/* Legacy aliases */}
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-courses" element={<SubModulesPage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId" element={<SubModuleContentPage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId/add-practice" element={<AddNewPracticePage />} />
|
||||
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId/practices/:practiceId/questions" element={<PracticeQuestionsPage />} />
|
||||
<Route path="category/:categoryId/courses/add-video" element={<AddVideoPage />} />
|
||||
<Route path="speaking" element={<SpeakingPage />} />
|
||||
<Route path="speaking/add-practice" element={<AddPracticePage />} />
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export function DashboardPage() {
|
|||
icon={BookOpen}
|
||||
label="Courses"
|
||||
value={dashboard.courses.total_courses.toLocaleString()}
|
||||
deltaLabel={`${dashboard.courses.total_sub_courses} sub-courses, ${dashboard.courses.total_videos} videos`}
|
||||
deltaLabel={`${dashboard.courses.total_sub_courses} sub-modules, ${dashboard.courses.total_videos} videos`}
|
||||
deltaPositive
|
||||
/>
|
||||
<StatCard
|
||||
|
|
|
|||
|
|
@ -167,18 +167,18 @@ function createEmptyQuestion(id: string): Question {
|
|||
}
|
||||
|
||||
export function AddNewPracticePage() {
|
||||
const { categoryId, courseId, subCourseId } = useParams()
|
||||
const { categoryId, courseId, subModuleId } = useParams()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const source = searchParams.get("source")
|
||||
const backTo = useMemo(() => {
|
||||
if (location.pathname.includes("/content/human-language/") && location.pathname.includes("/sub-module/")) {
|
||||
return `/content/human-language/${categoryId}/${courseId}/sub-module/${subCourseId}`
|
||||
return `/content/human-language/${categoryId}/${courseId}/sub-module/${subModuleId}`
|
||||
}
|
||||
if (source === "human-language") return "/content/human-language"
|
||||
return `/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`
|
||||
}, [location.pathname, source, categoryId, courseId, subCourseId])
|
||||
return `/content/category/${categoryId}/courses/${courseId}/sub-modules/${subModuleId}`
|
||||
}, [location.pathname, source, categoryId, courseId, subModuleId])
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<Step>(1)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
|
@ -312,7 +312,7 @@ export function AddNewPracticePage() {
|
|||
title: practiceTitle || "Untitled Practice",
|
||||
set_type: "PRACTICE",
|
||||
owner_type: "SUB_MODULE",
|
||||
owner_id: Number(subCourseId),
|
||||
owner_id: Number(subModuleId),
|
||||
...(practiceDescription.trim() ? { description: practiceDescription.trim() } : {}),
|
||||
...(persona?.name ? { persona: persona.name } : {}),
|
||||
shuffle_questions: shuffleQuestions,
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ export function AllCoursesPage() {
|
|||
className="group cursor-pointer"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/content/category/${course.category_id}/courses/${course.id}/sub-courses`,
|
||||
`/content/category/${course.category_id}/courses/${course.id}/sub-modules`,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
|
@ -393,7 +393,7 @@ export function AllCoursesPage() {
|
|||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigate(
|
||||
`/content/category/${course.category_id}/courses/${course.id}/sub-courses`,
|
||||
`/content/category/${course.category_id}/courses/${course.id}/sub-modules`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ import {
|
|||
getCoursesByCategory,
|
||||
getLearningPath,
|
||||
getQuestionSetsByOwner,
|
||||
getSubCourseEntryAssessment,
|
||||
getSubModuleEntryAssessment,
|
||||
reorderCategories,
|
||||
reorderCourses,
|
||||
reorderSubCourses,
|
||||
reorderSubModules,
|
||||
reorderVideos,
|
||||
reorderPractices,
|
||||
} from "../../api/courses.api"
|
||||
|
|
@ -320,7 +320,7 @@ export function CourseFlowBuilderPage() {
|
|||
try {
|
||||
const [setsRes, entryRes] = await Promise.allSettled([
|
||||
getQuestionSetsByOwner("SUB_COURSE", subCourseId),
|
||||
getSubCourseEntryAssessment(subCourseId),
|
||||
getSubModuleEntryAssessment(subCourseId),
|
||||
])
|
||||
|
||||
// No practice sets is a valid empty-state scenario; do not toast for 404/empty.
|
||||
|
|
@ -429,12 +429,12 @@ export function CourseFlowBuilderPage() {
|
|||
}))
|
||||
const previous = items
|
||||
setLearningPath((prev) => (prev ? { ...prev, sub_courses: reordered } : prev))
|
||||
setSavingKey("sub-courses")
|
||||
setSavingKey("sub-modules")
|
||||
try {
|
||||
await reorderSubCourses(toReorderItems(reordered))
|
||||
await reorderSubModules(toReorderItems(reordered))
|
||||
} catch (err: any) {
|
||||
setLearningPath((prev) => (prev ? { ...prev, sub_courses: previous } : prev))
|
||||
toast.error(err?.response?.data?.message || "Failed to reorder sub-courses.")
|
||||
toast.error(err?.response?.data?.message || "Failed to reorder sub-modules.")
|
||||
} finally {
|
||||
setSavingKey(null)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ export function CoursesPage() {
|
|||
}
|
||||
|
||||
const handleCourseClick = (courseId: number) => {
|
||||
navigate(`/content/category/${categoryId}/courses/${courseId}/sub-courses`)
|
||||
navigate(`/content/category/${categoryId}/courses/${courseId}/sub-modules`)
|
||||
}
|
||||
|
||||
const handleViewRatings = async (courseId: number) => {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import {
|
|||
createHumanLanguageLesson,
|
||||
deleteQuestionSet,
|
||||
deleteQuestion,
|
||||
deleteSubCourse,
|
||||
deleteSubModule,
|
||||
getHumanLanguageHierarchy,
|
||||
getQuestionById,
|
||||
getPracticeQuestions,
|
||||
|
|
@ -569,7 +569,7 @@ export function HumanLanguagePage() {
|
|||
setDeletingKey(key)
|
||||
try {
|
||||
for (const id of ids) {
|
||||
await deleteSubCourse(id)
|
||||
await deleteSubModule(id)
|
||||
}
|
||||
toast.success(successMessage)
|
||||
await loadHierarchy()
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import { Badge } from "../../components/ui/badge"
|
|||
import { Button } from "../../components/ui/button"
|
||||
import { Input } from "../../components/ui/input"
|
||||
import {
|
||||
getSubCoursesByCourse,
|
||||
getSubModulesByCourse,
|
||||
getQuestionSetsByOwner,
|
||||
getVideosBySubCourse,
|
||||
getVideosBySubModule,
|
||||
updatePractice,
|
||||
deleteQuestionSet,
|
||||
createCourseVideo,
|
||||
|
|
@ -34,10 +34,10 @@ type StatusFilter = "all" | "published" | "draft" | "archived"
|
|||
|
||||
/** Human Language–only sub-module editor: lesson (videos) + practice tabs; not used by general course flows. */
|
||||
export function HumanLanguageSubModulePage() {
|
||||
const { categoryId, courseId, subCourseId } = useParams<{
|
||||
const { categoryId, courseId, subModuleId } = useParams<{
|
||||
categoryId: string
|
||||
courseId: string
|
||||
subCourseId: string
|
||||
subModuleId: string
|
||||
}>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
|
@ -92,12 +92,12 @@ export function HumanLanguageSubModulePage() {
|
|||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!subCourseId || !courseId) return
|
||||
if (!subModuleId || !courseId) return
|
||||
|
||||
try {
|
||||
const subCoursesRes = await getSubCoursesByCourse(Number(courseId))
|
||||
const subCoursesRes = await getSubModulesByCourse(Number(courseId))
|
||||
const foundSubCourse = subCoursesRes.data.data.sub_courses?.find(
|
||||
(sc) => sc.id === Number(subCourseId)
|
||||
(sc) => sc.id === Number(subModuleId)
|
||||
)
|
||||
setSubCourse(foundSubCourse ?? null)
|
||||
} catch (err) {
|
||||
|
|
@ -109,13 +109,13 @@ export function HumanLanguageSubModulePage() {
|
|||
}
|
||||
|
||||
fetchData()
|
||||
}, [subCourseId, courseId])
|
||||
}, [subModuleId, courseId])
|
||||
|
||||
const fetchPractices = async () => {
|
||||
if (!subCourseId) return
|
||||
if (!subModuleId) return
|
||||
setPracticesLoading(true)
|
||||
try {
|
||||
const res = await getQuestionSetsByOwner("SUB_MODULE", Number(subCourseId))
|
||||
const res = await getQuestionSetsByOwner("SUB_MODULE", Number(subModuleId))
|
||||
const raw = res.data.data
|
||||
const list = Array.isArray(raw) ? raw : raw?.question_sets ?? []
|
||||
setPractices(list)
|
||||
|
|
@ -127,10 +127,10 @@ export function HumanLanguageSubModulePage() {
|
|||
}
|
||||
|
||||
const fetchVideos = async () => {
|
||||
if (!subCourseId) return
|
||||
if (!subModuleId) return
|
||||
setVideosLoading(true)
|
||||
try {
|
||||
const res = await getVideosBySubCourse(Number(subCourseId))
|
||||
const res = await getVideosBySubModule(Number(subModuleId))
|
||||
setVideos(res.data.data.videos ?? [])
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch videos:", err)
|
||||
|
|
@ -145,10 +145,10 @@ export function HumanLanguageSubModulePage() {
|
|||
} else if (activeTab === "lesson") {
|
||||
fetchVideos()
|
||||
}
|
||||
}, [activeTab, subCourseId])
|
||||
}, [activeTab, subModuleId])
|
||||
|
||||
const handleAddPractice = () => {
|
||||
navigate(`/content/human-language/${categoryId}/${courseId}/sub-module/${subCourseId}/add-practice`)
|
||||
navigate(`/content/human-language/${categoryId}/${courseId}/sub-module/${subModuleId}/add-practice`)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ export function HumanLanguageSubModulePage() {
|
|||
}
|
||||
|
||||
const handlePracticeClick = (practiceId: number) => {
|
||||
navigate(`/content/human-language/${categoryId}/${courseId}/sub-module/${subCourseId}/practices/${practiceId}/questions`)
|
||||
navigate(`/content/human-language/${categoryId}/${courseId}/sub-module/${subModuleId}/practices/${practiceId}/questions`)
|
||||
}
|
||||
|
||||
const handleAddVideo = () => {
|
||||
|
|
@ -248,7 +248,7 @@ export function HumanLanguageSubModulePage() {
|
|||
}
|
||||
|
||||
const handleSaveNewVideo = async () => {
|
||||
if (!subCourseId || !videoFile) return
|
||||
if (!subModuleId || !videoFile) return
|
||||
setSaving(true)
|
||||
setSaveError(null)
|
||||
try {
|
||||
|
|
@ -270,7 +270,7 @@ export function HumanLanguageSubModulePage() {
|
|||
const finalTitle = videoTitle.trim() || videoFile.name
|
||||
|
||||
await createCourseVideo({
|
||||
sub_course_id: Number(subCourseId),
|
||||
sub_module_id: Number(subModuleId),
|
||||
title: finalTitle,
|
||||
description: videoDescription.trim(),
|
||||
video_url: finalVideoUrl,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const typeColors: Record<QuestionType, string> = {
|
|||
}
|
||||
|
||||
export function PracticeQuestionsPage() {
|
||||
const { categoryId, courseId, subCourseId, practiceId } = useParams()
|
||||
const { categoryId, courseId, subModuleId, practiceId } = useParams()
|
||||
const location = useLocation()
|
||||
|
||||
const [questions, setQuestions] = useState<PracticeQuestion[]>([])
|
||||
|
|
@ -103,10 +103,10 @@ export function PracticeQuestionsPage() {
|
|||
|
||||
const backLink = useMemo(() => {
|
||||
if (location.pathname.includes("/content/human-language/") && location.pathname.includes("/sub-module/")) {
|
||||
return `/content/human-language/${categoryId}/${courseId}/sub-module/${subCourseId}`
|
||||
return `/content/human-language/${categoryId}/${courseId}/sub-module/${subModuleId}`
|
||||
}
|
||||
return `/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`
|
||||
}, [location.pathname, categoryId, courseId, subCourseId])
|
||||
return `/content/category/${categoryId}/courses/${courseId}/sub-modules/${subModuleId}`
|
||||
}, [location.pathname, categoryId, courseId, subModuleId])
|
||||
|
||||
const buildDefaultOptions = (type: QuestionType, sampleAnswerText?: string): DraftOption[] => {
|
||||
if (type === "TRUE_FALSE") {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
getCourseCategories,
|
||||
getCoursesByCategory,
|
||||
getQuestionById,
|
||||
getSubCoursesByCourse,
|
||||
getSubModulesByCourse,
|
||||
createQuestion,
|
||||
createQuestionSet,
|
||||
// getQuestions,
|
||||
|
|
@ -441,7 +441,7 @@ export function SpeakingPage() {
|
|||
|
||||
const subCourseResponses = await Promise.all(
|
||||
courseRecords.map(async ({ category, course }) => {
|
||||
const res = await getSubCoursesByCourse(course.id)
|
||||
const res = await getSubModulesByCourse(course.id)
|
||||
const subCourses = res.data?.data?.sub_courses ?? []
|
||||
return subCourses.map((subCourse: SubCourse) => ({
|
||||
id: subCourse.id,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import { Badge } from "../../components/ui/badge"
|
|||
import { Button } from "../../components/ui/button"
|
||||
import { Input } from "../../components/ui/input"
|
||||
import {
|
||||
getSubCoursesByCourse,
|
||||
getSubModulesByCourse,
|
||||
getQuestionSetsByOwner,
|
||||
getVideosBySubCourse,
|
||||
getVideosBySubModule,
|
||||
updatePractice,
|
||||
deleteQuestionSet,
|
||||
createCourseVideo,
|
||||
|
|
@ -34,11 +34,11 @@ import { SpinnerIcon } from "../../components/ui/spinner-icon"
|
|||
type TabType = "video" | "practice" | "ratings"
|
||||
type StatusFilter = "all" | "published" | "draft" | "archived"
|
||||
|
||||
export function SubCourseContentPage() {
|
||||
const { categoryId, courseId, subCourseId } = useParams<{
|
||||
export function SubModuleContentPage() {
|
||||
const { categoryId, courseId, subModuleId } = useParams<{
|
||||
categoryId: string
|
||||
courseId: string
|
||||
subCourseId: string
|
||||
subModuleId: string
|
||||
}>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
|
@ -99,12 +99,12 @@ export function SubCourseContentPage() {
|
|||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!subCourseId || !courseId) return
|
||||
if (!subModuleId || !courseId) return
|
||||
|
||||
try {
|
||||
const subCoursesRes = await getSubCoursesByCourse(Number(courseId))
|
||||
const subCoursesRes = await getSubModulesByCourse(Number(courseId))
|
||||
const foundSubCourse = subCoursesRes.data.data.sub_courses?.find(
|
||||
(sc) => sc.id === Number(subCourseId)
|
||||
(sc) => sc.id === Number(subModuleId)
|
||||
)
|
||||
setSubCourse(foundSubCourse ?? null)
|
||||
} catch (err) {
|
||||
|
|
@ -116,13 +116,13 @@ export function SubCourseContentPage() {
|
|||
}
|
||||
|
||||
fetchData()
|
||||
}, [subCourseId, courseId])
|
||||
}, [subModuleId, courseId])
|
||||
|
||||
const fetchPractices = async () => {
|
||||
if (!subCourseId) return
|
||||
if (!subModuleId) return
|
||||
setPracticesLoading(true)
|
||||
try {
|
||||
const res = await getQuestionSetsByOwner("SUB_MODULE", Number(subCourseId))
|
||||
const res = await getQuestionSetsByOwner("SUB_MODULE", Number(subModuleId))
|
||||
setPractices(res.data.data ?? [])
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch practices:", err)
|
||||
|
|
@ -132,10 +132,10 @@ export function SubCourseContentPage() {
|
|||
}
|
||||
|
||||
const fetchVideos = async () => {
|
||||
if (!subCourseId) return
|
||||
if (!subModuleId) return
|
||||
setVideosLoading(true)
|
||||
try {
|
||||
const res = await getVideosBySubCourse(Number(subCourseId))
|
||||
const res = await getVideosBySubModule(Number(subModuleId))
|
||||
setVideos(res.data.data.videos ?? [])
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch videos:", err)
|
||||
|
|
@ -145,12 +145,12 @@ export function SubCourseContentPage() {
|
|||
}
|
||||
|
||||
const fetchRatings = async (offset = 0) => {
|
||||
if (!subCourseId) return
|
||||
if (!subModuleId) return
|
||||
setRatingsLoading(true)
|
||||
try {
|
||||
const res = await getRatings({
|
||||
target_type: "sub_course",
|
||||
target_id: Number(subCourseId),
|
||||
target_id: Number(subModuleId),
|
||||
limit: ratingsPageSize,
|
||||
offset,
|
||||
})
|
||||
|
|
@ -170,7 +170,7 @@ export function SubCourseContentPage() {
|
|||
} else if (activeTab === "ratings") {
|
||||
fetchRatings(ratingsPage * ratingsPageSize)
|
||||
}
|
||||
}, [activeTab, subCourseId])
|
||||
}, [activeTab, subModuleId])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "ratings") {
|
||||
|
|
@ -179,7 +179,7 @@ export function SubCourseContentPage() {
|
|||
}, [ratingsPage])
|
||||
|
||||
const handleAddPractice = () => {
|
||||
navigate(`/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}/add-practice`)
|
||||
navigate(`/content/category/${categoryId}/courses/${courseId}/sub-modules/${subModuleId}/add-practice`)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ export function SubCourseContentPage() {
|
|||
}
|
||||
|
||||
const handlePracticeClick = (practiceId: number) => {
|
||||
navigate(`/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}/practices/${practiceId}/questions`)
|
||||
navigate(`/content/category/${categoryId}/courses/${courseId}/sub-modules/${subModuleId}/practices/${practiceId}/questions`)
|
||||
}
|
||||
|
||||
const handleAddVideo = () => {
|
||||
|
|
@ -279,7 +279,7 @@ export function SubCourseContentPage() {
|
|||
}
|
||||
|
||||
const handleSaveNewVideo = async () => {
|
||||
if (!subCourseId || !videoFile) return
|
||||
if (!subModuleId || !videoFile) return
|
||||
setSaving(true)
|
||||
setSaveError(null)
|
||||
try {
|
||||
|
|
@ -301,7 +301,7 @@ export function SubCourseContentPage() {
|
|||
const finalTitle = videoTitle.trim() || videoFile.name
|
||||
|
||||
await createCourseVideo({
|
||||
sub_course_id: Number(subCourseId),
|
||||
sub_module_id: Number(subModuleId),
|
||||
title: finalTitle,
|
||||
description: videoDescription.trim(),
|
||||
video_url: finalVideoUrl,
|
||||
|
|
@ -444,7 +444,7 @@ export function SubCourseContentPage() {
|
|||
<div className="space-y-6">
|
||||
{/* Back Button */}
|
||||
<Link
|
||||
to={`/content/category/${categoryId}/courses/${courseId}/sub-courses`}
|
||||
to={`/content/category/${categoryId}/courses/${courseId}/sub-modules`}
|
||||
className="group inline-flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-grayScale-500 transition-all hover:bg-grayScale-50 hover:text-grayScale-900"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 transition-transform group-hover:-translate-x-0.5" />
|
||||
|
|
|
|||
|
|
@ -24,16 +24,16 @@ import alertSrc from "../../assets/Alert.svg";
|
|||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
getSubCoursesByCourse,
|
||||
getSubModulesByCourse,
|
||||
getCoursesByCategory,
|
||||
getCourseCategories,
|
||||
createSubCourse,
|
||||
updateSubCourse,
|
||||
updateSubCourseStatus,
|
||||
deleteSubCourse,
|
||||
getSubCoursePrerequisites,
|
||||
addSubCoursePrerequisite,
|
||||
removeSubCoursePrerequisite,
|
||||
createSubModule,
|
||||
updateSubModule,
|
||||
updateSubModuleStatus,
|
||||
deleteSubModule,
|
||||
getSubModulePrerequisites,
|
||||
addSubModulePrerequisite,
|
||||
removeSubModulePrerequisite,
|
||||
} from "../../api/courses.api";
|
||||
import { uploadImageFile } from "../../api/files.api";
|
||||
import { Input } from "../../components/ui/input";
|
||||
|
|
@ -47,7 +47,7 @@ import type {
|
|||
import { SpinnerIcon } from "../../components/ui/spinner-icon";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function SubCoursesPage() {
|
||||
export function SubModulesPage() {
|
||||
const { categoryId, courseId } = useParams<{
|
||||
categoryId: string;
|
||||
courseId: string;
|
||||
|
|
@ -122,10 +122,10 @@ export function SubCoursesPage() {
|
|||
if (!courseId) return;
|
||||
|
||||
try {
|
||||
const subCoursesRes = await getSubCoursesByCourse(Number(courseId));
|
||||
const subCoursesRes = await getSubModulesByCourse(Number(courseId));
|
||||
setSubCourses(subCoursesRes.data.data.sub_courses ?? []);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch sub-courses:", err);
|
||||
console.error("Failed to fetch sub-modules:", err);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ export function SubCoursesPage() {
|
|||
try {
|
||||
const results = await Promise.all(
|
||||
scs.map((sc) =>
|
||||
getSubCoursePrerequisites(sc.id).then((res) => ({
|
||||
getSubModulePrerequisites(sc.id).then((res) => ({
|
||||
id: sc.id,
|
||||
data: res.data.data ?? [],
|
||||
})),
|
||||
|
|
@ -159,7 +159,7 @@ export function SubCoursesPage() {
|
|||
|
||||
try {
|
||||
const [subCoursesRes, coursesRes, categoriesRes] = await Promise.all([
|
||||
getSubCoursesByCourse(Number(courseId)),
|
||||
getSubModulesByCourse(Number(courseId)),
|
||||
getCoursesByCategory(Number(categoryId)),
|
||||
getCourseCategories(),
|
||||
]);
|
||||
|
|
@ -176,7 +176,7 @@ export function SubCoursesPage() {
|
|||
);
|
||||
setCategory(foundCategory ?? null);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch sub-courses:", err);
|
||||
console.error("Failed to fetch sub-modules:", err);
|
||||
setError("Failed to load courses");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -195,7 +195,7 @@ export function SubCoursesPage() {
|
|||
const handleToggleStatus = async (subCourse: SubCourse) => {
|
||||
setTogglingId(subCourse.id);
|
||||
try {
|
||||
await updateSubCourseStatus(subCourse.id, {
|
||||
await updateSubModuleStatus(subCourse.id, {
|
||||
is_active: !subCourse.is_active,
|
||||
level: subCourse.level,
|
||||
title: subCourse.title,
|
||||
|
|
@ -218,7 +218,7 @@ export function SubCoursesPage() {
|
|||
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deleteSubCourse(subCourseToDelete.id);
|
||||
await deleteSubModule(subCourseToDelete.id);
|
||||
setShowDeleteModal(false);
|
||||
setSubCourseToDelete(null);
|
||||
await fetchSubCourses();
|
||||
|
|
@ -264,7 +264,7 @@ export function SubCoursesPage() {
|
|||
? parsedOrder
|
||||
: nextSubCourseDisplayOrder();
|
||||
|
||||
await createSubCourse({
|
||||
await createSubModule({
|
||||
course_id: Number(courseId),
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
|
|
@ -309,7 +309,7 @@ export function SubCoursesPage() {
|
|||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
await updateSubCourse(subCourseToEdit.id, {
|
||||
await updateSubModule(subCourseToEdit.id, {
|
||||
title,
|
||||
description,
|
||||
level,
|
||||
|
|
@ -328,9 +328,9 @@ export function SubCoursesPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleSubCourseClick = (subCourseId: number) => {
|
||||
const handleSubModuleClick = (subModuleId: number) => {
|
||||
navigate(
|
||||
`/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`,
|
||||
`/content/category/${categoryId}/courses/${courseId}/sub-modules/${subModuleId}`,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -340,7 +340,7 @@ export function SubCoursesPage() {
|
|||
setPrereqLoading(true);
|
||||
setSelectedPrereqId(0);
|
||||
try {
|
||||
const res = await getSubCoursePrerequisites(subCourse.id);
|
||||
const res = await getSubModulePrerequisites(subCourse.id);
|
||||
setPrerequisites(res.data.data ?? []);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch prerequisites:", err);
|
||||
|
|
@ -354,10 +354,10 @@ export function SubCoursesPage() {
|
|||
if (!prereqSubCourse || !selectedPrereqId) return;
|
||||
setPrereqAdding(true);
|
||||
try {
|
||||
await addSubCoursePrerequisite(prereqSubCourse.id, {
|
||||
await addSubModulePrerequisite(prereqSubCourse.id, {
|
||||
prerequisite_sub_course_id: selectedPrereqId,
|
||||
});
|
||||
const res = await getSubCoursePrerequisites(prereqSubCourse.id);
|
||||
const res = await getSubModulePrerequisites(prereqSubCourse.id);
|
||||
setPrerequisites(res.data.data ?? []);
|
||||
setSelectedPrereqId(0);
|
||||
} catch (err) {
|
||||
|
|
@ -371,8 +371,8 @@ export function SubCoursesPage() {
|
|||
if (!prereqSubCourse) return;
|
||||
setPrereqRemoving(prereqId);
|
||||
try {
|
||||
await removeSubCoursePrerequisite(prereqSubCourse.id, prereqId);
|
||||
const res = await getSubCoursePrerequisites(prereqSubCourse.id);
|
||||
await removeSubModulePrerequisite(prereqSubCourse.id, prereqId);
|
||||
const res = await getSubModulePrerequisites(prereqSubCourse.id);
|
||||
setPrerequisites(res.data.data ?? []);
|
||||
} catch (err) {
|
||||
console.error("Failed to remove prerequisite:", err);
|
||||
|
|
@ -385,7 +385,7 @@ export function SubCoursesPage() {
|
|||
const flowLayers = (() => {
|
||||
if (subCourses.length === 0) return [];
|
||||
|
||||
// Find sub-courses with no prerequisites (roots)
|
||||
// Find sub-modules with no prerequisites (roots)
|
||||
const hasPrereqs = new Set<number>();
|
||||
const isPrereqOf = new Map<number, number[]>(); // prereqId -> [subCourseIds that depend on it]
|
||||
|
||||
|
|
@ -557,7 +557,7 @@ export function SubCoursesPage() {
|
|||
<Card
|
||||
key={subCourse.id}
|
||||
className="group cursor-pointer overflow-hidden border border-grayScale-100 bg-white shadow-sm transition-all duration-200 hover:shadow-soft hover:-translate-y-1 hover:border-brand-100"
|
||||
onClick={() => handleSubCourseClick(subCourse.id)}
|
||||
onClick={() => handleSubModuleClick(subCourse.id)}
|
||||
>
|
||||
{/* Thumbnail with level badge */}
|
||||
<div className="relative aspect-video w-full overflow-hidden">
|
||||
|
|
@ -738,7 +738,7 @@ export function SubCoursesPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Sub-course Modal — POST /course-management/sub-courses */}
|
||||
{/* Add Sub-module Modal */}
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden bg-black/40 p-3 backdrop-blur-sm sm:p-6">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ export interface GetSubCoursesResponse {
|
|||
metadata: unknown
|
||||
}
|
||||
|
||||
/** POST /course-management/sub-courses */
|
||||
/** Compatibility request used to create sub-modules */
|
||||
export interface CreateSubCourseRequest {
|
||||
course_id: number
|
||||
title: string
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user