diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index e5e3ef85..4d58e196 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -363,7 +363,7 @@ async def read_assignment_tasks( # Find assignments tasks for an assignment statement = select(AssignmentTask).where( - assignment.assignment_uuid == assignment_uuid + AssignmentTask.assignment_id == assignment.id ) # RBAC check diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx index fea5b9f3..73a74beb 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx @@ -1,3 +1,4 @@ +import { useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import { useLHSession } from '@components/Contexts/LHSessionContext'; import { getAPIUrl } from '@services/config/config'; import { createAssignmentTask } from '@services/courses/assignments' @@ -10,6 +11,7 @@ function NewTaskModal({ closeModal, assignment_uuid }: any) { const session = useLHSession() as any; const access_token = session?.data?.tokens?.access_token; const reminderShownRef = React.useRef(false); + const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any function showReminderToast() { // Check if the reminder has already been shown using sessionStorage @@ -33,10 +35,11 @@ function NewTaskModal({ closeModal, assignment_uuid }: any) { contents: {}, max_grade_value: 100, } - await createAssignmentTask(task_object, assignment_uuid, access_token) + const res = await createAssignmentTask(task_object, assignment_uuid, access_token) toast.success('Task created successfully') showReminderToast() mutate(`${getAPIUrl()}assignments/${assignment_uuid}/tasks`) + assignmentTaskStateHook({ type: 'setSelectedAssignmentTaskUUID', payload: res.data.assignment_task_uuid }) closeModal(false) } diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskContentEdit.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskContentEdit.tsx new file mode 100644 index 00000000..0485d125 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskContentEdit.tsx @@ -0,0 +1,24 @@ +import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; +import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; +import { useLHSession } from '@components/Contexts/LHSessionContext'; +import React, { useEffect } from 'react' +import TaskQuizObject from './TaskTypes/TaskQuizObject'; + +function AssignmentTaskContentEdit() { + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any + const assignment = useAssignments() as any + + useEffect(() => { + } + , [assignment, assignmentTaskStateHook]) + + return ( +
+ +
+ ) +} + +export default AssignmentTaskContentEdit \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskGeneralEdit.tsx similarity index 69% rename from apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx rename to apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskGeneralEdit.tsx index 85557ec4..271bd7e8 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskGeneralEdit.tsx @@ -16,103 +16,7 @@ import React, { use, useEffect } from 'react' import toast from 'react-hot-toast'; import { mutate } from 'swr'; -function AssignmentTaskEditor({ page }: any) { - const [selectedSubPage, setSelectedSubPage] = React.useState(page) - const assignment = useAssignments() as any - const assignmentTaskState = useAssignmentsTask() as any - const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any - const session = useLHSession() as any; - const access_token = session?.data?.tokens?.access_token; - - async function deleteTaskUI() { - const res = await deleteAssignmentTask(assignmentTaskState.assignmentTask.assignment_task_uuid, assignment.assignment_object.assignment_uuid, access_token) - if (res) { - assignmentTaskStateHook({ - type: 'SET_MULTIPLE_STATES', - payload: { - selectedAssignmentTaskUUID: null, - assignmentTask: {}, - }, - }); - mutate(`${getAPIUrl()}assignments/${assignment.assignment_object.assignment_uuid}/tasks`) - toast.success('Task deleted successfully') - } else { - toast.error('Error deleting task, please retry later.') - } - } - - useEffect(() => { - } - , [assignmentTaskState,assignmentTaskStateHook]) - - return ( -
- {assignmentTaskState.assignmentTask && Object.keys(assignmentTaskState.assignmentTask).length > 0 && ( -
-
-
-
- {assignmentTaskState?.assignmentTask.title} -
-
-
deleteTaskUI()} - className='flex px-2 py-1.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-red-800 bg-rose-100 border border-rose-600/10 shadow-rose-900/10 shadow-lg'> - -

Delete Task

-
-
-
-
-
setSelectedSubPage('general')} - className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'general' - ? 'border-b-4' - : 'opacity-50' - } cursor-pointer`} - > -
- -
General
-
-
-
setSelectedSubPage('content')} - className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'content' - ? 'border-b-4' - : 'opacity-50' - } cursor-pointer`} - > -
- -
Content
-
-
-
-
-
- {selectedSubPage === 'general' && } -
-
- )} - {Object.keys(assignmentTaskState.assignmentTask).length == 0 && ( -
-
-
- -
- No Task Selected -
-
-
-
- )} - -
- ) -} - -function AssignmentTaskGeneralEdit() { +export function AssignmentTaskGeneralEdit() { const session = useLHSession() as any; const access_token = session?.data?.tokens?.access_token; const assignmentTaskState = useAssignmentsTask() as any @@ -127,8 +31,6 @@ function AssignmentTaskGeneralEdit() { return errors; }; - - const formik = useFormik({ initialValues: { title: assignmentTaskState.assignmentTask.title, @@ -373,6 +275,4 @@ function UpdateTaskRef() { ) -} - -export default AssignmentTaskEditor \ No newline at end of file +} \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx new file mode 100644 index 00000000..1b53c085 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx @@ -0,0 +1,192 @@ +import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; +import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; +import { useLHSession } from '@components/Contexts/LHSessionContext'; +import AssignmentBoxUI from '@components/Objects/Assignments/AssignmentBoxUI'; +import { updateAssignmentTask } from '@services/courses/assignments'; +import { Check, Minus, Plus, PlusCircle, X } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import toast from 'react-hot-toast'; +import { v4 as uuidv4 } from 'uuid'; + +type QuizSchema = { + questionText: string; + questionUUID?: string; + options: { + optionUUID?: string; + text: string; + fileID: string; + type: 'text' | 'image' | 'audio' | 'video'; + correct: boolean; + }[]; +}; + +function TaskQuizObject() { + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + const assignmentTaskState = useAssignmentsTask() as any; + const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any; + const assignment = useAssignments() as any; + + // Teacher area + const [questions, setQuestions] = useState([ + { questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] }, + ]); + + const handleQuestionChange = (index: number, value: string) => { + const updatedQuestions = [...questions]; + updatedQuestions[index].questionText = value; + setQuestions(updatedQuestions); + }; + + const handleOptionChange = (qIndex: number, oIndex: number, value: string) => { + const updatedQuestions = [...questions]; + updatedQuestions[qIndex].options[oIndex].text = value; + setQuestions(updatedQuestions); + }; + + const addOption = (qIndex: number) => { + const updatedQuestions = [...questions]; + updatedQuestions[qIndex].options.push({ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }); + setQuestions(updatedQuestions); + }; + + const removeOption = (qIndex: number, oIndex: number) => { + const updatedQuestions = [...questions]; + updatedQuestions[qIndex].options.splice(oIndex, 1); + setQuestions(updatedQuestions); + }; + + const addQuestion = () => { + setQuestions([...questions, { questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] }]); + }; + + const removeQuestion = (qIndex: number) => { + const updatedQuestions = [...questions]; + updatedQuestions.splice(qIndex, 1); + setQuestions(updatedQuestions); + }; + + const toggleCorrectOption = (qIndex: number, oIndex: number) => { + const updatedQuestions = [...questions]; + // Find the option to toggle + const optionToToggle = updatedQuestions[qIndex].options[oIndex]; + // Toggle the 'correct' property of the option + optionToToggle.correct = !optionToToggle.correct; + setQuestions(updatedQuestions); + }; + + const saveFC = async () => { + // Save the quiz to the server + const values = { + contents: { + questions, + }, + }; + const res = await updateAssignmentTask(values, assignmentTaskState.assignmentTask.assignment_task_uuid, assignment.assignment_object.assignment_uuid, access_token); + if (res) { + assignmentTaskStateHook({ + type: 'reload', + }); + toast.success('Task saved successfully'); + } else { + toast.error('Error saving task, please retry later.'); + } + }; + + useEffect(() => { + if (assignmentTaskState.assignmentTask.contents?.questions) { + setQuestions(assignmentTaskState.assignmentTask.contents.questions); + } + }, [assignmentTaskState,assignment,assignmentTaskStateHook,access_token]); + + // Teacher area end + + return ( + +
+ {questions.map((question, qIndex) => ( +
+
+ handleQuestionChange(qIndex, e.target.value)} + placeholder="Question" + className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold" + /> +
removeQuestion(qIndex)} + > + +
+
+
+ {question.options.map((option, oIndex) => ( +
+
+
+

{String.fromCharCode(65 + oIndex)}

+
+ handleOptionChange(qIndex, oIndex, e.target.value)} + placeholder="Option" + className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold" + /> +
toggleCorrectOption(qIndex, oIndex)} + > + {option.correct ? : } + {option.correct ? ( +

Correct

+ ) : ( +

Incorrect

+ )} +
+
removeOption(qIndex, oIndex)} + > + +
+
+
+ {/* Show this at the last option */} + {oIndex === question.options.length - 1 && ( +
+
addOption(qIndex)} + > + + +
+
+ )} +
+
+ ))} +
+
+ ))} +
+
+
+ + Add Question +
+
+
+ ); +} + +export default TaskQuizObject; diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/TaskEditor.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/TaskEditor.tsx new file mode 100644 index 00000000..6d14990b --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/TaskEditor.tsx @@ -0,0 +1,125 @@ +'use client'; +import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; +import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; +import { useLHSession } from '@components/Contexts/LHSessionContext'; +import { useOrg } from '@components/Contexts/OrgContext'; +import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form'; +import * as Form from '@radix-ui/react-form'; +import { getAPIUrl } from '@services/config/config'; +import { getActivity, getActivityByID } from '@services/courses/activities'; +import { deleteAssignmentTask, updateAssignmentTask, updateReferenceFile } from '@services/courses/assignments'; +import { getTaskRefFileDir } from '@services/media/media'; +import { useFormik } from 'formik'; +import { ArrowBigUpDash, Cloud, File, GalleryVerticalEnd, Info, Loader, TentTree, Trash, Upload, UploadCloud } from 'lucide-react' +import Link from 'next/link'; +import React, { use, useEffect } from 'react' +import toast from 'react-hot-toast'; +import { mutate } from 'swr'; +import { AssignmentTaskGeneralEdit } from './Subs/AssignmentTaskGeneralEdit'; +import AssignmentTaskContentEdit from './Subs/AssignmentTaskContentEdit'; + +function AssignmentTaskEditor({ page }: any) { + const [selectedSubPage, setSelectedSubPage] = React.useState(page) + const assignment = useAssignments() as any + const assignmentTaskState = useAssignmentsTask() as any + const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + + async function deleteTaskUI() { + const res = await deleteAssignmentTask(assignmentTaskState.assignmentTask.assignment_task_uuid, assignment.assignment_object.assignment_uuid, access_token) + if (res) { + assignmentTaskStateHook({ + type: 'SET_MULTIPLE_STATES', + payload: { + selectedAssignmentTaskUUID: null, + assignmentTask: {}, + }, + }); + mutate(`${getAPIUrl()}assignments/${assignment.assignment_object.assignment_uuid}/tasks`) + mutate(`${getAPIUrl()}assignments/${assignment.assignment_object.assignment_uuid}`) + toast.success('Task deleted successfully') + } else { + toast.error('Error deleting task, please retry later.') + } + } + + useEffect(() => { + // Switch back to general page if the selectedAssignmentTaskUUID is changed + if (assignmentTaskState.selectedAssignmentTaskUUID !== assignmentTaskState.assignmentTask.assignment_task_uuid) { + setSelectedSubPage('general') + } + } + , [assignmentTaskState, assignmentTaskStateHook, selectedSubPage, assignment]) + + return ( +
+ {assignmentTaskState.assignmentTask && Object.keys(assignmentTaskState.assignmentTask).length > 0 && ( +
+
+
+
+ {assignmentTaskState?.assignmentTask.title} +
+
+
deleteTaskUI()} + className='flex px-2 py-1.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-red-800 bg-rose-100 border border-rose-600/10 shadow-rose-900/10 shadow-lg'> + +

Delete Task

+
+
+
+
+
setSelectedSubPage('general')} + className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'general' + ? 'border-b-4' + : 'opacity-50' + } cursor-pointer`} + > +
+ +
General
+
+
+
setSelectedSubPage('content')} + className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'content' + ? 'border-b-4' + : 'opacity-50' + } cursor-pointer`} + > +
+ +
Content
+
+
+
+
+
+ {selectedSubPage === 'general' && } + {selectedSubPage === 'content' && } +
+
+ )} + {Object.keys(assignmentTaskState.assignmentTask).length == 0 && ( +
+
+
+ +
+ No Task Selected +
+
+
+
+ )} + +
+ ) +} + + + +export default AssignmentTaskEditor \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx index 76cb6eef..66664d94 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx @@ -1,56 +1,57 @@ 'use client'; import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' import { BookOpen, BookX, EllipsisVertical, LayoutList } from 'lucide-react' -import React from 'react' -import AssignmentTaskEditor from './_components/TaskEditor'; -import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext'; +import React, { useEffect } from 'react' +import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; import AssignmentTasks from './_components/Tasks'; import { useParams } from 'next/navigation'; import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import ToolTip from '@components/StyledElements/Tooltip/Tooltip'; +import AssignmentTaskEditor from './_components/TaskEditor/TaskEditor'; function AssignmentEdit() { const params = useParams<{ assignmentuuid: string; }>() return (
-
-
-
- -
-
Assignment Editor
+ +
+
+
+ +
+
Assignment Editor
+
-
-
-
-
Published
-
- -
- -

Publish

-
-
- -
- -

Unpublish

-
-
+
+
+
Published
+
+ +
+ +

Publish

+
+
+ +
+ +

Unpublish

+
+
+
-
-
- +
+
@@ -65,10 +66,21 @@ function AssignmentEdit() {
- -
+
+
) } -export default AssignmentEdit \ No newline at end of file +export default AssignmentEdit + +function BrdCmpx() { + const assignment = useAssignments() as any + + useEffect(() => { + }, [assignment]) + + return ( + + ) +} \ No newline at end of file diff --git a/apps/web/components/Objects/Assignments/AssignmentBoxUI.tsx b/apps/web/components/Objects/Assignments/AssignmentBoxUI.tsx new file mode 100644 index 00000000..d27769e7 --- /dev/null +++ b/apps/web/components/Objects/Assignments/AssignmentBoxUI.tsx @@ -0,0 +1,53 @@ +import { BookUser, EllipsisVertical, ListTodo, Save } from 'lucide-react' +import React from 'react' + +type AssignmentBoxProps = { + type: 'quiz' | 'task' + view?: 'teacher' | 'student' + saveFC?: () => void + children: React.ReactNode + +} + +function AssignmentBoxUI({ type, view, saveFC, children }: AssignmentBoxProps) { + return ( +
+
+
+
+ {type === 'quiz' && +
+ +

Quiz

+
} +
+ + +
+ +
+ {view === 'teacher' && +
+ +

Teacher view

+
+ } +
+
+ + {/* Save button */} +
saveFC && saveFC()} + className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-slate-500 bg-white/60 hover:bg-white/80 linear transition-all nice-shadow '> + +

Save

+
+ +
+
+ {children} +
+ ) +} + +export default AssignmentBoxUI \ No newline at end of file