From bd7200a9cf72825e2bd4a127781be1f2686b10a8 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Sun, 19 Jul 2026 10:49:56 +0800 Subject: [PATCH] done --- src/contexts/AdminCoursesContext.jsx | 8 ++++---- .../admin/pages/courses/CourseAssessment.jsx | 20 +++++++++++++++++-- .../admin/pages/courses/units/ModifyQuiz.jsx | 15 +++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/contexts/AdminCoursesContext.jsx b/src/contexts/AdminCoursesContext.jsx index dae6d22..0db6c4e 100644 --- a/src/contexts/AdminCoursesContext.jsx +++ b/src/contexts/AdminCoursesContext.jsx @@ -614,9 +614,9 @@ export function CoursesProvider({ children }) { ); const bulkSyncQuizQuestions = useCallback( - (courseId, unitId, quizId, questions, updatedBy) => + (courseId, unitId, quizId, questions, updatedBy, confirmFullReplace = false) => request(async () => { - const { data } = await api.put(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/bulk-sync`, { questions, updatedBy }); + const { data } = await api.put(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/bulk-sync`, { questions, updatedBy, confirmFullReplace }); const result = data?.data?.data ?? []; setQuestions(result); toast("Quiz saved."); @@ -975,9 +975,9 @@ export function CoursesProvider({ children }) { ); const bulkSyncAssessmentQuestions = useCallback( - (courseId, assessmentId, questions, updatedBy) => + (courseId, assessmentId, questions, updatedBy, confirmFullReplace = false) => request(async () => { - const { data } = await api.put(`${BASE}/${courseId}/assessment/${assessmentId}/questions/bulk-sync`, { questions, updatedBy }); + const { data } = await api.put(`${BASE}/${courseId}/assessment/${assessmentId}/questions/bulk-sync`, { questions, updatedBy, confirmFullReplace }); const result = data?.data?.data ?? []; setQuestions(result); toast("Assessment saved."); diff --git a/src/modules/admin/pages/courses/CourseAssessment.jsx b/src/modules/admin/pages/courses/CourseAssessment.jsx index 61d3ed7..dfd4b71 100644 --- a/src/modules/admin/pages/courses/CourseAssessment.jsx +++ b/src/modules/admin/pages/courses/CourseAssessment.jsx @@ -233,6 +233,7 @@ export default function CourseAssessment() { const [confirmLoading, setConfirmLoading] = useState(false); const pendingSaveRef = useRef(null); // stores the meta+questions payload until confirmed const initialSnapshot = useRef(null); + const originalQuestionIdsRef = useRef([]); // ids loaded from the server — used to detect a full-pool wipe on save const questionRefs = useRef([]); const navItemRefs = useRef([]); @@ -271,6 +272,7 @@ export default function CourseAssessment() { setTitle(t); setPassingScore(ps); setTimeLimit(tl); setIsRequired(ir); setMaxQuestions(mq); setMaxAttempts(ma); setCooldownHours(ch); setShuffleQuestions(sq); setQuestions(qs); initialSnapshot.current = snapAssessment({ title: t, passingScore: ps, timeLimit: tl, isRequired: ir, maxQuestions: mq, maxAttempts: ma, cooldownHours: ch, shuffleQuestions: sq, questions: qs }); + originalQuestionIdsRef.current = qs.map((q) => q.question_id).filter(Boolean); }, [localAssessment]); // ── Measure sticky header → --assessment-h ──────────────────────────────── @@ -430,14 +432,28 @@ export default function CourseAssessment() { let id = assessmentId; if (!id) { const res = await createAssessment(courseId, meta); - id = res?.data?.data?.data?.assessment_id; + // res is the raw response body: { status, message, data: { data: assessment } } + // (createAssessment() itself unwraps the same object as data?.data?.data to get + // the assessment row — this needs the same two levels, not three). + id = res?.data?.data?.assessment_id; if (!id) return; setLocalAssessment((prev) => ({ ...prev, assessment_id: id })); } else { await updateAssessment(courseId, id, meta); } - await bulkSyncAssessmentQuestions(courseId, id, questions, user?.user_id); + // A save that keeps none of the previously-loaded question ids would archive the + // entire existing pool in one call — could be a deliberate "start over", but is + // exactly the signature of a stale/incomplete state bug. Require explicit confirmation. + const retainedIds = questions.map((q) => q.question_id).filter(Boolean); + const wipesPool = originalQuestionIdsRef.current.length > 0 && retainedIds.length === 0; + if (wipesPool && !window.confirm( + `This will permanently remove all ${originalQuestionIdsRef.current.length} existing question(s) from this assessment. Continue?` + )) { + return; + } + + await bulkSyncAssessmentQuestions(courseId, id, questions, user?.user_id, wipesPool); initialSnapshot.current = snapAssessment({ title, passingScore, timeLimit, isRequired, maxQuestions, maxAttempts, cooldownHours, shuffleQuestions, questions }); }; diff --git a/src/modules/admin/pages/courses/units/ModifyQuiz.jsx b/src/modules/admin/pages/courses/units/ModifyQuiz.jsx index 0a96d8e..3d2642a 100644 --- a/src/modules/admin/pages/courses/units/ModifyQuiz.jsx +++ b/src/modules/admin/pages/courses/units/ModifyQuiz.jsx @@ -222,6 +222,7 @@ export default function ModifyQuiz() { const navContainerRef = useRef(null); const headerRef = useRef(null); const initialSnapshot = useRef(null); + const originalQuestionIdsRef = useRef([]); // ids loaded from the server — used to detect a full-pool wipe on save // Junction revamp — this builder runs course-scoped AND from the standalone // Unit Library (/admin/units/:unitId/quiz/edit, no :courseId param). @@ -273,6 +274,7 @@ export default function ModifyQuiz() { const qs = (localQuiz.questions ?? []).map((q) => ({ ...q, _tempId: q.question_id, options: q.options ?? [] })); setTitle(t); setPassingScore(ps); setIsRequired(ir); setMaxQuestions(mq); setShuffleQuestions(sq); setQuestions(qs); initialSnapshot.current = snapQuiz({ title: t, passingScore: ps, isRequired: ir, maxQuestions: mq, shuffleQuestions: sq, questions: qs }); + originalQuestionIdsRef.current = qs.map((q) => q.question_id).filter(Boolean); }, [localQuiz]); // ── Measure sticky header → --quiz-h ────────────────────────────────────── @@ -416,7 +418,18 @@ export default function ModifyQuiz() { await updateQuiz(courseId, unitId, quizId, meta); } - await bulkSyncQuizQuestions(courseId, unitId, quizId, questions, user?.user_id); + // A save that keeps none of the previously-loaded question ids would archive the + // entire existing pool in one call — could be a deliberate "start over", but is + // exactly the signature of a stale/incomplete state bug. Require explicit confirmation. + const retainedIds = questions.map((q) => q.question_id).filter(Boolean); + const wipesPool = originalQuestionIdsRef.current.length > 0 && retainedIds.length === 0; + if (wipesPool && !window.confirm( + `This will permanently remove all ${originalQuestionIdsRef.current.length} existing question(s) from this quiz. Continue?` + )) { + return; + } + + await bulkSyncQuizQuestions(courseId, unitId, quizId, questions, user?.user_id, wipesPool); initialSnapshot.current = snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions }); navigate(`${scopeBase}/view`);