diff --git a/src/components/ApprovalWorkflows/WorkflowForm.tsx b/src/components/ApprovalWorkflows/WorkflowForm.tsx new file mode 100644 index 00000000..fc59db88 --- /dev/null +++ b/src/components/ApprovalWorkflows/WorkflowForm.tsx @@ -0,0 +1,143 @@ +import Option from "@/interfaces/option"; +import { AnimatePresence, Reorder } from "framer-motion"; +import { useEffect, useState } from "react"; +import { FaRegCheckCircle } from "react-icons/fa"; +import { IoIosAddCircleOutline } from "react-icons/io"; +import Button from "../Low/Button"; +import WorkflowStepComponent from "./WorkflowStep"; +import { ApprovalWorkflow, WorkflowStep } from "@/interfaces/approval.workflow"; + +const teacherOptions: Option[] = [ + // fetch from database? +] + +const directorOptions: Option[] = [ + // fetch from database? +] + +// Variants for animating steps when they are added/removed +const itemVariants = { + initial: { opacity: 0, y: -20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, x: 20 }, +}; + +interface Props { + workflow: ApprovalWorkflow; + onWorkflowChange: (workflow: ApprovalWorkflow) => void; +} + +export default function WorkflowForm({ workflow, onWorkflowChange }: Props) { + const [steps, setSteps] = useState(workflow.steps); + const [stepCounter, setStepCounter] = useState(3); // to guarantee unique keys used for animations + const lastStep = steps[steps.length - 1]; + + useEffect(() => { + setSteps(workflow.steps); + }, [workflow]); + + useEffect(() => { + const updatedWorkflow = { ...workflow, steps }; + onWorkflowChange(updatedWorkflow); + }, [steps]); + + const addStep = () => { + setSteps((prev) => { + const newStep: WorkflowStep = { + key: stepCounter, + stepType: "approval-by", + stepNumber: steps.length - 1, + completed: false, + }; + setStepCounter((count) => count + 1); + + return [...prev.slice(0, -1), newStep, lastStep]; + }); + }; + + const handleDelete = (key: number | undefined) => { + if (key){ + setSteps((prev) => prev.filter((step) => step.key !== key)); + } + }; + + const handleReorder = (newOrder: WorkflowStep[]) => { + const firstIndex = newOrder.findIndex((s) => s.firstStep); + if (firstIndex !== -1 && firstIndex !== 0) { + const [first] = newOrder.splice(firstIndex, 1); + newOrder.unshift(first); + } + + const finalIndex = newOrder.findIndex((s) => s.finalStep); + if (finalIndex !== -1 && finalIndex !== newOrder.length - 1) { + const [final] = newOrder.splice(finalIndex, 1); + newOrder.push(final); + } + + setSteps(newOrder); + }; + + return ( +
+ + + + + + {steps.map((step, index) => ( + + handleDelete(step.key)} + /> + {step.finalStep && + + } + + ))} + + + +
+ + + ); +}; \ No newline at end of file diff --git a/src/components/ApprovalWorkflows/WorkflowStep.tsx b/src/components/ApprovalWorkflows/WorkflowStep.tsx index 7d6adf9f..06ec7582 100644 --- a/src/components/ApprovalWorkflows/WorkflowStep.tsx +++ b/src/components/ApprovalWorkflows/WorkflowStep.tsx @@ -1,11 +1,11 @@ import Option from "@/interfaces/option"; -import clsx from "clsx"; import { useState } from "react"; import { BsTrash } from "react-icons/bs"; import { FaWpforms } from "react-icons/fa6"; import { LuGripHorizontal } from "react-icons/lu"; import WorkflowStepNumber from "./WorkflowStepNumber"; import WorkflowStepSelects from "./WorkflowStepSelects"; +import { WorkflowStep } from "@/interfaces/approval.workflow"; export type StepType = "form-intake" | "approval-by"; @@ -17,26 +17,16 @@ const directorOptions: Option[] = [ // fetch from database? ] -interface Props { - editView?: boolean, - finalStep?: boolean, - isSelected?: boolean, - stepNumber: number, - stepType: StepType, - requestedBy: string, - //requestedBy: TeacherUser | CorporateUser | MasterCorporateUser, - onDelete?: () => void; -} - -export default function WorkflowStep({ +export default function WorkflowStepComponent({ + stepType, + stepNumber, + completed = false, editView = false, finalStep = false, isSelected = false, - stepNumber, - stepType, requestedBy, onDelete, -}: Props) { +}: WorkflowStep) { // disable selectability of step if in editView const effectiveIsSelected = editView ? false : isSelected; @@ -57,7 +47,6 @@ export default function WorkflowStep({ } return ( -
@@ -94,8 +83,7 @@ export default function WorkflowStep({
{/* h-[40px] probably is not the best way to match the height with the select component, but for now should be ok */}
- - Form Intake + Form Intake
Prof. X @@ -110,6 +98,7 @@ export default function WorkflowStep({ data-tip="Delete" className="cursor-pointer tooltip" onClick={onDelete} + type="button" > diff --git a/src/demo/approval_workflows.json b/src/demo/approval_workflows.json index d0244fb8..b56a6920 100644 --- a/src/demo/approval_workflows.json +++ b/src/demo/approval_workflows.json @@ -1,42 +1,66 @@ [ { - "id": "local-test-id-1", + "id": "kajhfakscbka-asacaca-acawesae", "name": "name-1", - "module": "reading", - "status": "approved", - "approvers": "prof-1", - "step": "Concluded" - }, - { - "id": "local-test-id-2", - "name": "name-2", - "module": "reading", + "modules": [ + "reading", + "writing" + ], "status": "pending", - "approvers": "prof-2", - "step": "Concluded" - }, - { - "id": "local-test-id-3", - "name": "name-3", - "module": "listening", - "status": "rejected", - "approvers": "prof-3", - "step": "Concluded" - }, - { - "id": "local-test-id-4", - "name": "name-4", - "module": "writing", - "status": "approved", - "approvers": "prof-4", - "step": "Concluded" - }, - { - "id": "local-test-id-5", - "name": "name-5", - "module": "reading", - "status": "approved", - "approvers": "prof-5", - "step": "Concluded" + "approvers": "prof-1", + "steps": [ + { + "stepType": "form-intake", + "stepNumber": 1, + "completed": true, + "completedBy": "Prof. X", + "assignees": [ + "Prof. X", + "Prof. Y", + "Prof. Z" + ] + }, + { + "stepType": "approval-by", + "stepNumber": 2, + "completed": true, + "completedBy": "Prof. Y", + "assignees": [ + "Prof. X", + "Prof. Y", + "Prof. Z" + ] + }, + { + "stepType": "approval-by", + "stepNumber": 3, + "completed": false, + "assignees": [ + "Prof. X", + "Prof. Y", + "Prof. Z" + ] + }, + { + "stepType": "approval-by", + "stepNumber": 4, + "completed": false, + "assignees": [ + "Prof. X", + "Prof. Y", + "Prof. Z" + ] + }, + { + "stepType": "approval-by", + "stepNumber": 5, + "completed": false, + "assignees": [ + "Prof. X", + "Prof. Y", + "Prof. Z" + ] + } + ] } ] \ No newline at end of file diff --git a/src/interfaces/approval.workflow.ts b/src/interfaces/approval.workflow.ts index c91b06f5..51fd4239 100644 --- a/src/interfaces/approval.workflow.ts +++ b/src/interfaces/approval.workflow.ts @@ -1,12 +1,29 @@ import {Module} from "."; export interface ApprovalWorkflow { - id: string; - name: string; - module: Module; - status: ApprovalWorkflowStatus; - approvers: string; - step: string; + id: string, + name: string, + modules: Module[], + status: ApprovalWorkflowStatus, + steps: WorkflowStep[], +} + +export type StepType = "form-intake" | "approval-by"; + +export interface WorkflowStep { + key?: number, + stepType: StepType, + stepNumber: number, + completed: boolean, + completedBy?: string, + assignees?: string[], + editView?: boolean, + firstStep?: boolean, + finalStep?: boolean, + isSelected?: boolean, + requestedBy?: string, + //requestedBy: TeacherUser | CorporateUser | MasterCorporateUser, + onDelete?: () => void; } export type ApprovalWorkflowStatus = "approved" | "pending" | "rejected"; diff --git a/src/pages/approval-workflows/[id].tsx b/src/pages/approval-workflows/[id].tsx index d35af5ce..30e2abe1 100644 --- a/src/pages/approval-workflows/[id].tsx +++ b/src/pages/approval-workflows/[id].tsx @@ -5,13 +5,10 @@ import { sessionOptions } from "@/lib/session"; import { redirect } from "@/utils"; import { requestUser } from "@/utils/api"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; -import { AnimatePresence, Reorder } from "framer-motion"; import { withIronSessionSsr } from "iron-session/next"; import Head from "next/head"; import Link from "next/link"; import { BsChevronLeft } from "react-icons/bs"; -import { FaRegCheckCircle } from "react-icons/fa"; -import { IoIosAddCircleOutline } from "react-icons/io"; import { ToastContainer } from "react-toastify"; @@ -20,26 +17,9 @@ import approvalWorkflowsData from '../../demo/approval_workflows.json'; // to te import RequestedBy from "@/components/ApprovalWorkflows/RequestedBy"; import StartedOn from "@/components/ApprovalWorkflows/StartedOn"; import Status from "@/components/ApprovalWorkflows/Status"; -import WorkflowStep, { StepType } from "@/components/ApprovalWorkflows/WorkflowStep"; -import Button from "@/components/Low/Button"; -import { useState } from "react"; - -interface Step { - stepType: StepType; - firstStep?: boolean; - finalStep?: boolean; - key: number; -} - -// Variants for animating steps when they are added/removed -const itemVariants = { - initial: { opacity: 0, y: -20 }, - animate: { opacity: 1, y: 0 }, - exit: { opacity: 0, x: 20 }, -}; export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => { - const user = await requestUser(req, res) + const user = await requestUser(req, res); if (!user) return redirect("/login") if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) @@ -59,54 +39,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params } export default function Home({ approvalWorkflow }: { approvalWorkflow: ApprovalWorkflow }) { const { user } = useUser({ redirectTo: "/login" }); - const [selectedButton, setSelectedButton] = useState<"forms" | "progress">("forms"); - - const [steps, setSteps] = useState([ - { stepType: "form-intake", firstStep: true, key: 1 }, - { stepType: "approval-by", finalStep: true, key: 2 }, - ]); - const [stepCounter, setStepCounter] = useState(3); // to guarantee unique keys used for animations - const firstStep = steps[0]; - const lastStep = steps[steps.length - 1]; - const middleSteps = steps.slice(1, steps.length - 1); - - const addStep = () => { - setSteps((prev) => { - const newStep: Step = { - key: stepCounter, - stepType: "approval-by", - }; - setStepCounter((count) => count + 1); - - return [...prev.slice(0, -1), newStep, lastStep]; - }); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - // Handle form submission logic - console.log("Form submitted!", steps); - }; - - const handleDelete = (key: number) => { - setSteps((prev) => prev.filter((step) => step.key !== key)); - }; - - const handleReorder = (newOrder: Step[]) => { - const firstIndex = newOrder.findIndex((s) => s.firstStep); - if (firstIndex !== -1 && firstIndex !== 0) { - const [first] = newOrder.splice(firstIndex, 1); - newOrder.unshift(first); - } - - const finalIndex = newOrder.findIndex((s) => s.finalStep); - if (finalIndex !== -1 && finalIndex !== newOrder.length - 1) { - const [final] = newOrder.splice(finalIndex, 1); - newOrder.push(final); - } - - setSteps(newOrder); - }; + return ( <> @@ -145,85 +78,6 @@ export default function Home({ approvalWorkflow }: { approvalWorkflow: ApprovalW status="pending" />
-
- - -
- -
-
- - - - - - {steps.map((step, index) => ( - - handleDelete(step.key)} - /> - {step.finalStep && - - } - - ))} - - - - - -
-
)} diff --git a/src/pages/approval-workflows/create.tsx b/src/pages/approval-workflows/create.tsx new file mode 100644 index 00000000..e82cfdbb --- /dev/null +++ b/src/pages/approval-workflows/create.tsx @@ -0,0 +1,179 @@ +import Layout from "@/components/High/Layout"; +import useUser from "@/hooks/useUser"; +import { sessionOptions } from "@/lib/session"; +import { redirect } from "@/utils"; +import { requestUser } from "@/utils/api"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { withIronSessionSsr } from "iron-session/next"; +import Head from "next/head"; +import Link from "next/link"; +import { BsChevronLeft, BsTrash } from "react-icons/bs"; +import { ToastContainer } from "react-toastify"; +import { v4 as uuidv4 } from 'uuid'; + +import WorkflowForm from "@/components/ApprovalWorkflows/WorkflowForm"; +import Button from "@/components/Low/Button"; +import Input from "@/components/Low/Input"; +import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; +import { useState } from "react"; +import { MdFormatListBulletedAdd } from "react-icons/md"; + +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("/") + + return { + props: { user }, + }; +}, sessionOptions); + +export default function Home() { + const { user } = useUser({ redirectTo: "/login" }); + + const [workflows, setWorkflows] = useState([]); + const [selectedWorkflowId, setSelectedWorkflowId] = useState(null); + const currentWorkflow = workflows.find(wf => wf.id === selectedWorkflowId); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + // Handle form submission logic + console.log("Form submitted! Values:", workflows); + }; + + const handleAddNewWorkflow = () => { + const newId = uuidv4(); + const newWorkflow: ApprovalWorkflow = { + id: newId, + name: "", + modules: [], + status: "pending", + steps: [ + { key: Date.now(), completed: false , editView: true, stepType: "form-intake", stepNumber: 1, firstStep: true }, + { key: Date.now() + 1, completed: false, editView: true, stepType: "approval-by", stepNumber: 2, finalStep: true }, + ], + }; + setWorkflows((prev) => [...prev, newWorkflow]); + setSelectedWorkflowId(newId); + }; + + const onWorkflowChange = (updatedWorkflow: ApprovalWorkflow) => { + setWorkflows(prev => + prev.map(wf => (wf.id === updatedWorkflow.id ? updatedWorkflow : wf)) + ); + } + + const handleSelectWorkflow = (id: string) => { + setSelectedWorkflowId(id); + }; + + const handleDeleteWorkflow = (id: string) => { + if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return; + + setWorkflows(prev => prev.filter(wf => wf.id !== id)); + if (selectedWorkflowId === id) { + setSelectedWorkflowId(null); + } + }; + + return ( + <> + + Configure Workflows | EnCoach + + + + + + {user && ( + +
+
+ + + +

{"Configure Approval Workflows"}

+
+
+ +
+ + + +
+ {workflows.map((workflow) => ( + + + ))} +
+
+ +
+ {currentWorkflow && ( + <> +
+ { + const updatedWorkflow = { + ...currentWorkflow, + name: updatedName, + }; + onWorkflowChange(updatedWorkflow); + }} + /> + +
+ + + + )} + +
+ )} + + ); +} diff --git a/src/pages/approval-workflows/index.tsx b/src/pages/approval-workflows/index.tsx index 580e57c1..7a3166e3 100644 --- a/src/pages/approval-workflows/index.tsx +++ b/src/pages/approval-workflows/index.tsx @@ -1,8 +1,8 @@ import Layout from "@/components/High/Layout"; +import Button from "@/components/Low/Button"; import Select from "@/components/Low/Select"; import useApprovalWorkflows from "@/hooks/useApprovalWorkflows"; import useUser from "@/hooks/useUser"; -import useUsers from "@/hooks/useUsers"; import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel } from "@/interfaces/approval.workflow"; import { sessionOptions } from "@/lib/session"; import { redirect } from "@/utils"; @@ -17,6 +17,7 @@ import Link from "next/link"; import { useEffect, useState } from "react"; import { BsTrash } from "react-icons/bs"; import { FaRegEdit } from "react-icons/fa"; +import { IoIosAddCircleOutline } from "react-icons/io"; import { toast, ToastContainer } from "react-toastify"; const columnHelper = createColumnHelper(); @@ -119,10 +120,10 @@ export default function ApprovalWorkflows() { header: "Name", cell: (info) => info.getValue(), }), - columnHelper.accessor("module", { + /* columnHelper.accessor("module", { header: "Module", cell: (info) => info.getValue(), - }), + }), */ columnHelper.accessor("status", { header: "Status", cell: (info) => ( @@ -131,26 +132,26 @@ export default function ApprovalWorkflows() { ), }), - columnHelper.accessor("approvers", { + /* columnHelper.accessor("approvers", { header: "Approvers", cell: (info) => info.getValue(), }), columnHelper.accessor("step", { header: "Step", cell: (info) => info.getValue(), - }), + }), */ { header: "Actions", id: "actions", cell: ({ row }: { row: { original: ApprovalWorkflow } }) => { return (
- +
); }, @@ -179,6 +180,17 @@ export default function ApprovalWorkflows() {

Approval Workflows

+ + + +