import RequestedBy from "@/components/ApprovalWorkflows/RequestedBy"; import StartedOn from "@/components/ApprovalWorkflows/StartedOn"; import Status from "@/components/ApprovalWorkflows/Status"; import Tip from "@/components/ApprovalWorkflows/Tip"; import UserWithProfilePic from "@/components/ApprovalWorkflows/UserWithProfilePic"; import WorkflowStepComponent from "@/components/ApprovalWorkflows/WorkflowStepComponent"; import Layout from "@/components/High/Layout"; import Button from "@/components/Low/Button"; import useApprovalWorkflow from "@/hooks/useApprovalWorkflow"; import { ApprovalWorkflow, getUserTypeLabelShort, WorkflowStep } from "@/interfaces/approval.workflow"; import { User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import useExamStore from "@/stores/exam"; import { redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { getApprovalWorkflow } from "@/utils/approval.workflows.be"; import { getEntityWithRoles } from "@/utils/entities.be"; import { getExamById } from "@/utils/exams"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { doesEntityAllow } from "@/utils/permissions"; import { getSpecificUsers, getUser } from "@/utils/users.be"; import axios from "axios"; import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; import { withIronSessionSsr } from "iron-session/next"; import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; import { useState } from "react"; import { BsChevronLeft } from "react-icons/bs"; import { FaSpinner, FaWpforms } from "react-icons/fa6"; import { FiSave } from "react-icons/fi"; import { IoMdCheckmarkCircleOutline } from "react-icons/io"; import { IoDocumentTextOutline } from "react-icons/io5"; import { MdKeyboardArrowDown, MdKeyboardArrowUp, MdOutlineDoubleArrow } from "react-icons/md"; import { RiThumbUpLine } from "react-icons/ri"; import { RxCrossCircled } from "react-icons/rx"; import { TiEdit } from "react-icons/ti"; import { toast, ToastContainer } from "react-toastify"; export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => { const user = await requestUser(req, res); if (!user) return redirect("/login") if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) return redirect("/") const { id } = params as { id: string }; const workflow: ApprovalWorkflow | null = await getApprovalWorkflow("active-workflows", id); if (!workflow) return redirect("/approval-workflows") const entityWithRole = await getEntityWithRoles(workflow.entityId); if (!entityWithRole) return redirect("/approval-workflows"); if (!doesEntityAllow(user, entityWithRole, "view_workflows")) return redirect("/approval-workflows"); const allAssigneeIds: string[] = [ ...new Set( workflow.steps .map((step) => { const assignees = step.assignees; if (step.completedBy) { assignees.push(step.completedBy); } return assignees; }) .flat() ) ]; return { props: serialize({ user, initialWorkflow: workflow, id, workflowAssignees: await getSpecificUsers(allAssigneeIds), workflowRequester: await getUser(workflow.requester), }), }; }, sessionOptions); interface Props { user: User, initialWorkflow: ApprovalWorkflow, id: string, workflowAssignees: User[], workflowRequester: User, } export default function Home({ user, initialWorkflow, id, workflowAssignees, workflowRequester }: Props) { const { workflow, reload, isLoading } = useApprovalWorkflow(id); const currentWorkflow = workflow || initialWorkflow; let currentStepIndex = currentWorkflow.steps.findIndex(step => !step.completed || step.rejected); if (currentStepIndex === -1) currentStepIndex = currentWorkflow.steps.length - 1; const [selectedStepIndex, setSelectedStepIndex] = useState(currentStepIndex); const [selectedStep, setSelectedStep] = useState(currentWorkflow.steps[selectedStepIndex]); const [isPanelOpen, setIsPanelOpen] = useState(true); const [isAccordionOpen, setIsAccordionOpen] = useState(false); const [comments, setComments] = useState(selectedStep.comments || ""); const [viewExamIsLoading, setViewExamIsLoading] = useState(false); const [editExamIsLoading, setEditExamIsLoading] = useState(false); const router = useRouter(); const handleStepClick = (index: number, stepInfo: WorkflowStep) => { setSelectedStep(stepInfo); setSelectedStepIndex(index); setComments(stepInfo.comments || ""); setIsPanelOpen(true); }; const handleSaveComments = () => { const updatedWorkflow: ApprovalWorkflow = { ...currentWorkflow, steps: currentWorkflow.steps.map((step, index) => index === selectedStepIndex ? { ...step, comments: comments, } : step ) }; axios .put(`/api/approval-workflows/${id}`, updatedWorkflow) .then(() => { toast.success("Comments saved successfully."); reload(); }) .catch((reason) => { if (reason.response.status === 401) { toast.error("Not logged in!"); } else if (reason.response.status === 403) { toast.error("You do not have permission to approve this step!"); } else { toast.error("Something went wrong, please try again later."); } console.log("Submitted Values:", updatedWorkflow); return; }) }; const handleApproveStep = () => { const isLastStep = (selectedStepIndex + 1 === currentWorkflow.steps.length); if (isLastStep) { if (!confirm(`Are you sure you want to approve the last step and complete the approval process?`)) return; } const updatedWorkflow: ApprovalWorkflow = { ...currentWorkflow, status: selectedStepIndex === currentWorkflow.steps.length - 1 ? "approved" : "pending", steps: currentWorkflow.steps.map((step, index) => index === selectedStepIndex ? { ...step, completed: true, completedBy: user.id, completedDate: Date.now(), } : step ) }; axios .put(`/api/approval-workflows/${id}`, updatedWorkflow) .then(() => { toast.success("Step approved successfully."); reload(); }) .catch((reason) => { if (reason.response.status === 401) { toast.error("Not logged in!"); } else if (reason.response.status === 403) { toast.error("You do not have permission to approve this step!"); } else { toast.error("Something went wrong, please try again later."); } console.log("Submitted Values:", updatedWorkflow); return; }) if (isLastStep) { setIsPanelOpen(false); const examModule = currentWorkflow.modules[0]; const examId = currentWorkflow.examId; axios .patch(`/api/exam/${examModule}/${examId}`, { approved: true }) .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 = () => { if (!confirm(`Are you sure you want to reject this step? Doing so will terminate this approval workflow.`)) return; const updatedWorkflow: ApprovalWorkflow = { ...currentWorkflow, status: "rejected", steps: currentWorkflow.steps.map((step, index) => index === selectedStepIndex ? { ...step, completed: true, completedBy: user.id, completedDate: Date.now(), rejected: true, } : step ) }; axios .put(`/api/approval-workflows/${id}`, updatedWorkflow) .then(() => { toast.success("Step rejected successfully."); reload(); }) .catch((reason) => { if (reason.response.status === 401) { toast.error("Not logged in!"); } else if (reason.response.status === 403) { toast.error("You do not have permission to approve this step!"); } else { toast.error("Something went wrong, please try again later."); } console.log("Submitted Values:", updatedWorkflow); return; }) }; 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("Something went wrong while fetching exam!"); setViewExamIsLoading(false); return; } dispatch({ type: "INIT_EXAM", payload: { exams: [exam], modules: [examModule] }, }); router.push("/exam"); } } const handleEditExam = () => { setEditExamIsLoading(true); const examModule = currentWorkflow.modules[0]; const examId = currentWorkflow.examId; router.push(`/generation?id=${examId}&module=${examModule}`); } return ( <> Workflow | EnCoach

{currentWorkflow.name}

{currentWorkflow.steps.find((step) => !step.completed) === undefined && }
{currentWorkflow.steps.map((step, index) => ( handleStepClick(index, step)} /> ))}
{/* Side panel */}
{isPanelOpen && selectedStep && (

Step {selectedStepIndex + 1}


{selectedStep.stepType === "approval-by" ? ( <> Approval Step ) : ( <> Form Intake Step ) }
{selectedStep.completed ? (
{selectedStep.rejected ? "Rejected" : "Approved"} on {new Date(selectedStep.completedDate!).toLocaleString("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }).replace(", ", " at ")}

{selectedStep.rejected ? "Rejected" : "Approved"} by:

{(() => { const assignee = workflowAssignees.find( (assignee) => assignee.id === selectedStep.completedBy ); return assignee ? ( ) : ( "Unknown" ); })()}

No additional actions are required.

) : (
One assignee is required to sign off to complete this step:
{workflowAssignees.filter(user => selectedStep.assignees.includes(user.id)).map(user => ( ))}
)} {selectedStepIndex === currentStepIndex && !selectedStep.completed && !selectedStep.rejected &&
}
{/* Accordion for Exam Changes */}
setIsAccordionOpen((prev) => !prev)} >

Changes ({currentWorkflow.steps[selectedStepIndex].examChanges?.length || "0"})

{isAccordionOpen ? ( ) : ( )}
{isAccordionOpen && (
{currentWorkflow.steps[selectedStepIndex].examChanges?.length ? ( currentWorkflow.steps[selectedStepIndex].examChanges!.map((change, index) => ( <>

{change.charAt(0)} {change.slice(1)}


)) ) : (

No changes made so far.

)}
)}