diff --git a/src/components/ApprovalWorkflows/WorkflowForm.tsx b/src/components/ApprovalWorkflows/WorkflowForm.tsx index 39b0e2c5..4d27e150 100644 --- a/src/components/ApprovalWorkflows/WorkflowForm.tsx +++ b/src/components/ApprovalWorkflows/WorkflowForm.tsx @@ -13,12 +13,12 @@ interface Props { workflow: EditableApprovalWorkflow; onWorkflowChange: (workflow: EditableApprovalWorkflow) => void; entityApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]; + entityAvailableFormIntakers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]; isLoading: boolean; isRedirecting: boolean; } -export default function WorkflowForm({ workflow, onWorkflowChange, entityApprovers, isLoading, isRedirecting }: Props) { - const [stepCounter, setStepCounter] = useState(3); // to guarantee unique keys used for animations +export default function WorkflowForm({ workflow, onWorkflowChange, entityApprovers, entityAvailableFormIntakers, isLoading, isRedirecting }: Props) { const lastStep = workflow.steps[workflow.steps.length - 1]; const renumberSteps = (steps: EditableWorkflowStep[]): EditableWorkflowStep[] => { @@ -30,7 +30,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove const addStep = () => { const newStep: EditableWorkflowStep = { - key: stepCounter, + key: Date.now(), stepType: "approval-by", stepNumber: workflow.steps.length, completed: false, @@ -38,7 +38,6 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove firstStep: false, finalStep: false, }; - setStepCounter((count) => count + 1); const updatedSteps = [ ...workflow.steps.slice(0, -1), @@ -137,7 +136,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove finalStep={step.finalStep} onDelete={() => handleDelete(step.key)} onSelectChange={(numberOfSelects, idx, option) => handleSelectChange(step.key, numberOfSelects, idx, option)} - entityApprovers={entityApprovers} + entityApprovers={step.stepNumber === 1 ? entityAvailableFormIntakers : entityApprovers} /> {step.finalStep && diff --git a/src/pages/api/approval-workflows/create.ts b/src/pages/api/approval-workflows/create.ts index 0e292a00..ddd46da5 100644 --- a/src/pages/api/approval-workflows/create.ts +++ b/src/pages/api/approval-workflows/create.ts @@ -31,7 +31,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const configuredWorkflows: ApprovalWorkflow[] = filteredWorkflows; const entitiesIds: string[] = userEntitiesWithLabel.map((e) => e.id); - await replaceApprovalWorkflowsByEntities("configured-workflows", configuredWorkflows, entitiesIds); + await replaceApprovalWorkflowsByEntities(configuredWorkflows, entitiesIds); return res.status(201).json({ ok: true }); } diff --git a/src/pages/approval-workflows/[id]/clone.tsx b/src/pages/approval-workflows/[id]/clone.tsx index 6862d53c..d8a363ef 100644 --- a/src/pages/approval-workflows/[id]/clone.tsx +++ b/src/pages/approval-workflows/[id]/clone.tsx @@ -24,7 +24,7 @@ import { GrClearOption } from "react-icons/gr"; import { toast, ToastContainer } from "react-toastify"; import { useRouter } from "next/router"; import axios from "axios"; -import { getApprovalWorkflow } from "@/utils/approval.workflows.be"; +import { getApprovalWorkflow, getFormIntakersByEntity } from "@/utils/approval.workflows.be"; export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => { const user = await requestUser(req, res); @@ -47,6 +47,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params } user, workflow, userEntitiesWithLabel, + entityUnavailableFormIntakers: await getFormIntakersByEntity(workflow.entityId), userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[], }), }; @@ -56,10 +57,11 @@ interface Props { user: User, workflow: ApprovalWorkflow, userEntitiesWithLabel: Entity[], + entityUnavailableFormIntakers: string[], userEntitiesApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[], } -export default function Home({ user, workflow, userEntitiesWithLabel, userEntitiesApprovers }: Props) { +export default function Home({ user, workflow, userEntitiesWithLabel, entityUnavailableFormIntakers, userEntitiesApprovers }: Props) { const [cloneWorkflow, setCloneWorkflow] = useState(null); const [entityId, setEntityId] = useState(workflow.entityId); const [entityApprovers, setEntityApprovers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]); @@ -276,6 +278,7 @@ export default function Home({ user, workflow, userEntitiesWithLabel, userEntiti workflow={cloneWorkflow} onWorkflowChange={onWorkflowChange} entityApprovers={entityApprovers} + entityAvailableFormIntakers={entityApprovers.filter(approver => !entityUnavailableFormIntakers.includes(approver.id))} isLoading={isLoading} isRedirecting={isRedirecting} /> diff --git a/src/pages/approval-workflows/create.tsx b/src/pages/approval-workflows/create.tsx index 03c058e3..0527b651 100644 --- a/src/pages/approval-workflows/create.tsx +++ b/src/pages/approval-workflows/create.tsx @@ -34,7 +34,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { return redirect("/") const userEntitiesWithLabel = await getEntities(user.entities.map(entity => entity.id)); - + const allConfiguredWorkflows = await getApprovalWorkflowsByEntities("configured-workflows", userEntitiesWithLabel.map(entity => entity.id)); return { @@ -59,6 +59,7 @@ export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLab 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); @@ -72,11 +73,40 @@ export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLab approver.entities.some(entity => entity.id === entityId) ) ); - } else { - setEntityApprovers([]); } }, [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 => ({ @@ -102,7 +132,7 @@ export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLab })) })); - const requestData = {filteredWorkflows, userEntitiesWithLabel}; + const requestData = { filteredWorkflows, userEntitiesWithLabel }; axios .post(`/api/approval-workflows/create`, requestData) @@ -316,6 +346,7 @@ export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLab workflow={currentWorkflow} onWorkflowChange={onWorkflowChange} entityApprovers={entityApprovers} + entityAvailableFormIntakers={entityAvailableFormIntakers} isLoading={isLoading} isRedirecting={isRedirecting} /> diff --git a/src/utils/approval.workflows.be.ts b/src/utils/approval.workflows.be.ts index 0d226a69..baf02bfd 100644 --- a/src/utils/approval.workflows.be.ts +++ b/src/utils/approval.workflows.be.ts @@ -22,6 +22,39 @@ export const getApprovalWorkflowsByEntities = async (collection: string, ids: st .toArray(); }; +export const getApprovalWorkflowByFormIntaker = async (entityId: string, formIntakerId: string) => { + return await db.collection("configured-workflows").findOne({ + entityId, + steps: { + $elemMatch: { + stepNumber: 1, + assignees: formIntakerId, + }, + }, + }); +}; + +export const getFormIntakersByEntity = async (entityId: string) => { + const results = await db + .collection("configured-workflows") + .aggregate([ + // 1. Match workflows with the provided entityId + { $match: { entityId } }, + // 2. Unwind the steps array to process each step individually + { $unwind: "$steps" }, + // 3. Filter for the first step (you could also check for a "firstStep" flag if you prefer) + { $match: { "steps.stepNumber": 1 } }, + // 4. Unwind the assignees array so that each assignee is handled separately + { $unwind: "$steps.assignees" }, + // 5. Group by null (i.e. all documents) and add each assignee to a set to remove duplicates + { $group: { _id: null, assignees: { $addToSet: "$steps.assignees" } } }, + ]) + .toArray(); + + // Return the assignees if the aggregation found any; otherwise return an empty array + return results.length > 0 ? results[0].assignees : []; +}; + export const createApprovalWorkflow = async (collection: string, workflow: ApprovalWorkflow) => { const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow; return await db.collection(collection).insertOne(workflowWithoutId); @@ -35,7 +68,7 @@ export const createApprovalWorkflows = async (collection: string, workflows: App export const updateApprovalWorkflow = async (collection: string, workflow: ApprovalWorkflow) => { const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow; - return await db.collection(collection).replaceOne({ _id: _id }, workflowWithoutId); + return await db.collection(collection).replaceOne({ _id: new ObjectId(_id) }, workflowWithoutId); }; export const updateApprovalWorkflows = async (collection: string, workflows: ApprovalWorkflow[]) => { @@ -56,25 +89,25 @@ export const deleteApprovalWorkflow = async (collection: string, id: string) => return await db.collection(collection).deleteOne({ _id: new ObjectId(id) }); }; -export const replaceApprovalWorkflowsByEntities = async (collection: string, workflows: ApprovalWorkflow[], entityIds: string[]) => { - // 1. Keep track of the _id values of all workflows we want to end up with - const finalIds = new Set(); - - // 2. Process incoming workflows - for (const workflow of workflows) { - if (workflow._id) { - // Replace the existing ones - await updateApprovalWorkflow(collection, workflow); - finalIds.add(workflow._id.toString()); - } else { - // Insert if no _id - const insertResult = await createApprovalWorkflow(collection, workflow); - finalIds.add(insertResult.insertedId.toString()); - } - } - +export const replaceApprovalWorkflowsByEntities = async (workflows: ApprovalWorkflow[], entityIds: string[]) => { + // 1. Keep track of the _id values of all workflows we want to end up with + const finalIds = new Set(); + + // 2. Process incoming workflows + for (const workflow of workflows) { + if (workflow._id) { + // Replace the existing ones + await updateApprovalWorkflow("configured-workflows", workflow); + finalIds.add(workflow._id.toString()); + } else { + // Insert if no _id + const insertResult = await createApprovalWorkflow("configured-workflows", workflow); + finalIds.add(insertResult.insertedId.toString()); + } + } + // 3. Delete any existing workflow (within these entityIds) that wasn't in the final list - await db.collection(collection).deleteMany({ + await db.collection("configured-workflows").deleteMany({ _id: { $nin: Array.from(finalIds).map((id) => new ObjectId(id)), },