import Tip from "@/components/ApprovalWorkflows/Tip"; import WorkflowForm from "@/components/ApprovalWorkflows/WorkflowForm"; import Button from "@/components/Low/Button"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; import { ApprovalWorkflow, EditableApprovalWorkflow } from "@/interfaces/approval.workflow"; import { Entity } from "@/interfaces/entity"; import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import { mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { getApprovalWorkflowsByEntities } from "@/utils/approval.workflows.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { findAllowedEntities } from "@/utils/permissions"; import { isAdmin } from "@/utils/users"; import { getEntitiesUsers } 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 { useEffect, useState } from "react"; import { BsChevronLeft, BsTrash } from "react-icons/bs"; import { FaRegClone } from "react-icons/fa6"; import { MdFormatListBulletedAdd } from "react-icons/md"; import { toast, ToastContainer } from "react-toastify"; import { v4 as uuidv4 } from 'uuid'; export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { 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 entityIDS = mapBy(user.entities, "id"); const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); const userEntitiesWithLabel = findAllowedEntities(user, entities, "configure_workflows"); const allConfiguredWorkflows = await getApprovalWorkflowsByEntities("configured-workflows", userEntitiesWithLabel.map(entity => entity.id)); const approverTypes = ["teacher", "corporate", "mastercorporate"]; if (user.type === "developer") { approverTypes.push("developer"); } const userEntitiesApprovers = await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: { $in: approverTypes } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]; return { props: serialize({ user, allConfiguredWorkflows, userEntitiesWithLabel, userEntitiesApprovers, }), }; }, sessionOptions); interface Props { user: User, allConfiguredWorkflows: EditableApprovalWorkflow[], userEntitiesWithLabel: Entity[], userEntitiesApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[], } export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLabel, userEntitiesApprovers }: Props) { const [workflows, setWorkflows] = useState(allConfiguredWorkflows); const [selectedWorkflowId, setSelectedWorkflowId] = useState(undefined); const [entityId, setEntityId] = useState(null); const [entityApprovers, setEntityApprovers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]); const [entityAvailableFormIntakers, setEntityAvailableFormIntakers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]); const [isAdding, setIsAdding] = useState(false); // used to temporary timeout new workflow button. With animations, clicking too fast might cause state inconsistencies between renders. const [isLoading, setIsLoading] = useState(false); const [isRedirecting, setIsRedirecting] = useState(false); const router = useRouter(); useEffect(() => { if (entityId) { setEntityApprovers( userEntitiesApprovers.filter(approver => approver.entities.some(entity => entity.id === entityId) ) ); } }, [entityId, userEntitiesApprovers]); useEffect(() => { if (entityId) { // Get all workflows for the selected entity const workflowsForEntity = workflows.filter(wf => wf.entityId === entityId); // For all workflows except the current one, collect the first step assignees const assignedFormIntakers = workflowsForEntity.reduce((acc, wf) => { if (wf.id === selectedWorkflowId) return acc; // skip current workflow so its selection isn’t removed const formIntakeStep = wf.steps.find(step => step.stepType === "form-intake"); if (formIntakeStep) { // Only consider non-null assignees const validAssignees = formIntakeStep.assignees.filter( (assignee): assignee is string => !!assignee ); return acc.concat(validAssignees); } return acc; }, []); // Now filter out any user from entityApprovers whose id is in the assignedFormIntakers list. // (The selected one in the current workflow is allowed even if it is in the list.) const availableFormIntakers = entityApprovers.filter(assignee => !assignedFormIntakers.includes(assignee.id) ); setEntityAvailableFormIntakers(availableFormIntakers); } }, [entityId, entityApprovers, workflows, selectedWorkflowId]); const currentWorkflow = workflows.find(wf => wf.id === selectedWorkflowId); const ENTITY_OPTIONS = userEntitiesWithLabel.map(entity => ({ label: entity.label, value: entity.id, filter: (x: EditableApprovalWorkflow) => x.entityId === entity.id, })); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); if (workflows.length === 0) { setIsLoading(false); return; } for (const workflow of workflows) { for (const step of workflow.steps) { if (step.assignees.every(x => !x)) { toast.warning("There are empty steps in at least one of the configured workflows."); setIsLoading(false); return; } } } const filteredWorkflows: ApprovalWorkflow[] = workflows.map(workflow => ({ ...workflow, steps: workflow.steps.map(step => ({ ...step, assignees: step.assignees.filter((assignee): assignee is string => assignee !== null && assignee !== undefined) })) })); const requestData = { filteredWorkflows, userEntitiesWithLabel }; axios .post(`/api/approval-workflows/create`, requestData) .then(() => { toast.success("Approval Workflows created successfully."); setIsRedirecting(true); router.push("/approval-workflows"); }) .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 create Approval Workflows!"); } else { toast.error("Something went wrong, please try again later."); } setIsLoading(false); console.log("Submitted Values:", filteredWorkflows); return; }) }; const handleAddNewWorkflow = () => { if (isAdding) return; setIsAdding(true); const newId = uuidv4(); // this id is only used in UI. it is ommited on submission to DB and lets mongo handle unique id. const newWorkflow: EditableApprovalWorkflow = { id: newId, name: "", entityId: "", modules: [], requester: user.id, startDate: Date.now(), status: "pending", steps: [ { key: 9998, stepType: "form-intake", stepNumber: 1, completed: false, firstStep: true, finalStep: false, assignees: [null] }, { key: 9999, stepType: "approval-by", stepNumber: 2, completed: false, firstStep: false, finalStep: true, assignees: [null] }, ], }; setWorkflows((prev) => [...prev, newWorkflow]); handleSelectWorkflow(newId); setTimeout(() => setIsAdding(false), 300); }; const onWorkflowChange = (updatedWorkflow: EditableApprovalWorkflow) => { setWorkflows(prev => prev.map(wf => (wf.id === updatedWorkflow.id ? updatedWorkflow : wf)) ); } const handleSelectWorkflow = (id: string | undefined) => { setSelectedWorkflowId(id); const selectedWorkflow = workflows.find(wf => wf.id === id); if (selectedWorkflow) { setEntityId(selectedWorkflow.entityId || null); } else { setEntityId(null); } }; const handleCloneWorkflow = (id: string) => { const workflowToClone = workflows.find(wf => wf.id === id); if (!workflowToClone) return; const newId = uuidv4(); const clonedWorkflow: EditableApprovalWorkflow = { ...workflowToClone, id: newId, steps: workflowToClone.steps.map(step => ({ ...step, assignees: step.firstStep ? [null] : [...step.assignees], // we can't have more than one form intaker per teacher per entity })), }; setWorkflows(prev => { const updatedWorkflows = [...prev, clonedWorkflow]; setSelectedWorkflowId(newId); setEntityId(clonedWorkflow.entityId || null); return updatedWorkflows; }); }; const handleDeleteWorkflow = (id: string) => { if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return; const updatedWorkflows = workflows.filter(wf => wf.id !== id); setWorkflows(updatedWorkflows); if (selectedWorkflowId === id) { handleSelectWorkflow(updatedWorkflows.find(wf => wf.id)?.id); } }; const handleEntityChange = (wf: EditableApprovalWorkflow, entityId: string) => { const updatedWorkflow = { ...wf, entityId: entityId, steps: wf.steps.map(step => ({ ...step, assignees: step.assignees.map(() => null) })) } onWorkflowChange(updatedWorkflow); } return ( <> Configure Workflows | EnCoach

{"Configure Approval Workflows"}

{workflows.length !== 0 &&
}
{workflows.map((workflow) => ( ))}
{currentWorkflow && ( <>
{ const updatedWorkflow = { ...currentWorkflow, name: updatedName, }; onWorkflowChange(updatedWorkflow); }} />