new course management hierarchy integration

This commit is contained in:
Yared Yemane 2026-04-10 03:05:29 -07:00
parent 26e1b0a7d5
commit dd6fe3a9c8
5 changed files with 223 additions and 36 deletions

View File

@ -58,14 +58,75 @@ import type {
CreateCourseVideoRequest, CreateCourseVideoRequest,
} from "../types/course.types" } from "../types/course.types"
type UnifiedHierarchyRow = {
category_id: number
category_name: string
sub_category_id?: number | null
sub_category_name?: string | null
course_id?: number | null
course_title?: string | null
}
type CourseHierarchyRow = {
course_id: number
course_title: string
level_id?: number | null
cefr_level?: string | null
module_id?: number | null
module_title?: string | null
sub_module_id?: number | null
sub_module_title?: string | null
}
export const getCourseCategories = () => export const getCourseCategories = () =>
http.get<GetCourseCategoriesResponse>("/course-management/categories") http.get("/course-management/hierarchy").then((res) => {
const rows: UnifiedHierarchyRow[] = res.data?.data ?? []
const categoriesMap = new Map<number, { id: number; name: string; is_active: boolean; created_at: string }>()
rows.forEach((r) => {
if (!categoriesMap.has(r.category_id)) {
categoriesMap.set(r.category_id, {
id: r.category_id,
name: r.category_name,
is_active: true,
created_at: new Date().toISOString(),
})
}
})
const categories = Array.from(categoriesMap.values())
return {
...res,
data: {
...res.data,
data: {
categories,
total_count: categories.length,
},
},
} as unknown as { data: GetCourseCategoriesResponse }
})
export const createCourseCategory = (data: CreateCourseCategoryRequest) => export const createCourseCategory = (data: CreateCourseCategoryRequest) =>
http.post("/course-management/categories", data) http.post("/course-management/sub-categories", { category_id: data.parent_id ?? 1, name: data.name })
export const getCoursesByCategory = (categoryId: number) => export const getCoursesByCategory = (categoryId: number) =>
http.get<GetCoursesResponse>(`/course-management/categories/${categoryId}/courses`) http.get("/course-management/hierarchy").then((res) => {
const rows: UnifiedHierarchyRow[] = res.data?.data ?? []
const courses = rows
.filter((r) => r.category_id === categoryId && r.course_id)
.map((r) => ({
id: Number(r.course_id),
category_id: r.category_id,
sub_category_id: r.sub_category_id ?? null,
title: r.course_title ?? "",
description: "",
thumbnail: "",
is_active: true,
}))
return {
...res,
data: { ...res.data, data: { courses, total_count: courses.length } },
} as unknown as { data: GetCoursesResponse }
})
export const createCourse = (data: CreateCourseRequest) => export const createCourse = (data: CreateCourseRequest) =>
http.post("/course-management/courses", data) http.post("/course-management/courses", data)
@ -86,10 +147,63 @@ export const updateCourse = (courseId: number, data: UpdateCourseRequest) =>
// SubCourse APIs (New Hierarchy) // SubCourse APIs (New Hierarchy)
export const getSubCoursesByCourse = (courseId: number) => export const getSubCoursesByCourse = (courseId: number) =>
http.get<GetSubCoursesResponse>(`/course-management/courses/${courseId}/sub-courses`) 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 }>()
rows.forEach((r, idx) => {
if (!r.sub_module_id) return
if (!subModuleMap.has(r.sub_module_id)) {
subModuleMap.set(r.sub_module_id, {
id: r.sub_module_id,
course_id: courseId,
module_id: r.module_id ?? undefined,
title: r.sub_module_title ?? "",
description: "",
level: r.cefr_level ?? "",
cefr_level: r.cefr_level ?? undefined,
thumbnail: "",
display_order: idx + 1,
sub_level: r.cefr_level ?? undefined,
is_active: true,
})
}
})
const sub_courses = Array.from(subModuleMap.values())
return {
...res,
data: {
...res.data,
data: { sub_courses, total_count: sub_courses.length },
},
} as unknown as { data: GetSubCoursesResponse }
})
export const createSubCourse = (data: CreateSubCourseRequest) => export const createSubCourse = (data: CreateSubCourseRequest) =>
http.post("/course-management/sub-courses", data) http
.post("/course-management/levels", {
course_id: data.course_id,
cefr_level: data.sub_level || data.level,
display_order: data.display_order ?? 0,
is_active: true,
})
.then((levelRes) =>
http.post("/course-management/modules", {
level_id: levelRes.data?.data?.id,
title: `${data.sub_level || data.level} Module`,
description: `${data.title} container module`,
display_order: 1,
is_active: true,
}),
)
.then((moduleRes) =>
http.post("/course-management/sub-modules", {
module_id: moduleRes.data?.data?.id,
title: data.title,
description: data.description,
display_order: data.display_order ?? 0,
is_active: true,
}),
)
export const updateSubCourseThumbnail = (subCourseId: number, thumbnailUrl: string) => export const updateSubCourseThumbnail = (subCourseId: number, thumbnailUrl: string) =>
http.post(`/course-management/sub-courses/${subCourseId}/thumbnail`, { http.post(`/course-management/sub-courses/${subCourseId}/thumbnail`, {
@ -97,46 +211,78 @@ export const updateSubCourseThumbnail = (subCourseId: number, thumbnailUrl: stri
}) })
export const updateSubCourse = (subCourseId: number, data: UpdateSubCourseRequest) => export const updateSubCourse = (subCourseId: number, data: UpdateSubCourseRequest) =>
http.patch(`/course-management/sub-courses/${subCourseId}`, data) http.put(`/course-management/sub-modules/${subCourseId}`, data)
export const updateSubCourseStatus = (subCourseId: number, data: UpdateSubCourseStatusRequest) => export const updateSubCourseStatus = (subCourseId: number, data: UpdateSubCourseStatusRequest) =>
http.patch(`/course-management/sub-courses/${subCourseId}`, data) http.put(`/course-management/sub-modules/${subCourseId}`, data)
export const deleteSubCourse = (subCourseId: number) => export const deleteSubCourse = (subCourseId: number) =>
http.delete(`/course-management/sub-courses/${subCourseId}`) http.delete(`/course-management/sub-modules/${subCourseId}`)
// SubCourse Video APIs // SubCourse Video APIs
export const getVideosBySubCourse = (subCourseId: number) => export const getVideosBySubCourse = (subCourseId: number) =>
http.get<GetSubCourseVideosResponse>(`/course-management/sub-courses/${subCourseId}/videos`) http.get<GetSubCourseVideosResponse>(`/course-management/sub-modules/${subCourseId}/videos`)
export const createSubCourseVideo = (data: CreateSubCourseVideoRequest) => export const createSubCourseVideo = (data: CreateSubCourseVideoRequest) =>
http.post("/course-management/sub-course-videos", data) http.post("/course-management/sub-module-videos", {
sub_module_id: data.sub_module_id ?? data.sub_course_id,
title: data.title,
description: data.description,
video_url: data.video_url,
})
export const createCourseVideo = (data: CreateCourseVideoRequest) => export const createCourseVideo = (data: CreateCourseVideoRequest) =>
http.post("/course-management/videos", data) http.post("/course-management/sub-module-videos", {
sub_module_id: data.sub_module_id ?? data.sub_course_id,
title: data.title,
description: data.description,
video_url: data.video_url,
duration: data.duration,
resolution: data.resolution,
visibility: data.visibility,
display_order: data.display_order,
status: data.status,
})
export const updateSubCourseVideo = (videoId: number, data: UpdateSubCourseVideoRequest) => export const updateSubCourseVideo = (videoId: number, data: UpdateSubCourseVideoRequest) =>
http.put(`/course-management/sub-course-videos/${videoId}`, data) http.put(`/course-management/sub-module-videos/${videoId}`, data)
export const deleteSubCourseVideo = (videoId: number) => export const deleteSubCourseVideo = (videoId: number) =>
http.delete(`/course-management/sub-course-videos/${videoId}`) http.delete(`/course-management/sub-module-videos/${videoId}`)
// Practice APIs - for SubCourse practices (New Hierarchy) // Practice APIs - for SubCourse practices (New Hierarchy)
// Practices are question sets: POST /question-sets with set_type: "PRACTICE", owner_type: "SUB_COURSE". // Practices are question sets: POST /question-sets with set_type: "PRACTICE", owner_type: "SUB_COURSE".
export const getPracticesBySubCourse = (subCourseId: number) => export const getPracticesBySubCourse = (subCourseId: number) =>
http.get<GetQuestionSetsResponse>("/question-sets/by-owner", { http.get<GetQuestionSetsResponse>("/question-sets/by-owner", {
params: { owner_type: "SUB_COURSE", owner_id: subCourseId }, params: { owner_type: "SUB_MODULE", owner_id: subCourseId },
}) })
export const createPractice = (data: CreatePracticeRequest) => export const createPractice = (data: CreatePracticeRequest) =>
http.post<CreateQuestionSetResponse>("/question-sets", { http
title: data.title, .post<CreateQuestionSetResponse>("/question-sets", {
set_type: "PRACTICE", title: data.title,
owner_type: "SUB_COURSE", set_type: "PRACTICE",
owner_id: data.sub_course_id, owner_type: "SUB_MODULE",
...(data.description?.trim() ? { description: data.description.trim() } : {}), owner_id: data.sub_module_id ?? data.sub_course_id,
...(data.persona ? { persona: data.persona } : {}), ...(data.description?.trim() ? { description: data.description.trim() } : {}),
}) ...(data.persona ? { persona: data.persona } : {}),
...(data.intro_video_url?.trim() ? { intro_video_url: data.intro_video_url.trim() } : {}),
})
.then((res) => {
const questionSetID = res.data?.data?.id
const subModuleID = data.sub_module_id ?? data.sub_course_id
if (!questionSetID || !subModuleID) return res
return http
.post("/course-management/sub-module-practices", {
sub_module_id: subModuleID,
title: data.title,
description: data.description,
thumbnail: data.thumbnail,
intro_video_url: data.intro_video_url,
question_set_id: questionSetID,
})
.then(() => res)
})
export const updatePractice = (practiceId: number, data: UpdatePracticeRequest) => export const updatePractice = (practiceId: number, data: UpdatePracticeRequest) =>
http.put(`/course-management/practices/${practiceId}`, data) http.put(`/course-management/practices/${practiceId}`, data)
@ -230,7 +376,10 @@ export const getQuestionSets = (params?: GetQuestionSetsParams) =>
export const getQuestionSetsByOwner = (ownerType: string, ownerId: number) => export const getQuestionSetsByOwner = (ownerType: string, ownerId: number) =>
http.get<GetQuestionSetsResponse>("/question-sets/by-owner", { http.get<GetQuestionSetsResponse>("/question-sets/by-owner", {
params: { owner_type: ownerType, owner_id: ownerId }, params: {
owner_type: ownerType === "SUB_COURSE" ? "SUB_MODULE" : ownerType,
owner_id: ownerId,
},
}) })
export const getQuestionSetById = (questionSetId: number) => export const getQuestionSetById = (questionSetId: number) =>
@ -298,10 +447,34 @@ export const getHumanLanguageLessonsByCourse = (courseId: number, cefr_level: st
}) })
export const getHumanLanguageHierarchy = () => export const getHumanLanguageHierarchy = () =>
http.get<GetHumanLanguageHierarchyResponse>("/course-management/human-language/hierarchy") http.get<GetHumanLanguageHierarchyResponse>("/course-management/hierarchy")
export const createHumanLanguageLesson = (data: CreateHumanLanguageLessonRequest) => export const createHumanLanguageLesson = (data: CreateHumanLanguageLessonRequest) =>
http.post("/course-management/human-language/lessons", data) http
.post("/course-management/levels", {
course_id: data.course_id,
cefr_level: data.cefr_level,
display_order: data.display_order ?? 0,
is_active: true,
})
.then((levelRes) =>
http.post("/course-management/modules", {
level_id: levelRes.data?.data?.id,
title: `${data.cefr_level} Module`,
description: "Generated module for CEFR level",
display_order: 1,
is_active: true,
}),
)
.then((moduleRes) =>
http.post("/course-management/sub-modules", {
module_id: moduleRes.data?.data?.id,
title: data.title,
description: data.description ?? "",
display_order: data.display_order ?? 0,
is_active: true,
}),
)
export const getSubCourseEntryAssessment = (subCourseId: number) => export const getSubCourseEntryAssessment = (subCourseId: number) =>
http.get<GetSubCourseEntryAssessmentResponse>( http.get<GetSubCourseEntryAssessmentResponse>(

View File

@ -311,7 +311,7 @@ export function AddNewPracticePage() {
const setRes = await createQuestionSet({ const setRes = await createQuestionSet({
title: practiceTitle || "Untitled Practice", title: practiceTitle || "Untitled Practice",
set_type: "PRACTICE", set_type: "PRACTICE",
owner_type: "SUB_COURSE", owner_type: "SUB_MODULE",
owner_id: Number(subCourseId), owner_id: Number(subCourseId),
...(practiceDescription.trim() ? { description: practiceDescription.trim() } : {}), ...(practiceDescription.trim() ? { description: practiceDescription.trim() } : {}),
...(persona?.name ? { persona: persona.name } : {}), ...(persona?.name ? { persona: persona.name } : {}),

View File

@ -115,7 +115,7 @@ export function HumanLanguageSubModulePage() {
if (!subCourseId) return if (!subCourseId) return
setPracticesLoading(true) setPracticesLoading(true)
try { try {
const res = await getQuestionSetsByOwner("SUB_COURSE", Number(subCourseId)) const res = await getQuestionSetsByOwner("SUB_MODULE", Number(subCourseId))
const raw = res.data.data const raw = res.data.data
const list = Array.isArray(raw) ? raw : raw?.question_sets ?? [] const list = Array.isArray(raw) ? raw : raw?.question_sets ?? []
setPractices(list) setPractices(list)

View File

@ -122,7 +122,7 @@ export function SubCourseContentPage() {
if (!subCourseId) return if (!subCourseId) return
setPracticesLoading(true) setPracticesLoading(true)
try { try {
const res = await getQuestionSetsByOwner("SUB_COURSE", Number(subCourseId)) const res = await getQuestionSetsByOwner("SUB_MODULE", Number(subCourseId))
setPractices(res.data.data ?? []) setPractices(res.data.data ?? [])
} catch (err) { } catch (err) {
console.error("Failed to fetch practices:", err) console.error("Failed to fetch practices:", err)

View File

@ -25,6 +25,7 @@ export interface GetCourseCategoriesResponse {
export interface Course { export interface Course {
id: number id: number
category_id: number category_id: number
sub_category_id?: number | null
title: string title: string
description: string description: string
thumbnail: string thumbnail: string
@ -191,9 +192,11 @@ export interface UpdateModuleStatusRequest {
export interface SubCourse { export interface SubCourse {
id: number id: number
course_id: number course_id: number
module_id?: number
title: string title: string
description: string description: string
level: string level: string
cefr_level?: string
thumbnail: string thumbnail: string
display_order: number display_order: number
sub_level?: string sub_level?: string
@ -237,7 +240,8 @@ export interface UpdateSubCourseStatusRequest {
// SubCourse Video // SubCourse Video
export interface SubCourseVideo { export interface SubCourseVideo {
id: number id: number
sub_course_id: number sub_course_id?: number
sub_module_id?: number
title: string title: string
description: string description: string
video_url: string video_url: string
@ -259,7 +263,8 @@ export interface GetSubCourseVideosResponse {
} }
export interface CreateSubCourseVideoRequest { export interface CreateSubCourseVideoRequest {
sub_course_id: number sub_course_id?: number
sub_module_id?: number
title: string title: string
description: string description: string
video_url: string video_url: string
@ -269,7 +274,8 @@ export type VideoVisibility = "PUBLISHED" | "DRAFT" | "PRIVATE" | "UNLISTED" | s
export type VideoStatus = "PUBLISHED" | "DRAFT" | "ARCHIVED" | string export type VideoStatus = "PUBLISHED" | "DRAFT" | "ARCHIVED" | string
export interface CreateCourseVideoRequest { export interface CreateCourseVideoRequest {
sub_course_id: number sub_course_id?: number
sub_module_id?: number
title: string title: string
description: string description: string
video_url: string video_url: string
@ -281,7 +287,8 @@ export interface CreateCourseVideoRequest {
} }
export interface CreateVimeoVideoRequest { export interface CreateVimeoVideoRequest {
sub_course_id: number sub_course_id?: number
sub_module_id?: number
title: string title: string
description: string description: string
source_url: string source_url: string
@ -298,9 +305,13 @@ export interface UpdateSubCourseVideoRequest {
// Practice now belongs to SubCourse // Practice now belongs to SubCourse
export interface Practice { export interface Practice {
id: number id: number
sub_course_id: number sub_course_id?: number
sub_module_id?: number
title: string title: string
description: string description: string
thumbnail?: string
intro_video_url?: string
question_set_id?: number
banner_image: string banner_image: string
persona: string persona: string
is_active: boolean is_active: boolean
@ -318,9 +329,12 @@ export interface GetPracticesResponse {
} }
export interface CreatePracticeRequest { export interface CreatePracticeRequest {
sub_course_id: number sub_course_id?: number
sub_module_id?: number
title: string title: string
description: string description: string
thumbnail?: string
intro_video_url?: string
persona?: string persona?: string
} }
@ -390,7 +404,7 @@ export interface UpdatePracticeQuestionRequest {
// Question Sets (Practice sets fetched via /question-sets) // Question Sets (Practice sets fetched via /question-sets)
export type QuestionSetType = "PRACTICE" | "EXAM" export type QuestionSetType = "PRACTICE" | "EXAM"
export type QuestionSetStatus = "PUBLISHED" | "DRAFT" | "ARCHIVED" export type QuestionSetStatus = "PUBLISHED" | "DRAFT" | "ARCHIVED"
export type QuestionSetOwnerType = "SUB_COURSE" | "COURSE" export type QuestionSetOwnerType = "SUB_COURSE" | "SUB_MODULE" | "COURSE"
export interface QuestionSet { export interface QuestionSet {
id: number id: number
@ -415,7 +429,7 @@ export interface GetQuestionSetsResponse {
export interface GetQuestionSetsParams { export interface GetQuestionSetsParams {
set_type?: "PRACTICE" | "INITIAL_ASSESSMENT" | "EXAM" | string set_type?: "PRACTICE" | "INITIAL_ASSESSMENT" | "EXAM" | string
owner_type?: "SUB_COURSE" | "COURSE" | string owner_type?: "SUB_COURSE" | "SUB_MODULE" | "COURSE" | string
owner_id?: number owner_id?: number
status?: "DRAFT" | "PUBLISHED" | "ARCHIVED" | string status?: "DRAFT" | "PUBLISHED" | "ARCHIVED" | string
limit?: number limit?: number