- 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:
Joao Correia
2025-02-04 23:22:56 +00:00
parent de15eb5ee1
commit b215885dc6
6 changed files with 96 additions and 20 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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",

View File

@@ -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,

View File

@@ -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"