- Make isDiagnostic false when all steps of the exam workflow have been approved.
- Implement Load Exam and Edit Exam buttons
This commit is contained in:
@@ -195,7 +195,7 @@ const LevelSettings: React.FC = () => {
|
|||||||
category: s.settings.category
|
category: s.settings.category
|
||||||
};
|
};
|
||||||
}).filter(part => part.exercises.length > 0),
|
}).filter(part => part.exercises.length > 0),
|
||||||
isDiagnostic: false,
|
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
minTimer,
|
minTimer,
|
||||||
module: "level",
|
module: "level",
|
||||||
id: title,
|
id: title,
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ const ListeningSettings: React.FC = () => {
|
|||||||
category: s.settings.category
|
category: s.settings.category
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
isDiagnostic: false,
|
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
minTimer,
|
minTimer,
|
||||||
module: "listening",
|
module: "listening",
|
||||||
id: title,
|
id: title,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const ReadingSettings: React.FC = () => {
|
|||||||
category: localSettings.category
|
category: localSettings.category
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
isDiagnostic: false,
|
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
minTimer,
|
minTimer,
|
||||||
module: "reading",
|
module: "reading",
|
||||||
id: title,
|
id: title,
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ const SpeakingSettings: React.FC = () => {
|
|||||||
minTimer,
|
minTimer,
|
||||||
module: "speaking",
|
module: "speaking",
|
||||||
id: title,
|
id: title,
|
||||||
isDiagnostic: false,
|
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
variant: undefined,
|
variant: undefined,
|
||||||
difficulty,
|
difficulty,
|
||||||
instructorGender: "varied",
|
instructorGender: "varied",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const WritingSettings: React.FC = () => {
|
|||||||
minTimer,
|
minTimer,
|
||||||
module: "writing",
|
module: "writing",
|
||||||
id: title,
|
id: title,
|
||||||
isDiagnostic: false,
|
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
|
||||||
variant: undefined,
|
variant: undefined,
|
||||||
difficulty,
|
difficulty,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import useApprovalWorkflow from "@/hooks/useApprovalWorkflow";
|
|||||||
import { ApprovalWorkflow, getUserTypeLabelShort, WorkflowStep } from "@/interfaces/approval.workflow";
|
import { ApprovalWorkflow, getUserTypeLabelShort, WorkflowStep } from "@/interfaces/approval.workflow";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import useExamStore from "@/stores/exam";
|
||||||
import { redirect, serialize } from "@/utils";
|
import { redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
|
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
|
||||||
|
import { getExamById } from "@/utils/exams";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { getSpecificUsers, getUser } from "@/utils/users.be";
|
import { getSpecificUsers, getUser } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -20,6 +22,7 @@ import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
|||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BsChevronLeft } from "react-icons/bs";
|
import { BsChevronLeft } from "react-icons/bs";
|
||||||
import { FaSpinner, FaWpforms } from "react-icons/fa6";
|
import { FaSpinner, FaWpforms } from "react-icons/fa6";
|
||||||
@@ -79,14 +82,18 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor
|
|||||||
|
|
||||||
const currentWorkflow = workflow || initialWorkflow;
|
const currentWorkflow = workflow || initialWorkflow;
|
||||||
|
|
||||||
let currentStep = currentWorkflow.steps.findIndex(step => !step.completed || step.rejected);
|
let currentStepIndex = currentWorkflow.steps.findIndex(step => !step.completed || step.rejected);
|
||||||
if (currentStep === -1)
|
if (currentStepIndex === -1)
|
||||||
currentStep = currentWorkflow.steps.length - 1;
|
currentStepIndex = currentWorkflow.steps.length - 1;
|
||||||
|
|
||||||
const [selectedStepIndex, setSelectedStepIndex] = useState<number>(currentStep);
|
const [selectedStepIndex, setSelectedStepIndex] = useState<number>(currentStepIndex);
|
||||||
const [selectedStep, setSelectedStep] = useState<WorkflowStep>(currentWorkflow.steps[selectedStepIndex]);
|
const [selectedStep, setSelectedStep] = useState<WorkflowStep>(currentWorkflow.steps[selectedStepIndex]);
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
||||||
const [comments, setComments] = useState<string>(selectedStep.comments || "");
|
const [comments, setComments] = useState<string>(selectedStep.comments || "");
|
||||||
|
const [viewExamIsLoading, setViewExamIsLoading] = useState<boolean>(false);
|
||||||
|
const [editExamIsLoading, setEditExamIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleStepClick = (index: number, stepInfo: WorkflowStep) => {
|
const handleStepClick = (index: number, stepInfo: WorkflowStep) => {
|
||||||
setSelectedStep(stepInfo);
|
setSelectedStep(stepInfo);
|
||||||
@@ -128,6 +135,11 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleApproveStep = () => {
|
const handleApproveStep = () => {
|
||||||
|
const isLastStep = (selectedStepIndex + 1 === currentWorkflow.steps.length);
|
||||||
|
if (isLastStep) {
|
||||||
|
if (!confirm(`Are you sure you want to approve the last step? Doing so will approve the exam.`)) return;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedWorkflow: ApprovalWorkflow = {
|
const updatedWorkflow: ApprovalWorkflow = {
|
||||||
...currentWorkflow,
|
...currentWorkflow,
|
||||||
status: selectedStepIndex === currentWorkflow.steps.length - 1 ? "approved" : "pending",
|
status: selectedStepIndex === currentWorkflow.steps.length - 1 ? "approved" : "pending",
|
||||||
@@ -161,15 +173,35 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor
|
|||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
|
|
||||||
if (selectedStepIndex + 1 < currentWorkflow.steps.length){
|
if (isLastStep) {
|
||||||
handleStepClick(selectedStepIndex + 1, currentWorkflow.steps[selectedStepIndex + 1]);
|
|
||||||
} else {
|
|
||||||
setIsPanelOpen(false);
|
setIsPanelOpen(false);
|
||||||
|
const examModule = currentWorkflow.modules[0];
|
||||||
|
const examId = currentWorkflow.examId;
|
||||||
|
|
||||||
|
axios
|
||||||
|
.patch(`/api/exam/${examModule}/${examId}`, { isDiagnostic: false })
|
||||||
|
.then(() => toast.success(`The exam was successfuly approved and this workflow has been completed.`))
|
||||||
|
.catch((reason) => {
|
||||||
|
if (reason.response.status === 404) {
|
||||||
|
toast.error("Exam not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason.response.status === 403) {
|
||||||
|
toast.error("You do not have permission to update this exam!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error("Something went wrong, please try again later.");
|
||||||
|
})
|
||||||
|
.finally(reload);
|
||||||
|
} else {
|
||||||
|
handleStepClick(selectedStepIndex + 1, currentWorkflow.steps[selectedStepIndex + 1]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRejectStep = () => {
|
const handleRejectStep = () => {
|
||||||
if (!confirm(`Are you sure you want to reject this step?`)) return;
|
if (!confirm(`Are you sure you want to reject this step? Doing so will terminate this approval workflow.`)) return;
|
||||||
|
|
||||||
const updatedWorkflow: ApprovalWorkflow = {
|
const updatedWorkflow: ApprovalWorkflow = {
|
||||||
...currentWorkflow,
|
...currentWorkflow,
|
||||||
@@ -206,12 +238,36 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewExam = () => {
|
const dispatch = useExamStore((store) => store.dispatch);
|
||||||
|
const handleViewExam = async () => {
|
||||||
|
setViewExamIsLoading(true);
|
||||||
|
const examModule = currentWorkflow.modules[0];
|
||||||
|
const examId = currentWorkflow.examId;
|
||||||
|
|
||||||
|
if (examModule && examId) {
|
||||||
|
const exam = await getExamById(examModule, examId.trim());
|
||||||
|
if (!exam) {
|
||||||
|
toast.error(
|
||||||
|
"Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
|
||||||
|
{ toastId: "invalid-exam-id" }
|
||||||
|
);
|
||||||
|
setViewExamIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: "INIT_EXAM",
|
||||||
|
payload: { exams: [exam], modules: [examModule] },
|
||||||
|
});
|
||||||
|
router.push("/exam");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditExam = () => {
|
const handleEditExam = () => {
|
||||||
|
setEditExamIsLoading(true);
|
||||||
|
const examModule = currentWorkflow.modules[0];
|
||||||
|
const examId = currentWorkflow.examId;
|
||||||
|
|
||||||
|
router.push(`/generation?id=${examId}&module=${examModule}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -257,21 +313,41 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor
|
|||||||
color="purple"
|
color="purple"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={handleViewExam}
|
onClick={handleViewExam}
|
||||||
|
disabled={viewExamIsLoading}
|
||||||
padding="px-6 py-2"
|
padding="px-6 py-2"
|
||||||
className="w-[240px] text-lg flex items-center justify-center gap-2 text-left"
|
className="w-[240px] text-lg flex items-center justify-center gap-2 text-left"
|
||||||
>
|
>
|
||||||
<IoDocumentTextOutline />
|
{viewExamIsLoading ? (
|
||||||
View Exam
|
<>
|
||||||
|
<FaSpinner className="animate-spin size-5" />
|
||||||
|
Loading...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IoDocumentTextOutline />
|
||||||
|
Load Exam
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={handleEditExam}
|
onClick={handleEditExam}
|
||||||
padding="px-6 py-2"
|
padding="px-6 py-2"
|
||||||
|
disabled={!currentWorkflow.steps[currentStepIndex].assignees.includes(user.id) || editExamIsLoading}
|
||||||
className="w-[240px] text-lg flex items-center justify-center gap-2 text-left"
|
className="w-[240px] text-lg flex items-center justify-center gap-2 text-left"
|
||||||
>
|
>
|
||||||
<TiEdit size={20} />
|
{editExamIsLoading ? (
|
||||||
Edit Exam
|
<>
|
||||||
|
<FaSpinner className="animate-spin size-5" />
|
||||||
|
Loading...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TiEdit size={20} />
|
||||||
|
Edit Exam
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -292,7 +368,7 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor
|
|||||||
stepType={step.stepType}
|
stepType={step.stepType}
|
||||||
assignees={step.assignees}
|
assignees={step.assignees}
|
||||||
finalStep={index === currentWorkflow.steps.length - 1}
|
finalStep={index === currentWorkflow.steps.length - 1}
|
||||||
currentStep={index === currentStep}
|
currentStep={index === currentStepIndex}
|
||||||
selected={index === selectedStepIndex}
|
selected={index === selectedStepIndex}
|
||||||
onClick={() => handleStepClick(index, step)}
|
onClick={() => handleStepClick(index, step)}
|
||||||
/>
|
/>
|
||||||
@@ -393,7 +469,7 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedStepIndex === currentStep && !selectedStep.completed && !selectedStep.rejected &&
|
{selectedStepIndex === currentStepIndex && !selectedStep.completed && !selectedStep.rejected &&
|
||||||
<div className="flex flex-row gap-2 ">
|
<div className="flex flex-row gap-2 ">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
Reference in New Issue
Block a user