add sub-category and course delete controls in human language page
Wire delete actions and confirmation dialogs for selected sub-categories and courses, backed by the new sub-category delete API route. Made-with: Cursor
This commit is contained in:
parent
78111f161f
commit
bfbdf0fc19
|
|
@ -153,6 +153,9 @@ export const createCourseCategory = (data: CreateCourseCategoryRequest) =>
|
||||||
? http.post("/course-management/sub-categories", { category_id: data.parent_id, name: data.name })
|
? http.post("/course-management/sub-categories", { category_id: data.parent_id, name: data.name })
|
||||||
: http.post("/course-management/categories", { name: data.name })
|
: http.post("/course-management/categories", { name: data.name })
|
||||||
|
|
||||||
|
export const deleteCourseSubCategory = (subCategoryId: number) =>
|
||||||
|
http.delete(`/course-management/sub-categories/${subCategoryId}`)
|
||||||
|
|
||||||
export const getCoursesByCategory = (categoryId: number) =>
|
export const getCoursesByCategory = (categoryId: number) =>
|
||||||
http.get("/course-management/hierarchy").then((res) => {
|
http.get("/course-management/hierarchy").then((res) => {
|
||||||
const rows: UnifiedHierarchyRow[] = res.data?.data ?? []
|
const rows: UnifiedHierarchyRow[] = res.data?.data ?? []
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ import {
|
||||||
createCourse,
|
createCourse,
|
||||||
createCourseCategory,
|
createCourseCategory,
|
||||||
createHumanLanguageLesson,
|
createHumanLanguageLesson,
|
||||||
|
deleteCourse,
|
||||||
|
deleteCourseSubCategory,
|
||||||
deleteQuestionSet,
|
deleteQuestionSet,
|
||||||
deleteQuestion,
|
deleteQuestion,
|
||||||
deleteSubModule,
|
deleteSubModule,
|
||||||
|
|
@ -325,6 +327,10 @@ export function HumanLanguagePage() {
|
||||||
const [quickCourseName, setQuickCourseName] = useState("")
|
const [quickCourseName, setQuickCourseName] = useState("")
|
||||||
const [quickSearch, setQuickSearch] = useState("")
|
const [quickSearch, setQuickSearch] = useState("")
|
||||||
const [quickCreating, setQuickCreating] = useState(false)
|
const [quickCreating, setQuickCreating] = useState(false)
|
||||||
|
const [subCategoryTargetDelete, setSubCategoryTargetDelete] = useState<{ id: number; name: string } | null>(null)
|
||||||
|
const [courseTargetDelete, setCourseTargetDelete] = useState<{ id: number; name: string } | null>(null)
|
||||||
|
const [deletingSubCategory, setDeletingSubCategory] = useState(false)
|
||||||
|
const [deletingCourse, setDeletingCourse] = useState(false)
|
||||||
const [deletingKey, setDeletingKey] = useState<string | null>(null)
|
const [deletingKey, setDeletingKey] = useState<string | null>(null)
|
||||||
/** Course IDs whose path body is collapsed (headers stay visible). */
|
/** Course IDs whose path body is collapsed (headers stay visible). */
|
||||||
const [collapsedPathIds, setCollapsedPathIds] = useState<number[]>([])
|
const [collapsedPathIds, setCollapsedPathIds] = useState<number[]>([])
|
||||||
|
|
@ -656,6 +662,41 @@ export function HumanLanguagePage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteSelectedSubCategory = async () => {
|
||||||
|
if (!subCategoryTargetDelete) return
|
||||||
|
setDeletingSubCategory(true)
|
||||||
|
try {
|
||||||
|
await deleteCourseSubCategory(subCategoryTargetDelete.id)
|
||||||
|
toast.success("Sub-category deleted")
|
||||||
|
setSubCategoryTargetDelete(null)
|
||||||
|
setSelectedSubCategoryId("ALL")
|
||||||
|
setSelectedCourseId("ALL")
|
||||||
|
await loadHierarchy()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete sub-category:", error)
|
||||||
|
toast.error("Failed to delete sub-category")
|
||||||
|
} finally {
|
||||||
|
setDeletingSubCategory(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteSelectedCourse = async () => {
|
||||||
|
if (!courseTargetDelete) return
|
||||||
|
setDeletingCourse(true)
|
||||||
|
try {
|
||||||
|
await deleteCourse(courseTargetDelete.id)
|
||||||
|
toast.success("Course deleted")
|
||||||
|
setCourseTargetDelete(null)
|
||||||
|
setSelectedCourseId("ALL")
|
||||||
|
await loadHierarchy()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete course:", error)
|
||||||
|
toast.error("Failed to delete course")
|
||||||
|
} finally {
|
||||||
|
setDeletingCourse(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadPracticeQuestionsIfNeeded = async (practiceId: number, forceRefresh = false) => {
|
const loadPracticeQuestionsIfNeeded = async (practiceId: number, forceRefresh = false) => {
|
||||||
let skipFetch = false
|
let skipFetch = false
|
||||||
setPracticeQuestionsState((prev) => {
|
setPracticeQuestionsState((prev) => {
|
||||||
|
|
@ -1164,6 +1205,44 @@ export function HumanLanguagePage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="gap-1.5 border-red-200 text-red-600 hover:bg-red-50"
|
||||||
|
disabled={selectedSubCategoryId === "ALL"}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedSubCategoryId === "ALL") return
|
||||||
|
const selected = subCategories.find((s) => s.sub_category_id === selectedSubCategoryId)
|
||||||
|
setSubCategoryTargetDelete({
|
||||||
|
id: Number(selectedSubCategoryId),
|
||||||
|
name: selected?.sub_category_name ?? `Sub-category ${selectedSubCategoryId}`,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
Delete selected sub-category
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="gap-1.5 border-red-200 text-red-600 hover:bg-red-50"
|
||||||
|
disabled={selectedCourseId === "ALL"}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedCourseId === "ALL") return
|
||||||
|
const selected = availableCourses.find((c) => c.course_id === selectedCourseId)
|
||||||
|
setCourseTargetDelete({
|
||||||
|
id: Number(selectedCourseId),
|
||||||
|
name: selected?.course_name ?? `Course ${selectedCourseId}`,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
Delete selected course
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2 py-8 text-sm text-grayScale-500">
|
<div className="flex items-center gap-2 py-8 text-sm text-grayScale-500">
|
||||||
<SpinnerIcon className="h-4 w-4" />
|
<SpinnerIcon className="h-4 w-4" />
|
||||||
|
|
@ -2170,6 +2249,58 @@ export function HumanLanguagePage() {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={subCategoryTargetDelete !== null} onOpenChange={(open) => !open && setSubCategoryTargetDelete(null)}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete sub-category?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{subCategoryTargetDelete
|
||||||
|
? `This will permanently delete "${subCategoryTargetDelete.name}" and all courses under it.`
|
||||||
|
: ""}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setSubCategoryTargetDelete(null)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
onClick={() => void handleDeleteSelectedSubCategory()}
|
||||||
|
disabled={deletingSubCategory}
|
||||||
|
>
|
||||||
|
{deletingSubCategory ? "Deleting..." : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={courseTargetDelete !== null} onOpenChange={(open) => !open && setCourseTargetDelete(null)}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete course?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{courseTargetDelete
|
||||||
|
? `This will permanently delete "${courseTargetDelete.name}" and all nested content under it.`
|
||||||
|
: ""}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setCourseTargetDelete(null)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
onClick={() => void handleDeleteSelectedCourse()}
|
||||||
|
disabled={deletingCourse}
|
||||||
|
>
|
||||||
|
{deletingCourse ? "Deleting..." : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={questionDialog.open}
|
open={questionDialog.open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user